@evident-ai/cli 3.4.1-dev.15755a4 → 3.4.1-dev.1856549
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 +303 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,6 +98,10 @@ Options:
|
|
|
98
98
|
healthy when the runner starts it itself (default: `180`). On expiry the runner
|
|
99
99
|
warns and comes online anyway rather than failing. Env:
|
|
100
100
|
`EVIDENT_OPENCODE_START_TIMEOUT` (seconds).
|
|
101
|
+
- `--litestream-config <path>` — Start `litestream replicate` for the OpenCode
|
|
102
|
+
session database with this config file. Use this for runner images that persist
|
|
103
|
+
the session database; the file is produced by `runner-synchroniser litestream-config`.
|
|
104
|
+
Omit it to disable replication.
|
|
101
105
|
- `--claude-usage-reporting <mode>` — Whether to report the local Claude Code
|
|
102
106
|
subscription's rate-limit usage to Evident, so it shows on the runner page:
|
|
103
107
|
`auto` (default) reports it when a usable Claude Code login is found on this
|
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
|
|
|
@@ -1467,7 +1524,7 @@ function drainSessionDbRecoveryReport({
|
|
|
1467
1524
|
skippedLines++;
|
|
1468
1525
|
return [];
|
|
1469
1526
|
}
|
|
1470
|
-
return [value];
|
|
1527
|
+
return [{ ...value, replication_suspended: value.replication_suspended ?? false }];
|
|
1471
1528
|
} catch (error2) {
|
|
1472
1529
|
skippedLines++;
|
|
1473
1530
|
console.error(
|
|
@@ -1492,12 +1549,39 @@ function buildSessionDbRecoveryActivity(record) {
|
|
|
1492
1549
|
const level = record.severity === "warning" ? "warn" : record.severity === "error" ? "error" : null;
|
|
1493
1550
|
if (!level) return null;
|
|
1494
1551
|
const noVerifiedPoint = record.verified_restore_point ? ` The verified restore point is ${record.verified_restore_point}.` : " No verified restore point is known.";
|
|
1552
|
+
const replication = record.replication_suspended ? " This start is not backing up its new session history; restart after fixing the cause." : "";
|
|
1553
|
+
const giveupMessage = (() => {
|
|
1554
|
+
switch (record.reason) {
|
|
1555
|
+
case "restore_deadline_exceeded":
|
|
1556
|
+
return "The restore did not finish before this startup deadline. Earlier sessions are unavailable for this start; collect the SESSION-DB-RESTORE-TRUNCATED boot log before retrying.";
|
|
1557
|
+
case "restore_tool_unusable":
|
|
1558
|
+
case "classification_unrecognised":
|
|
1559
|
+
return "The restore or classification tool produced an unsupported result. Inspect the named boot marker or log and repair the runner image or bundle before restarting.";
|
|
1560
|
+
case "synchroniser_config_unevaluable":
|
|
1561
|
+
case "synchroniser_config_incomplete":
|
|
1562
|
+
return "The installed hook and synchroniser bundle could not agree on configuration. Inspect the named boot marker or log and repair the runner image or bundle before restarting.";
|
|
1563
|
+
case "synchroniser_config_unresolved":
|
|
1564
|
+
return "The synchroniser configuration could not be resolved. Inspect the preceding boot error and repair the identified runner image, bundle, or local environment before restarting.";
|
|
1565
|
+
case "litestream_config_unavailable":
|
|
1566
|
+
return "The local Litestream configuration could not be generated or written. Inspect the preceding boot error and repair the runner image, bundle, local environment, or write permissions before restarting.";
|
|
1567
|
+
case "classification_fatal":
|
|
1568
|
+
return "Session history could not be restored because backup setup could not be established. Inspect the named boot marker or log and repair the identified backup configuration or permissions.";
|
|
1569
|
+
default:
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
})();
|
|
1573
|
+
if (giveupMessage)
|
|
1574
|
+
return {
|
|
1575
|
+
level,
|
|
1576
|
+
metadata: withoutContractFields(record),
|
|
1577
|
+
message: `${giveupMessage}${replication}`
|
|
1578
|
+
};
|
|
1495
1579
|
switch (record.outcome) {
|
|
1496
1580
|
case "fresh_session_db":
|
|
1497
1581
|
return {
|
|
1498
1582
|
level,
|
|
1499
1583
|
metadata: withoutContractFields(record),
|
|
1500
|
-
message: `This runner started with a fresh session database. Earlier sessions are unavailable, and queued conversation or session references from before this start may no longer resolve.${noVerifiedPoint} Review the runner's backup configuration before relying on restored session history
|
|
1584
|
+
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.${replication}`
|
|
1501
1585
|
};
|
|
1502
1586
|
case "restore_retried":
|
|
1503
1587
|
return {
|
|
@@ -1535,7 +1619,7 @@ function buildSessionDbRecoveryActivity(record) {
|
|
|
1535
1619
|
return {
|
|
1536
1620
|
level,
|
|
1537
1621
|
metadata: withoutContractFields(record),
|
|
1538
|
-
message:
|
|
1622
|
+
message: `This runner started without restoring session history 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.${replication}`
|
|
1539
1623
|
};
|
|
1540
1624
|
case "session_db_boot_refused":
|
|
1541
1625
|
return {
|
|
@@ -1575,7 +1659,7 @@ var STRING_OR_NULL_FIELDS = ["quarantine_destination", "verified_restore_point"]
|
|
|
1575
1659
|
function isSessionDbRecoveryRecord(value) {
|
|
1576
1660
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1577
1661
|
const record = value;
|
|
1578
|
-
return record.v === 1 && record.event === "session_db_recovery" && typeof record.at === "string" && STAGES.has(record.stage) && OUTCOMES.has(record.outcome) && SEVERITIES.has(record.severity) && typeof record.reason === "string" && NUMBER_FIELDS.every((field) => record[field] === null || Number.isInteger(record[field])) && STRING_OR_NULL_FIELDS.every(
|
|
1662
|
+
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" && (record.replication_suspended === void 0 || typeof record.replication_suspended === "boolean") && NUMBER_FIELDS.every((field) => record[field] === null || Number.isInteger(record[field])) && STRING_OR_NULL_FIELDS.every(
|
|
1579
1663
|
(field) => record[field] === null || typeof record[field] === "string"
|
|
1580
1664
|
);
|
|
1581
1665
|
}
|
|
@@ -1624,6 +1708,62 @@ function buildOpenCodeVersionWarning(version2) {
|
|
|
1624
1708
|
|
|
1625
1709
|
// src/lib/opencode/process.ts
|
|
1626
1710
|
import { execSync, spawn } from "child_process";
|
|
1711
|
+
|
|
1712
|
+
// src/lib/process-stop.ts
|
|
1713
|
+
async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
1714
|
+
if (!child.pid) {
|
|
1715
|
+
return { outcome: "not-running", code: child.exitCode, signal: child.signalCode };
|
|
1716
|
+
}
|
|
1717
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1718
|
+
return { outcome: "exited", code: child.exitCode, signal: child.signalCode };
|
|
1719
|
+
}
|
|
1720
|
+
return new Promise((resolve3, reject) => {
|
|
1721
|
+
let forced = false;
|
|
1722
|
+
let settled = false;
|
|
1723
|
+
const timer = setTimeout(() => {
|
|
1724
|
+
forced = true;
|
|
1725
|
+
try {
|
|
1726
|
+
sendKill();
|
|
1727
|
+
} catch (error2) {
|
|
1728
|
+
if (error2.code === "ESRCH") {
|
|
1729
|
+
finish({ outcome: "exited", code: child.exitCode, signal: child.signalCode });
|
|
1730
|
+
} else {
|
|
1731
|
+
fail(error2);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}, timeoutMs);
|
|
1735
|
+
const finish = (result) => {
|
|
1736
|
+
if (settled) return;
|
|
1737
|
+
settled = true;
|
|
1738
|
+
clearTimeout(timer);
|
|
1739
|
+
child.removeListener("exit", onExit);
|
|
1740
|
+
resolve3(result);
|
|
1741
|
+
};
|
|
1742
|
+
const fail = (error2) => {
|
|
1743
|
+
if (settled) return;
|
|
1744
|
+
settled = true;
|
|
1745
|
+
clearTimeout(timer);
|
|
1746
|
+
child.removeListener("exit", onExit);
|
|
1747
|
+
reject(error2);
|
|
1748
|
+
};
|
|
1749
|
+
const onExit = (code, signal) => {
|
|
1750
|
+
finish({ outcome: forced ? "killed" : "exited", code, signal });
|
|
1751
|
+
};
|
|
1752
|
+
child.once("exit", onExit);
|
|
1753
|
+
try {
|
|
1754
|
+
sendTerm();
|
|
1755
|
+
} catch (error2) {
|
|
1756
|
+
if (error2.code === "ESRCH") {
|
|
1757
|
+
finish({ outcome: "exited", code: child.exitCode, signal: child.signalCode });
|
|
1758
|
+
} else {
|
|
1759
|
+
fail(error2);
|
|
1760
|
+
}
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// src/lib/opencode/process.ts
|
|
1627
1767
|
var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
|
|
1628
1768
|
function getProcessCwd(pid) {
|
|
1629
1769
|
const platform = process.platform;
|
|
@@ -1795,23 +1935,20 @@ async function startOpenCode(port) {
|
|
|
1795
1935
|
});
|
|
1796
1936
|
return child;
|
|
1797
1937
|
}
|
|
1798
|
-
function
|
|
1799
|
-
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
|
-
try {
|
|
1938
|
+
function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
|
|
1939
|
+
const sendSignal = (signal) => {
|
|
1803
1940
|
if (process.platform === "win32") {
|
|
1804
|
-
opencodeProcess.kill(
|
|
1941
|
+
opencodeProcess.kill(signal);
|
|
1805
1942
|
} else {
|
|
1806
|
-
process.kill(-opencodeProcess.pid,
|
|
1943
|
+
process.kill(-opencodeProcess.pid, signal);
|
|
1807
1944
|
}
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1945
|
+
};
|
|
1946
|
+
return stopProcessAndWait(
|
|
1947
|
+
opencodeProcess,
|
|
1948
|
+
timeoutMs,
|
|
1949
|
+
() => sendSignal("SIGTERM"),
|
|
1950
|
+
() => sendSignal("SIGKILL")
|
|
1951
|
+
);
|
|
1815
1952
|
}
|
|
1816
1953
|
|
|
1817
1954
|
// src/lib/opencode/install.ts
|
|
@@ -2098,6 +2235,7 @@ async function createOpenCodeSession(port, directory) {
|
|
|
2098
2235
|
return data.id;
|
|
2099
2236
|
}
|
|
2100
2237
|
async function getModelAttachmentCapability(port, model) {
|
|
2238
|
+
const { model: baseModel } = splitModelVariant(model);
|
|
2101
2239
|
try {
|
|
2102
2240
|
const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
|
|
2103
2241
|
if (!res.ok) {
|
|
@@ -2114,9 +2252,9 @@ async function getModelAttachmentCapability(port, model) {
|
|
|
2114
2252
|
);
|
|
2115
2253
|
return null;
|
|
2116
2254
|
}
|
|
2117
|
-
const slash =
|
|
2118
|
-
const providerId = slash > 0 ?
|
|
2119
|
-
let modelId = slash > 0 ?
|
|
2255
|
+
const slash = baseModel ? baseModel.indexOf("/") : -1;
|
|
2256
|
+
const providerId = slash > 0 ? baseModel.slice(0, slash) : void 0;
|
|
2257
|
+
let modelId = slash > 0 ? baseModel.slice(slash + 1) : void 0;
|
|
2120
2258
|
const defaults2 = body?.default && typeof body.default === "object" ? body.default : void 0;
|
|
2121
2259
|
let provider = providerId ? providers.find((p) => p?.id === providerId) : void 0;
|
|
2122
2260
|
if (!provider && !providerId) {
|
|
@@ -2196,6 +2334,29 @@ async function buildFileParts(attachments, capable) {
|
|
|
2196
2334
|
}
|
|
2197
2335
|
return { parts, outcomes, capabilityUnknown };
|
|
2198
2336
|
}
|
|
2337
|
+
function splitModelVariant(raw) {
|
|
2338
|
+
const value = raw?.trim();
|
|
2339
|
+
if (!value) return {};
|
|
2340
|
+
const hashIndex = value.indexOf("#");
|
|
2341
|
+
if (hashIndex === -1) return { model: value };
|
|
2342
|
+
const model = value.slice(0, hashIndex).trim() || void 0;
|
|
2343
|
+
const variant = value.slice(hashIndex + 1).trim() || void 0;
|
|
2344
|
+
return { model, variant };
|
|
2345
|
+
}
|
|
2346
|
+
function applyModelOptions(body, options) {
|
|
2347
|
+
if (options?.agent) body.agent = options.agent;
|
|
2348
|
+
const { model, variant } = splitModelVariant(options?.model);
|
|
2349
|
+
if (model) {
|
|
2350
|
+
const slashIndex = model.indexOf("/");
|
|
2351
|
+
if (slashIndex !== -1) {
|
|
2352
|
+
body.model = {
|
|
2353
|
+
providerID: model.substring(0, slashIndex),
|
|
2354
|
+
modelID: model.substring(slashIndex + 1)
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
if (variant) body.variant = variant;
|
|
2359
|
+
}
|
|
2199
2360
|
function messageText(m) {
|
|
2200
2361
|
if (!m || !Array.isArray(m.parts)) return "";
|
|
2201
2362
|
return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
@@ -2220,18 +2381,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
2220
2381
|
const body = {
|
|
2221
2382
|
parts
|
|
2222
2383
|
};
|
|
2223
|
-
|
|
2224
|
-
body.agent = options.agent;
|
|
2225
|
-
}
|
|
2226
|
-
if (options?.model) {
|
|
2227
|
-
const slashIndex = options.model.indexOf("/");
|
|
2228
|
-
if (slashIndex !== -1) {
|
|
2229
|
-
body.model = {
|
|
2230
|
-
providerID: options.model.substring(0, slashIndex),
|
|
2231
|
-
modelID: options.model.substring(slashIndex + 1)
|
|
2232
|
-
};
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2384
|
+
applyModelOptions(body, options);
|
|
2235
2385
|
const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/prompt_async`, {
|
|
2236
2386
|
method: "POST",
|
|
2237
2387
|
headers: { "Content-Type": "application/json" },
|
|
@@ -2239,7 +2389,10 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
2239
2389
|
});
|
|
2240
2390
|
if (res.status < 200 || res.status >= 300) {
|
|
2241
2391
|
const text = await res.text().catch(() => "");
|
|
2242
|
-
|
|
2392
|
+
const { variant } = splitModelVariant(options?.model);
|
|
2393
|
+
throw new Error(
|
|
2394
|
+
`OpenCode prompt_async failed: HTTP ${res.status}${text ? `: ${text}` : ""}${variant ? ` (variant: ${variant})` : ""}`
|
|
2395
|
+
);
|
|
2243
2396
|
}
|
|
2244
2397
|
const READ_BACK_ATTEMPTS = 5;
|
|
2245
2398
|
const READ_BACK_DELAY_MS = 150;
|
|
@@ -2394,7 +2547,7 @@ function isPreamblePinnedRunning(messages, userMessageId) {
|
|
|
2394
2547
|
return completedOf(reply) != null && finishOf(reply) === "tool-calls";
|
|
2395
2548
|
}
|
|
2396
2549
|
function isB2AbandonmentConfirmed(params) {
|
|
2397
|
-
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false;
|
|
2550
|
+
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
|
|
2398
2551
|
}
|
|
2399
2552
|
function isAmbiguousTerminalFinish(m) {
|
|
2400
2553
|
if (completedOf(m) == null) return false;
|
|
@@ -2407,7 +2560,7 @@ function isAmbiguousFinishPinnedRunning(messages, userMessageId) {
|
|
|
2407
2560
|
return isAmbiguousTerminalFinish(reply);
|
|
2408
2561
|
}
|
|
2409
2562
|
function isAmbiguousFinishResolved(params) {
|
|
2410
|
-
return params.sessionOngoing === false || params.pinnedForMs >= params.maxPinnedMs;
|
|
2563
|
+
return params.sessionOngoing === false || params.sessionOngoing !== true && params.pinnedForMs >= params.maxPinnedMs;
|
|
2411
2564
|
}
|
|
2412
2565
|
function messageError(messages, userMessageId) {
|
|
2413
2566
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
@@ -3189,6 +3342,22 @@ function writeTunnelReadyMarker(path, agentId) {
|
|
|
3189
3342
|
}
|
|
3190
3343
|
}
|
|
3191
3344
|
|
|
3345
|
+
// src/lib/replication.ts
|
|
3346
|
+
import { spawn as spawn2 } from "child_process";
|
|
3347
|
+
function startSessionDbReplication(configPath) {
|
|
3348
|
+
return spawn2("litestream", ["replicate", "-config", configPath], {
|
|
3349
|
+
stdio: "inherit"
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
async function stopSessionDbReplication(child, timeoutMs) {
|
|
3353
|
+
return stopProcessAndWait(
|
|
3354
|
+
child,
|
|
3355
|
+
timeoutMs,
|
|
3356
|
+
() => child.kill("SIGTERM"),
|
|
3357
|
+
() => child.kill("SIGKILL")
|
|
3358
|
+
);
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3192
3361
|
// src/lib/openai-usage.ts
|
|
3193
3362
|
import { readFileSync as readFileSync3 } from "fs";
|
|
3194
3363
|
import { homedir as homedir2 } from "os";
|
|
@@ -5683,6 +5852,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5683
5852
|
deliveryDeadlineAnchored: false,
|
|
5684
5853
|
b2PinnedSinceMs: 0,
|
|
5685
5854
|
b2LastDescendantCheckMs: 0,
|
|
5855
|
+
b2RootOngoingHeldLogged: false,
|
|
5686
5856
|
b2AbandonedSignalled: false,
|
|
5687
5857
|
ambiguousPinnedSinceMs: 0,
|
|
5688
5858
|
ambiguousResolved: false
|
|
@@ -5777,6 +5947,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5777
5947
|
deliveryDeadlineAnchored: false,
|
|
5778
5948
|
b2PinnedSinceMs: 0,
|
|
5779
5949
|
b2LastDescendantCheckMs: 0,
|
|
5950
|
+
b2RootOngoingHeldLogged: false,
|
|
5780
5951
|
b2AbandonedSignalled: false,
|
|
5781
5952
|
ambiguousPinnedSinceMs: 0,
|
|
5782
5953
|
ambiguousResolved: false
|
|
@@ -6130,6 +6301,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6130
6301
|
if (snapshotReadable) {
|
|
6131
6302
|
inFlight.b2PinnedSinceMs = 0;
|
|
6132
6303
|
inFlight.b2LastDescendantCheckMs = 0;
|
|
6304
|
+
inFlight.b2RootOngoingHeldLogged = false;
|
|
6133
6305
|
inFlight.b2AbandonedSignalled = false;
|
|
6134
6306
|
}
|
|
6135
6307
|
} else {
|
|
@@ -6141,11 +6313,15 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6141
6313
|
const pinnedForMs = this.now() - inFlight.b2PinnedSinceMs;
|
|
6142
6314
|
if (pinnedForMs >= B2_ABANDONMENT_MIN_PINNED_MS && this.now() - inFlight.b2LastDescendantCheckMs >= B2_ABANDONMENT_RECHECK_MS) {
|
|
6143
6315
|
inFlight.b2LastDescendantCheckMs = this.now();
|
|
6144
|
-
const descendantOngoing = await
|
|
6316
|
+
const [descendantOngoing, rootOngoing] = await Promise.all([
|
|
6317
|
+
this.isAnyDescendantSessionOngoing(sessionId),
|
|
6318
|
+
isSessionOngoing(this.port, sessionId)
|
|
6319
|
+
]);
|
|
6145
6320
|
if (isB2AbandonmentConfirmed({
|
|
6146
6321
|
pinnedForMs,
|
|
6147
6322
|
minPinnedMs: B2_ABANDONMENT_MIN_PINNED_MS,
|
|
6148
|
-
descendantOngoing
|
|
6323
|
+
descendantOngoing,
|
|
6324
|
+
rootOngoing
|
|
6149
6325
|
})) {
|
|
6150
6326
|
inFlight.b2AbandonedSignalled = true;
|
|
6151
6327
|
this.log({
|
|
@@ -6154,12 +6330,26 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6154
6330
|
conversation_id: conv.id,
|
|
6155
6331
|
message_id: id
|
|
6156
6332
|
});
|
|
6333
|
+
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
6157
6334
|
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
6158
|
-
watched_for_ms: pinnedForMs
|
|
6335
|
+
watched_for_ms: pinnedForMs,
|
|
6336
|
+
finish: reply?.info?.finish ?? reply?.finish,
|
|
6337
|
+
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
6338
|
+
...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
|
|
6339
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
6159
6340
|
});
|
|
6160
6341
|
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
6161
6342
|
return;
|
|
6162
6343
|
}
|
|
6344
|
+
if (rootOngoing === true && !inFlight.b2RootOngoingHeldLogged) {
|
|
6345
|
+
inFlight.b2RootOngoingHeldLogged = true;
|
|
6346
|
+
this.log({
|
|
6347
|
+
level: "warn",
|
|
6348
|
+
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`,
|
|
6349
|
+
conversation_id: conv.id,
|
|
6350
|
+
message_id: id
|
|
6351
|
+
});
|
|
6352
|
+
}
|
|
6163
6353
|
}
|
|
6164
6354
|
}
|
|
6165
6355
|
const ambiguousPinnedNow = activelyRunning && isAmbiguousFinishPinnedRunning(messages, inFlight.opencodeMessageId);
|
|
@@ -7902,6 +8092,7 @@ var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_
|
|
|
7902
8092
|
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
7903
8093
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
|
|
7904
8094
|
var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
8095
|
+
var CHILD_STOP_TIMEOUT_MS = 1e4;
|
|
7905
8096
|
function resolveLogLevel(options) {
|
|
7906
8097
|
const accepted = Object.keys(LOG_LEVELS);
|
|
7907
8098
|
const validate = (value, source) => {
|
|
@@ -8507,7 +8698,17 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
8507
8698
|
setTimer: (timer) => {
|
|
8508
8699
|
state.claudeUsageTimer = timer;
|
|
8509
8700
|
},
|
|
8510
|
-
fetchUsage:
|
|
8701
|
+
fetchUsage: async () => {
|
|
8702
|
+
const usage = await getClaudeUsage();
|
|
8703
|
+
if (usage.ownerLookupError) {
|
|
8704
|
+
logActivity(state, {
|
|
8705
|
+
type: "info",
|
|
8706
|
+
level: "debug",
|
|
8707
|
+
message: `Claude usage owner lookup failed: ${usage.ownerLookupError}`
|
|
8708
|
+
});
|
|
8709
|
+
}
|
|
8710
|
+
return usage;
|
|
8711
|
+
},
|
|
8511
8712
|
report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
|
|
8512
8713
|
isLocalCredentialProblem,
|
|
8513
8714
|
forcedOnHint: "run `claude` to sign in",
|
|
@@ -8679,15 +8880,31 @@ async function cleanup(state, opts = {}) {
|
|
|
8679
8880
|
}
|
|
8680
8881
|
if (state.opencodeProcess) {
|
|
8681
8882
|
const opencodeProcess = state.opencodeProcess;
|
|
8682
|
-
|
|
8883
|
+
const result = await timeShutdownPhase(
|
|
8884
|
+
state,
|
|
8885
|
+
durations,
|
|
8886
|
+
"opencode_stop",
|
|
8887
|
+
() => stopOpenCodeAndWait(opencodeProcess, CHILD_STOP_TIMEOUT_MS)
|
|
8888
|
+
);
|
|
8683
8889
|
if (state.interactive) {
|
|
8684
|
-
logActivity(state, { type: "info", message:
|
|
8890
|
+
logActivity(state, { type: "info", message: `Stopped OpenCode process (${result.outcome})` });
|
|
8685
8891
|
displayStatus(state);
|
|
8686
8892
|
} else {
|
|
8687
|
-
log2(state,
|
|
8893
|
+
log2(state, `Stopped OpenCode process (${result.outcome})`);
|
|
8688
8894
|
}
|
|
8689
8895
|
state.opencodeProcess = null;
|
|
8690
8896
|
}
|
|
8897
|
+
if (state.litestreamProcess) {
|
|
8898
|
+
const litestreamProcess = state.litestreamProcess;
|
|
8899
|
+
const result = await timeShutdownPhase(
|
|
8900
|
+
state,
|
|
8901
|
+
durations,
|
|
8902
|
+
"litestream_stop",
|
|
8903
|
+
() => stopSessionDbReplication(litestreamProcess, CHILD_STOP_TIMEOUT_MS)
|
|
8904
|
+
);
|
|
8905
|
+
log2(state, `Stopped litestream replication (${result.outcome})`);
|
|
8906
|
+
state.litestreamProcess = null;
|
|
8907
|
+
}
|
|
8691
8908
|
return durations;
|
|
8692
8909
|
}
|
|
8693
8910
|
async function run(options) {
|
|
@@ -8721,6 +8938,7 @@ async function run(options) {
|
|
|
8721
8938
|
opencodeConnected: false,
|
|
8722
8939
|
opencodeVersion: null,
|
|
8723
8940
|
opencodeProcess: null,
|
|
8941
|
+
litestreamProcess: null,
|
|
8724
8942
|
connection: null,
|
|
8725
8943
|
channelDriver: null,
|
|
8726
8944
|
running: true,
|
|
@@ -8985,6 +9203,40 @@ async function run(options) {
|
|
|
8985
9203
|
ocSpinner?.fail(error2.message);
|
|
8986
9204
|
throw error2;
|
|
8987
9205
|
}
|
|
9206
|
+
if (options.litestreamConfig) {
|
|
9207
|
+
const litestreamProcess = startSessionDbReplication(options.litestreamConfig);
|
|
9208
|
+
state.litestreamProcess = litestreamProcess;
|
|
9209
|
+
let failureHandled = false;
|
|
9210
|
+
const failRunForReplication = (message) => {
|
|
9211
|
+
if (failureHandled || state.shuttingDown || !state.running) return;
|
|
9212
|
+
failureHandled = true;
|
|
9213
|
+
state.shuttingDown = true;
|
|
9214
|
+
logActivity(state, { type: "error", error: message });
|
|
9215
|
+
if (state.interactive) displayStatus(state);
|
|
9216
|
+
void (async () => {
|
|
9217
|
+
try {
|
|
9218
|
+
await cleanup(state);
|
|
9219
|
+
await shutdownTelemetry();
|
|
9220
|
+
} catch (error2) {
|
|
9221
|
+
console.error(
|
|
9222
|
+
`[run] replication failure cleanup failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
9223
|
+
);
|
|
9224
|
+
}
|
|
9225
|
+
process.exit(1);
|
|
9226
|
+
})();
|
|
9227
|
+
};
|
|
9228
|
+
litestreamProcess.on("exit", (code, signal) => {
|
|
9229
|
+
failRunForReplication(
|
|
9230
|
+
`Litestream replication stopped unexpectedly (code: ${code ?? "null"}, signal: ${signal ?? "none"})`
|
|
9231
|
+
);
|
|
9232
|
+
});
|
|
9233
|
+
litestreamProcess.on("error", (error2) => {
|
|
9234
|
+
failRunForReplication(
|
|
9235
|
+
`Litestream replication failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
9236
|
+
);
|
|
9237
|
+
});
|
|
9238
|
+
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
9239
|
+
}
|
|
8988
9240
|
const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
|
|
8989
9241
|
const channelDriver = new ChannelDriver({
|
|
8990
9242
|
agentId: state.agentId,
|
|
@@ -9245,6 +9497,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
9245
9497
|
).option(
|
|
9246
9498
|
"--tunnel-ready-file <path>",
|
|
9247
9499
|
"Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
|
|
9500
|
+
).option(
|
|
9501
|
+
"--litestream-config <path>",
|
|
9502
|
+
"Replicate the OpenCode session database with `litestream replicate` using this config file (written by the runner image). Omit to disable replication."
|
|
9248
9503
|
).action(
|
|
9249
9504
|
(options) => {
|
|
9250
9505
|
run({
|
|
@@ -9276,7 +9531,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
9276
9531
|
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
9277
9532
|
// resolveFileSyncDirectories.
|
|
9278
9533
|
enableFileSyncTo: options.enableFileSyncTo,
|
|
9279
|
-
tunnelReadyFile: options.tunnelReadyFile
|
|
9534
|
+
tunnelReadyFile: options.tunnelReadyFile,
|
|
9535
|
+
litestreamConfig: options.litestreamConfig
|
|
9280
9536
|
});
|
|
9281
9537
|
}
|
|
9282
9538
|
);
|