@evident-ai/cli 3.4.0 → 3.4.1-dev.11f0c53
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 +818 -114
- 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
|
|
@@ -1900,6 +2208,7 @@ async function createOpenCodeSession(port, directory) {
|
|
|
1900
2208
|
return data.id;
|
|
1901
2209
|
}
|
|
1902
2210
|
async function getModelAttachmentCapability(port, model) {
|
|
2211
|
+
const { model: baseModel } = splitModelVariant(model);
|
|
1903
2212
|
try {
|
|
1904
2213
|
const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
|
|
1905
2214
|
if (!res.ok) {
|
|
@@ -1916,9 +2225,9 @@ async function getModelAttachmentCapability(port, model) {
|
|
|
1916
2225
|
);
|
|
1917
2226
|
return null;
|
|
1918
2227
|
}
|
|
1919
|
-
const slash =
|
|
1920
|
-
const providerId = slash > 0 ?
|
|
1921
|
-
let modelId = slash > 0 ?
|
|
2228
|
+
const slash = baseModel ? baseModel.indexOf("/") : -1;
|
|
2229
|
+
const providerId = slash > 0 ? baseModel.slice(0, slash) : void 0;
|
|
2230
|
+
let modelId = slash > 0 ? baseModel.slice(slash + 1) : void 0;
|
|
1922
2231
|
const defaults2 = body?.default && typeof body.default === "object" ? body.default : void 0;
|
|
1923
2232
|
let provider = providerId ? providers.find((p) => p?.id === providerId) : void 0;
|
|
1924
2233
|
if (!provider && !providerId) {
|
|
@@ -1998,6 +2307,29 @@ async function buildFileParts(attachments, capable) {
|
|
|
1998
2307
|
}
|
|
1999
2308
|
return { parts, outcomes, capabilityUnknown };
|
|
2000
2309
|
}
|
|
2310
|
+
function splitModelVariant(raw) {
|
|
2311
|
+
const value = raw?.trim();
|
|
2312
|
+
if (!value) return {};
|
|
2313
|
+
const hashIndex = value.indexOf("#");
|
|
2314
|
+
if (hashIndex === -1) return { model: value };
|
|
2315
|
+
const model = value.slice(0, hashIndex).trim() || void 0;
|
|
2316
|
+
const variant = value.slice(hashIndex + 1).trim() || void 0;
|
|
2317
|
+
return { model, variant };
|
|
2318
|
+
}
|
|
2319
|
+
function applyModelOptions(body, options) {
|
|
2320
|
+
if (options?.agent) body.agent = options.agent;
|
|
2321
|
+
const { model, variant } = splitModelVariant(options?.model);
|
|
2322
|
+
if (model) {
|
|
2323
|
+
const slashIndex = model.indexOf("/");
|
|
2324
|
+
if (slashIndex !== -1) {
|
|
2325
|
+
body.model = {
|
|
2326
|
+
providerID: model.substring(0, slashIndex),
|
|
2327
|
+
modelID: model.substring(slashIndex + 1)
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
if (variant) body.variant = variant;
|
|
2332
|
+
}
|
|
2001
2333
|
function messageText(m) {
|
|
2002
2334
|
if (!m || !Array.isArray(m.parts)) return "";
|
|
2003
2335
|
return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
@@ -2022,18 +2354,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
2022
2354
|
const body = {
|
|
2023
2355
|
parts
|
|
2024
2356
|
};
|
|
2025
|
-
|
|
2026
|
-
body.agent = options.agent;
|
|
2027
|
-
}
|
|
2028
|
-
if (options?.model) {
|
|
2029
|
-
const slashIndex = options.model.indexOf("/");
|
|
2030
|
-
if (slashIndex !== -1) {
|
|
2031
|
-
body.model = {
|
|
2032
|
-
providerID: options.model.substring(0, slashIndex),
|
|
2033
|
-
modelID: options.model.substring(slashIndex + 1)
|
|
2034
|
-
};
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2357
|
+
applyModelOptions(body, options);
|
|
2037
2358
|
const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/prompt_async`, {
|
|
2038
2359
|
method: "POST",
|
|
2039
2360
|
headers: { "Content-Type": "application/json" },
|
|
@@ -2041,7 +2362,10 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
2041
2362
|
});
|
|
2042
2363
|
if (res.status < 200 || res.status >= 300) {
|
|
2043
2364
|
const text = await res.text().catch(() => "");
|
|
2044
|
-
|
|
2365
|
+
const { variant } = splitModelVariant(options?.model);
|
|
2366
|
+
throw new Error(
|
|
2367
|
+
`OpenCode prompt_async failed: HTTP ${res.status}${text ? `: ${text}` : ""}${variant ? ` (variant: ${variant})` : ""}`
|
|
2368
|
+
);
|
|
2045
2369
|
}
|
|
2046
2370
|
const READ_BACK_ATTEMPTS = 5;
|
|
2047
2371
|
const READ_BACK_DELAY_MS = 150;
|
|
@@ -2196,7 +2520,7 @@ function isPreamblePinnedRunning(messages, userMessageId) {
|
|
|
2196
2520
|
return completedOf(reply) != null && finishOf(reply) === "tool-calls";
|
|
2197
2521
|
}
|
|
2198
2522
|
function isB2AbandonmentConfirmed(params) {
|
|
2199
|
-
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false;
|
|
2523
|
+
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
|
|
2200
2524
|
}
|
|
2201
2525
|
function isAmbiguousTerminalFinish(m) {
|
|
2202
2526
|
if (completedOf(m) == null) return false;
|
|
@@ -2209,7 +2533,7 @@ function isAmbiguousFinishPinnedRunning(messages, userMessageId) {
|
|
|
2209
2533
|
return isAmbiguousTerminalFinish(reply);
|
|
2210
2534
|
}
|
|
2211
2535
|
function isAmbiguousFinishResolved(params) {
|
|
2212
|
-
return params.sessionOngoing === false || params.pinnedForMs >= params.maxPinnedMs;
|
|
2536
|
+
return params.sessionOngoing === false || params.sessionOngoing !== true && params.pinnedForMs >= params.maxPinnedMs;
|
|
2213
2537
|
}
|
|
2214
2538
|
function messageError(messages, userMessageId) {
|
|
2215
2539
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
@@ -2419,10 +2743,10 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
2419
2743
|
|
|
2420
2744
|
// src/lib/opencode/session-db-size.ts
|
|
2421
2745
|
import { statSync as statSync2 } from "fs";
|
|
2422
|
-
import { join as
|
|
2746
|
+
import { join as join3 } from "path";
|
|
2423
2747
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
2424
2748
|
function statSessionDbBytes(homeDir) {
|
|
2425
|
-
const dbPath =
|
|
2749
|
+
const dbPath = join3(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
2426
2750
|
try {
|
|
2427
2751
|
return statSync2(dbPath).size;
|
|
2428
2752
|
} catch (err) {
|
|
@@ -2991,6 +3315,193 @@ function writeTunnelReadyMarker(path, agentId) {
|
|
|
2991
3315
|
}
|
|
2992
3316
|
}
|
|
2993
3317
|
|
|
3318
|
+
// src/lib/replication.ts
|
|
3319
|
+
import { spawn as spawn2 } from "child_process";
|
|
3320
|
+
function startSessionDbReplication(configPath) {
|
|
3321
|
+
return spawn2("litestream", ["replicate", "-config", configPath], {
|
|
3322
|
+
stdio: "inherit"
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
async function stopSessionDbReplication(child, timeoutMs) {
|
|
3326
|
+
return stopProcessAndWait(
|
|
3327
|
+
child,
|
|
3328
|
+
timeoutMs,
|
|
3329
|
+
() => child.kill("SIGTERM"),
|
|
3330
|
+
() => child.kill("SIGKILL")
|
|
3331
|
+
);
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
// src/lib/openai-usage.ts
|
|
3335
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3336
|
+
import { homedir as homedir2 } from "os";
|
|
3337
|
+
import { join as join4 } from "path";
|
|
3338
|
+
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
3339
|
+
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
3340
|
+
var OpenAiUsageError = class extends Error {
|
|
3341
|
+
constructor(message, reason) {
|
|
3342
|
+
super(message);
|
|
3343
|
+
this.reason = reason;
|
|
3344
|
+
}
|
|
3345
|
+
};
|
|
3346
|
+
function isLocalCredentialProblem2(err) {
|
|
3347
|
+
return err instanceof OpenAiUsageError && (err.reason === "no_credentials" || err.reason === "credentials_expired");
|
|
3348
|
+
}
|
|
3349
|
+
function readOpenCodeChatGptCredentials() {
|
|
3350
|
+
try {
|
|
3351
|
+
const raw = readFileSync3(join4(homedir2(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
3352
|
+
let parsed;
|
|
3353
|
+
try {
|
|
3354
|
+
parsed = JSON.parse(raw);
|
|
3355
|
+
} catch {
|
|
3356
|
+
return null;
|
|
3357
|
+
}
|
|
3358
|
+
const entry = parsed.openai;
|
|
3359
|
+
if (entry?.type !== "oauth" || typeof entry.access !== "string" || !entry.access || typeof entry.expires !== "number") {
|
|
3360
|
+
return null;
|
|
3361
|
+
}
|
|
3362
|
+
return { accessToken: entry.access, expiresAt: entry.expires };
|
|
3363
|
+
} catch (err) {
|
|
3364
|
+
const code = err.code;
|
|
3365
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
3366
|
+
console.warn(
|
|
3367
|
+
`readOpenCodeChatGptCredentials: reading auth.json failed (${code ?? "unknown"})`
|
|
3368
|
+
);
|
|
3369
|
+
}
|
|
3370
|
+
return null;
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
function toWindow2(headers, name) {
|
|
3374
|
+
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
3375
|
+
const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
|
|
3376
|
+
if (utilizationHeader == null || utilizationHeader === "" || windowMinutesHeader == null) {
|
|
3377
|
+
return null;
|
|
3378
|
+
}
|
|
3379
|
+
const utilization = Number(utilizationHeader);
|
|
3380
|
+
const windowMinutes = Number(windowMinutesHeader);
|
|
3381
|
+
if (!Number.isFinite(utilization) || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
|
|
3382
|
+
return null;
|
|
3383
|
+
}
|
|
3384
|
+
const resetAtHeader = headers.get(`x-codex-${name}-reset-at`);
|
|
3385
|
+
const resetSeconds = resetAtHeader == null || resetAtHeader === "" ? NaN : Number(resetAtHeader);
|
|
3386
|
+
const resetsAt = Number.isFinite(resetSeconds) ? new Date(resetSeconds * 1e3).toISOString() : null;
|
|
3387
|
+
return { utilization: Math.min(100, Math.max(0, utilization)), windowMinutes, resetsAt };
|
|
3388
|
+
}
|
|
3389
|
+
function parseCodexUsageHeaders(headers) {
|
|
3390
|
+
return {
|
|
3391
|
+
primary: toWindow2(headers, "primary"),
|
|
3392
|
+
secondary: toWindow2(headers, "secondary"),
|
|
3393
|
+
hasCredits: headers.has("x-codex-credits-has-credits") ? headers.get("x-codex-credits-has-credits")?.toLowerCase() === "true" : null,
|
|
3394
|
+
creditsUnlimited: headers.has("x-codex-credits-unlimited") ? headers.get("x-codex-credits-unlimited")?.toLowerCase() === "true" : null
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
function normalizeProbeModel(model) {
|
|
3398
|
+
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
3399
|
+
}
|
|
3400
|
+
async function resolveProbeModels(port) {
|
|
3401
|
+
try {
|
|
3402
|
+
const res = await withRequestTimeout(
|
|
3403
|
+
fetch,
|
|
3404
|
+
REQUEST_TIMEOUT_MS
|
|
3405
|
+
)(`${opencodeBase(port)}/config/providers`);
|
|
3406
|
+
if (!res.ok) {
|
|
3407
|
+
console.error(
|
|
3408
|
+
`[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
3409
|
+
);
|
|
3410
|
+
return [];
|
|
3411
|
+
}
|
|
3412
|
+
const body = await res.json();
|
|
3413
|
+
const provider = body?.providers?.find((candidate) => candidate?.id === "openai");
|
|
3414
|
+
if (!provider || !provider.models || typeof provider.models !== "object") return [];
|
|
3415
|
+
const candidates = [
|
|
3416
|
+
...typeof body?.default?.openai === "string" ? [body.default.openai] : [],
|
|
3417
|
+
...Object.keys(provider.models)
|
|
3418
|
+
].map(normalizeProbeModel);
|
|
3419
|
+
return [...new Set(candidates)].slice(0, 4);
|
|
3420
|
+
} catch (err) {
|
|
3421
|
+
console.error(
|
|
3422
|
+
`[resolveProbeModels] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
3423
|
+
);
|
|
3424
|
+
return [];
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
function hasPrimaryHeaders(headers) {
|
|
3428
|
+
return [
|
|
3429
|
+
"x-codex-primary-used-percent",
|
|
3430
|
+
"x-codex-primary-window-minutes",
|
|
3431
|
+
"x-codex-primary-reset-at"
|
|
3432
|
+
].some((name) => headers.has(name));
|
|
3433
|
+
}
|
|
3434
|
+
async function getOpenAiUsage(port) {
|
|
3435
|
+
const credentials2 = readOpenCodeChatGptCredentials();
|
|
3436
|
+
if (!credentials2) {
|
|
3437
|
+
throw new OpenAiUsageError(
|
|
3438
|
+
"No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
|
|
3439
|
+
"no_credentials"
|
|
3440
|
+
);
|
|
3441
|
+
}
|
|
3442
|
+
if (credentials2.expiresAt < Date.now()) {
|
|
3443
|
+
throw new OpenAiUsageError(
|
|
3444
|
+
"ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
|
|
3445
|
+
"credentials_expired"
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
const models = await resolveProbeModels(port);
|
|
3449
|
+
if (models.length === 0) {
|
|
3450
|
+
throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
|
|
3451
|
+
}
|
|
3452
|
+
let lastStatus;
|
|
3453
|
+
for (const model of models) {
|
|
3454
|
+
let res;
|
|
3455
|
+
try {
|
|
3456
|
+
res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
|
|
3457
|
+
method: "POST",
|
|
3458
|
+
headers: {
|
|
3459
|
+
Authorization: `Bearer ${credentials2.accessToken}`,
|
|
3460
|
+
"Content-Type": "application/json"
|
|
3461
|
+
},
|
|
3462
|
+
body: JSON.stringify({ model, store: false, stream: true })
|
|
3463
|
+
});
|
|
3464
|
+
} catch (err) {
|
|
3465
|
+
throw new OpenAiUsageError(
|
|
3466
|
+
`OpenAI usage probe request failed: ${err instanceof Error ? err.message : String(err)}.`,
|
|
3467
|
+
"request_failed"
|
|
3468
|
+
);
|
|
3469
|
+
}
|
|
3470
|
+
try {
|
|
3471
|
+
lastStatus = res.status;
|
|
3472
|
+
if (hasPrimaryHeaders(res.headers)) {
|
|
3473
|
+
const usage = parseCodexUsageHeaders(res.headers);
|
|
3474
|
+
if (!usage.primary && !usage.secondary) {
|
|
3475
|
+
throw new OpenAiUsageError(
|
|
3476
|
+
`OpenAI usage probe returned no usable window (HTTP ${res.status}).`,
|
|
3477
|
+
"no_usable_window"
|
|
3478
|
+
);
|
|
3479
|
+
}
|
|
3480
|
+
return usage;
|
|
3481
|
+
}
|
|
3482
|
+
if (res.status === 401) {
|
|
3483
|
+
throw new OpenAiUsageError(
|
|
3484
|
+
"ChatGPT credentials have expired (HTTP 401).",
|
|
3485
|
+
"credentials_expired"
|
|
3486
|
+
);
|
|
3487
|
+
}
|
|
3488
|
+
if (res.status === 403 || res.status === 429) {
|
|
3489
|
+
throw new OpenAiUsageError(
|
|
3490
|
+
`OpenAI usage probe was blocked (HTTP ${res.status}).`,
|
|
3491
|
+
"probe_blocked"
|
|
3492
|
+
);
|
|
3493
|
+
}
|
|
3494
|
+
} finally {
|
|
3495
|
+
await res.body?.cancel().catch(() => {
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
throw new OpenAiUsageError(
|
|
3500
|
+
`OpenAI usage probe failed: HTTP ${lastStatus ?? "unknown"}.`,
|
|
3501
|
+
"request_failed"
|
|
3502
|
+
);
|
|
3503
|
+
}
|
|
3504
|
+
|
|
2994
3505
|
// src/lib/reporting-schedule.ts
|
|
2995
3506
|
function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
|
|
2996
3507
|
const jitterRangeMs = baseMs * jitterFraction;
|
|
@@ -2999,6 +3510,31 @@ function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
|
|
|
2999
3510
|
function firstReportDelayMs(random = Math.random) {
|
|
3000
3511
|
return 5e3 + random() * 1e4;
|
|
3001
3512
|
}
|
|
3513
|
+
var VALID_USAGE_REPORTING_MODES = ["auto", "on", "off"];
|
|
3514
|
+
function resolveUsageReportingMode(flagValue, env, names) {
|
|
3515
|
+
const raw = flagValue ?? env[names.envVar];
|
|
3516
|
+
if (raw === void 0 || raw === "") return { mode: "auto", warnings: [] };
|
|
3517
|
+
const normalized = raw.trim().toLowerCase();
|
|
3518
|
+
if (VALID_USAGE_REPORTING_MODES.includes(normalized)) {
|
|
3519
|
+
return { mode: normalized, warnings: [] };
|
|
3520
|
+
}
|
|
3521
|
+
const source = flagValue !== void 0 ? names.flagName : names.envVar;
|
|
3522
|
+
return {
|
|
3523
|
+
mode: "auto",
|
|
3524
|
+
warnings: [
|
|
3525
|
+
`Ignoring invalid ${source} "${raw}": expected one of ${VALID_USAGE_REPORTING_MODES.join(", ")}; using auto`
|
|
3526
|
+
]
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3529
|
+
var BASE_USAGE_REPORT_DELAY_MS = 10 * 6e4;
|
|
3530
|
+
var USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
3531
|
+
function usageReportDelayMs(random = Math.random) {
|
|
3532
|
+
return jitteredDelayMs(BASE_USAGE_REPORT_DELAY_MS, USAGE_REPORT_DELAY_JITTER_FRACTION, random);
|
|
3533
|
+
}
|
|
3534
|
+
var USAGE_REPORT_FAILURE_REESCALATION_TICKS = 6;
|
|
3535
|
+
function usageReportFailureLogLevel(consecutiveFailures) {
|
|
3536
|
+
return reportFailureLogLevel(consecutiveFailures, USAGE_REPORT_FAILURE_REESCALATION_TICKS);
|
|
3537
|
+
}
|
|
3002
3538
|
function reportFailureLogLevel(consecutiveFailures, reescalationTicks) {
|
|
3003
3539
|
return consecutiveFailures === 1 || consecutiveFailures % reescalationTicks === 0 ? "warn" : "debug";
|
|
3004
3540
|
}
|
|
@@ -3007,33 +3543,26 @@ function failureStreakSuffix(consecutiveFailures) {
|
|
|
3007
3543
|
}
|
|
3008
3544
|
|
|
3009
3545
|
// src/lib/claude-usage-reporting.ts
|
|
3010
|
-
var VALID_MODES = ["auto", "on", "off"];
|
|
3011
3546
|
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
|
-
};
|
|
3547
|
+
return resolveUsageReportingMode(flagValue, env, {
|
|
3548
|
+
flagName: "--claude-usage-reporting",
|
|
3549
|
+
envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
3550
|
+
});
|
|
3027
3551
|
}
|
|
3028
|
-
var BASE_REPORT_DELAY_MS = 10 * 6e4;
|
|
3029
|
-
var REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
3030
3552
|
function nextReportDelayMs(random = Math.random) {
|
|
3031
|
-
return
|
|
3553
|
+
return usageReportDelayMs(random);
|
|
3032
3554
|
}
|
|
3033
3555
|
var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
|
|
3034
|
-
var CLAUDE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
3035
3556
|
function claudeUsageFailureLogLevel(consecutiveFailures) {
|
|
3036
|
-
return
|
|
3557
|
+
return usageReportFailureLogLevel(consecutiveFailures);
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
// src/lib/openai-usage-reporting.ts
|
|
3561
|
+
function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
3562
|
+
return resolveUsageReportingMode(flagValue, env, {
|
|
3563
|
+
flagName: "--openai-usage-reporting",
|
|
3564
|
+
envVar: "EVIDENT_OPENAI_USAGE_REPORTING"
|
|
3565
|
+
});
|
|
3037
3566
|
}
|
|
3038
3567
|
|
|
3039
3568
|
// src/lib/resource-usage-reporting.ts
|
|
@@ -3192,15 +3721,15 @@ function createResourceUsageCollector(homeDir) {
|
|
|
3192
3721
|
}
|
|
3193
3722
|
|
|
3194
3723
|
// src/lib/channels/driver.ts
|
|
3195
|
-
import { homedir as
|
|
3724
|
+
import { homedir as homedir3 } from "os";
|
|
3196
3725
|
|
|
3197
3726
|
// src/lib/runner-file-sync.ts
|
|
3198
|
-
import { join as
|
|
3727
|
+
import { join as join6 } from "path";
|
|
3199
3728
|
|
|
3200
3729
|
// src/lib/file-push.ts
|
|
3201
3730
|
import { randomUUID } from "crypto";
|
|
3202
3731
|
import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
|
|
3203
|
-
import { basename, dirname as dirname3, isAbsolute, join as
|
|
3732
|
+
import { basename, dirname as dirname3, isAbsolute, join as join5, relative, resolve as resolve2, sep } from "path";
|
|
3204
3733
|
var FILE_MODE = 384;
|
|
3205
3734
|
var DIRECTORY_MODE = 448;
|
|
3206
3735
|
async function writePushedFile(request) {
|
|
@@ -3233,7 +3762,7 @@ async function writePushedFile(request) {
|
|
|
3233
3762
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
3234
3763
|
dirname3(candidate)
|
|
3235
3764
|
);
|
|
3236
|
-
const realTarget =
|
|
3765
|
+
const realTarget = join5(existingAncestor, ...missingSegments, basename(candidate));
|
|
3237
3766
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
3238
3767
|
if (allowedDirectory === null) {
|
|
3239
3768
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -3269,7 +3798,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
3269
3798
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
3270
3799
|
return null;
|
|
3271
3800
|
}
|
|
3272
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
3801
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join5(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
3273
3802
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
3274
3803
|
return null;
|
|
3275
3804
|
}
|
|
@@ -3342,13 +3871,13 @@ function contains(realDirectory, realTarget) {
|
|
|
3342
3871
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
3343
3872
|
let current = existingAncestor;
|
|
3344
3873
|
for (const segment of missingSegments) {
|
|
3345
|
-
current =
|
|
3874
|
+
current = join5(current, segment);
|
|
3346
3875
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
3347
3876
|
await chmod(current, DIRECTORY_MODE);
|
|
3348
3877
|
}
|
|
3349
3878
|
}
|
|
3350
3879
|
async function writeAtomically(realTarget, content) {
|
|
3351
|
-
const temporaryPath =
|
|
3880
|
+
const temporaryPath = join5(dirname3(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
3352
3881
|
let handle;
|
|
3353
3882
|
try {
|
|
3354
3883
|
handle = await open2(temporaryPath, "wx", FILE_MODE);
|
|
@@ -3390,20 +3919,28 @@ async function syncPendingRunnerFiles(options) {
|
|
|
3390
3919
|
for (const id of options.ackFailures.keys()) {
|
|
3391
3920
|
if (!pendingIds.has(id)) options.ackFailures.delete(id);
|
|
3392
3921
|
}
|
|
3393
|
-
if (pending.length === 0)
|
|
3922
|
+
if (pending.length === 0) {
|
|
3923
|
+
return { applied: 0, claudeCredentialApplied: false, opencodeAuthApplied: false };
|
|
3924
|
+
}
|
|
3394
3925
|
options.log({
|
|
3395
3926
|
level: "info",
|
|
3396
3927
|
message: `Runner file sync: ${pending.length} file(s) queued for this runner`
|
|
3397
3928
|
});
|
|
3398
3929
|
let applied = 0;
|
|
3399
3930
|
let claudeCredentialApplied = false;
|
|
3931
|
+
let opencodeAuthApplied = false;
|
|
3400
3932
|
for (const file of pending) {
|
|
3401
3933
|
if ((options.ackFailures.get(file.id) ?? 0) >= MAX_ACK_ATTEMPTS) continue;
|
|
3402
3934
|
const outcome = await applyOne(options, file);
|
|
3403
3935
|
if (outcome.applied) applied += 1;
|
|
3404
3936
|
if (outcome.claudeCredentialApplied) claudeCredentialApplied = true;
|
|
3937
|
+
if (outcome.opencodeAuthApplied) opencodeAuthApplied = true;
|
|
3405
3938
|
}
|
|
3406
|
-
return {
|
|
3939
|
+
return {
|
|
3940
|
+
applied,
|
|
3941
|
+
claudeCredentialApplied,
|
|
3942
|
+
opencodeAuthApplied
|
|
3943
|
+
};
|
|
3407
3944
|
}
|
|
3408
3945
|
async function listPendingFiles(options) {
|
|
3409
3946
|
let res;
|
|
@@ -3464,10 +4001,18 @@ function asPendingFile(entry) {
|
|
|
3464
4001
|
if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
|
|
3465
4002
|
return { id, path, size };
|
|
3466
4003
|
}
|
|
3467
|
-
var NOT_APPLIED = {
|
|
4004
|
+
var NOT_APPLIED = {
|
|
4005
|
+
applied: false,
|
|
4006
|
+
claudeCredentialApplied: false,
|
|
4007
|
+
opencodeAuthApplied: false
|
|
4008
|
+
};
|
|
3468
4009
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
3469
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
3470
|
-
return expanded ===
|
|
4010
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4011
|
+
return expanded === join6(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
4012
|
+
}
|
|
4013
|
+
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
4014
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4015
|
+
return expanded === join6(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
3471
4016
|
}
|
|
3472
4017
|
async function applyOne(options, file) {
|
|
3473
4018
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -3523,7 +4068,8 @@ async function applyOne(options, file) {
|
|
|
3523
4068
|
await ack(options, file, "applied");
|
|
3524
4069
|
return {
|
|
3525
4070
|
applied: true,
|
|
3526
|
-
claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir)
|
|
4071
|
+
claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir),
|
|
4072
|
+
opencodeAuthApplied: isOpenCodeAuthPath(file.path, options.homeDir)
|
|
3527
4073
|
};
|
|
3528
4074
|
}
|
|
3529
4075
|
function durableDownloadCode(status2) {
|
|
@@ -3977,6 +4523,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3977
4523
|
* that way rather than "fixing" it into a count.
|
|
3978
4524
|
*/
|
|
3979
4525
|
claudeCredentialApplyCount = 0;
|
|
4526
|
+
opencodeAuthApplyCount = 0;
|
|
3980
4527
|
/**
|
|
3981
4528
|
* The currently-executing `drainPending()` promise, or null when idle. Lets a
|
|
3982
4529
|
* graceful shutdown (`waitForInFlight`) await an in-progress drain so a turn it
|
|
@@ -4011,7 +4558,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4011
4558
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
4012
4559
|
this.now = config.now ?? (() => Date.now());
|
|
4013
4560
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
4014
|
-
this.homeDir = config.homeDir ??
|
|
4561
|
+
this.homeDir = config.homeDir ?? homedir3();
|
|
4015
4562
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
4016
4563
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
4017
4564
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
@@ -4081,6 +4628,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4081
4628
|
});
|
|
4082
4629
|
this.appliedFileCount += result.applied;
|
|
4083
4630
|
if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;
|
|
4631
|
+
if (result.opencodeAuthApplied) this.opencodeAuthApplyCount += 1;
|
|
4084
4632
|
return result.applied;
|
|
4085
4633
|
} catch (err) {
|
|
4086
4634
|
this.log({
|
|
@@ -4188,7 +4736,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4188
4736
|
return {
|
|
4189
4737
|
appliedFiles: this.appliedFileCount,
|
|
4190
4738
|
inFlight: this.syncingFiles,
|
|
4191
|
-
claudeCredentialApplies: this.claudeCredentialApplyCount
|
|
4739
|
+
claudeCredentialApplies: this.claudeCredentialApplyCount,
|
|
4740
|
+
opencodeAuthApplyCount: this.opencodeAuthApplyCount
|
|
4192
4741
|
};
|
|
4193
4742
|
}
|
|
4194
4743
|
/**
|
|
@@ -5276,6 +5825,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5276
5825
|
deliveryDeadlineAnchored: false,
|
|
5277
5826
|
b2PinnedSinceMs: 0,
|
|
5278
5827
|
b2LastDescendantCheckMs: 0,
|
|
5828
|
+
b2RootOngoingHeldLogged: false,
|
|
5279
5829
|
b2AbandonedSignalled: false,
|
|
5280
5830
|
ambiguousPinnedSinceMs: 0,
|
|
5281
5831
|
ambiguousResolved: false
|
|
@@ -5370,6 +5920,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5370
5920
|
deliveryDeadlineAnchored: false,
|
|
5371
5921
|
b2PinnedSinceMs: 0,
|
|
5372
5922
|
b2LastDescendantCheckMs: 0,
|
|
5923
|
+
b2RootOngoingHeldLogged: false,
|
|
5373
5924
|
b2AbandonedSignalled: false,
|
|
5374
5925
|
ambiguousPinnedSinceMs: 0,
|
|
5375
5926
|
ambiguousResolved: false
|
|
@@ -5723,6 +6274,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5723
6274
|
if (snapshotReadable) {
|
|
5724
6275
|
inFlight.b2PinnedSinceMs = 0;
|
|
5725
6276
|
inFlight.b2LastDescendantCheckMs = 0;
|
|
6277
|
+
inFlight.b2RootOngoingHeldLogged = false;
|
|
5726
6278
|
inFlight.b2AbandonedSignalled = false;
|
|
5727
6279
|
}
|
|
5728
6280
|
} else {
|
|
@@ -5734,11 +6286,15 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5734
6286
|
const pinnedForMs = this.now() - inFlight.b2PinnedSinceMs;
|
|
5735
6287
|
if (pinnedForMs >= B2_ABANDONMENT_MIN_PINNED_MS && this.now() - inFlight.b2LastDescendantCheckMs >= B2_ABANDONMENT_RECHECK_MS) {
|
|
5736
6288
|
inFlight.b2LastDescendantCheckMs = this.now();
|
|
5737
|
-
const descendantOngoing = await
|
|
6289
|
+
const [descendantOngoing, rootOngoing] = await Promise.all([
|
|
6290
|
+
this.isAnyDescendantSessionOngoing(sessionId),
|
|
6291
|
+
isSessionOngoing(this.port, sessionId)
|
|
6292
|
+
]);
|
|
5738
6293
|
if (isB2AbandonmentConfirmed({
|
|
5739
6294
|
pinnedForMs,
|
|
5740
6295
|
minPinnedMs: B2_ABANDONMENT_MIN_PINNED_MS,
|
|
5741
|
-
descendantOngoing
|
|
6296
|
+
descendantOngoing,
|
|
6297
|
+
rootOngoing
|
|
5742
6298
|
})) {
|
|
5743
6299
|
inFlight.b2AbandonedSignalled = true;
|
|
5744
6300
|
this.log({
|
|
@@ -5747,12 +6303,26 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5747
6303
|
conversation_id: conv.id,
|
|
5748
6304
|
message_id: id
|
|
5749
6305
|
});
|
|
6306
|
+
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
5750
6307
|
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
5751
|
-
watched_for_ms: pinnedForMs
|
|
6308
|
+
watched_for_ms: pinnedForMs,
|
|
6309
|
+
finish: reply?.info?.finish ?? reply?.finish,
|
|
6310
|
+
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
6311
|
+
...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
|
|
6312
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
5752
6313
|
});
|
|
5753
6314
|
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
5754
6315
|
return;
|
|
5755
6316
|
}
|
|
6317
|
+
if (rootOngoing === true && !inFlight.b2RootOngoingHeldLogged) {
|
|
6318
|
+
inFlight.b2RootOngoingHeldLogged = true;
|
|
6319
|
+
this.log({
|
|
6320
|
+
level: "warn",
|
|
6321
|
+
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`,
|
|
6322
|
+
conversation_id: conv.id,
|
|
6323
|
+
message_id: id
|
|
6324
|
+
});
|
|
6325
|
+
}
|
|
5756
6326
|
}
|
|
5757
6327
|
}
|
|
5758
6328
|
const ambiguousPinnedNow = activelyRunning && isAmbiguousFinishPinnedRunning(messages, inFlight.opencodeMessageId);
|
|
@@ -7495,6 +8065,7 @@ var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_
|
|
|
7495
8065
|
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
7496
8066
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
|
|
7497
8067
|
var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
8068
|
+
var CHILD_STOP_TIMEOUT_MS = 1e4;
|
|
7498
8069
|
function resolveLogLevel(options) {
|
|
7499
8070
|
const accepted = Object.keys(LOG_LEVELS);
|
|
7500
8071
|
const validate = (value, source) => {
|
|
@@ -7525,7 +8096,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
7525
8096
|
if (trimmed === "") {
|
|
7526
8097
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
7527
8098
|
}
|
|
7528
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
8099
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join7(homeDir, trimmed.slice(2)) : trimmed;
|
|
7529
8100
|
if (!isAbsolute2(expanded)) {
|
|
7530
8101
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
7531
8102
|
}
|
|
@@ -7623,7 +8194,7 @@ function logActivity(state, entry) {
|
|
|
7623
8194
|
const level = entry.level ?? (entry.type === "error" ? "error" : "info");
|
|
7624
8195
|
if (!meetsThreshold(state, level)) return;
|
|
7625
8196
|
forwardRunnerActivity(
|
|
7626
|
-
{ level, message: entry.message, error: entry.error },
|
|
8197
|
+
{ level, message: entry.message, error: entry.error, metadata: entry.metadata },
|
|
7627
8198
|
{ agentId: state.agentId, authHeader: state.authHeader }
|
|
7628
8199
|
);
|
|
7629
8200
|
const fullEntry = {
|
|
@@ -7643,6 +8214,26 @@ function logActivity(state, entry) {
|
|
|
7643
8214
|
}
|
|
7644
8215
|
}
|
|
7645
8216
|
}
|
|
8217
|
+
function reportSessionDbRecovery(state) {
|
|
8218
|
+
try {
|
|
8219
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir4(), env: process.env });
|
|
8220
|
+
for (const record of report.records) {
|
|
8221
|
+
const activity = buildSessionDbRecoveryActivity(record);
|
|
8222
|
+
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
8223
|
+
logActivity(state, {
|
|
8224
|
+
type: activity.level === "error" ? "error" : "info",
|
|
8225
|
+
level: activity.level,
|
|
8226
|
+
...activity.level === "error" ? { error: activity.message } : { message: activity.message },
|
|
8227
|
+
metadata: activity.metadata
|
|
8228
|
+
});
|
|
8229
|
+
}
|
|
8230
|
+
acknowledgeSessionDbRecoveryReport(report.path);
|
|
8231
|
+
} catch (error2) {
|
|
8232
|
+
console.error(
|
|
8233
|
+
`[run] could not report session-DB recovery activity: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
8234
|
+
);
|
|
8235
|
+
}
|
|
8236
|
+
}
|
|
7646
8237
|
function displayStatus(state) {
|
|
7647
8238
|
if (!state.interactive) return;
|
|
7648
8239
|
const attempt = state.connection?.reconnectAttempt ?? 0;
|
|
@@ -7733,6 +8324,7 @@ async function driveChannels(state, driver) {
|
|
|
7733
8324
|
let lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
|
|
7734
8325
|
let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
|
|
7735
8326
|
let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
|
|
8327
|
+
let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
|
|
7736
8328
|
while (state.running) {
|
|
7737
8329
|
const cycleStartedAtMs = performance.now();
|
|
7738
8330
|
let idleThisCycle = false;
|
|
@@ -7765,6 +8357,10 @@ async function driveChannels(state, driver) {
|
|
|
7765
8357
|
const claudeCredentialApplied = claudeCredentialApplies !== lastSeenClaudeApplies;
|
|
7766
8358
|
lastSeenClaudeApplies = claudeCredentialApplies;
|
|
7767
8359
|
if (claudeCredentialApplied) state.claudeUsageRearm?.();
|
|
8360
|
+
const opencodeAuthApplies = fileActivitySnapshot.opencodeAuthApplyCount;
|
|
8361
|
+
const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
|
|
8362
|
+
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
8363
|
+
if (opencodeAuthApplied) state.openaiUsageRearm?.();
|
|
7768
8364
|
if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
|
|
7769
8365
|
idlePolls = 0;
|
|
7770
8366
|
idleMs = 0;
|
|
@@ -7833,7 +8429,7 @@ async function driveChannels(state, driver) {
|
|
|
7833
8429
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
7834
8430
|
var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
|
|
7835
8431
|
function sessionDbPath() {
|
|
7836
|
-
return
|
|
8432
|
+
return join7(homedir4(), ".local", "share", "opencode", "opencode.db");
|
|
7837
8433
|
}
|
|
7838
8434
|
async function runSweep(state, driver, config) {
|
|
7839
8435
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
@@ -7916,7 +8512,7 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
7916
8512
|
for (const warning2 of config.warnings) {
|
|
7917
8513
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
7918
8514
|
}
|
|
7919
|
-
const dbBytes = statSessionDbBytes(
|
|
8515
|
+
const dbBytes = statSessionDbBytes(homedir4());
|
|
7920
8516
|
void (async () => {
|
|
7921
8517
|
const reclaimSkipReason = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
7922
8518
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
@@ -7944,23 +8540,20 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
7944
8540
|
);
|
|
7945
8541
|
state.sessionCleanupTimers.push(interval, firstSweep);
|
|
7946
8542
|
}
|
|
7947
|
-
function
|
|
7948
|
-
const { mode, warnings } =
|
|
7949
|
-
options.claudeUsageReporting,
|
|
7950
|
-
process.env
|
|
7951
|
-
);
|
|
8543
|
+
function scheduleUsageReporting(state, params) {
|
|
8544
|
+
const { mode, warnings } = params.resolved;
|
|
7952
8545
|
for (const warning2 of warnings) {
|
|
7953
8546
|
logActivity(state, {
|
|
7954
8547
|
type: "info",
|
|
7955
8548
|
level: "warn",
|
|
7956
|
-
message:
|
|
8549
|
+
message: `${params.label} usage reporting: ${warning2}`
|
|
7957
8550
|
});
|
|
7958
8551
|
}
|
|
7959
8552
|
if (mode === "off") {
|
|
7960
8553
|
logActivity(state, {
|
|
7961
8554
|
type: "info",
|
|
7962
8555
|
level: "debug",
|
|
7963
|
-
message:
|
|
8556
|
+
message: `${params.label} usage reporting is off (${params.offFlagHint})`
|
|
7964
8557
|
});
|
|
7965
8558
|
return null;
|
|
7966
8559
|
}
|
|
@@ -7969,7 +8562,7 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
7969
8562
|
let rearmRequested = false;
|
|
7970
8563
|
const armProbe = () => {
|
|
7971
8564
|
phase = "probe-pending";
|
|
7972
|
-
|
|
8565
|
+
params.setTimer(setTimeout(() => void tick(true), params.firstDelayMs()));
|
|
7973
8566
|
};
|
|
7974
8567
|
const scheduleNextTick = () => {
|
|
7975
8568
|
if (rearmRequested) {
|
|
@@ -7978,7 +8571,7 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
7978
8571
|
return;
|
|
7979
8572
|
}
|
|
7980
8573
|
phase = "steady-pending";
|
|
7981
|
-
|
|
8574
|
+
params.setTimer(setTimeout(() => void tick(false), params.nextDelayMs()));
|
|
7982
8575
|
};
|
|
7983
8576
|
const rearm = () => {
|
|
7984
8577
|
switch (phase) {
|
|
@@ -7988,9 +8581,9 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
7988
8581
|
case "probe-pending":
|
|
7989
8582
|
return;
|
|
7990
8583
|
case "steady-pending":
|
|
7991
|
-
if (
|
|
7992
|
-
clearTimeout(
|
|
7993
|
-
|
|
8584
|
+
if (params.getTimer()) {
|
|
8585
|
+
clearTimeout(params.getTimer());
|
|
8586
|
+
params.setTimer(null);
|
|
7994
8587
|
}
|
|
7995
8588
|
rearmRequested = false;
|
|
7996
8589
|
armProbe();
|
|
@@ -8004,45 +8597,45 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
8004
8597
|
const tick = async (isProbe) => {
|
|
8005
8598
|
phase = "tick-in-flight";
|
|
8006
8599
|
try {
|
|
8007
|
-
const usage = await
|
|
8008
|
-
const result = await
|
|
8600
|
+
const usage = await params.fetchUsage();
|
|
8601
|
+
const result = await params.report(usage);
|
|
8009
8602
|
if (result.ok) {
|
|
8010
8603
|
if (consecutiveFailures > 0) {
|
|
8011
8604
|
logActivity(state, {
|
|
8012
8605
|
type: "info",
|
|
8013
8606
|
level: "info",
|
|
8014
|
-
message:
|
|
8607
|
+
message: `${params.label} usage reporting recovered`
|
|
8015
8608
|
});
|
|
8016
8609
|
}
|
|
8017
8610
|
consecutiveFailures = 0;
|
|
8018
8611
|
logActivity(state, {
|
|
8019
8612
|
type: "info",
|
|
8020
8613
|
level: "debug",
|
|
8021
|
-
message:
|
|
8614
|
+
message: `Reported ${params.label} usage to Evident`
|
|
8022
8615
|
});
|
|
8023
8616
|
} else {
|
|
8024
8617
|
consecutiveFailures++;
|
|
8025
8618
|
logActivity(state, {
|
|
8026
8619
|
type: "info",
|
|
8027
|
-
level:
|
|
8028
|
-
message: `Failed to report
|
|
8620
|
+
level: params.failureLogLevel(consecutiveFailures),
|
|
8621
|
+
message: `Failed to report ${params.label} usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
|
|
8029
8622
|
});
|
|
8030
8623
|
}
|
|
8031
8624
|
scheduleNextTick();
|
|
8032
8625
|
} catch (error2) {
|
|
8033
|
-
if (
|
|
8626
|
+
if (params.isLocalCredentialProblem(error2)) {
|
|
8034
8627
|
if (mode === "on") {
|
|
8035
8628
|
logActivity(state, {
|
|
8036
8629
|
type: "info",
|
|
8037
8630
|
level: "warn",
|
|
8038
|
-
message:
|
|
8631
|
+
message: `${params.label} usage reporting is forced on but no usable login was found \u2014 ${params.forcedOnHint}; reporting will keep retrying`
|
|
8039
8632
|
});
|
|
8040
8633
|
scheduleNextTick();
|
|
8041
8634
|
} else if (isProbe) {
|
|
8042
8635
|
logActivity(state, {
|
|
8043
8636
|
type: "info",
|
|
8044
8637
|
level: "debug",
|
|
8045
|
-
message:
|
|
8638
|
+
message: `${params.label} usage reporting: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
8046
8639
|
});
|
|
8047
8640
|
phase = "dormant";
|
|
8048
8641
|
if (rearmRequested) rearm();
|
|
@@ -8050,7 +8643,7 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
8050
8643
|
logActivity(state, {
|
|
8051
8644
|
type: "info",
|
|
8052
8645
|
level: "debug",
|
|
8053
|
-
message:
|
|
8646
|
+
message: `${params.label} usage reporting: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
8054
8647
|
});
|
|
8055
8648
|
scheduleNextTick();
|
|
8056
8649
|
}
|
|
@@ -8059,8 +8652,8 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
8059
8652
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
8060
8653
|
logActivity(state, {
|
|
8061
8654
|
type: "info",
|
|
8062
|
-
level:
|
|
8063
|
-
message:
|
|
8655
|
+
level: params.failureLogLevel(consecutiveFailures),
|
|
8656
|
+
message: `${params.label} usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
|
|
8064
8657
|
});
|
|
8065
8658
|
scheduleNextTick();
|
|
8066
8659
|
}
|
|
@@ -8069,6 +8662,34 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
8069
8662
|
armProbe();
|
|
8070
8663
|
return rearm;
|
|
8071
8664
|
}
|
|
8665
|
+
function scheduleClaudeUsageReporting(state, options) {
|
|
8666
|
+
return scheduleUsageReporting(state, {
|
|
8667
|
+
label: "Claude",
|
|
8668
|
+
resolved: resolveClaudeUsageReportingMode(options.claudeUsageReporting, process.env),
|
|
8669
|
+
offFlagHint: "--claude-usage-reporting off",
|
|
8670
|
+
getTimer: () => state.claudeUsageTimer,
|
|
8671
|
+
setTimer: (timer) => {
|
|
8672
|
+
state.claudeUsageTimer = timer;
|
|
8673
|
+
},
|
|
8674
|
+
fetchUsage: async () => {
|
|
8675
|
+
const usage = await getClaudeUsage();
|
|
8676
|
+
if (usage.ownerLookupError) {
|
|
8677
|
+
logActivity(state, {
|
|
8678
|
+
type: "info",
|
|
8679
|
+
level: "debug",
|
|
8680
|
+
message: `Claude usage owner lookup failed: ${usage.ownerLookupError}`
|
|
8681
|
+
});
|
|
8682
|
+
}
|
|
8683
|
+
return usage;
|
|
8684
|
+
},
|
|
8685
|
+
report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
|
|
8686
|
+
isLocalCredentialProblem,
|
|
8687
|
+
forcedOnHint: "run `claude` to sign in",
|
|
8688
|
+
firstDelayMs: () => FIRST_REPORT_DELAY_MS,
|
|
8689
|
+
nextDelayMs: nextReportDelayMs,
|
|
8690
|
+
failureLogLevel: claudeUsageFailureLogLevel
|
|
8691
|
+
});
|
|
8692
|
+
}
|
|
8072
8693
|
var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
|
|
8073
8694
|
var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
8074
8695
|
var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
@@ -8092,7 +8713,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
8092
8713
|
});
|
|
8093
8714
|
return;
|
|
8094
8715
|
}
|
|
8095
|
-
const collect = createResourceUsageCollector(
|
|
8716
|
+
const collect = createResourceUsageCollector(homedir4());
|
|
8096
8717
|
let consecutiveFailures = 0;
|
|
8097
8718
|
const tick = async () => {
|
|
8098
8719
|
try {
|
|
@@ -8193,6 +8814,11 @@ async function cleanup(state, opts = {}) {
|
|
|
8193
8814
|
state.claudeUsageTimer = null;
|
|
8194
8815
|
}
|
|
8195
8816
|
state.claudeUsageRearm = null;
|
|
8817
|
+
if (state.openaiUsageTimer) {
|
|
8818
|
+
clearTimeout(state.openaiUsageTimer);
|
|
8819
|
+
state.openaiUsageTimer = null;
|
|
8820
|
+
}
|
|
8821
|
+
state.openaiUsageRearm = null;
|
|
8196
8822
|
if (state.resourceUsageTimer) {
|
|
8197
8823
|
clearTimeout(state.resourceUsageTimer);
|
|
8198
8824
|
state.resourceUsageTimer = null;
|
|
@@ -8227,15 +8853,31 @@ async function cleanup(state, opts = {}) {
|
|
|
8227
8853
|
}
|
|
8228
8854
|
if (state.opencodeProcess) {
|
|
8229
8855
|
const opencodeProcess = state.opencodeProcess;
|
|
8230
|
-
|
|
8856
|
+
const result = await timeShutdownPhase(
|
|
8857
|
+
state,
|
|
8858
|
+
durations,
|
|
8859
|
+
"opencode_stop",
|
|
8860
|
+
() => stopOpenCodeAndWait(opencodeProcess, CHILD_STOP_TIMEOUT_MS)
|
|
8861
|
+
);
|
|
8231
8862
|
if (state.interactive) {
|
|
8232
|
-
logActivity(state, { type: "info", message:
|
|
8863
|
+
logActivity(state, { type: "info", message: `Stopped OpenCode process (${result.outcome})` });
|
|
8233
8864
|
displayStatus(state);
|
|
8234
8865
|
} else {
|
|
8235
|
-
log2(state,
|
|
8866
|
+
log2(state, `Stopped OpenCode process (${result.outcome})`);
|
|
8236
8867
|
}
|
|
8237
8868
|
state.opencodeProcess = null;
|
|
8238
8869
|
}
|
|
8870
|
+
if (state.litestreamProcess) {
|
|
8871
|
+
const litestreamProcess = state.litestreamProcess;
|
|
8872
|
+
const result = await timeShutdownPhase(
|
|
8873
|
+
state,
|
|
8874
|
+
durations,
|
|
8875
|
+
"litestream_stop",
|
|
8876
|
+
() => stopSessionDbReplication(litestreamProcess, CHILD_STOP_TIMEOUT_MS)
|
|
8877
|
+
);
|
|
8878
|
+
log2(state, `Stopped litestream replication (${result.outcome})`);
|
|
8879
|
+
state.litestreamProcess = null;
|
|
8880
|
+
}
|
|
8239
8881
|
return durations;
|
|
8240
8882
|
}
|
|
8241
8883
|
async function run(options) {
|
|
@@ -8244,7 +8886,7 @@ async function run(options) {
|
|
|
8244
8886
|
let fileSyncDirectories;
|
|
8245
8887
|
try {
|
|
8246
8888
|
logLevel = resolveLogLevel(options);
|
|
8247
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
8889
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir4());
|
|
8248
8890
|
} catch (error2) {
|
|
8249
8891
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
8250
8892
|
if (options.json) {
|
|
@@ -8269,6 +8911,7 @@ async function run(options) {
|
|
|
8269
8911
|
opencodeConnected: false,
|
|
8270
8912
|
opencodeVersion: null,
|
|
8271
8913
|
opencodeProcess: null,
|
|
8914
|
+
litestreamProcess: null,
|
|
8272
8915
|
connection: null,
|
|
8273
8916
|
channelDriver: null,
|
|
8274
8917
|
running: true,
|
|
@@ -8279,6 +8922,8 @@ async function run(options) {
|
|
|
8279
8922
|
sessionCleanupTimers: [],
|
|
8280
8923
|
claudeUsageTimer: null,
|
|
8281
8924
|
claudeUsageRearm: null,
|
|
8925
|
+
openaiUsageTimer: null,
|
|
8926
|
+
openaiUsageRearm: null,
|
|
8282
8927
|
resourceUsageTimer: null,
|
|
8283
8928
|
authHeader: ""
|
|
8284
8929
|
};
|
|
@@ -8474,6 +9119,7 @@ async function run(options) {
|
|
|
8474
9119
|
} else {
|
|
8475
9120
|
log2(state, "Not running in a MicroVM (MICROVM_ID unset) \u2014 nothing to report", "debug");
|
|
8476
9121
|
}
|
|
9122
|
+
reportSessionDbRecovery(state);
|
|
8477
9123
|
const { timeoutMs: opencodeStartTimeoutMs, warnings: opencodeStartTimeoutWarnings } = resolveOpenCodeStartTimeoutMs(options, process.env);
|
|
8478
9124
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
8479
9125
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
@@ -8530,6 +9176,40 @@ async function run(options) {
|
|
|
8530
9176
|
ocSpinner?.fail(error2.message);
|
|
8531
9177
|
throw error2;
|
|
8532
9178
|
}
|
|
9179
|
+
if (options.litestreamConfig) {
|
|
9180
|
+
const litestreamProcess = startSessionDbReplication(options.litestreamConfig);
|
|
9181
|
+
state.litestreamProcess = litestreamProcess;
|
|
9182
|
+
let failureHandled = false;
|
|
9183
|
+
const failRunForReplication = (message) => {
|
|
9184
|
+
if (failureHandled || state.shuttingDown || !state.running) return;
|
|
9185
|
+
failureHandled = true;
|
|
9186
|
+
state.shuttingDown = true;
|
|
9187
|
+
logActivity(state, { type: "error", error: message });
|
|
9188
|
+
if (state.interactive) displayStatus(state);
|
|
9189
|
+
void (async () => {
|
|
9190
|
+
try {
|
|
9191
|
+
await cleanup(state);
|
|
9192
|
+
await shutdownTelemetry();
|
|
9193
|
+
} catch (error2) {
|
|
9194
|
+
console.error(
|
|
9195
|
+
`[run] replication failure cleanup failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
9196
|
+
);
|
|
9197
|
+
}
|
|
9198
|
+
process.exit(1);
|
|
9199
|
+
})();
|
|
9200
|
+
};
|
|
9201
|
+
litestreamProcess.on("exit", (code, signal) => {
|
|
9202
|
+
failRunForReplication(
|
|
9203
|
+
`Litestream replication stopped unexpectedly (code: ${code ?? "null"}, signal: ${signal ?? "none"})`
|
|
9204
|
+
);
|
|
9205
|
+
});
|
|
9206
|
+
litestreamProcess.on("error", (error2) => {
|
|
9207
|
+
failRunForReplication(
|
|
9208
|
+
`Litestream replication failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
9209
|
+
);
|
|
9210
|
+
});
|
|
9211
|
+
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
9212
|
+
}
|
|
8533
9213
|
const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
|
|
8534
9214
|
const channelDriver = new ChannelDriver({
|
|
8535
9215
|
agentId: state.agentId,
|
|
@@ -8541,7 +9221,7 @@ async function run(options) {
|
|
|
8541
9221
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
8542
9222
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
8543
9223
|
fileSyncDirectories,
|
|
8544
|
-
homeDir:
|
|
9224
|
+
homeDir: homedir4(),
|
|
8545
9225
|
maxActiveSessions,
|
|
8546
9226
|
log: (entry) => (
|
|
8547
9227
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -8679,6 +9359,22 @@ async function run(options) {
|
|
|
8679
9359
|
}
|
|
8680
9360
|
scheduleSessionCleanup(state, channelDriver, options);
|
|
8681
9361
|
state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
|
|
9362
|
+
state.openaiUsageRearm = scheduleUsageReporting(state, {
|
|
9363
|
+
label: "OpenAI",
|
|
9364
|
+
resolved: resolveOpenAiUsageReportingMode(options.openaiUsageReporting, process.env),
|
|
9365
|
+
offFlagHint: "--openai-usage-reporting off",
|
|
9366
|
+
getTimer: () => state.openaiUsageTimer,
|
|
9367
|
+
setTimer: (timer) => {
|
|
9368
|
+
state.openaiUsageTimer = timer;
|
|
9369
|
+
},
|
|
9370
|
+
fetchUsage: () => getOpenAiUsage(state.port),
|
|
9371
|
+
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
9372
|
+
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
9373
|
+
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
9374
|
+
firstDelayMs: firstReportDelayMs,
|
|
9375
|
+
nextDelayMs: usageReportDelayMs,
|
|
9376
|
+
failureLogLevel: usageReportFailureLogLevel
|
|
9377
|
+
});
|
|
8682
9378
|
scheduleResourceUsageReporting(state, options);
|
|
8683
9379
|
if (!interactive || state.json) {
|
|
8684
9380
|
log2(state, "Driving channel messages...");
|
|
@@ -8760,6 +9456,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
8760
9456
|
).option(
|
|
8761
9457
|
"--claude-usage-reporting <mode>",
|
|
8762
9458
|
"Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
9459
|
+
).option(
|
|
9460
|
+
"--openai-usage-reporting <mode>",
|
|
9461
|
+
"Report OpenAI plan usage to Evident: auto | on | off (default: auto). Env: EVIDENT_OPENAI_USAGE_REPORTING"
|
|
8763
9462
|
).option(
|
|
8764
9463
|
"--no-resource-usage-reporting",
|
|
8765
9464
|
"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 +9470,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
8771
9470
|
).option(
|
|
8772
9471
|
"--tunnel-ready-file <path>",
|
|
8773
9472
|
"Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
|
|
9473
|
+
).option(
|
|
9474
|
+
"--litestream-config <path>",
|
|
9475
|
+
"Replicate the OpenCode session database with `litestream replicate` using this config file (written by the runner image). Omit to disable replication."
|
|
8774
9476
|
).action(
|
|
8775
9477
|
(options) => {
|
|
8776
9478
|
run({
|
|
@@ -8795,13 +9497,15 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
8795
9497
|
// Raw string — the resolver in run.ts single-sources parsing
|
|
8796
9498
|
// (resolveClaudeUsageReportingMode).
|
|
8797
9499
|
claudeUsageReporting: options.claudeUsageReporting,
|
|
9500
|
+
openaiUsageReporting: options.openaiUsageReporting,
|
|
8798
9501
|
// Raw value — resolution is single-sourced in run.ts's
|
|
8799
9502
|
// resolveResourceUsageReportingEnabled.
|
|
8800
9503
|
resourceUsageReporting: options.resourceUsageReporting,
|
|
8801
9504
|
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
8802
9505
|
// resolveFileSyncDirectories.
|
|
8803
9506
|
enableFileSyncTo: options.enableFileSyncTo,
|
|
8804
|
-
tunnelReadyFile: options.tunnelReadyFile
|
|
9507
|
+
tunnelReadyFile: options.tunnelReadyFile,
|
|
9508
|
+
litestreamConfig: options.litestreamConfig
|
|
8805
9509
|
});
|
|
8806
9510
|
}
|
|
8807
9511
|
);
|