@evident-ai/cli 3.5.2-dev.43244a2 → 3.5.2-dev.507f0a3
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 +357 -88
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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.
|
|
1160
|
+
var CLI_VERSION = (true ? "3.5.2-dev.507f0a3" : 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1320
|
-
|
|
1321
|
-
|
|
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:
|
|
1363
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
|
|
1327
1364
|
source: "opencode"
|
|
1328
1365
|
},
|
|
1329
1366
|
getContext()
|
|
1330
1367
|
);
|
|
1331
1368
|
};
|
|
1332
|
-
|
|
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
|
|
1350
|
-
|
|
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
|
-
|
|
1361
|
-
`
|
|
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([
|
|
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
|
-
|
|
1386
|
-
`
|
|
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
|
-
|
|
1397
|
-
`
|
|
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(
|
|
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
|
-
|
|
1421
|
-
`
|
|
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) =>
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
4261
|
+
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
|
|
4117
4262
|
const synchroniserEnv = await runSynchroniser(["env"], {
|
|
4118
4263
|
timeoutMs: SESSION_DB_SYNCHRONISER_TIMEOUT_MS,
|
|
4119
4264
|
env
|
|
@@ -4493,12 +4638,15 @@ function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
|
4493
4638
|
}
|
|
4494
4639
|
} catch {
|
|
4495
4640
|
try {
|
|
4496
|
-
const psOutput = execSync(
|
|
4641
|
+
const psOutput = execSync("ps aux", {
|
|
4497
4642
|
encoding: "utf-8",
|
|
4498
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4643
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4644
|
+
// The whole table reaches Node; the 1 MiB default would surface a spurious failure.
|
|
4645
|
+
maxBuffer: 16 * 1024 * 1024
|
|
4499
4646
|
}).trim();
|
|
4500
4647
|
if (psOutput) {
|
|
4501
4648
|
for (const line of psOutput.split("\n")) {
|
|
4649
|
+
if (!psPattern.test(line)) continue;
|
|
4502
4650
|
const parts = line.trim().split(/\s+/);
|
|
4503
4651
|
if (parts.length >= 2) {
|
|
4504
4652
|
const pid = parseInt(parts[1], 10);
|
|
@@ -4507,8 +4655,9 @@ function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
|
4507
4655
|
}
|
|
4508
4656
|
}
|
|
4509
4657
|
} catch (err) {
|
|
4658
|
+
const reason = (typeof err === "object" && err !== null && "stderr" in err && typeof err.stderr === "string" ? err.stderr : "").split("\n").map((line) => line.trim()).find(Boolean) ?? (err instanceof Error ? err.message : String(err)).split("\n").map((line) => line.trim()).find(Boolean) ?? "no error detail available";
|
|
4510
4659
|
console.warn(
|
|
4511
|
-
`
|
|
4660
|
+
`Could not list running processes, so an already-running OpenCode may not be detected: ${reason}`
|
|
4512
4661
|
);
|
|
4513
4662
|
}
|
|
4514
4663
|
}
|
|
@@ -4540,7 +4689,7 @@ function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
|
4540
4689
|
return instances;
|
|
4541
4690
|
}
|
|
4542
4691
|
function findOpenCodeProcesses() {
|
|
4543
|
-
return findProcessesByPattern("opencode serve|opencode-serve",
|
|
4692
|
+
return findProcessesByPattern("opencode serve|opencode-serve", /opencode (serve|--port)/);
|
|
4544
4693
|
}
|
|
4545
4694
|
async function scanPortsForOpenCode() {
|
|
4546
4695
|
const instances = [];
|
|
@@ -4928,11 +5077,11 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
4928
5077
|
|
|
4929
5078
|
// src/lib/opencode/log-tail.ts
|
|
4930
5079
|
import { statSync as statSync4 } from "node:fs";
|
|
4931
|
-
import { homedir as
|
|
5080
|
+
import { homedir as homedir5 } from "node:os";
|
|
4932
5081
|
import { join as join7 } from "node:path";
|
|
4933
5082
|
import { open as open2, stat } from "node:fs/promises";
|
|
4934
5083
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
4935
|
-
function resolveOpenCodeLogPath(homeDir =
|
|
5084
|
+
function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
|
|
4936
5085
|
const dataDir = env.XDG_DATA_HOME || join7(homeDir, ".local", "share");
|
|
4937
5086
|
return join7(dataDir, "opencode", "log", "opencode.log");
|
|
4938
5087
|
}
|
|
@@ -5087,7 +5236,11 @@ async function reclaimSessionDbSpace(input) {
|
|
|
5087
5236
|
console.warn(
|
|
5088
5237
|
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
|
|
5089
5238
|
);
|
|
5090
|
-
return {
|
|
5239
|
+
return {
|
|
5240
|
+
ok: false,
|
|
5241
|
+
skipped: "full-vacuum-blocked",
|
|
5242
|
+
detail: "a session turn is live"
|
|
5243
|
+
};
|
|
5091
5244
|
}
|
|
5092
5245
|
const fileBytesForGuard = statSync5(dbPath).size;
|
|
5093
5246
|
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
@@ -5095,7 +5248,7 @@ async function reclaimSessionDbSpace(input) {
|
|
|
5095
5248
|
console.warn(
|
|
5096
5249
|
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
|
|
5097
5250
|
);
|
|
5098
|
-
return { ok: false, skipped: "insufficient-disk-space" };
|
|
5251
|
+
return { ok: false, skipped: "insufficient-disk-space", detail: skipReason };
|
|
5099
5252
|
}
|
|
5100
5253
|
const beforeBytes = readLogicalBytes(db);
|
|
5101
5254
|
db.exec("PRAGMA auto_vacuum=INCREMENTAL");
|
|
@@ -5357,6 +5510,8 @@ var StreamForwarder = class {
|
|
|
5357
5510
|
if (done) break;
|
|
5358
5511
|
const chunk = Buffer.from(value);
|
|
5359
5512
|
for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
|
|
5513
|
+
await stream.pausePromise;
|
|
5514
|
+
if (ac.signal.aborted) break;
|
|
5360
5515
|
const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
|
|
5361
5516
|
if (this.binaryFramesSupported) {
|
|
5362
5517
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
@@ -5399,6 +5554,7 @@ function classifyUpgradeRejection(headers) {
|
|
|
5399
5554
|
}
|
|
5400
5555
|
var MAX_RECONNECT_DELAY = 3e4;
|
|
5401
5556
|
var BASE_RECONNECT_DELAY = 500;
|
|
5557
|
+
var RELAY_SILENCE_TIMEOUT_MS = 9e4;
|
|
5402
5558
|
function getReconnectDelay(attempt) {
|
|
5403
5559
|
const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
|
|
5404
5560
|
const jitter = Math.random() * 1e3;
|
|
@@ -5470,10 +5626,27 @@ function connectTunnel(options) {
|
|
|
5470
5626
|
ws.close();
|
|
5471
5627
|
reject(new Error("Connection timeout"));
|
|
5472
5628
|
}, 3e4);
|
|
5629
|
+
let relaySilenceTimeout;
|
|
5630
|
+
let relaySilenceTimedOut = false;
|
|
5631
|
+
const clearRelaySilenceTimeout = () => {
|
|
5632
|
+
if (relaySilenceTimeout === void 0) return;
|
|
5633
|
+
clearTimeout(relaySilenceTimeout);
|
|
5634
|
+
relaySilenceTimeout = void 0;
|
|
5635
|
+
};
|
|
5636
|
+
const armRelaySilenceTimeout = () => {
|
|
5637
|
+
clearRelaySilenceTimeout();
|
|
5638
|
+
relaySilenceTimeout = setTimeout(() => {
|
|
5639
|
+
relaySilenceTimeout = void 0;
|
|
5640
|
+
relaySilenceTimedOut = true;
|
|
5641
|
+
ws.terminate();
|
|
5642
|
+
}, RELAY_SILENCE_TIMEOUT_MS);
|
|
5643
|
+
relaySilenceTimeout.unref?.();
|
|
5644
|
+
};
|
|
5473
5645
|
let upgradeRejection = null;
|
|
5474
5646
|
let upgradeRejectionReason = null;
|
|
5475
5647
|
ws.on("unexpected-response", (_req, res) => {
|
|
5476
5648
|
clearTimeout(connectionTimeout);
|
|
5649
|
+
clearRelaySilenceTimeout();
|
|
5477
5650
|
const reason = classifyUpgradeRejection(res.headers);
|
|
5478
5651
|
upgradeRejectionReason = reason;
|
|
5479
5652
|
const chunks = [];
|
|
@@ -5503,6 +5676,7 @@ function connectTunnel(options) {
|
|
|
5503
5676
|
onInfo?.("WebSocket connection established");
|
|
5504
5677
|
});
|
|
5505
5678
|
ws.on("message", (data, isBinary) => {
|
|
5679
|
+
if (relaySilenceTimeout !== void 0) armRelaySilenceTimeout();
|
|
5506
5680
|
if (isBinary) {
|
|
5507
5681
|
try {
|
|
5508
5682
|
const frame = decodeBinaryBodyFrame(toUint8Array(data));
|
|
@@ -5536,6 +5710,7 @@ function connectTunnel(options) {
|
|
|
5536
5710
|
switch (message.type) {
|
|
5537
5711
|
case "connected": {
|
|
5538
5712
|
clearTimeout(connectionTimeout);
|
|
5713
|
+
armRelaySilenceTimeout();
|
|
5539
5714
|
forwarder.setBinaryFramesSupported(message.binary_frames === true);
|
|
5540
5715
|
const connectedAgentId = message.agent_id ?? agentId;
|
|
5541
5716
|
onConnected?.(connectedAgentId);
|
|
@@ -5560,6 +5735,7 @@ function connectTunnel(options) {
|
|
|
5560
5735
|
});
|
|
5561
5736
|
ws.on("error", (error2) => {
|
|
5562
5737
|
clearTimeout(connectionTimeout);
|
|
5738
|
+
clearRelaySilenceTimeout();
|
|
5563
5739
|
const detail = upgradeRejection ?? describeSocketError(error2, url);
|
|
5564
5740
|
if (upgradeRejectionReason === "do_code_updated") {
|
|
5565
5741
|
onWarning?.("Relay redeployed \u2014 reconnecting");
|
|
@@ -5571,7 +5747,8 @@ function connectTunnel(options) {
|
|
|
5571
5747
|
);
|
|
5572
5748
|
});
|
|
5573
5749
|
ws.on("close", (code, reason) => {
|
|
5574
|
-
|
|
5750
|
+
clearRelaySilenceTimeout();
|
|
5751
|
+
const reasonStr = (relaySilenceTimedOut ? "no relay heartbeat for 90s \u2014 reconnecting" : void 0) || reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
|
|
5575
5752
|
forwarder.abortAll();
|
|
5576
5753
|
onDisconnected?.(code, reasonStr);
|
|
5577
5754
|
});
|
|
@@ -5579,9 +5756,10 @@ function connectTunnel(options) {
|
|
|
5579
5756
|
}
|
|
5580
5757
|
|
|
5581
5758
|
// src/lib/tunnel/runner-connection.ts
|
|
5582
|
-
var RunnerConnection = class {
|
|
5759
|
+
var RunnerConnection = class _RunnerConnection {
|
|
5583
5760
|
opts;
|
|
5584
5761
|
sleep;
|
|
5762
|
+
now;
|
|
5585
5763
|
connection = null;
|
|
5586
5764
|
resolvedAgentId;
|
|
5587
5765
|
/** True while a (re)connect loop is in flight. */
|
|
@@ -5590,10 +5768,17 @@ var RunnerConnection = class {
|
|
|
5590
5768
|
reconnectPromise = null;
|
|
5591
5769
|
/** 1-based count of the current reconnect attempt streak. */
|
|
5592
5770
|
reconnectAttempt = 0;
|
|
5771
|
+
generation = 0;
|
|
5772
|
+
disconnectedAtMs = null;
|
|
5773
|
+
static RELAY_RECONNECT_CLOSE_REASONS = /* @__PURE__ */ new Set([
|
|
5774
|
+
"No pong received",
|
|
5775
|
+
"Orphaned socket"
|
|
5776
|
+
]);
|
|
5593
5777
|
constructor(opts) {
|
|
5594
5778
|
this.opts = opts;
|
|
5595
5779
|
this.resolvedAgentId = opts.agentId;
|
|
5596
5780
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5781
|
+
this.now = opts.now ?? Date.now;
|
|
5597
5782
|
}
|
|
5598
5783
|
get agentId() {
|
|
5599
5784
|
return this.resolvedAgentId;
|
|
@@ -5629,14 +5814,24 @@ var RunnerConnection = class {
|
|
|
5629
5814
|
port: this.opts.port,
|
|
5630
5815
|
openCodePassword: this.opts.openCodePassword,
|
|
5631
5816
|
onConnected: (agentId) => {
|
|
5817
|
+
const attempt = this.reconnectAttempt + 1;
|
|
5632
5818
|
this.reconnectAttempt = 0;
|
|
5633
5819
|
this.reconnecting = false;
|
|
5634
5820
|
this.resolvedAgentId = agentId;
|
|
5635
|
-
|
|
5821
|
+
this.generation++;
|
|
5822
|
+
const downtimeMs = this.disconnectedAtMs === null ? null : this.now() - this.disconnectedAtMs;
|
|
5823
|
+
this.disconnectedAtMs = null;
|
|
5824
|
+
events.onConnected(agentId, isReconnect, {
|
|
5825
|
+
generation: this.generation,
|
|
5826
|
+
attempt,
|
|
5827
|
+
downtimeMs
|
|
5828
|
+
});
|
|
5636
5829
|
},
|
|
5637
5830
|
onDisconnected: (code, reason) => {
|
|
5638
|
-
|
|
5639
|
-
|
|
5831
|
+
this.disconnectedAtMs = this.now();
|
|
5832
|
+
events.onDisconnected(code, reason, { generation: this.generation });
|
|
5833
|
+
const relayRequestedReconnect = code === 1e3 && _RunnerConnection.RELAY_RECONNECT_CLOSE_REASONS.has(reason);
|
|
5834
|
+
if (this.opts.isRunning() && (code !== 1e3 || relayRequestedReconnect) && !this.reconnecting) {
|
|
5640
5835
|
this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
|
|
5641
5836
|
events.onError?.(`Reconnection failed: ${err.message}`);
|
|
5642
5837
|
});
|
|
@@ -5976,7 +6171,7 @@ function createResourceUsageCollector(homeDir) {
|
|
|
5976
6171
|
}
|
|
5977
6172
|
|
|
5978
6173
|
// src/lib/channels/driver.ts
|
|
5979
|
-
import { homedir as
|
|
6174
|
+
import { homedir as homedir6 } from "node:os";
|
|
5980
6175
|
|
|
5981
6176
|
// src/lib/runner-file-sync.ts
|
|
5982
6177
|
import { join as join9 } from "node:path";
|
|
@@ -6859,7 +7054,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6859
7054
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
6860
7055
|
this.now = config.now ?? (() => Date.now());
|
|
6861
7056
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
6862
|
-
this.homeDir = config.homeDir ??
|
|
7057
|
+
this.homeDir = config.homeDir ?? homedir6();
|
|
6863
7058
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
6864
7059
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
6865
7060
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
@@ -12112,6 +12307,7 @@ function createCredentialSync({
|
|
|
12112
12307
|
|
|
12113
12308
|
// src/commands/run.ts
|
|
12114
12309
|
var MAX_ACTIVITY_LOG_ENTRIES = 10;
|
|
12310
|
+
var MAX_DISCONNECT_REASON_LENGTH = 200;
|
|
12115
12311
|
var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
|
|
12116
12312
|
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
12117
12313
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
|
|
@@ -12267,7 +12463,13 @@ function logActivity(state, entry) {
|
|
|
12267
12463
|
const level = entry.level ?? (entry.type === "error" ? "error" : "info");
|
|
12268
12464
|
if (!meetsThreshold(state, level)) return;
|
|
12269
12465
|
forwardRunnerActivity(
|
|
12270
|
-
{
|
|
12466
|
+
{
|
|
12467
|
+
level,
|
|
12468
|
+
message: entry.message,
|
|
12469
|
+
error: entry.error,
|
|
12470
|
+
metadata: entry.metadata,
|
|
12471
|
+
...entry.source ? { source: entry.source } : {}
|
|
12472
|
+
},
|
|
12271
12473
|
{ agentId: state.agentId, authHeader: state.authHeader }
|
|
12272
12474
|
);
|
|
12273
12475
|
const fullEntry = {
|
|
@@ -12289,7 +12491,7 @@ function logActivity(state, entry) {
|
|
|
12289
12491
|
}
|
|
12290
12492
|
function reportSessionDbRecovery(state) {
|
|
12291
12493
|
try {
|
|
12292
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
12494
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir7(), env: process.env });
|
|
12293
12495
|
for (const record of report.records) {
|
|
12294
12496
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
12295
12497
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -12324,7 +12526,9 @@ function displayStatus(state) {
|
|
|
12324
12526
|
const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
|
|
12325
12527
|
const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
|
|
12326
12528
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
12327
|
-
const detail = last ? chalk7.dim(
|
|
12529
|
+
const detail = last ? chalk7.dim(
|
|
12530
|
+
` \xB7 ${last.timestamp.toISOString()} ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`
|
|
12531
|
+
) : "";
|
|
12328
12532
|
const agent = state.agentName ?? state.agentId;
|
|
12329
12533
|
console.log(
|
|
12330
12534
|
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
@@ -12343,7 +12547,7 @@ function reportOpenCodeV2CredentialImport(state, result) {
|
|
|
12343
12547
|
logActivity(state, {
|
|
12344
12548
|
type: "info",
|
|
12345
12549
|
level: "warn",
|
|
12346
|
-
message: `OpenCode V2 saved login(s) for ${integrationIds} on this machine, but OpenCode V2 will not use them.
|
|
12550
|
+
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.`
|
|
12347
12551
|
});
|
|
12348
12552
|
}
|
|
12349
12553
|
if (result.imported.length === 0 && result.unsupported.length === 0 && result.failed.length === 0) {
|
|
@@ -12368,7 +12572,7 @@ async function importOpenCodeV2CredentialsForState(state) {
|
|
|
12368
12572
|
}
|
|
12369
12573
|
const result = await importOpenCodeV2Credentials({
|
|
12370
12574
|
request: client.request,
|
|
12371
|
-
homeDir:
|
|
12575
|
+
homeDir: homedir7(),
|
|
12372
12576
|
directory,
|
|
12373
12577
|
guard: state.opencodeV2CredentialImportGuard,
|
|
12374
12578
|
log: (message, level = "info") => logActivity(state, { type: "info", level, message })
|
|
@@ -12587,7 +12791,14 @@ function shouldWarnForReclaimSkip(reason) {
|
|
|
12587
12791
|
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
12588
12792
|
}
|
|
12589
12793
|
function sessionDbPath() {
|
|
12590
|
-
return join11(
|
|
12794
|
+
return join11(homedir7(), ".local", "share", "opencode", "opencode.db");
|
|
12795
|
+
}
|
|
12796
|
+
function logSessionCleanupActivity(state, phase, entry) {
|
|
12797
|
+
logActivity(state, {
|
|
12798
|
+
...entry,
|
|
12799
|
+
source: "cli.session-cleanup",
|
|
12800
|
+
metadata: { ...entry.metadata, phase }
|
|
12801
|
+
});
|
|
12591
12802
|
}
|
|
12592
12803
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
12593
12804
|
const record = {
|
|
@@ -12630,9 +12841,13 @@ async function runSweep(state, driver, config) {
|
|
|
12630
12841
|
try {
|
|
12631
12842
|
const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
|
|
12632
12843
|
if (sessions === null) {
|
|
12633
|
-
|
|
12844
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12634
12845
|
type: "info",
|
|
12635
|
-
message: `Session cleanup: could not list sessions (opencode unreachable); skipping this sweep (${mode})
|
|
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
|
+
}
|
|
12636
12851
|
});
|
|
12637
12852
|
return;
|
|
12638
12853
|
}
|
|
@@ -12676,21 +12891,47 @@ async function runSweep(state, driver, config) {
|
|
|
12676
12891
|
const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
|
|
12677
12892
|
const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
|
|
12678
12893
|
const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
|
|
12679
|
-
|
|
12894
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12680
12895
|
type: "info",
|
|
12681
|
-
message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${checkpointNote}
|
|
12896
|
+
message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${checkpointNote}`,
|
|
12897
|
+
metadata: {
|
|
12898
|
+
outcome: "reclaimed",
|
|
12899
|
+
mode: reclaimResult.mode,
|
|
12900
|
+
before_bytes: reclaimResult.beforeBytes,
|
|
12901
|
+
after_bytes: reclaimResult.afterBytes,
|
|
12902
|
+
checkpoint_busy: reclaimResult.checkpoint.busy,
|
|
12903
|
+
wal_frames_pending: reclaimResult.checkpoint.log,
|
|
12904
|
+
sessions_inspected: sessions.length,
|
|
12905
|
+
sessions_deleted: deleted,
|
|
12906
|
+
sessions_failed: failed,
|
|
12907
|
+
sessions_skipped_active: skippedNewlyActive
|
|
12908
|
+
}
|
|
12682
12909
|
});
|
|
12683
12910
|
} else {
|
|
12684
|
-
|
|
12911
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12685
12912
|
type: "info",
|
|
12686
|
-
|
|
12913
|
+
level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
|
|
12914
|
+
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : ""),
|
|
12915
|
+
metadata: {
|
|
12916
|
+
outcome: "reclaim-skipped",
|
|
12917
|
+
reason: reclaimResult.skipped,
|
|
12918
|
+
...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
|
|
12923
|
+
}
|
|
12687
12924
|
});
|
|
12688
12925
|
}
|
|
12689
12926
|
} catch (error2) {
|
|
12690
12927
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
12691
|
-
|
|
12928
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12692
12929
|
type: "error",
|
|
12693
|
-
error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}
|
|
12930
|
+
error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}`,
|
|
12931
|
+
metadata: {
|
|
12932
|
+
outcome: "sweep-failed",
|
|
12933
|
+
reason: "sweep-error"
|
|
12934
|
+
}
|
|
12694
12935
|
});
|
|
12695
12936
|
}
|
|
12696
12937
|
}
|
|
@@ -12706,14 +12947,19 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
12706
12947
|
for (const warning2 of config.warnings) {
|
|
12707
12948
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
12708
12949
|
}
|
|
12709
|
-
const dbBytes = statSessionDbBytes(
|
|
12950
|
+
const dbBytes = statSessionDbBytes(homedir7());
|
|
12710
12951
|
void (async () => {
|
|
12711
12952
|
const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
12712
12953
|
if (reclaimAvailability !== null) {
|
|
12713
|
-
|
|
12954
|
+
logSessionCleanupActivity(state, "startup", {
|
|
12714
12955
|
type: "info",
|
|
12715
12956
|
level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
|
|
12716
|
-
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
|
|
12957
|
+
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : ""),
|
|
12958
|
+
metadata: {
|
|
12959
|
+
outcome: "preflight-skipped",
|
|
12960
|
+
reason: reclaimAvailability.reason,
|
|
12961
|
+
...reclaimAvailability.detail !== void 0 ? { detail: reclaimAvailability.detail } : {}
|
|
12962
|
+
}
|
|
12717
12963
|
});
|
|
12718
12964
|
}
|
|
12719
12965
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
@@ -12730,9 +12976,15 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
12730
12976
|
);
|
|
12731
12977
|
});
|
|
12732
12978
|
if (!config.enabled) return;
|
|
12733
|
-
|
|
12979
|
+
logSessionCleanupActivity(state, "startup", {
|
|
12734
12980
|
type: "info",
|
|
12735
|
-
message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)
|
|
12981
|
+
message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)`,
|
|
12982
|
+
metadata: {
|
|
12983
|
+
outcome: "armed",
|
|
12984
|
+
interval_ms: config.intervalMs,
|
|
12985
|
+
max_age_ms: config.maxAgeMs ?? null,
|
|
12986
|
+
max_count: config.maxCount ?? null
|
|
12987
|
+
}
|
|
12736
12988
|
});
|
|
12737
12989
|
const interval = setInterval(() => void runSweep(state, driver, config), config.intervalMs);
|
|
12738
12990
|
const firstSweep = setTimeout(
|
|
@@ -12911,7 +13163,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
12911
13163
|
});
|
|
12912
13164
|
return;
|
|
12913
13165
|
}
|
|
12914
|
-
const { collect, stop } = createResourceUsageCollector(
|
|
13166
|
+
const { collect, stop } = createResourceUsageCollector(homedir7());
|
|
12915
13167
|
state.stopResourceUsageSampling = stop;
|
|
12916
13168
|
let consecutiveFailures = 0;
|
|
12917
13169
|
const tick = async () => {
|
|
@@ -13136,7 +13388,7 @@ async function run(options) {
|
|
|
13136
13388
|
let fileSyncDirectories;
|
|
13137
13389
|
try {
|
|
13138
13390
|
logLevel = resolveLogLevel(options);
|
|
13139
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
13391
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir7());
|
|
13140
13392
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
13141
13393
|
throw new Error(
|
|
13142
13394
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -13191,6 +13443,7 @@ async function run(options) {
|
|
|
13191
13443
|
authHeader: ""
|
|
13192
13444
|
};
|
|
13193
13445
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
13446
|
+
setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
|
|
13194
13447
|
if (options.credentialSyncMarker) {
|
|
13195
13448
|
state.credentialSync = createCredentialSync({
|
|
13196
13449
|
markerPath: options.credentialSyncMarker,
|
|
@@ -13376,13 +13629,18 @@ async function run(options) {
|
|
|
13376
13629
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
13377
13630
|
}
|
|
13378
13631
|
state.credentialSync?.arm();
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
|
|
13382
|
-
|
|
13383
|
-
|
|
13384
|
-
|
|
13385
|
-
|
|
13632
|
+
const openCodeActivityForwarder = createOpenCodeActivityForwarder(() => ({
|
|
13633
|
+
agentId: state.agentId,
|
|
13634
|
+
authHeader: state.authHeader
|
|
13635
|
+
}));
|
|
13636
|
+
const openCodeLogTail = tailOpenCodeLogFile(
|
|
13637
|
+
resolveOpenCodeLogPath(homedir7(), process.env),
|
|
13638
|
+
openCodeActivityForwarder
|
|
13639
|
+
);
|
|
13640
|
+
state.stopOpenCodeLogTail = () => {
|
|
13641
|
+
openCodeActivityForwarder.flush();
|
|
13642
|
+
openCodeLogTail.stop();
|
|
13643
|
+
};
|
|
13386
13644
|
let sessionDbVerifyFatal = false;
|
|
13387
13645
|
if (!options.restoreSessionDb) {
|
|
13388
13646
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -13499,7 +13757,7 @@ async function run(options) {
|
|
|
13499
13757
|
const provenance = checkSessionDbProvenance({
|
|
13500
13758
|
dbPath: sessionDbPath(),
|
|
13501
13759
|
currentVersion: state.opencodeVersion,
|
|
13502
|
-
homeDir:
|
|
13760
|
+
homeDir: homedir7(),
|
|
13503
13761
|
env: process.env
|
|
13504
13762
|
});
|
|
13505
13763
|
if (provenance.anomaly) {
|
|
@@ -13669,7 +13927,7 @@ async function run(options) {
|
|
|
13669
13927
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
13670
13928
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
13671
13929
|
fileSyncDirectories,
|
|
13672
|
-
homeDir:
|
|
13930
|
+
homeDir: homedir7(),
|
|
13673
13931
|
maxActiveSessions,
|
|
13674
13932
|
log: (entry) => (
|
|
13675
13933
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -13691,12 +13949,13 @@ async function run(options) {
|
|
|
13691
13949
|
openCodePassword: state.opencodePassword,
|
|
13692
13950
|
isRunning: () => state.running,
|
|
13693
13951
|
events: {
|
|
13694
|
-
onConnected: (agentId, isReconnect) => {
|
|
13952
|
+
onConnected: (agentId, isReconnect, transition) => {
|
|
13695
13953
|
state.connected = true;
|
|
13696
13954
|
state.agentId = agentId;
|
|
13955
|
+
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})`;
|
|
13697
13956
|
logActivity(state, {
|
|
13698
13957
|
type: "info",
|
|
13699
|
-
message:
|
|
13958
|
+
message: connectionMessage
|
|
13700
13959
|
});
|
|
13701
13960
|
if (options.tunnelReadyFile) {
|
|
13702
13961
|
const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
|
|
@@ -13717,7 +13976,12 @@ async function run(options) {
|
|
|
13717
13976
|
version: state.opencodeVersion,
|
|
13718
13977
|
major: state.opencodeApiVersion,
|
|
13719
13978
|
connected: state.opencodeConnected
|
|
13720
|
-
})
|
|
13979
|
+
}),
|
|
13980
|
+
connection_generation: transition.generation,
|
|
13981
|
+
...isReconnect && transition.downtimeMs !== null ? {
|
|
13982
|
+
reconnect_attempt: transition.attempt,
|
|
13983
|
+
downtime_ms: transition.downtimeMs
|
|
13984
|
+
} : {}
|
|
13721
13985
|
});
|
|
13722
13986
|
if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
|
|
13723
13987
|
if (state.interactive) displayStatus(state);
|
|
@@ -13739,13 +14003,18 @@ async function run(options) {
|
|
|
13739
14003
|
if (state.interactive) displayStatus(state);
|
|
13740
14004
|
});
|
|
13741
14005
|
},
|
|
13742
|
-
onDisconnected: (code, reason) => {
|
|
14006
|
+
onDisconnected: (code, reason, transition) => {
|
|
13743
14007
|
state.connected = false;
|
|
14008
|
+
const safeReason = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
|
|
13744
14009
|
logActivity(state, {
|
|
13745
14010
|
type: "info",
|
|
13746
|
-
message: `Tunnel disconnected (code: ${code}, reason: ${
|
|
14011
|
+
message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason})`
|
|
14012
|
+
});
|
|
14013
|
+
emitAgentDisconnected(state.agentId, {
|
|
14014
|
+
code,
|
|
14015
|
+
reason: safeReason,
|
|
14016
|
+
connection_generation: transition.generation
|
|
13747
14017
|
});
|
|
13748
|
-
emitAgentDisconnected(state.agentId, { code, reason });
|
|
13749
14018
|
if (state.interactive) displayStatus(state);
|
|
13750
14019
|
},
|
|
13751
14020
|
onError: (error2) => {
|
|
@@ -13845,7 +14114,7 @@ async function run(options) {
|
|
|
13845
14114
|
},
|
|
13846
14115
|
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
13847
14116
|
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
13848
|
-
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
14117
|
+
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`",
|
|
13849
14118
|
firstDelayMs: firstReportDelayMs,
|
|
13850
14119
|
nextDelayMs: usageReportDelayMs,
|
|
13851
14120
|
failureLogLevel: usageReportFailureLogLevel
|
|
@@ -13888,7 +14157,7 @@ async function run(options) {
|
|
|
13888
14157
|
}
|
|
13889
14158
|
|
|
13890
14159
|
// src/index.ts
|
|
13891
|
-
var
|
|
14160
|
+
var version = getCliVersion();
|
|
13892
14161
|
var program = new Command();
|
|
13893
14162
|
program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
|
|
13894
14163
|
"--endpoint <url>",
|