@evident-ai/cli 3.4.0 → 3.4.1-dev.15755a4
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/README.md +10 -1
- package/dist/index.js +547 -72
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -746,6 +746,40 @@ async function reportClaudeUsage(agentId, authHeader, snapshot) {
|
|
|
746
746
|
return { ok: false, error: describeBestEffortError(error2) };
|
|
747
747
|
}
|
|
748
748
|
}
|
|
749
|
+
function toReportedOpenAiWindow(window) {
|
|
750
|
+
if (!window) return null;
|
|
751
|
+
return {
|
|
752
|
+
utilization: window.utilization,
|
|
753
|
+
window_minutes: window.windowMinutes,
|
|
754
|
+
resets_at: window.resetsAt
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
async function reportOpenAiUsage(agentId, authHeader, snapshot) {
|
|
758
|
+
try {
|
|
759
|
+
const apiUrl = getApiUrlConfig();
|
|
760
|
+
const response = await fetch(`${apiUrl}/runners/${agentId}/openai-usage`, {
|
|
761
|
+
method: "POST",
|
|
762
|
+
headers: { Authorization: authHeader, "Content-Type": "application/json" },
|
|
763
|
+
body: JSON.stringify({
|
|
764
|
+
primary: toReportedOpenAiWindow(snapshot.primary),
|
|
765
|
+
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
766
|
+
has_credits: snapshot.hasCredits,
|
|
767
|
+
credits_unlimited: snapshot.creditsUnlimited
|
|
768
|
+
}),
|
|
769
|
+
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
770
|
+
});
|
|
771
|
+
if (!response.ok) {
|
|
772
|
+
const serverMessage = await readErrorMessage(response);
|
|
773
|
+
return {
|
|
774
|
+
ok: false,
|
|
775
|
+
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
return { ok: true };
|
|
779
|
+
} catch (error2) {
|
|
780
|
+
return { ok: false, error: describeBestEffortError(error2) };
|
|
781
|
+
}
|
|
782
|
+
}
|
|
749
783
|
async function reportResourceUsage(agentId, authHeader, usage) {
|
|
750
784
|
try {
|
|
751
785
|
const apiUrl = getApiUrlConfig();
|
|
@@ -1092,8 +1126,8 @@ async function claudeUsage() {
|
|
|
1092
1126
|
}
|
|
1093
1127
|
|
|
1094
1128
|
// src/commands/run.ts
|
|
1095
|
-
import { homedir as
|
|
1096
|
-
import { isAbsolute as isAbsolute2, join as
|
|
1129
|
+
import { homedir as homedir4 } from "os";
|
|
1130
|
+
import { isAbsolute as isAbsolute2, join as join7, parse, resolve as resolvePath } from "path";
|
|
1097
1131
|
import chalk6 from "chalk";
|
|
1098
1132
|
|
|
1099
1133
|
// ../../packages/types/src/agents/index.ts
|
|
@@ -1326,6 +1360,8 @@ var SEVERITY_BY_LEVEL = {
|
|
|
1326
1360
|
error: "error"
|
|
1327
1361
|
};
|
|
1328
1362
|
var MAX_MESSAGE_LENGTH = 500;
|
|
1363
|
+
var MAX_METADATA_VALUE_LENGTH = 200;
|
|
1364
|
+
var MAX_METADATA_ENTRIES = 20;
|
|
1329
1365
|
var TRUNCATION_MARKER = "\u2026";
|
|
1330
1366
|
function redact(message) {
|
|
1331
1367
|
return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/https?:\/\/\S+/g, "<url>");
|
|
@@ -1334,6 +1370,24 @@ function truncate(message) {
|
|
|
1334
1370
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
1335
1371
|
return message.slice(0, MAX_MESSAGE_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
|
|
1336
1372
|
}
|
|
1373
|
+
function sanitiseMetadata(metadata) {
|
|
1374
|
+
if (!metadata) return {};
|
|
1375
|
+
const entries = Object.entries(metadata).slice(0, MAX_METADATA_ENTRIES);
|
|
1376
|
+
if (Object.keys(metadata).length > entries.length) {
|
|
1377
|
+
console.error(
|
|
1378
|
+
`[runner-activity-telemetry] dropped ${Object.keys(metadata).length - entries.length} metadata entries`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
const sanitised = [];
|
|
1382
|
+
for (const [key, value] of entries) {
|
|
1383
|
+
if (typeof value === "string") {
|
|
1384
|
+
sanitised.push([key, truncate(redact(value).slice(0, MAX_METADATA_VALUE_LENGTH))]);
|
|
1385
|
+
} else if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
|
|
1386
|
+
sanitised.push([key, value]);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return Object.fromEntries(sanitised);
|
|
1390
|
+
}
|
|
1337
1391
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1338
1392
|
var RATE_LIMIT_MAX_EVENTS = 30;
|
|
1339
1393
|
var windowStartedAt = 0;
|
|
@@ -1372,7 +1426,7 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1372
1426
|
logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
|
|
1373
1427
|
severity: SEVERITY_BY_LEVEL[entry.level],
|
|
1374
1428
|
message,
|
|
1375
|
-
metadata: { source: "cli.run" },
|
|
1429
|
+
metadata: { ...sanitiseMetadata(entry.metadata), source: "cli.run" },
|
|
1376
1430
|
agentId: context.agentId
|
|
1377
1431
|
});
|
|
1378
1432
|
} catch (err) {
|
|
@@ -1382,6 +1436,150 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1382
1436
|
}
|
|
1383
1437
|
}
|
|
1384
1438
|
|
|
1439
|
+
// src/lib/opencode/session-db-recovery-report.ts
|
|
1440
|
+
import { readFileSync as readFileSync2, unlinkSync } from "fs";
|
|
1441
|
+
import { join as join2 } from "path";
|
|
1442
|
+
function sessionDbRecoveryReportPath(homeDir, env) {
|
|
1443
|
+
const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
|
|
1444
|
+
return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
|
|
1445
|
+
}
|
|
1446
|
+
function drainSessionDbRecoveryReport({
|
|
1447
|
+
homeDir,
|
|
1448
|
+
env
|
|
1449
|
+
}) {
|
|
1450
|
+
const path = sessionDbRecoveryReportPath(homeDir, env);
|
|
1451
|
+
let content;
|
|
1452
|
+
try {
|
|
1453
|
+
content = readFileSync2(path, "utf8");
|
|
1454
|
+
} catch (error2) {
|
|
1455
|
+
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
|
|
1456
|
+
return { path, records: [], skippedLines: 0, readError: null };
|
|
1457
|
+
const readError = error2 instanceof Error ? error2.message : String(error2);
|
|
1458
|
+
console.error(`[session-db-recovery-report] could not read ${path}: ${readError}`);
|
|
1459
|
+
return { path, records: [], skippedLines: 0, readError };
|
|
1460
|
+
}
|
|
1461
|
+
let skippedLines = 0;
|
|
1462
|
+
const records = content.split("\n").flatMap((line) => {
|
|
1463
|
+
if (!line.trim()) return [];
|
|
1464
|
+
try {
|
|
1465
|
+
const value = JSON.parse(line);
|
|
1466
|
+
if (!isSessionDbRecoveryRecord(value)) {
|
|
1467
|
+
skippedLines++;
|
|
1468
|
+
return [];
|
|
1469
|
+
}
|
|
1470
|
+
return [value];
|
|
1471
|
+
} catch (error2) {
|
|
1472
|
+
skippedLines++;
|
|
1473
|
+
console.error(
|
|
1474
|
+
`[session-db-recovery-report] skipped malformed record in ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1475
|
+
);
|
|
1476
|
+
return [];
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
return { path, records, skippedLines, readError: null };
|
|
1480
|
+
}
|
|
1481
|
+
function acknowledgeSessionDbRecoveryReport(path) {
|
|
1482
|
+
try {
|
|
1483
|
+
unlinkSync(path);
|
|
1484
|
+
} catch (error2) {
|
|
1485
|
+
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return;
|
|
1486
|
+
console.error(
|
|
1487
|
+
`[session-db-recovery-report] could not remove ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
function buildSessionDbRecoveryActivity(record) {
|
|
1492
|
+
const level = record.severity === "warning" ? "warn" : record.severity === "error" ? "error" : null;
|
|
1493
|
+
if (!level) return null;
|
|
1494
|
+
const noVerifiedPoint = record.verified_restore_point ? ` The verified restore point is ${record.verified_restore_point}.` : " No verified restore point is known.";
|
|
1495
|
+
switch (record.outcome) {
|
|
1496
|
+
case "fresh_session_db":
|
|
1497
|
+
return {
|
|
1498
|
+
level,
|
|
1499
|
+
metadata: withoutContractFields(record),
|
|
1500
|
+
message: `This runner started with a fresh session database. Earlier sessions are unavailable, and queued conversation or session references from before this start may no longer resolve.${noVerifiedPoint} Review the runner's backup configuration before relying on restored session history.`
|
|
1501
|
+
};
|
|
1502
|
+
case "restore_retried":
|
|
1503
|
+
return {
|
|
1504
|
+
level,
|
|
1505
|
+
metadata: withoutContractFields(record),
|
|
1506
|
+
message: "The initial session database restore failed; the runner will retry it. Monitor the runner backup for another restore failure."
|
|
1507
|
+
};
|
|
1508
|
+
case "replica_recovered":
|
|
1509
|
+
if (record.reason === "quarantine")
|
|
1510
|
+
return {
|
|
1511
|
+
level,
|
|
1512
|
+
metadata: withoutContractFields(record),
|
|
1513
|
+
message: `Session database recovery quarantined ${record.quarantined_objects ?? "unknown"} objects (${record.quarantined_bytes ?? "unknown"} bytes); ${record.quarantine_failed_objects ?? "unknown"} moves failed. Review the preserved backup at ${record.quarantine_destination ?? "an unknown destination"} before deleting it.`
|
|
1514
|
+
};
|
|
1515
|
+
if (record.reason === "prune")
|
|
1516
|
+
return {
|
|
1517
|
+
level,
|
|
1518
|
+
metadata: withoutContractFields(record),
|
|
1519
|
+
message: "Session database recovery discarded a damaged newest backup and retried. Sessions recorded after the previous backup point may be unavailable. Review the runner backup for another restore failure."
|
|
1520
|
+
};
|
|
1521
|
+
if (record.reason === "clear")
|
|
1522
|
+
return {
|
|
1523
|
+
level,
|
|
1524
|
+
metadata: withoutContractFields(record),
|
|
1525
|
+
message: "Session database recovery deleted the damaged backup and prior session history is unavailable. Review the runner backup configuration before relying on restored session history."
|
|
1526
|
+
};
|
|
1527
|
+
return null;
|
|
1528
|
+
case "history_rolled_back":
|
|
1529
|
+
return {
|
|
1530
|
+
level,
|
|
1531
|
+
metadata: withoutContractFields(record),
|
|
1532
|
+
message: `Session history was rolled back to verified restore point ${record.verified_restore_point ?? "unknown"}; everything after it is unavailable. Review the runner backup for another restore failure.`
|
|
1533
|
+
};
|
|
1534
|
+
case "restore_misconfigured":
|
|
1535
|
+
return {
|
|
1536
|
+
level,
|
|
1537
|
+
metadata: withoutContractFields(record),
|
|
1538
|
+
message: "This runner started with a fresh session database because it could not verify where its session history is backed up. Nothing in the backup was changed; check the runner backup configuration and permissions."
|
|
1539
|
+
};
|
|
1540
|
+
case "session_db_boot_refused":
|
|
1541
|
+
return {
|
|
1542
|
+
level,
|
|
1543
|
+
metadata: withoutContractFields(record),
|
|
1544
|
+
message: `This runner did not come online because its damaged session database could not be safely separated from its active backup or proven removed. Backed-up session history remains readable at ${record.quarantine_destination ?? "its original location or the quarantine destination named in the boot logs"}; any local session-database files that remain were left in place and nothing opened or wrote them. See the runner boot logs for SESSION-DB-LOCAL-DISCARD-FAILED details.`
|
|
1545
|
+
};
|
|
1546
|
+
default:
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
function withoutContractFields(record) {
|
|
1551
|
+
const { v: _v, event: _event, ...metadata } = record;
|
|
1552
|
+
return metadata;
|
|
1553
|
+
}
|
|
1554
|
+
var OUTCOMES = /* @__PURE__ */ new Set([
|
|
1555
|
+
"replica_recovered",
|
|
1556
|
+
"restore_retried",
|
|
1557
|
+
"fresh_session_db",
|
|
1558
|
+
"history_rolled_back",
|
|
1559
|
+
"restore_misconfigured",
|
|
1560
|
+
"session_db_boot_refused"
|
|
1561
|
+
]);
|
|
1562
|
+
var STAGES = /* @__PURE__ */ new Set(["restore", "verify"]);
|
|
1563
|
+
var SEVERITIES = /* @__PURE__ */ new Set(["warning", "error"]);
|
|
1564
|
+
var NUMBER_FIELDS = [
|
|
1565
|
+
"litestream_exit_code",
|
|
1566
|
+
"attempt",
|
|
1567
|
+
"replica_objects",
|
|
1568
|
+
"replica_bytes",
|
|
1569
|
+
"quarantined_objects",
|
|
1570
|
+
"quarantine_failed_objects",
|
|
1571
|
+
"quarantined_bytes",
|
|
1572
|
+
"restore_points_tried"
|
|
1573
|
+
];
|
|
1574
|
+
var STRING_OR_NULL_FIELDS = ["quarantine_destination", "verified_restore_point"];
|
|
1575
|
+
function isSessionDbRecoveryRecord(value) {
|
|
1576
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1577
|
+
const record = value;
|
|
1578
|
+
return record.v === 1 && record.event === "session_db_recovery" && typeof record.at === "string" && STAGES.has(record.stage) && OUTCOMES.has(record.outcome) && SEVERITIES.has(record.severity) && typeof record.reason === "string" && NUMBER_FIELDS.every((field) => record[field] === null || Number.isInteger(record[field])) && STRING_OR_NULL_FIELDS.every(
|
|
1579
|
+
(field) => record[field] === null || typeof record[field] === "string"
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1385
1583
|
// src/lib/opencode/health.ts
|
|
1386
1584
|
async function checkOpenCodeHealth(port) {
|
|
1387
1585
|
try {
|
|
@@ -2419,10 +2617,10 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
2419
2617
|
|
|
2420
2618
|
// src/lib/opencode/session-db-size.ts
|
|
2421
2619
|
import { statSync as statSync2 } from "fs";
|
|
2422
|
-
import { join as
|
|
2620
|
+
import { join as join3 } from "path";
|
|
2423
2621
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
2424
2622
|
function statSessionDbBytes(homeDir) {
|
|
2425
|
-
const dbPath =
|
|
2623
|
+
const dbPath = join3(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
2426
2624
|
try {
|
|
2427
2625
|
return statSync2(dbPath).size;
|
|
2428
2626
|
} catch (err) {
|
|
@@ -2991,6 +3189,177 @@ function writeTunnelReadyMarker(path, agentId) {
|
|
|
2991
3189
|
}
|
|
2992
3190
|
}
|
|
2993
3191
|
|
|
3192
|
+
// src/lib/openai-usage.ts
|
|
3193
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3194
|
+
import { homedir as homedir2 } from "os";
|
|
3195
|
+
import { join as join4 } from "path";
|
|
3196
|
+
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
3197
|
+
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
3198
|
+
var OpenAiUsageError = class extends Error {
|
|
3199
|
+
constructor(message, reason) {
|
|
3200
|
+
super(message);
|
|
3201
|
+
this.reason = reason;
|
|
3202
|
+
}
|
|
3203
|
+
};
|
|
3204
|
+
function isLocalCredentialProblem2(err) {
|
|
3205
|
+
return err instanceof OpenAiUsageError && (err.reason === "no_credentials" || err.reason === "credentials_expired");
|
|
3206
|
+
}
|
|
3207
|
+
function readOpenCodeChatGptCredentials() {
|
|
3208
|
+
try {
|
|
3209
|
+
const raw = readFileSync3(join4(homedir2(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
3210
|
+
let parsed;
|
|
3211
|
+
try {
|
|
3212
|
+
parsed = JSON.parse(raw);
|
|
3213
|
+
} catch {
|
|
3214
|
+
return null;
|
|
3215
|
+
}
|
|
3216
|
+
const entry = parsed.openai;
|
|
3217
|
+
if (entry?.type !== "oauth" || typeof entry.access !== "string" || !entry.access || typeof entry.expires !== "number") {
|
|
3218
|
+
return null;
|
|
3219
|
+
}
|
|
3220
|
+
return { accessToken: entry.access, expiresAt: entry.expires };
|
|
3221
|
+
} catch (err) {
|
|
3222
|
+
const code = err.code;
|
|
3223
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
3224
|
+
console.warn(
|
|
3225
|
+
`readOpenCodeChatGptCredentials: reading auth.json failed (${code ?? "unknown"})`
|
|
3226
|
+
);
|
|
3227
|
+
}
|
|
3228
|
+
return null;
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
function toWindow2(headers, name) {
|
|
3232
|
+
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
3233
|
+
const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
|
|
3234
|
+
if (utilizationHeader == null || utilizationHeader === "" || windowMinutesHeader == null) {
|
|
3235
|
+
return null;
|
|
3236
|
+
}
|
|
3237
|
+
const utilization = Number(utilizationHeader);
|
|
3238
|
+
const windowMinutes = Number(windowMinutesHeader);
|
|
3239
|
+
if (!Number.isFinite(utilization) || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
|
|
3240
|
+
return null;
|
|
3241
|
+
}
|
|
3242
|
+
const resetAtHeader = headers.get(`x-codex-${name}-reset-at`);
|
|
3243
|
+
const resetSeconds = resetAtHeader == null || resetAtHeader === "" ? NaN : Number(resetAtHeader);
|
|
3244
|
+
const resetsAt = Number.isFinite(resetSeconds) ? new Date(resetSeconds * 1e3).toISOString() : null;
|
|
3245
|
+
return { utilization: Math.min(100, Math.max(0, utilization)), windowMinutes, resetsAt };
|
|
3246
|
+
}
|
|
3247
|
+
function parseCodexUsageHeaders(headers) {
|
|
3248
|
+
return {
|
|
3249
|
+
primary: toWindow2(headers, "primary"),
|
|
3250
|
+
secondary: toWindow2(headers, "secondary"),
|
|
3251
|
+
hasCredits: headers.has("x-codex-credits-has-credits") ? headers.get("x-codex-credits-has-credits")?.toLowerCase() === "true" : null,
|
|
3252
|
+
creditsUnlimited: headers.has("x-codex-credits-unlimited") ? headers.get("x-codex-credits-unlimited")?.toLowerCase() === "true" : null
|
|
3253
|
+
};
|
|
3254
|
+
}
|
|
3255
|
+
function normalizeProbeModel(model) {
|
|
3256
|
+
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
3257
|
+
}
|
|
3258
|
+
async function resolveProbeModels(port) {
|
|
3259
|
+
try {
|
|
3260
|
+
const res = await withRequestTimeout(
|
|
3261
|
+
fetch,
|
|
3262
|
+
REQUEST_TIMEOUT_MS
|
|
3263
|
+
)(`${opencodeBase(port)}/config/providers`);
|
|
3264
|
+
if (!res.ok) {
|
|
3265
|
+
console.error(
|
|
3266
|
+
`[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
3267
|
+
);
|
|
3268
|
+
return [];
|
|
3269
|
+
}
|
|
3270
|
+
const body = await res.json();
|
|
3271
|
+
const provider = body?.providers?.find((candidate) => candidate?.id === "openai");
|
|
3272
|
+
if (!provider || !provider.models || typeof provider.models !== "object") return [];
|
|
3273
|
+
const candidates = [
|
|
3274
|
+
...typeof body?.default?.openai === "string" ? [body.default.openai] : [],
|
|
3275
|
+
...Object.keys(provider.models)
|
|
3276
|
+
].map(normalizeProbeModel);
|
|
3277
|
+
return [...new Set(candidates)].slice(0, 4);
|
|
3278
|
+
} catch (err) {
|
|
3279
|
+
console.error(
|
|
3280
|
+
`[resolveProbeModels] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
3281
|
+
);
|
|
3282
|
+
return [];
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
function hasPrimaryHeaders(headers) {
|
|
3286
|
+
return [
|
|
3287
|
+
"x-codex-primary-used-percent",
|
|
3288
|
+
"x-codex-primary-window-minutes",
|
|
3289
|
+
"x-codex-primary-reset-at"
|
|
3290
|
+
].some((name) => headers.has(name));
|
|
3291
|
+
}
|
|
3292
|
+
async function getOpenAiUsage(port) {
|
|
3293
|
+
const credentials2 = readOpenCodeChatGptCredentials();
|
|
3294
|
+
if (!credentials2) {
|
|
3295
|
+
throw new OpenAiUsageError(
|
|
3296
|
+
"No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
|
|
3297
|
+
"no_credentials"
|
|
3298
|
+
);
|
|
3299
|
+
}
|
|
3300
|
+
if (credentials2.expiresAt < Date.now()) {
|
|
3301
|
+
throw new OpenAiUsageError(
|
|
3302
|
+
"ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
|
|
3303
|
+
"credentials_expired"
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
const models = await resolveProbeModels(port);
|
|
3307
|
+
if (models.length === 0) {
|
|
3308
|
+
throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
|
|
3309
|
+
}
|
|
3310
|
+
let lastStatus;
|
|
3311
|
+
for (const model of models) {
|
|
3312
|
+
let res;
|
|
3313
|
+
try {
|
|
3314
|
+
res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
|
|
3315
|
+
method: "POST",
|
|
3316
|
+
headers: {
|
|
3317
|
+
Authorization: `Bearer ${credentials2.accessToken}`,
|
|
3318
|
+
"Content-Type": "application/json"
|
|
3319
|
+
},
|
|
3320
|
+
body: JSON.stringify({ model, store: false, stream: true })
|
|
3321
|
+
});
|
|
3322
|
+
} catch (err) {
|
|
3323
|
+
throw new OpenAiUsageError(
|
|
3324
|
+
`OpenAI usage probe request failed: ${err instanceof Error ? err.message : String(err)}.`,
|
|
3325
|
+
"request_failed"
|
|
3326
|
+
);
|
|
3327
|
+
}
|
|
3328
|
+
try {
|
|
3329
|
+
lastStatus = res.status;
|
|
3330
|
+
if (hasPrimaryHeaders(res.headers)) {
|
|
3331
|
+
const usage = parseCodexUsageHeaders(res.headers);
|
|
3332
|
+
if (!usage.primary && !usage.secondary) {
|
|
3333
|
+
throw new OpenAiUsageError(
|
|
3334
|
+
`OpenAI usage probe returned no usable window (HTTP ${res.status}).`,
|
|
3335
|
+
"no_usable_window"
|
|
3336
|
+
);
|
|
3337
|
+
}
|
|
3338
|
+
return usage;
|
|
3339
|
+
}
|
|
3340
|
+
if (res.status === 401) {
|
|
3341
|
+
throw new OpenAiUsageError(
|
|
3342
|
+
"ChatGPT credentials have expired (HTTP 401).",
|
|
3343
|
+
"credentials_expired"
|
|
3344
|
+
);
|
|
3345
|
+
}
|
|
3346
|
+
if (res.status === 403 || res.status === 429) {
|
|
3347
|
+
throw new OpenAiUsageError(
|
|
3348
|
+
`OpenAI usage probe was blocked (HTTP ${res.status}).`,
|
|
3349
|
+
"probe_blocked"
|
|
3350
|
+
);
|
|
3351
|
+
}
|
|
3352
|
+
} finally {
|
|
3353
|
+
await res.body?.cancel().catch(() => {
|
|
3354
|
+
});
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
throw new OpenAiUsageError(
|
|
3358
|
+
`OpenAI usage probe failed: HTTP ${lastStatus ?? "unknown"}.`,
|
|
3359
|
+
"request_failed"
|
|
3360
|
+
);
|
|
3361
|
+
}
|
|
3362
|
+
|
|
2994
3363
|
// src/lib/reporting-schedule.ts
|
|
2995
3364
|
function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
|
|
2996
3365
|
const jitterRangeMs = baseMs * jitterFraction;
|
|
@@ -2999,6 +3368,31 @@ function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
|
|
|
2999
3368
|
function firstReportDelayMs(random = Math.random) {
|
|
3000
3369
|
return 5e3 + random() * 1e4;
|
|
3001
3370
|
}
|
|
3371
|
+
var VALID_USAGE_REPORTING_MODES = ["auto", "on", "off"];
|
|
3372
|
+
function resolveUsageReportingMode(flagValue, env, names) {
|
|
3373
|
+
const raw = flagValue ?? env[names.envVar];
|
|
3374
|
+
if (raw === void 0 || raw === "") return { mode: "auto", warnings: [] };
|
|
3375
|
+
const normalized = raw.trim().toLowerCase();
|
|
3376
|
+
if (VALID_USAGE_REPORTING_MODES.includes(normalized)) {
|
|
3377
|
+
return { mode: normalized, warnings: [] };
|
|
3378
|
+
}
|
|
3379
|
+
const source = flagValue !== void 0 ? names.flagName : names.envVar;
|
|
3380
|
+
return {
|
|
3381
|
+
mode: "auto",
|
|
3382
|
+
warnings: [
|
|
3383
|
+
`Ignoring invalid ${source} "${raw}": expected one of ${VALID_USAGE_REPORTING_MODES.join(", ")}; using auto`
|
|
3384
|
+
]
|
|
3385
|
+
};
|
|
3386
|
+
}
|
|
3387
|
+
var BASE_USAGE_REPORT_DELAY_MS = 10 * 6e4;
|
|
3388
|
+
var USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
3389
|
+
function usageReportDelayMs(random = Math.random) {
|
|
3390
|
+
return jitteredDelayMs(BASE_USAGE_REPORT_DELAY_MS, USAGE_REPORT_DELAY_JITTER_FRACTION, random);
|
|
3391
|
+
}
|
|
3392
|
+
var USAGE_REPORT_FAILURE_REESCALATION_TICKS = 6;
|
|
3393
|
+
function usageReportFailureLogLevel(consecutiveFailures) {
|
|
3394
|
+
return reportFailureLogLevel(consecutiveFailures, USAGE_REPORT_FAILURE_REESCALATION_TICKS);
|
|
3395
|
+
}
|
|
3002
3396
|
function reportFailureLogLevel(consecutiveFailures, reescalationTicks) {
|
|
3003
3397
|
return consecutiveFailures === 1 || consecutiveFailures % reescalationTicks === 0 ? "warn" : "debug";
|
|
3004
3398
|
}
|
|
@@ -3007,33 +3401,26 @@ function failureStreakSuffix(consecutiveFailures) {
|
|
|
3007
3401
|
}
|
|
3008
3402
|
|
|
3009
3403
|
// src/lib/claude-usage-reporting.ts
|
|
3010
|
-
var VALID_MODES = ["auto", "on", "off"];
|
|
3011
3404
|
function resolveClaudeUsageReportingMode(flagValue, env) {
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
}
|
|
3016
|
-
const normalized = raw.trim().toLowerCase();
|
|
3017
|
-
if (VALID_MODES.includes(normalized)) {
|
|
3018
|
-
return { mode: normalized, warnings: [] };
|
|
3019
|
-
}
|
|
3020
|
-
const source = flagValue !== void 0 ? "--claude-usage-reporting" : "EVIDENT_CLAUDE_USAGE_REPORTING";
|
|
3021
|
-
return {
|
|
3022
|
-
mode: "auto",
|
|
3023
|
-
warnings: [
|
|
3024
|
-
`Ignoring invalid ${source} "${raw}": expected one of ${VALID_MODES.join(", ")}; using auto`
|
|
3025
|
-
]
|
|
3026
|
-
};
|
|
3405
|
+
return resolveUsageReportingMode(flagValue, env, {
|
|
3406
|
+
flagName: "--claude-usage-reporting",
|
|
3407
|
+
envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
3408
|
+
});
|
|
3027
3409
|
}
|
|
3028
|
-
var BASE_REPORT_DELAY_MS = 10 * 6e4;
|
|
3029
|
-
var REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
3030
3410
|
function nextReportDelayMs(random = Math.random) {
|
|
3031
|
-
return
|
|
3411
|
+
return usageReportDelayMs(random);
|
|
3032
3412
|
}
|
|
3033
3413
|
var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
|
|
3034
|
-
var CLAUDE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
3035
3414
|
function claudeUsageFailureLogLevel(consecutiveFailures) {
|
|
3036
|
-
return
|
|
3415
|
+
return usageReportFailureLogLevel(consecutiveFailures);
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
// src/lib/openai-usage-reporting.ts
|
|
3419
|
+
function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
3420
|
+
return resolveUsageReportingMode(flagValue, env, {
|
|
3421
|
+
flagName: "--openai-usage-reporting",
|
|
3422
|
+
envVar: "EVIDENT_OPENAI_USAGE_REPORTING"
|
|
3423
|
+
});
|
|
3037
3424
|
}
|
|
3038
3425
|
|
|
3039
3426
|
// src/lib/resource-usage-reporting.ts
|
|
@@ -3192,15 +3579,15 @@ function createResourceUsageCollector(homeDir) {
|
|
|
3192
3579
|
}
|
|
3193
3580
|
|
|
3194
3581
|
// src/lib/channels/driver.ts
|
|
3195
|
-
import { homedir as
|
|
3582
|
+
import { homedir as homedir3 } from "os";
|
|
3196
3583
|
|
|
3197
3584
|
// src/lib/runner-file-sync.ts
|
|
3198
|
-
import { join as
|
|
3585
|
+
import { join as join6 } from "path";
|
|
3199
3586
|
|
|
3200
3587
|
// src/lib/file-push.ts
|
|
3201
3588
|
import { randomUUID } from "crypto";
|
|
3202
3589
|
import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
|
|
3203
|
-
import { basename, dirname as dirname3, isAbsolute, join as
|
|
3590
|
+
import { basename, dirname as dirname3, isAbsolute, join as join5, relative, resolve as resolve2, sep } from "path";
|
|
3204
3591
|
var FILE_MODE = 384;
|
|
3205
3592
|
var DIRECTORY_MODE = 448;
|
|
3206
3593
|
async function writePushedFile(request) {
|
|
@@ -3233,7 +3620,7 @@ async function writePushedFile(request) {
|
|
|
3233
3620
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
3234
3621
|
dirname3(candidate)
|
|
3235
3622
|
);
|
|
3236
|
-
const realTarget =
|
|
3623
|
+
const realTarget = join5(existingAncestor, ...missingSegments, basename(candidate));
|
|
3237
3624
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
3238
3625
|
if (allowedDirectory === null) {
|
|
3239
3626
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -3269,7 +3656,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
3269
3656
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
3270
3657
|
return null;
|
|
3271
3658
|
}
|
|
3272
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
3659
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join5(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
3273
3660
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
3274
3661
|
return null;
|
|
3275
3662
|
}
|
|
@@ -3342,13 +3729,13 @@ function contains(realDirectory, realTarget) {
|
|
|
3342
3729
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
3343
3730
|
let current = existingAncestor;
|
|
3344
3731
|
for (const segment of missingSegments) {
|
|
3345
|
-
current =
|
|
3732
|
+
current = join5(current, segment);
|
|
3346
3733
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
3347
3734
|
await chmod(current, DIRECTORY_MODE);
|
|
3348
3735
|
}
|
|
3349
3736
|
}
|
|
3350
3737
|
async function writeAtomically(realTarget, content) {
|
|
3351
|
-
const temporaryPath =
|
|
3738
|
+
const temporaryPath = join5(dirname3(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
3352
3739
|
let handle;
|
|
3353
3740
|
try {
|
|
3354
3741
|
handle = await open2(temporaryPath, "wx", FILE_MODE);
|
|
@@ -3390,20 +3777,28 @@ async function syncPendingRunnerFiles(options) {
|
|
|
3390
3777
|
for (const id of options.ackFailures.keys()) {
|
|
3391
3778
|
if (!pendingIds.has(id)) options.ackFailures.delete(id);
|
|
3392
3779
|
}
|
|
3393
|
-
if (pending.length === 0)
|
|
3780
|
+
if (pending.length === 0) {
|
|
3781
|
+
return { applied: 0, claudeCredentialApplied: false, opencodeAuthApplied: false };
|
|
3782
|
+
}
|
|
3394
3783
|
options.log({
|
|
3395
3784
|
level: "info",
|
|
3396
3785
|
message: `Runner file sync: ${pending.length} file(s) queued for this runner`
|
|
3397
3786
|
});
|
|
3398
3787
|
let applied = 0;
|
|
3399
3788
|
let claudeCredentialApplied = false;
|
|
3789
|
+
let opencodeAuthApplied = false;
|
|
3400
3790
|
for (const file of pending) {
|
|
3401
3791
|
if ((options.ackFailures.get(file.id) ?? 0) >= MAX_ACK_ATTEMPTS) continue;
|
|
3402
3792
|
const outcome = await applyOne(options, file);
|
|
3403
3793
|
if (outcome.applied) applied += 1;
|
|
3404
3794
|
if (outcome.claudeCredentialApplied) claudeCredentialApplied = true;
|
|
3795
|
+
if (outcome.opencodeAuthApplied) opencodeAuthApplied = true;
|
|
3405
3796
|
}
|
|
3406
|
-
return {
|
|
3797
|
+
return {
|
|
3798
|
+
applied,
|
|
3799
|
+
claudeCredentialApplied,
|
|
3800
|
+
opencodeAuthApplied
|
|
3801
|
+
};
|
|
3407
3802
|
}
|
|
3408
3803
|
async function listPendingFiles(options) {
|
|
3409
3804
|
let res;
|
|
@@ -3464,10 +3859,18 @@ function asPendingFile(entry) {
|
|
|
3464
3859
|
if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
|
|
3465
3860
|
return { id, path, size };
|
|
3466
3861
|
}
|
|
3467
|
-
var NOT_APPLIED = {
|
|
3862
|
+
var NOT_APPLIED = {
|
|
3863
|
+
applied: false,
|
|
3864
|
+
claudeCredentialApplied: false,
|
|
3865
|
+
opencodeAuthApplied: false
|
|
3866
|
+
};
|
|
3468
3867
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
3469
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
3470
|
-
return expanded ===
|
|
3868
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
3869
|
+
return expanded === join6(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
3870
|
+
}
|
|
3871
|
+
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
3872
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
3873
|
+
return expanded === join6(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
3471
3874
|
}
|
|
3472
3875
|
async function applyOne(options, file) {
|
|
3473
3876
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -3523,7 +3926,8 @@ async function applyOne(options, file) {
|
|
|
3523
3926
|
await ack(options, file, "applied");
|
|
3524
3927
|
return {
|
|
3525
3928
|
applied: true,
|
|
3526
|
-
claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir)
|
|
3929
|
+
claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir),
|
|
3930
|
+
opencodeAuthApplied: isOpenCodeAuthPath(file.path, options.homeDir)
|
|
3527
3931
|
};
|
|
3528
3932
|
}
|
|
3529
3933
|
function durableDownloadCode(status2) {
|
|
@@ -3977,6 +4381,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3977
4381
|
* that way rather than "fixing" it into a count.
|
|
3978
4382
|
*/
|
|
3979
4383
|
claudeCredentialApplyCount = 0;
|
|
4384
|
+
opencodeAuthApplyCount = 0;
|
|
3980
4385
|
/**
|
|
3981
4386
|
* The currently-executing `drainPending()` promise, or null when idle. Lets a
|
|
3982
4387
|
* graceful shutdown (`waitForInFlight`) await an in-progress drain so a turn it
|
|
@@ -4011,7 +4416,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4011
4416
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
4012
4417
|
this.now = config.now ?? (() => Date.now());
|
|
4013
4418
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
4014
|
-
this.homeDir = config.homeDir ??
|
|
4419
|
+
this.homeDir = config.homeDir ?? homedir3();
|
|
4015
4420
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
4016
4421
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
4017
4422
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
@@ -4081,6 +4486,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4081
4486
|
});
|
|
4082
4487
|
this.appliedFileCount += result.applied;
|
|
4083
4488
|
if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;
|
|
4489
|
+
if (result.opencodeAuthApplied) this.opencodeAuthApplyCount += 1;
|
|
4084
4490
|
return result.applied;
|
|
4085
4491
|
} catch (err) {
|
|
4086
4492
|
this.log({
|
|
@@ -4188,7 +4594,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4188
4594
|
return {
|
|
4189
4595
|
appliedFiles: this.appliedFileCount,
|
|
4190
4596
|
inFlight: this.syncingFiles,
|
|
4191
|
-
claudeCredentialApplies: this.claudeCredentialApplyCount
|
|
4597
|
+
claudeCredentialApplies: this.claudeCredentialApplyCount,
|
|
4598
|
+
opencodeAuthApplyCount: this.opencodeAuthApplyCount
|
|
4192
4599
|
};
|
|
4193
4600
|
}
|
|
4194
4601
|
/**
|
|
@@ -7525,7 +7932,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
7525
7932
|
if (trimmed === "") {
|
|
7526
7933
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
7527
7934
|
}
|
|
7528
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
7935
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join7(homeDir, trimmed.slice(2)) : trimmed;
|
|
7529
7936
|
if (!isAbsolute2(expanded)) {
|
|
7530
7937
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
7531
7938
|
}
|
|
@@ -7623,7 +8030,7 @@ function logActivity(state, entry) {
|
|
|
7623
8030
|
const level = entry.level ?? (entry.type === "error" ? "error" : "info");
|
|
7624
8031
|
if (!meetsThreshold(state, level)) return;
|
|
7625
8032
|
forwardRunnerActivity(
|
|
7626
|
-
{ level, message: entry.message, error: entry.error },
|
|
8033
|
+
{ level, message: entry.message, error: entry.error, metadata: entry.metadata },
|
|
7627
8034
|
{ agentId: state.agentId, authHeader: state.authHeader }
|
|
7628
8035
|
);
|
|
7629
8036
|
const fullEntry = {
|
|
@@ -7643,6 +8050,26 @@ function logActivity(state, entry) {
|
|
|
7643
8050
|
}
|
|
7644
8051
|
}
|
|
7645
8052
|
}
|
|
8053
|
+
function reportSessionDbRecovery(state) {
|
|
8054
|
+
try {
|
|
8055
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir4(), env: process.env });
|
|
8056
|
+
for (const record of report.records) {
|
|
8057
|
+
const activity = buildSessionDbRecoveryActivity(record);
|
|
8058
|
+
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
8059
|
+
logActivity(state, {
|
|
8060
|
+
type: activity.level === "error" ? "error" : "info",
|
|
8061
|
+
level: activity.level,
|
|
8062
|
+
...activity.level === "error" ? { error: activity.message } : { message: activity.message },
|
|
8063
|
+
metadata: activity.metadata
|
|
8064
|
+
});
|
|
8065
|
+
}
|
|
8066
|
+
acknowledgeSessionDbRecoveryReport(report.path);
|
|
8067
|
+
} catch (error2) {
|
|
8068
|
+
console.error(
|
|
8069
|
+
`[run] could not report session-DB recovery activity: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
8070
|
+
);
|
|
8071
|
+
}
|
|
8072
|
+
}
|
|
7646
8073
|
function displayStatus(state) {
|
|
7647
8074
|
if (!state.interactive) return;
|
|
7648
8075
|
const attempt = state.connection?.reconnectAttempt ?? 0;
|
|
@@ -7733,6 +8160,7 @@ async function driveChannels(state, driver) {
|
|
|
7733
8160
|
let lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
|
|
7734
8161
|
let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
|
|
7735
8162
|
let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
|
|
8163
|
+
let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
|
|
7736
8164
|
while (state.running) {
|
|
7737
8165
|
const cycleStartedAtMs = performance.now();
|
|
7738
8166
|
let idleThisCycle = false;
|
|
@@ -7765,6 +8193,10 @@ async function driveChannels(state, driver) {
|
|
|
7765
8193
|
const claudeCredentialApplied = claudeCredentialApplies !== lastSeenClaudeApplies;
|
|
7766
8194
|
lastSeenClaudeApplies = claudeCredentialApplies;
|
|
7767
8195
|
if (claudeCredentialApplied) state.claudeUsageRearm?.();
|
|
8196
|
+
const opencodeAuthApplies = fileActivitySnapshot.opencodeAuthApplyCount;
|
|
8197
|
+
const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
|
|
8198
|
+
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
8199
|
+
if (opencodeAuthApplied) state.openaiUsageRearm?.();
|
|
7768
8200
|
if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
|
|
7769
8201
|
idlePolls = 0;
|
|
7770
8202
|
idleMs = 0;
|
|
@@ -7833,7 +8265,7 @@ async function driveChannels(state, driver) {
|
|
|
7833
8265
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
7834
8266
|
var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
|
|
7835
8267
|
function sessionDbPath() {
|
|
7836
|
-
return
|
|
8268
|
+
return join7(homedir4(), ".local", "share", "opencode", "opencode.db");
|
|
7837
8269
|
}
|
|
7838
8270
|
async function runSweep(state, driver, config) {
|
|
7839
8271
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
@@ -7916,7 +8348,7 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
7916
8348
|
for (const warning2 of config.warnings) {
|
|
7917
8349
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
7918
8350
|
}
|
|
7919
|
-
const dbBytes = statSessionDbBytes(
|
|
8351
|
+
const dbBytes = statSessionDbBytes(homedir4());
|
|
7920
8352
|
void (async () => {
|
|
7921
8353
|
const reclaimSkipReason = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
7922
8354
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
@@ -7944,23 +8376,20 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
7944
8376
|
);
|
|
7945
8377
|
state.sessionCleanupTimers.push(interval, firstSweep);
|
|
7946
8378
|
}
|
|
7947
|
-
function
|
|
7948
|
-
const { mode, warnings } =
|
|
7949
|
-
options.claudeUsageReporting,
|
|
7950
|
-
process.env
|
|
7951
|
-
);
|
|
8379
|
+
function scheduleUsageReporting(state, params) {
|
|
8380
|
+
const { mode, warnings } = params.resolved;
|
|
7952
8381
|
for (const warning2 of warnings) {
|
|
7953
8382
|
logActivity(state, {
|
|
7954
8383
|
type: "info",
|
|
7955
8384
|
level: "warn",
|
|
7956
|
-
message:
|
|
8385
|
+
message: `${params.label} usage reporting: ${warning2}`
|
|
7957
8386
|
});
|
|
7958
8387
|
}
|
|
7959
8388
|
if (mode === "off") {
|
|
7960
8389
|
logActivity(state, {
|
|
7961
8390
|
type: "info",
|
|
7962
8391
|
level: "debug",
|
|
7963
|
-
message:
|
|
8392
|
+
message: `${params.label} usage reporting is off (${params.offFlagHint})`
|
|
7964
8393
|
});
|
|
7965
8394
|
return null;
|
|
7966
8395
|
}
|
|
@@ -7969,7 +8398,7 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
7969
8398
|
let rearmRequested = false;
|
|
7970
8399
|
const armProbe = () => {
|
|
7971
8400
|
phase = "probe-pending";
|
|
7972
|
-
|
|
8401
|
+
params.setTimer(setTimeout(() => void tick(true), params.firstDelayMs()));
|
|
7973
8402
|
};
|
|
7974
8403
|
const scheduleNextTick = () => {
|
|
7975
8404
|
if (rearmRequested) {
|
|
@@ -7978,7 +8407,7 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
7978
8407
|
return;
|
|
7979
8408
|
}
|
|
7980
8409
|
phase = "steady-pending";
|
|
7981
|
-
|
|
8410
|
+
params.setTimer(setTimeout(() => void tick(false), params.nextDelayMs()));
|
|
7982
8411
|
};
|
|
7983
8412
|
const rearm = () => {
|
|
7984
8413
|
switch (phase) {
|
|
@@ -7988,9 +8417,9 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
7988
8417
|
case "probe-pending":
|
|
7989
8418
|
return;
|
|
7990
8419
|
case "steady-pending":
|
|
7991
|
-
if (
|
|
7992
|
-
clearTimeout(
|
|
7993
|
-
|
|
8420
|
+
if (params.getTimer()) {
|
|
8421
|
+
clearTimeout(params.getTimer());
|
|
8422
|
+
params.setTimer(null);
|
|
7994
8423
|
}
|
|
7995
8424
|
rearmRequested = false;
|
|
7996
8425
|
armProbe();
|
|
@@ -8004,45 +8433,45 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
8004
8433
|
const tick = async (isProbe) => {
|
|
8005
8434
|
phase = "tick-in-flight";
|
|
8006
8435
|
try {
|
|
8007
|
-
const usage = await
|
|
8008
|
-
const result = await
|
|
8436
|
+
const usage = await params.fetchUsage();
|
|
8437
|
+
const result = await params.report(usage);
|
|
8009
8438
|
if (result.ok) {
|
|
8010
8439
|
if (consecutiveFailures > 0) {
|
|
8011
8440
|
logActivity(state, {
|
|
8012
8441
|
type: "info",
|
|
8013
8442
|
level: "info",
|
|
8014
|
-
message:
|
|
8443
|
+
message: `${params.label} usage reporting recovered`
|
|
8015
8444
|
});
|
|
8016
8445
|
}
|
|
8017
8446
|
consecutiveFailures = 0;
|
|
8018
8447
|
logActivity(state, {
|
|
8019
8448
|
type: "info",
|
|
8020
8449
|
level: "debug",
|
|
8021
|
-
message:
|
|
8450
|
+
message: `Reported ${params.label} usage to Evident`
|
|
8022
8451
|
});
|
|
8023
8452
|
} else {
|
|
8024
8453
|
consecutiveFailures++;
|
|
8025
8454
|
logActivity(state, {
|
|
8026
8455
|
type: "info",
|
|
8027
|
-
level:
|
|
8028
|
-
message: `Failed to report
|
|
8456
|
+
level: params.failureLogLevel(consecutiveFailures),
|
|
8457
|
+
message: `Failed to report ${params.label} usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
|
|
8029
8458
|
});
|
|
8030
8459
|
}
|
|
8031
8460
|
scheduleNextTick();
|
|
8032
8461
|
} catch (error2) {
|
|
8033
|
-
if (
|
|
8462
|
+
if (params.isLocalCredentialProblem(error2)) {
|
|
8034
8463
|
if (mode === "on") {
|
|
8035
8464
|
logActivity(state, {
|
|
8036
8465
|
type: "info",
|
|
8037
8466
|
level: "warn",
|
|
8038
|
-
message:
|
|
8467
|
+
message: `${params.label} usage reporting is forced on but no usable login was found \u2014 ${params.forcedOnHint}; reporting will keep retrying`
|
|
8039
8468
|
});
|
|
8040
8469
|
scheduleNextTick();
|
|
8041
8470
|
} else if (isProbe) {
|
|
8042
8471
|
logActivity(state, {
|
|
8043
8472
|
type: "info",
|
|
8044
8473
|
level: "debug",
|
|
8045
|
-
message:
|
|
8474
|
+
message: `${params.label} usage reporting: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
8046
8475
|
});
|
|
8047
8476
|
phase = "dormant";
|
|
8048
8477
|
if (rearmRequested) rearm();
|
|
@@ -8050,7 +8479,7 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
8050
8479
|
logActivity(state, {
|
|
8051
8480
|
type: "info",
|
|
8052
8481
|
level: "debug",
|
|
8053
|
-
message:
|
|
8482
|
+
message: `${params.label} usage reporting: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
8054
8483
|
});
|
|
8055
8484
|
scheduleNextTick();
|
|
8056
8485
|
}
|
|
@@ -8059,8 +8488,8 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
8059
8488
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
8060
8489
|
logActivity(state, {
|
|
8061
8490
|
type: "info",
|
|
8062
|
-
level:
|
|
8063
|
-
message:
|
|
8491
|
+
level: params.failureLogLevel(consecutiveFailures),
|
|
8492
|
+
message: `${params.label} usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
|
|
8064
8493
|
});
|
|
8065
8494
|
scheduleNextTick();
|
|
8066
8495
|
}
|
|
@@ -8069,6 +8498,24 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
8069
8498
|
armProbe();
|
|
8070
8499
|
return rearm;
|
|
8071
8500
|
}
|
|
8501
|
+
function scheduleClaudeUsageReporting(state, options) {
|
|
8502
|
+
return scheduleUsageReporting(state, {
|
|
8503
|
+
label: "Claude",
|
|
8504
|
+
resolved: resolveClaudeUsageReportingMode(options.claudeUsageReporting, process.env),
|
|
8505
|
+
offFlagHint: "--claude-usage-reporting off",
|
|
8506
|
+
getTimer: () => state.claudeUsageTimer,
|
|
8507
|
+
setTimer: (timer) => {
|
|
8508
|
+
state.claudeUsageTimer = timer;
|
|
8509
|
+
},
|
|
8510
|
+
fetchUsage: getClaudeUsage,
|
|
8511
|
+
report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
|
|
8512
|
+
isLocalCredentialProblem,
|
|
8513
|
+
forcedOnHint: "run `claude` to sign in",
|
|
8514
|
+
firstDelayMs: () => FIRST_REPORT_DELAY_MS,
|
|
8515
|
+
nextDelayMs: nextReportDelayMs,
|
|
8516
|
+
failureLogLevel: claudeUsageFailureLogLevel
|
|
8517
|
+
});
|
|
8518
|
+
}
|
|
8072
8519
|
var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
|
|
8073
8520
|
var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
8074
8521
|
var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
@@ -8092,7 +8539,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
8092
8539
|
});
|
|
8093
8540
|
return;
|
|
8094
8541
|
}
|
|
8095
|
-
const collect = createResourceUsageCollector(
|
|
8542
|
+
const collect = createResourceUsageCollector(homedir4());
|
|
8096
8543
|
let consecutiveFailures = 0;
|
|
8097
8544
|
const tick = async () => {
|
|
8098
8545
|
try {
|
|
@@ -8193,6 +8640,11 @@ async function cleanup(state, opts = {}) {
|
|
|
8193
8640
|
state.claudeUsageTimer = null;
|
|
8194
8641
|
}
|
|
8195
8642
|
state.claudeUsageRearm = null;
|
|
8643
|
+
if (state.openaiUsageTimer) {
|
|
8644
|
+
clearTimeout(state.openaiUsageTimer);
|
|
8645
|
+
state.openaiUsageTimer = null;
|
|
8646
|
+
}
|
|
8647
|
+
state.openaiUsageRearm = null;
|
|
8196
8648
|
if (state.resourceUsageTimer) {
|
|
8197
8649
|
clearTimeout(state.resourceUsageTimer);
|
|
8198
8650
|
state.resourceUsageTimer = null;
|
|
@@ -8244,7 +8696,7 @@ async function run(options) {
|
|
|
8244
8696
|
let fileSyncDirectories;
|
|
8245
8697
|
try {
|
|
8246
8698
|
logLevel = resolveLogLevel(options);
|
|
8247
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
8699
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir4());
|
|
8248
8700
|
} catch (error2) {
|
|
8249
8701
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
8250
8702
|
if (options.json) {
|
|
@@ -8279,6 +8731,8 @@ async function run(options) {
|
|
|
8279
8731
|
sessionCleanupTimers: [],
|
|
8280
8732
|
claudeUsageTimer: null,
|
|
8281
8733
|
claudeUsageRearm: null,
|
|
8734
|
+
openaiUsageTimer: null,
|
|
8735
|
+
openaiUsageRearm: null,
|
|
8282
8736
|
resourceUsageTimer: null,
|
|
8283
8737
|
authHeader: ""
|
|
8284
8738
|
};
|
|
@@ -8474,6 +8928,7 @@ async function run(options) {
|
|
|
8474
8928
|
} else {
|
|
8475
8929
|
log2(state, "Not running in a MicroVM (MICROVM_ID unset) \u2014 nothing to report", "debug");
|
|
8476
8930
|
}
|
|
8931
|
+
reportSessionDbRecovery(state);
|
|
8477
8932
|
const { timeoutMs: opencodeStartTimeoutMs, warnings: opencodeStartTimeoutWarnings } = resolveOpenCodeStartTimeoutMs(options, process.env);
|
|
8478
8933
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
8479
8934
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
@@ -8541,7 +8996,7 @@ async function run(options) {
|
|
|
8541
8996
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
8542
8997
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
8543
8998
|
fileSyncDirectories,
|
|
8544
|
-
homeDir:
|
|
8999
|
+
homeDir: homedir4(),
|
|
8545
9000
|
maxActiveSessions,
|
|
8546
9001
|
log: (entry) => (
|
|
8547
9002
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -8679,6 +9134,22 @@ async function run(options) {
|
|
|
8679
9134
|
}
|
|
8680
9135
|
scheduleSessionCleanup(state, channelDriver, options);
|
|
8681
9136
|
state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
|
|
9137
|
+
state.openaiUsageRearm = scheduleUsageReporting(state, {
|
|
9138
|
+
label: "OpenAI",
|
|
9139
|
+
resolved: resolveOpenAiUsageReportingMode(options.openaiUsageReporting, process.env),
|
|
9140
|
+
offFlagHint: "--openai-usage-reporting off",
|
|
9141
|
+
getTimer: () => state.openaiUsageTimer,
|
|
9142
|
+
setTimer: (timer) => {
|
|
9143
|
+
state.openaiUsageTimer = timer;
|
|
9144
|
+
},
|
|
9145
|
+
fetchUsage: () => getOpenAiUsage(state.port),
|
|
9146
|
+
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
9147
|
+
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
9148
|
+
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
9149
|
+
firstDelayMs: firstReportDelayMs,
|
|
9150
|
+
nextDelayMs: usageReportDelayMs,
|
|
9151
|
+
failureLogLevel: usageReportFailureLogLevel
|
|
9152
|
+
});
|
|
8682
9153
|
scheduleResourceUsageReporting(state, options);
|
|
8683
9154
|
if (!interactive || state.json) {
|
|
8684
9155
|
log2(state, "Driving channel messages...");
|
|
@@ -8760,6 +9231,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
8760
9231
|
).option(
|
|
8761
9232
|
"--claude-usage-reporting <mode>",
|
|
8762
9233
|
"Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
9234
|
+
).option(
|
|
9235
|
+
"--openai-usage-reporting <mode>",
|
|
9236
|
+
"Report OpenAI plan usage to Evident: auto | on | off (default: auto). Env: EVIDENT_OPENAI_USAGE_REPORTING"
|
|
8763
9237
|
).option(
|
|
8764
9238
|
"--no-resource-usage-reporting",
|
|
8765
9239
|
"Don't report this machine's CPU and memory usage to Evident (reporting is on by default). Env: EVIDENT_RESOURCE_USAGE_REPORTING=off"
|
|
@@ -8795,6 +9269,7 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
8795
9269
|
// Raw string — the resolver in run.ts single-sources parsing
|
|
8796
9270
|
// (resolveClaudeUsageReportingMode).
|
|
8797
9271
|
claudeUsageReporting: options.claudeUsageReporting,
|
|
9272
|
+
openaiUsageReporting: options.openaiUsageReporting,
|
|
8798
9273
|
// Raw value — resolution is single-sourced in run.ts's
|
|
8799
9274
|
// resolveResourceUsageReportingEnabled.
|
|
8800
9275
|
resourceUsageReporting: options.resourceUsageReporting,
|