@evident-ai/cli 3.4.0 → 3.4.1-dev.1549524
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 +14 -1
- package/dist/index.js +786 -98
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -722,6 +722,14 @@ function toReportedWindow(window) {
|
|
|
722
722
|
if (!window) return null;
|
|
723
723
|
return { utilization: window.utilization, resets_at: window.resetsAt };
|
|
724
724
|
}
|
|
725
|
+
function toReportedOwner(snapshot) {
|
|
726
|
+
if (!snapshot.owner) return null;
|
|
727
|
+
return {
|
|
728
|
+
email: snapshot.owner.email,
|
|
729
|
+
organization_name: snapshot.owner.organizationName,
|
|
730
|
+
rate_limit_tier: snapshot.owner.rateLimitTier
|
|
731
|
+
};
|
|
732
|
+
}
|
|
725
733
|
async function reportClaudeUsage(agentId, authHeader, snapshot) {
|
|
726
734
|
try {
|
|
727
735
|
const apiUrl = getApiUrlConfig();
|
|
@@ -730,7 +738,42 @@ async function reportClaudeUsage(agentId, authHeader, snapshot) {
|
|
|
730
738
|
headers: { Authorization: authHeader, "Content-Type": "application/json" },
|
|
731
739
|
body: JSON.stringify({
|
|
732
740
|
five_hour: toReportedWindow(snapshot.fiveHour),
|
|
733
|
-
seven_day: toReportedWindow(snapshot.sevenDay)
|
|
741
|
+
seven_day: toReportedWindow(snapshot.sevenDay),
|
|
742
|
+
owner: toReportedOwner(snapshot)
|
|
743
|
+
}),
|
|
744
|
+
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
745
|
+
});
|
|
746
|
+
if (!response.ok) {
|
|
747
|
+
const serverMessage = await readErrorMessage(response);
|
|
748
|
+
return {
|
|
749
|
+
ok: false,
|
|
750
|
+
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
return { ok: true };
|
|
754
|
+
} catch (error2) {
|
|
755
|
+
return { ok: false, error: describeBestEffortError(error2) };
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function toReportedOpenAiWindow(window) {
|
|
759
|
+
if (!window) return null;
|
|
760
|
+
return {
|
|
761
|
+
utilization: window.utilization,
|
|
762
|
+
window_minutes: window.windowMinutes,
|
|
763
|
+
resets_at: window.resetsAt
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
async function reportOpenAiUsage(agentId, authHeader, snapshot) {
|
|
767
|
+
try {
|
|
768
|
+
const apiUrl = getApiUrlConfig();
|
|
769
|
+
const response = await fetch(`${apiUrl}/runners/${agentId}/openai-usage`, {
|
|
770
|
+
method: "POST",
|
|
771
|
+
headers: { Authorization: authHeader, "Content-Type": "application/json" },
|
|
772
|
+
body: JSON.stringify({
|
|
773
|
+
primary: toReportedOpenAiWindow(snapshot.primary),
|
|
774
|
+
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
775
|
+
has_credits: snapshot.hasCredits,
|
|
776
|
+
credits_unlimited: snapshot.creditsUnlimited
|
|
734
777
|
}),
|
|
735
778
|
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
736
779
|
});
|
|
@@ -962,7 +1005,10 @@ import { readFileSync } from "fs";
|
|
|
962
1005
|
import { homedir } from "os";
|
|
963
1006
|
import { join } from "path";
|
|
964
1007
|
var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
1008
|
+
var CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
1009
|
+
var CLAUDE_PROFILE_TIMEOUT_MS = 2e3;
|
|
965
1010
|
var KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
1011
|
+
var cachedOwner = null;
|
|
966
1012
|
var CLAUDE_CREDENTIALS_SEGMENTS = [".claude", ".credentials.json"];
|
|
967
1013
|
function parseClaudeCliCredentials(raw) {
|
|
968
1014
|
let parsed;
|
|
@@ -1036,6 +1082,47 @@ function toWindow(value) {
|
|
|
1036
1082
|
}
|
|
1037
1083
|
return { utilization: window.utilization, resetsAt };
|
|
1038
1084
|
}
|
|
1085
|
+
function ownerLookupFailure(error2) {
|
|
1086
|
+
const name = error2?.name;
|
|
1087
|
+
return name === "TimeoutError" || name === "AbortError" ? "timed out" : "request failed";
|
|
1088
|
+
}
|
|
1089
|
+
async function getClaudeUsageOwner(accessToken) {
|
|
1090
|
+
if (cachedOwner?.accessToken === accessToken) {
|
|
1091
|
+
return { owner: cachedOwner.owner, ownerLookupError: null };
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
const response = await fetch(CLAUDE_PROFILE_URL, {
|
|
1095
|
+
headers: {
|
|
1096
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1097
|
+
"Content-Type": "application/json",
|
|
1098
|
+
"anthropic-version": "2023-06-01"
|
|
1099
|
+
},
|
|
1100
|
+
signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
|
|
1101
|
+
});
|
|
1102
|
+
if (!response.ok) {
|
|
1103
|
+
return { owner: null, ownerLookupError: `HTTP ${response.status}` };
|
|
1104
|
+
}
|
|
1105
|
+
let body;
|
|
1106
|
+
try {
|
|
1107
|
+
body = await response.json();
|
|
1108
|
+
} catch (error2) {
|
|
1109
|
+
return { owner: null, ownerLookupError: "malformed response" };
|
|
1110
|
+
}
|
|
1111
|
+
const profile = body;
|
|
1112
|
+
if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
|
|
1113
|
+
return { owner: null, ownerLookupError: "malformed response" };
|
|
1114
|
+
}
|
|
1115
|
+
const owner = {
|
|
1116
|
+
email: profile.account.email,
|
|
1117
|
+
organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
|
|
1118
|
+
rateLimitTier: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
|
|
1119
|
+
};
|
|
1120
|
+
cachedOwner = { accessToken, owner };
|
|
1121
|
+
return { owner, ownerLookupError: null };
|
|
1122
|
+
} catch (error2) {
|
|
1123
|
+
return { owner: null, ownerLookupError: ownerLookupFailure(error2) };
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1039
1126
|
async function getClaudeUsage() {
|
|
1040
1127
|
const credentials2 = readClaudeCliCredentials();
|
|
1041
1128
|
if (!credentials2) {
|
|
@@ -1055,15 +1142,19 @@ async function getClaudeUsage() {
|
|
|
1055
1142
|
Authorization: `Bearer ${credentials2.accessToken}`,
|
|
1056
1143
|
"Content-Type": "application/json",
|
|
1057
1144
|
"anthropic-version": "2023-06-01"
|
|
1058
|
-
}
|
|
1145
|
+
},
|
|
1146
|
+
signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
|
|
1059
1147
|
});
|
|
1060
1148
|
if (!res.ok) {
|
|
1061
1149
|
throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
|
|
1062
1150
|
}
|
|
1063
1151
|
const body = await res.json();
|
|
1152
|
+
const { owner, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
|
|
1064
1153
|
return {
|
|
1065
1154
|
fiveHour: toWindow(body.five_hour),
|
|
1066
|
-
sevenDay: toWindow(body.seven_day)
|
|
1155
|
+
sevenDay: toWindow(body.seven_day),
|
|
1156
|
+
owner,
|
|
1157
|
+
ownerLookupError
|
|
1067
1158
|
};
|
|
1068
1159
|
}
|
|
1069
1160
|
|
|
@@ -1092,8 +1183,8 @@ async function claudeUsage() {
|
|
|
1092
1183
|
}
|
|
1093
1184
|
|
|
1094
1185
|
// src/commands/run.ts
|
|
1095
|
-
import { homedir as
|
|
1096
|
-
import { isAbsolute as isAbsolute2, join as
|
|
1186
|
+
import { homedir as homedir4 } from "os";
|
|
1187
|
+
import { isAbsolute as isAbsolute2, join as join7, parse, resolve as resolvePath } from "path";
|
|
1097
1188
|
import chalk6 from "chalk";
|
|
1098
1189
|
|
|
1099
1190
|
// ../../packages/types/src/agents/index.ts
|
|
@@ -1326,6 +1417,8 @@ var SEVERITY_BY_LEVEL = {
|
|
|
1326
1417
|
error: "error"
|
|
1327
1418
|
};
|
|
1328
1419
|
var MAX_MESSAGE_LENGTH = 500;
|
|
1420
|
+
var MAX_METADATA_VALUE_LENGTH = 200;
|
|
1421
|
+
var MAX_METADATA_ENTRIES = 20;
|
|
1329
1422
|
var TRUNCATION_MARKER = "\u2026";
|
|
1330
1423
|
function redact(message) {
|
|
1331
1424
|
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 +1427,24 @@ function truncate(message) {
|
|
|
1334
1427
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
1335
1428
|
return message.slice(0, MAX_MESSAGE_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
|
|
1336
1429
|
}
|
|
1430
|
+
function sanitiseMetadata(metadata) {
|
|
1431
|
+
if (!metadata) return {};
|
|
1432
|
+
const entries = Object.entries(metadata).slice(0, MAX_METADATA_ENTRIES);
|
|
1433
|
+
if (Object.keys(metadata).length > entries.length) {
|
|
1434
|
+
console.error(
|
|
1435
|
+
`[runner-activity-telemetry] dropped ${Object.keys(metadata).length - entries.length} metadata entries`
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
const sanitised = [];
|
|
1439
|
+
for (const [key, value] of entries) {
|
|
1440
|
+
if (typeof value === "string") {
|
|
1441
|
+
sanitised.push([key, truncate(redact(value).slice(0, MAX_METADATA_VALUE_LENGTH))]);
|
|
1442
|
+
} else if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
|
|
1443
|
+
sanitised.push([key, value]);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
return Object.fromEntries(sanitised);
|
|
1447
|
+
}
|
|
1337
1448
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1338
1449
|
var RATE_LIMIT_MAX_EVENTS = 30;
|
|
1339
1450
|
var windowStartedAt = 0;
|
|
@@ -1372,7 +1483,7 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1372
1483
|
logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
|
|
1373
1484
|
severity: SEVERITY_BY_LEVEL[entry.level],
|
|
1374
1485
|
message,
|
|
1375
|
-
metadata: { source: "cli.run" },
|
|
1486
|
+
metadata: { ...sanitiseMetadata(entry.metadata), source: "cli.run" },
|
|
1376
1487
|
agentId: context.agentId
|
|
1377
1488
|
});
|
|
1378
1489
|
} catch (err) {
|
|
@@ -1382,6 +1493,150 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1382
1493
|
}
|
|
1383
1494
|
}
|
|
1384
1495
|
|
|
1496
|
+
// src/lib/opencode/session-db-recovery-report.ts
|
|
1497
|
+
import { readFileSync as readFileSync2, unlinkSync } from "fs";
|
|
1498
|
+
import { join as join2 } from "path";
|
|
1499
|
+
function sessionDbRecoveryReportPath(homeDir, env) {
|
|
1500
|
+
const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
|
|
1501
|
+
return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
|
|
1502
|
+
}
|
|
1503
|
+
function drainSessionDbRecoveryReport({
|
|
1504
|
+
homeDir,
|
|
1505
|
+
env
|
|
1506
|
+
}) {
|
|
1507
|
+
const path = sessionDbRecoveryReportPath(homeDir, env);
|
|
1508
|
+
let content;
|
|
1509
|
+
try {
|
|
1510
|
+
content = readFileSync2(path, "utf8");
|
|
1511
|
+
} catch (error2) {
|
|
1512
|
+
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
|
|
1513
|
+
return { path, records: [], skippedLines: 0, readError: null };
|
|
1514
|
+
const readError = error2 instanceof Error ? error2.message : String(error2);
|
|
1515
|
+
console.error(`[session-db-recovery-report] could not read ${path}: ${readError}`);
|
|
1516
|
+
return { path, records: [], skippedLines: 0, readError };
|
|
1517
|
+
}
|
|
1518
|
+
let skippedLines = 0;
|
|
1519
|
+
const records = content.split("\n").flatMap((line) => {
|
|
1520
|
+
if (!line.trim()) return [];
|
|
1521
|
+
try {
|
|
1522
|
+
const value = JSON.parse(line);
|
|
1523
|
+
if (!isSessionDbRecoveryRecord(value)) {
|
|
1524
|
+
skippedLines++;
|
|
1525
|
+
return [];
|
|
1526
|
+
}
|
|
1527
|
+
return [value];
|
|
1528
|
+
} catch (error2) {
|
|
1529
|
+
skippedLines++;
|
|
1530
|
+
console.error(
|
|
1531
|
+
`[session-db-recovery-report] skipped malformed record in ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1532
|
+
);
|
|
1533
|
+
return [];
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
return { path, records, skippedLines, readError: null };
|
|
1537
|
+
}
|
|
1538
|
+
function acknowledgeSessionDbRecoveryReport(path) {
|
|
1539
|
+
try {
|
|
1540
|
+
unlinkSync(path);
|
|
1541
|
+
} catch (error2) {
|
|
1542
|
+
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return;
|
|
1543
|
+
console.error(
|
|
1544
|
+
`[session-db-recovery-report] could not remove ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
function buildSessionDbRecoveryActivity(record) {
|
|
1549
|
+
const level = record.severity === "warning" ? "warn" : record.severity === "error" ? "error" : null;
|
|
1550
|
+
if (!level) return null;
|
|
1551
|
+
const noVerifiedPoint = record.verified_restore_point ? ` The verified restore point is ${record.verified_restore_point}.` : " No verified restore point is known.";
|
|
1552
|
+
switch (record.outcome) {
|
|
1553
|
+
case "fresh_session_db":
|
|
1554
|
+
return {
|
|
1555
|
+
level,
|
|
1556
|
+
metadata: withoutContractFields(record),
|
|
1557
|
+
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.`
|
|
1558
|
+
};
|
|
1559
|
+
case "restore_retried":
|
|
1560
|
+
return {
|
|
1561
|
+
level,
|
|
1562
|
+
metadata: withoutContractFields(record),
|
|
1563
|
+
message: "The initial session database restore failed; the runner will retry it. Monitor the runner backup for another restore failure."
|
|
1564
|
+
};
|
|
1565
|
+
case "replica_recovered":
|
|
1566
|
+
if (record.reason === "quarantine")
|
|
1567
|
+
return {
|
|
1568
|
+
level,
|
|
1569
|
+
metadata: withoutContractFields(record),
|
|
1570
|
+
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.`
|
|
1571
|
+
};
|
|
1572
|
+
if (record.reason === "prune")
|
|
1573
|
+
return {
|
|
1574
|
+
level,
|
|
1575
|
+
metadata: withoutContractFields(record),
|
|
1576
|
+
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."
|
|
1577
|
+
};
|
|
1578
|
+
if (record.reason === "clear")
|
|
1579
|
+
return {
|
|
1580
|
+
level,
|
|
1581
|
+
metadata: withoutContractFields(record),
|
|
1582
|
+
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."
|
|
1583
|
+
};
|
|
1584
|
+
return null;
|
|
1585
|
+
case "history_rolled_back":
|
|
1586
|
+
return {
|
|
1587
|
+
level,
|
|
1588
|
+
metadata: withoutContractFields(record),
|
|
1589
|
+
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.`
|
|
1590
|
+
};
|
|
1591
|
+
case "restore_misconfigured":
|
|
1592
|
+
return {
|
|
1593
|
+
level,
|
|
1594
|
+
metadata: withoutContractFields(record),
|
|
1595
|
+
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."
|
|
1596
|
+
};
|
|
1597
|
+
case "session_db_boot_refused":
|
|
1598
|
+
return {
|
|
1599
|
+
level,
|
|
1600
|
+
metadata: withoutContractFields(record),
|
|
1601
|
+
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.`
|
|
1602
|
+
};
|
|
1603
|
+
default:
|
|
1604
|
+
return null;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function withoutContractFields(record) {
|
|
1608
|
+
const { v: _v, event: _event, ...metadata } = record;
|
|
1609
|
+
return metadata;
|
|
1610
|
+
}
|
|
1611
|
+
var OUTCOMES = /* @__PURE__ */ new Set([
|
|
1612
|
+
"replica_recovered",
|
|
1613
|
+
"restore_retried",
|
|
1614
|
+
"fresh_session_db",
|
|
1615
|
+
"history_rolled_back",
|
|
1616
|
+
"restore_misconfigured",
|
|
1617
|
+
"session_db_boot_refused"
|
|
1618
|
+
]);
|
|
1619
|
+
var STAGES = /* @__PURE__ */ new Set(["restore", "verify"]);
|
|
1620
|
+
var SEVERITIES = /* @__PURE__ */ new Set(["warning", "error"]);
|
|
1621
|
+
var NUMBER_FIELDS = [
|
|
1622
|
+
"litestream_exit_code",
|
|
1623
|
+
"attempt",
|
|
1624
|
+
"replica_objects",
|
|
1625
|
+
"replica_bytes",
|
|
1626
|
+
"quarantined_objects",
|
|
1627
|
+
"quarantine_failed_objects",
|
|
1628
|
+
"quarantined_bytes",
|
|
1629
|
+
"restore_points_tried"
|
|
1630
|
+
];
|
|
1631
|
+
var STRING_OR_NULL_FIELDS = ["quarantine_destination", "verified_restore_point"];
|
|
1632
|
+
function isSessionDbRecoveryRecord(value) {
|
|
1633
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1634
|
+
const record = value;
|
|
1635
|
+
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(
|
|
1636
|
+
(field) => record[field] === null || typeof record[field] === "string"
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1385
1640
|
// src/lib/opencode/health.ts
|
|
1386
1641
|
async function checkOpenCodeHealth(port) {
|
|
1387
1642
|
try {
|
|
@@ -1426,6 +1681,62 @@ function buildOpenCodeVersionWarning(version2) {
|
|
|
1426
1681
|
|
|
1427
1682
|
// src/lib/opencode/process.ts
|
|
1428
1683
|
import { execSync, spawn } from "child_process";
|
|
1684
|
+
|
|
1685
|
+
// src/lib/process-stop.ts
|
|
1686
|
+
async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
1687
|
+
if (!child.pid) {
|
|
1688
|
+
return { outcome: "not-running", code: child.exitCode, signal: child.signalCode };
|
|
1689
|
+
}
|
|
1690
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1691
|
+
return { outcome: "exited", code: child.exitCode, signal: child.signalCode };
|
|
1692
|
+
}
|
|
1693
|
+
return new Promise((resolve3, reject) => {
|
|
1694
|
+
let forced = false;
|
|
1695
|
+
let settled = false;
|
|
1696
|
+
const timer = setTimeout(() => {
|
|
1697
|
+
forced = true;
|
|
1698
|
+
try {
|
|
1699
|
+
sendKill();
|
|
1700
|
+
} catch (error2) {
|
|
1701
|
+
if (error2.code === "ESRCH") {
|
|
1702
|
+
finish({ outcome: "exited", code: child.exitCode, signal: child.signalCode });
|
|
1703
|
+
} else {
|
|
1704
|
+
fail(error2);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}, timeoutMs);
|
|
1708
|
+
const finish = (result) => {
|
|
1709
|
+
if (settled) return;
|
|
1710
|
+
settled = true;
|
|
1711
|
+
clearTimeout(timer);
|
|
1712
|
+
child.removeListener("exit", onExit);
|
|
1713
|
+
resolve3(result);
|
|
1714
|
+
};
|
|
1715
|
+
const fail = (error2) => {
|
|
1716
|
+
if (settled) return;
|
|
1717
|
+
settled = true;
|
|
1718
|
+
clearTimeout(timer);
|
|
1719
|
+
child.removeListener("exit", onExit);
|
|
1720
|
+
reject(error2);
|
|
1721
|
+
};
|
|
1722
|
+
const onExit = (code, signal) => {
|
|
1723
|
+
finish({ outcome: forced ? "killed" : "exited", code, signal });
|
|
1724
|
+
};
|
|
1725
|
+
child.once("exit", onExit);
|
|
1726
|
+
try {
|
|
1727
|
+
sendTerm();
|
|
1728
|
+
} catch (error2) {
|
|
1729
|
+
if (error2.code === "ESRCH") {
|
|
1730
|
+
finish({ outcome: "exited", code: child.exitCode, signal: child.signalCode });
|
|
1731
|
+
} else {
|
|
1732
|
+
fail(error2);
|
|
1733
|
+
}
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// src/lib/opencode/process.ts
|
|
1429
1740
|
var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
|
|
1430
1741
|
function getProcessCwd(pid) {
|
|
1431
1742
|
const platform = process.platform;
|
|
@@ -1597,23 +1908,20 @@ async function startOpenCode(port) {
|
|
|
1597
1908
|
});
|
|
1598
1909
|
return child;
|
|
1599
1910
|
}
|
|
1600
|
-
function
|
|
1601
|
-
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
try {
|
|
1911
|
+
function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
|
|
1912
|
+
const sendSignal = (signal) => {
|
|
1605
1913
|
if (process.platform === "win32") {
|
|
1606
|
-
opencodeProcess.kill(
|
|
1914
|
+
opencodeProcess.kill(signal);
|
|
1607
1915
|
} else {
|
|
1608
|
-
process.kill(-opencodeProcess.pid,
|
|
1916
|
+
process.kill(-opencodeProcess.pid, signal);
|
|
1609
1917
|
}
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1918
|
+
};
|
|
1919
|
+
return stopProcessAndWait(
|
|
1920
|
+
opencodeProcess,
|
|
1921
|
+
timeoutMs,
|
|
1922
|
+
() => sendSignal("SIGTERM"),
|
|
1923
|
+
() => sendSignal("SIGKILL")
|
|
1924
|
+
);
|
|
1617
1925
|
}
|
|
1618
1926
|
|
|
1619
1927
|
// src/lib/opencode/install.ts
|
|
@@ -2196,7 +2504,7 @@ function isPreamblePinnedRunning(messages, userMessageId) {
|
|
|
2196
2504
|
return completedOf(reply) != null && finishOf(reply) === "tool-calls";
|
|
2197
2505
|
}
|
|
2198
2506
|
function isB2AbandonmentConfirmed(params) {
|
|
2199
|
-
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false;
|
|
2507
|
+
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
|
|
2200
2508
|
}
|
|
2201
2509
|
function isAmbiguousTerminalFinish(m) {
|
|
2202
2510
|
if (completedOf(m) == null) return false;
|
|
@@ -2209,7 +2517,7 @@ function isAmbiguousFinishPinnedRunning(messages, userMessageId) {
|
|
|
2209
2517
|
return isAmbiguousTerminalFinish(reply);
|
|
2210
2518
|
}
|
|
2211
2519
|
function isAmbiguousFinishResolved(params) {
|
|
2212
|
-
return params.sessionOngoing === false || params.pinnedForMs >= params.maxPinnedMs;
|
|
2520
|
+
return params.sessionOngoing === false || params.sessionOngoing !== true && params.pinnedForMs >= params.maxPinnedMs;
|
|
2213
2521
|
}
|
|
2214
2522
|
function messageError(messages, userMessageId) {
|
|
2215
2523
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
@@ -2419,10 +2727,10 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
2419
2727
|
|
|
2420
2728
|
// src/lib/opencode/session-db-size.ts
|
|
2421
2729
|
import { statSync as statSync2 } from "fs";
|
|
2422
|
-
import { join as
|
|
2730
|
+
import { join as join3 } from "path";
|
|
2423
2731
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
2424
2732
|
function statSessionDbBytes(homeDir) {
|
|
2425
|
-
const dbPath =
|
|
2733
|
+
const dbPath = join3(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
2426
2734
|
try {
|
|
2427
2735
|
return statSync2(dbPath).size;
|
|
2428
2736
|
} catch (err) {
|
|
@@ -2991,6 +3299,193 @@ function writeTunnelReadyMarker(path, agentId) {
|
|
|
2991
3299
|
}
|
|
2992
3300
|
}
|
|
2993
3301
|
|
|
3302
|
+
// src/lib/replication.ts
|
|
3303
|
+
import { spawn as spawn2 } from "child_process";
|
|
3304
|
+
function startSessionDbReplication(configPath) {
|
|
3305
|
+
return spawn2("litestream", ["replicate", "-config", configPath], {
|
|
3306
|
+
stdio: "inherit"
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
async function stopSessionDbReplication(child, timeoutMs) {
|
|
3310
|
+
return stopProcessAndWait(
|
|
3311
|
+
child,
|
|
3312
|
+
timeoutMs,
|
|
3313
|
+
() => child.kill("SIGTERM"),
|
|
3314
|
+
() => child.kill("SIGKILL")
|
|
3315
|
+
);
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
// src/lib/openai-usage.ts
|
|
3319
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3320
|
+
import { homedir as homedir2 } from "os";
|
|
3321
|
+
import { join as join4 } from "path";
|
|
3322
|
+
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
3323
|
+
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
3324
|
+
var OpenAiUsageError = class extends Error {
|
|
3325
|
+
constructor(message, reason) {
|
|
3326
|
+
super(message);
|
|
3327
|
+
this.reason = reason;
|
|
3328
|
+
}
|
|
3329
|
+
};
|
|
3330
|
+
function isLocalCredentialProblem2(err) {
|
|
3331
|
+
return err instanceof OpenAiUsageError && (err.reason === "no_credentials" || err.reason === "credentials_expired");
|
|
3332
|
+
}
|
|
3333
|
+
function readOpenCodeChatGptCredentials() {
|
|
3334
|
+
try {
|
|
3335
|
+
const raw = readFileSync3(join4(homedir2(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
3336
|
+
let parsed;
|
|
3337
|
+
try {
|
|
3338
|
+
parsed = JSON.parse(raw);
|
|
3339
|
+
} catch {
|
|
3340
|
+
return null;
|
|
3341
|
+
}
|
|
3342
|
+
const entry = parsed.openai;
|
|
3343
|
+
if (entry?.type !== "oauth" || typeof entry.access !== "string" || !entry.access || typeof entry.expires !== "number") {
|
|
3344
|
+
return null;
|
|
3345
|
+
}
|
|
3346
|
+
return { accessToken: entry.access, expiresAt: entry.expires };
|
|
3347
|
+
} catch (err) {
|
|
3348
|
+
const code = err.code;
|
|
3349
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
3350
|
+
console.warn(
|
|
3351
|
+
`readOpenCodeChatGptCredentials: reading auth.json failed (${code ?? "unknown"})`
|
|
3352
|
+
);
|
|
3353
|
+
}
|
|
3354
|
+
return null;
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
function toWindow2(headers, name) {
|
|
3358
|
+
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
3359
|
+
const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
|
|
3360
|
+
if (utilizationHeader == null || utilizationHeader === "" || windowMinutesHeader == null) {
|
|
3361
|
+
return null;
|
|
3362
|
+
}
|
|
3363
|
+
const utilization = Number(utilizationHeader);
|
|
3364
|
+
const windowMinutes = Number(windowMinutesHeader);
|
|
3365
|
+
if (!Number.isFinite(utilization) || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
|
|
3366
|
+
return null;
|
|
3367
|
+
}
|
|
3368
|
+
const resetAtHeader = headers.get(`x-codex-${name}-reset-at`);
|
|
3369
|
+
const resetSeconds = resetAtHeader == null || resetAtHeader === "" ? NaN : Number(resetAtHeader);
|
|
3370
|
+
const resetsAt = Number.isFinite(resetSeconds) ? new Date(resetSeconds * 1e3).toISOString() : null;
|
|
3371
|
+
return { utilization: Math.min(100, Math.max(0, utilization)), windowMinutes, resetsAt };
|
|
3372
|
+
}
|
|
3373
|
+
function parseCodexUsageHeaders(headers) {
|
|
3374
|
+
return {
|
|
3375
|
+
primary: toWindow2(headers, "primary"),
|
|
3376
|
+
secondary: toWindow2(headers, "secondary"),
|
|
3377
|
+
hasCredits: headers.has("x-codex-credits-has-credits") ? headers.get("x-codex-credits-has-credits")?.toLowerCase() === "true" : null,
|
|
3378
|
+
creditsUnlimited: headers.has("x-codex-credits-unlimited") ? headers.get("x-codex-credits-unlimited")?.toLowerCase() === "true" : null
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
function normalizeProbeModel(model) {
|
|
3382
|
+
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
3383
|
+
}
|
|
3384
|
+
async function resolveProbeModels(port) {
|
|
3385
|
+
try {
|
|
3386
|
+
const res = await withRequestTimeout(
|
|
3387
|
+
fetch,
|
|
3388
|
+
REQUEST_TIMEOUT_MS
|
|
3389
|
+
)(`${opencodeBase(port)}/config/providers`);
|
|
3390
|
+
if (!res.ok) {
|
|
3391
|
+
console.error(
|
|
3392
|
+
`[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
3393
|
+
);
|
|
3394
|
+
return [];
|
|
3395
|
+
}
|
|
3396
|
+
const body = await res.json();
|
|
3397
|
+
const provider = body?.providers?.find((candidate) => candidate?.id === "openai");
|
|
3398
|
+
if (!provider || !provider.models || typeof provider.models !== "object") return [];
|
|
3399
|
+
const candidates = [
|
|
3400
|
+
...typeof body?.default?.openai === "string" ? [body.default.openai] : [],
|
|
3401
|
+
...Object.keys(provider.models)
|
|
3402
|
+
].map(normalizeProbeModel);
|
|
3403
|
+
return [...new Set(candidates)].slice(0, 4);
|
|
3404
|
+
} catch (err) {
|
|
3405
|
+
console.error(
|
|
3406
|
+
`[resolveProbeModels] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
3407
|
+
);
|
|
3408
|
+
return [];
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
function hasPrimaryHeaders(headers) {
|
|
3412
|
+
return [
|
|
3413
|
+
"x-codex-primary-used-percent",
|
|
3414
|
+
"x-codex-primary-window-minutes",
|
|
3415
|
+
"x-codex-primary-reset-at"
|
|
3416
|
+
].some((name) => headers.has(name));
|
|
3417
|
+
}
|
|
3418
|
+
async function getOpenAiUsage(port) {
|
|
3419
|
+
const credentials2 = readOpenCodeChatGptCredentials();
|
|
3420
|
+
if (!credentials2) {
|
|
3421
|
+
throw new OpenAiUsageError(
|
|
3422
|
+
"No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
|
|
3423
|
+
"no_credentials"
|
|
3424
|
+
);
|
|
3425
|
+
}
|
|
3426
|
+
if (credentials2.expiresAt < Date.now()) {
|
|
3427
|
+
throw new OpenAiUsageError(
|
|
3428
|
+
"ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
|
|
3429
|
+
"credentials_expired"
|
|
3430
|
+
);
|
|
3431
|
+
}
|
|
3432
|
+
const models = await resolveProbeModels(port);
|
|
3433
|
+
if (models.length === 0) {
|
|
3434
|
+
throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
|
|
3435
|
+
}
|
|
3436
|
+
let lastStatus;
|
|
3437
|
+
for (const model of models) {
|
|
3438
|
+
let res;
|
|
3439
|
+
try {
|
|
3440
|
+
res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
|
|
3441
|
+
method: "POST",
|
|
3442
|
+
headers: {
|
|
3443
|
+
Authorization: `Bearer ${credentials2.accessToken}`,
|
|
3444
|
+
"Content-Type": "application/json"
|
|
3445
|
+
},
|
|
3446
|
+
body: JSON.stringify({ model, store: false, stream: true })
|
|
3447
|
+
});
|
|
3448
|
+
} catch (err) {
|
|
3449
|
+
throw new OpenAiUsageError(
|
|
3450
|
+
`OpenAI usage probe request failed: ${err instanceof Error ? err.message : String(err)}.`,
|
|
3451
|
+
"request_failed"
|
|
3452
|
+
);
|
|
3453
|
+
}
|
|
3454
|
+
try {
|
|
3455
|
+
lastStatus = res.status;
|
|
3456
|
+
if (hasPrimaryHeaders(res.headers)) {
|
|
3457
|
+
const usage = parseCodexUsageHeaders(res.headers);
|
|
3458
|
+
if (!usage.primary && !usage.secondary) {
|
|
3459
|
+
throw new OpenAiUsageError(
|
|
3460
|
+
`OpenAI usage probe returned no usable window (HTTP ${res.status}).`,
|
|
3461
|
+
"no_usable_window"
|
|
3462
|
+
);
|
|
3463
|
+
}
|
|
3464
|
+
return usage;
|
|
3465
|
+
}
|
|
3466
|
+
if (res.status === 401) {
|
|
3467
|
+
throw new OpenAiUsageError(
|
|
3468
|
+
"ChatGPT credentials have expired (HTTP 401).",
|
|
3469
|
+
"credentials_expired"
|
|
3470
|
+
);
|
|
3471
|
+
}
|
|
3472
|
+
if (res.status === 403 || res.status === 429) {
|
|
3473
|
+
throw new OpenAiUsageError(
|
|
3474
|
+
`OpenAI usage probe was blocked (HTTP ${res.status}).`,
|
|
3475
|
+
"probe_blocked"
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
} finally {
|
|
3479
|
+
await res.body?.cancel().catch(() => {
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
throw new OpenAiUsageError(
|
|
3484
|
+
`OpenAI usage probe failed: HTTP ${lastStatus ?? "unknown"}.`,
|
|
3485
|
+
"request_failed"
|
|
3486
|
+
);
|
|
3487
|
+
}
|
|
3488
|
+
|
|
2994
3489
|
// src/lib/reporting-schedule.ts
|
|
2995
3490
|
function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
|
|
2996
3491
|
const jitterRangeMs = baseMs * jitterFraction;
|
|
@@ -2999,6 +3494,31 @@ function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
|
|
|
2999
3494
|
function firstReportDelayMs(random = Math.random) {
|
|
3000
3495
|
return 5e3 + random() * 1e4;
|
|
3001
3496
|
}
|
|
3497
|
+
var VALID_USAGE_REPORTING_MODES = ["auto", "on", "off"];
|
|
3498
|
+
function resolveUsageReportingMode(flagValue, env, names) {
|
|
3499
|
+
const raw = flagValue ?? env[names.envVar];
|
|
3500
|
+
if (raw === void 0 || raw === "") return { mode: "auto", warnings: [] };
|
|
3501
|
+
const normalized = raw.trim().toLowerCase();
|
|
3502
|
+
if (VALID_USAGE_REPORTING_MODES.includes(normalized)) {
|
|
3503
|
+
return { mode: normalized, warnings: [] };
|
|
3504
|
+
}
|
|
3505
|
+
const source = flagValue !== void 0 ? names.flagName : names.envVar;
|
|
3506
|
+
return {
|
|
3507
|
+
mode: "auto",
|
|
3508
|
+
warnings: [
|
|
3509
|
+
`Ignoring invalid ${source} "${raw}": expected one of ${VALID_USAGE_REPORTING_MODES.join(", ")}; using auto`
|
|
3510
|
+
]
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
var BASE_USAGE_REPORT_DELAY_MS = 10 * 6e4;
|
|
3514
|
+
var USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
3515
|
+
function usageReportDelayMs(random = Math.random) {
|
|
3516
|
+
return jitteredDelayMs(BASE_USAGE_REPORT_DELAY_MS, USAGE_REPORT_DELAY_JITTER_FRACTION, random);
|
|
3517
|
+
}
|
|
3518
|
+
var USAGE_REPORT_FAILURE_REESCALATION_TICKS = 6;
|
|
3519
|
+
function usageReportFailureLogLevel(consecutiveFailures) {
|
|
3520
|
+
return reportFailureLogLevel(consecutiveFailures, USAGE_REPORT_FAILURE_REESCALATION_TICKS);
|
|
3521
|
+
}
|
|
3002
3522
|
function reportFailureLogLevel(consecutiveFailures, reescalationTicks) {
|
|
3003
3523
|
return consecutiveFailures === 1 || consecutiveFailures % reescalationTicks === 0 ? "warn" : "debug";
|
|
3004
3524
|
}
|
|
@@ -3007,33 +3527,26 @@ function failureStreakSuffix(consecutiveFailures) {
|
|
|
3007
3527
|
}
|
|
3008
3528
|
|
|
3009
3529
|
// src/lib/claude-usage-reporting.ts
|
|
3010
|
-
var VALID_MODES = ["auto", "on", "off"];
|
|
3011
3530
|
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
|
-
};
|
|
3531
|
+
return resolveUsageReportingMode(flagValue, env, {
|
|
3532
|
+
flagName: "--claude-usage-reporting",
|
|
3533
|
+
envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
3534
|
+
});
|
|
3027
3535
|
}
|
|
3028
|
-
var BASE_REPORT_DELAY_MS = 10 * 6e4;
|
|
3029
|
-
var REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
3030
3536
|
function nextReportDelayMs(random = Math.random) {
|
|
3031
|
-
return
|
|
3537
|
+
return usageReportDelayMs(random);
|
|
3032
3538
|
}
|
|
3033
3539
|
var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
|
|
3034
|
-
var CLAUDE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
3035
3540
|
function claudeUsageFailureLogLevel(consecutiveFailures) {
|
|
3036
|
-
return
|
|
3541
|
+
return usageReportFailureLogLevel(consecutiveFailures);
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
// src/lib/openai-usage-reporting.ts
|
|
3545
|
+
function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
3546
|
+
return resolveUsageReportingMode(flagValue, env, {
|
|
3547
|
+
flagName: "--openai-usage-reporting",
|
|
3548
|
+
envVar: "EVIDENT_OPENAI_USAGE_REPORTING"
|
|
3549
|
+
});
|
|
3037
3550
|
}
|
|
3038
3551
|
|
|
3039
3552
|
// src/lib/resource-usage-reporting.ts
|
|
@@ -3192,15 +3705,15 @@ function createResourceUsageCollector(homeDir) {
|
|
|
3192
3705
|
}
|
|
3193
3706
|
|
|
3194
3707
|
// src/lib/channels/driver.ts
|
|
3195
|
-
import { homedir as
|
|
3708
|
+
import { homedir as homedir3 } from "os";
|
|
3196
3709
|
|
|
3197
3710
|
// src/lib/runner-file-sync.ts
|
|
3198
|
-
import { join as
|
|
3711
|
+
import { join as join6 } from "path";
|
|
3199
3712
|
|
|
3200
3713
|
// src/lib/file-push.ts
|
|
3201
3714
|
import { randomUUID } from "crypto";
|
|
3202
3715
|
import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
|
|
3203
|
-
import { basename, dirname as dirname3, isAbsolute, join as
|
|
3716
|
+
import { basename, dirname as dirname3, isAbsolute, join as join5, relative, resolve as resolve2, sep } from "path";
|
|
3204
3717
|
var FILE_MODE = 384;
|
|
3205
3718
|
var DIRECTORY_MODE = 448;
|
|
3206
3719
|
async function writePushedFile(request) {
|
|
@@ -3233,7 +3746,7 @@ async function writePushedFile(request) {
|
|
|
3233
3746
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
3234
3747
|
dirname3(candidate)
|
|
3235
3748
|
);
|
|
3236
|
-
const realTarget =
|
|
3749
|
+
const realTarget = join5(existingAncestor, ...missingSegments, basename(candidate));
|
|
3237
3750
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
3238
3751
|
if (allowedDirectory === null) {
|
|
3239
3752
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -3269,7 +3782,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
3269
3782
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
3270
3783
|
return null;
|
|
3271
3784
|
}
|
|
3272
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
3785
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join5(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
3273
3786
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
3274
3787
|
return null;
|
|
3275
3788
|
}
|
|
@@ -3342,13 +3855,13 @@ function contains(realDirectory, realTarget) {
|
|
|
3342
3855
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
3343
3856
|
let current = existingAncestor;
|
|
3344
3857
|
for (const segment of missingSegments) {
|
|
3345
|
-
current =
|
|
3858
|
+
current = join5(current, segment);
|
|
3346
3859
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
3347
3860
|
await chmod(current, DIRECTORY_MODE);
|
|
3348
3861
|
}
|
|
3349
3862
|
}
|
|
3350
3863
|
async function writeAtomically(realTarget, content) {
|
|
3351
|
-
const temporaryPath =
|
|
3864
|
+
const temporaryPath = join5(dirname3(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
3352
3865
|
let handle;
|
|
3353
3866
|
try {
|
|
3354
3867
|
handle = await open2(temporaryPath, "wx", FILE_MODE);
|
|
@@ -3390,20 +3903,28 @@ async function syncPendingRunnerFiles(options) {
|
|
|
3390
3903
|
for (const id of options.ackFailures.keys()) {
|
|
3391
3904
|
if (!pendingIds.has(id)) options.ackFailures.delete(id);
|
|
3392
3905
|
}
|
|
3393
|
-
if (pending.length === 0)
|
|
3906
|
+
if (pending.length === 0) {
|
|
3907
|
+
return { applied: 0, claudeCredentialApplied: false, opencodeAuthApplied: false };
|
|
3908
|
+
}
|
|
3394
3909
|
options.log({
|
|
3395
3910
|
level: "info",
|
|
3396
3911
|
message: `Runner file sync: ${pending.length} file(s) queued for this runner`
|
|
3397
3912
|
});
|
|
3398
3913
|
let applied = 0;
|
|
3399
3914
|
let claudeCredentialApplied = false;
|
|
3915
|
+
let opencodeAuthApplied = false;
|
|
3400
3916
|
for (const file of pending) {
|
|
3401
3917
|
if ((options.ackFailures.get(file.id) ?? 0) >= MAX_ACK_ATTEMPTS) continue;
|
|
3402
3918
|
const outcome = await applyOne(options, file);
|
|
3403
3919
|
if (outcome.applied) applied += 1;
|
|
3404
3920
|
if (outcome.claudeCredentialApplied) claudeCredentialApplied = true;
|
|
3921
|
+
if (outcome.opencodeAuthApplied) opencodeAuthApplied = true;
|
|
3405
3922
|
}
|
|
3406
|
-
return {
|
|
3923
|
+
return {
|
|
3924
|
+
applied,
|
|
3925
|
+
claudeCredentialApplied,
|
|
3926
|
+
opencodeAuthApplied
|
|
3927
|
+
};
|
|
3407
3928
|
}
|
|
3408
3929
|
async function listPendingFiles(options) {
|
|
3409
3930
|
let res;
|
|
@@ -3464,10 +3985,18 @@ function asPendingFile(entry) {
|
|
|
3464
3985
|
if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
|
|
3465
3986
|
return { id, path, size };
|
|
3466
3987
|
}
|
|
3467
|
-
var NOT_APPLIED = {
|
|
3988
|
+
var NOT_APPLIED = {
|
|
3989
|
+
applied: false,
|
|
3990
|
+
claudeCredentialApplied: false,
|
|
3991
|
+
opencodeAuthApplied: false
|
|
3992
|
+
};
|
|
3468
3993
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
3469
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
3470
|
-
return expanded ===
|
|
3994
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
3995
|
+
return expanded === join6(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
3996
|
+
}
|
|
3997
|
+
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
3998
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
3999
|
+
return expanded === join6(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
3471
4000
|
}
|
|
3472
4001
|
async function applyOne(options, file) {
|
|
3473
4002
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -3523,7 +4052,8 @@ async function applyOne(options, file) {
|
|
|
3523
4052
|
await ack(options, file, "applied");
|
|
3524
4053
|
return {
|
|
3525
4054
|
applied: true,
|
|
3526
|
-
claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir)
|
|
4055
|
+
claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir),
|
|
4056
|
+
opencodeAuthApplied: isOpenCodeAuthPath(file.path, options.homeDir)
|
|
3527
4057
|
};
|
|
3528
4058
|
}
|
|
3529
4059
|
function durableDownloadCode(status2) {
|
|
@@ -3977,6 +4507,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3977
4507
|
* that way rather than "fixing" it into a count.
|
|
3978
4508
|
*/
|
|
3979
4509
|
claudeCredentialApplyCount = 0;
|
|
4510
|
+
opencodeAuthApplyCount = 0;
|
|
3980
4511
|
/**
|
|
3981
4512
|
* The currently-executing `drainPending()` promise, or null when idle. Lets a
|
|
3982
4513
|
* graceful shutdown (`waitForInFlight`) await an in-progress drain so a turn it
|
|
@@ -4011,7 +4542,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4011
4542
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
4012
4543
|
this.now = config.now ?? (() => Date.now());
|
|
4013
4544
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
4014
|
-
this.homeDir = config.homeDir ??
|
|
4545
|
+
this.homeDir = config.homeDir ?? homedir3();
|
|
4015
4546
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
4016
4547
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
4017
4548
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
@@ -4081,6 +4612,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4081
4612
|
});
|
|
4082
4613
|
this.appliedFileCount += result.applied;
|
|
4083
4614
|
if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;
|
|
4615
|
+
if (result.opencodeAuthApplied) this.opencodeAuthApplyCount += 1;
|
|
4084
4616
|
return result.applied;
|
|
4085
4617
|
} catch (err) {
|
|
4086
4618
|
this.log({
|
|
@@ -4188,7 +4720,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4188
4720
|
return {
|
|
4189
4721
|
appliedFiles: this.appliedFileCount,
|
|
4190
4722
|
inFlight: this.syncingFiles,
|
|
4191
|
-
claudeCredentialApplies: this.claudeCredentialApplyCount
|
|
4723
|
+
claudeCredentialApplies: this.claudeCredentialApplyCount,
|
|
4724
|
+
opencodeAuthApplyCount: this.opencodeAuthApplyCount
|
|
4192
4725
|
};
|
|
4193
4726
|
}
|
|
4194
4727
|
/**
|
|
@@ -5276,6 +5809,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5276
5809
|
deliveryDeadlineAnchored: false,
|
|
5277
5810
|
b2PinnedSinceMs: 0,
|
|
5278
5811
|
b2LastDescendantCheckMs: 0,
|
|
5812
|
+
b2RootOngoingHeldLogged: false,
|
|
5279
5813
|
b2AbandonedSignalled: false,
|
|
5280
5814
|
ambiguousPinnedSinceMs: 0,
|
|
5281
5815
|
ambiguousResolved: false
|
|
@@ -5370,6 +5904,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5370
5904
|
deliveryDeadlineAnchored: false,
|
|
5371
5905
|
b2PinnedSinceMs: 0,
|
|
5372
5906
|
b2LastDescendantCheckMs: 0,
|
|
5907
|
+
b2RootOngoingHeldLogged: false,
|
|
5373
5908
|
b2AbandonedSignalled: false,
|
|
5374
5909
|
ambiguousPinnedSinceMs: 0,
|
|
5375
5910
|
ambiguousResolved: false
|
|
@@ -5723,6 +6258,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5723
6258
|
if (snapshotReadable) {
|
|
5724
6259
|
inFlight.b2PinnedSinceMs = 0;
|
|
5725
6260
|
inFlight.b2LastDescendantCheckMs = 0;
|
|
6261
|
+
inFlight.b2RootOngoingHeldLogged = false;
|
|
5726
6262
|
inFlight.b2AbandonedSignalled = false;
|
|
5727
6263
|
}
|
|
5728
6264
|
} else {
|
|
@@ -5734,11 +6270,15 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5734
6270
|
const pinnedForMs = this.now() - inFlight.b2PinnedSinceMs;
|
|
5735
6271
|
if (pinnedForMs >= B2_ABANDONMENT_MIN_PINNED_MS && this.now() - inFlight.b2LastDescendantCheckMs >= B2_ABANDONMENT_RECHECK_MS) {
|
|
5736
6272
|
inFlight.b2LastDescendantCheckMs = this.now();
|
|
5737
|
-
const descendantOngoing = await
|
|
6273
|
+
const [descendantOngoing, rootOngoing] = await Promise.all([
|
|
6274
|
+
this.isAnyDescendantSessionOngoing(sessionId),
|
|
6275
|
+
isSessionOngoing(this.port, sessionId)
|
|
6276
|
+
]);
|
|
5738
6277
|
if (isB2AbandonmentConfirmed({
|
|
5739
6278
|
pinnedForMs,
|
|
5740
6279
|
minPinnedMs: B2_ABANDONMENT_MIN_PINNED_MS,
|
|
5741
|
-
descendantOngoing
|
|
6280
|
+
descendantOngoing,
|
|
6281
|
+
rootOngoing
|
|
5742
6282
|
})) {
|
|
5743
6283
|
inFlight.b2AbandonedSignalled = true;
|
|
5744
6284
|
this.log({
|
|
@@ -5747,12 +6287,26 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5747
6287
|
conversation_id: conv.id,
|
|
5748
6288
|
message_id: id
|
|
5749
6289
|
});
|
|
6290
|
+
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
5750
6291
|
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
5751
|
-
watched_for_ms: pinnedForMs
|
|
6292
|
+
watched_for_ms: pinnedForMs,
|
|
6293
|
+
finish: reply?.info?.finish ?? reply?.finish,
|
|
6294
|
+
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
6295
|
+
...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
|
|
6296
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
5752
6297
|
});
|
|
5753
6298
|
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
5754
6299
|
return;
|
|
5755
6300
|
}
|
|
6301
|
+
if (rootOngoing === true && !inFlight.b2RootOngoingHeldLogged) {
|
|
6302
|
+
inFlight.b2RootOngoingHeldLogged = true;
|
|
6303
|
+
this.log({
|
|
6304
|
+
level: "warn",
|
|
6305
|
+
message: `Message ${id.slice(0, 8)} b2-pinned for ${Math.round(pinnedForMs / 1e3)}s with root_ongoing=${rootOngoing} and descendant_ongoing=${descendantOngoing} \u2014 holding until OpenCode confirms the root is idle`,
|
|
6306
|
+
conversation_id: conv.id,
|
|
6307
|
+
message_id: id
|
|
6308
|
+
});
|
|
6309
|
+
}
|
|
5756
6310
|
}
|
|
5757
6311
|
}
|
|
5758
6312
|
const ambiguousPinnedNow = activelyRunning && isAmbiguousFinishPinnedRunning(messages, inFlight.opencodeMessageId);
|
|
@@ -7495,6 +8049,7 @@ var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_
|
|
|
7495
8049
|
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
7496
8050
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
|
|
7497
8051
|
var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
8052
|
+
var CHILD_STOP_TIMEOUT_MS = 1e4;
|
|
7498
8053
|
function resolveLogLevel(options) {
|
|
7499
8054
|
const accepted = Object.keys(LOG_LEVELS);
|
|
7500
8055
|
const validate = (value, source) => {
|
|
@@ -7525,7 +8080,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
7525
8080
|
if (trimmed === "") {
|
|
7526
8081
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
7527
8082
|
}
|
|
7528
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
8083
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join7(homeDir, trimmed.slice(2)) : trimmed;
|
|
7529
8084
|
if (!isAbsolute2(expanded)) {
|
|
7530
8085
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
7531
8086
|
}
|
|
@@ -7623,7 +8178,7 @@ function logActivity(state, entry) {
|
|
|
7623
8178
|
const level = entry.level ?? (entry.type === "error" ? "error" : "info");
|
|
7624
8179
|
if (!meetsThreshold(state, level)) return;
|
|
7625
8180
|
forwardRunnerActivity(
|
|
7626
|
-
{ level, message: entry.message, error: entry.error },
|
|
8181
|
+
{ level, message: entry.message, error: entry.error, metadata: entry.metadata },
|
|
7627
8182
|
{ agentId: state.agentId, authHeader: state.authHeader }
|
|
7628
8183
|
);
|
|
7629
8184
|
const fullEntry = {
|
|
@@ -7643,6 +8198,26 @@ function logActivity(state, entry) {
|
|
|
7643
8198
|
}
|
|
7644
8199
|
}
|
|
7645
8200
|
}
|
|
8201
|
+
function reportSessionDbRecovery(state) {
|
|
8202
|
+
try {
|
|
8203
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir4(), env: process.env });
|
|
8204
|
+
for (const record of report.records) {
|
|
8205
|
+
const activity = buildSessionDbRecoveryActivity(record);
|
|
8206
|
+
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
8207
|
+
logActivity(state, {
|
|
8208
|
+
type: activity.level === "error" ? "error" : "info",
|
|
8209
|
+
level: activity.level,
|
|
8210
|
+
...activity.level === "error" ? { error: activity.message } : { message: activity.message },
|
|
8211
|
+
metadata: activity.metadata
|
|
8212
|
+
});
|
|
8213
|
+
}
|
|
8214
|
+
acknowledgeSessionDbRecoveryReport(report.path);
|
|
8215
|
+
} catch (error2) {
|
|
8216
|
+
console.error(
|
|
8217
|
+
`[run] could not report session-DB recovery activity: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
8218
|
+
);
|
|
8219
|
+
}
|
|
8220
|
+
}
|
|
7646
8221
|
function displayStatus(state) {
|
|
7647
8222
|
if (!state.interactive) return;
|
|
7648
8223
|
const attempt = state.connection?.reconnectAttempt ?? 0;
|
|
@@ -7733,6 +8308,7 @@ async function driveChannels(state, driver) {
|
|
|
7733
8308
|
let lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
|
|
7734
8309
|
let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
|
|
7735
8310
|
let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
|
|
8311
|
+
let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
|
|
7736
8312
|
while (state.running) {
|
|
7737
8313
|
const cycleStartedAtMs = performance.now();
|
|
7738
8314
|
let idleThisCycle = false;
|
|
@@ -7765,6 +8341,10 @@ async function driveChannels(state, driver) {
|
|
|
7765
8341
|
const claudeCredentialApplied = claudeCredentialApplies !== lastSeenClaudeApplies;
|
|
7766
8342
|
lastSeenClaudeApplies = claudeCredentialApplies;
|
|
7767
8343
|
if (claudeCredentialApplied) state.claudeUsageRearm?.();
|
|
8344
|
+
const opencodeAuthApplies = fileActivitySnapshot.opencodeAuthApplyCount;
|
|
8345
|
+
const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
|
|
8346
|
+
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
8347
|
+
if (opencodeAuthApplied) state.openaiUsageRearm?.();
|
|
7768
8348
|
if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
|
|
7769
8349
|
idlePolls = 0;
|
|
7770
8350
|
idleMs = 0;
|
|
@@ -7833,7 +8413,7 @@ async function driveChannels(state, driver) {
|
|
|
7833
8413
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
7834
8414
|
var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
|
|
7835
8415
|
function sessionDbPath() {
|
|
7836
|
-
return
|
|
8416
|
+
return join7(homedir4(), ".local", "share", "opencode", "opencode.db");
|
|
7837
8417
|
}
|
|
7838
8418
|
async function runSweep(state, driver, config) {
|
|
7839
8419
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
@@ -7916,7 +8496,7 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
7916
8496
|
for (const warning2 of config.warnings) {
|
|
7917
8497
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
7918
8498
|
}
|
|
7919
|
-
const dbBytes = statSessionDbBytes(
|
|
8499
|
+
const dbBytes = statSessionDbBytes(homedir4());
|
|
7920
8500
|
void (async () => {
|
|
7921
8501
|
const reclaimSkipReason = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
7922
8502
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
@@ -7944,23 +8524,20 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
7944
8524
|
);
|
|
7945
8525
|
state.sessionCleanupTimers.push(interval, firstSweep);
|
|
7946
8526
|
}
|
|
7947
|
-
function
|
|
7948
|
-
const { mode, warnings } =
|
|
7949
|
-
options.claudeUsageReporting,
|
|
7950
|
-
process.env
|
|
7951
|
-
);
|
|
8527
|
+
function scheduleUsageReporting(state, params) {
|
|
8528
|
+
const { mode, warnings } = params.resolved;
|
|
7952
8529
|
for (const warning2 of warnings) {
|
|
7953
8530
|
logActivity(state, {
|
|
7954
8531
|
type: "info",
|
|
7955
8532
|
level: "warn",
|
|
7956
|
-
message:
|
|
8533
|
+
message: `${params.label} usage reporting: ${warning2}`
|
|
7957
8534
|
});
|
|
7958
8535
|
}
|
|
7959
8536
|
if (mode === "off") {
|
|
7960
8537
|
logActivity(state, {
|
|
7961
8538
|
type: "info",
|
|
7962
8539
|
level: "debug",
|
|
7963
|
-
message:
|
|
8540
|
+
message: `${params.label} usage reporting is off (${params.offFlagHint})`
|
|
7964
8541
|
});
|
|
7965
8542
|
return null;
|
|
7966
8543
|
}
|
|
@@ -7969,7 +8546,7 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
7969
8546
|
let rearmRequested = false;
|
|
7970
8547
|
const armProbe = () => {
|
|
7971
8548
|
phase = "probe-pending";
|
|
7972
|
-
|
|
8549
|
+
params.setTimer(setTimeout(() => void tick(true), params.firstDelayMs()));
|
|
7973
8550
|
};
|
|
7974
8551
|
const scheduleNextTick = () => {
|
|
7975
8552
|
if (rearmRequested) {
|
|
@@ -7978,7 +8555,7 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
7978
8555
|
return;
|
|
7979
8556
|
}
|
|
7980
8557
|
phase = "steady-pending";
|
|
7981
|
-
|
|
8558
|
+
params.setTimer(setTimeout(() => void tick(false), params.nextDelayMs()));
|
|
7982
8559
|
};
|
|
7983
8560
|
const rearm = () => {
|
|
7984
8561
|
switch (phase) {
|
|
@@ -7988,9 +8565,9 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
7988
8565
|
case "probe-pending":
|
|
7989
8566
|
return;
|
|
7990
8567
|
case "steady-pending":
|
|
7991
|
-
if (
|
|
7992
|
-
clearTimeout(
|
|
7993
|
-
|
|
8568
|
+
if (params.getTimer()) {
|
|
8569
|
+
clearTimeout(params.getTimer());
|
|
8570
|
+
params.setTimer(null);
|
|
7994
8571
|
}
|
|
7995
8572
|
rearmRequested = false;
|
|
7996
8573
|
armProbe();
|
|
@@ -8004,45 +8581,45 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
8004
8581
|
const tick = async (isProbe) => {
|
|
8005
8582
|
phase = "tick-in-flight";
|
|
8006
8583
|
try {
|
|
8007
|
-
const usage = await
|
|
8008
|
-
const result = await
|
|
8584
|
+
const usage = await params.fetchUsage();
|
|
8585
|
+
const result = await params.report(usage);
|
|
8009
8586
|
if (result.ok) {
|
|
8010
8587
|
if (consecutiveFailures > 0) {
|
|
8011
8588
|
logActivity(state, {
|
|
8012
8589
|
type: "info",
|
|
8013
8590
|
level: "info",
|
|
8014
|
-
message:
|
|
8591
|
+
message: `${params.label} usage reporting recovered`
|
|
8015
8592
|
});
|
|
8016
8593
|
}
|
|
8017
8594
|
consecutiveFailures = 0;
|
|
8018
8595
|
logActivity(state, {
|
|
8019
8596
|
type: "info",
|
|
8020
8597
|
level: "debug",
|
|
8021
|
-
message:
|
|
8598
|
+
message: `Reported ${params.label} usage to Evident`
|
|
8022
8599
|
});
|
|
8023
8600
|
} else {
|
|
8024
8601
|
consecutiveFailures++;
|
|
8025
8602
|
logActivity(state, {
|
|
8026
8603
|
type: "info",
|
|
8027
|
-
level:
|
|
8028
|
-
message: `Failed to report
|
|
8604
|
+
level: params.failureLogLevel(consecutiveFailures),
|
|
8605
|
+
message: `Failed to report ${params.label} usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
|
|
8029
8606
|
});
|
|
8030
8607
|
}
|
|
8031
8608
|
scheduleNextTick();
|
|
8032
8609
|
} catch (error2) {
|
|
8033
|
-
if (
|
|
8610
|
+
if (params.isLocalCredentialProblem(error2)) {
|
|
8034
8611
|
if (mode === "on") {
|
|
8035
8612
|
logActivity(state, {
|
|
8036
8613
|
type: "info",
|
|
8037
8614
|
level: "warn",
|
|
8038
|
-
message:
|
|
8615
|
+
message: `${params.label} usage reporting is forced on but no usable login was found \u2014 ${params.forcedOnHint}; reporting will keep retrying`
|
|
8039
8616
|
});
|
|
8040
8617
|
scheduleNextTick();
|
|
8041
8618
|
} else if (isProbe) {
|
|
8042
8619
|
logActivity(state, {
|
|
8043
8620
|
type: "info",
|
|
8044
8621
|
level: "debug",
|
|
8045
|
-
message:
|
|
8622
|
+
message: `${params.label} usage reporting: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
8046
8623
|
});
|
|
8047
8624
|
phase = "dormant";
|
|
8048
8625
|
if (rearmRequested) rearm();
|
|
@@ -8050,7 +8627,7 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
8050
8627
|
logActivity(state, {
|
|
8051
8628
|
type: "info",
|
|
8052
8629
|
level: "debug",
|
|
8053
|
-
message:
|
|
8630
|
+
message: `${params.label} usage reporting: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
8054
8631
|
});
|
|
8055
8632
|
scheduleNextTick();
|
|
8056
8633
|
}
|
|
@@ -8059,8 +8636,8 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
8059
8636
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
8060
8637
|
logActivity(state, {
|
|
8061
8638
|
type: "info",
|
|
8062
|
-
level:
|
|
8063
|
-
message:
|
|
8639
|
+
level: params.failureLogLevel(consecutiveFailures),
|
|
8640
|
+
message: `${params.label} usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
|
|
8064
8641
|
});
|
|
8065
8642
|
scheduleNextTick();
|
|
8066
8643
|
}
|
|
@@ -8069,6 +8646,34 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
8069
8646
|
armProbe();
|
|
8070
8647
|
return rearm;
|
|
8071
8648
|
}
|
|
8649
|
+
function scheduleClaudeUsageReporting(state, options) {
|
|
8650
|
+
return scheduleUsageReporting(state, {
|
|
8651
|
+
label: "Claude",
|
|
8652
|
+
resolved: resolveClaudeUsageReportingMode(options.claudeUsageReporting, process.env),
|
|
8653
|
+
offFlagHint: "--claude-usage-reporting off",
|
|
8654
|
+
getTimer: () => state.claudeUsageTimer,
|
|
8655
|
+
setTimer: (timer) => {
|
|
8656
|
+
state.claudeUsageTimer = timer;
|
|
8657
|
+
},
|
|
8658
|
+
fetchUsage: async () => {
|
|
8659
|
+
const usage = await getClaudeUsage();
|
|
8660
|
+
if (usage.ownerLookupError) {
|
|
8661
|
+
logActivity(state, {
|
|
8662
|
+
type: "info",
|
|
8663
|
+
level: "debug",
|
|
8664
|
+
message: `Claude usage owner lookup failed: ${usage.ownerLookupError}`
|
|
8665
|
+
});
|
|
8666
|
+
}
|
|
8667
|
+
return usage;
|
|
8668
|
+
},
|
|
8669
|
+
report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
|
|
8670
|
+
isLocalCredentialProblem,
|
|
8671
|
+
forcedOnHint: "run `claude` to sign in",
|
|
8672
|
+
firstDelayMs: () => FIRST_REPORT_DELAY_MS,
|
|
8673
|
+
nextDelayMs: nextReportDelayMs,
|
|
8674
|
+
failureLogLevel: claudeUsageFailureLogLevel
|
|
8675
|
+
});
|
|
8676
|
+
}
|
|
8072
8677
|
var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
|
|
8073
8678
|
var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
8074
8679
|
var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
@@ -8092,7 +8697,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
8092
8697
|
});
|
|
8093
8698
|
return;
|
|
8094
8699
|
}
|
|
8095
|
-
const collect = createResourceUsageCollector(
|
|
8700
|
+
const collect = createResourceUsageCollector(homedir4());
|
|
8096
8701
|
let consecutiveFailures = 0;
|
|
8097
8702
|
const tick = async () => {
|
|
8098
8703
|
try {
|
|
@@ -8193,6 +8798,11 @@ async function cleanup(state, opts = {}) {
|
|
|
8193
8798
|
state.claudeUsageTimer = null;
|
|
8194
8799
|
}
|
|
8195
8800
|
state.claudeUsageRearm = null;
|
|
8801
|
+
if (state.openaiUsageTimer) {
|
|
8802
|
+
clearTimeout(state.openaiUsageTimer);
|
|
8803
|
+
state.openaiUsageTimer = null;
|
|
8804
|
+
}
|
|
8805
|
+
state.openaiUsageRearm = null;
|
|
8196
8806
|
if (state.resourceUsageTimer) {
|
|
8197
8807
|
clearTimeout(state.resourceUsageTimer);
|
|
8198
8808
|
state.resourceUsageTimer = null;
|
|
@@ -8227,15 +8837,31 @@ async function cleanup(state, opts = {}) {
|
|
|
8227
8837
|
}
|
|
8228
8838
|
if (state.opencodeProcess) {
|
|
8229
8839
|
const opencodeProcess = state.opencodeProcess;
|
|
8230
|
-
|
|
8840
|
+
const result = await timeShutdownPhase(
|
|
8841
|
+
state,
|
|
8842
|
+
durations,
|
|
8843
|
+
"opencode_stop",
|
|
8844
|
+
() => stopOpenCodeAndWait(opencodeProcess, CHILD_STOP_TIMEOUT_MS)
|
|
8845
|
+
);
|
|
8231
8846
|
if (state.interactive) {
|
|
8232
|
-
logActivity(state, { type: "info", message:
|
|
8847
|
+
logActivity(state, { type: "info", message: `Stopped OpenCode process (${result.outcome})` });
|
|
8233
8848
|
displayStatus(state);
|
|
8234
8849
|
} else {
|
|
8235
|
-
log2(state,
|
|
8850
|
+
log2(state, `Stopped OpenCode process (${result.outcome})`);
|
|
8236
8851
|
}
|
|
8237
8852
|
state.opencodeProcess = null;
|
|
8238
8853
|
}
|
|
8854
|
+
if (state.litestreamProcess) {
|
|
8855
|
+
const litestreamProcess = state.litestreamProcess;
|
|
8856
|
+
const result = await timeShutdownPhase(
|
|
8857
|
+
state,
|
|
8858
|
+
durations,
|
|
8859
|
+
"litestream_stop",
|
|
8860
|
+
() => stopSessionDbReplication(litestreamProcess, CHILD_STOP_TIMEOUT_MS)
|
|
8861
|
+
);
|
|
8862
|
+
log2(state, `Stopped litestream replication (${result.outcome})`);
|
|
8863
|
+
state.litestreamProcess = null;
|
|
8864
|
+
}
|
|
8239
8865
|
return durations;
|
|
8240
8866
|
}
|
|
8241
8867
|
async function run(options) {
|
|
@@ -8244,7 +8870,7 @@ async function run(options) {
|
|
|
8244
8870
|
let fileSyncDirectories;
|
|
8245
8871
|
try {
|
|
8246
8872
|
logLevel = resolveLogLevel(options);
|
|
8247
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
8873
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir4());
|
|
8248
8874
|
} catch (error2) {
|
|
8249
8875
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
8250
8876
|
if (options.json) {
|
|
@@ -8269,6 +8895,7 @@ async function run(options) {
|
|
|
8269
8895
|
opencodeConnected: false,
|
|
8270
8896
|
opencodeVersion: null,
|
|
8271
8897
|
opencodeProcess: null,
|
|
8898
|
+
litestreamProcess: null,
|
|
8272
8899
|
connection: null,
|
|
8273
8900
|
channelDriver: null,
|
|
8274
8901
|
running: true,
|
|
@@ -8279,6 +8906,8 @@ async function run(options) {
|
|
|
8279
8906
|
sessionCleanupTimers: [],
|
|
8280
8907
|
claudeUsageTimer: null,
|
|
8281
8908
|
claudeUsageRearm: null,
|
|
8909
|
+
openaiUsageTimer: null,
|
|
8910
|
+
openaiUsageRearm: null,
|
|
8282
8911
|
resourceUsageTimer: null,
|
|
8283
8912
|
authHeader: ""
|
|
8284
8913
|
};
|
|
@@ -8474,6 +9103,7 @@ async function run(options) {
|
|
|
8474
9103
|
} else {
|
|
8475
9104
|
log2(state, "Not running in a MicroVM (MICROVM_ID unset) \u2014 nothing to report", "debug");
|
|
8476
9105
|
}
|
|
9106
|
+
reportSessionDbRecovery(state);
|
|
8477
9107
|
const { timeoutMs: opencodeStartTimeoutMs, warnings: opencodeStartTimeoutWarnings } = resolveOpenCodeStartTimeoutMs(options, process.env);
|
|
8478
9108
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
8479
9109
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
@@ -8530,6 +9160,40 @@ async function run(options) {
|
|
|
8530
9160
|
ocSpinner?.fail(error2.message);
|
|
8531
9161
|
throw error2;
|
|
8532
9162
|
}
|
|
9163
|
+
if (options.litestreamConfig) {
|
|
9164
|
+
const litestreamProcess = startSessionDbReplication(options.litestreamConfig);
|
|
9165
|
+
state.litestreamProcess = litestreamProcess;
|
|
9166
|
+
let failureHandled = false;
|
|
9167
|
+
const failRunForReplication = (message) => {
|
|
9168
|
+
if (failureHandled || state.shuttingDown || !state.running) return;
|
|
9169
|
+
failureHandled = true;
|
|
9170
|
+
state.shuttingDown = true;
|
|
9171
|
+
logActivity(state, { type: "error", error: message });
|
|
9172
|
+
if (state.interactive) displayStatus(state);
|
|
9173
|
+
void (async () => {
|
|
9174
|
+
try {
|
|
9175
|
+
await cleanup(state);
|
|
9176
|
+
await shutdownTelemetry();
|
|
9177
|
+
} catch (error2) {
|
|
9178
|
+
console.error(
|
|
9179
|
+
`[run] replication failure cleanup failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
9180
|
+
);
|
|
9181
|
+
}
|
|
9182
|
+
process.exit(1);
|
|
9183
|
+
})();
|
|
9184
|
+
};
|
|
9185
|
+
litestreamProcess.on("exit", (code, signal) => {
|
|
9186
|
+
failRunForReplication(
|
|
9187
|
+
`Litestream replication stopped unexpectedly (code: ${code ?? "null"}, signal: ${signal ?? "none"})`
|
|
9188
|
+
);
|
|
9189
|
+
});
|
|
9190
|
+
litestreamProcess.on("error", (error2) => {
|
|
9191
|
+
failRunForReplication(
|
|
9192
|
+
`Litestream replication failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
9193
|
+
);
|
|
9194
|
+
});
|
|
9195
|
+
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
9196
|
+
}
|
|
8533
9197
|
const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
|
|
8534
9198
|
const channelDriver = new ChannelDriver({
|
|
8535
9199
|
agentId: state.agentId,
|
|
@@ -8541,7 +9205,7 @@ async function run(options) {
|
|
|
8541
9205
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
8542
9206
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
8543
9207
|
fileSyncDirectories,
|
|
8544
|
-
homeDir:
|
|
9208
|
+
homeDir: homedir4(),
|
|
8545
9209
|
maxActiveSessions,
|
|
8546
9210
|
log: (entry) => (
|
|
8547
9211
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -8679,6 +9343,22 @@ async function run(options) {
|
|
|
8679
9343
|
}
|
|
8680
9344
|
scheduleSessionCleanup(state, channelDriver, options);
|
|
8681
9345
|
state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
|
|
9346
|
+
state.openaiUsageRearm = scheduleUsageReporting(state, {
|
|
9347
|
+
label: "OpenAI",
|
|
9348
|
+
resolved: resolveOpenAiUsageReportingMode(options.openaiUsageReporting, process.env),
|
|
9349
|
+
offFlagHint: "--openai-usage-reporting off",
|
|
9350
|
+
getTimer: () => state.openaiUsageTimer,
|
|
9351
|
+
setTimer: (timer) => {
|
|
9352
|
+
state.openaiUsageTimer = timer;
|
|
9353
|
+
},
|
|
9354
|
+
fetchUsage: () => getOpenAiUsage(state.port),
|
|
9355
|
+
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
9356
|
+
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
9357
|
+
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
9358
|
+
firstDelayMs: firstReportDelayMs,
|
|
9359
|
+
nextDelayMs: usageReportDelayMs,
|
|
9360
|
+
failureLogLevel: usageReportFailureLogLevel
|
|
9361
|
+
});
|
|
8682
9362
|
scheduleResourceUsageReporting(state, options);
|
|
8683
9363
|
if (!interactive || state.json) {
|
|
8684
9364
|
log2(state, "Driving channel messages...");
|
|
@@ -8760,6 +9440,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
8760
9440
|
).option(
|
|
8761
9441
|
"--claude-usage-reporting <mode>",
|
|
8762
9442
|
"Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
9443
|
+
).option(
|
|
9444
|
+
"--openai-usage-reporting <mode>",
|
|
9445
|
+
"Report OpenAI plan usage to Evident: auto | on | off (default: auto). Env: EVIDENT_OPENAI_USAGE_REPORTING"
|
|
8763
9446
|
).option(
|
|
8764
9447
|
"--no-resource-usage-reporting",
|
|
8765
9448
|
"Don't report this machine's CPU and memory usage to Evident (reporting is on by default). Env: EVIDENT_RESOURCE_USAGE_REPORTING=off"
|
|
@@ -8771,6 +9454,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
8771
9454
|
).option(
|
|
8772
9455
|
"--tunnel-ready-file <path>",
|
|
8773
9456
|
"Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
|
|
9457
|
+
).option(
|
|
9458
|
+
"--litestream-config <path>",
|
|
9459
|
+
"Replicate the OpenCode session database with `litestream replicate` using this config file (written by the runner image). Omit to disable replication."
|
|
8774
9460
|
).action(
|
|
8775
9461
|
(options) => {
|
|
8776
9462
|
run({
|
|
@@ -8795,13 +9481,15 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
8795
9481
|
// Raw string — the resolver in run.ts single-sources parsing
|
|
8796
9482
|
// (resolveClaudeUsageReportingMode).
|
|
8797
9483
|
claudeUsageReporting: options.claudeUsageReporting,
|
|
9484
|
+
openaiUsageReporting: options.openaiUsageReporting,
|
|
8798
9485
|
// Raw value — resolution is single-sourced in run.ts's
|
|
8799
9486
|
// resolveResourceUsageReportingEnabled.
|
|
8800
9487
|
resourceUsageReporting: options.resourceUsageReporting,
|
|
8801
9488
|
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
8802
9489
|
// resolveFileSyncDirectories.
|
|
8803
9490
|
enableFileSyncTo: options.enableFileSyncTo,
|
|
8804
|
-
tunnelReadyFile: options.tunnelReadyFile
|
|
9491
|
+
tunnelReadyFile: options.tunnelReadyFile,
|
|
9492
|
+
litestreamConfig: options.litestreamConfig
|
|
8805
9493
|
});
|
|
8806
9494
|
}
|
|
8807
9495
|
);
|