@evident-ai/cli 3.4.1-dev.ab61560 → 3.4.1-dev.b1151a3
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 +11 -0
- package/dist/index.js +1135 -217
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,8 +11,8 @@ import chalk2 from "chalk";
|
|
|
11
11
|
|
|
12
12
|
// src/lib/config.ts
|
|
13
13
|
import Conf from "conf";
|
|
14
|
-
import { chmodSync, existsSync, statSync } from "fs";
|
|
15
|
-
import { dirname } from "path";
|
|
14
|
+
import { chmodSync, existsSync, statSync } from "node:fs";
|
|
15
|
+
import { dirname } from "node:path";
|
|
16
16
|
var PRODUCTION_API_URL = "https://api.production.evident.run/v1";
|
|
17
17
|
var PRODUCTION_TUNNEL_URL = "wss://tunnel.production.evident.run";
|
|
18
18
|
var defaults = {
|
|
@@ -623,6 +623,19 @@ function isInteractive(jsonOutput) {
|
|
|
623
623
|
return true;
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
+
// src/lib/subscription-usage-report.ts
|
|
627
|
+
function toReportedSubscription(collected) {
|
|
628
|
+
if (!collected) return null;
|
|
629
|
+
if (collected.ownerEmail === null && collected.planType === null && collected.organizationName === null) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
owner_email: collected.ownerEmail,
|
|
634
|
+
plan_type: collected.planType,
|
|
635
|
+
organization_name: collected.organizationName
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
626
639
|
// src/commands/agent-lookup.ts
|
|
627
640
|
async function readErrorMessage(response) {
|
|
628
641
|
const text = await response.text().catch(() => "");
|
|
@@ -722,14 +735,6 @@ function toReportedWindow(window) {
|
|
|
722
735
|
if (!window) return null;
|
|
723
736
|
return { utilization: window.utilization, resets_at: window.resetsAt };
|
|
724
737
|
}
|
|
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
|
-
}
|
|
733
738
|
async function reportClaudeUsage(agentId, authHeader, snapshot) {
|
|
734
739
|
try {
|
|
735
740
|
const apiUrl = getApiUrlConfig();
|
|
@@ -739,7 +744,7 @@ async function reportClaudeUsage(agentId, authHeader, snapshot) {
|
|
|
739
744
|
body: JSON.stringify({
|
|
740
745
|
five_hour: toReportedWindow(snapshot.fiveHour),
|
|
741
746
|
seven_day: toReportedWindow(snapshot.sevenDay),
|
|
742
|
-
|
|
747
|
+
subscription: toReportedSubscription(snapshot.subscription)
|
|
743
748
|
}),
|
|
744
749
|
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
745
750
|
});
|
|
@@ -773,7 +778,8 @@ async function reportOpenAiUsage(agentId, authHeader, snapshot) {
|
|
|
773
778
|
primary: toReportedOpenAiWindow(snapshot.primary),
|
|
774
779
|
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
775
780
|
has_credits: snapshot.hasCredits,
|
|
776
|
-
credits_unlimited: snapshot.creditsUnlimited
|
|
781
|
+
credits_unlimited: snapshot.creditsUnlimited,
|
|
782
|
+
subscription: toReportedSubscription(snapshot.subscription)
|
|
777
783
|
}),
|
|
778
784
|
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
779
785
|
});
|
|
@@ -797,6 +803,7 @@ async function reportResourceUsage(agentId, authHeader, usage) {
|
|
|
797
803
|
headers: { Authorization: authHeader, "Content-Type": "application/json" },
|
|
798
804
|
body: JSON.stringify({
|
|
799
805
|
cpu_percent: usage.cpuPercent,
|
|
806
|
+
cpu_peak_percent: usage.cpuPeakPercent,
|
|
800
807
|
cpu_count: usage.cpuCount,
|
|
801
808
|
memory_total_bytes: usage.memoryTotalBytes,
|
|
802
809
|
memory_available_bytes: usage.memoryAvailableBytes,
|
|
@@ -1000,10 +1007,10 @@ async function status(options = {}) {
|
|
|
1000
1007
|
}
|
|
1001
1008
|
|
|
1002
1009
|
// src/lib/claude-usage.ts
|
|
1003
|
-
import { execFileSync } from "child_process";
|
|
1004
|
-
import { readFileSync } from "fs";
|
|
1005
|
-
import { homedir } from "os";
|
|
1006
|
-
import { join } from "path";
|
|
1010
|
+
import { execFileSync } from "node:child_process";
|
|
1011
|
+
import { readFileSync } from "node:fs";
|
|
1012
|
+
import { homedir } from "node:os";
|
|
1013
|
+
import { join } from "node:path";
|
|
1007
1014
|
var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
1008
1015
|
var CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
1009
1016
|
var CLAUDE_PROFILE_TIMEOUT_MS = 2e3;
|
|
@@ -1088,7 +1095,7 @@ function ownerLookupFailure(error2) {
|
|
|
1088
1095
|
}
|
|
1089
1096
|
async function getClaudeUsageOwner(accessToken) {
|
|
1090
1097
|
if (cachedOwner?.accessToken === accessToken) {
|
|
1091
|
-
return {
|
|
1098
|
+
return { subscription: cachedOwner.owner, ownerLookupError: null };
|
|
1092
1099
|
}
|
|
1093
1100
|
try {
|
|
1094
1101
|
const response = await fetch(CLAUDE_PROFILE_URL, {
|
|
@@ -1100,27 +1107,27 @@ async function getClaudeUsageOwner(accessToken) {
|
|
|
1100
1107
|
signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
|
|
1101
1108
|
});
|
|
1102
1109
|
if (!response.ok) {
|
|
1103
|
-
return {
|
|
1110
|
+
return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
|
|
1104
1111
|
}
|
|
1105
1112
|
let body;
|
|
1106
1113
|
try {
|
|
1107
1114
|
body = await response.json();
|
|
1108
1115
|
} catch (error2) {
|
|
1109
|
-
return {
|
|
1116
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1110
1117
|
}
|
|
1111
1118
|
const profile = body;
|
|
1112
1119
|
if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
|
|
1113
|
-
return {
|
|
1120
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1114
1121
|
}
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1122
|
+
const subscription = {
|
|
1123
|
+
ownerEmail: profile.account.email,
|
|
1117
1124
|
organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
|
|
1118
|
-
|
|
1125
|
+
planType: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
|
|
1119
1126
|
};
|
|
1120
|
-
cachedOwner = { accessToken, owner };
|
|
1121
|
-
return {
|
|
1127
|
+
cachedOwner = { accessToken, owner: subscription };
|
|
1128
|
+
return { subscription, ownerLookupError: null };
|
|
1122
1129
|
} catch (error2) {
|
|
1123
|
-
return {
|
|
1130
|
+
return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
|
|
1124
1131
|
}
|
|
1125
1132
|
}
|
|
1126
1133
|
async function getClaudeUsage() {
|
|
@@ -1149,11 +1156,11 @@ async function getClaudeUsage() {
|
|
|
1149
1156
|
throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
|
|
1150
1157
|
}
|
|
1151
1158
|
const body = await res.json();
|
|
1152
|
-
const {
|
|
1159
|
+
const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
|
|
1153
1160
|
return {
|
|
1154
1161
|
fiveHour: toWindow(body.five_hour),
|
|
1155
1162
|
sevenDay: toWindow(body.seven_day),
|
|
1156
|
-
|
|
1163
|
+
subscription,
|
|
1157
1164
|
ownerLookupError
|
|
1158
1165
|
};
|
|
1159
1166
|
}
|
|
@@ -1183,10 +1190,10 @@ async function claudeUsage() {
|
|
|
1183
1190
|
}
|
|
1184
1191
|
|
|
1185
1192
|
// src/commands/run.ts
|
|
1186
|
-
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
1187
|
-
import { homedir as
|
|
1188
|
-
import { isAbsolute as isAbsolute3, join as
|
|
1189
|
-
import
|
|
1193
|
+
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1194
|
+
import { homedir as homedir6 } from "node:os";
|
|
1195
|
+
import { isAbsolute as isAbsolute3, join as join10, parse, resolve as resolvePath2 } from "node:path";
|
|
1196
|
+
import chalk7 from "chalk";
|
|
1190
1197
|
|
|
1191
1198
|
// ../../packages/types/src/agents/index.ts
|
|
1192
1199
|
var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
|
|
@@ -1207,6 +1214,7 @@ var TelemetryEventTypes = {
|
|
|
1207
1214
|
// ../../packages/types/src/tunnel/index.ts
|
|
1208
1215
|
var MAX_FRAME_BYTES = 256 * 1024;
|
|
1209
1216
|
var TUNNEL_DRAIN_PING_PATH = "/__evident/drain";
|
|
1217
|
+
var TUNNEL_USAGE_REARM_PING_PATH = "/__evident/usage-rearm";
|
|
1210
1218
|
|
|
1211
1219
|
// ../../packages/types/src/runner-files.ts
|
|
1212
1220
|
var MAX_FILE_PUSH_BYTES = 64 * 1024;
|
|
@@ -1244,7 +1252,7 @@ function stripQuery(url) {
|
|
|
1244
1252
|
|
|
1245
1253
|
// src/commands/run.ts
|
|
1246
1254
|
import ora3 from "ora";
|
|
1247
|
-
import { select as
|
|
1255
|
+
import { select as select4 } from "@inquirer/prompts";
|
|
1248
1256
|
|
|
1249
1257
|
// src/lib/telemetry.ts
|
|
1250
1258
|
var CLI_VERSION = (true ? "3.0.0" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
@@ -1417,12 +1425,50 @@ var SEVERITY_BY_LEVEL = {
|
|
|
1417
1425
|
warn: "warning",
|
|
1418
1426
|
error: "error"
|
|
1419
1427
|
};
|
|
1428
|
+
function parseOpenCodeLogLine(line) {
|
|
1429
|
+
const normalisedLine = line.replace(/\r$/, "");
|
|
1430
|
+
const levelMatch = normalisedLine.match(/(?:^|\s)level=(\w+)/i);
|
|
1431
|
+
if (!levelMatch) return null;
|
|
1432
|
+
const level = levelMatch[1].toUpperCase();
|
|
1433
|
+
if (level !== "WARN" && level !== "ERROR") return null;
|
|
1434
|
+
const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
|
|
1435
|
+
return { level: level === "WARN" ? "warn" : "error", sessionID: sessionMatch?.[1] };
|
|
1436
|
+
}
|
|
1437
|
+
var MAX_LINE_BUFFER_BYTES = 16 * 1024;
|
|
1438
|
+
function createOpenCodeActivityForwarder(getContext) {
|
|
1439
|
+
let buffer = Buffer.alloc(0);
|
|
1440
|
+
const flushLine = (line) => {
|
|
1441
|
+
const parsed = parseOpenCodeLogLine(line);
|
|
1442
|
+
if (!parsed) return;
|
|
1443
|
+
forwardRunnerActivity(
|
|
1444
|
+
{
|
|
1445
|
+
level: parsed.level,
|
|
1446
|
+
error: line,
|
|
1447
|
+
metadata: parsed.sessionID ? { sessionID: parsed.sessionID } : void 0,
|
|
1448
|
+
source: "opencode"
|
|
1449
|
+
},
|
|
1450
|
+
getContext()
|
|
1451
|
+
);
|
|
1452
|
+
};
|
|
1453
|
+
return (chunk) => {
|
|
1454
|
+
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
|
|
1455
|
+
let newlineIndex;
|
|
1456
|
+
while ((newlineIndex = buffer.indexOf(10)) !== -1) {
|
|
1457
|
+
flushLine(buffer.subarray(0, newlineIndex).toString("utf-8").replace(/\r$/, ""));
|
|
1458
|
+
buffer = buffer.subarray(newlineIndex + 1);
|
|
1459
|
+
}
|
|
1460
|
+
if (buffer.length > MAX_LINE_BUFFER_BYTES) {
|
|
1461
|
+
flushLine(buffer.toString("utf-8"));
|
|
1462
|
+
buffer = Buffer.alloc(0);
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1420
1466
|
var MAX_MESSAGE_LENGTH = 500;
|
|
1421
1467
|
var MAX_METADATA_VALUE_LENGTH = 200;
|
|
1422
1468
|
var MAX_METADATA_ENTRIES = 20;
|
|
1423
1469
|
var TRUNCATION_MARKER = "\u2026";
|
|
1424
1470
|
function redact(message) {
|
|
1425
|
-
return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/https?:\/\/\S+/g, "<url>");
|
|
1471
|
+
return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/(?<![A-Za-z0-9_-])sk-ant-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-ant-***").replace(/(?<![A-Za-z0-9_-])sk-proj-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-proj-***").replace(/(?<![A-Za-z0-9_-])sk-[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "sk-***").replace(/(?<![A-Za-z0-9_-])(gh[oprsu])_[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "$1_***").replace(/(?<![A-Za-z0-9_-])github_pat_[A-Za-z0-9_]{20,}(?![A-Za-z0-9_-])/g, "github_pat_***").replace(/https?:\/\/\S+/g, "<url>");
|
|
1426
1472
|
}
|
|
1427
1473
|
function truncate(message) {
|
|
1428
1474
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
@@ -1448,43 +1494,47 @@ function sanitiseMetadata(metadata) {
|
|
|
1448
1494
|
}
|
|
1449
1495
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1450
1496
|
var RATE_LIMIT_MAX_EVENTS = 30;
|
|
1451
|
-
var
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1497
|
+
var rateWindows = /* @__PURE__ */ new Map();
|
|
1498
|
+
function admitUnderRateLimit(source, now) {
|
|
1499
|
+
let window = rateWindows.get(source);
|
|
1500
|
+
if (!window) {
|
|
1501
|
+
window = { windowStartedAt: 0, windowCount: 0, windowDroppedCount: 0 };
|
|
1502
|
+
rateWindows.set(source, window);
|
|
1503
|
+
}
|
|
1504
|
+
if (now - window.windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
|
|
1505
|
+
if (window.windowDroppedCount > 0) {
|
|
1457
1506
|
console.error(
|
|
1458
|
-
`[runner-activity-telemetry] rate cap reached: dropped ${windowDroppedCount} ${windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (cap ${RATE_LIMIT_MAX_EVENTS}/min)`
|
|
1507
|
+
`[runner-activity-telemetry] rate cap reached: dropped ${window.windowDroppedCount} ${window.windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (cap ${RATE_LIMIT_MAX_EVENTS}/min) for source "${source}"`
|
|
1459
1508
|
);
|
|
1460
1509
|
}
|
|
1461
|
-
windowStartedAt = now;
|
|
1462
|
-
windowCount = 0;
|
|
1463
|
-
windowDroppedCount = 0;
|
|
1510
|
+
window.windowStartedAt = now;
|
|
1511
|
+
window.windowCount = 0;
|
|
1512
|
+
window.windowDroppedCount = 0;
|
|
1464
1513
|
}
|
|
1465
|
-
if (windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1466
|
-
windowDroppedCount++;
|
|
1467
|
-
if (windowDroppedCount === 1) {
|
|
1514
|
+
if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1515
|
+
window.windowDroppedCount++;
|
|
1516
|
+
if (window.windowDroppedCount === 1) {
|
|
1468
1517
|
console.error(
|
|
1469
|
-
`[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ${RATE_LIMIT_WINDOW_MS / 1e3}s) \u2014 dropping further entries this window`
|
|
1518
|
+
`[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ${RATE_LIMIT_WINDOW_MS / 1e3}s) \u2014 dropping further entries this window for source "${source}"`
|
|
1470
1519
|
);
|
|
1471
1520
|
}
|
|
1472
1521
|
return false;
|
|
1473
1522
|
}
|
|
1474
|
-
windowCount++;
|
|
1523
|
+
window.windowCount++;
|
|
1475
1524
|
return true;
|
|
1476
1525
|
}
|
|
1477
1526
|
function forwardRunnerActivity(entry, context) {
|
|
1478
1527
|
try {
|
|
1479
1528
|
if (!FORWARDED_LEVELS.has(entry.level)) return;
|
|
1480
1529
|
if (!context.agentId || !context.authHeader) return;
|
|
1481
|
-
|
|
1530
|
+
const source = entry.source ?? "cli.run";
|
|
1531
|
+
if (!admitUnderRateLimit(source, Date.now())) return;
|
|
1482
1532
|
const rawMessage = entry.error ?? entry.message ?? "";
|
|
1483
1533
|
const message = truncate(redact(rawMessage));
|
|
1484
1534
|
logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
|
|
1485
1535
|
severity: SEVERITY_BY_LEVEL[entry.level],
|
|
1486
1536
|
message,
|
|
1487
|
-
metadata: { ...sanitiseMetadata(entry.metadata), source
|
|
1537
|
+
metadata: { ...sanitiseMetadata(entry.metadata), source },
|
|
1488
1538
|
agentId: context.agentId
|
|
1489
1539
|
});
|
|
1490
1540
|
} catch (err) {
|
|
@@ -1495,8 +1545,8 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1495
1545
|
}
|
|
1496
1546
|
|
|
1497
1547
|
// src/lib/opencode/session-db-recovery-report.ts
|
|
1498
|
-
import { readFileSync as readFileSync2, unlinkSync } from "fs";
|
|
1499
|
-
import { join as join2 } from "path";
|
|
1548
|
+
import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
|
|
1549
|
+
import { join as join2 } from "node:path";
|
|
1500
1550
|
function sessionDbRecoveryReportPath(homeDir, env) {
|
|
1501
1551
|
const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
|
|
1502
1552
|
return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
|
|
@@ -1709,13 +1759,13 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
1709
1759
|
}
|
|
1710
1760
|
|
|
1711
1761
|
// src/lib/opencode/session-db-boot.ts
|
|
1712
|
-
import { spawn as spawn2 } from "child_process";
|
|
1713
|
-
import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "fs";
|
|
1714
|
-
import { homedir as homedir2 } from "os";
|
|
1715
|
-
import { dirname as dirname2, resolve as resolvePath } from "path";
|
|
1762
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1763
|
+
import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
1764
|
+
import { homedir as homedir2 } from "node:os";
|
|
1765
|
+
import { dirname as dirname2, resolve as resolvePath } from "node:path";
|
|
1716
1766
|
|
|
1717
1767
|
// src/lib/runner-synchroniser.ts
|
|
1718
|
-
import { spawn } from "child_process";
|
|
1768
|
+
import { spawn } from "node:child_process";
|
|
1719
1769
|
function appendError(stderr, error2) {
|
|
1720
1770
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1721
1771
|
return stderr === "" ? message : `${stderr}
|
|
@@ -2240,9 +2290,9 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
2240
2290
|
}
|
|
2241
2291
|
|
|
2242
2292
|
// src/lib/opencode/session-db-provenance.ts
|
|
2243
|
-
import { createRequire } from "module";
|
|
2244
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2245
|
-
import { dirname as dirname3, join as join3 } from "path";
|
|
2293
|
+
import { createRequire } from "node:module";
|
|
2294
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2295
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
2246
2296
|
var require2 = createRequire(import.meta.url);
|
|
2247
2297
|
function readSessionDbMigrationIds(dbPath) {
|
|
2248
2298
|
let db;
|
|
@@ -2436,6 +2486,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
2436
2486
|
|
|
2437
2487
|
// src/lib/opencode/process.ts
|
|
2438
2488
|
var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
|
|
2489
|
+
var VALID_OPENCODE_LOG_LEVELS = /* @__PURE__ */ new Set(["DEBUG", "INFO", "WARN", "ERROR"]);
|
|
2490
|
+
function resolveOpenCodeLogLevel(env) {
|
|
2491
|
+
const raw = env.OPENCODE_LOG_LEVEL;
|
|
2492
|
+
if (!raw) return "INFO";
|
|
2493
|
+
const upper = raw.toUpperCase();
|
|
2494
|
+
if (VALID_OPENCODE_LOG_LEVELS.has(upper)) return upper;
|
|
2495
|
+
console.warn(
|
|
2496
|
+
`startOpenCode: ignoring invalid OPENCODE_LOG_LEVEL "${raw}" (expected DEBUG|INFO|WARN|ERROR) \u2014 using INFO`
|
|
2497
|
+
);
|
|
2498
|
+
return "INFO";
|
|
2499
|
+
}
|
|
2439
2500
|
function getProcessCwd(pid) {
|
|
2440
2501
|
const platform = process.platform;
|
|
2441
2502
|
try {
|
|
@@ -2484,14 +2545,14 @@ function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
2484
2545
|
}
|
|
2485
2546
|
return null;
|
|
2486
2547
|
}
|
|
2487
|
-
function
|
|
2548
|
+
function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
2488
2549
|
const instances = [];
|
|
2489
2550
|
try {
|
|
2490
2551
|
const platform = process.platform;
|
|
2491
2552
|
if (platform === "darwin" || platform === "linux") {
|
|
2492
2553
|
let pids = [];
|
|
2493
2554
|
try {
|
|
2494
|
-
const pgrepOutput = execSync(
|
|
2555
|
+
const pgrepOutput = execSync(`pgrep -f "${pgrepPattern}"`, {
|
|
2495
2556
|
encoding: "utf-8",
|
|
2496
2557
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2497
2558
|
}).trim();
|
|
@@ -2500,7 +2561,7 @@ function findOpenCodeProcesses() {
|
|
|
2500
2561
|
}
|
|
2501
2562
|
} catch {
|
|
2502
2563
|
try {
|
|
2503
|
-
const psOutput = execSync(
|
|
2564
|
+
const psOutput = execSync(`ps aux | grep -E "${psPattern}" | grep -v grep`, {
|
|
2504
2565
|
encoding: "utf-8",
|
|
2505
2566
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2506
2567
|
}).trim();
|
|
@@ -2546,6 +2607,9 @@ function findOpenCodeProcesses() {
|
|
|
2546
2607
|
}
|
|
2547
2608
|
return instances;
|
|
2548
2609
|
}
|
|
2610
|
+
function findOpenCodeProcesses() {
|
|
2611
|
+
return findProcessesByPattern("opencode serve|opencode-serve", "opencode (serve|--port)");
|
|
2612
|
+
}
|
|
2549
2613
|
async function scanPortsForOpenCode() {
|
|
2550
2614
|
const instances = [];
|
|
2551
2615
|
const checks = OPENCODE_PORT_RANGE.map(async (port) => {
|
|
@@ -2592,7 +2656,7 @@ async function findHealthyOpenCodeInstances() {
|
|
|
2592
2656
|
}
|
|
2593
2657
|
async function startOpenCode(port, options = {}) {
|
|
2594
2658
|
let command = "opencode";
|
|
2595
|
-
const printLogs = options.inheritStdio ? ["--print-logs"] : [];
|
|
2659
|
+
const printLogs = options.inheritStdio ? ["--print-logs", "--log-level", resolveOpenCodeLogLevel(process.env)] : [];
|
|
2596
2660
|
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...printLogs];
|
|
2597
2661
|
try {
|
|
2598
2662
|
execSync("which opencode", { stdio: "ignore" });
|
|
@@ -2649,6 +2713,19 @@ function isOpenCodeInstalled() {
|
|
|
2649
2713
|
return false;
|
|
2650
2714
|
}
|
|
2651
2715
|
}
|
|
2716
|
+
function isOpenCode2Installed() {
|
|
2717
|
+
try {
|
|
2718
|
+
const platform = process.platform;
|
|
2719
|
+
if (platform === "win32") {
|
|
2720
|
+
execSync2("where opencode2", { stdio: "ignore" });
|
|
2721
|
+
} else {
|
|
2722
|
+
execSync2("which opencode2", { stdio: "ignore" });
|
|
2723
|
+
}
|
|
2724
|
+
return true;
|
|
2725
|
+
} catch {
|
|
2726
|
+
return false;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2652
2729
|
async function promptOpenCodeInstall(interactive) {
|
|
2653
2730
|
if (!interactive) {
|
|
2654
2731
|
console.log(
|
|
@@ -2658,7 +2735,11 @@ async function promptOpenCodeInstall(interactive) {
|
|
|
2658
2735
|
install_url: OPENCODE_INSTALL_URL,
|
|
2659
2736
|
install_commands: {
|
|
2660
2737
|
npm: "npm install -g opencode-ai",
|
|
2661
|
-
curl: "curl -fsSL https://opencode.ai/install.sh | sh"
|
|
2738
|
+
curl: "curl -fsSL https://opencode.ai/install.sh | sh",
|
|
2739
|
+
v2: {
|
|
2740
|
+
npm: "npm install -g @opencode-ai/cli@beta",
|
|
2741
|
+
curl: "curl -fsSL https://opencode.ai/v2/install | bash"
|
|
2742
|
+
}
|
|
2662
2743
|
}
|
|
2663
2744
|
})
|
|
2664
2745
|
);
|
|
@@ -3142,6 +3223,44 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
3142
3223
|
}
|
|
3143
3224
|
return lastOk ?? last;
|
|
3144
3225
|
}
|
|
3226
|
+
function collectSubagentSessions(messages, userMessageId) {
|
|
3227
|
+
if (!messages || messages.length === 0) return [];
|
|
3228
|
+
const byParent = messages.filter(
|
|
3229
|
+
(message) => roleOf(message) === "assistant" && parentIdOf(message) === userMessageId
|
|
3230
|
+
);
|
|
3231
|
+
const assistants = byParent.length > 0 ? byParent : [];
|
|
3232
|
+
if (assistants.length === 0) {
|
|
3233
|
+
const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
|
|
3234
|
+
if (userIndex === -1) return [];
|
|
3235
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
3236
|
+
const message = messages[i];
|
|
3237
|
+
if (roleOf(message) === "user") break;
|
|
3238
|
+
if (roleOf(message) === "assistant") assistants.push(message);
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
const refs = [];
|
|
3242
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3243
|
+
for (const message of assistants) {
|
|
3244
|
+
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
3245
|
+
for (const part of parts) {
|
|
3246
|
+
if (!part || typeof part !== "object" || part.type !== "tool" || part.tool !== "task")
|
|
3247
|
+
continue;
|
|
3248
|
+
const state = part.state;
|
|
3249
|
+
if (!state || typeof state !== "object") continue;
|
|
3250
|
+
const metadata = state.metadata;
|
|
3251
|
+
if (!metadata || typeof metadata !== "object") continue;
|
|
3252
|
+
const sessionId = metadata.sessionId;
|
|
3253
|
+
if (typeof sessionId !== "string" || sessionId.length === 0 || seen.has(sessionId)) continue;
|
|
3254
|
+
seen.add(sessionId);
|
|
3255
|
+
const start = state.time?.start;
|
|
3256
|
+
refs.push({
|
|
3257
|
+
sessionId,
|
|
3258
|
+
startedAtMs: typeof start === "number" && Number.isFinite(start) ? start : null
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
return refs;
|
|
3263
|
+
}
|
|
3145
3264
|
function messageUsage(messages, userMessageId) {
|
|
3146
3265
|
if (!messages || messages.length === 0) return null;
|
|
3147
3266
|
const byParentAll = messages.filter(
|
|
@@ -3270,8 +3389,7 @@ function isAbortedTerminalReply(messages, userMessageId) {
|
|
|
3270
3389
|
}
|
|
3271
3390
|
return false;
|
|
3272
3391
|
}
|
|
3273
|
-
function
|
|
3274
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
3392
|
+
function classifyReplyAuthError(reply) {
|
|
3275
3393
|
const error2 = errorOf(reply);
|
|
3276
3394
|
if (error2 == null || typeof error2 !== "object") return null;
|
|
3277
3395
|
const e = error2;
|
|
@@ -3296,6 +3414,32 @@ function messageFailure(messages, userMessageId) {
|
|
|
3296
3414
|
}
|
|
3297
3415
|
return null;
|
|
3298
3416
|
}
|
|
3417
|
+
function messageFailure(messages, userMessageId) {
|
|
3418
|
+
return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
|
|
3419
|
+
}
|
|
3420
|
+
function findLatestSubagentAuthOutcome(messages, sinceMs) {
|
|
3421
|
+
if (!messages || messages.length === 0) return null;
|
|
3422
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3423
|
+
const message = messages[i];
|
|
3424
|
+
if (roleOf(message) !== "assistant") continue;
|
|
3425
|
+
const created = createdOf(message);
|
|
3426
|
+
if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
|
|
3427
|
+
const failure = classifyReplyAuthError(message);
|
|
3428
|
+
if (failure) {
|
|
3429
|
+
if (!failure.providerId) return null;
|
|
3430
|
+
return { providerId: failure.providerId, outcome: "failed", failure };
|
|
3431
|
+
}
|
|
3432
|
+
const providerId = message.info?.providerID;
|
|
3433
|
+
if (errorOf(message) == null && typeof providerId === "string" && providerId.length > 0) {
|
|
3434
|
+
return { providerId, outcome: "succeeded" };
|
|
3435
|
+
}
|
|
3436
|
+
return null;
|
|
3437
|
+
}
|
|
3438
|
+
return null;
|
|
3439
|
+
}
|
|
3440
|
+
function findSubagentAuthOutcome(messages, sinceMs) {
|
|
3441
|
+
return findLatestSubagentAuthOutcome(messages, sinceMs);
|
|
3442
|
+
}
|
|
3299
3443
|
function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProviderId, replyModelId = null) {
|
|
3300
3444
|
if (classified != null) return classified;
|
|
3301
3445
|
if (hasConfiguredProvider !== false) return null;
|
|
@@ -3343,6 +3487,94 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3343
3487
|
return null;
|
|
3344
3488
|
}
|
|
3345
3489
|
}
|
|
3490
|
+
function sessionErrorReason(error2) {
|
|
3491
|
+
const record = typeof error2 === "object" && error2 !== null ? error2 : null;
|
|
3492
|
+
const data = record?.data;
|
|
3493
|
+
const dataRecord = typeof data === "object" && data !== null ? data : null;
|
|
3494
|
+
const rawReason = typeof dataRecord?.message === "string" && dataRecord.message || typeof record?.message === "string" && record.message || typeof error2 === "string" && error2 || typeof record?.name === "string" && record.name || "OpenCode reported a session error with no details";
|
|
3495
|
+
const reason = rawReason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
3496
|
+
return reason || "OpenCode reported a session error with no details";
|
|
3497
|
+
}
|
|
3498
|
+
function parseSessionErrorFrame(data) {
|
|
3499
|
+
let parsed;
|
|
3500
|
+
try {
|
|
3501
|
+
parsed = JSON.parse(data);
|
|
3502
|
+
} catch (error2) {
|
|
3503
|
+
void error2;
|
|
3504
|
+
return null;
|
|
3505
|
+
}
|
|
3506
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
3507
|
+
const parsedRecord = parsed;
|
|
3508
|
+
const payload = parsedRecord.payload;
|
|
3509
|
+
const event = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : parsedRecord;
|
|
3510
|
+
if (event.type !== "session.error") return null;
|
|
3511
|
+
const properties = event.properties;
|
|
3512
|
+
if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
|
|
3513
|
+
return null;
|
|
3514
|
+
}
|
|
3515
|
+
const propertiesRecord = properties;
|
|
3516
|
+
const sessionId = propertiesRecord.sessionID;
|
|
3517
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
3518
|
+
return {
|
|
3519
|
+
sessionId,
|
|
3520
|
+
reason: sessionErrorReason(propertiesRecord.error)
|
|
3521
|
+
};
|
|
3522
|
+
}
|
|
3523
|
+
async function readSessionErrorStream(port, options) {
|
|
3524
|
+
let reader = null;
|
|
3525
|
+
try {
|
|
3526
|
+
const response = await fetch(`${opencodeBase(port)}/event`, {
|
|
3527
|
+
headers: { accept: "text/event-stream" },
|
|
3528
|
+
signal: options.signal
|
|
3529
|
+
});
|
|
3530
|
+
if (!response.ok || !response.body) {
|
|
3531
|
+
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
3532
|
+
}
|
|
3533
|
+
reader = response.body.getReader();
|
|
3534
|
+
const decoder = new TextDecoder();
|
|
3535
|
+
let buffer = "";
|
|
3536
|
+
const processLine = (line) => {
|
|
3537
|
+
const trimmed = line.trimEnd();
|
|
3538
|
+
if (!trimmed.startsWith("data:")) return;
|
|
3539
|
+
const event = parseSessionErrorFrame(trimmed.slice("data:".length).replace(/^ /, ""));
|
|
3540
|
+
if (event) options.onSessionError(event);
|
|
3541
|
+
};
|
|
3542
|
+
while (true) {
|
|
3543
|
+
const { done, value } = await reader.read();
|
|
3544
|
+
if (done) return { reason: "ended" };
|
|
3545
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3546
|
+
const lines = buffer.split("\n");
|
|
3547
|
+
buffer = lines.pop() ?? "";
|
|
3548
|
+
for (const line of lines) processLine(line);
|
|
3549
|
+
}
|
|
3550
|
+
} catch (err) {
|
|
3551
|
+
if (options.signal.aborted) return { reason: "aborted" };
|
|
3552
|
+
return {
|
|
3553
|
+
reason: "unavailable",
|
|
3554
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
3555
|
+
};
|
|
3556
|
+
} finally {
|
|
3557
|
+
if (reader) void reader.cancel().catch(() => void 0);
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
async function reloadProviderCache(port) {
|
|
3561
|
+
try {
|
|
3562
|
+
const res = await timedFetch(`${opencodeBase(port)}/config`, {
|
|
3563
|
+
method: "PATCH",
|
|
3564
|
+
headers: { "Content-Type": "application/json" },
|
|
3565
|
+
body: JSON.stringify({})
|
|
3566
|
+
});
|
|
3567
|
+
if (!res.ok) {
|
|
3568
|
+
console.error(
|
|
3569
|
+
`[reloadProviderCache] PATCH /config returned HTTP ${res.status} (port ${port})`
|
|
3570
|
+
);
|
|
3571
|
+
}
|
|
3572
|
+
} catch (err) {
|
|
3573
|
+
console.error(
|
|
3574
|
+
`[reloadProviderCache] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
3575
|
+
);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3346
3578
|
|
|
3347
3579
|
// src/lib/opencode/session-cleanup.ts
|
|
3348
3580
|
var DURATION_UNIT_MS = {
|
|
@@ -3449,8 +3681,8 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
3449
3681
|
}
|
|
3450
3682
|
|
|
3451
3683
|
// src/lib/opencode/session-db-size.ts
|
|
3452
|
-
import { statSync as statSync3 } from "fs";
|
|
3453
|
-
import { join as join4 } from "path";
|
|
3684
|
+
import { statSync as statSync3 } from "node:fs";
|
|
3685
|
+
import { join as join4 } from "node:path";
|
|
3454
3686
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
3455
3687
|
function statSessionDbBytes(homeDir) {
|
|
3456
3688
|
const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
@@ -3480,9 +3712,96 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
3480
3712
|
return null;
|
|
3481
3713
|
}
|
|
3482
3714
|
|
|
3715
|
+
// src/lib/opencode/log-tail.ts
|
|
3716
|
+
import { statSync as statSync4 } from "node:fs";
|
|
3717
|
+
import { homedir as homedir3 } from "node:os";
|
|
3718
|
+
import { join as join5 } from "node:path";
|
|
3719
|
+
import { open as open2, stat } from "node:fs/promises";
|
|
3720
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
3721
|
+
function resolveOpenCodeLogPath(homeDir = homedir3(), env = process.env) {
|
|
3722
|
+
const dataDir = env.XDG_DATA_HOME || join5(homeDir, ".local", "share");
|
|
3723
|
+
return join5(dataDir, "opencode", "log", "opencode.log");
|
|
3724
|
+
}
|
|
3725
|
+
function isEnoent(error2) {
|
|
3726
|
+
return error2?.code === "ENOENT";
|
|
3727
|
+
}
|
|
3728
|
+
function reportFailure(operation, logPath, error2) {
|
|
3729
|
+
console.error(
|
|
3730
|
+
`[opencode-log-tail] ${operation} failed for ${logPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3731
|
+
);
|
|
3732
|
+
}
|
|
3733
|
+
function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
3734
|
+
const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
3735
|
+
let offset = 0;
|
|
3736
|
+
let inode = null;
|
|
3737
|
+
let baselineReady = true;
|
|
3738
|
+
try {
|
|
3739
|
+
const initial = statSync4(logPath);
|
|
3740
|
+
offset = initial.size;
|
|
3741
|
+
inode = initial.ino;
|
|
3742
|
+
} catch (error2) {
|
|
3743
|
+
if (!isEnoent(error2)) {
|
|
3744
|
+
reportFailure("initial stat", logPath, error2);
|
|
3745
|
+
baselineReady = false;
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
let polling = false;
|
|
3749
|
+
let stopped = false;
|
|
3750
|
+
const poll = async () => {
|
|
3751
|
+
if (polling || stopped) return;
|
|
3752
|
+
polling = true;
|
|
3753
|
+
try {
|
|
3754
|
+
let current;
|
|
3755
|
+
try {
|
|
3756
|
+
current = await stat(logPath);
|
|
3757
|
+
} catch (error2) {
|
|
3758
|
+
if (!isEnoent(error2)) reportFailure("stat", logPath, error2);
|
|
3759
|
+
return;
|
|
3760
|
+
}
|
|
3761
|
+
if (!baselineReady) {
|
|
3762
|
+
offset = current.size;
|
|
3763
|
+
inode = current.ino;
|
|
3764
|
+
baselineReady = true;
|
|
3765
|
+
return;
|
|
3766
|
+
}
|
|
3767
|
+
if (inode !== null && current.ino !== inode || current.size < offset) {
|
|
3768
|
+
offset = 0;
|
|
3769
|
+
}
|
|
3770
|
+
inode = current.ino;
|
|
3771
|
+
if (current.size === offset) return;
|
|
3772
|
+
const length = current.size - offset;
|
|
3773
|
+
const fh = await open2(logPath, "r");
|
|
3774
|
+
try {
|
|
3775
|
+
const buf = Buffer.alloc(length);
|
|
3776
|
+
const { bytesRead } = await fh.read(buf, 0, length, offset);
|
|
3777
|
+
offset += bytesRead;
|
|
3778
|
+
if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
|
|
3779
|
+
} finally {
|
|
3780
|
+
await fh.close();
|
|
3781
|
+
}
|
|
3782
|
+
} catch (error2) {
|
|
3783
|
+
if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
|
|
3784
|
+
} finally {
|
|
3785
|
+
polling = false;
|
|
3786
|
+
}
|
|
3787
|
+
};
|
|
3788
|
+
const interval = setInterval(() => void poll(), pollIntervalMs);
|
|
3789
|
+
void poll();
|
|
3790
|
+
return {
|
|
3791
|
+
stop: () => {
|
|
3792
|
+
stopped = true;
|
|
3793
|
+
clearInterval(interval);
|
|
3794
|
+
}
|
|
3795
|
+
};
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3483
3798
|
// src/lib/opencode/session-db-reclaim.ts
|
|
3484
|
-
import { statSync as
|
|
3485
|
-
import { dirname as dirname4 } from "path";
|
|
3799
|
+
import { statSync as statSync5, statfsSync } from "node:fs";
|
|
3800
|
+
import { dirname as dirname4 } from "node:path";
|
|
3801
|
+
function errorMessage(error2) {
|
|
3802
|
+
if (!(error2 instanceof Error)) return String(error2);
|
|
3803
|
+
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
3804
|
+
}
|
|
3486
3805
|
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
3487
3806
|
try {
|
|
3488
3807
|
const fsStats = statfsSync(dirname4(dbPath));
|
|
@@ -3508,17 +3827,17 @@ async function probeReclaimAvailability(input) {
|
|
|
3508
3827
|
const { dbPath, requiredBytes } = input;
|
|
3509
3828
|
let sqlite;
|
|
3510
3829
|
try {
|
|
3511
|
-
sqlite = await import("sqlite");
|
|
3830
|
+
sqlite = await import("node:sqlite");
|
|
3512
3831
|
} catch (err) {
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
return "sqlite-unavailable";
|
|
3832
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
3833
|
+
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
3834
|
+
return { reason: "sqlite-unavailable", detail };
|
|
3517
3835
|
}
|
|
3518
3836
|
let autoVacuum = null;
|
|
3519
3837
|
try {
|
|
3520
3838
|
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
3521
3839
|
try {
|
|
3840
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3522
3841
|
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3523
3842
|
} finally {
|
|
3524
3843
|
db.close();
|
|
@@ -3529,23 +3848,25 @@ async function probeReclaimAvailability(input) {
|
|
|
3529
3848
|
);
|
|
3530
3849
|
}
|
|
3531
3850
|
if (autoVacuum !== 0) return null;
|
|
3532
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
|
|
3851
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
3533
3852
|
}
|
|
3534
3853
|
async function reclaimSessionDbSpace(input) {
|
|
3535
3854
|
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
3536
3855
|
let sqlite;
|
|
3537
3856
|
try {
|
|
3538
|
-
sqlite = await import("sqlite");
|
|
3857
|
+
sqlite = await import("node:sqlite");
|
|
3539
3858
|
} catch (err) {
|
|
3859
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
3540
3860
|
console.warn(
|
|
3541
|
-
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${
|
|
3861
|
+
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
3542
3862
|
);
|
|
3543
|
-
return { ok: false, skipped: "sqlite-unavailable" };
|
|
3863
|
+
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
3544
3864
|
}
|
|
3545
3865
|
const { DatabaseSync } = sqlite;
|
|
3546
3866
|
let db;
|
|
3547
3867
|
try {
|
|
3548
3868
|
db = new DatabaseSync(dbPath);
|
|
3869
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3549
3870
|
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3550
3871
|
if (autoVacuum === 0) {
|
|
3551
3872
|
if (!allowFullVacuum) {
|
|
@@ -3554,7 +3875,7 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3554
3875
|
);
|
|
3555
3876
|
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
3556
3877
|
}
|
|
3557
|
-
const fileBytesForGuard =
|
|
3878
|
+
const fileBytesForGuard = statSync5(dbPath).size;
|
|
3558
3879
|
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
3559
3880
|
if (skipReason !== null) {
|
|
3560
3881
|
console.warn(
|
|
@@ -3582,10 +3903,12 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3582
3903
|
);
|
|
3583
3904
|
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
3584
3905
|
} catch (err) {
|
|
3585
|
-
console.error(
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3906
|
+
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
3907
|
+
return {
|
|
3908
|
+
ok: false,
|
|
3909
|
+
skipped: "reclaim-error",
|
|
3910
|
+
detail: errorMessage(err)
|
|
3911
|
+
};
|
|
3589
3912
|
} finally {
|
|
3590
3913
|
db?.close();
|
|
3591
3914
|
}
|
|
@@ -3626,7 +3949,6 @@ var StreamForwarder = class {
|
|
|
3626
3949
|
handleFrame(frame) {
|
|
3627
3950
|
switch (frame.type) {
|
|
3628
3951
|
case "open":
|
|
3629
|
-
this.callbacks.onOpen?.(frame.sid, frame.method, frame.path);
|
|
3630
3952
|
void this.handleOpen(frame);
|
|
3631
3953
|
break;
|
|
3632
3954
|
case "req_data":
|
|
@@ -3662,12 +3984,21 @@ var StreamForwarder = class {
|
|
|
3662
3984
|
const { sid, method, path, headers, has_body } = frame;
|
|
3663
3985
|
const correlationId = headers?.[CORRELATION_ID_HEADER];
|
|
3664
3986
|
const startedAt = Date.now();
|
|
3987
|
+
if (path !== TUNNEL_DRAIN_PING_PATH && path !== TUNNEL_USAGE_REARM_PING_PATH) {
|
|
3988
|
+
this.callbacks.onOpen?.(sid, method, path);
|
|
3989
|
+
}
|
|
3665
3990
|
if (path === TUNNEL_DRAIN_PING_PATH) {
|
|
3666
3991
|
this.callbacks.onDrainPing?.();
|
|
3667
3992
|
this.send({ type: "head", sid, status: 204, headers: {} });
|
|
3668
3993
|
this.send({ type: "res_end", sid });
|
|
3669
3994
|
return;
|
|
3670
3995
|
}
|
|
3996
|
+
if (path === TUNNEL_USAGE_REARM_PING_PATH) {
|
|
3997
|
+
this.callbacks.onUsageRearmPing?.();
|
|
3998
|
+
this.send({ type: "head", sid, status: 204, headers: {} });
|
|
3999
|
+
this.send({ type: "res_end", sid });
|
|
4000
|
+
return;
|
|
4001
|
+
}
|
|
3671
4002
|
if (process.env.DEBUG) {
|
|
3672
4003
|
log("debug", "agent_request", {
|
|
3673
4004
|
correlation_id: correlationId,
|
|
@@ -3812,7 +4143,8 @@ function connectTunnel(options) {
|
|
|
3812
4143
|
onResponse,
|
|
3813
4144
|
onInfo,
|
|
3814
4145
|
onWarning,
|
|
3815
|
-
onDrainPing
|
|
4146
|
+
onDrainPing,
|
|
4147
|
+
onUsageRearmPing
|
|
3816
4148
|
} = options;
|
|
3817
4149
|
const tunnelUrl = getTunnelUrlConfig();
|
|
3818
4150
|
const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
|
|
@@ -3824,7 +4156,8 @@ function connectTunnel(options) {
|
|
|
3824
4156
|
});
|
|
3825
4157
|
const forwarder = new StreamForwarder(ws, port, {
|
|
3826
4158
|
onHead: () => onResponse?.(),
|
|
3827
|
-
onDrainPing: () => onDrainPing?.()
|
|
4159
|
+
onDrainPing: () => onDrainPing?.(),
|
|
4160
|
+
onUsageRearmPing: () => onUsageRearmPing?.()
|
|
3828
4161
|
});
|
|
3829
4162
|
const connectionTimeout = setTimeout(() => {
|
|
3830
4163
|
ws.close();
|
|
@@ -3867,8 +4200,8 @@ function connectTunnel(options) {
|
|
|
3867
4200
|
try {
|
|
3868
4201
|
message = JSON.parse(data.toString());
|
|
3869
4202
|
} catch (error2) {
|
|
3870
|
-
const
|
|
3871
|
-
onError?.(`Failed to handle message: ${
|
|
4203
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
4204
|
+
onError?.(`Failed to handle message: ${errorMessage3}`);
|
|
3872
4205
|
return;
|
|
3873
4206
|
}
|
|
3874
4207
|
if (isStreamFrame(message)) {
|
|
@@ -3985,6 +4318,7 @@ var RunnerConnection = class {
|
|
|
3985
4318
|
onError: (error2) => events.onError?.(error2),
|
|
3986
4319
|
onResponse: () => events.onResponse?.(),
|
|
3987
4320
|
onDrainPing: () => events.onDrainPing?.(),
|
|
4321
|
+
onUsageRearmPing: () => events.onUsageRearmPing?.(),
|
|
3988
4322
|
onInfo: (message) => events.onInfo?.(message),
|
|
3989
4323
|
onWarning: (message) => events.onWarning?.(message)
|
|
3990
4324
|
});
|
|
@@ -4011,7 +4345,7 @@ var RunnerConnection = class {
|
|
|
4011
4345
|
};
|
|
4012
4346
|
|
|
4013
4347
|
// src/lib/tunnel/ready-marker.ts
|
|
4014
|
-
import { writeFileSync as writeFileSync3 } from "fs";
|
|
4348
|
+
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
4015
4349
|
function writeTunnelReadyMarker(path, agentId) {
|
|
4016
4350
|
try {
|
|
4017
4351
|
writeFileSync3(path, `${agentId}
|
|
@@ -4023,7 +4357,7 @@ function writeTunnelReadyMarker(path, agentId) {
|
|
|
4023
4357
|
}
|
|
4024
4358
|
|
|
4025
4359
|
// src/lib/replication.ts
|
|
4026
|
-
import { spawn as spawn4 } from "child_process";
|
|
4360
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
4027
4361
|
function startSessionDbReplication(configPath) {
|
|
4028
4362
|
return spawn4("litestream", ["replicate", "-config", configPath], {
|
|
4029
4363
|
stdio: "inherit"
|
|
@@ -4039,7 +4373,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
4039
4373
|
}
|
|
4040
4374
|
|
|
4041
4375
|
// src/lib/process-liveness.ts
|
|
4042
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
4376
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
4043
4377
|
function isProcessAlive(pid) {
|
|
4044
4378
|
try {
|
|
4045
4379
|
process.kill(pid, 0);
|
|
@@ -4065,9 +4399,9 @@ function isProcessAlive(pid) {
|
|
|
4065
4399
|
}
|
|
4066
4400
|
|
|
4067
4401
|
// src/lib/openai-usage.ts
|
|
4068
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
4069
|
-
import { homedir as
|
|
4070
|
-
import { join as
|
|
4402
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
4403
|
+
import { homedir as homedir4 } from "node:os";
|
|
4404
|
+
import { join as join6 } from "node:path";
|
|
4071
4405
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4072
4406
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4073
4407
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4081,7 +4415,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4081
4415
|
}
|
|
4082
4416
|
function readOpenCodeChatGptCredentials() {
|
|
4083
4417
|
try {
|
|
4084
|
-
const raw = readFileSync5(
|
|
4418
|
+
const raw = readFileSync5(join6(homedir4(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4085
4419
|
let parsed;
|
|
4086
4420
|
try {
|
|
4087
4421
|
parsed = JSON.parse(raw);
|
|
@@ -4103,6 +4437,23 @@ function readOpenCodeChatGptCredentials() {
|
|
|
4103
4437
|
return null;
|
|
4104
4438
|
}
|
|
4105
4439
|
}
|
|
4440
|
+
function parseChatGptIdentity(accessToken) {
|
|
4441
|
+
const segments = accessToken.split(".");
|
|
4442
|
+
if (segments.length !== 3) return null;
|
|
4443
|
+
let payload;
|
|
4444
|
+
try {
|
|
4445
|
+
const parsed = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf8"));
|
|
4446
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
4447
|
+
payload = parsed;
|
|
4448
|
+
} catch {
|
|
4449
|
+
return null;
|
|
4450
|
+
}
|
|
4451
|
+
const profile = payload["https://api.openai.com/profile"];
|
|
4452
|
+
const auth = payload["https://api.openai.com/auth"];
|
|
4453
|
+
const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
|
|
4454
|
+
const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
|
|
4455
|
+
return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
|
|
4456
|
+
}
|
|
4106
4457
|
function toWindow2(headers, name) {
|
|
4107
4458
|
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
4108
4459
|
const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
|
|
@@ -4178,6 +4529,7 @@ async function getOpenAiUsage(port) {
|
|
|
4178
4529
|
"credentials_expired"
|
|
4179
4530
|
);
|
|
4180
4531
|
}
|
|
4532
|
+
const subscription = parseChatGptIdentity(credentials2.accessToken);
|
|
4181
4533
|
const models = await resolveProbeModels(port);
|
|
4182
4534
|
if (models.length === 0) {
|
|
4183
4535
|
throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
|
|
@@ -4210,7 +4562,7 @@ async function getOpenAiUsage(port) {
|
|
|
4210
4562
|
"no_usable_window"
|
|
4211
4563
|
);
|
|
4212
4564
|
}
|
|
4213
|
-
return usage;
|
|
4565
|
+
return { ...usage, subscription };
|
|
4214
4566
|
}
|
|
4215
4567
|
if (res.status === 401) {
|
|
4216
4568
|
throw new OpenAiUsageError(
|
|
@@ -4325,8 +4677,8 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
4325
4677
|
}
|
|
4326
4678
|
|
|
4327
4679
|
// src/lib/resource-usage.ts
|
|
4328
|
-
import { cpus, totalmem, freemem } from "os";
|
|
4329
|
-
import { statfsSync as statfsSync2 } from "fs";
|
|
4680
|
+
import { cpus, totalmem, freemem } from "node:os";
|
|
4681
|
+
import { statfsSync as statfsSync2 } from "node:fs";
|
|
4330
4682
|
|
|
4331
4683
|
// src/lib/ecs-task-metadata.ts
|
|
4332
4684
|
var ECS_METADATA_TIMEOUT_MS = 2e3;
|
|
@@ -4411,58 +4763,97 @@ function readDisk(homeDir) {
|
|
|
4411
4763
|
};
|
|
4412
4764
|
}
|
|
4413
4765
|
}
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4766
|
+
var CPU_PEAK_WINDOW_MS = 6e4;
|
|
4767
|
+
var CPU_PEAK_WINDOW_SAMPLE_COUNT = 4;
|
|
4768
|
+
var CPU_PEAK_SAMPLE_INTERVAL_MS = CPU_PEAK_WINDOW_MS / CPU_PEAK_WINDOW_SAMPLE_COUNT;
|
|
4769
|
+
function createCpuPeakSampler() {
|
|
4770
|
+
const sampleHistory = new Array(CPU_PEAK_WINDOW_SAMPLE_COUNT);
|
|
4771
|
+
sampleHistory[0] = readCpuSample();
|
|
4772
|
+
let nextSampleIndex = 1;
|
|
4773
|
+
let sampleCount = 1;
|
|
4774
|
+
let peak = null;
|
|
4775
|
+
const timer = setInterval(() => {
|
|
4417
4776
|
const current = readCpuSample();
|
|
4418
|
-
const
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
const warnings = [];
|
|
4425
|
-
if (disk.warning) warnings.push(disk.warning);
|
|
4426
|
-
if (ecsWarning) warnings.push(ecsWarning);
|
|
4427
|
-
let cpuPercent = hostCpuPercent;
|
|
4428
|
-
let cpuCount = hostCpuCount;
|
|
4429
|
-
let memoryTotalBytes = totalmem();
|
|
4430
|
-
let memoryAvailableBytes = freemem();
|
|
4431
|
-
if (limits !== null) {
|
|
4432
|
-
cpuCount = limits.cpuCount;
|
|
4433
|
-
memoryTotalBytes = limits.memoryTotalBytes;
|
|
4434
|
-
memoryAvailableBytes = clamp(
|
|
4435
|
-
limits.memoryTotalBytes - (totalmem() - freemem()),
|
|
4436
|
-
0,
|
|
4437
|
-
limits.memoryTotalBytes
|
|
4438
|
-
);
|
|
4439
|
-
cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
|
|
4777
|
+
const sampleFromWindowAgo = sampleCount === CPU_PEAK_WINDOW_SAMPLE_COUNT ? sampleHistory[nextSampleIndex] : void 0;
|
|
4778
|
+
if (sampleFromWindowAgo !== void 0) {
|
|
4779
|
+
const percentage = cpuPercentBetween(sampleFromWindowAgo, current);
|
|
4780
|
+
if (percentage !== null) {
|
|
4781
|
+
peak = peak === null ? percentage : Math.max(peak, percentage);
|
|
4782
|
+
}
|
|
4440
4783
|
}
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4784
|
+
sampleHistory[nextSampleIndex] = current;
|
|
4785
|
+
nextSampleIndex = (nextSampleIndex + 1) % CPU_PEAK_WINDOW_SAMPLE_COUNT;
|
|
4786
|
+
sampleCount = Math.min(sampleCount + 1, CPU_PEAK_WINDOW_SAMPLE_COUNT);
|
|
4787
|
+
}, CPU_PEAK_SAMPLE_INTERVAL_MS);
|
|
4788
|
+
return {
|
|
4789
|
+
takeAndReset: () => {
|
|
4790
|
+
const currentPeak = peak;
|
|
4791
|
+
peak = null;
|
|
4792
|
+
return currentPeak;
|
|
4793
|
+
},
|
|
4794
|
+
stop: () => clearInterval(timer)
|
|
4795
|
+
};
|
|
4796
|
+
}
|
|
4797
|
+
function createResourceUsageCollector(homeDir) {
|
|
4798
|
+
let previous = readCpuSample();
|
|
4799
|
+
const cpuPeakSampler = createCpuPeakSampler();
|
|
4800
|
+
return {
|
|
4801
|
+
collect: async () => {
|
|
4802
|
+
const current = readCpuSample();
|
|
4803
|
+
const hostCpuPercent = cpuPercentBetween(previous, current);
|
|
4804
|
+
const hostCpuPeakPercent = cpuPeakSampler.takeAndReset();
|
|
4805
|
+
const hostCpuCount = cpus().length;
|
|
4806
|
+
previous = current;
|
|
4807
|
+
const disk = readDisk(homeDir);
|
|
4808
|
+
const opencodeDbBytes = statSessionDbBytes(homeDir);
|
|
4809
|
+
const { limits, warning: ecsWarning } = await readEcsTaskLimits(process.env);
|
|
4810
|
+
const warnings = [];
|
|
4811
|
+
if (disk.warning) warnings.push(disk.warning);
|
|
4812
|
+
if (ecsWarning) warnings.push(ecsWarning);
|
|
4813
|
+
let cpuPercent = hostCpuPercent;
|
|
4814
|
+
let cpuPeakPercent = hostCpuPeakPercent;
|
|
4815
|
+
let cpuCount = hostCpuCount;
|
|
4816
|
+
let memoryTotalBytes = totalmem();
|
|
4817
|
+
let memoryAvailableBytes = freemem();
|
|
4818
|
+
if (limits !== null) {
|
|
4819
|
+
cpuCount = limits.cpuCount;
|
|
4820
|
+
memoryTotalBytes = limits.memoryTotalBytes;
|
|
4821
|
+
memoryAvailableBytes = clamp(
|
|
4822
|
+
limits.memoryTotalBytes - (totalmem() - freemem()),
|
|
4823
|
+
0,
|
|
4824
|
+
limits.memoryTotalBytes
|
|
4825
|
+
);
|
|
4826
|
+
cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
|
|
4827
|
+
cpuPeakPercent = hostCpuPeakPercent === null ? null : clamp(round2(hostCpuPeakPercent * hostCpuCount / limits.cpuCount), 0, 100);
|
|
4828
|
+
}
|
|
4829
|
+
return {
|
|
4830
|
+
usage: {
|
|
4831
|
+
cpuPercent,
|
|
4832
|
+
cpuPeakPercent,
|
|
4833
|
+
cpuCount,
|
|
4834
|
+
memoryTotalBytes,
|
|
4835
|
+
memoryAvailableBytes,
|
|
4836
|
+
diskTotalBytes: disk.totalBytes,
|
|
4837
|
+
diskFreeBytes: disk.freeBytes,
|
|
4838
|
+
opencodeDbBytes
|
|
4839
|
+
},
|
|
4840
|
+
warnings
|
|
4841
|
+
};
|
|
4842
|
+
},
|
|
4843
|
+
stop: cpuPeakSampler.stop
|
|
4453
4844
|
};
|
|
4454
4845
|
}
|
|
4455
4846
|
|
|
4456
4847
|
// src/lib/channels/driver.ts
|
|
4457
|
-
import { homedir as
|
|
4848
|
+
import { homedir as homedir5 } from "node:os";
|
|
4458
4849
|
|
|
4459
4850
|
// src/lib/runner-file-sync.ts
|
|
4460
|
-
import { join as
|
|
4851
|
+
import { join as join8 } from "node:path";
|
|
4461
4852
|
|
|
4462
4853
|
// src/lib/file-push.ts
|
|
4463
|
-
import { randomUUID } from "crypto";
|
|
4464
|
-
import { chmod, mkdir, open as
|
|
4465
|
-
import { basename, dirname as dirname5, isAbsolute, join as
|
|
4854
|
+
import { randomUUID } from "node:crypto";
|
|
4855
|
+
import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
|
|
4856
|
+
import { basename, dirname as dirname5, isAbsolute, join as join7, relative, resolve as resolve2, sep } from "node:path";
|
|
4466
4857
|
var FILE_MODE = 384;
|
|
4467
4858
|
var DIRECTORY_MODE = 448;
|
|
4468
4859
|
async function writePushedFile(request) {
|
|
@@ -4495,7 +4886,7 @@ async function writePushedFile(request) {
|
|
|
4495
4886
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
4496
4887
|
dirname5(candidate)
|
|
4497
4888
|
);
|
|
4498
|
-
const realTarget =
|
|
4889
|
+
const realTarget = join7(existingAncestor, ...missingSegments, basename(candidate));
|
|
4499
4890
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
4500
4891
|
if (allowedDirectory === null) {
|
|
4501
4892
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -4531,7 +4922,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
4531
4922
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
4532
4923
|
return null;
|
|
4533
4924
|
}
|
|
4534
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4925
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4535
4926
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
4536
4927
|
return null;
|
|
4537
4928
|
}
|
|
@@ -4604,16 +4995,16 @@ function contains(realDirectory, realTarget) {
|
|
|
4604
4995
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
4605
4996
|
let current = existingAncestor;
|
|
4606
4997
|
for (const segment of missingSegments) {
|
|
4607
|
-
current =
|
|
4998
|
+
current = join7(current, segment);
|
|
4608
4999
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
4609
5000
|
await chmod(current, DIRECTORY_MODE);
|
|
4610
5001
|
}
|
|
4611
5002
|
}
|
|
4612
5003
|
async function writeAtomically(realTarget, content) {
|
|
4613
|
-
const temporaryPath =
|
|
5004
|
+
const temporaryPath = join7(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
4614
5005
|
let handle;
|
|
4615
5006
|
try {
|
|
4616
|
-
handle = await
|
|
5007
|
+
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
4617
5008
|
await handle.writeFile(content);
|
|
4618
5009
|
await handle.chmod(FILE_MODE);
|
|
4619
5010
|
await handle.close();
|
|
@@ -4740,12 +5131,12 @@ var NOT_APPLIED = {
|
|
|
4740
5131
|
opencodeAuthApplied: false
|
|
4741
5132
|
};
|
|
4742
5133
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
4743
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4744
|
-
return expanded ===
|
|
5134
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5135
|
+
return expanded === join8(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
4745
5136
|
}
|
|
4746
5137
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
4747
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4748
|
-
return expanded ===
|
|
5138
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5139
|
+
return expanded === join8(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
4749
5140
|
}
|
|
4750
5141
|
async function applyOne(options, file) {
|
|
4751
5142
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -4905,6 +5296,9 @@ var DEFAULT_RETRY_POLICY = {
|
|
|
4905
5296
|
baseDelayMs: 500,
|
|
4906
5297
|
maxDelayMs: 3e4
|
|
4907
5298
|
};
|
|
5299
|
+
var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
|
|
5300
|
+
var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
5301
|
+
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
4908
5302
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
4909
5303
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
4910
5304
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
@@ -5045,6 +5439,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5045
5439
|
* message; it is removed once its in-flight set empties.
|
|
5046
5440
|
*/
|
|
5047
5441
|
watchers = /* @__PURE__ */ new Map();
|
|
5442
|
+
sessionErrorStream = null;
|
|
5443
|
+
/**
|
|
5444
|
+
* Session-error failures currently being reported; entries are empty at rest
|
|
5445
|
+
* because each handoff deletes its id in `finally`.
|
|
5446
|
+
*/
|
|
5447
|
+
sessionErrorHandled = /* @__PURE__ */ new Set();
|
|
5448
|
+
/**
|
|
5449
|
+
* Session errors that arrived before their dispatch was registered. Bounded FIFO
|
|
5450
|
+
* with a short TTL so an unmatched session cannot retain an event indefinitely.
|
|
5451
|
+
*/
|
|
5452
|
+
bufferedSessionErrors = /* @__PURE__ */ new Map();
|
|
5048
5453
|
/**
|
|
5049
5454
|
* AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
|
|
5050
5455
|
* dispatched and are still in-flight. A message in this set is never
|
|
@@ -5292,7 +5697,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5292
5697
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
5293
5698
|
this.now = config.now ?? (() => Date.now());
|
|
5294
5699
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
5295
|
-
this.homeDir = config.homeDir ??
|
|
5700
|
+
this.homeDir = config.homeDir ?? homedir5();
|
|
5296
5701
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
5297
5702
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
5298
5703
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
@@ -5508,6 +5913,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5508
5913
|
*/
|
|
5509
5914
|
stop() {
|
|
5510
5915
|
this.stopped = true;
|
|
5916
|
+
this.sessionErrorStream?.abort.abort();
|
|
5917
|
+
this.sessionErrorStream = null;
|
|
5511
5918
|
}
|
|
5512
5919
|
/**
|
|
5513
5920
|
* The server clears this request when a new MicroVM identity is recorded, so a
|
|
@@ -5582,6 +5989,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5582
5989
|
*/
|
|
5583
5990
|
async processConversation(conv) {
|
|
5584
5991
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
5992
|
+
this.ensureSessionErrorStream();
|
|
5585
5993
|
const messages = await this.getPendingMessages(conv.id);
|
|
5586
5994
|
let dispatched = 0;
|
|
5587
5995
|
let skippedAlreadyDispatched = 0;
|
|
@@ -5654,7 +6062,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5654
6062
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
5655
6063
|
break;
|
|
5656
6064
|
}
|
|
5657
|
-
const
|
|
6065
|
+
const errorMessage3 = err instanceof Error ? err.message : String(err);
|
|
5658
6066
|
this.sessions.delete(conv.id);
|
|
5659
6067
|
this.supersede(conv.id, sessionId);
|
|
5660
6068
|
this.log({
|
|
@@ -5663,7 +6071,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5663
6071
|
conversation_id: conv.id,
|
|
5664
6072
|
message_id: message.id
|
|
5665
6073
|
});
|
|
5666
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6074
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5667
6075
|
this.log({
|
|
5668
6076
|
level: "warn",
|
|
5669
6077
|
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -5674,7 +6082,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5674
6082
|
});
|
|
5675
6083
|
this.log({
|
|
5676
6084
|
level: "error",
|
|
5677
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
6085
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
|
|
5678
6086
|
conversation_id: conv.id,
|
|
5679
6087
|
message_id: message.id
|
|
5680
6088
|
});
|
|
@@ -5695,14 +6103,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5695
6103
|
this.unconfirmedDispatchFailures.delete(message.id);
|
|
5696
6104
|
this.sessions.delete(conv.id);
|
|
5697
6105
|
this.supersede(conv.id, sessionId);
|
|
5698
|
-
const
|
|
6106
|
+
const errorMessage3 = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
|
|
5699
6107
|
this.log({
|
|
5700
6108
|
level: "error",
|
|
5701
|
-
message:
|
|
6109
|
+
message: errorMessage3,
|
|
5702
6110
|
conversation_id: conv.id,
|
|
5703
6111
|
message_id: message.id
|
|
5704
6112
|
});
|
|
5705
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6113
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5706
6114
|
this.log({
|
|
5707
6115
|
level: "warn",
|
|
5708
6116
|
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -6036,6 +6444,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6036
6444
|
});
|
|
6037
6445
|
await this.markFailed(conv.id, message.id, sessionId, error2, usage, failure);
|
|
6038
6446
|
}
|
|
6447
|
+
if (ocId !== null) {
|
|
6448
|
+
await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
|
|
6449
|
+
}
|
|
6039
6450
|
} catch (err) {
|
|
6040
6451
|
if (err instanceof ChannelAuthError) throw err;
|
|
6041
6452
|
this.log({
|
|
@@ -6575,6 +6986,24 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6575
6986
|
ambiguousPinnedSinceMs: 0,
|
|
6576
6987
|
ambiguousResolved: false
|
|
6577
6988
|
});
|
|
6989
|
+
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
6990
|
+
if (!buffered) return;
|
|
6991
|
+
this.bufferedSessionErrors.delete(sessionId);
|
|
6992
|
+
if (this.now() - buffered.receivedAt < SESSION_ERROR_BUFFER_TTL_MS) {
|
|
6993
|
+
this.handleSessionError(buffered.event);
|
|
6994
|
+
}
|
|
6995
|
+
}
|
|
6996
|
+
bufferSessionError(event) {
|
|
6997
|
+
this.bufferedSessionErrors.delete(event.sessionId);
|
|
6998
|
+
this.bufferedSessionErrors.set(event.sessionId, {
|
|
6999
|
+
event,
|
|
7000
|
+
receivedAt: this.now()
|
|
7001
|
+
});
|
|
7002
|
+
while (this.bufferedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
|
|
7003
|
+
const oldest = this.bufferedSessionErrors.keys().next().value;
|
|
7004
|
+
if (typeof oldest !== "string") break;
|
|
7005
|
+
this.bufferedSessionErrors.delete(oldest);
|
|
7006
|
+
}
|
|
6578
7007
|
}
|
|
6579
7008
|
/**
|
|
6580
7009
|
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
@@ -6779,6 +7208,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6779
7208
|
ensureWatcherRunning(sessionId) {
|
|
6780
7209
|
const watcher = this.watchers.get(sessionId);
|
|
6781
7210
|
if (!watcher) return;
|
|
7211
|
+
this.ensureSessionErrorStream();
|
|
6782
7212
|
if (watcher.loop) return;
|
|
6783
7213
|
if (watcher.inFlight.size === 0) {
|
|
6784
7214
|
this.watchers.delete(sessionId);
|
|
@@ -6794,6 +7224,154 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6794
7224
|
});
|
|
6795
7225
|
watcher.loop = loop;
|
|
6796
7226
|
}
|
|
7227
|
+
ensureSessionErrorStream() {
|
|
7228
|
+
if (this.sessionErrorStream || this.stopped) return;
|
|
7229
|
+
const abort = new AbortController();
|
|
7230
|
+
const loop = this.runSessionErrorStream(abort.signal);
|
|
7231
|
+
this.sessionErrorStream = { abort, loop };
|
|
7232
|
+
}
|
|
7233
|
+
async runSessionErrorStream(signal) {
|
|
7234
|
+
let attempt = 0;
|
|
7235
|
+
let warned = false;
|
|
7236
|
+
while (!this.stopped && !signal.aborted) {
|
|
7237
|
+
const openedAt = this.now();
|
|
7238
|
+
try {
|
|
7239
|
+
const outcome = await readSessionErrorStream(this.port, {
|
|
7240
|
+
signal,
|
|
7241
|
+
onSessionError: (event) => this.handleSessionError(event)
|
|
7242
|
+
});
|
|
7243
|
+
if (outcome.reason === "aborted" || signal.aborted) return;
|
|
7244
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
7245
|
+
if (outcome.reason === "unavailable" || outcome.reason === "ended") {
|
|
7246
|
+
if (!healthy) {
|
|
7247
|
+
const detail = outcome.reason === "unavailable" ? outcome.detail : "stream ended";
|
|
7248
|
+
this.log({
|
|
7249
|
+
level: warned ? "debug" : "warn",
|
|
7250
|
+
message: `OpenCode session-error stream ${warned ? "still unavailable" : "unavailable"} (${detail}); transcript polling remains the evidence path`
|
|
7251
|
+
});
|
|
7252
|
+
warned = true;
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7255
|
+
if (healthy) {
|
|
7256
|
+
if (warned) {
|
|
7257
|
+
this.log({
|
|
7258
|
+
level: "info",
|
|
7259
|
+
message: "OpenCode session-error stream reconnected; transcript polling remains the evidence path"
|
|
7260
|
+
});
|
|
7261
|
+
warned = false;
|
|
7262
|
+
}
|
|
7263
|
+
attempt = 0;
|
|
7264
|
+
} else {
|
|
7265
|
+
attempt += 1;
|
|
7266
|
+
}
|
|
7267
|
+
if (this.stopped || signal.aborted) return;
|
|
7268
|
+
await this.sleep(backoffDelay(healthy ? 0 : attempt - 1, this.retry));
|
|
7269
|
+
} catch (err) {
|
|
7270
|
+
if (this.stopped || signal.aborted) return;
|
|
7271
|
+
this.log({
|
|
7272
|
+
level: "error",
|
|
7273
|
+
message: `OpenCode session-error stream failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
|
|
7274
|
+
});
|
|
7275
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
7276
|
+
const delayAttempt = healthy ? 0 : attempt;
|
|
7277
|
+
attempt = healthy ? 0 : attempt + 1;
|
|
7278
|
+
try {
|
|
7279
|
+
await this.sleep(backoffDelay(delayAttempt, this.retry));
|
|
7280
|
+
} catch (sleepErr) {
|
|
7281
|
+
this.log({
|
|
7282
|
+
level: "error",
|
|
7283
|
+
message: `OpenCode session-error stream backoff failed unexpectedly: ${sleepErr instanceof Error ? sleepErr.message : String(sleepErr)}`
|
|
7284
|
+
});
|
|
7285
|
+
}
|
|
7286
|
+
}
|
|
7287
|
+
}
|
|
7288
|
+
}
|
|
7289
|
+
handleSessionError(event) {
|
|
7290
|
+
try {
|
|
7291
|
+
const watcher = this.watchers.get(event.sessionId);
|
|
7292
|
+
if (!watcher) {
|
|
7293
|
+
this.bufferSessionError(event);
|
|
7294
|
+
this.log({
|
|
7295
|
+
level: "debug",
|
|
7296
|
+
message: `Ignoring session error for unknown session ${event.sessionId.slice(0, 8)}`
|
|
7297
|
+
});
|
|
7298
|
+
return;
|
|
7299
|
+
}
|
|
7300
|
+
if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
|
|
7301
|
+
this.log({
|
|
7302
|
+
level: "debug",
|
|
7303
|
+
message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
|
|
7304
|
+
conversation_id: watcher.conv.id
|
|
7305
|
+
});
|
|
7306
|
+
return;
|
|
7307
|
+
}
|
|
7308
|
+
const inFlight = [...watcher.inFlight.values()].filter((message) => !message.started && !message.done).sort((a, b) => a.dispatchedAt - b.dispatchedAt)[0];
|
|
7309
|
+
if (!inFlight) {
|
|
7310
|
+
this.bufferSessionError(event);
|
|
7311
|
+
this.log({
|
|
7312
|
+
level: "debug",
|
|
7313
|
+
message: `No queued in-flight turn to correlate with session error in ${event.sessionId.slice(0, 8)}`,
|
|
7314
|
+
conversation_id: watcher.conv.id
|
|
7315
|
+
});
|
|
7316
|
+
return;
|
|
7317
|
+
}
|
|
7318
|
+
if (this.sessionErrorHandled.has(inFlight.evidentMessageId)) return;
|
|
7319
|
+
this.sessionErrorHandled.add(inFlight.evidentMessageId);
|
|
7320
|
+
void this.failFromSessionError(watcher, event, inFlight);
|
|
7321
|
+
} catch (err) {
|
|
7322
|
+
this.log({
|
|
7323
|
+
level: "error",
|
|
7324
|
+
message: `Failed to handle OpenCode session error for ${event.sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`
|
|
7325
|
+
});
|
|
7326
|
+
}
|
|
7327
|
+
}
|
|
7328
|
+
async failFromSessionError(watcher, event, inFlight) {
|
|
7329
|
+
try {
|
|
7330
|
+
const messages = await getSessionMessages(this.port, event.sessionId);
|
|
7331
|
+
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7332
|
+
if (state !== "queued") {
|
|
7333
|
+
this.log({
|
|
7334
|
+
level: "debug",
|
|
7335
|
+
message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
|
|
7336
|
+
conversation_id: watcher.conv.id,
|
|
7337
|
+
message_id: inFlight.evidentMessageId
|
|
7338
|
+
});
|
|
7339
|
+
return;
|
|
7340
|
+
}
|
|
7341
|
+
this.log({
|
|
7342
|
+
level: "error",
|
|
7343
|
+
message: `OpenCode could not run message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)}: ${event.reason}`,
|
|
7344
|
+
conversation_id: watcher.conv.id,
|
|
7345
|
+
message_id: inFlight.evidentMessageId
|
|
7346
|
+
});
|
|
7347
|
+
await this.markFailed(
|
|
7348
|
+
watcher.conv.id,
|
|
7349
|
+
inFlight.evidentMessageId,
|
|
7350
|
+
event.sessionId,
|
|
7351
|
+
`OpenCode could not run this turn: ${event.reason}`
|
|
7352
|
+
);
|
|
7353
|
+
inFlight.done = true;
|
|
7354
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7355
|
+
} catch (err) {
|
|
7356
|
+
if (err instanceof ChannelAuthError) {
|
|
7357
|
+
this.log({
|
|
7358
|
+
level: "warn",
|
|
7359
|
+
message: `OpenCode session error could not mark message ${inFlight.evidentMessageId.slice(0, 8)} failed because authentication failed: ${err.message}; leaving it to transcript polling / the existing give-up path`,
|
|
7360
|
+
conversation_id: watcher.conv.id,
|
|
7361
|
+
message_id: inFlight.evidentMessageId
|
|
7362
|
+
});
|
|
7363
|
+
} else {
|
|
7364
|
+
this.log({
|
|
7365
|
+
level: "warn",
|
|
7366
|
+
message: `OpenCode session error could not mark message ${inFlight.evidentMessageId.slice(0, 8)} failed: ${err instanceof Error ? err.message : String(err)}; leaving it to transcript polling / the existing give-up path`,
|
|
7367
|
+
conversation_id: watcher.conv.id,
|
|
7368
|
+
message_id: inFlight.evidentMessageId
|
|
7369
|
+
});
|
|
7370
|
+
}
|
|
7371
|
+
} finally {
|
|
7372
|
+
this.sessionErrorHandled.delete(inFlight.evidentMessageId);
|
|
7373
|
+
}
|
|
7374
|
+
}
|
|
6797
7375
|
/**
|
|
6798
7376
|
* The per-session polling loop (WI-3). Once per tick it:
|
|
6799
7377
|
* 1. polls `GET /session/:id/message` once and, per in-flight message,
|
|
@@ -7000,6 +7578,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7000
7578
|
return;
|
|
7001
7579
|
}
|
|
7002
7580
|
inFlight.done = true;
|
|
7581
|
+
await this.reportSubagentAuthFailures(
|
|
7582
|
+
watcher.conv.id,
|
|
7583
|
+
inFlight.opencodeMessageId,
|
|
7584
|
+
inFlight.evidentMessageId,
|
|
7585
|
+
messages
|
|
7586
|
+
);
|
|
7003
7587
|
}
|
|
7004
7588
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7005
7589
|
return;
|
|
@@ -7242,6 +7826,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7242
7826
|
return;
|
|
7243
7827
|
}
|
|
7244
7828
|
inFlight.done = true;
|
|
7829
|
+
await this.reportSubagentAuthFailures(
|
|
7830
|
+
watcher.conv.id,
|
|
7831
|
+
inFlight.opencodeMessageId,
|
|
7832
|
+
inFlight.evidentMessageId,
|
|
7833
|
+
messages
|
|
7834
|
+
);
|
|
7245
7835
|
}
|
|
7246
7836
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7247
7837
|
}
|
|
@@ -7412,6 +8002,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7412
8002
|
});
|
|
7413
8003
|
return;
|
|
7414
8004
|
}
|
|
8005
|
+
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
7415
8006
|
this.dontRedispatch.delete(row.id);
|
|
7416
8007
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
7417
8008
|
return;
|
|
@@ -7573,6 +8164,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7573
8164
|
});
|
|
7574
8165
|
return;
|
|
7575
8166
|
}
|
|
8167
|
+
if (ocId !== null) {
|
|
8168
|
+
await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
|
|
8169
|
+
}
|
|
7576
8170
|
this.dontRedispatch.delete(row.id);
|
|
7577
8171
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
7578
8172
|
}
|
|
@@ -7666,14 +8260,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7666
8260
|
this.unconfirmedDispatchFailures.delete(row.id);
|
|
7667
8261
|
this.sessions.delete(readoptConv.id);
|
|
7668
8262
|
this.supersede(readoptConv.id, sessionId);
|
|
7669
|
-
const
|
|
8263
|
+
const errorMessage3 = `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
|
|
7670
8264
|
this.log({
|
|
7671
8265
|
level: "error",
|
|
7672
|
-
message:
|
|
8266
|
+
message: errorMessage3,
|
|
7673
8267
|
conversation_id: row.conversation_id,
|
|
7674
8268
|
message_id: row.id
|
|
7675
8269
|
});
|
|
7676
|
-
await this.markFailed(row.conversation_id, row.id, null,
|
|
8270
|
+
await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
|
|
7677
8271
|
this.log({
|
|
7678
8272
|
level: "warn",
|
|
7679
8273
|
message: `markFailed PATCH for message ${row.id.slice(0, 8)} (conversation ${row.conversation_id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -8524,6 +9118,111 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8524
9118
|
reply?.info?.modelID ?? null
|
|
8525
9119
|
);
|
|
8526
9120
|
}
|
|
9121
|
+
async recordSubagentModelAuthFailure(failure, conversationId, messageId) {
|
|
9122
|
+
const providerId = failure.providerId ?? "(unknown)";
|
|
9123
|
+
try {
|
|
9124
|
+
const res = await this.fetchImpl(
|
|
9125
|
+
`${this.apiUrl}/runners/${this.agentId}/model-auth-failures`,
|
|
9126
|
+
{
|
|
9127
|
+
method: "POST",
|
|
9128
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
9129
|
+
body: JSON.stringify({
|
|
9130
|
+
provider_id: failure.providerId,
|
|
9131
|
+
model_id: failure.modelId,
|
|
9132
|
+
reason: failure.reason
|
|
9133
|
+
})
|
|
9134
|
+
}
|
|
9135
|
+
);
|
|
9136
|
+
if (!res.ok) {
|
|
9137
|
+
this.log({
|
|
9138
|
+
level: "warn",
|
|
9139
|
+
message: `Sub-agent model-auth report for provider ${providerId} (${failure.reason}) returned HTTP ${res.status} (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)})`,
|
|
9140
|
+
conversation_id: conversationId,
|
|
9141
|
+
message_id: messageId
|
|
9142
|
+
});
|
|
9143
|
+
}
|
|
9144
|
+
} catch (err) {
|
|
9145
|
+
this.log({
|
|
9146
|
+
level: "warn",
|
|
9147
|
+
message: `Sub-agent model-auth report for provider ${providerId} (${failure.reason}) failed with status network-error (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)}): ${err instanceof Error ? err.message : String(err)}`,
|
|
9148
|
+
conversation_id: conversationId,
|
|
9149
|
+
message_id: messageId
|
|
9150
|
+
});
|
|
9151
|
+
}
|
|
9152
|
+
}
|
|
9153
|
+
async clearSubagentModelAuthFailure(providerId, conversationId, messageId) {
|
|
9154
|
+
try {
|
|
9155
|
+
const res = await this.fetchImpl(
|
|
9156
|
+
`${this.apiUrl}/runners/${this.agentId}/model-auth-failures/${encodeURIComponent(providerId)}`,
|
|
9157
|
+
{
|
|
9158
|
+
method: "DELETE",
|
|
9159
|
+
headers: { Authorization: this.getAuthHeader() }
|
|
9160
|
+
}
|
|
9161
|
+
);
|
|
9162
|
+
if (!res.ok) {
|
|
9163
|
+
this.log({
|
|
9164
|
+
level: "warn",
|
|
9165
|
+
message: `Sub-agent model-auth clear for provider ${providerId} returned HTTP ${res.status} (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)})`,
|
|
9166
|
+
conversation_id: conversationId,
|
|
9167
|
+
message_id: messageId
|
|
9168
|
+
});
|
|
9169
|
+
}
|
|
9170
|
+
} catch (err) {
|
|
9171
|
+
this.log({
|
|
9172
|
+
level: "warn",
|
|
9173
|
+
message: `Sub-agent model-auth clear for provider ${providerId} failed with status network-error (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)}): ${err instanceof Error ? err.message : String(err)}`,
|
|
9174
|
+
conversation_id: conversationId,
|
|
9175
|
+
message_id: messageId
|
|
9176
|
+
});
|
|
9177
|
+
}
|
|
9178
|
+
}
|
|
9179
|
+
async reportSubagentAuthFailures(conversationId, opencodeMessageId, evidentMessageId, messages) {
|
|
9180
|
+
const refs = collectSubagentSessions(messages, opencodeMessageId);
|
|
9181
|
+
if (refs.length === 0) return;
|
|
9182
|
+
const failedProviders = /* @__PURE__ */ new Map();
|
|
9183
|
+
const succeededProviders = /* @__PURE__ */ new Set();
|
|
9184
|
+
for (const ref of refs) {
|
|
9185
|
+
try {
|
|
9186
|
+
const childMessages = await getSessionMessages(this.port, ref.sessionId);
|
|
9187
|
+
if (childMessages === null) {
|
|
9188
|
+
this.log({
|
|
9189
|
+
level: "debug",
|
|
9190
|
+
message: `Could not read sub-agent session ${ref.sessionId} while checking credential failures \u2014 skipping it`,
|
|
9191
|
+
conversation_id: conversationId,
|
|
9192
|
+
message_id: evidentMessageId
|
|
9193
|
+
});
|
|
9194
|
+
continue;
|
|
9195
|
+
}
|
|
9196
|
+
const outcome = findSubagentAuthOutcome(childMessages, ref.startedAtMs);
|
|
9197
|
+
if (!outcome) continue;
|
|
9198
|
+
if (outcome.outcome === "failed") {
|
|
9199
|
+
failedProviders.set(outcome.providerId, outcome.failure);
|
|
9200
|
+
} else {
|
|
9201
|
+
succeededProviders.add(outcome.providerId);
|
|
9202
|
+
}
|
|
9203
|
+
} catch (err) {
|
|
9204
|
+
this.log({
|
|
9205
|
+
level: "warn",
|
|
9206
|
+
message: `Failed to inspect sub-agent session ${ref.sessionId} for credential failures (conversation ${conversationId.slice(0, 8)}, message ${evidentMessageId.slice(0, 8)}): ${err instanceof Error ? err.message : String(err)}`,
|
|
9207
|
+
conversation_id: conversationId,
|
|
9208
|
+
message_id: evidentMessageId
|
|
9209
|
+
});
|
|
9210
|
+
}
|
|
9211
|
+
}
|
|
9212
|
+
for (const [providerId, failure] of failedProviders) {
|
|
9213
|
+
this.log({
|
|
9214
|
+
level: "warn",
|
|
9215
|
+
message: `Sub-agent turn failed on provider ${providerId} (${failure.reason}) \u2014 recording credential evidence`,
|
|
9216
|
+
conversation_id: conversationId,
|
|
9217
|
+
message_id: evidentMessageId
|
|
9218
|
+
});
|
|
9219
|
+
await this.recordSubagentModelAuthFailure(failure, conversationId, evidentMessageId);
|
|
9220
|
+
}
|
|
9221
|
+
for (const providerId of succeededProviders) {
|
|
9222
|
+
if (failedProviders.has(providerId)) continue;
|
|
9223
|
+
await this.clearSubagentModelAuthFailure(providerId, conversationId, evidentMessageId);
|
|
9224
|
+
}
|
|
9225
|
+
}
|
|
8527
9226
|
/**
|
|
8528
9227
|
* Best-effort, SINGLE-ATTEMPT runner→server telemetry ping
|
|
8529
9228
|
* (queued-followup-redrive). `POST .../messages/:id/signal {signal, ...extra}`
|
|
@@ -8677,6 +9376,13 @@ import chalk5 from "chalk";
|
|
|
8677
9376
|
import ora2 from "ora";
|
|
8678
9377
|
import { select as select2 } from "@inquirer/prompts";
|
|
8679
9378
|
var INTERACTIVE_START_TIMEOUT_MS = 3e4;
|
|
9379
|
+
function checkNonInteractivePortConflict(port, isPortInUseFn) {
|
|
9380
|
+
if (isPortInUseFn(port)) {
|
|
9381
|
+
throw new Error(
|
|
9382
|
+
`Port ${port} is already in use by a non-OpenCode process. Free it or pass --port.`
|
|
9383
|
+
);
|
|
9384
|
+
}
|
|
9385
|
+
}
|
|
8680
9386
|
async function ensureOpenCodeRunning(ctx) {
|
|
8681
9387
|
const healthCheck = await checkOpenCodeHealth(ctx.port);
|
|
8682
9388
|
if (healthCheck.healthy) {
|
|
@@ -8724,6 +9430,7 @@ async function ensureOpenCodeRunning(ctx) {
|
|
|
8724
9430
|
}
|
|
8725
9431
|
}
|
|
8726
9432
|
if (!ctx.interactive) {
|
|
9433
|
+
checkNonInteractivePortConflict(ctx.port, isPortInUse);
|
|
8727
9434
|
ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
|
|
8728
9435
|
const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
|
|
8729
9436
|
const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
|
|
@@ -8805,9 +9512,119 @@ Port ${port} is already in use.`));
|
|
|
8805
9512
|
return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
|
|
8806
9513
|
}
|
|
8807
9514
|
|
|
9515
|
+
// src/commands/ensure-opencode-v2.ts
|
|
9516
|
+
import chalk6 from "chalk";
|
|
9517
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
9518
|
+
async function probeOpenCode2WithoutPassword(port) {
|
|
9519
|
+
try {
|
|
9520
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
9521
|
+
signal: AbortSignal.timeout(2e3)
|
|
9522
|
+
});
|
|
9523
|
+
if (response.status === 401) {
|
|
9524
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
9525
|
+
}
|
|
9526
|
+
if (!response.ok) {
|
|
9527
|
+
return { healthy: false, error: `HTTP ${response.status}` };
|
|
9528
|
+
}
|
|
9529
|
+
return { healthy: true };
|
|
9530
|
+
} catch (error2) {
|
|
9531
|
+
return {
|
|
9532
|
+
healthy: false,
|
|
9533
|
+
error: error2 instanceof Error ? error2.message : "Unknown error"
|
|
9534
|
+
};
|
|
9535
|
+
}
|
|
9536
|
+
}
|
|
9537
|
+
function unknownPasswordError(port) {
|
|
9538
|
+
return new Error(
|
|
9539
|
+
`OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
|
|
9540
|
+
);
|
|
9541
|
+
}
|
|
9542
|
+
function v2SessionSupportIncompleteError() {
|
|
9543
|
+
return new Error(
|
|
9544
|
+
"OpenCode V2 (opencode2) session support is incomplete \u2014 see https://github.com/sroze/evident/issues/2075. Not starting a new opencode2 process."
|
|
9545
|
+
);
|
|
9546
|
+
}
|
|
9547
|
+
async function ensureOpenCode2Running(ctx) {
|
|
9548
|
+
const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
|
|
9549
|
+
if (initialHealth.authFailed) {
|
|
9550
|
+
throw unknownPasswordError(ctx.port);
|
|
9551
|
+
}
|
|
9552
|
+
if (initialHealth.healthy) {
|
|
9553
|
+
return {
|
|
9554
|
+
port: ctx.port,
|
|
9555
|
+
process: null,
|
|
9556
|
+
version: null,
|
|
9557
|
+
notReadyReason: null,
|
|
9558
|
+
password: null
|
|
9559
|
+
};
|
|
9560
|
+
}
|
|
9561
|
+
if (!isOpenCode2Installed()) {
|
|
9562
|
+
throw new Error(
|
|
9563
|
+
"OpenCode V2 (opencode2) is not installed. Install it with: npm install -g @opencode-ai/cli@beta"
|
|
9564
|
+
);
|
|
9565
|
+
}
|
|
9566
|
+
let port = ctx.port;
|
|
9567
|
+
if (!ctx.interactive) {
|
|
9568
|
+
checkNonInteractivePortConflict(port, isPortInUse);
|
|
9569
|
+
} else if (isPortInUse(port)) {
|
|
9570
|
+
console.log(chalk6.yellow(`
|
|
9571
|
+
Port ${port} is already in use.`));
|
|
9572
|
+
const alternativePort = findAvailablePort(port + 1);
|
|
9573
|
+
if (alternativePort) {
|
|
9574
|
+
const useAlternative = await select3({
|
|
9575
|
+
message: `Use port ${alternativePort} instead?`,
|
|
9576
|
+
choices: [
|
|
9577
|
+
{ name: `Yes, use port ${alternativePort}`, value: "yes" },
|
|
9578
|
+
{ name: "No, I will free the port manually", value: "no" }
|
|
9579
|
+
]
|
|
9580
|
+
});
|
|
9581
|
+
if (useAlternative === "yes") {
|
|
9582
|
+
port = alternativePort;
|
|
9583
|
+
} else {
|
|
9584
|
+
throw new Error(`Port ${ctx.port} is in use`);
|
|
9585
|
+
}
|
|
9586
|
+
}
|
|
9587
|
+
}
|
|
9588
|
+
if (!ctx.interactive) {
|
|
9589
|
+
throw v2SessionSupportIncompleteError();
|
|
9590
|
+
}
|
|
9591
|
+
console.log(chalk6.yellow(`
|
|
9592
|
+
${v2SessionSupportIncompleteError().message}`));
|
|
9593
|
+
const action = await select3({
|
|
9594
|
+
message: "OpenCode V2 is not running. What would you like to do?",
|
|
9595
|
+
choices: [
|
|
9596
|
+
{
|
|
9597
|
+
name: "Show me the command",
|
|
9598
|
+
value: "manual",
|
|
9599
|
+
description: "Display the command to run manually"
|
|
9600
|
+
},
|
|
9601
|
+
{
|
|
9602
|
+
name: "Continue without OpenCode V2",
|
|
9603
|
+
value: "continue",
|
|
9604
|
+
description: "Requests will fail until OpenCode V2 starts"
|
|
9605
|
+
}
|
|
9606
|
+
]
|
|
9607
|
+
});
|
|
9608
|
+
if (action === "manual") {
|
|
9609
|
+
blank();
|
|
9610
|
+
console.log(chalk6.bold("Run this command in another terminal:"));
|
|
9611
|
+
blank();
|
|
9612
|
+
console.log(` ${chalk6.cyan(`opencode2 serve --port ${port}`)}`);
|
|
9613
|
+
blank();
|
|
9614
|
+
throw new Error("Please start OpenCode V2 manually");
|
|
9615
|
+
}
|
|
9616
|
+
return {
|
|
9617
|
+
port,
|
|
9618
|
+
process: null,
|
|
9619
|
+
version: null,
|
|
9620
|
+
notReadyReason: "you chose to continue without OpenCode V2",
|
|
9621
|
+
password: null
|
|
9622
|
+
};
|
|
9623
|
+
}
|
|
9624
|
+
|
|
8808
9625
|
// src/lib/runner-credentials.ts
|
|
8809
|
-
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
8810
|
-
import { spawn as spawn5 } from "child_process";
|
|
9626
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
9627
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
8811
9628
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
8812
9629
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
8813
9630
|
var GITHUB_PROBE_TIMEOUT_MS = 1e4;
|
|
@@ -9079,11 +9896,11 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
9079
9896
|
}
|
|
9080
9897
|
|
|
9081
9898
|
// src/lib/opencode/config-overlay.ts
|
|
9082
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
9083
|
-
import { copyFileSync, existsSync as existsSync2, statSync as
|
|
9084
|
-
import { isAbsolute as isAbsolute2, join as
|
|
9899
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
9900
|
+
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
9901
|
+
import { isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "node:path";
|
|
9085
9902
|
function isFile(filePath) {
|
|
9086
|
-
return existsSync2(filePath) &&
|
|
9903
|
+
return existsSync2(filePath) && statSync6(filePath).isFile();
|
|
9087
9904
|
}
|
|
9088
9905
|
function applyRunnerOpenCodeConfig({
|
|
9089
9906
|
overlayPath,
|
|
@@ -9095,7 +9912,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9095
9912
|
return;
|
|
9096
9913
|
}
|
|
9097
9914
|
const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
|
|
9098
|
-
const target = isFile(
|
|
9915
|
+
const target = isFile(join9(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
9099
9916
|
if (!isFile(source)) {
|
|
9100
9917
|
log3(
|
|
9101
9918
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -9103,7 +9920,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9103
9920
|
);
|
|
9104
9921
|
return;
|
|
9105
9922
|
}
|
|
9106
|
-
copyFileSync(source,
|
|
9923
|
+
copyFileSync(source, join9(cwd, target));
|
|
9107
9924
|
try {
|
|
9108
9925
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
9109
9926
|
stdio: "ignore"
|
|
@@ -9112,11 +9929,11 @@ function applyRunnerOpenCodeConfig({
|
|
|
9112
9929
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
9113
9930
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
9114
9931
|
}
|
|
9115
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
9932
|
+
log3(`Applied runner OpenCode config ${source} to ${join9(cwd, target)}`);
|
|
9116
9933
|
}
|
|
9117
9934
|
|
|
9118
9935
|
// src/lib/credential-sync.ts
|
|
9119
|
-
import { renameSync, writeFileSync as writeFileSync5 } from "fs";
|
|
9936
|
+
import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
9120
9937
|
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
9121
9938
|
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
9122
9939
|
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
@@ -9126,7 +9943,7 @@ var MAX_FLUSH_PASSES = 2;
|
|
|
9126
9943
|
function outcomesWith(outcome) {
|
|
9127
9944
|
return { claude: outcome, opencode: outcome };
|
|
9128
9945
|
}
|
|
9129
|
-
function
|
|
9946
|
+
function errorMessage2(error2) {
|
|
9130
9947
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
9131
9948
|
}
|
|
9132
9949
|
function waitForSettlement(promise, timeoutMs) {
|
|
@@ -9153,7 +9970,7 @@ function writeMarker(markerPath, outcomes, log3) {
|
|
|
9153
9970
|
writeFileSync5(temporaryPath, body, { mode: 384 });
|
|
9154
9971
|
renameSync(temporaryPath, markerPath);
|
|
9155
9972
|
} catch (error2) {
|
|
9156
|
-
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${
|
|
9973
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
|
|
9157
9974
|
}
|
|
9158
9975
|
}
|
|
9159
9976
|
function intervalSeconds(env, log3) {
|
|
@@ -9185,7 +10002,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
|
9185
10002
|
},
|
|
9186
10003
|
(error2) => {
|
|
9187
10004
|
failed = true;
|
|
9188
|
-
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${
|
|
10005
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
|
|
9189
10006
|
}
|
|
9190
10007
|
);
|
|
9191
10008
|
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
@@ -9245,7 +10062,7 @@ function createCredentialSync({
|
|
|
9245
10062
|
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
9246
10063
|
} catch (error2) {
|
|
9247
10064
|
outcomes[store] = "failed";
|
|
9248
|
-
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${
|
|
10065
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
|
|
9249
10066
|
}
|
|
9250
10067
|
}
|
|
9251
10068
|
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
@@ -9387,7 +10204,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9387
10204
|
if (trimmed === "") {
|
|
9388
10205
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
9389
10206
|
}
|
|
9390
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
10207
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join10(homeDir, trimmed.slice(2)) : trimmed;
|
|
9391
10208
|
if (!isAbsolute3(expanded)) {
|
|
9392
10209
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
9393
10210
|
}
|
|
@@ -9411,6 +10228,28 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9411
10228
|
var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
|
|
9412
10229
|
var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
|
|
9413
10230
|
var OPENCODE_START_TIMEOUT_ENV = "EVIDENT_OPENCODE_START_TIMEOUT";
|
|
10231
|
+
var OPENCODE_VERSION_ENV = "EVIDENT_OPENCODE_VERSION";
|
|
10232
|
+
function resolveOpenCodeVersion(options, env = process.env) {
|
|
10233
|
+
let raw;
|
|
10234
|
+
let source;
|
|
10235
|
+
if (options.opencodeVersion !== void 0) {
|
|
10236
|
+
raw = options.opencodeVersion;
|
|
10237
|
+
source = "--opencode-version";
|
|
10238
|
+
} else if (env[OPENCODE_VERSION_ENV] !== void 0 && env[OPENCODE_VERSION_ENV] !== "") {
|
|
10239
|
+
raw = env[OPENCODE_VERSION_ENV];
|
|
10240
|
+
source = OPENCODE_VERSION_ENV;
|
|
10241
|
+
} else {
|
|
10242
|
+
return { version: "v1", warnings: [] };
|
|
10243
|
+
}
|
|
10244
|
+
const normalized = raw.trim().toLowerCase();
|
|
10245
|
+
if (normalized !== "v1" && normalized !== "v2") {
|
|
10246
|
+
return {
|
|
10247
|
+
version: "v1",
|
|
10248
|
+
warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or v2; using the default v1`]
|
|
10249
|
+
};
|
|
10250
|
+
}
|
|
10251
|
+
return { version: normalized, warnings: [] };
|
|
10252
|
+
}
|
|
9414
10253
|
function resolveOpenCodeStartTimeoutMs(options, env = process.env) {
|
|
9415
10254
|
const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1e3;
|
|
9416
10255
|
let raw;
|
|
@@ -9477,7 +10316,7 @@ function log2(state, message, level = "info") {
|
|
|
9477
10316
|
})
|
|
9478
10317
|
);
|
|
9479
10318
|
} else if (!state.interactive) {
|
|
9480
|
-
const prefix = level === "error" ?
|
|
10319
|
+
const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
|
|
9481
10320
|
console.log(`${prefix} ${message}`);
|
|
9482
10321
|
}
|
|
9483
10322
|
}
|
|
@@ -9507,7 +10346,7 @@ function logActivity(state, entry) {
|
|
|
9507
10346
|
}
|
|
9508
10347
|
function reportSessionDbRecovery(state) {
|
|
9509
10348
|
try {
|
|
9510
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
10349
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
|
|
9511
10350
|
for (const record of report.records) {
|
|
9512
10351
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
9513
10352
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -9538,18 +10377,18 @@ function reportSessionDbRecoveryRecord(state, record) {
|
|
|
9538
10377
|
function displayStatus(state) {
|
|
9539
10378
|
if (!state.interactive) return;
|
|
9540
10379
|
const attempt = state.connection?.reconnectAttempt ?? 0;
|
|
9541
|
-
const tunnel = state.connected ?
|
|
9542
|
-
const opencode = state.opencodeConnected ?
|
|
9543
|
-
const messages = state.messageCount > 0 ?
|
|
10380
|
+
const tunnel = state.connected ? chalk7.green("tunnel: connected") : attempt > 0 ? chalk7.yellow(`tunnel: reconnecting (#${attempt})`) : chalk7.yellow("tunnel: connecting");
|
|
10381
|
+
const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
|
|
10382
|
+
const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
|
|
9544
10383
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
9545
|
-
const detail = last ?
|
|
10384
|
+
const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
|
|
9546
10385
|
const agent = state.agentName ?? state.agentId;
|
|
9547
10386
|
console.log(
|
|
9548
|
-
`${
|
|
10387
|
+
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
9549
10388
|
);
|
|
9550
10389
|
}
|
|
9551
10390
|
async function promptForLogin(promptMessage, successMessage) {
|
|
9552
|
-
const action = await
|
|
10391
|
+
const action = await select4({
|
|
9553
10392
|
message: promptMessage,
|
|
9554
10393
|
choices: [
|
|
9555
10394
|
{
|
|
@@ -9565,7 +10404,7 @@ async function promptForLogin(promptMessage, successMessage) {
|
|
|
9565
10404
|
]
|
|
9566
10405
|
});
|
|
9567
10406
|
if (action === "exit") {
|
|
9568
|
-
console.log(
|
|
10407
|
+
console.log(chalk7.dim(`
|
|
9569
10408
|
You can log in later by running: ${getCliName()} login`));
|
|
9570
10409
|
process.exit(0);
|
|
9571
10410
|
}
|
|
@@ -9576,7 +10415,7 @@ You can log in later by running: ${getCliName()} login`));
|
|
|
9576
10415
|
process.exit(1);
|
|
9577
10416
|
}
|
|
9578
10417
|
blank();
|
|
9579
|
-
console.log(
|
|
10418
|
+
console.log(chalk7.green(successMessage));
|
|
9580
10419
|
blank();
|
|
9581
10420
|
return { token: credentials2.token, authType: "bearer", user: credentials2.user };
|
|
9582
10421
|
}
|
|
@@ -9589,12 +10428,12 @@ async function handleAuthError(state, error2) {
|
|
|
9589
10428
|
if (state.interactive) displayStatus(state);
|
|
9590
10429
|
if (!state.interactive) {
|
|
9591
10430
|
blank();
|
|
9592
|
-
console.log(
|
|
9593
|
-
console.log(
|
|
10431
|
+
console.log(chalk7.red("Authentication expired"));
|
|
10432
|
+
console.log(chalk7.dim("Your authentication token is no longer valid."));
|
|
9594
10433
|
blank();
|
|
9595
|
-
console.log(
|
|
9596
|
-
console.log(
|
|
9597
|
-
console.log(
|
|
10434
|
+
console.log(chalk7.dim("To fix this:"));
|
|
10435
|
+
console.log(chalk7.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
|
|
10436
|
+
console.log(chalk7.dim(" 2. Restart this command"));
|
|
9598
10437
|
blank();
|
|
9599
10438
|
await cleanup(state);
|
|
9600
10439
|
await shutdownTelemetry();
|
|
@@ -9602,7 +10441,7 @@ async function handleAuthError(state, error2) {
|
|
|
9602
10441
|
return { success: false };
|
|
9603
10442
|
}
|
|
9604
10443
|
blank();
|
|
9605
|
-
console.log(
|
|
10444
|
+
console.log(chalk7.yellow("Your authentication has expired."));
|
|
9606
10445
|
blank();
|
|
9607
10446
|
try {
|
|
9608
10447
|
const credentials2 = await promptForLogin(
|
|
@@ -9666,6 +10505,14 @@ async function driveChannels(state, driver) {
|
|
|
9666
10505
|
const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
|
|
9667
10506
|
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
9668
10507
|
if (opencodeAuthApplied) state.openaiUsageRearm?.();
|
|
10508
|
+
if (claudeCredentialApplied || opencodeAuthApplied) {
|
|
10509
|
+
void reloadProviderCache(state.port).catch(
|
|
10510
|
+
(error2) => logActivity(state, {
|
|
10511
|
+
type: "error",
|
|
10512
|
+
error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
10513
|
+
})
|
|
10514
|
+
);
|
|
10515
|
+
}
|
|
9669
10516
|
if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
|
|
9670
10517
|
idlePolls = 0;
|
|
9671
10518
|
idleMs = 0;
|
|
@@ -9693,8 +10540,8 @@ async function driveChannels(state, driver) {
|
|
|
9693
10540
|
state.running = false;
|
|
9694
10541
|
break;
|
|
9695
10542
|
}
|
|
9696
|
-
const
|
|
9697
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
10543
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
|
|
10544
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage3}` });
|
|
9698
10545
|
if (state.interactive) displayStatus(state);
|
|
9699
10546
|
if (driver.hasInFlightWatchers()) {
|
|
9700
10547
|
consecutiveDrainFailures = 0;
|
|
@@ -9732,9 +10579,18 @@ async function driveChannels(state, driver) {
|
|
|
9732
10579
|
}
|
|
9733
10580
|
}
|
|
9734
10581
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
9735
|
-
var SESSION_DB_RECLAIM_MAX_PAGES =
|
|
10582
|
+
var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
|
|
10583
|
+
function shouldWarnForReclaimSkip(reason) {
|
|
10584
|
+
if (reason !== "sqlite-unavailable") return false;
|
|
10585
|
+
const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
|
|
10586
|
+
if (!version2) return false;
|
|
10587
|
+
const major = Number(version2[1]);
|
|
10588
|
+
const minor = Number(version2[2]);
|
|
10589
|
+
const patch = Number(version2[3]);
|
|
10590
|
+
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
10591
|
+
}
|
|
9736
10592
|
function sessionDbPath() {
|
|
9737
|
-
return
|
|
10593
|
+
return join10(homedir6(), ".local", "share", "opencode", "opencode.db");
|
|
9738
10594
|
}
|
|
9739
10595
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
9740
10596
|
const record = {
|
|
@@ -9830,7 +10686,7 @@ async function runSweep(state, driver, config) {
|
|
|
9830
10686
|
} else {
|
|
9831
10687
|
logActivity(state, {
|
|
9832
10688
|
type: "info",
|
|
9833
|
-
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})`
|
|
10689
|
+
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : "")
|
|
9834
10690
|
});
|
|
9835
10691
|
}
|
|
9836
10692
|
} catch (error2) {
|
|
@@ -9853,13 +10709,20 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
9853
10709
|
for (const warning2 of config.warnings) {
|
|
9854
10710
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
9855
10711
|
}
|
|
9856
|
-
const dbBytes = statSessionDbBytes(
|
|
10712
|
+
const dbBytes = statSessionDbBytes(homedir6());
|
|
9857
10713
|
void (async () => {
|
|
9858
|
-
const
|
|
10714
|
+
const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
10715
|
+
if (reclaimAvailability !== null) {
|
|
10716
|
+
logActivity(state, {
|
|
10717
|
+
type: "info",
|
|
10718
|
+
level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
|
|
10719
|
+
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
|
|
10720
|
+
});
|
|
10721
|
+
}
|
|
9859
10722
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
9860
10723
|
dbBytes,
|
|
9861
10724
|
cleanupEnabled: config.enabled,
|
|
9862
|
-
reclaimSkipReason
|
|
10725
|
+
reclaimSkipReason: reclaimAvailability?.reason ?? null
|
|
9863
10726
|
});
|
|
9864
10727
|
if (sizeWarning !== null) {
|
|
9865
10728
|
logActivity(state, { type: "info", level: "warn", message: sizeWarning });
|
|
@@ -10054,7 +10917,8 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10054
10917
|
});
|
|
10055
10918
|
return;
|
|
10056
10919
|
}
|
|
10057
|
-
const collect = createResourceUsageCollector(
|
|
10920
|
+
const { collect, stop } = createResourceUsageCollector(homedir6());
|
|
10921
|
+
state.stopResourceUsageSampling = stop;
|
|
10058
10922
|
let consecutiveFailures = 0;
|
|
10059
10923
|
const tick = async () => {
|
|
10060
10924
|
try {
|
|
@@ -10150,6 +11014,8 @@ async function cleanup(state, opts = {}) {
|
|
|
10150
11014
|
clearTimeout(timer);
|
|
10151
11015
|
}
|
|
10152
11016
|
state.sessionCleanupTimers = [];
|
|
11017
|
+
state.stopOpenCodeLogTail?.();
|
|
11018
|
+
state.stopOpenCodeLogTail = null;
|
|
10153
11019
|
if (state.claudeUsageTimer) {
|
|
10154
11020
|
clearTimeout(state.claudeUsageTimer);
|
|
10155
11021
|
state.claudeUsageTimer = null;
|
|
@@ -10164,6 +11030,8 @@ async function cleanup(state, opts = {}) {
|
|
|
10164
11030
|
clearTimeout(state.resourceUsageTimer);
|
|
10165
11031
|
state.resourceUsageTimer = null;
|
|
10166
11032
|
}
|
|
11033
|
+
state.stopResourceUsageSampling?.();
|
|
11034
|
+
state.stopResourceUsageSampling = null;
|
|
10167
11035
|
const credentialSync = state.credentialSync;
|
|
10168
11036
|
const flushCredentials = credentialSync ? async (phase, publish) => {
|
|
10169
11037
|
await timeShutdownPhase(state, durations, phase, async () => {
|
|
@@ -10286,7 +11154,7 @@ async function run(options) {
|
|
|
10286
11154
|
let fileSyncDirectories;
|
|
10287
11155
|
try {
|
|
10288
11156
|
logLevel = resolveLogLevel(options);
|
|
10289
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
11157
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
|
|
10290
11158
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
10291
11159
|
throw new Error(
|
|
10292
11160
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -10317,6 +11185,7 @@ async function run(options) {
|
|
|
10317
11185
|
opencodeVersion: null,
|
|
10318
11186
|
sessionDbProvenanceAnomaly: false,
|
|
10319
11187
|
opencodeProcess: null,
|
|
11188
|
+
stopOpenCodeLogTail: null,
|
|
10320
11189
|
litestreamProcess: null,
|
|
10321
11190
|
connection: null,
|
|
10322
11191
|
channelDriver: null,
|
|
@@ -10331,6 +11200,7 @@ async function run(options) {
|
|
|
10331
11200
|
openaiUsageTimer: null,
|
|
10332
11201
|
openaiUsageRearm: null,
|
|
10333
11202
|
resourceUsageTimer: null,
|
|
11203
|
+
stopResourceUsageSampling: null,
|
|
10334
11204
|
credentialSync: null,
|
|
10335
11205
|
authHeader: ""
|
|
10336
11206
|
};
|
|
@@ -10383,15 +11253,15 @@ async function run(options) {
|
|
|
10383
11253
|
printError("Authentication required");
|
|
10384
11254
|
blank();
|
|
10385
11255
|
console.log(
|
|
10386
|
-
|
|
11256
|
+
chalk7.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
|
|
10387
11257
|
);
|
|
10388
|
-
console.log(
|
|
11258
|
+
console.log(chalk7.dim("Or run `evident login` for interactive authentication"));
|
|
10389
11259
|
blank();
|
|
10390
11260
|
process.exit(1);
|
|
10391
11261
|
return;
|
|
10392
11262
|
}
|
|
10393
11263
|
blank();
|
|
10394
|
-
console.log(
|
|
11264
|
+
console.log(chalk7.yellow("You are not logged in to Evident."));
|
|
10395
11265
|
blank();
|
|
10396
11266
|
credentials2 = await promptForLogin(
|
|
10397
11267
|
"Would you like to log in now?",
|
|
@@ -10441,7 +11311,7 @@ async function run(options) {
|
|
|
10441
11311
|
);
|
|
10442
11312
|
blank();
|
|
10443
11313
|
console.log(
|
|
10444
|
-
|
|
11314
|
+
chalk7.dim(
|
|
10445
11315
|
"Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
|
|
10446
11316
|
)
|
|
10447
11317
|
);
|
|
@@ -10464,15 +11334,15 @@ async function run(options) {
|
|
|
10464
11334
|
);
|
|
10465
11335
|
if (interactive && !state.json) {
|
|
10466
11336
|
blank();
|
|
10467
|
-
console.log(
|
|
10468
|
-
console.log(
|
|
11337
|
+
console.log(chalk7.bold("Evident Run"));
|
|
11338
|
+
console.log(chalk7.dim("-".repeat(40)));
|
|
10469
11339
|
}
|
|
10470
11340
|
const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
|
|
10471
11341
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
10472
11342
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
10473
11343
|
spinner?.fail("Authentication failed");
|
|
10474
11344
|
blank();
|
|
10475
|
-
console.log(
|
|
11345
|
+
console.log(chalk7.yellow("Your authentication token is invalid or expired."));
|
|
10476
11346
|
blank();
|
|
10477
11347
|
credentials2 = await promptForLogin(
|
|
10478
11348
|
"Would you like to log in again?",
|
|
@@ -10520,6 +11390,13 @@ async function run(options) {
|
|
|
10520
11390
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
10521
11391
|
}
|
|
10522
11392
|
state.credentialSync?.arm();
|
|
11393
|
+
state.stopOpenCodeLogTail = tailOpenCodeLogFile(
|
|
11394
|
+
resolveOpenCodeLogPath(homedir6(), process.env),
|
|
11395
|
+
createOpenCodeActivityForwarder(() => ({
|
|
11396
|
+
agentId: state.agentId,
|
|
11397
|
+
authHeader: state.authHeader
|
|
11398
|
+
}))
|
|
11399
|
+
).stop;
|
|
10523
11400
|
let sessionDbVerifyFatal = false;
|
|
10524
11401
|
if (!options.restoreSessionDb) {
|
|
10525
11402
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -10569,6 +11446,13 @@ async function run(options) {
|
|
|
10569
11446
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
10570
11447
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
10571
11448
|
}
|
|
11449
|
+
const { version: opencodeVersion, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(
|
|
11450
|
+
options,
|
|
11451
|
+
process.env
|
|
11452
|
+
);
|
|
11453
|
+
for (const warning2 of opencodeVersionWarnings) {
|
|
11454
|
+
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
11455
|
+
}
|
|
10572
11456
|
const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
|
|
10573
11457
|
for (const warning2 of maxActiveSessionsWarnings) {
|
|
10574
11458
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
@@ -10576,7 +11460,14 @@ async function run(options) {
|
|
|
10576
11460
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
10577
11461
|
const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
|
|
10578
11462
|
try {
|
|
10579
|
-
const oc = await
|
|
11463
|
+
const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
|
|
11464
|
+
port: state.port,
|
|
11465
|
+
interactive: state.interactive,
|
|
11466
|
+
agentId: state.agentId,
|
|
11467
|
+
log: (message) => log2(state, message),
|
|
11468
|
+
startTimeoutMs: opencodeStartTimeoutMs,
|
|
11469
|
+
inheritStdio: Boolean(options.opencodePidFile)
|
|
11470
|
+
}) : await ensureOpenCodeRunning({
|
|
10580
11471
|
port: state.port,
|
|
10581
11472
|
interactive: state.interactive,
|
|
10582
11473
|
agentId: state.agentId,
|
|
@@ -10603,7 +11494,7 @@ async function run(options) {
|
|
|
10603
11494
|
const provenance = checkSessionDbProvenance({
|
|
10604
11495
|
dbPath: sessionDbPath(),
|
|
10605
11496
|
currentVersion: state.opencodeVersion,
|
|
10606
|
-
homeDir:
|
|
11497
|
+
homeDir: homedir6(),
|
|
10607
11498
|
env: process.env
|
|
10608
11499
|
});
|
|
10609
11500
|
if (provenance.anomaly) {
|
|
@@ -10630,6 +11521,7 @@ async function run(options) {
|
|
|
10630
11521
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
10631
11522
|
}
|
|
10632
11523
|
}
|
|
11524
|
+
await reloadProviderCache(state.port);
|
|
10633
11525
|
const noProviderWarning = buildNoProviderWarning(
|
|
10634
11526
|
await hasAnyConfiguredProvider(state.port)
|
|
10635
11527
|
);
|
|
@@ -10638,10 +11530,10 @@ async function run(options) {
|
|
|
10638
11530
|
if (state.interactive && !state.json) {
|
|
10639
11531
|
logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
|
|
10640
11532
|
blank();
|
|
10641
|
-
console.log(
|
|
11533
|
+
console.log(chalk7.yellow("\u26A0 No OpenCode model provider is configured."));
|
|
10642
11534
|
console.log(
|
|
10643
|
-
|
|
10644
|
-
`Run ${
|
|
11535
|
+
chalk7.dim(
|
|
11536
|
+
`Run ${chalk7.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
|
|
10645
11537
|
)
|
|
10646
11538
|
);
|
|
10647
11539
|
blank();
|
|
@@ -10765,7 +11657,7 @@ async function run(options) {
|
|
|
10765
11657
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
10766
11658
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
10767
11659
|
fileSyncDirectories,
|
|
10768
|
-
homeDir:
|
|
11660
|
+
homeDir: homedir6(),
|
|
10769
11661
|
maxActiveSessions,
|
|
10770
11662
|
log: (entry) => (
|
|
10771
11663
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -10891,6 +11783,18 @@ async function run(options) {
|
|
|
10891
11783
|
if (state.interactive) displayStatus(state);
|
|
10892
11784
|
});
|
|
10893
11785
|
},
|
|
11786
|
+
// Both loops are rearmed because `rearm()` is idempotent for the
|
|
11787
|
+
// provider that did not just connect, and is a no-op when reporting is off.
|
|
11788
|
+
onUsageRearmPing: () => {
|
|
11789
|
+
if (!state.running) return;
|
|
11790
|
+
logActivity(state, {
|
|
11791
|
+
type: "info",
|
|
11792
|
+
level: "debug",
|
|
11793
|
+
message: "Usage rearm ping received"
|
|
11794
|
+
});
|
|
11795
|
+
state.claudeUsageRearm?.();
|
|
11796
|
+
state.openaiUsageRearm?.();
|
|
11797
|
+
},
|
|
10894
11798
|
onInfo: (message) => logActivity(state, { type: "info", message })
|
|
10895
11799
|
}
|
|
10896
11800
|
});
|
|
@@ -10911,7 +11815,17 @@ async function run(options) {
|
|
|
10911
11815
|
setTimer: (timer) => {
|
|
10912
11816
|
state.openaiUsageTimer = timer;
|
|
10913
11817
|
},
|
|
10914
|
-
fetchUsage: () =>
|
|
11818
|
+
fetchUsage: async () => {
|
|
11819
|
+
const usage = await getOpenAiUsage(state.port);
|
|
11820
|
+
if (usage.subscription === null) {
|
|
11821
|
+
logActivity(state, {
|
|
11822
|
+
type: "info",
|
|
11823
|
+
level: "debug",
|
|
11824
|
+
message: "OpenAI usage subscription could not be identified from the local credential"
|
|
11825
|
+
});
|
|
11826
|
+
}
|
|
11827
|
+
return usage;
|
|
11828
|
+
},
|
|
10915
11829
|
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
10916
11830
|
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
10917
11831
|
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
@@ -10985,6 +11899,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10985
11899
|
).option("-v, --verbose", "Alias for --log-level debug (ignored if --log-level is set)").option("-c, --conversation <id>", "Process only this specific conversation").option("--idle-timeout <seconds>", "Exit after N seconds idle").option(
|
|
10986
11900
|
"--opencode-start-timeout <seconds>",
|
|
10987
11901
|
"Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
|
|
11902
|
+
).option(
|
|
11903
|
+
"--opencode-version <v1|v2>",
|
|
11904
|
+
"Which OpenCode major version to launch: v1 or v2 (default: v1). Env: EVIDENT_OPENCODE_VERSION"
|
|
10988
11905
|
).option("--json", "Output in JSON format").option(
|
|
10989
11906
|
"--session-cleanup-max-age <duration>",
|
|
10990
11907
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -11053,6 +11970,7 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
11053
11970
|
// Raw string — validation/precedence is single-sourced in run.ts's
|
|
11054
11971
|
// resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).
|
|
11055
11972
|
opencodeStartTimeout: options.opencodeStartTimeout,
|
|
11973
|
+
opencodeVersion: options.opencodeVersion,
|
|
11056
11974
|
json: options.json,
|
|
11057
11975
|
// Raw strings — the resolver in run.ts single-sources parsing (M1).
|
|
11058
11976
|
sessionCleanupMaxAge: options.sessionCleanupMaxAge,
|