@evident-ai/cli 3.4.1-dev.b1bf8c5 → 3.4.1-dev.b45408b
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 +4 -0
- package/dist/index.js +445 -47
- 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,8 @@ 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)
|
|
734
743
|
}),
|
|
735
744
|
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
736
745
|
});
|
|
@@ -996,7 +1005,10 @@ import { readFileSync } from "fs";
|
|
|
996
1005
|
import { homedir } from "os";
|
|
997
1006
|
import { join } from "path";
|
|
998
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;
|
|
999
1010
|
var KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
1011
|
+
var cachedOwner = null;
|
|
1000
1012
|
var CLAUDE_CREDENTIALS_SEGMENTS = [".claude", ".credentials.json"];
|
|
1001
1013
|
function parseClaudeCliCredentials(raw) {
|
|
1002
1014
|
let parsed;
|
|
@@ -1070,6 +1082,47 @@ function toWindow(value) {
|
|
|
1070
1082
|
}
|
|
1071
1083
|
return { utilization: window.utilization, resetsAt };
|
|
1072
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
|
+
}
|
|
1073
1126
|
async function getClaudeUsage() {
|
|
1074
1127
|
const credentials2 = readClaudeCliCredentials();
|
|
1075
1128
|
if (!credentials2) {
|
|
@@ -1089,15 +1142,19 @@ async function getClaudeUsage() {
|
|
|
1089
1142
|
Authorization: `Bearer ${credentials2.accessToken}`,
|
|
1090
1143
|
"Content-Type": "application/json",
|
|
1091
1144
|
"anthropic-version": "2023-06-01"
|
|
1092
|
-
}
|
|
1145
|
+
},
|
|
1146
|
+
signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
|
|
1093
1147
|
});
|
|
1094
1148
|
if (!res.ok) {
|
|
1095
1149
|
throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
|
|
1096
1150
|
}
|
|
1097
1151
|
const body = await res.json();
|
|
1152
|
+
const { owner, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
|
|
1098
1153
|
return {
|
|
1099
1154
|
fiveHour: toWindow(body.five_hour),
|
|
1100
|
-
sevenDay: toWindow(body.seven_day)
|
|
1155
|
+
sevenDay: toWindow(body.seven_day),
|
|
1156
|
+
owner,
|
|
1157
|
+
ownerLookupError
|
|
1101
1158
|
};
|
|
1102
1159
|
}
|
|
1103
1160
|
|
|
@@ -1127,7 +1184,7 @@ async function claudeUsage() {
|
|
|
1127
1184
|
|
|
1128
1185
|
// src/commands/run.ts
|
|
1129
1186
|
import { homedir as homedir4 } from "os";
|
|
1130
|
-
import { isAbsolute as isAbsolute2, join as
|
|
1187
|
+
import { isAbsolute as isAbsolute2, join as join7, parse, resolve as resolvePath } from "path";
|
|
1131
1188
|
import chalk6 from "chalk";
|
|
1132
1189
|
|
|
1133
1190
|
// ../../packages/types/src/agents/index.ts
|
|
@@ -1360,6 +1417,8 @@ var SEVERITY_BY_LEVEL = {
|
|
|
1360
1417
|
error: "error"
|
|
1361
1418
|
};
|
|
1362
1419
|
var MAX_MESSAGE_LENGTH = 500;
|
|
1420
|
+
var MAX_METADATA_VALUE_LENGTH = 200;
|
|
1421
|
+
var MAX_METADATA_ENTRIES = 20;
|
|
1363
1422
|
var TRUNCATION_MARKER = "\u2026";
|
|
1364
1423
|
function redact(message) {
|
|
1365
1424
|
return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/https?:\/\/\S+/g, "<url>");
|
|
@@ -1368,6 +1427,24 @@ function truncate(message) {
|
|
|
1368
1427
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
1369
1428
|
return message.slice(0, MAX_MESSAGE_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
|
|
1370
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
|
+
}
|
|
1371
1448
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1372
1449
|
var RATE_LIMIT_MAX_EVENTS = 30;
|
|
1373
1450
|
var windowStartedAt = 0;
|
|
@@ -1406,7 +1483,7 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1406
1483
|
logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
|
|
1407
1484
|
severity: SEVERITY_BY_LEVEL[entry.level],
|
|
1408
1485
|
message,
|
|
1409
|
-
metadata: { source: "cli.run" },
|
|
1486
|
+
metadata: { ...sanitiseMetadata(entry.metadata), source: "cli.run" },
|
|
1410
1487
|
agentId: context.agentId
|
|
1411
1488
|
});
|
|
1412
1489
|
} catch (err) {
|
|
@@ -1416,6 +1493,150 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1416
1493
|
}
|
|
1417
1494
|
}
|
|
1418
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
|
+
|
|
1419
1640
|
// src/lib/opencode/health.ts
|
|
1420
1641
|
async function checkOpenCodeHealth(port) {
|
|
1421
1642
|
try {
|
|
@@ -1460,6 +1681,62 @@ function buildOpenCodeVersionWarning(version2) {
|
|
|
1460
1681
|
|
|
1461
1682
|
// src/lib/opencode/process.ts
|
|
1462
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
|
|
1463
1740
|
var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
|
|
1464
1741
|
function getProcessCwd(pid) {
|
|
1465
1742
|
const platform = process.platform;
|
|
@@ -1631,23 +1908,20 @@ async function startOpenCode(port) {
|
|
|
1631
1908
|
});
|
|
1632
1909
|
return child;
|
|
1633
1910
|
}
|
|
1634
|
-
function
|
|
1635
|
-
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
try {
|
|
1911
|
+
function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
|
|
1912
|
+
const sendSignal = (signal) => {
|
|
1639
1913
|
if (process.platform === "win32") {
|
|
1640
|
-
opencodeProcess.kill(
|
|
1914
|
+
opencodeProcess.kill(signal);
|
|
1641
1915
|
} else {
|
|
1642
|
-
process.kill(-opencodeProcess.pid,
|
|
1643
|
-
}
|
|
1644
|
-
} catch (err) {
|
|
1645
|
-
if (err.code !== "ESRCH") {
|
|
1646
|
-
console.warn(
|
|
1647
|
-
`stopOpenCode: kill failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1648
|
-
);
|
|
1916
|
+
process.kill(-opencodeProcess.pid, signal);
|
|
1649
1917
|
}
|
|
1650
|
-
}
|
|
1918
|
+
};
|
|
1919
|
+
return stopProcessAndWait(
|
|
1920
|
+
opencodeProcess,
|
|
1921
|
+
timeoutMs,
|
|
1922
|
+
() => sendSignal("SIGTERM"),
|
|
1923
|
+
() => sendSignal("SIGKILL")
|
|
1924
|
+
);
|
|
1651
1925
|
}
|
|
1652
1926
|
|
|
1653
1927
|
// src/lib/opencode/install.ts
|
|
@@ -2230,7 +2504,7 @@ function isPreamblePinnedRunning(messages, userMessageId) {
|
|
|
2230
2504
|
return completedOf(reply) != null && finishOf(reply) === "tool-calls";
|
|
2231
2505
|
}
|
|
2232
2506
|
function isB2AbandonmentConfirmed(params) {
|
|
2233
|
-
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false;
|
|
2507
|
+
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
|
|
2234
2508
|
}
|
|
2235
2509
|
function isAmbiguousTerminalFinish(m) {
|
|
2236
2510
|
if (completedOf(m) == null) return false;
|
|
@@ -2243,7 +2517,7 @@ function isAmbiguousFinishPinnedRunning(messages, userMessageId) {
|
|
|
2243
2517
|
return isAmbiguousTerminalFinish(reply);
|
|
2244
2518
|
}
|
|
2245
2519
|
function isAmbiguousFinishResolved(params) {
|
|
2246
|
-
return params.sessionOngoing === false || params.pinnedForMs >= params.maxPinnedMs;
|
|
2520
|
+
return params.sessionOngoing === false || params.sessionOngoing !== true && params.pinnedForMs >= params.maxPinnedMs;
|
|
2247
2521
|
}
|
|
2248
2522
|
function messageError(messages, userMessageId) {
|
|
2249
2523
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
@@ -2453,10 +2727,10 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
2453
2727
|
|
|
2454
2728
|
// src/lib/opencode/session-db-size.ts
|
|
2455
2729
|
import { statSync as statSync2 } from "fs";
|
|
2456
|
-
import { join as
|
|
2730
|
+
import { join as join3 } from "path";
|
|
2457
2731
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
2458
2732
|
function statSessionDbBytes(homeDir) {
|
|
2459
|
-
const dbPath =
|
|
2733
|
+
const dbPath = join3(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
2460
2734
|
try {
|
|
2461
2735
|
return statSync2(dbPath).size;
|
|
2462
2736
|
} catch (err) {
|
|
@@ -3025,10 +3299,26 @@ function writeTunnelReadyMarker(path, agentId) {
|
|
|
3025
3299
|
}
|
|
3026
3300
|
}
|
|
3027
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
|
+
|
|
3028
3318
|
// src/lib/openai-usage.ts
|
|
3029
|
-
import { readFileSync as
|
|
3319
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3030
3320
|
import { homedir as homedir2 } from "os";
|
|
3031
|
-
import { join as
|
|
3321
|
+
import { join as join4 } from "path";
|
|
3032
3322
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
3033
3323
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
3034
3324
|
var OpenAiUsageError = class extends Error {
|
|
@@ -3042,7 +3332,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
3042
3332
|
}
|
|
3043
3333
|
function readOpenCodeChatGptCredentials() {
|
|
3044
3334
|
try {
|
|
3045
|
-
const raw =
|
|
3335
|
+
const raw = readFileSync3(join4(homedir2(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
3046
3336
|
let parsed;
|
|
3047
3337
|
try {
|
|
3048
3338
|
parsed = JSON.parse(raw);
|
|
@@ -3418,12 +3708,12 @@ function createResourceUsageCollector(homeDir) {
|
|
|
3418
3708
|
import { homedir as homedir3 } from "os";
|
|
3419
3709
|
|
|
3420
3710
|
// src/lib/runner-file-sync.ts
|
|
3421
|
-
import { join as
|
|
3711
|
+
import { join as join6 } from "path";
|
|
3422
3712
|
|
|
3423
3713
|
// src/lib/file-push.ts
|
|
3424
3714
|
import { randomUUID } from "crypto";
|
|
3425
3715
|
import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
|
|
3426
|
-
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";
|
|
3427
3717
|
var FILE_MODE = 384;
|
|
3428
3718
|
var DIRECTORY_MODE = 448;
|
|
3429
3719
|
async function writePushedFile(request) {
|
|
@@ -3456,7 +3746,7 @@ async function writePushedFile(request) {
|
|
|
3456
3746
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
3457
3747
|
dirname3(candidate)
|
|
3458
3748
|
);
|
|
3459
|
-
const realTarget =
|
|
3749
|
+
const realTarget = join5(existingAncestor, ...missingSegments, basename(candidate));
|
|
3460
3750
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
3461
3751
|
if (allowedDirectory === null) {
|
|
3462
3752
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -3492,7 +3782,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
3492
3782
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
3493
3783
|
return null;
|
|
3494
3784
|
}
|
|
3495
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
3785
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join5(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
3496
3786
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
3497
3787
|
return null;
|
|
3498
3788
|
}
|
|
@@ -3565,13 +3855,13 @@ function contains(realDirectory, realTarget) {
|
|
|
3565
3855
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
3566
3856
|
let current = existingAncestor;
|
|
3567
3857
|
for (const segment of missingSegments) {
|
|
3568
|
-
current =
|
|
3858
|
+
current = join5(current, segment);
|
|
3569
3859
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
3570
3860
|
await chmod(current, DIRECTORY_MODE);
|
|
3571
3861
|
}
|
|
3572
3862
|
}
|
|
3573
3863
|
async function writeAtomically(realTarget, content) {
|
|
3574
|
-
const temporaryPath =
|
|
3864
|
+
const temporaryPath = join5(dirname3(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
3575
3865
|
let handle;
|
|
3576
3866
|
try {
|
|
3577
3867
|
handle = await open2(temporaryPath, "wx", FILE_MODE);
|
|
@@ -3701,12 +3991,12 @@ var NOT_APPLIED = {
|
|
|
3701
3991
|
opencodeAuthApplied: false
|
|
3702
3992
|
};
|
|
3703
3993
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
3704
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
3705
|
-
return expanded ===
|
|
3994
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
3995
|
+
return expanded === join6(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
3706
3996
|
}
|
|
3707
3997
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
3708
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
3709
|
-
return expanded ===
|
|
3998
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
3999
|
+
return expanded === join6(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
3710
4000
|
}
|
|
3711
4001
|
async function applyOne(options, file) {
|
|
3712
4002
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -5519,6 +5809,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5519
5809
|
deliveryDeadlineAnchored: false,
|
|
5520
5810
|
b2PinnedSinceMs: 0,
|
|
5521
5811
|
b2LastDescendantCheckMs: 0,
|
|
5812
|
+
b2RootOngoingHeldLogged: false,
|
|
5522
5813
|
b2AbandonedSignalled: false,
|
|
5523
5814
|
ambiguousPinnedSinceMs: 0,
|
|
5524
5815
|
ambiguousResolved: false
|
|
@@ -5613,6 +5904,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5613
5904
|
deliveryDeadlineAnchored: false,
|
|
5614
5905
|
b2PinnedSinceMs: 0,
|
|
5615
5906
|
b2LastDescendantCheckMs: 0,
|
|
5907
|
+
b2RootOngoingHeldLogged: false,
|
|
5616
5908
|
b2AbandonedSignalled: false,
|
|
5617
5909
|
ambiguousPinnedSinceMs: 0,
|
|
5618
5910
|
ambiguousResolved: false
|
|
@@ -5966,6 +6258,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5966
6258
|
if (snapshotReadable) {
|
|
5967
6259
|
inFlight.b2PinnedSinceMs = 0;
|
|
5968
6260
|
inFlight.b2LastDescendantCheckMs = 0;
|
|
6261
|
+
inFlight.b2RootOngoingHeldLogged = false;
|
|
5969
6262
|
inFlight.b2AbandonedSignalled = false;
|
|
5970
6263
|
}
|
|
5971
6264
|
} else {
|
|
@@ -5977,11 +6270,15 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5977
6270
|
const pinnedForMs = this.now() - inFlight.b2PinnedSinceMs;
|
|
5978
6271
|
if (pinnedForMs >= B2_ABANDONMENT_MIN_PINNED_MS && this.now() - inFlight.b2LastDescendantCheckMs >= B2_ABANDONMENT_RECHECK_MS) {
|
|
5979
6272
|
inFlight.b2LastDescendantCheckMs = this.now();
|
|
5980
|
-
const descendantOngoing = await
|
|
6273
|
+
const [descendantOngoing, rootOngoing] = await Promise.all([
|
|
6274
|
+
this.isAnyDescendantSessionOngoing(sessionId),
|
|
6275
|
+
isSessionOngoing(this.port, sessionId)
|
|
6276
|
+
]);
|
|
5981
6277
|
if (isB2AbandonmentConfirmed({
|
|
5982
6278
|
pinnedForMs,
|
|
5983
6279
|
minPinnedMs: B2_ABANDONMENT_MIN_PINNED_MS,
|
|
5984
|
-
descendantOngoing
|
|
6280
|
+
descendantOngoing,
|
|
6281
|
+
rootOngoing
|
|
5985
6282
|
})) {
|
|
5986
6283
|
inFlight.b2AbandonedSignalled = true;
|
|
5987
6284
|
this.log({
|
|
@@ -5990,12 +6287,26 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5990
6287
|
conversation_id: conv.id,
|
|
5991
6288
|
message_id: id
|
|
5992
6289
|
});
|
|
6290
|
+
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
5993
6291
|
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
5994
|
-
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
|
|
5995
6297
|
});
|
|
5996
6298
|
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
5997
6299
|
return;
|
|
5998
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
|
+
}
|
|
5999
6310
|
}
|
|
6000
6311
|
}
|
|
6001
6312
|
const ambiguousPinnedNow = activelyRunning && isAmbiguousFinishPinnedRunning(messages, inFlight.opencodeMessageId);
|
|
@@ -7738,6 +8049,7 @@ var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_
|
|
|
7738
8049
|
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
7739
8050
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
|
|
7740
8051
|
var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
8052
|
+
var CHILD_STOP_TIMEOUT_MS = 1e4;
|
|
7741
8053
|
function resolveLogLevel(options) {
|
|
7742
8054
|
const accepted = Object.keys(LOG_LEVELS);
|
|
7743
8055
|
const validate = (value, source) => {
|
|
@@ -7768,7 +8080,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
7768
8080
|
if (trimmed === "") {
|
|
7769
8081
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
7770
8082
|
}
|
|
7771
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
8083
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join7(homeDir, trimmed.slice(2)) : trimmed;
|
|
7772
8084
|
if (!isAbsolute2(expanded)) {
|
|
7773
8085
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
7774
8086
|
}
|
|
@@ -7866,7 +8178,7 @@ function logActivity(state, entry) {
|
|
|
7866
8178
|
const level = entry.level ?? (entry.type === "error" ? "error" : "info");
|
|
7867
8179
|
if (!meetsThreshold(state, level)) return;
|
|
7868
8180
|
forwardRunnerActivity(
|
|
7869
|
-
{ level, message: entry.message, error: entry.error },
|
|
8181
|
+
{ level, message: entry.message, error: entry.error, metadata: entry.metadata },
|
|
7870
8182
|
{ agentId: state.agentId, authHeader: state.authHeader }
|
|
7871
8183
|
);
|
|
7872
8184
|
const fullEntry = {
|
|
@@ -7886,6 +8198,26 @@ function logActivity(state, entry) {
|
|
|
7886
8198
|
}
|
|
7887
8199
|
}
|
|
7888
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
|
+
}
|
|
7889
8221
|
function displayStatus(state) {
|
|
7890
8222
|
if (!state.interactive) return;
|
|
7891
8223
|
const attempt = state.connection?.reconnectAttempt ?? 0;
|
|
@@ -8081,7 +8413,7 @@ async function driveChannels(state, driver) {
|
|
|
8081
8413
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
8082
8414
|
var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
|
|
8083
8415
|
function sessionDbPath() {
|
|
8084
|
-
return
|
|
8416
|
+
return join7(homedir4(), ".local", "share", "opencode", "opencode.db");
|
|
8085
8417
|
}
|
|
8086
8418
|
async function runSweep(state, driver, config) {
|
|
8087
8419
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
@@ -8323,7 +8655,17 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
8323
8655
|
setTimer: (timer) => {
|
|
8324
8656
|
state.claudeUsageTimer = timer;
|
|
8325
8657
|
},
|
|
8326
|
-
fetchUsage:
|
|
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
|
+
},
|
|
8327
8669
|
report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
|
|
8328
8670
|
isLocalCredentialProblem,
|
|
8329
8671
|
forcedOnHint: "run `claude` to sign in",
|
|
@@ -8495,15 +8837,31 @@ async function cleanup(state, opts = {}) {
|
|
|
8495
8837
|
}
|
|
8496
8838
|
if (state.opencodeProcess) {
|
|
8497
8839
|
const opencodeProcess = state.opencodeProcess;
|
|
8498
|
-
|
|
8840
|
+
const result = await timeShutdownPhase(
|
|
8841
|
+
state,
|
|
8842
|
+
durations,
|
|
8843
|
+
"opencode_stop",
|
|
8844
|
+
() => stopOpenCodeAndWait(opencodeProcess, CHILD_STOP_TIMEOUT_MS)
|
|
8845
|
+
);
|
|
8499
8846
|
if (state.interactive) {
|
|
8500
|
-
logActivity(state, { type: "info", message:
|
|
8847
|
+
logActivity(state, { type: "info", message: `Stopped OpenCode process (${result.outcome})` });
|
|
8501
8848
|
displayStatus(state);
|
|
8502
8849
|
} else {
|
|
8503
|
-
log2(state,
|
|
8850
|
+
log2(state, `Stopped OpenCode process (${result.outcome})`);
|
|
8504
8851
|
}
|
|
8505
8852
|
state.opencodeProcess = null;
|
|
8506
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
|
+
}
|
|
8507
8865
|
return durations;
|
|
8508
8866
|
}
|
|
8509
8867
|
async function run(options) {
|
|
@@ -8537,6 +8895,7 @@ async function run(options) {
|
|
|
8537
8895
|
opencodeConnected: false,
|
|
8538
8896
|
opencodeVersion: null,
|
|
8539
8897
|
opencodeProcess: null,
|
|
8898
|
+
litestreamProcess: null,
|
|
8540
8899
|
connection: null,
|
|
8541
8900
|
channelDriver: null,
|
|
8542
8901
|
running: true,
|
|
@@ -8744,6 +9103,7 @@ async function run(options) {
|
|
|
8744
9103
|
} else {
|
|
8745
9104
|
log2(state, "Not running in a MicroVM (MICROVM_ID unset) \u2014 nothing to report", "debug");
|
|
8746
9105
|
}
|
|
9106
|
+
reportSessionDbRecovery(state);
|
|
8747
9107
|
const { timeoutMs: opencodeStartTimeoutMs, warnings: opencodeStartTimeoutWarnings } = resolveOpenCodeStartTimeoutMs(options, process.env);
|
|
8748
9108
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
8749
9109
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
@@ -8800,6 +9160,40 @@ async function run(options) {
|
|
|
8800
9160
|
ocSpinner?.fail(error2.message);
|
|
8801
9161
|
throw error2;
|
|
8802
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
|
+
}
|
|
8803
9197
|
const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
|
|
8804
9198
|
const channelDriver = new ChannelDriver({
|
|
8805
9199
|
agentId: state.agentId,
|
|
@@ -9060,6 +9454,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
9060
9454
|
).option(
|
|
9061
9455
|
"--tunnel-ready-file <path>",
|
|
9062
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."
|
|
9063
9460
|
).action(
|
|
9064
9461
|
(options) => {
|
|
9065
9462
|
run({
|
|
@@ -9091,7 +9488,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
9091
9488
|
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
9092
9489
|
// resolveFileSyncDirectories.
|
|
9093
9490
|
enableFileSyncTo: options.enableFileSyncTo,
|
|
9094
|
-
tunnelReadyFile: options.tunnelReadyFile
|
|
9491
|
+
tunnelReadyFile: options.tunnelReadyFile,
|
|
9492
|
+
litestreamConfig: options.litestreamConfig
|
|
9095
9493
|
});
|
|
9096
9494
|
}
|
|
9097
9495
|
);
|