@evident-ai/cli 3.4.1-dev.88fb738 → 3.4.1-dev.8c54b1f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/index.js +1555 -321
- 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
|
|
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}
|
|
@@ -1728,10 +1778,14 @@ function runSynchroniser(args, opts) {
|
|
|
1728
1778
|
let stderr = "";
|
|
1729
1779
|
let settled = false;
|
|
1730
1780
|
const timer = {};
|
|
1781
|
+
let abortListener;
|
|
1782
|
+
let spawnListener;
|
|
1731
1783
|
const finish = (result) => {
|
|
1732
1784
|
if (settled) return;
|
|
1733
1785
|
settled = true;
|
|
1734
1786
|
if (timer.handle) clearTimeout(timer.handle);
|
|
1787
|
+
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
1788
|
+
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
1735
1789
|
resolve4(result);
|
|
1736
1790
|
};
|
|
1737
1791
|
try {
|
|
@@ -1757,6 +1811,25 @@ function runSynchroniser(args, opts) {
|
|
|
1757
1811
|
child.once("close", (code) => {
|
|
1758
1812
|
finish({ code, stdout, stderr, timedOut: false });
|
|
1759
1813
|
});
|
|
1814
|
+
if (opts.signal) {
|
|
1815
|
+
const killChild = () => {
|
|
1816
|
+
if (child.pid === void 0) {
|
|
1817
|
+
if (!spawnListener) {
|
|
1818
|
+
spawnListener = killChild;
|
|
1819
|
+
child.once("spawn", spawnListener);
|
|
1820
|
+
}
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
child.kill("SIGKILL");
|
|
1824
|
+
};
|
|
1825
|
+
abortListener = killChild;
|
|
1826
|
+
if (opts.signal.aborted) {
|
|
1827
|
+
abortListener();
|
|
1828
|
+
} else {
|
|
1829
|
+
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
1830
|
+
if (opts.signal.aborted) abortListener();
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1760
1833
|
timer.handle = setTimeout(
|
|
1761
1834
|
() => {
|
|
1762
1835
|
child.kill("SIGKILL");
|
|
@@ -2217,9 +2290,9 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
2217
2290
|
}
|
|
2218
2291
|
|
|
2219
2292
|
// src/lib/opencode/session-db-provenance.ts
|
|
2220
|
-
import { createRequire } from "module";
|
|
2221
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2222
|
-
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";
|
|
2223
2296
|
var require2 = createRequire(import.meta.url);
|
|
2224
2297
|
function readSessionDbMigrationIds(dbPath) {
|
|
2225
2298
|
let db;
|
|
@@ -2413,6 +2486,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
2413
2486
|
|
|
2414
2487
|
// src/lib/opencode/process.ts
|
|
2415
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
|
+
}
|
|
2416
2500
|
function getProcessCwd(pid) {
|
|
2417
2501
|
const platform = process.platform;
|
|
2418
2502
|
try {
|
|
@@ -2461,14 +2545,14 @@ function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
2461
2545
|
}
|
|
2462
2546
|
return null;
|
|
2463
2547
|
}
|
|
2464
|
-
function
|
|
2548
|
+
function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
2465
2549
|
const instances = [];
|
|
2466
2550
|
try {
|
|
2467
2551
|
const platform = process.platform;
|
|
2468
2552
|
if (platform === "darwin" || platform === "linux") {
|
|
2469
2553
|
let pids = [];
|
|
2470
2554
|
try {
|
|
2471
|
-
const pgrepOutput = execSync(
|
|
2555
|
+
const pgrepOutput = execSync(`pgrep -f "${pgrepPattern}"`, {
|
|
2472
2556
|
encoding: "utf-8",
|
|
2473
2557
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2474
2558
|
}).trim();
|
|
@@ -2477,7 +2561,7 @@ function findOpenCodeProcesses() {
|
|
|
2477
2561
|
}
|
|
2478
2562
|
} catch {
|
|
2479
2563
|
try {
|
|
2480
|
-
const psOutput = execSync(
|
|
2564
|
+
const psOutput = execSync(`ps aux | grep -E "${psPattern}" | grep -v grep`, {
|
|
2481
2565
|
encoding: "utf-8",
|
|
2482
2566
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2483
2567
|
}).trim();
|
|
@@ -2523,6 +2607,9 @@ function findOpenCodeProcesses() {
|
|
|
2523
2607
|
}
|
|
2524
2608
|
return instances;
|
|
2525
2609
|
}
|
|
2610
|
+
function findOpenCodeProcesses() {
|
|
2611
|
+
return findProcessesByPattern("opencode serve|opencode-serve", "opencode (serve|--port)");
|
|
2612
|
+
}
|
|
2526
2613
|
async function scanPortsForOpenCode() {
|
|
2527
2614
|
const instances = [];
|
|
2528
2615
|
const checks = OPENCODE_PORT_RANGE.map(async (port) => {
|
|
@@ -2569,7 +2656,7 @@ async function findHealthyOpenCodeInstances() {
|
|
|
2569
2656
|
}
|
|
2570
2657
|
async function startOpenCode(port, options = {}) {
|
|
2571
2658
|
let command = "opencode";
|
|
2572
|
-
const printLogs = options.inheritStdio ? ["--print-logs"] : [];
|
|
2659
|
+
const printLogs = options.inheritStdio ? ["--print-logs", "--log-level", resolveOpenCodeLogLevel(process.env)] : [];
|
|
2573
2660
|
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...printLogs];
|
|
2574
2661
|
try {
|
|
2575
2662
|
execSync("which opencode", { stdio: "ignore" });
|
|
@@ -2626,6 +2713,19 @@ function isOpenCodeInstalled() {
|
|
|
2626
2713
|
return false;
|
|
2627
2714
|
}
|
|
2628
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
|
+
}
|
|
2629
2729
|
async function promptOpenCodeInstall(interactive) {
|
|
2630
2730
|
if (!interactive) {
|
|
2631
2731
|
console.log(
|
|
@@ -2635,7 +2735,11 @@ async function promptOpenCodeInstall(interactive) {
|
|
|
2635
2735
|
install_url: OPENCODE_INSTALL_URL,
|
|
2636
2736
|
install_commands: {
|
|
2637
2737
|
npm: "npm install -g opencode-ai",
|
|
2638
|
-
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
|
+
}
|
|
2639
2743
|
}
|
|
2640
2744
|
})
|
|
2641
2745
|
);
|
|
@@ -3119,6 +3223,44 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
3119
3223
|
}
|
|
3120
3224
|
return lastOk ?? last;
|
|
3121
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
|
+
}
|
|
3122
3264
|
function messageUsage(messages, userMessageId) {
|
|
3123
3265
|
if (!messages || messages.length === 0) return null;
|
|
3124
3266
|
const byParentAll = messages.filter(
|
|
@@ -3247,8 +3389,7 @@ function isAbortedTerminalReply(messages, userMessageId) {
|
|
|
3247
3389
|
}
|
|
3248
3390
|
return false;
|
|
3249
3391
|
}
|
|
3250
|
-
function
|
|
3251
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
3392
|
+
function classifyReplyAuthError(reply) {
|
|
3252
3393
|
const error2 = errorOf(reply);
|
|
3253
3394
|
if (error2 == null || typeof error2 !== "object") return null;
|
|
3254
3395
|
const e = error2;
|
|
@@ -3273,6 +3414,32 @@ function messageFailure(messages, userMessageId) {
|
|
|
3273
3414
|
}
|
|
3274
3415
|
return null;
|
|
3275
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
|
+
}
|
|
3276
3443
|
function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProviderId, replyModelId = null) {
|
|
3277
3444
|
if (classified != null) return classified;
|
|
3278
3445
|
if (hasConfiguredProvider !== false) return null;
|
|
@@ -3320,6 +3487,94 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3320
3487
|
return null;
|
|
3321
3488
|
}
|
|
3322
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
|
+
}
|
|
3323
3578
|
|
|
3324
3579
|
// src/lib/opencode/session-cleanup.ts
|
|
3325
3580
|
var DURATION_UNIT_MS = {
|
|
@@ -3426,8 +3681,8 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
3426
3681
|
}
|
|
3427
3682
|
|
|
3428
3683
|
// src/lib/opencode/session-db-size.ts
|
|
3429
|
-
import { statSync as statSync3 } from "fs";
|
|
3430
|
-
import { join as join4 } from "path";
|
|
3684
|
+
import { statSync as statSync3 } from "node:fs";
|
|
3685
|
+
import { join as join4 } from "node:path";
|
|
3431
3686
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
3432
3687
|
function statSessionDbBytes(homeDir) {
|
|
3433
3688
|
const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
@@ -3457,9 +3712,96 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
3457
3712
|
return null;
|
|
3458
3713
|
}
|
|
3459
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
|
+
|
|
3460
3798
|
// src/lib/opencode/session-db-reclaim.ts
|
|
3461
|
-
import { statSync as
|
|
3462
|
-
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
|
+
}
|
|
3463
3805
|
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
3464
3806
|
try {
|
|
3465
3807
|
const fsStats = statfsSync(dirname4(dbPath));
|
|
@@ -3485,17 +3827,17 @@ async function probeReclaimAvailability(input) {
|
|
|
3485
3827
|
const { dbPath, requiredBytes } = input;
|
|
3486
3828
|
let sqlite;
|
|
3487
3829
|
try {
|
|
3488
|
-
sqlite = await import("sqlite");
|
|
3830
|
+
sqlite = await import("node:sqlite");
|
|
3489
3831
|
} catch (err) {
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
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 };
|
|
3494
3835
|
}
|
|
3495
3836
|
let autoVacuum = null;
|
|
3496
3837
|
try {
|
|
3497
3838
|
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
3498
3839
|
try {
|
|
3840
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3499
3841
|
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3500
3842
|
} finally {
|
|
3501
3843
|
db.close();
|
|
@@ -3506,23 +3848,25 @@ async function probeReclaimAvailability(input) {
|
|
|
3506
3848
|
);
|
|
3507
3849
|
}
|
|
3508
3850
|
if (autoVacuum !== 0) return null;
|
|
3509
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
|
|
3851
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
3510
3852
|
}
|
|
3511
3853
|
async function reclaimSessionDbSpace(input) {
|
|
3512
3854
|
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
3513
3855
|
let sqlite;
|
|
3514
3856
|
try {
|
|
3515
|
-
sqlite = await import("sqlite");
|
|
3857
|
+
sqlite = await import("node:sqlite");
|
|
3516
3858
|
} catch (err) {
|
|
3859
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
3517
3860
|
console.warn(
|
|
3518
|
-
`[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}`
|
|
3519
3862
|
);
|
|
3520
|
-
return { ok: false, skipped: "sqlite-unavailable" };
|
|
3863
|
+
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
3521
3864
|
}
|
|
3522
3865
|
const { DatabaseSync } = sqlite;
|
|
3523
3866
|
let db;
|
|
3524
3867
|
try {
|
|
3525
3868
|
db = new DatabaseSync(dbPath);
|
|
3869
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3526
3870
|
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3527
3871
|
if (autoVacuum === 0) {
|
|
3528
3872
|
if (!allowFullVacuum) {
|
|
@@ -3531,7 +3875,7 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3531
3875
|
);
|
|
3532
3876
|
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
3533
3877
|
}
|
|
3534
|
-
const fileBytesForGuard =
|
|
3878
|
+
const fileBytesForGuard = statSync5(dbPath).size;
|
|
3535
3879
|
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
3536
3880
|
if (skipReason !== null) {
|
|
3537
3881
|
console.warn(
|
|
@@ -3559,10 +3903,12 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3559
3903
|
);
|
|
3560
3904
|
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
3561
3905
|
} catch (err) {
|
|
3562
|
-
console.error(
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3906
|
+
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
3907
|
+
return {
|
|
3908
|
+
ok: false,
|
|
3909
|
+
skipped: "reclaim-error",
|
|
3910
|
+
detail: errorMessage(err)
|
|
3911
|
+
};
|
|
3566
3912
|
} finally {
|
|
3567
3913
|
db?.close();
|
|
3568
3914
|
}
|
|
@@ -3603,7 +3949,6 @@ var StreamForwarder = class {
|
|
|
3603
3949
|
handleFrame(frame) {
|
|
3604
3950
|
switch (frame.type) {
|
|
3605
3951
|
case "open":
|
|
3606
|
-
this.callbacks.onOpen?.(frame.sid, frame.method, frame.path);
|
|
3607
3952
|
void this.handleOpen(frame);
|
|
3608
3953
|
break;
|
|
3609
3954
|
case "req_data":
|
|
@@ -3639,12 +3984,21 @@ var StreamForwarder = class {
|
|
|
3639
3984
|
const { sid, method, path, headers, has_body } = frame;
|
|
3640
3985
|
const correlationId = headers?.[CORRELATION_ID_HEADER];
|
|
3641
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
|
+
}
|
|
3642
3990
|
if (path === TUNNEL_DRAIN_PING_PATH) {
|
|
3643
3991
|
this.callbacks.onDrainPing?.();
|
|
3644
3992
|
this.send({ type: "head", sid, status: 204, headers: {} });
|
|
3645
3993
|
this.send({ type: "res_end", sid });
|
|
3646
3994
|
return;
|
|
3647
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
|
+
}
|
|
3648
4002
|
if (process.env.DEBUG) {
|
|
3649
4003
|
log("debug", "agent_request", {
|
|
3650
4004
|
correlation_id: correlationId,
|
|
@@ -3789,7 +4143,8 @@ function connectTunnel(options) {
|
|
|
3789
4143
|
onResponse,
|
|
3790
4144
|
onInfo,
|
|
3791
4145
|
onWarning,
|
|
3792
|
-
onDrainPing
|
|
4146
|
+
onDrainPing,
|
|
4147
|
+
onUsageRearmPing
|
|
3793
4148
|
} = options;
|
|
3794
4149
|
const tunnelUrl = getTunnelUrlConfig();
|
|
3795
4150
|
const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
|
|
@@ -3801,7 +4156,8 @@ function connectTunnel(options) {
|
|
|
3801
4156
|
});
|
|
3802
4157
|
const forwarder = new StreamForwarder(ws, port, {
|
|
3803
4158
|
onHead: () => onResponse?.(),
|
|
3804
|
-
onDrainPing: () => onDrainPing?.()
|
|
4159
|
+
onDrainPing: () => onDrainPing?.(),
|
|
4160
|
+
onUsageRearmPing: () => onUsageRearmPing?.()
|
|
3805
4161
|
});
|
|
3806
4162
|
const connectionTimeout = setTimeout(() => {
|
|
3807
4163
|
ws.close();
|
|
@@ -3844,8 +4200,8 @@ function connectTunnel(options) {
|
|
|
3844
4200
|
try {
|
|
3845
4201
|
message = JSON.parse(data.toString());
|
|
3846
4202
|
} catch (error2) {
|
|
3847
|
-
const
|
|
3848
|
-
onError?.(`Failed to handle message: ${
|
|
4203
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
4204
|
+
onError?.(`Failed to handle message: ${errorMessage3}`);
|
|
3849
4205
|
return;
|
|
3850
4206
|
}
|
|
3851
4207
|
if (isStreamFrame(message)) {
|
|
@@ -3962,6 +4318,7 @@ var RunnerConnection = class {
|
|
|
3962
4318
|
onError: (error2) => events.onError?.(error2),
|
|
3963
4319
|
onResponse: () => events.onResponse?.(),
|
|
3964
4320
|
onDrainPing: () => events.onDrainPing?.(),
|
|
4321
|
+
onUsageRearmPing: () => events.onUsageRearmPing?.(),
|
|
3965
4322
|
onInfo: (message) => events.onInfo?.(message),
|
|
3966
4323
|
onWarning: (message) => events.onWarning?.(message)
|
|
3967
4324
|
});
|
|
@@ -3988,7 +4345,7 @@ var RunnerConnection = class {
|
|
|
3988
4345
|
};
|
|
3989
4346
|
|
|
3990
4347
|
// src/lib/tunnel/ready-marker.ts
|
|
3991
|
-
import { writeFileSync as writeFileSync3 } from "fs";
|
|
4348
|
+
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
3992
4349
|
function writeTunnelReadyMarker(path, agentId) {
|
|
3993
4350
|
try {
|
|
3994
4351
|
writeFileSync3(path, `${agentId}
|
|
@@ -4000,7 +4357,7 @@ function writeTunnelReadyMarker(path, agentId) {
|
|
|
4000
4357
|
}
|
|
4001
4358
|
|
|
4002
4359
|
// src/lib/replication.ts
|
|
4003
|
-
import { spawn as spawn4 } from "child_process";
|
|
4360
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
4004
4361
|
function startSessionDbReplication(configPath) {
|
|
4005
4362
|
return spawn4("litestream", ["replicate", "-config", configPath], {
|
|
4006
4363
|
stdio: "inherit"
|
|
@@ -4016,7 +4373,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
4016
4373
|
}
|
|
4017
4374
|
|
|
4018
4375
|
// src/lib/process-liveness.ts
|
|
4019
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
4376
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
4020
4377
|
function isProcessAlive(pid) {
|
|
4021
4378
|
try {
|
|
4022
4379
|
process.kill(pid, 0);
|
|
@@ -4042,9 +4399,9 @@ function isProcessAlive(pid) {
|
|
|
4042
4399
|
}
|
|
4043
4400
|
|
|
4044
4401
|
// src/lib/openai-usage.ts
|
|
4045
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
4046
|
-
import { homedir as
|
|
4047
|
-
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";
|
|
4048
4405
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4049
4406
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4050
4407
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4058,7 +4415,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4058
4415
|
}
|
|
4059
4416
|
function readOpenCodeChatGptCredentials() {
|
|
4060
4417
|
try {
|
|
4061
|
-
const raw = readFileSync5(
|
|
4418
|
+
const raw = readFileSync5(join6(homedir4(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4062
4419
|
let parsed;
|
|
4063
4420
|
try {
|
|
4064
4421
|
parsed = JSON.parse(raw);
|
|
@@ -4080,6 +4437,23 @@ function readOpenCodeChatGptCredentials() {
|
|
|
4080
4437
|
return null;
|
|
4081
4438
|
}
|
|
4082
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
|
+
}
|
|
4083
4457
|
function toWindow2(headers, name) {
|
|
4084
4458
|
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
4085
4459
|
const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
|
|
@@ -4155,6 +4529,7 @@ async function getOpenAiUsage(port) {
|
|
|
4155
4529
|
"credentials_expired"
|
|
4156
4530
|
);
|
|
4157
4531
|
}
|
|
4532
|
+
const subscription = parseChatGptIdentity(credentials2.accessToken);
|
|
4158
4533
|
const models = await resolveProbeModels(port);
|
|
4159
4534
|
if (models.length === 0) {
|
|
4160
4535
|
throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
|
|
@@ -4187,7 +4562,7 @@ async function getOpenAiUsage(port) {
|
|
|
4187
4562
|
"no_usable_window"
|
|
4188
4563
|
);
|
|
4189
4564
|
}
|
|
4190
|
-
return usage;
|
|
4565
|
+
return { ...usage, subscription };
|
|
4191
4566
|
}
|
|
4192
4567
|
if (res.status === 401) {
|
|
4193
4568
|
throw new OpenAiUsageError(
|
|
@@ -4302,8 +4677,8 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
4302
4677
|
}
|
|
4303
4678
|
|
|
4304
4679
|
// src/lib/resource-usage.ts
|
|
4305
|
-
import { cpus, totalmem, freemem } from "os";
|
|
4306
|
-
import { statfsSync as statfsSync2 } from "fs";
|
|
4680
|
+
import { cpus, totalmem, freemem } from "node:os";
|
|
4681
|
+
import { statfsSync as statfsSync2 } from "node:fs";
|
|
4307
4682
|
|
|
4308
4683
|
// src/lib/ecs-task-metadata.ts
|
|
4309
4684
|
var ECS_METADATA_TIMEOUT_MS = 2e3;
|
|
@@ -4388,58 +4763,97 @@ function readDisk(homeDir) {
|
|
|
4388
4763
|
};
|
|
4389
4764
|
}
|
|
4390
4765
|
}
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
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(() => {
|
|
4394
4776
|
const current = readCpuSample();
|
|
4395
|
-
const
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
const warnings = [];
|
|
4402
|
-
if (disk.warning) warnings.push(disk.warning);
|
|
4403
|
-
if (ecsWarning) warnings.push(ecsWarning);
|
|
4404
|
-
let cpuPercent = hostCpuPercent;
|
|
4405
|
-
let cpuCount = hostCpuCount;
|
|
4406
|
-
let memoryTotalBytes = totalmem();
|
|
4407
|
-
let memoryAvailableBytes = freemem();
|
|
4408
|
-
if (limits !== null) {
|
|
4409
|
-
cpuCount = limits.cpuCount;
|
|
4410
|
-
memoryTotalBytes = limits.memoryTotalBytes;
|
|
4411
|
-
memoryAvailableBytes = clamp(
|
|
4412
|
-
limits.memoryTotalBytes - (totalmem() - freemem()),
|
|
4413
|
-
0,
|
|
4414
|
-
limits.memoryTotalBytes
|
|
4415
|
-
);
|
|
4416
|
-
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
|
+
}
|
|
4417
4783
|
}
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
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
|
|
4430
4844
|
};
|
|
4431
4845
|
}
|
|
4432
4846
|
|
|
4433
4847
|
// src/lib/channels/driver.ts
|
|
4434
|
-
import { homedir as
|
|
4848
|
+
import { homedir as homedir5 } from "node:os";
|
|
4435
4849
|
|
|
4436
4850
|
// src/lib/runner-file-sync.ts
|
|
4437
|
-
import { join as
|
|
4851
|
+
import { join as join8 } from "node:path";
|
|
4438
4852
|
|
|
4439
4853
|
// src/lib/file-push.ts
|
|
4440
|
-
import { randomUUID } from "crypto";
|
|
4441
|
-
import { chmod, mkdir, open as
|
|
4442
|
-
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";
|
|
4443
4857
|
var FILE_MODE = 384;
|
|
4444
4858
|
var DIRECTORY_MODE = 448;
|
|
4445
4859
|
async function writePushedFile(request) {
|
|
@@ -4472,7 +4886,7 @@ async function writePushedFile(request) {
|
|
|
4472
4886
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
4473
4887
|
dirname5(candidate)
|
|
4474
4888
|
);
|
|
4475
|
-
const realTarget =
|
|
4889
|
+
const realTarget = join7(existingAncestor, ...missingSegments, basename(candidate));
|
|
4476
4890
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
4477
4891
|
if (allowedDirectory === null) {
|
|
4478
4892
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -4508,7 +4922,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
4508
4922
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
4509
4923
|
return null;
|
|
4510
4924
|
}
|
|
4511
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4925
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4512
4926
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
4513
4927
|
return null;
|
|
4514
4928
|
}
|
|
@@ -4581,16 +4995,16 @@ function contains(realDirectory, realTarget) {
|
|
|
4581
4995
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
4582
4996
|
let current = existingAncestor;
|
|
4583
4997
|
for (const segment of missingSegments) {
|
|
4584
|
-
current =
|
|
4998
|
+
current = join7(current, segment);
|
|
4585
4999
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
4586
5000
|
await chmod(current, DIRECTORY_MODE);
|
|
4587
5001
|
}
|
|
4588
5002
|
}
|
|
4589
5003
|
async function writeAtomically(realTarget, content) {
|
|
4590
|
-
const temporaryPath =
|
|
5004
|
+
const temporaryPath = join7(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
4591
5005
|
let handle;
|
|
4592
5006
|
try {
|
|
4593
|
-
handle = await
|
|
5007
|
+
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
4594
5008
|
await handle.writeFile(content);
|
|
4595
5009
|
await handle.chmod(FILE_MODE);
|
|
4596
5010
|
await handle.close();
|
|
@@ -4717,12 +5131,12 @@ var NOT_APPLIED = {
|
|
|
4717
5131
|
opencodeAuthApplied: false
|
|
4718
5132
|
};
|
|
4719
5133
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
4720
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4721
|
-
return expanded ===
|
|
5134
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5135
|
+
return expanded === join8(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
4722
5136
|
}
|
|
4723
5137
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
4724
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4725
|
-
return expanded ===
|
|
5138
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5139
|
+
return expanded === join8(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
4726
5140
|
}
|
|
4727
5141
|
async function applyOne(options, file) {
|
|
4728
5142
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -4882,6 +5296,9 @@ var DEFAULT_RETRY_POLICY = {
|
|
|
4882
5296
|
baseDelayMs: 500,
|
|
4883
5297
|
maxDelayMs: 3e4
|
|
4884
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;
|
|
4885
5302
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
4886
5303
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
4887
5304
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
@@ -5022,6 +5439,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5022
5439
|
* message; it is removed once its in-flight set empties.
|
|
5023
5440
|
*/
|
|
5024
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();
|
|
5025
5453
|
/**
|
|
5026
5454
|
* AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
|
|
5027
5455
|
* dispatched and are still in-flight. A message in this set is never
|
|
@@ -5249,6 +5677,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5249
5677
|
* and stops opencode.
|
|
5250
5678
|
*/
|
|
5251
5679
|
stopped = false;
|
|
5680
|
+
recycleRequestedFlag = false;
|
|
5252
5681
|
constructor(config) {
|
|
5253
5682
|
this.agentId = config.agentId;
|
|
5254
5683
|
this.port = config.port;
|
|
@@ -5268,7 +5697,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5268
5697
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
5269
5698
|
this.now = config.now ?? (() => Date.now());
|
|
5270
5699
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
5271
|
-
this.homeDir = config.homeDir ??
|
|
5700
|
+
this.homeDir = config.homeDir ?? homedir5();
|
|
5272
5701
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
5273
5702
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
5274
5703
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
@@ -5354,6 +5783,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5354
5783
|
let dispatched = 0;
|
|
5355
5784
|
try {
|
|
5356
5785
|
const conversations = await this.getPendingConversations();
|
|
5786
|
+
if (this.recycleRequestedFlag) {
|
|
5787
|
+
this.stop();
|
|
5788
|
+
}
|
|
5357
5789
|
if (conversations.length > 0) {
|
|
5358
5790
|
const total = conversations.reduce((sum, c) => sum + (c.pending_message_count ?? 0), 0);
|
|
5359
5791
|
this.log({
|
|
@@ -5481,6 +5913,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5481
5913
|
*/
|
|
5482
5914
|
stop() {
|
|
5483
5915
|
this.stopped = true;
|
|
5916
|
+
this.sessionErrorStream?.abort.abort();
|
|
5917
|
+
this.sessionErrorStream = null;
|
|
5918
|
+
}
|
|
5919
|
+
/**
|
|
5920
|
+
* The server clears this request when a new MicroVM identity is recorded, so a
|
|
5921
|
+
* same-VM tunnel reconnect does not consume it. This is a plain read rather
|
|
5922
|
+
* than a consume; `run.ts` guards the action once-only.
|
|
5923
|
+
*/
|
|
5924
|
+
get recycleRequested() {
|
|
5925
|
+
return this.recycleRequestedFlag;
|
|
5484
5926
|
}
|
|
5485
5927
|
/**
|
|
5486
5928
|
* Wait (up to `timeoutMs`) for in-flight watcher work to settle during a
|
|
@@ -5547,6 +5989,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5547
5989
|
*/
|
|
5548
5990
|
async processConversation(conv) {
|
|
5549
5991
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
5992
|
+
this.ensureSessionErrorStream();
|
|
5550
5993
|
const messages = await this.getPendingMessages(conv.id);
|
|
5551
5994
|
let dispatched = 0;
|
|
5552
5995
|
let skippedAlreadyDispatched = 0;
|
|
@@ -5619,7 +6062,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5619
6062
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
5620
6063
|
break;
|
|
5621
6064
|
}
|
|
5622
|
-
const
|
|
6065
|
+
const errorMessage3 = err instanceof Error ? err.message : String(err);
|
|
5623
6066
|
this.sessions.delete(conv.id);
|
|
5624
6067
|
this.supersede(conv.id, sessionId);
|
|
5625
6068
|
this.log({
|
|
@@ -5628,7 +6071,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5628
6071
|
conversation_id: conv.id,
|
|
5629
6072
|
message_id: message.id
|
|
5630
6073
|
});
|
|
5631
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6074
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5632
6075
|
this.log({
|
|
5633
6076
|
level: "warn",
|
|
5634
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)}`,
|
|
@@ -5639,7 +6082,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5639
6082
|
});
|
|
5640
6083
|
this.log({
|
|
5641
6084
|
level: "error",
|
|
5642
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
6085
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
|
|
5643
6086
|
conversation_id: conv.id,
|
|
5644
6087
|
message_id: message.id
|
|
5645
6088
|
});
|
|
@@ -5660,14 +6103,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5660
6103
|
this.unconfirmedDispatchFailures.delete(message.id);
|
|
5661
6104
|
this.sessions.delete(conv.id);
|
|
5662
6105
|
this.supersede(conv.id, sessionId);
|
|
5663
|
-
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.`;
|
|
5664
6107
|
this.log({
|
|
5665
6108
|
level: "error",
|
|
5666
|
-
message:
|
|
6109
|
+
message: errorMessage3,
|
|
5667
6110
|
conversation_id: conv.id,
|
|
5668
6111
|
message_id: message.id
|
|
5669
6112
|
});
|
|
5670
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6113
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5671
6114
|
this.log({
|
|
5672
6115
|
level: "warn",
|
|
5673
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)}`,
|
|
@@ -6001,6 +6444,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6001
6444
|
});
|
|
6002
6445
|
await this.markFailed(conv.id, message.id, sessionId, error2, usage, failure);
|
|
6003
6446
|
}
|
|
6447
|
+
if (ocId !== null) {
|
|
6448
|
+
await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
|
|
6449
|
+
}
|
|
6004
6450
|
} catch (err) {
|
|
6005
6451
|
if (err instanceof ChannelAuthError) throw err;
|
|
6006
6452
|
this.log({
|
|
@@ -6540,6 +6986,24 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6540
6986
|
ambiguousPinnedSinceMs: 0,
|
|
6541
6987
|
ambiguousResolved: false
|
|
6542
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
|
+
}
|
|
6543
7007
|
}
|
|
6544
7008
|
/**
|
|
6545
7009
|
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
@@ -6744,6 +7208,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6744
7208
|
ensureWatcherRunning(sessionId) {
|
|
6745
7209
|
const watcher = this.watchers.get(sessionId);
|
|
6746
7210
|
if (!watcher) return;
|
|
7211
|
+
this.ensureSessionErrorStream();
|
|
6747
7212
|
if (watcher.loop) return;
|
|
6748
7213
|
if (watcher.inFlight.size === 0) {
|
|
6749
7214
|
this.watchers.delete(sessionId);
|
|
@@ -6759,80 +7224,228 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6759
7224
|
});
|
|
6760
7225
|
watcher.loop = loop;
|
|
6761
7226
|
}
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
while (watcher.inFlight.size > 0) {
|
|
6789
|
-
if (watcher.generation !== generation) return;
|
|
6790
|
-
watcher.lastTickAt = this.now();
|
|
6791
|
-
await this.sleep(this.pausedPollIntervalMs);
|
|
6792
|
-
if (watcher.generation !== generation) return;
|
|
6793
|
-
let messages = null;
|
|
6794
|
-
try {
|
|
6795
|
-
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
6796
|
-
if (res.ok) {
|
|
6797
|
-
const body = await res.json();
|
|
6798
|
-
messages = Array.isArray(body) ? body : null;
|
|
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;
|
|
6799
7253
|
}
|
|
6800
|
-
} catch {
|
|
6801
7254
|
}
|
|
6802
|
-
if (
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
continue;
|
|
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;
|
|
6810
7262
|
}
|
|
7263
|
+
attempt = 0;
|
|
7264
|
+
} else {
|
|
7265
|
+
attempt += 1;
|
|
6811
7266
|
}
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
sessionId,
|
|
6817
|
-
watcher,
|
|
6818
|
-
inFlight,
|
|
6819
|
-
messages,
|
|
6820
|
-
openQuestions,
|
|
6821
|
-
openPermissions,
|
|
6822
|
-
questionsPolledOk,
|
|
6823
|
-
permissionsPolledOk
|
|
6824
|
-
);
|
|
6825
|
-
}
|
|
6826
|
-
}
|
|
6827
|
-
} catch (err) {
|
|
6828
|
-
if (err instanceof ChannelAuthError) {
|
|
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;
|
|
6829
7271
|
this.log({
|
|
6830
7272
|
level: "error",
|
|
6831
|
-
message: `
|
|
6832
|
-
conversation_id: watcher.conv.id
|
|
7273
|
+
message: `OpenCode session-error stream failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
|
|
6833
7274
|
});
|
|
6834
|
-
|
|
6835
|
-
|
|
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
|
+
}
|
|
7375
|
+
/**
|
|
7376
|
+
* The per-session polling loop (WI-3). Once per tick it:
|
|
7377
|
+
* 1. polls `GET /session/:id/message` once and, per in-flight message,
|
|
7378
|
+
* computes `messageRunState` and fires markProcessing (queued→running) /
|
|
7379
|
+
* markDone (done) exactly once per transition;
|
|
7380
|
+
* 2. applies the idle-path re-dispatch guard (a dispatched message that never
|
|
7381
|
+
* APPEARS → re-dispatch — D1 obligation 2);
|
|
7382
|
+
* 3. polls `/question` + `/permission` (scoped to the session) and surfaces
|
|
7383
|
+
* NEW ones via `reportInteraction`, carrying the PAUSED message's own
|
|
7384
|
+
* `source_message_id`;
|
|
7385
|
+
* 4. drops messages that completed or timed out from the in-flight set.
|
|
7386
|
+
* Exits when the in-flight set empties. Never throws.
|
|
7387
|
+
*
|
|
7388
|
+
* `generation` (#1618) is the incarnation this call was started under.
|
|
7389
|
+
* `reconcileWatchers` can restart a stalled loop by bumping
|
|
7390
|
+
* `watcher.generation` and starting a NEW `runWatcherLoop` over the same
|
|
7391
|
+
* `SessionWatcher` object — the stalled promise itself cannot be cancelled,
|
|
7392
|
+
* so this loop instead checks at the top of every iteration, right after
|
|
7393
|
+
* waking from `sleep`, and right before servicing any message, and quietly
|
|
7394
|
+
* retires (returns without touching anything) the moment it is no longer the
|
|
7395
|
+
* watcher's current generation. Retiring mid-tick can still let ONE
|
|
7396
|
+
* `serviceInFlightMessage` pass complete first — acceptable, since that
|
|
7397
|
+
* method contains no non-idempotent action.
|
|
7398
|
+
*/
|
|
7399
|
+
async runWatcherLoop(sessionId, watcher, generation) {
|
|
7400
|
+
try {
|
|
7401
|
+
while (watcher.inFlight.size > 0) {
|
|
7402
|
+
if (watcher.generation !== generation) return;
|
|
7403
|
+
watcher.lastTickAt = this.now();
|
|
7404
|
+
await this.sleep(this.pausedPollIntervalMs);
|
|
7405
|
+
if (watcher.generation !== generation) return;
|
|
7406
|
+
let messages = null;
|
|
7407
|
+
try {
|
|
7408
|
+
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
7409
|
+
if (res.ok) {
|
|
7410
|
+
const body = await res.json();
|
|
7411
|
+
messages = Array.isArray(body) ? body : null;
|
|
7412
|
+
}
|
|
7413
|
+
} catch {
|
|
7414
|
+
}
|
|
7415
|
+
if (messages != null && messages.length > 0) {
|
|
7416
|
+
watcher.lastGoodPollAt = this.now();
|
|
7417
|
+
watcher.hadUsablePoll = true;
|
|
7418
|
+
} else {
|
|
7419
|
+
const emptyButReachable = messages != null;
|
|
7420
|
+
const graceApplies = !emptyButReachable || watcher.hadUsablePoll;
|
|
7421
|
+
if (graceApplies && this.now() - watcher.lastGoodPollAt < POLL_MISS_GRACE_MS) {
|
|
7422
|
+
continue;
|
|
7423
|
+
}
|
|
7424
|
+
}
|
|
7425
|
+
const { openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk } = await this.pollInteractions(sessionId, watcher, messages);
|
|
7426
|
+
if (watcher.generation !== generation) return;
|
|
7427
|
+
for (const inFlight of [...watcher.inFlight.values()]) {
|
|
7428
|
+
await this.serviceInFlightMessage(
|
|
7429
|
+
sessionId,
|
|
7430
|
+
watcher,
|
|
7431
|
+
inFlight,
|
|
7432
|
+
messages,
|
|
7433
|
+
openQuestions,
|
|
7434
|
+
openPermissions,
|
|
7435
|
+
questionsPolledOk,
|
|
7436
|
+
permissionsPolledOk
|
|
7437
|
+
);
|
|
7438
|
+
}
|
|
7439
|
+
}
|
|
7440
|
+
} catch (err) {
|
|
7441
|
+
if (err instanceof ChannelAuthError) {
|
|
7442
|
+
this.log({
|
|
7443
|
+
level: "error",
|
|
7444
|
+
message: `Session watcher aborted on auth failure for session ${sessionId.slice(0, 8)} \u2014 clearing in-flight state for re-drive after re-auth: ${err.message}`,
|
|
7445
|
+
conversation_id: watcher.conv.id
|
|
7446
|
+
});
|
|
7447
|
+
for (const evidentMessageId of [...watcher.inFlight.keys()]) {
|
|
7448
|
+
this.readopted.delete(evidentMessageId);
|
|
6836
7449
|
this.removeInFlight(watcher, evidentMessageId);
|
|
6837
7450
|
}
|
|
6838
7451
|
return;
|
|
@@ -6965,6 +7578,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6965
7578
|
return;
|
|
6966
7579
|
}
|
|
6967
7580
|
inFlight.done = true;
|
|
7581
|
+
await this.reportSubagentAuthFailures(
|
|
7582
|
+
watcher.conv.id,
|
|
7583
|
+
inFlight.opencodeMessageId,
|
|
7584
|
+
inFlight.evidentMessageId,
|
|
7585
|
+
messages
|
|
7586
|
+
);
|
|
6968
7587
|
}
|
|
6969
7588
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
6970
7589
|
return;
|
|
@@ -7207,6 +7826,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7207
7826
|
return;
|
|
7208
7827
|
}
|
|
7209
7828
|
inFlight.done = true;
|
|
7829
|
+
await this.reportSubagentAuthFailures(
|
|
7830
|
+
watcher.conv.id,
|
|
7831
|
+
inFlight.opencodeMessageId,
|
|
7832
|
+
inFlight.evidentMessageId,
|
|
7833
|
+
messages
|
|
7834
|
+
);
|
|
7210
7835
|
}
|
|
7211
7836
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7212
7837
|
}
|
|
@@ -7377,6 +8002,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7377
8002
|
});
|
|
7378
8003
|
return;
|
|
7379
8004
|
}
|
|
8005
|
+
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
7380
8006
|
this.dontRedispatch.delete(row.id);
|
|
7381
8007
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
7382
8008
|
return;
|
|
@@ -7538,6 +8164,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7538
8164
|
});
|
|
7539
8165
|
return;
|
|
7540
8166
|
}
|
|
8167
|
+
if (ocId !== null) {
|
|
8168
|
+
await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
|
|
8169
|
+
}
|
|
7541
8170
|
this.dontRedispatch.delete(row.id);
|
|
7542
8171
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
7543
8172
|
}
|
|
@@ -7631,14 +8260,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7631
8260
|
this.unconfirmedDispatchFailures.delete(row.id);
|
|
7632
8261
|
this.sessions.delete(readoptConv.id);
|
|
7633
8262
|
this.supersede(readoptConv.id, sessionId);
|
|
7634
|
-
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.`;
|
|
7635
8264
|
this.log({
|
|
7636
8265
|
level: "error",
|
|
7637
|
-
message:
|
|
8266
|
+
message: errorMessage3,
|
|
7638
8267
|
conversation_id: row.conversation_id,
|
|
7639
8268
|
message_id: row.id
|
|
7640
8269
|
});
|
|
7641
|
-
await this.markFailed(row.conversation_id, row.id, null,
|
|
8270
|
+
await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
|
|
7642
8271
|
this.log({
|
|
7643
8272
|
level: "warn",
|
|
7644
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)}`,
|
|
@@ -8253,6 +8882,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8253
8882
|
throw new Error(`Failed to get pending conversations: HTTP ${res.status}`);
|
|
8254
8883
|
}
|
|
8255
8884
|
const data = await res.json();
|
|
8885
|
+
this.recycleRequestedFlag = data.recycle_requested === true;
|
|
8256
8886
|
let conversations = data.conversations;
|
|
8257
8887
|
if (this.conversationFilter) {
|
|
8258
8888
|
conversations = conversations.filter((c) => c.id === this.conversationFilter);
|
|
@@ -8488,6 +9118,111 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8488
9118
|
reply?.info?.modelID ?? null
|
|
8489
9119
|
);
|
|
8490
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
|
+
}
|
|
8491
9226
|
/**
|
|
8492
9227
|
* Best-effort, SINGLE-ATTEMPT runner→server telemetry ping
|
|
8493
9228
|
* (queued-followup-redrive). `POST .../messages/:id/signal {signal, ...extra}`
|
|
@@ -8641,6 +9376,13 @@ import chalk5 from "chalk";
|
|
|
8641
9376
|
import ora2 from "ora";
|
|
8642
9377
|
import { select as select2 } from "@inquirer/prompts";
|
|
8643
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
|
+
}
|
|
8644
9386
|
async function ensureOpenCodeRunning(ctx) {
|
|
8645
9387
|
const healthCheck = await checkOpenCodeHealth(ctx.port);
|
|
8646
9388
|
if (healthCheck.healthy) {
|
|
@@ -8688,6 +9430,7 @@ async function ensureOpenCodeRunning(ctx) {
|
|
|
8688
9430
|
}
|
|
8689
9431
|
}
|
|
8690
9432
|
if (!ctx.interactive) {
|
|
9433
|
+
checkNonInteractivePortConflict(ctx.port, isPortInUse);
|
|
8691
9434
|
ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
|
|
8692
9435
|
const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
|
|
8693
9436
|
const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
|
|
@@ -8769,9 +9512,119 @@ Port ${port} is already in use.`));
|
|
|
8769
9512
|
return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
|
|
8770
9513
|
}
|
|
8771
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
|
+
|
|
8772
9625
|
// src/lib/runner-credentials.ts
|
|
8773
|
-
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
8774
|
-
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";
|
|
8775
9628
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
8776
9629
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
8777
9630
|
var GITHUB_PROBE_TIMEOUT_MS = 1e4;
|
|
@@ -9043,11 +9896,11 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
9043
9896
|
}
|
|
9044
9897
|
|
|
9045
9898
|
// src/lib/opencode/config-overlay.ts
|
|
9046
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
9047
|
-
import { copyFileSync, existsSync as existsSync2, statSync as
|
|
9048
|
-
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";
|
|
9049
9902
|
function isFile(filePath) {
|
|
9050
|
-
return existsSync2(filePath) &&
|
|
9903
|
+
return existsSync2(filePath) && statSync6(filePath).isFile();
|
|
9051
9904
|
}
|
|
9052
9905
|
function applyRunnerOpenCodeConfig({
|
|
9053
9906
|
overlayPath,
|
|
@@ -9059,7 +9912,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9059
9912
|
return;
|
|
9060
9913
|
}
|
|
9061
9914
|
const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
|
|
9062
|
-
const target = isFile(
|
|
9915
|
+
const target = isFile(join9(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
9063
9916
|
if (!isFile(source)) {
|
|
9064
9917
|
log3(
|
|
9065
9918
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -9067,7 +9920,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9067
9920
|
);
|
|
9068
9921
|
return;
|
|
9069
9922
|
}
|
|
9070
|
-
copyFileSync(source,
|
|
9923
|
+
copyFileSync(source, join9(cwd, target));
|
|
9071
9924
|
try {
|
|
9072
9925
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
9073
9926
|
stdio: "ignore"
|
|
@@ -9076,7 +9929,242 @@ function applyRunnerOpenCodeConfig({
|
|
|
9076
9929
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
9077
9930
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
9078
9931
|
}
|
|
9079
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
9932
|
+
log3(`Applied runner OpenCode config ${source} to ${join9(cwd, target)}`);
|
|
9933
|
+
}
|
|
9934
|
+
|
|
9935
|
+
// src/lib/credential-sync.ts
|
|
9936
|
+
import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
9937
|
+
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
9938
|
+
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
9939
|
+
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
9940
|
+
var DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS = 60;
|
|
9941
|
+
var STORES = ["claude", "opencode"];
|
|
9942
|
+
var MAX_FLUSH_PASSES = 2;
|
|
9943
|
+
function outcomesWith(outcome) {
|
|
9944
|
+
return { claude: outcome, opencode: outcome };
|
|
9945
|
+
}
|
|
9946
|
+
function errorMessage2(error2) {
|
|
9947
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
9948
|
+
}
|
|
9949
|
+
function waitForSettlement(promise, timeoutMs) {
|
|
9950
|
+
return new Promise((resolve4) => {
|
|
9951
|
+
let settled = false;
|
|
9952
|
+
const timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
|
|
9953
|
+
const finish = (value) => {
|
|
9954
|
+
if (settled) return;
|
|
9955
|
+
settled = true;
|
|
9956
|
+
clearTimeout(timer);
|
|
9957
|
+
resolve4(value);
|
|
9958
|
+
};
|
|
9959
|
+
promise.then(
|
|
9960
|
+
() => finish(true),
|
|
9961
|
+
() => finish(true)
|
|
9962
|
+
);
|
|
9963
|
+
});
|
|
9964
|
+
}
|
|
9965
|
+
function writeMarker(markerPath, outcomes, log3) {
|
|
9966
|
+
const body = `${STORES.map((store) => `${store}=${outcomes[store]}`).join("\n")}
|
|
9967
|
+
`;
|
|
9968
|
+
const temporaryPath = `${markerPath}.tmp`;
|
|
9969
|
+
try {
|
|
9970
|
+
writeFileSync5(temporaryPath, body, { mode: 384 });
|
|
9971
|
+
renameSync(temporaryPath, markerPath);
|
|
9972
|
+
} catch (error2) {
|
|
9973
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
|
|
9974
|
+
}
|
|
9975
|
+
}
|
|
9976
|
+
function intervalSeconds(env, log3) {
|
|
9977
|
+
const raw = env.CREDS_SYNC_INTERVAL;
|
|
9978
|
+
if (raw === void 0 || /^[1-9][0-9]*$/.test(raw) && Number.isSafeInteger(Number(raw))) {
|
|
9979
|
+
return raw === void 0 ? DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS : Number(raw);
|
|
9980
|
+
}
|
|
9981
|
+
log3(
|
|
9982
|
+
`CREDENTIAL-SYNC-INTERVAL-INVALID: CREDS_SYNC_INTERVAL='${raw}' is not a positive integer; using ${DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS}s`,
|
|
9983
|
+
"warn"
|
|
9984
|
+
);
|
|
9985
|
+
return DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS;
|
|
9986
|
+
}
|
|
9987
|
+
async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
9988
|
+
const remainingMs = deadlineAt - Date.now();
|
|
9989
|
+
if (remainingMs <= 0) return { outcome: "timeout", orphaned: false };
|
|
9990
|
+
const controller = new AbortController();
|
|
9991
|
+
let result;
|
|
9992
|
+
let failed = false;
|
|
9993
|
+
const completion = Promise.resolve().then(
|
|
9994
|
+
() => synchroniserRunner(["sync-once", store], {
|
|
9995
|
+
timeoutMs: remainingMs,
|
|
9996
|
+
env,
|
|
9997
|
+
signal: controller.signal
|
|
9998
|
+
})
|
|
9999
|
+
).then(
|
|
10000
|
+
(value) => {
|
|
10001
|
+
result = value;
|
|
10002
|
+
},
|
|
10003
|
+
(error2) => {
|
|
10004
|
+
failed = true;
|
|
10005
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
|
|
10006
|
+
}
|
|
10007
|
+
);
|
|
10008
|
+
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
10009
|
+
const settledBeforeDeadline = await waitForSettlement(completion, remainingMs);
|
|
10010
|
+
clearTimeout(abortTimer);
|
|
10011
|
+
if (!settledBeforeDeadline) {
|
|
10012
|
+
controller.abort();
|
|
10013
|
+
const settledAfterAbort = await waitForSettlement(completion, CREDENTIAL_FLUSH_ABORT_GRACE_MS);
|
|
10014
|
+
if (!settledAfterAbort) return { outcome: "timeout", orphaned: true };
|
|
10015
|
+
return { outcome: "timeout", orphaned: false };
|
|
10016
|
+
}
|
|
10017
|
+
if (failed || !result) return { outcome: "failed", orphaned: false };
|
|
10018
|
+
if (result.timedOut || Date.now() >= deadlineAt) {
|
|
10019
|
+
return { outcome: "timeout", orphaned: false };
|
|
10020
|
+
}
|
|
10021
|
+
return { outcome: result.code === 0 ? "ok" : "failed", orphaned: false };
|
|
10022
|
+
}
|
|
10023
|
+
function createCredentialSync({
|
|
10024
|
+
markerPath,
|
|
10025
|
+
env,
|
|
10026
|
+
log: log3,
|
|
10027
|
+
synchroniserRunner = runSynchroniser
|
|
10028
|
+
}) {
|
|
10029
|
+
const persistenceDisabled = !env.PERSISTENCE_BUCKET;
|
|
10030
|
+
let disabled = persistenceDisabled;
|
|
10031
|
+
let armed = false;
|
|
10032
|
+
let stopped = false;
|
|
10033
|
+
let timer;
|
|
10034
|
+
let inFlight;
|
|
10035
|
+
let activeTickAbort;
|
|
10036
|
+
let lastTickFailed;
|
|
10037
|
+
let flushPromise;
|
|
10038
|
+
const scheduleTick = (intervalMs, startTick2) => {
|
|
10039
|
+
if (stopped) return;
|
|
10040
|
+
timer = setTimeout(() => {
|
|
10041
|
+
timer = void 0;
|
|
10042
|
+
startTick2();
|
|
10043
|
+
}, intervalMs);
|
|
10044
|
+
};
|
|
10045
|
+
const startTick = (intervalMs) => {
|
|
10046
|
+
if (stopped) return;
|
|
10047
|
+
const controller = new AbortController();
|
|
10048
|
+
activeTickAbort = controller;
|
|
10049
|
+
const tick = (async () => {
|
|
10050
|
+
const outcomes = {
|
|
10051
|
+
claude: "failed",
|
|
10052
|
+
opencode: "failed"
|
|
10053
|
+
};
|
|
10054
|
+
for (const store of STORES) {
|
|
10055
|
+
if (controller.signal.aborted) break;
|
|
10056
|
+
try {
|
|
10057
|
+
const result = await synchroniserRunner(["sync-once", store], {
|
|
10058
|
+
timeoutMs: CREDENTIAL_SYNC_TICK_TIMEOUT_MS,
|
|
10059
|
+
env,
|
|
10060
|
+
signal: controller.signal
|
|
10061
|
+
});
|
|
10062
|
+
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
10063
|
+
} catch (error2) {
|
|
10064
|
+
outcomes[store] = "failed";
|
|
10065
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
|
|
10066
|
+
}
|
|
10067
|
+
}
|
|
10068
|
+
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
10069
|
+
log3(
|
|
10070
|
+
`CREDENTIAL-SYNC-TICK: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
|
|
10071
|
+
"debug"
|
|
10072
|
+
);
|
|
10073
|
+
if (failed && lastTickFailed !== true) {
|
|
10074
|
+
log3(
|
|
10075
|
+
"CREDENTIAL-SYNC-FAILED: an interval credential sync failed; retrying on the next tick",
|
|
10076
|
+
"warn"
|
|
10077
|
+
);
|
|
10078
|
+
} else if (!failed && lastTickFailed === true) {
|
|
10079
|
+
log3("CREDENTIAL-SYNC-RECOVERED: interval credential sync succeeded again", "warn");
|
|
10080
|
+
}
|
|
10081
|
+
lastTickFailed = failed;
|
|
10082
|
+
})().finally(() => {
|
|
10083
|
+
if (activeTickAbort === controller) activeTickAbort = void 0;
|
|
10084
|
+
if (inFlight === tick) inFlight = void 0;
|
|
10085
|
+
scheduleTick(intervalMs, () => startTick(intervalMs));
|
|
10086
|
+
});
|
|
10087
|
+
inFlight = tick;
|
|
10088
|
+
};
|
|
10089
|
+
const performFlush = async () => {
|
|
10090
|
+
stopped = true;
|
|
10091
|
+
if (timer) {
|
|
10092
|
+
clearTimeout(timer);
|
|
10093
|
+
timer = void 0;
|
|
10094
|
+
}
|
|
10095
|
+
const deadlineAt = Date.now() + CREDENTIAL_FLUSH_DEADLINE_MS;
|
|
10096
|
+
if (inFlight) {
|
|
10097
|
+
const settled = await waitForSettlement(inFlight, CREDENTIAL_FLUSH_DEADLINE_MS);
|
|
10098
|
+
if (!settled) {
|
|
10099
|
+
activeTickAbort?.abort();
|
|
10100
|
+
const settledAfterAbort = await waitForSettlement(
|
|
10101
|
+
inFlight,
|
|
10102
|
+
CREDENTIAL_FLUSH_ABORT_GRACE_MS
|
|
10103
|
+
);
|
|
10104
|
+
if (!settledAfterAbort) {
|
|
10105
|
+
log3(
|
|
10106
|
+
"CREDENTIAL-FLUSH-ORPHANED-TICK: a sync-once child could not be proven settled; skipping the boundary flush rather than becoming a second writer",
|
|
10107
|
+
"warn"
|
|
10108
|
+
);
|
|
10109
|
+
return { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
10110
|
+
}
|
|
10111
|
+
}
|
|
10112
|
+
}
|
|
10113
|
+
if (disabled) return { outcomes: outcomesWith("skipped"), orphaned: false };
|
|
10114
|
+
const outcomes = outcomesWith("timeout");
|
|
10115
|
+
for (const store of STORES) {
|
|
10116
|
+
const result = await runFlushStore(store, env, synchroniserRunner, log3, deadlineAt);
|
|
10117
|
+
if (result.orphaned) {
|
|
10118
|
+
log3(
|
|
10119
|
+
"CREDENTIAL-FLUSH-ORPHANED-CHILD: a flush sync-once child could not be proven settled; refusing another flush pass rather than becoming a second writer",
|
|
10120
|
+
"warn"
|
|
10121
|
+
);
|
|
10122
|
+
return { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
10123
|
+
}
|
|
10124
|
+
outcomes[store] = result.outcome;
|
|
10125
|
+
}
|
|
10126
|
+
return { outcomes, orphaned: false };
|
|
10127
|
+
};
|
|
10128
|
+
let flushPasses = 0;
|
|
10129
|
+
let lastFlush;
|
|
10130
|
+
return {
|
|
10131
|
+
arm() {
|
|
10132
|
+
if (stopped || armed) return;
|
|
10133
|
+
armed = true;
|
|
10134
|
+
if (persistenceDisabled) {
|
|
10135
|
+
disabled = true;
|
|
10136
|
+
log3(
|
|
10137
|
+
"CREDENTIAL-SYNC-DISABLED: LITESTREAM_BUCKET/LITESTREAM_PREFIX are not both set; no interval credential sync this boot",
|
|
10138
|
+
"warn"
|
|
10139
|
+
);
|
|
10140
|
+
return;
|
|
10141
|
+
}
|
|
10142
|
+
disabled = false;
|
|
10143
|
+
const intervalMs = intervalSeconds(env, log3) * 1e3;
|
|
10144
|
+
scheduleTick(intervalMs, () => startTick(intervalMs));
|
|
10145
|
+
},
|
|
10146
|
+
async stopAndFlush(publish) {
|
|
10147
|
+
let result;
|
|
10148
|
+
const runningFlush = flushPromise;
|
|
10149
|
+
if (runningFlush) {
|
|
10150
|
+
result = await runningFlush;
|
|
10151
|
+
} else if (flushPasses >= MAX_FLUSH_PASSES || lastFlush?.orphaned) {
|
|
10152
|
+
result = lastFlush ?? { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
10153
|
+
} else {
|
|
10154
|
+
flushPasses++;
|
|
10155
|
+
const currentFlush = performFlush();
|
|
10156
|
+
flushPromise = currentFlush;
|
|
10157
|
+
try {
|
|
10158
|
+
result = await currentFlush;
|
|
10159
|
+
lastFlush = result;
|
|
10160
|
+
} finally {
|
|
10161
|
+
if (flushPromise === currentFlush) flushPromise = void 0;
|
|
10162
|
+
}
|
|
10163
|
+
}
|
|
10164
|
+
if (publish) writeMarker(markerPath, result.outcomes, log3);
|
|
10165
|
+
return result.outcomes;
|
|
10166
|
+
}
|
|
10167
|
+
};
|
|
9080
10168
|
}
|
|
9081
10169
|
|
|
9082
10170
|
// src/commands/run.ts
|
|
@@ -9116,7 +10204,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9116
10204
|
if (trimmed === "") {
|
|
9117
10205
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
9118
10206
|
}
|
|
9119
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
10207
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join10(homeDir, trimmed.slice(2)) : trimmed;
|
|
9120
10208
|
if (!isAbsolute3(expanded)) {
|
|
9121
10209
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
9122
10210
|
}
|
|
@@ -9140,6 +10228,28 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9140
10228
|
var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
|
|
9141
10229
|
var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
|
|
9142
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
|
+
}
|
|
9143
10253
|
function resolveOpenCodeStartTimeoutMs(options, env = process.env) {
|
|
9144
10254
|
const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1e3;
|
|
9145
10255
|
let raw;
|
|
@@ -9206,7 +10316,7 @@ function log2(state, message, level = "info") {
|
|
|
9206
10316
|
})
|
|
9207
10317
|
);
|
|
9208
10318
|
} else if (!state.interactive) {
|
|
9209
|
-
const prefix = level === "error" ?
|
|
10319
|
+
const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
|
|
9210
10320
|
console.log(`${prefix} ${message}`);
|
|
9211
10321
|
}
|
|
9212
10322
|
}
|
|
@@ -9236,7 +10346,7 @@ function logActivity(state, entry) {
|
|
|
9236
10346
|
}
|
|
9237
10347
|
function reportSessionDbRecovery(state) {
|
|
9238
10348
|
try {
|
|
9239
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
10349
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
|
|
9240
10350
|
for (const record of report.records) {
|
|
9241
10351
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
9242
10352
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -9267,18 +10377,18 @@ function reportSessionDbRecoveryRecord(state, record) {
|
|
|
9267
10377
|
function displayStatus(state) {
|
|
9268
10378
|
if (!state.interactive) return;
|
|
9269
10379
|
const attempt = state.connection?.reconnectAttempt ?? 0;
|
|
9270
|
-
const tunnel = state.connected ?
|
|
9271
|
-
const opencode = state.opencodeConnected ?
|
|
9272
|
-
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`) : "";
|
|
9273
10383
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
9274
|
-
const detail = last ?
|
|
10384
|
+
const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
|
|
9275
10385
|
const agent = state.agentName ?? state.agentId;
|
|
9276
10386
|
console.log(
|
|
9277
|
-
`${
|
|
10387
|
+
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
9278
10388
|
);
|
|
9279
10389
|
}
|
|
9280
10390
|
async function promptForLogin(promptMessage, successMessage) {
|
|
9281
|
-
const action = await
|
|
10391
|
+
const action = await select4({
|
|
9282
10392
|
message: promptMessage,
|
|
9283
10393
|
choices: [
|
|
9284
10394
|
{
|
|
@@ -9294,7 +10404,7 @@ async function promptForLogin(promptMessage, successMessage) {
|
|
|
9294
10404
|
]
|
|
9295
10405
|
});
|
|
9296
10406
|
if (action === "exit") {
|
|
9297
|
-
console.log(
|
|
10407
|
+
console.log(chalk7.dim(`
|
|
9298
10408
|
You can log in later by running: ${getCliName()} login`));
|
|
9299
10409
|
process.exit(0);
|
|
9300
10410
|
}
|
|
@@ -9305,7 +10415,7 @@ You can log in later by running: ${getCliName()} login`));
|
|
|
9305
10415
|
process.exit(1);
|
|
9306
10416
|
}
|
|
9307
10417
|
blank();
|
|
9308
|
-
console.log(
|
|
10418
|
+
console.log(chalk7.green(successMessage));
|
|
9309
10419
|
blank();
|
|
9310
10420
|
return { token: credentials2.token, authType: "bearer", user: credentials2.user };
|
|
9311
10421
|
}
|
|
@@ -9318,12 +10428,12 @@ async function handleAuthError(state, error2) {
|
|
|
9318
10428
|
if (state.interactive) displayStatus(state);
|
|
9319
10429
|
if (!state.interactive) {
|
|
9320
10430
|
blank();
|
|
9321
|
-
console.log(
|
|
9322
|
-
console.log(
|
|
10431
|
+
console.log(chalk7.red("Authentication expired"));
|
|
10432
|
+
console.log(chalk7.dim("Your authentication token is no longer valid."));
|
|
9323
10433
|
blank();
|
|
9324
|
-
console.log(
|
|
9325
|
-
console.log(
|
|
9326
|
-
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"));
|
|
9327
10437
|
blank();
|
|
9328
10438
|
await cleanup(state);
|
|
9329
10439
|
await shutdownTelemetry();
|
|
@@ -9331,7 +10441,7 @@ async function handleAuthError(state, error2) {
|
|
|
9331
10441
|
return { success: false };
|
|
9332
10442
|
}
|
|
9333
10443
|
blank();
|
|
9334
|
-
console.log(
|
|
10444
|
+
console.log(chalk7.yellow("Your authentication has expired."));
|
|
9335
10445
|
blank();
|
|
9336
10446
|
try {
|
|
9337
10447
|
const credentials2 = await promptForLogin(
|
|
@@ -9376,6 +10486,10 @@ async function driveChannels(state, driver) {
|
|
|
9376
10486
|
consecutiveDrainFailures = 0;
|
|
9377
10487
|
unreachableMs = 0;
|
|
9378
10488
|
state.messageCount += processed;
|
|
10489
|
+
if (driver.recycleRequested) {
|
|
10490
|
+
await beginGracefulShutdown(state, "recycle");
|
|
10491
|
+
return;
|
|
10492
|
+
}
|
|
9379
10493
|
const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
|
|
9380
10494
|
lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
|
|
9381
10495
|
const fileActivitySnapshot = driver.fileSyncActivity();
|
|
@@ -9391,6 +10505,14 @@ async function driveChannels(state, driver) {
|
|
|
9391
10505
|
const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
|
|
9392
10506
|
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
9393
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
|
+
}
|
|
9394
10516
|
if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
|
|
9395
10517
|
idlePolls = 0;
|
|
9396
10518
|
idleMs = 0;
|
|
@@ -9418,8 +10540,8 @@ async function driveChannels(state, driver) {
|
|
|
9418
10540
|
state.running = false;
|
|
9419
10541
|
break;
|
|
9420
10542
|
}
|
|
9421
|
-
const
|
|
9422
|
-
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}` });
|
|
9423
10545
|
if (state.interactive) displayStatus(state);
|
|
9424
10546
|
if (driver.hasInFlightWatchers()) {
|
|
9425
10547
|
consecutiveDrainFailures = 0;
|
|
@@ -9457,9 +10579,18 @@ async function driveChannels(state, driver) {
|
|
|
9457
10579
|
}
|
|
9458
10580
|
}
|
|
9459
10581
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
9460
|
-
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
|
+
}
|
|
9461
10592
|
function sessionDbPath() {
|
|
9462
|
-
return
|
|
10593
|
+
return join10(homedir6(), ".local", "share", "opencode", "opencode.db");
|
|
9463
10594
|
}
|
|
9464
10595
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
9465
10596
|
const record = {
|
|
@@ -9555,7 +10686,7 @@ async function runSweep(state, driver, config) {
|
|
|
9555
10686
|
} else {
|
|
9556
10687
|
logActivity(state, {
|
|
9557
10688
|
type: "info",
|
|
9558
|
-
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}` : "")
|
|
9559
10690
|
});
|
|
9560
10691
|
}
|
|
9561
10692
|
} catch (error2) {
|
|
@@ -9578,13 +10709,20 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
9578
10709
|
for (const warning2 of config.warnings) {
|
|
9579
10710
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
9580
10711
|
}
|
|
9581
|
-
const dbBytes = statSessionDbBytes(
|
|
10712
|
+
const dbBytes = statSessionDbBytes(homedir6());
|
|
9582
10713
|
void (async () => {
|
|
9583
|
-
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
|
+
}
|
|
9584
10722
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
9585
10723
|
dbBytes,
|
|
9586
10724
|
cleanupEnabled: config.enabled,
|
|
9587
|
-
reclaimSkipReason
|
|
10725
|
+
reclaimSkipReason: reclaimAvailability?.reason ?? null
|
|
9588
10726
|
});
|
|
9589
10727
|
if (sizeWarning !== null) {
|
|
9590
10728
|
logActivity(state, { type: "info", level: "warn", message: sizeWarning });
|
|
@@ -9779,7 +10917,8 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
9779
10917
|
});
|
|
9780
10918
|
return;
|
|
9781
10919
|
}
|
|
9782
|
-
const collect = createResourceUsageCollector(
|
|
10920
|
+
const { collect, stop } = createResourceUsageCollector(homedir6());
|
|
10921
|
+
state.stopResourceUsageSampling = stop;
|
|
9783
10922
|
let consecutiveFailures = 0;
|
|
9784
10923
|
const tick = async () => {
|
|
9785
10924
|
try {
|
|
@@ -9875,6 +11014,8 @@ async function cleanup(state, opts = {}) {
|
|
|
9875
11014
|
clearTimeout(timer);
|
|
9876
11015
|
}
|
|
9877
11016
|
state.sessionCleanupTimers = [];
|
|
11017
|
+
state.stopOpenCodeLogTail?.();
|
|
11018
|
+
state.stopOpenCodeLogTail = null;
|
|
9878
11019
|
if (state.claudeUsageTimer) {
|
|
9879
11020
|
clearTimeout(state.claudeUsageTimer);
|
|
9880
11021
|
state.claudeUsageTimer = null;
|
|
@@ -9889,21 +11030,41 @@ async function cleanup(state, opts = {}) {
|
|
|
9889
11030
|
clearTimeout(state.resourceUsageTimer);
|
|
9890
11031
|
state.resourceUsageTimer = null;
|
|
9891
11032
|
}
|
|
11033
|
+
state.stopResourceUsageSampling?.();
|
|
11034
|
+
state.stopResourceUsageSampling = null;
|
|
11035
|
+
const credentialSync = state.credentialSync;
|
|
11036
|
+
const flushCredentials = credentialSync ? async (phase, publish) => {
|
|
11037
|
+
await timeShutdownPhase(state, durations, phase, async () => {
|
|
11038
|
+
const outcomes = await credentialSync.stopAndFlush(publish);
|
|
11039
|
+
const level = Object.values(outcomes).every((outcome) => outcome === "ok") ? "info" : "warn";
|
|
11040
|
+
log2(
|
|
11041
|
+
state,
|
|
11042
|
+
`Credential flush: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
|
|
11043
|
+
level
|
|
11044
|
+
);
|
|
11045
|
+
});
|
|
11046
|
+
} : void 0;
|
|
11047
|
+
let drainSettled = true;
|
|
9892
11048
|
if (opts.graceful && state.channelDriver) {
|
|
9893
11049
|
state.channelDriver.stop();
|
|
11050
|
+
}
|
|
11051
|
+
if (flushCredentials) {
|
|
11052
|
+
await flushCredentials("credential_flush", !(opts.graceful && state.channelDriver));
|
|
11053
|
+
}
|
|
11054
|
+
if (opts.graceful && state.channelDriver) {
|
|
9894
11055
|
log2(state, "Draining in-flight channel work before shutdown...");
|
|
9895
11056
|
if (state.interactive) {
|
|
9896
11057
|
logActivity(state, { type: "info", message: "Draining in-flight work before shutdown..." });
|
|
9897
11058
|
displayStatus(state);
|
|
9898
11059
|
}
|
|
9899
11060
|
const driver = state.channelDriver;
|
|
9900
|
-
|
|
11061
|
+
drainSettled = await timeShutdownPhase(
|
|
9901
11062
|
state,
|
|
9902
11063
|
durations,
|
|
9903
11064
|
"drain",
|
|
9904
11065
|
() => driver.waitForInFlight(SHUTDOWN_DRAIN_TIMEOUT_MS)
|
|
9905
11066
|
);
|
|
9906
|
-
if (!
|
|
11067
|
+
if (!drainSettled) {
|
|
9907
11068
|
logActivity(state, {
|
|
9908
11069
|
type: "info",
|
|
9909
11070
|
message: "Shutdown drain timed out with work still in flight \u2014 leaving it for restart recovery"
|
|
@@ -9911,6 +11072,9 @@ async function cleanup(state, opts = {}) {
|
|
|
9911
11072
|
if (state.interactive) displayStatus(state);
|
|
9912
11073
|
}
|
|
9913
11074
|
}
|
|
11075
|
+
if (opts.graceful && state.channelDriver && flushCredentials && drainSettled) {
|
|
11076
|
+
await flushCredentials("credential_flush_final", true);
|
|
11077
|
+
}
|
|
9914
11078
|
await timeShutdownPhase(state, durations, "offline_notify", () => notifyOffline(state));
|
|
9915
11079
|
if (state.connection) {
|
|
9916
11080
|
const connection = state.connection;
|
|
@@ -9946,13 +11110,51 @@ async function cleanup(state, opts = {}) {
|
|
|
9946
11110
|
}
|
|
9947
11111
|
return durations;
|
|
9948
11112
|
}
|
|
11113
|
+
async function beginGracefulShutdown(state, trigger) {
|
|
11114
|
+
if (state.shuttingDown) return;
|
|
11115
|
+
state.shuttingDown = true;
|
|
11116
|
+
const shutdownStartedAt = Date.now();
|
|
11117
|
+
const shutdownMessage = trigger === "recycle" ? "Recycle requested \u2014 finishing in-flight work, then exiting" : "Shutting down...";
|
|
11118
|
+
if (state.interactive) {
|
|
11119
|
+
logActivity(state, { type: "info", message: shutdownMessage });
|
|
11120
|
+
displayStatus(state);
|
|
11121
|
+
} else {
|
|
11122
|
+
log2(state, shutdownMessage);
|
|
11123
|
+
}
|
|
11124
|
+
const durations = await cleanup(state, { graceful: true });
|
|
11125
|
+
const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
|
|
11126
|
+
await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
|
|
11127
|
+
let timer;
|
|
11128
|
+
const flushed = shutdownTelemetry().then(
|
|
11129
|
+
() => true,
|
|
11130
|
+
(error2) => {
|
|
11131
|
+
log2(
|
|
11132
|
+
state,
|
|
11133
|
+
`Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
11134
|
+
"warn"
|
|
11135
|
+
);
|
|
11136
|
+
return true;
|
|
11137
|
+
}
|
|
11138
|
+
);
|
|
11139
|
+
const timedOut = new Promise((resolve4) => {
|
|
11140
|
+
timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
|
|
11141
|
+
});
|
|
11142
|
+
if (!await Promise.race([flushed, timedOut])) {
|
|
11143
|
+
log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
|
|
11144
|
+
}
|
|
11145
|
+
clearTimeout(timer);
|
|
11146
|
+
});
|
|
11147
|
+
const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
|
|
11148
|
+
log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
|
|
11149
|
+
process.exit(0);
|
|
11150
|
+
}
|
|
9949
11151
|
async function run(options) {
|
|
9950
11152
|
const interactive = isInteractive(options.json);
|
|
9951
11153
|
let logLevel;
|
|
9952
11154
|
let fileSyncDirectories;
|
|
9953
11155
|
try {
|
|
9954
11156
|
logLevel = resolveLogLevel(options);
|
|
9955
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
11157
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
|
|
9956
11158
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
9957
11159
|
throw new Error(
|
|
9958
11160
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -9983,6 +11185,7 @@ async function run(options) {
|
|
|
9983
11185
|
opencodeVersion: null,
|
|
9984
11186
|
sessionDbProvenanceAnomaly: false,
|
|
9985
11187
|
opencodeProcess: null,
|
|
11188
|
+
stopOpenCodeLogTail: null,
|
|
9986
11189
|
litestreamProcess: null,
|
|
9987
11190
|
connection: null,
|
|
9988
11191
|
channelDriver: null,
|
|
@@ -9997,9 +11200,24 @@ async function run(options) {
|
|
|
9997
11200
|
openaiUsageTimer: null,
|
|
9998
11201
|
openaiUsageRearm: null,
|
|
9999
11202
|
resourceUsageTimer: null,
|
|
11203
|
+
stopResourceUsageSampling: null,
|
|
11204
|
+
credentialSync: null,
|
|
10000
11205
|
authHeader: ""
|
|
10001
11206
|
};
|
|
10002
11207
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
11208
|
+
if (options.credentialSyncMarker) {
|
|
11209
|
+
state.credentialSync = createCredentialSync({
|
|
11210
|
+
markerPath: options.credentialSyncMarker,
|
|
11211
|
+
env: process.env,
|
|
11212
|
+
log: (message, level = "info") => {
|
|
11213
|
+
if (level === "error") {
|
|
11214
|
+
logActivity(state, { type: "error", error: message });
|
|
11215
|
+
} else {
|
|
11216
|
+
logActivity(state, { type: "info", level, message });
|
|
11217
|
+
}
|
|
11218
|
+
}
|
|
11219
|
+
});
|
|
11220
|
+
}
|
|
10003
11221
|
if (fileSyncDirectories.length > 0) {
|
|
10004
11222
|
log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
|
|
10005
11223
|
} else {
|
|
@@ -10025,43 +11243,7 @@ async function run(options) {
|
|
|
10025
11243
|
"warn"
|
|
10026
11244
|
);
|
|
10027
11245
|
}
|
|
10028
|
-
const handleSignal =
|
|
10029
|
-
if (state.shuttingDown) return;
|
|
10030
|
-
state.shuttingDown = true;
|
|
10031
|
-
const shutdownStartedAt = Date.now();
|
|
10032
|
-
if (state.interactive) {
|
|
10033
|
-
logActivity(state, { type: "info", message: "Shutting down..." });
|
|
10034
|
-
displayStatus(state);
|
|
10035
|
-
} else {
|
|
10036
|
-
log2(state, "Shutting down...");
|
|
10037
|
-
}
|
|
10038
|
-
const durations = await cleanup(state, { graceful: true });
|
|
10039
|
-
const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
|
|
10040
|
-
await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
|
|
10041
|
-
let timer;
|
|
10042
|
-
const flushed = shutdownTelemetry().then(
|
|
10043
|
-
() => true,
|
|
10044
|
-
(error2) => {
|
|
10045
|
-
log2(
|
|
10046
|
-
state,
|
|
10047
|
-
`Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
10048
|
-
"warn"
|
|
10049
|
-
);
|
|
10050
|
-
return true;
|
|
10051
|
-
}
|
|
10052
|
-
);
|
|
10053
|
-
const timedOut = new Promise((resolve4) => {
|
|
10054
|
-
timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
|
|
10055
|
-
});
|
|
10056
|
-
if (!await Promise.race([flushed, timedOut])) {
|
|
10057
|
-
log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
|
|
10058
|
-
}
|
|
10059
|
-
clearTimeout(timer);
|
|
10060
|
-
});
|
|
10061
|
-
const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
|
|
10062
|
-
log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
|
|
10063
|
-
process.exit(0);
|
|
10064
|
-
};
|
|
11246
|
+
const handleSignal = () => beginGracefulShutdown(state, "signal");
|
|
10065
11247
|
process.on("SIGINT", handleSignal);
|
|
10066
11248
|
process.on("SIGTERM", handleSignal);
|
|
10067
11249
|
try {
|
|
@@ -10071,15 +11253,15 @@ async function run(options) {
|
|
|
10071
11253
|
printError("Authentication required");
|
|
10072
11254
|
blank();
|
|
10073
11255
|
console.log(
|
|
10074
|
-
|
|
11256
|
+
chalk7.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
|
|
10075
11257
|
);
|
|
10076
|
-
console.log(
|
|
11258
|
+
console.log(chalk7.dim("Or run `evident login` for interactive authentication"));
|
|
10077
11259
|
blank();
|
|
10078
11260
|
process.exit(1);
|
|
10079
11261
|
return;
|
|
10080
11262
|
}
|
|
10081
11263
|
blank();
|
|
10082
|
-
console.log(
|
|
11264
|
+
console.log(chalk7.yellow("You are not logged in to Evident."));
|
|
10083
11265
|
blank();
|
|
10084
11266
|
credentials2 = await promptForLogin(
|
|
10085
11267
|
"Would you like to log in now?",
|
|
@@ -10129,7 +11311,7 @@ async function run(options) {
|
|
|
10129
11311
|
);
|
|
10130
11312
|
blank();
|
|
10131
11313
|
console.log(
|
|
10132
|
-
|
|
11314
|
+
chalk7.dim(
|
|
10133
11315
|
"Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
|
|
10134
11316
|
)
|
|
10135
11317
|
);
|
|
@@ -10152,15 +11334,15 @@ async function run(options) {
|
|
|
10152
11334
|
);
|
|
10153
11335
|
if (interactive && !state.json) {
|
|
10154
11336
|
blank();
|
|
10155
|
-
console.log(
|
|
10156
|
-
console.log(
|
|
11337
|
+
console.log(chalk7.bold("Evident Run"));
|
|
11338
|
+
console.log(chalk7.dim("-".repeat(40)));
|
|
10157
11339
|
}
|
|
10158
11340
|
const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
|
|
10159
11341
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
10160
11342
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
10161
11343
|
spinner?.fail("Authentication failed");
|
|
10162
11344
|
blank();
|
|
10163
|
-
console.log(
|
|
11345
|
+
console.log(chalk7.yellow("Your authentication token is invalid or expired."));
|
|
10164
11346
|
blank();
|
|
10165
11347
|
credentials2 = await promptForLogin(
|
|
10166
11348
|
"Would you like to log in again?",
|
|
@@ -10207,6 +11389,14 @@ async function run(options) {
|
|
|
10207
11389
|
await restoreCredentialStores(credentialContext);
|
|
10208
11390
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
10209
11391
|
}
|
|
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;
|
|
10210
11400
|
let sessionDbVerifyFatal = false;
|
|
10211
11401
|
if (!options.restoreSessionDb) {
|
|
10212
11402
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -10256,6 +11446,13 @@ async function run(options) {
|
|
|
10256
11446
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
10257
11447
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
10258
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
|
+
}
|
|
10259
11456
|
const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
|
|
10260
11457
|
for (const warning2 of maxActiveSessionsWarnings) {
|
|
10261
11458
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
@@ -10263,7 +11460,14 @@ async function run(options) {
|
|
|
10263
11460
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
10264
11461
|
const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
|
|
10265
11462
|
try {
|
|
10266
|
-
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({
|
|
10267
11471
|
port: state.port,
|
|
10268
11472
|
interactive: state.interactive,
|
|
10269
11473
|
agentId: state.agentId,
|
|
@@ -10276,7 +11480,7 @@ async function run(options) {
|
|
|
10276
11480
|
state.opencodeVersion = oc.version;
|
|
10277
11481
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
10278
11482
|
try {
|
|
10279
|
-
|
|
11483
|
+
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
10280
11484
|
`, { mode: 384 });
|
|
10281
11485
|
chmodSync3(options.opencodePidFile, 384);
|
|
10282
11486
|
} catch (error2) {
|
|
@@ -10290,7 +11494,7 @@ async function run(options) {
|
|
|
10290
11494
|
const provenance = checkSessionDbProvenance({
|
|
10291
11495
|
dbPath: sessionDbPath(),
|
|
10292
11496
|
currentVersion: state.opencodeVersion,
|
|
10293
|
-
homeDir:
|
|
11497
|
+
homeDir: homedir6(),
|
|
10294
11498
|
env: process.env
|
|
10295
11499
|
});
|
|
10296
11500
|
if (provenance.anomaly) {
|
|
@@ -10325,10 +11529,10 @@ async function run(options) {
|
|
|
10325
11529
|
if (state.interactive && !state.json) {
|
|
10326
11530
|
logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
|
|
10327
11531
|
blank();
|
|
10328
|
-
console.log(
|
|
11532
|
+
console.log(chalk7.yellow("\u26A0 No OpenCode model provider is configured."));
|
|
10329
11533
|
console.log(
|
|
10330
|
-
|
|
10331
|
-
`Run ${
|
|
11534
|
+
chalk7.dim(
|
|
11535
|
+
`Run ${chalk7.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
|
|
10332
11536
|
)
|
|
10333
11537
|
);
|
|
10334
11538
|
blank();
|
|
@@ -10392,7 +11596,7 @@ async function run(options) {
|
|
|
10392
11596
|
});
|
|
10393
11597
|
try {
|
|
10394
11598
|
if (litestreamProcess.pid !== void 0) {
|
|
10395
|
-
|
|
11599
|
+
writeFileSync6(options.litestreamPidFile, `${litestreamProcess.pid}
|
|
10396
11600
|
`, {
|
|
10397
11601
|
mode: 384
|
|
10398
11602
|
});
|
|
@@ -10452,7 +11656,7 @@ async function run(options) {
|
|
|
10452
11656
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
10453
11657
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
10454
11658
|
fileSyncDirectories,
|
|
10455
|
-
homeDir:
|
|
11659
|
+
homeDir: homedir6(),
|
|
10456
11660
|
maxActiveSessions,
|
|
10457
11661
|
log: (entry) => (
|
|
10458
11662
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -10578,6 +11782,18 @@ async function run(options) {
|
|
|
10578
11782
|
if (state.interactive) displayStatus(state);
|
|
10579
11783
|
});
|
|
10580
11784
|
},
|
|
11785
|
+
// Both loops are rearmed because `rearm()` is idempotent for the
|
|
11786
|
+
// provider that did not just connect, and is a no-op when reporting is off.
|
|
11787
|
+
onUsageRearmPing: () => {
|
|
11788
|
+
if (!state.running) return;
|
|
11789
|
+
logActivity(state, {
|
|
11790
|
+
type: "info",
|
|
11791
|
+
level: "debug",
|
|
11792
|
+
message: "Usage rearm ping received"
|
|
11793
|
+
});
|
|
11794
|
+
state.claudeUsageRearm?.();
|
|
11795
|
+
state.openaiUsageRearm?.();
|
|
11796
|
+
},
|
|
10581
11797
|
onInfo: (message) => logActivity(state, { type: "info", message })
|
|
10582
11798
|
}
|
|
10583
11799
|
});
|
|
@@ -10598,7 +11814,17 @@ async function run(options) {
|
|
|
10598
11814
|
setTimer: (timer) => {
|
|
10599
11815
|
state.openaiUsageTimer = timer;
|
|
10600
11816
|
},
|
|
10601
|
-
fetchUsage: () =>
|
|
11817
|
+
fetchUsage: async () => {
|
|
11818
|
+
const usage = await getOpenAiUsage(state.port);
|
|
11819
|
+
if (usage.subscription === null) {
|
|
11820
|
+
logActivity(state, {
|
|
11821
|
+
type: "info",
|
|
11822
|
+
level: "debug",
|
|
11823
|
+
message: "OpenAI usage subscription could not be identified from the local credential"
|
|
11824
|
+
});
|
|
11825
|
+
}
|
|
11826
|
+
return usage;
|
|
11827
|
+
},
|
|
10602
11828
|
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
10603
11829
|
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
10604
11830
|
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
@@ -10672,6 +11898,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10672
11898
|
).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(
|
|
10673
11899
|
"--opencode-start-timeout <seconds>",
|
|
10674
11900
|
"Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
|
|
11901
|
+
).option(
|
|
11902
|
+
"--opencode-version <v1|v2>",
|
|
11903
|
+
"Which OpenCode major version to launch: v1 or v2 (default: v1). Env: EVIDENT_OPENCODE_VERSION"
|
|
10675
11904
|
).option("--json", "Output in JSON format").option(
|
|
10676
11905
|
"--session-cleanup-max-age <duration>",
|
|
10677
11906
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -10722,6 +11951,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10722
11951
|
).option(
|
|
10723
11952
|
"--opencode-config-overlay <path>",
|
|
10724
11953
|
"Apply this runner-provided OpenCode config before starting OpenCode."
|
|
11954
|
+
).option(
|
|
11955
|
+
"--credential-sync-marker <path>",
|
|
11956
|
+
"Own the interval credential sync and write this marker once the shutdown flush has finished, so the runner image's lifecycle hooks can wait on it."
|
|
10725
11957
|
).action(
|
|
10726
11958
|
(options) => {
|
|
10727
11959
|
run({
|
|
@@ -10737,6 +11969,7 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10737
11969
|
// Raw string — validation/precedence is single-sourced in run.ts's
|
|
10738
11970
|
// resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).
|
|
10739
11971
|
opencodeStartTimeout: options.opencodeStartTimeout,
|
|
11972
|
+
opencodeVersion: options.opencodeVersion,
|
|
10740
11973
|
json: options.json,
|
|
10741
11974
|
// Raw strings — the resolver in run.ts single-sources parsing (M1).
|
|
10742
11975
|
sessionCleanupMaxAge: options.sessionCleanupMaxAge,
|
|
@@ -10760,7 +11993,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10760
11993
|
sessionDbNoReplicateMarker: options.sessionDbNoReplicateMarker,
|
|
10761
11994
|
restoreSessionDb: options.restoreSessionDb,
|
|
10762
11995
|
restoreRunnerCredentials: options.restoreRunnerCredentials,
|
|
10763
|
-
opencodeConfigOverlay: options.opencodeConfigOverlay
|
|
11996
|
+
opencodeConfigOverlay: options.opencodeConfigOverlay,
|
|
11997
|
+
credentialSyncMarker: options.credentialSyncMarker
|
|
10764
11998
|
});
|
|
10765
11999
|
}
|
|
10766
12000
|
);
|