@evident-ai/cli 3.4.1-dev.c03b461 → 3.4.1-dev.c2c5e11
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 +1931 -296
- 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,21 +3223,112 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
3119
3223
|
}
|
|
3120
3224
|
return lastOk ?? last;
|
|
3121
3225
|
}
|
|
3122
|
-
function
|
|
3123
|
-
if (!messages || messages.length === 0) return
|
|
3124
|
-
const
|
|
3125
|
-
(
|
|
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
|
|
3126
3230
|
);
|
|
3127
|
-
const
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
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
|
+
}
|
|
3135
3261
|
}
|
|
3136
|
-
|
|
3262
|
+
return refs;
|
|
3263
|
+
}
|
|
3264
|
+
function finiteNumber(value) {
|
|
3265
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3266
|
+
}
|
|
3267
|
+
function taskCallModel(value) {
|
|
3268
|
+
if (!value || typeof value !== "object") return null;
|
|
3269
|
+
const model = value;
|
|
3270
|
+
const modelID = typeof model.modelID === "string" ? model.modelID : void 0;
|
|
3271
|
+
const providerID = typeof model.providerID === "string" ? model.providerID : void 0;
|
|
3272
|
+
return modelID || providerID ? { modelID, providerID } : null;
|
|
3273
|
+
}
|
|
3274
|
+
function collectTaskCalls(messages, userMessageId) {
|
|
3275
|
+
if (!messages || messages.length === 0) return [];
|
|
3276
|
+
const calls = [];
|
|
3277
|
+
for (const message of messages) {
|
|
3278
|
+
if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
|
|
3279
|
+
for (const part of message.parts ?? []) {
|
|
3280
|
+
if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
|
|
3281
|
+
continue;
|
|
3282
|
+
}
|
|
3283
|
+
const rawName = part.state.input?.subagent_type;
|
|
3284
|
+
const subagentName = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : rawName === void 0 ? "general" : "unknown";
|
|
3285
|
+
const metadata = part.state.metadata;
|
|
3286
|
+
calls.push({
|
|
3287
|
+
callID: part.callID,
|
|
3288
|
+
subagentName,
|
|
3289
|
+
childSessionId: typeof metadata?.sessionId === "string" ? metadata.sessionId : null,
|
|
3290
|
+
parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
|
|
3291
|
+
model: taskCallModel(metadata?.model),
|
|
3292
|
+
status: part.state.status ?? "unknown",
|
|
3293
|
+
timeStart: finiteNumber(part.state.time?.start),
|
|
3294
|
+
timeEnd: finiteNumber(part.state.time?.end)
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
return calls;
|
|
3299
|
+
}
|
|
3300
|
+
function attributeTaskCallUsage(messages, windows) {
|
|
3301
|
+
const eligibleWindows = windows.filter(
|
|
3302
|
+
(window) => window.timeStart !== null && Number.isFinite(window.timeStart)
|
|
3303
|
+
);
|
|
3304
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
3305
|
+
for (const window of eligibleWindows) assignments.set(window.callID, []);
|
|
3306
|
+
const unattributed = [];
|
|
3307
|
+
for (const message of messages ?? []) {
|
|
3308
|
+
if (roleOf(message) !== "assistant") continue;
|
|
3309
|
+
const created = finiteNumber(createdOf(message));
|
|
3310
|
+
const matching = created === null ? [] : eligibleWindows.filter(
|
|
3311
|
+
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
3312
|
+
);
|
|
3313
|
+
if (matching.length === 0) {
|
|
3314
|
+
unattributed.push(message);
|
|
3315
|
+
continue;
|
|
3316
|
+
}
|
|
3317
|
+
matching.sort((a, b) => a.timeStart - b.timeStart);
|
|
3318
|
+
assignments.get(matching[0].callID)?.push(message);
|
|
3319
|
+
}
|
|
3320
|
+
return {
|
|
3321
|
+
invocations: eligibleWindows.map((window) => {
|
|
3322
|
+
const assigned = assignments.get(window.callID) ?? [];
|
|
3323
|
+
return { callID: window.callID, messages: assigned, usage: sumAssistantUsage(assigned) };
|
|
3324
|
+
}),
|
|
3325
|
+
unattributed
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
function sumAssistantUsage(messages) {
|
|
3329
|
+
if (!messages || messages.length === 0) return null;
|
|
3330
|
+
const nonErrored = messages.filter((message) => errorOf(message) == null);
|
|
3331
|
+
const selected = nonErrored.length > 0 ? nonErrored : messages;
|
|
3137
3332
|
let sawAnyUsage = false;
|
|
3138
3333
|
let inputSum = 0;
|
|
3139
3334
|
let outputSum = 0;
|
|
@@ -3144,7 +3339,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
3144
3339
|
let sawCost = false;
|
|
3145
3340
|
let modelId = null;
|
|
3146
3341
|
let providerId = null;
|
|
3147
|
-
for (const m of
|
|
3342
|
+
for (const m of selected) {
|
|
3148
3343
|
const info = m.info;
|
|
3149
3344
|
if (!info) continue;
|
|
3150
3345
|
const tokens = info.tokens;
|
|
@@ -3179,12 +3374,28 @@ function messageUsage(messages, userMessageId) {
|
|
|
3179
3374
|
usage_tokens_reasoning: reasoningSum,
|
|
3180
3375
|
usage_tokens_cache_read: cacheReadSum,
|
|
3181
3376
|
usage_tokens_cache_write: cacheWriteSum,
|
|
3182
|
-
// NULL means
|
|
3183
|
-
//
|
|
3184
|
-
// `sawCost` true with `costSum === 0`.
|
|
3377
|
+
// NULL means OpenCode never reported a cost; it is distinct from a genuine
|
|
3378
|
+
// zero-cost message, which sets `sawCost` with `costSum === 0`.
|
|
3185
3379
|
usage_cost_usd: sawCost ? costSum : null
|
|
3186
3380
|
};
|
|
3187
3381
|
}
|
|
3382
|
+
function messageUsage(messages, userMessageId) {
|
|
3383
|
+
if (!messages || messages.length === 0) return null;
|
|
3384
|
+
const byParentAll = messages.filter(
|
|
3385
|
+
(m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
|
|
3386
|
+
);
|
|
3387
|
+
const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
|
|
3388
|
+
const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
|
|
3389
|
+
let correlated;
|
|
3390
|
+
if (byParent.length > 0) {
|
|
3391
|
+
correlated = byParent;
|
|
3392
|
+
} else {
|
|
3393
|
+
const reply = findAssistantReplyAfter(messages, userMessageId);
|
|
3394
|
+
correlated = reply ? [reply] : [];
|
|
3395
|
+
}
|
|
3396
|
+
if (correlated.length === 0) return null;
|
|
3397
|
+
return sumAssistantUsage(correlated);
|
|
3398
|
+
}
|
|
3188
3399
|
function messageRunState(messages, userMessageId) {
|
|
3189
3400
|
if (!messages || messages.length === 0) return "unknown";
|
|
3190
3401
|
const hasUser = messages.some((m) => idOf(m) === userMessageId);
|
|
@@ -3247,8 +3458,7 @@ function isAbortedTerminalReply(messages, userMessageId) {
|
|
|
3247
3458
|
}
|
|
3248
3459
|
return false;
|
|
3249
3460
|
}
|
|
3250
|
-
function
|
|
3251
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
3461
|
+
function classifyReplyAuthError(reply) {
|
|
3252
3462
|
const error2 = errorOf(reply);
|
|
3253
3463
|
if (error2 == null || typeof error2 !== "object") return null;
|
|
3254
3464
|
const e = error2;
|
|
@@ -3273,6 +3483,32 @@ function messageFailure(messages, userMessageId) {
|
|
|
3273
3483
|
}
|
|
3274
3484
|
return null;
|
|
3275
3485
|
}
|
|
3486
|
+
function messageFailure(messages, userMessageId) {
|
|
3487
|
+
return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
|
|
3488
|
+
}
|
|
3489
|
+
function findLatestSubagentAuthOutcome(messages, sinceMs) {
|
|
3490
|
+
if (!messages || messages.length === 0) return null;
|
|
3491
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3492
|
+
const message = messages[i];
|
|
3493
|
+
if (roleOf(message) !== "assistant") continue;
|
|
3494
|
+
const created = createdOf(message);
|
|
3495
|
+
if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
|
|
3496
|
+
const failure = classifyReplyAuthError(message);
|
|
3497
|
+
if (failure) {
|
|
3498
|
+
if (!failure.providerId) return null;
|
|
3499
|
+
return { providerId: failure.providerId, outcome: "failed", failure };
|
|
3500
|
+
}
|
|
3501
|
+
const providerId = message.info?.providerID;
|
|
3502
|
+
if (errorOf(message) == null && typeof providerId === "string" && providerId.length > 0) {
|
|
3503
|
+
return { providerId, outcome: "succeeded" };
|
|
3504
|
+
}
|
|
3505
|
+
return null;
|
|
3506
|
+
}
|
|
3507
|
+
return null;
|
|
3508
|
+
}
|
|
3509
|
+
function findSubagentAuthOutcome(messages, sinceMs) {
|
|
3510
|
+
return findLatestSubagentAuthOutcome(messages, sinceMs);
|
|
3511
|
+
}
|
|
3276
3512
|
function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProviderId, replyModelId = null) {
|
|
3277
3513
|
if (classified != null) return classified;
|
|
3278
3514
|
if (hasConfiguredProvider !== false) return null;
|
|
@@ -3289,6 +3525,28 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
|
3289
3525
|
(m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
3290
3526
|
);
|
|
3291
3527
|
}
|
|
3528
|
+
function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
|
|
3529
|
+
if (!messages || messages.length === 0) return false;
|
|
3530
|
+
const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
|
|
3531
|
+
if (userIndex === -1) return false;
|
|
3532
|
+
let hasLaterUser = false;
|
|
3533
|
+
let hasStartedLaterUser = false;
|
|
3534
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
3535
|
+
const message = messages[i];
|
|
3536
|
+
if (roleOf(message) !== "user") continue;
|
|
3537
|
+
hasLaterUser = true;
|
|
3538
|
+
const laterUserMessageId = idOf(message);
|
|
3539
|
+
if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
|
|
3540
|
+
return false;
|
|
3541
|
+
}
|
|
3542
|
+
if (messages.some(
|
|
3543
|
+
(candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
|
|
3544
|
+
)) {
|
|
3545
|
+
hasStartedLaterUser = true;
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
return hasLaterUser && hasStartedLaterUser;
|
|
3549
|
+
}
|
|
3292
3550
|
async function hasAnyConfiguredProvider(port) {
|
|
3293
3551
|
try {
|
|
3294
3552
|
const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
|
|
@@ -3320,6 +3578,94 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3320
3578
|
return null;
|
|
3321
3579
|
}
|
|
3322
3580
|
}
|
|
3581
|
+
function sessionErrorReason(error2) {
|
|
3582
|
+
const record = typeof error2 === "object" && error2 !== null ? error2 : null;
|
|
3583
|
+
const data = record?.data;
|
|
3584
|
+
const dataRecord = typeof data === "object" && data !== null ? data : null;
|
|
3585
|
+
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";
|
|
3586
|
+
const reason = rawReason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
3587
|
+
return reason || "OpenCode reported a session error with no details";
|
|
3588
|
+
}
|
|
3589
|
+
function parseSessionErrorFrame(data) {
|
|
3590
|
+
let parsed;
|
|
3591
|
+
try {
|
|
3592
|
+
parsed = JSON.parse(data);
|
|
3593
|
+
} catch (error2) {
|
|
3594
|
+
void error2;
|
|
3595
|
+
return null;
|
|
3596
|
+
}
|
|
3597
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
3598
|
+
const parsedRecord = parsed;
|
|
3599
|
+
const payload = parsedRecord.payload;
|
|
3600
|
+
const event = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : parsedRecord;
|
|
3601
|
+
if (event.type !== "session.error") return null;
|
|
3602
|
+
const properties = event.properties;
|
|
3603
|
+
if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
|
|
3604
|
+
return null;
|
|
3605
|
+
}
|
|
3606
|
+
const propertiesRecord = properties;
|
|
3607
|
+
const sessionId = propertiesRecord.sessionID;
|
|
3608
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
3609
|
+
return {
|
|
3610
|
+
sessionId,
|
|
3611
|
+
reason: sessionErrorReason(propertiesRecord.error)
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
async function readSessionErrorStream(port, options) {
|
|
3615
|
+
let reader = null;
|
|
3616
|
+
try {
|
|
3617
|
+
const response = await fetch(`${opencodeBase(port)}/event`, {
|
|
3618
|
+
headers: { accept: "text/event-stream" },
|
|
3619
|
+
signal: options.signal
|
|
3620
|
+
});
|
|
3621
|
+
if (!response.ok || !response.body) {
|
|
3622
|
+
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
3623
|
+
}
|
|
3624
|
+
reader = response.body.getReader();
|
|
3625
|
+
const decoder = new TextDecoder();
|
|
3626
|
+
let buffer = "";
|
|
3627
|
+
const processLine = (line) => {
|
|
3628
|
+
const trimmed = line.trimEnd();
|
|
3629
|
+
if (!trimmed.startsWith("data:")) return;
|
|
3630
|
+
const event = parseSessionErrorFrame(trimmed.slice("data:".length).replace(/^ /, ""));
|
|
3631
|
+
if (event) options.onSessionError(event);
|
|
3632
|
+
};
|
|
3633
|
+
while (true) {
|
|
3634
|
+
const { done, value } = await reader.read();
|
|
3635
|
+
if (done) return { reason: "ended" };
|
|
3636
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3637
|
+
const lines = buffer.split("\n");
|
|
3638
|
+
buffer = lines.pop() ?? "";
|
|
3639
|
+
for (const line of lines) processLine(line);
|
|
3640
|
+
}
|
|
3641
|
+
} catch (err) {
|
|
3642
|
+
if (options.signal.aborted) return { reason: "aborted" };
|
|
3643
|
+
return {
|
|
3644
|
+
reason: "unavailable",
|
|
3645
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
3646
|
+
};
|
|
3647
|
+
} finally {
|
|
3648
|
+
if (reader) void reader.cancel().catch(() => void 0);
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
async function reloadProviderCache(port) {
|
|
3652
|
+
try {
|
|
3653
|
+
const res = await timedFetch(`${opencodeBase(port)}/config`, {
|
|
3654
|
+
method: "PATCH",
|
|
3655
|
+
headers: { "Content-Type": "application/json" },
|
|
3656
|
+
body: JSON.stringify({})
|
|
3657
|
+
});
|
|
3658
|
+
if (!res.ok) {
|
|
3659
|
+
console.error(
|
|
3660
|
+
`[reloadProviderCache] PATCH /config returned HTTP ${res.status} (port ${port})`
|
|
3661
|
+
);
|
|
3662
|
+
}
|
|
3663
|
+
} catch (err) {
|
|
3664
|
+
console.error(
|
|
3665
|
+
`[reloadProviderCache] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
3666
|
+
);
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3323
3669
|
|
|
3324
3670
|
// src/lib/opencode/session-cleanup.ts
|
|
3325
3671
|
var DURATION_UNIT_MS = {
|
|
@@ -3426,8 +3772,8 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
3426
3772
|
}
|
|
3427
3773
|
|
|
3428
3774
|
// src/lib/opencode/session-db-size.ts
|
|
3429
|
-
import { statSync as statSync3 } from "fs";
|
|
3430
|
-
import { join as join4 } from "path";
|
|
3775
|
+
import { statSync as statSync3 } from "node:fs";
|
|
3776
|
+
import { join as join4 } from "node:path";
|
|
3431
3777
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
3432
3778
|
function statSessionDbBytes(homeDir) {
|
|
3433
3779
|
const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
@@ -3457,9 +3803,96 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
3457
3803
|
return null;
|
|
3458
3804
|
}
|
|
3459
3805
|
|
|
3806
|
+
// src/lib/opencode/log-tail.ts
|
|
3807
|
+
import { statSync as statSync4 } from "node:fs";
|
|
3808
|
+
import { homedir as homedir3 } from "node:os";
|
|
3809
|
+
import { join as join5 } from "node:path";
|
|
3810
|
+
import { open as open2, stat } from "node:fs/promises";
|
|
3811
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
3812
|
+
function resolveOpenCodeLogPath(homeDir = homedir3(), env = process.env) {
|
|
3813
|
+
const dataDir = env.XDG_DATA_HOME || join5(homeDir, ".local", "share");
|
|
3814
|
+
return join5(dataDir, "opencode", "log", "opencode.log");
|
|
3815
|
+
}
|
|
3816
|
+
function isEnoent(error2) {
|
|
3817
|
+
return error2?.code === "ENOENT";
|
|
3818
|
+
}
|
|
3819
|
+
function reportFailure(operation, logPath, error2) {
|
|
3820
|
+
console.error(
|
|
3821
|
+
`[opencode-log-tail] ${operation} failed for ${logPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3822
|
+
);
|
|
3823
|
+
}
|
|
3824
|
+
function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
3825
|
+
const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
3826
|
+
let offset = 0;
|
|
3827
|
+
let inode = null;
|
|
3828
|
+
let baselineReady = true;
|
|
3829
|
+
try {
|
|
3830
|
+
const initial = statSync4(logPath);
|
|
3831
|
+
offset = initial.size;
|
|
3832
|
+
inode = initial.ino;
|
|
3833
|
+
} catch (error2) {
|
|
3834
|
+
if (!isEnoent(error2)) {
|
|
3835
|
+
reportFailure("initial stat", logPath, error2);
|
|
3836
|
+
baselineReady = false;
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
let polling = false;
|
|
3840
|
+
let stopped = false;
|
|
3841
|
+
const poll = async () => {
|
|
3842
|
+
if (polling || stopped) return;
|
|
3843
|
+
polling = true;
|
|
3844
|
+
try {
|
|
3845
|
+
let current;
|
|
3846
|
+
try {
|
|
3847
|
+
current = await stat(logPath);
|
|
3848
|
+
} catch (error2) {
|
|
3849
|
+
if (!isEnoent(error2)) reportFailure("stat", logPath, error2);
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3852
|
+
if (!baselineReady) {
|
|
3853
|
+
offset = current.size;
|
|
3854
|
+
inode = current.ino;
|
|
3855
|
+
baselineReady = true;
|
|
3856
|
+
return;
|
|
3857
|
+
}
|
|
3858
|
+
if (inode !== null && current.ino !== inode || current.size < offset) {
|
|
3859
|
+
offset = 0;
|
|
3860
|
+
}
|
|
3861
|
+
inode = current.ino;
|
|
3862
|
+
if (current.size === offset) return;
|
|
3863
|
+
const length = current.size - offset;
|
|
3864
|
+
const fh = await open2(logPath, "r");
|
|
3865
|
+
try {
|
|
3866
|
+
const buf = Buffer.alloc(length);
|
|
3867
|
+
const { bytesRead } = await fh.read(buf, 0, length, offset);
|
|
3868
|
+
offset += bytesRead;
|
|
3869
|
+
if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
|
|
3870
|
+
} finally {
|
|
3871
|
+
await fh.close();
|
|
3872
|
+
}
|
|
3873
|
+
} catch (error2) {
|
|
3874
|
+
if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
|
|
3875
|
+
} finally {
|
|
3876
|
+
polling = false;
|
|
3877
|
+
}
|
|
3878
|
+
};
|
|
3879
|
+
const interval = setInterval(() => void poll(), pollIntervalMs);
|
|
3880
|
+
void poll();
|
|
3881
|
+
return {
|
|
3882
|
+
stop: () => {
|
|
3883
|
+
stopped = true;
|
|
3884
|
+
clearInterval(interval);
|
|
3885
|
+
}
|
|
3886
|
+
};
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3460
3889
|
// src/lib/opencode/session-db-reclaim.ts
|
|
3461
|
-
import { statSync as
|
|
3462
|
-
import { dirname as dirname4 } from "path";
|
|
3890
|
+
import { statSync as statSync5, statfsSync } from "node:fs";
|
|
3891
|
+
import { dirname as dirname4 } from "node:path";
|
|
3892
|
+
function errorMessage(error2) {
|
|
3893
|
+
if (!(error2 instanceof Error)) return String(error2);
|
|
3894
|
+
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
3895
|
+
}
|
|
3463
3896
|
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
3464
3897
|
try {
|
|
3465
3898
|
const fsStats = statfsSync(dirname4(dbPath));
|
|
@@ -3485,17 +3918,17 @@ async function probeReclaimAvailability(input) {
|
|
|
3485
3918
|
const { dbPath, requiredBytes } = input;
|
|
3486
3919
|
let sqlite;
|
|
3487
3920
|
try {
|
|
3488
|
-
sqlite = await import("sqlite");
|
|
3921
|
+
sqlite = await import("node:sqlite");
|
|
3489
3922
|
} catch (err) {
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
return "sqlite-unavailable";
|
|
3923
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
3924
|
+
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
3925
|
+
return { reason: "sqlite-unavailable", detail };
|
|
3494
3926
|
}
|
|
3495
3927
|
let autoVacuum = null;
|
|
3496
3928
|
try {
|
|
3497
3929
|
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
3498
3930
|
try {
|
|
3931
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3499
3932
|
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3500
3933
|
} finally {
|
|
3501
3934
|
db.close();
|
|
@@ -3506,23 +3939,25 @@ async function probeReclaimAvailability(input) {
|
|
|
3506
3939
|
);
|
|
3507
3940
|
}
|
|
3508
3941
|
if (autoVacuum !== 0) return null;
|
|
3509
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
|
|
3942
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
3510
3943
|
}
|
|
3511
3944
|
async function reclaimSessionDbSpace(input) {
|
|
3512
3945
|
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
3513
3946
|
let sqlite;
|
|
3514
3947
|
try {
|
|
3515
|
-
sqlite = await import("sqlite");
|
|
3948
|
+
sqlite = await import("node:sqlite");
|
|
3516
3949
|
} catch (err) {
|
|
3950
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
3517
3951
|
console.warn(
|
|
3518
|
-
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${
|
|
3952
|
+
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
3519
3953
|
);
|
|
3520
|
-
return { ok: false, skipped: "sqlite-unavailable" };
|
|
3954
|
+
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
3521
3955
|
}
|
|
3522
3956
|
const { DatabaseSync } = sqlite;
|
|
3523
3957
|
let db;
|
|
3524
3958
|
try {
|
|
3525
3959
|
db = new DatabaseSync(dbPath);
|
|
3960
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3526
3961
|
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3527
3962
|
if (autoVacuum === 0) {
|
|
3528
3963
|
if (!allowFullVacuum) {
|
|
@@ -3531,7 +3966,7 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3531
3966
|
);
|
|
3532
3967
|
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
3533
3968
|
}
|
|
3534
|
-
const fileBytesForGuard =
|
|
3969
|
+
const fileBytesForGuard = statSync5(dbPath).size;
|
|
3535
3970
|
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
3536
3971
|
if (skipReason !== null) {
|
|
3537
3972
|
console.warn(
|
|
@@ -3559,10 +3994,12 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3559
3994
|
);
|
|
3560
3995
|
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
3561
3996
|
} catch (err) {
|
|
3562
|
-
console.error(
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3997
|
+
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
3998
|
+
return {
|
|
3999
|
+
ok: false,
|
|
4000
|
+
skipped: "reclaim-error",
|
|
4001
|
+
detail: errorMessage(err)
|
|
4002
|
+
};
|
|
3566
4003
|
} finally {
|
|
3567
4004
|
db?.close();
|
|
3568
4005
|
}
|
|
@@ -3603,7 +4040,6 @@ var StreamForwarder = class {
|
|
|
3603
4040
|
handleFrame(frame) {
|
|
3604
4041
|
switch (frame.type) {
|
|
3605
4042
|
case "open":
|
|
3606
|
-
this.callbacks.onOpen?.(frame.sid, frame.method, frame.path);
|
|
3607
4043
|
void this.handleOpen(frame);
|
|
3608
4044
|
break;
|
|
3609
4045
|
case "req_data":
|
|
@@ -3639,12 +4075,21 @@ var StreamForwarder = class {
|
|
|
3639
4075
|
const { sid, method, path, headers, has_body } = frame;
|
|
3640
4076
|
const correlationId = headers?.[CORRELATION_ID_HEADER];
|
|
3641
4077
|
const startedAt = Date.now();
|
|
4078
|
+
if (path !== TUNNEL_DRAIN_PING_PATH && path !== TUNNEL_USAGE_REARM_PING_PATH) {
|
|
4079
|
+
this.callbacks.onOpen?.(sid, method, path);
|
|
4080
|
+
}
|
|
3642
4081
|
if (path === TUNNEL_DRAIN_PING_PATH) {
|
|
3643
4082
|
this.callbacks.onDrainPing?.();
|
|
3644
4083
|
this.send({ type: "head", sid, status: 204, headers: {} });
|
|
3645
4084
|
this.send({ type: "res_end", sid });
|
|
3646
4085
|
return;
|
|
3647
4086
|
}
|
|
4087
|
+
if (path === TUNNEL_USAGE_REARM_PING_PATH) {
|
|
4088
|
+
this.callbacks.onUsageRearmPing?.();
|
|
4089
|
+
this.send({ type: "head", sid, status: 204, headers: {} });
|
|
4090
|
+
this.send({ type: "res_end", sid });
|
|
4091
|
+
return;
|
|
4092
|
+
}
|
|
3648
4093
|
if (process.env.DEBUG) {
|
|
3649
4094
|
log("debug", "agent_request", {
|
|
3650
4095
|
correlation_id: correlationId,
|
|
@@ -3789,7 +4234,8 @@ function connectTunnel(options) {
|
|
|
3789
4234
|
onResponse,
|
|
3790
4235
|
onInfo,
|
|
3791
4236
|
onWarning,
|
|
3792
|
-
onDrainPing
|
|
4237
|
+
onDrainPing,
|
|
4238
|
+
onUsageRearmPing
|
|
3793
4239
|
} = options;
|
|
3794
4240
|
const tunnelUrl = getTunnelUrlConfig();
|
|
3795
4241
|
const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
|
|
@@ -3801,7 +4247,8 @@ function connectTunnel(options) {
|
|
|
3801
4247
|
});
|
|
3802
4248
|
const forwarder = new StreamForwarder(ws, port, {
|
|
3803
4249
|
onHead: () => onResponse?.(),
|
|
3804
|
-
onDrainPing: () => onDrainPing?.()
|
|
4250
|
+
onDrainPing: () => onDrainPing?.(),
|
|
4251
|
+
onUsageRearmPing: () => onUsageRearmPing?.()
|
|
3805
4252
|
});
|
|
3806
4253
|
const connectionTimeout = setTimeout(() => {
|
|
3807
4254
|
ws.close();
|
|
@@ -3844,8 +4291,8 @@ function connectTunnel(options) {
|
|
|
3844
4291
|
try {
|
|
3845
4292
|
message = JSON.parse(data.toString());
|
|
3846
4293
|
} catch (error2) {
|
|
3847
|
-
const
|
|
3848
|
-
onError?.(`Failed to handle message: ${
|
|
4294
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
4295
|
+
onError?.(`Failed to handle message: ${errorMessage3}`);
|
|
3849
4296
|
return;
|
|
3850
4297
|
}
|
|
3851
4298
|
if (isStreamFrame(message)) {
|
|
@@ -3962,6 +4409,7 @@ var RunnerConnection = class {
|
|
|
3962
4409
|
onError: (error2) => events.onError?.(error2),
|
|
3963
4410
|
onResponse: () => events.onResponse?.(),
|
|
3964
4411
|
onDrainPing: () => events.onDrainPing?.(),
|
|
4412
|
+
onUsageRearmPing: () => events.onUsageRearmPing?.(),
|
|
3965
4413
|
onInfo: (message) => events.onInfo?.(message),
|
|
3966
4414
|
onWarning: (message) => events.onWarning?.(message)
|
|
3967
4415
|
});
|
|
@@ -3988,7 +4436,7 @@ var RunnerConnection = class {
|
|
|
3988
4436
|
};
|
|
3989
4437
|
|
|
3990
4438
|
// src/lib/tunnel/ready-marker.ts
|
|
3991
|
-
import { writeFileSync as writeFileSync3 } from "fs";
|
|
4439
|
+
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
3992
4440
|
function writeTunnelReadyMarker(path, agentId) {
|
|
3993
4441
|
try {
|
|
3994
4442
|
writeFileSync3(path, `${agentId}
|
|
@@ -4000,7 +4448,7 @@ function writeTunnelReadyMarker(path, agentId) {
|
|
|
4000
4448
|
}
|
|
4001
4449
|
|
|
4002
4450
|
// src/lib/replication.ts
|
|
4003
|
-
import { spawn as spawn4 } from "child_process";
|
|
4451
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
4004
4452
|
function startSessionDbReplication(configPath) {
|
|
4005
4453
|
return spawn4("litestream", ["replicate", "-config", configPath], {
|
|
4006
4454
|
stdio: "inherit"
|
|
@@ -4016,7 +4464,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
4016
4464
|
}
|
|
4017
4465
|
|
|
4018
4466
|
// src/lib/process-liveness.ts
|
|
4019
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
4467
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
4020
4468
|
function isProcessAlive(pid) {
|
|
4021
4469
|
try {
|
|
4022
4470
|
process.kill(pid, 0);
|
|
@@ -4042,9 +4490,9 @@ function isProcessAlive(pid) {
|
|
|
4042
4490
|
}
|
|
4043
4491
|
|
|
4044
4492
|
// src/lib/openai-usage.ts
|
|
4045
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
4046
|
-
import { homedir as
|
|
4047
|
-
import { join as
|
|
4493
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
4494
|
+
import { homedir as homedir4 } from "node:os";
|
|
4495
|
+
import { join as join6 } from "node:path";
|
|
4048
4496
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4049
4497
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4050
4498
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4058,7 +4506,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4058
4506
|
}
|
|
4059
4507
|
function readOpenCodeChatGptCredentials() {
|
|
4060
4508
|
try {
|
|
4061
|
-
const raw = readFileSync5(
|
|
4509
|
+
const raw = readFileSync5(join6(homedir4(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4062
4510
|
let parsed;
|
|
4063
4511
|
try {
|
|
4064
4512
|
parsed = JSON.parse(raw);
|
|
@@ -4080,6 +4528,23 @@ function readOpenCodeChatGptCredentials() {
|
|
|
4080
4528
|
return null;
|
|
4081
4529
|
}
|
|
4082
4530
|
}
|
|
4531
|
+
function parseChatGptIdentity(accessToken) {
|
|
4532
|
+
const segments = accessToken.split(".");
|
|
4533
|
+
if (segments.length !== 3) return null;
|
|
4534
|
+
let payload;
|
|
4535
|
+
try {
|
|
4536
|
+
const parsed = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf8"));
|
|
4537
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
4538
|
+
payload = parsed;
|
|
4539
|
+
} catch {
|
|
4540
|
+
return null;
|
|
4541
|
+
}
|
|
4542
|
+
const profile = payload["https://api.openai.com/profile"];
|
|
4543
|
+
const auth = payload["https://api.openai.com/auth"];
|
|
4544
|
+
const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
|
|
4545
|
+
const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
|
|
4546
|
+
return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
|
|
4547
|
+
}
|
|
4083
4548
|
function toWindow2(headers, name) {
|
|
4084
4549
|
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
4085
4550
|
const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
|
|
@@ -4155,6 +4620,7 @@ async function getOpenAiUsage(port) {
|
|
|
4155
4620
|
"credentials_expired"
|
|
4156
4621
|
);
|
|
4157
4622
|
}
|
|
4623
|
+
const subscription = parseChatGptIdentity(credentials2.accessToken);
|
|
4158
4624
|
const models = await resolveProbeModels(port);
|
|
4159
4625
|
if (models.length === 0) {
|
|
4160
4626
|
throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
|
|
@@ -4187,7 +4653,7 @@ async function getOpenAiUsage(port) {
|
|
|
4187
4653
|
"no_usable_window"
|
|
4188
4654
|
);
|
|
4189
4655
|
}
|
|
4190
|
-
return usage;
|
|
4656
|
+
return { ...usage, subscription };
|
|
4191
4657
|
}
|
|
4192
4658
|
if (res.status === 401) {
|
|
4193
4659
|
throw new OpenAiUsageError(
|
|
@@ -4302,8 +4768,8 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
4302
4768
|
}
|
|
4303
4769
|
|
|
4304
4770
|
// src/lib/resource-usage.ts
|
|
4305
|
-
import { cpus, totalmem, freemem } from "os";
|
|
4306
|
-
import { statfsSync as statfsSync2 } from "fs";
|
|
4771
|
+
import { cpus, totalmem, freemem } from "node:os";
|
|
4772
|
+
import { statfsSync as statfsSync2 } from "node:fs";
|
|
4307
4773
|
|
|
4308
4774
|
// src/lib/ecs-task-metadata.ts
|
|
4309
4775
|
var ECS_METADATA_TIMEOUT_MS = 2e3;
|
|
@@ -4388,58 +4854,97 @@ function readDisk(homeDir) {
|
|
|
4388
4854
|
};
|
|
4389
4855
|
}
|
|
4390
4856
|
}
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4857
|
+
var CPU_PEAK_WINDOW_MS = 6e4;
|
|
4858
|
+
var CPU_PEAK_WINDOW_SAMPLE_COUNT = 4;
|
|
4859
|
+
var CPU_PEAK_SAMPLE_INTERVAL_MS = CPU_PEAK_WINDOW_MS / CPU_PEAK_WINDOW_SAMPLE_COUNT;
|
|
4860
|
+
function createCpuPeakSampler() {
|
|
4861
|
+
const sampleHistory = new Array(CPU_PEAK_WINDOW_SAMPLE_COUNT);
|
|
4862
|
+
sampleHistory[0] = readCpuSample();
|
|
4863
|
+
let nextSampleIndex = 1;
|
|
4864
|
+
let sampleCount = 1;
|
|
4865
|
+
let peak = null;
|
|
4866
|
+
const timer = setInterval(() => {
|
|
4394
4867
|
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);
|
|
4868
|
+
const sampleFromWindowAgo = sampleCount === CPU_PEAK_WINDOW_SAMPLE_COUNT ? sampleHistory[nextSampleIndex] : void 0;
|
|
4869
|
+
if (sampleFromWindowAgo !== void 0) {
|
|
4870
|
+
const percentage = cpuPercentBetween(sampleFromWindowAgo, current);
|
|
4871
|
+
if (percentage !== null) {
|
|
4872
|
+
peak = peak === null ? percentage : Math.max(peak, percentage);
|
|
4873
|
+
}
|
|
4417
4874
|
}
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4875
|
+
sampleHistory[nextSampleIndex] = current;
|
|
4876
|
+
nextSampleIndex = (nextSampleIndex + 1) % CPU_PEAK_WINDOW_SAMPLE_COUNT;
|
|
4877
|
+
sampleCount = Math.min(sampleCount + 1, CPU_PEAK_WINDOW_SAMPLE_COUNT);
|
|
4878
|
+
}, CPU_PEAK_SAMPLE_INTERVAL_MS);
|
|
4879
|
+
return {
|
|
4880
|
+
takeAndReset: () => {
|
|
4881
|
+
const currentPeak = peak;
|
|
4882
|
+
peak = null;
|
|
4883
|
+
return currentPeak;
|
|
4884
|
+
},
|
|
4885
|
+
stop: () => clearInterval(timer)
|
|
4886
|
+
};
|
|
4887
|
+
}
|
|
4888
|
+
function createResourceUsageCollector(homeDir) {
|
|
4889
|
+
let previous = readCpuSample();
|
|
4890
|
+
const cpuPeakSampler = createCpuPeakSampler();
|
|
4891
|
+
return {
|
|
4892
|
+
collect: async () => {
|
|
4893
|
+
const current = readCpuSample();
|
|
4894
|
+
const hostCpuPercent = cpuPercentBetween(previous, current);
|
|
4895
|
+
const hostCpuPeakPercent = cpuPeakSampler.takeAndReset();
|
|
4896
|
+
const hostCpuCount = cpus().length;
|
|
4897
|
+
previous = current;
|
|
4898
|
+
const disk = readDisk(homeDir);
|
|
4899
|
+
const opencodeDbBytes = statSessionDbBytes(homeDir);
|
|
4900
|
+
const { limits, warning: ecsWarning } = await readEcsTaskLimits(process.env);
|
|
4901
|
+
const warnings = [];
|
|
4902
|
+
if (disk.warning) warnings.push(disk.warning);
|
|
4903
|
+
if (ecsWarning) warnings.push(ecsWarning);
|
|
4904
|
+
let cpuPercent = hostCpuPercent;
|
|
4905
|
+
let cpuPeakPercent = hostCpuPeakPercent;
|
|
4906
|
+
let cpuCount = hostCpuCount;
|
|
4907
|
+
let memoryTotalBytes = totalmem();
|
|
4908
|
+
let memoryAvailableBytes = freemem();
|
|
4909
|
+
if (limits !== null) {
|
|
4910
|
+
cpuCount = limits.cpuCount;
|
|
4911
|
+
memoryTotalBytes = limits.memoryTotalBytes;
|
|
4912
|
+
memoryAvailableBytes = clamp(
|
|
4913
|
+
limits.memoryTotalBytes - (totalmem() - freemem()),
|
|
4914
|
+
0,
|
|
4915
|
+
limits.memoryTotalBytes
|
|
4916
|
+
);
|
|
4917
|
+
cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
|
|
4918
|
+
cpuPeakPercent = hostCpuPeakPercent === null ? null : clamp(round2(hostCpuPeakPercent * hostCpuCount / limits.cpuCount), 0, 100);
|
|
4919
|
+
}
|
|
4920
|
+
return {
|
|
4921
|
+
usage: {
|
|
4922
|
+
cpuPercent,
|
|
4923
|
+
cpuPeakPercent,
|
|
4924
|
+
cpuCount,
|
|
4925
|
+
memoryTotalBytes,
|
|
4926
|
+
memoryAvailableBytes,
|
|
4927
|
+
diskTotalBytes: disk.totalBytes,
|
|
4928
|
+
diskFreeBytes: disk.freeBytes,
|
|
4929
|
+
opencodeDbBytes
|
|
4930
|
+
},
|
|
4931
|
+
warnings
|
|
4932
|
+
};
|
|
4933
|
+
},
|
|
4934
|
+
stop: cpuPeakSampler.stop
|
|
4430
4935
|
};
|
|
4431
4936
|
}
|
|
4432
4937
|
|
|
4433
4938
|
// src/lib/channels/driver.ts
|
|
4434
|
-
import { homedir as
|
|
4939
|
+
import { homedir as homedir5 } from "node:os";
|
|
4435
4940
|
|
|
4436
4941
|
// src/lib/runner-file-sync.ts
|
|
4437
|
-
import { join as
|
|
4942
|
+
import { join as join8 } from "node:path";
|
|
4438
4943
|
|
|
4439
4944
|
// 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
|
|
4945
|
+
import { randomUUID } from "node:crypto";
|
|
4946
|
+
import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
|
|
4947
|
+
import { basename, dirname as dirname5, isAbsolute, join as join7, relative, resolve as resolve2, sep } from "node:path";
|
|
4443
4948
|
var FILE_MODE = 384;
|
|
4444
4949
|
var DIRECTORY_MODE = 448;
|
|
4445
4950
|
async function writePushedFile(request) {
|
|
@@ -4472,7 +4977,7 @@ async function writePushedFile(request) {
|
|
|
4472
4977
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
4473
4978
|
dirname5(candidate)
|
|
4474
4979
|
);
|
|
4475
|
-
const realTarget =
|
|
4980
|
+
const realTarget = join7(existingAncestor, ...missingSegments, basename(candidate));
|
|
4476
4981
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
4477
4982
|
if (allowedDirectory === null) {
|
|
4478
4983
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -4508,7 +5013,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
4508
5013
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
4509
5014
|
return null;
|
|
4510
5015
|
}
|
|
4511
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
5016
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4512
5017
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
4513
5018
|
return null;
|
|
4514
5019
|
}
|
|
@@ -4581,16 +5086,16 @@ function contains(realDirectory, realTarget) {
|
|
|
4581
5086
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
4582
5087
|
let current = existingAncestor;
|
|
4583
5088
|
for (const segment of missingSegments) {
|
|
4584
|
-
current =
|
|
5089
|
+
current = join7(current, segment);
|
|
4585
5090
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
4586
5091
|
await chmod(current, DIRECTORY_MODE);
|
|
4587
5092
|
}
|
|
4588
5093
|
}
|
|
4589
5094
|
async function writeAtomically(realTarget, content) {
|
|
4590
|
-
const temporaryPath =
|
|
5095
|
+
const temporaryPath = join7(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
4591
5096
|
let handle;
|
|
4592
5097
|
try {
|
|
4593
|
-
handle = await
|
|
5098
|
+
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
4594
5099
|
await handle.writeFile(content);
|
|
4595
5100
|
await handle.chmod(FILE_MODE);
|
|
4596
5101
|
await handle.close();
|
|
@@ -4717,12 +5222,12 @@ var NOT_APPLIED = {
|
|
|
4717
5222
|
opencodeAuthApplied: false
|
|
4718
5223
|
};
|
|
4719
5224
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
4720
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4721
|
-
return expanded ===
|
|
5225
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5226
|
+
return expanded === join8(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
4722
5227
|
}
|
|
4723
5228
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
4724
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4725
|
-
return expanded ===
|
|
5229
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5230
|
+
return expanded === join8(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
4726
5231
|
}
|
|
4727
5232
|
async function applyOne(options, file) {
|
|
4728
5233
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -4882,6 +5387,10 @@ var DEFAULT_RETRY_POLICY = {
|
|
|
4882
5387
|
baseDelayMs: 500,
|
|
4883
5388
|
maxDelayMs: 3e4
|
|
4884
5389
|
};
|
|
5390
|
+
var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
|
|
5391
|
+
var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
|
|
5392
|
+
var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
5393
|
+
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
4885
5394
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
4886
5395
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
4887
5396
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
@@ -5022,6 +5531,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5022
5531
|
* message; it is removed once its in-flight set empties.
|
|
5023
5532
|
*/
|
|
5024
5533
|
watchers = /* @__PURE__ */ new Map();
|
|
5534
|
+
sessionErrorStream = null;
|
|
5535
|
+
/**
|
|
5536
|
+
* Session-error failures currently being reported; entries are empty at rest
|
|
5537
|
+
* because each handoff deletes its id in `finally`.
|
|
5538
|
+
*/
|
|
5539
|
+
sessionErrorHandled = /* @__PURE__ */ new Set();
|
|
5540
|
+
/**
|
|
5541
|
+
* Session errors that arrived before their dispatch was registered. Bounded FIFO
|
|
5542
|
+
* with a short TTL so an unmatched session cannot retain an event indefinitely.
|
|
5543
|
+
*/
|
|
5544
|
+
bufferedSessionErrors = /* @__PURE__ */ new Map();
|
|
5025
5545
|
/**
|
|
5026
5546
|
* AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
|
|
5027
5547
|
* dispatched and are still in-flight. A message in this set is never
|
|
@@ -5205,6 +5725,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5205
5725
|
* no watcher) can resolve the title.
|
|
5206
5726
|
*/
|
|
5207
5727
|
sessionTitles = /* @__PURE__ */ new Map();
|
|
5728
|
+
/** One best-effort terminal subagent collection per Evident message id. */
|
|
5729
|
+
subagentInvocationCollections = /* @__PURE__ */ new Map();
|
|
5730
|
+
/**
|
|
5731
|
+
* Early snapshots are only liveness hints; they must not become the terminal
|
|
5732
|
+
* collection when the task parts or child transcript have advanced.
|
|
5733
|
+
*/
|
|
5734
|
+
subagentInvocationPrefetches = /* @__PURE__ */ new Map();
|
|
5208
5735
|
/** Serialises drains so a reconnect during a drain doesn't double-process. */
|
|
5209
5736
|
draining = false;
|
|
5210
5737
|
/**
|
|
@@ -5249,6 +5776,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5249
5776
|
* and stops opencode.
|
|
5250
5777
|
*/
|
|
5251
5778
|
stopped = false;
|
|
5779
|
+
recycleRequestedFlag = false;
|
|
5252
5780
|
constructor(config) {
|
|
5253
5781
|
this.agentId = config.agentId;
|
|
5254
5782
|
this.port = config.port;
|
|
@@ -5268,7 +5796,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5268
5796
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
5269
5797
|
this.now = config.now ?? (() => Date.now());
|
|
5270
5798
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
5271
|
-
this.homeDir = config.homeDir ??
|
|
5799
|
+
this.homeDir = config.homeDir ?? homedir5();
|
|
5272
5800
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
5273
5801
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
5274
5802
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
@@ -5354,6 +5882,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5354
5882
|
let dispatched = 0;
|
|
5355
5883
|
try {
|
|
5356
5884
|
const conversations = await this.getPendingConversations();
|
|
5885
|
+
if (this.recycleRequestedFlag) {
|
|
5886
|
+
this.stop();
|
|
5887
|
+
}
|
|
5357
5888
|
if (conversations.length > 0) {
|
|
5358
5889
|
const total = conversations.reduce((sum, c) => sum + (c.pending_message_count ?? 0), 0);
|
|
5359
5890
|
this.log({
|
|
@@ -5415,6 +5946,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5415
5946
|
}
|
|
5416
5947
|
return ids;
|
|
5417
5948
|
}
|
|
5949
|
+
/**
|
|
5950
|
+
* OpenCode user-message ids tracked for other Evident messages in a session.
|
|
5951
|
+
* Excluding this message makes an unattributed later row fail safe; a missing
|
|
5952
|
+
* watcher yields no attributions, per `hasLaterSiblingTurnStarted`'s docblock.
|
|
5953
|
+
*/
|
|
5954
|
+
siblingOpencodeMessageIds(watcher, ownEvidentMessageId) {
|
|
5955
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5956
|
+
if (!watcher) return ids;
|
|
5957
|
+
for (const inFlight of watcher.inFlight.values()) {
|
|
5958
|
+
if (inFlight.evidentMessageId !== ownEvidentMessageId) {
|
|
5959
|
+
ids.add(inFlight.opencodeMessageId);
|
|
5960
|
+
}
|
|
5961
|
+
}
|
|
5962
|
+
return ids;
|
|
5963
|
+
}
|
|
5418
5964
|
/**
|
|
5419
5965
|
* File-pull work, for `run.ts`'s idle accounting (#559).
|
|
5420
5966
|
*
|
|
@@ -5481,6 +6027,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5481
6027
|
*/
|
|
5482
6028
|
stop() {
|
|
5483
6029
|
this.stopped = true;
|
|
6030
|
+
this.sessionErrorStream?.abort.abort();
|
|
6031
|
+
this.sessionErrorStream = null;
|
|
6032
|
+
}
|
|
6033
|
+
/**
|
|
6034
|
+
* The server clears this request when a new MicroVM identity is recorded, so a
|
|
6035
|
+
* same-VM tunnel reconnect does not consume it. This is a plain read rather
|
|
6036
|
+
* than a consume; `run.ts` guards the action once-only.
|
|
6037
|
+
*/
|
|
6038
|
+
get recycleRequested() {
|
|
6039
|
+
return this.recycleRequestedFlag;
|
|
5484
6040
|
}
|
|
5485
6041
|
/**
|
|
5486
6042
|
* Wait (up to `timeoutMs`) for in-flight watcher work to settle during a
|
|
@@ -5547,6 +6103,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5547
6103
|
*/
|
|
5548
6104
|
async processConversation(conv) {
|
|
5549
6105
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
6106
|
+
this.ensureSessionErrorStream();
|
|
5550
6107
|
const messages = await this.getPendingMessages(conv.id);
|
|
5551
6108
|
let dispatched = 0;
|
|
5552
6109
|
let skippedAlreadyDispatched = 0;
|
|
@@ -5619,7 +6176,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5619
6176
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
5620
6177
|
break;
|
|
5621
6178
|
}
|
|
5622
|
-
const
|
|
6179
|
+
const errorMessage3 = err instanceof Error ? err.message : String(err);
|
|
5623
6180
|
this.sessions.delete(conv.id);
|
|
5624
6181
|
this.supersede(conv.id, sessionId);
|
|
5625
6182
|
this.log({
|
|
@@ -5628,7 +6185,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5628
6185
|
conversation_id: conv.id,
|
|
5629
6186
|
message_id: message.id
|
|
5630
6187
|
});
|
|
5631
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6188
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5632
6189
|
this.log({
|
|
5633
6190
|
level: "warn",
|
|
5634
6191
|
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 +6196,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5639
6196
|
});
|
|
5640
6197
|
this.log({
|
|
5641
6198
|
level: "error",
|
|
5642
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
6199
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
|
|
5643
6200
|
conversation_id: conv.id,
|
|
5644
6201
|
message_id: message.id
|
|
5645
6202
|
});
|
|
@@ -5660,14 +6217,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5660
6217
|
this.unconfirmedDispatchFailures.delete(message.id);
|
|
5661
6218
|
this.sessions.delete(conv.id);
|
|
5662
6219
|
this.supersede(conv.id, sessionId);
|
|
5663
|
-
const
|
|
6220
|
+
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
6221
|
this.log({
|
|
5665
6222
|
level: "error",
|
|
5666
|
-
message:
|
|
6223
|
+
message: errorMessage3,
|
|
5667
6224
|
conversation_id: conv.id,
|
|
5668
6225
|
message_id: message.id
|
|
5669
6226
|
});
|
|
5670
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6227
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5671
6228
|
this.log({
|
|
5672
6229
|
level: "warn",
|
|
5673
6230
|
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)}`,
|
|
@@ -5893,6 +6450,23 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5893
6450
|
if (state === "running" || state === "queued") {
|
|
5894
6451
|
const ongoing = await isSessionOngoing(this.port, sessionId);
|
|
5895
6452
|
if (ongoing === true) {
|
|
6453
|
+
if (state === "queued") {
|
|
6454
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(
|
|
6455
|
+
this.watchers.get(sessionId),
|
|
6456
|
+
message.id
|
|
6457
|
+
);
|
|
6458
|
+
if (hasLaterSiblingTurnStarted(messages, ocId ?? "", siblingOcIds)) {
|
|
6459
|
+
this.log({
|
|
6460
|
+
level: "warn",
|
|
6461
|
+
message: `Re-drive: OpenCode already served a later, different Evident message's turn in session ${sessionId.slice(0, 8)} while message ${message.id.slice(0, 8)} produced no reply \u2014 re-dispatching instead of reattaching to someone else's turn`,
|
|
6462
|
+
conversation_id: conv.id,
|
|
6463
|
+
message_id: message.id
|
|
6464
|
+
});
|
|
6465
|
+
this.clearRedriveUnresolved(message.id);
|
|
6466
|
+
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
6467
|
+
return "dispatch";
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
5896
6470
|
return this.reattachRedrive(conv, sessionId, message, ocId);
|
|
5897
6471
|
}
|
|
5898
6472
|
if (ongoing === false) {
|
|
@@ -5982,16 +6556,37 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5982
6556
|
if (state === "done") {
|
|
5983
6557
|
const title = await this.resolveSessionTitle(sessionId, conv.id);
|
|
5984
6558
|
const usage = messageUsage(messages, ocId ?? "");
|
|
6559
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
6560
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
6561
|
+
messages,
|
|
6562
|
+
ocId ?? "",
|
|
6563
|
+
message.id
|
|
6564
|
+
);
|
|
5985
6565
|
this.log({
|
|
5986
6566
|
level: "info",
|
|
5987
6567
|
message: `Re-drive: message ${message.id.slice(0, 8)} completed while its row was wrongly reclaimed to pending \u2014 marking done instead of re-dispatching`,
|
|
5988
6568
|
conversation_id: conv.id,
|
|
5989
6569
|
message_id: message.id
|
|
5990
6570
|
});
|
|
5991
|
-
await this.markDone(
|
|
6571
|
+
await this.markDone(
|
|
6572
|
+
conv.id,
|
|
6573
|
+
message.id,
|
|
6574
|
+
sessionId,
|
|
6575
|
+
ocId,
|
|
6576
|
+
title,
|
|
6577
|
+
usage,
|
|
6578
|
+
usageAgentName,
|
|
6579
|
+
subagentInvocations
|
|
6580
|
+
);
|
|
5992
6581
|
} else {
|
|
5993
6582
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
5994
6583
|
const usage = messageUsage(messages, ocId ?? "");
|
|
6584
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
6585
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
6586
|
+
messages,
|
|
6587
|
+
ocId ?? "",
|
|
6588
|
+
message.id
|
|
6589
|
+
);
|
|
5995
6590
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
5996
6591
|
this.log({
|
|
5997
6592
|
level: "error",
|
|
@@ -5999,7 +6594,19 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5999
6594
|
conversation_id: conv.id,
|
|
6000
6595
|
message_id: message.id
|
|
6001
6596
|
});
|
|
6002
|
-
await this.markFailed(
|
|
6597
|
+
await this.markFailed(
|
|
6598
|
+
conv.id,
|
|
6599
|
+
message.id,
|
|
6600
|
+
sessionId,
|
|
6601
|
+
error2,
|
|
6602
|
+
usage,
|
|
6603
|
+
failure,
|
|
6604
|
+
usageAgentName,
|
|
6605
|
+
subagentInvocations
|
|
6606
|
+
);
|
|
6607
|
+
}
|
|
6608
|
+
if (ocId !== null) {
|
|
6609
|
+
await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
|
|
6003
6610
|
}
|
|
6004
6611
|
} catch (err) {
|
|
6005
6612
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -6540,9 +7147,27 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6540
7147
|
ambiguousPinnedSinceMs: 0,
|
|
6541
7148
|
ambiguousResolved: false
|
|
6542
7149
|
});
|
|
7150
|
+
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
7151
|
+
if (!buffered) return;
|
|
7152
|
+
this.bufferedSessionErrors.delete(sessionId);
|
|
7153
|
+
if (this.now() - buffered.receivedAt < SESSION_ERROR_BUFFER_TTL_MS) {
|
|
7154
|
+
this.handleSessionError(buffered.event);
|
|
7155
|
+
}
|
|
6543
7156
|
}
|
|
6544
|
-
|
|
6545
|
-
|
|
7157
|
+
bufferSessionError(event) {
|
|
7158
|
+
this.bufferedSessionErrors.delete(event.sessionId);
|
|
7159
|
+
this.bufferedSessionErrors.set(event.sessionId, {
|
|
7160
|
+
event,
|
|
7161
|
+
receivedAt: this.now()
|
|
7162
|
+
});
|
|
7163
|
+
while (this.bufferedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
|
|
7164
|
+
const oldest = this.bufferedSessionErrors.keys().next().value;
|
|
7165
|
+
if (typeof oldest !== "string") break;
|
|
7166
|
+
this.bufferedSessionErrors.delete(oldest);
|
|
7167
|
+
}
|
|
7168
|
+
}
|
|
7169
|
+
/**
|
|
7170
|
+
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
6546
7171
|
* EQUAL (#1618) so a watcher whose loop has not started ticking yet is never
|
|
6547
7172
|
* misread as stalled by the very first reconciliation that sees it.
|
|
6548
7173
|
*/
|
|
@@ -6744,6 +7369,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6744
7369
|
ensureWatcherRunning(sessionId) {
|
|
6745
7370
|
const watcher = this.watchers.get(sessionId);
|
|
6746
7371
|
if (!watcher) return;
|
|
7372
|
+
this.ensureSessionErrorStream();
|
|
6747
7373
|
if (watcher.loop) return;
|
|
6748
7374
|
if (watcher.inFlight.size === 0) {
|
|
6749
7375
|
this.watchers.delete(sessionId);
|
|
@@ -6759,6 +7385,154 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6759
7385
|
});
|
|
6760
7386
|
watcher.loop = loop;
|
|
6761
7387
|
}
|
|
7388
|
+
ensureSessionErrorStream() {
|
|
7389
|
+
if (this.sessionErrorStream || this.stopped) return;
|
|
7390
|
+
const abort = new AbortController();
|
|
7391
|
+
const loop = this.runSessionErrorStream(abort.signal);
|
|
7392
|
+
this.sessionErrorStream = { abort, loop };
|
|
7393
|
+
}
|
|
7394
|
+
async runSessionErrorStream(signal) {
|
|
7395
|
+
let attempt = 0;
|
|
7396
|
+
let warned = false;
|
|
7397
|
+
while (!this.stopped && !signal.aborted) {
|
|
7398
|
+
const openedAt = this.now();
|
|
7399
|
+
try {
|
|
7400
|
+
const outcome = await readSessionErrorStream(this.port, {
|
|
7401
|
+
signal,
|
|
7402
|
+
onSessionError: (event) => this.handleSessionError(event)
|
|
7403
|
+
});
|
|
7404
|
+
if (outcome.reason === "aborted" || signal.aborted) return;
|
|
7405
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
7406
|
+
if (outcome.reason === "unavailable" || outcome.reason === "ended") {
|
|
7407
|
+
if (!healthy) {
|
|
7408
|
+
const detail = outcome.reason === "unavailable" ? outcome.detail : "stream ended";
|
|
7409
|
+
this.log({
|
|
7410
|
+
level: warned ? "debug" : "warn",
|
|
7411
|
+
message: `OpenCode session-error stream ${warned ? "still unavailable" : "unavailable"} (${detail}); transcript polling remains the evidence path`
|
|
7412
|
+
});
|
|
7413
|
+
warned = true;
|
|
7414
|
+
}
|
|
7415
|
+
}
|
|
7416
|
+
if (healthy) {
|
|
7417
|
+
if (warned) {
|
|
7418
|
+
this.log({
|
|
7419
|
+
level: "info",
|
|
7420
|
+
message: "OpenCode session-error stream reconnected; transcript polling remains the evidence path"
|
|
7421
|
+
});
|
|
7422
|
+
warned = false;
|
|
7423
|
+
}
|
|
7424
|
+
attempt = 0;
|
|
7425
|
+
} else {
|
|
7426
|
+
attempt += 1;
|
|
7427
|
+
}
|
|
7428
|
+
if (this.stopped || signal.aborted) return;
|
|
7429
|
+
await this.sleep(backoffDelay(healthy ? 0 : attempt - 1, this.retry));
|
|
7430
|
+
} catch (err) {
|
|
7431
|
+
if (this.stopped || signal.aborted) return;
|
|
7432
|
+
this.log({
|
|
7433
|
+
level: "error",
|
|
7434
|
+
message: `OpenCode session-error stream failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
|
|
7435
|
+
});
|
|
7436
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
7437
|
+
const delayAttempt = healthy ? 0 : attempt;
|
|
7438
|
+
attempt = healthy ? 0 : attempt + 1;
|
|
7439
|
+
try {
|
|
7440
|
+
await this.sleep(backoffDelay(delayAttempt, this.retry));
|
|
7441
|
+
} catch (sleepErr) {
|
|
7442
|
+
this.log({
|
|
7443
|
+
level: "error",
|
|
7444
|
+
message: `OpenCode session-error stream backoff failed unexpectedly: ${sleepErr instanceof Error ? sleepErr.message : String(sleepErr)}`
|
|
7445
|
+
});
|
|
7446
|
+
}
|
|
7447
|
+
}
|
|
7448
|
+
}
|
|
7449
|
+
}
|
|
7450
|
+
handleSessionError(event) {
|
|
7451
|
+
try {
|
|
7452
|
+
const watcher = this.watchers.get(event.sessionId);
|
|
7453
|
+
if (!watcher) {
|
|
7454
|
+
this.bufferSessionError(event);
|
|
7455
|
+
this.log({
|
|
7456
|
+
level: "debug",
|
|
7457
|
+
message: `Ignoring session error for unknown session ${event.sessionId.slice(0, 8)}`
|
|
7458
|
+
});
|
|
7459
|
+
return;
|
|
7460
|
+
}
|
|
7461
|
+
if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
|
|
7462
|
+
this.log({
|
|
7463
|
+
level: "debug",
|
|
7464
|
+
message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
|
|
7465
|
+
conversation_id: watcher.conv.id
|
|
7466
|
+
});
|
|
7467
|
+
return;
|
|
7468
|
+
}
|
|
7469
|
+
const inFlight = [...watcher.inFlight.values()].filter((message) => !message.started && !message.done).sort((a, b) => a.dispatchedAt - b.dispatchedAt)[0];
|
|
7470
|
+
if (!inFlight) {
|
|
7471
|
+
this.bufferSessionError(event);
|
|
7472
|
+
this.log({
|
|
7473
|
+
level: "debug",
|
|
7474
|
+
message: `No queued in-flight turn to correlate with session error in ${event.sessionId.slice(0, 8)}`,
|
|
7475
|
+
conversation_id: watcher.conv.id
|
|
7476
|
+
});
|
|
7477
|
+
return;
|
|
7478
|
+
}
|
|
7479
|
+
if (this.sessionErrorHandled.has(inFlight.evidentMessageId)) return;
|
|
7480
|
+
this.sessionErrorHandled.add(inFlight.evidentMessageId);
|
|
7481
|
+
void this.failFromSessionError(watcher, event, inFlight);
|
|
7482
|
+
} catch (err) {
|
|
7483
|
+
this.log({
|
|
7484
|
+
level: "error",
|
|
7485
|
+
message: `Failed to handle OpenCode session error for ${event.sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`
|
|
7486
|
+
});
|
|
7487
|
+
}
|
|
7488
|
+
}
|
|
7489
|
+
async failFromSessionError(watcher, event, inFlight) {
|
|
7490
|
+
try {
|
|
7491
|
+
const messages = await getSessionMessages(this.port, event.sessionId);
|
|
7492
|
+
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7493
|
+
if (state !== "queued") {
|
|
7494
|
+
this.log({
|
|
7495
|
+
level: "debug",
|
|
7496
|
+
message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
|
|
7497
|
+
conversation_id: watcher.conv.id,
|
|
7498
|
+
message_id: inFlight.evidentMessageId
|
|
7499
|
+
});
|
|
7500
|
+
return;
|
|
7501
|
+
}
|
|
7502
|
+
this.log({
|
|
7503
|
+
level: "error",
|
|
7504
|
+
message: `OpenCode could not run message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)}: ${event.reason}`,
|
|
7505
|
+
conversation_id: watcher.conv.id,
|
|
7506
|
+
message_id: inFlight.evidentMessageId
|
|
7507
|
+
});
|
|
7508
|
+
await this.markFailed(
|
|
7509
|
+
watcher.conv.id,
|
|
7510
|
+
inFlight.evidentMessageId,
|
|
7511
|
+
event.sessionId,
|
|
7512
|
+
`OpenCode could not run this turn: ${event.reason}`
|
|
7513
|
+
);
|
|
7514
|
+
inFlight.done = true;
|
|
7515
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7516
|
+
} catch (err) {
|
|
7517
|
+
if (err instanceof ChannelAuthError) {
|
|
7518
|
+
this.log({
|
|
7519
|
+
level: "warn",
|
|
7520
|
+
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`,
|
|
7521
|
+
conversation_id: watcher.conv.id,
|
|
7522
|
+
message_id: inFlight.evidentMessageId
|
|
7523
|
+
});
|
|
7524
|
+
} else {
|
|
7525
|
+
this.log({
|
|
7526
|
+
level: "warn",
|
|
7527
|
+
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`,
|
|
7528
|
+
conversation_id: watcher.conv.id,
|
|
7529
|
+
message_id: inFlight.evidentMessageId
|
|
7530
|
+
});
|
|
7531
|
+
}
|
|
7532
|
+
} finally {
|
|
7533
|
+
this.sessionErrorHandled.delete(inFlight.evidentMessageId);
|
|
7534
|
+
}
|
|
7535
|
+
}
|
|
6762
7536
|
/**
|
|
6763
7537
|
* The per-session polling loop (WI-3). Once per tick it:
|
|
6764
7538
|
* 1. polls `GET /session/:id/message` once and, per in-flight message,
|
|
@@ -6871,6 +7645,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6871
7645
|
const conv = watcher.conv;
|
|
6872
7646
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
6873
7647
|
const id = inFlight.evidentMessageId;
|
|
7648
|
+
if (messages && collectTaskCalls(messages, inFlight.opencodeMessageId).length > 0 && !this.subagentInvocationPrefetches.has(id)) {
|
|
7649
|
+
void this.resolveSubagentInvocations(
|
|
7650
|
+
messages,
|
|
7651
|
+
inFlight.opencodeMessageId,
|
|
7652
|
+
id,
|
|
7653
|
+
"prefetch"
|
|
7654
|
+
).catch((err) => {
|
|
7655
|
+
this.log({
|
|
7656
|
+
level: "warn",
|
|
7657
|
+
message: `Best-effort subagent usage prefetch failed for message ${id.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
7658
|
+
conversation_id: conv.id,
|
|
7659
|
+
message_id: id
|
|
7660
|
+
});
|
|
7661
|
+
});
|
|
7662
|
+
}
|
|
6874
7663
|
if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
|
|
6875
7664
|
else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
|
|
6876
7665
|
if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
|
|
@@ -6924,6 +7713,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6924
7713
|
message_id: inFlight.evidentMessageId
|
|
6925
7714
|
});
|
|
6926
7715
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
7716
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
7717
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
7718
|
+
messages,
|
|
7719
|
+
inFlight.opencodeMessageId,
|
|
7720
|
+
inFlight.evidentMessageId
|
|
7721
|
+
);
|
|
6927
7722
|
const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
|
|
6928
7723
|
try {
|
|
6929
7724
|
await this.markFailed(
|
|
@@ -6932,7 +7727,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6932
7727
|
sessionId,
|
|
6933
7728
|
error2,
|
|
6934
7729
|
usage,
|
|
6935
|
-
failure
|
|
7730
|
+
failure,
|
|
7731
|
+
usageAgentName,
|
|
7732
|
+
subagentInvocations
|
|
6936
7733
|
);
|
|
6937
7734
|
} catch (err) {
|
|
6938
7735
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -6965,13 +7762,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6965
7762
|
return;
|
|
6966
7763
|
}
|
|
6967
7764
|
inFlight.done = true;
|
|
7765
|
+
await this.reportSubagentAuthFailures(
|
|
7766
|
+
watcher.conv.id,
|
|
7767
|
+
inFlight.opencodeMessageId,
|
|
7768
|
+
inFlight.evidentMessageId,
|
|
7769
|
+
messages
|
|
7770
|
+
);
|
|
6968
7771
|
}
|
|
6969
7772
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
6970
7773
|
return;
|
|
6971
7774
|
}
|
|
7775
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
|
|
7776
|
+
const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
|
|
6972
7777
|
const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
|
|
6973
7778
|
const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
|
|
6974
|
-
if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
|
|
7779
|
+
if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
|
|
6975
7780
|
inFlight.stuckReported = true;
|
|
6976
7781
|
void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
|
|
6977
7782
|
stuck_for_ms: this.now() - inFlight.dispatchedAt
|
|
@@ -7132,7 +7937,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7132
7937
|
const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
|
|
7133
7938
|
(sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
|
|
7134
7939
|
);
|
|
7135
|
-
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
7940
|
+
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
|
|
7136
7941
|
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
7137
7942
|
this.log({
|
|
7138
7943
|
level: "debug",
|
|
@@ -7167,6 +7972,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7167
7972
|
});
|
|
7168
7973
|
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
7169
7974
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
7975
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
7976
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
7977
|
+
messages,
|
|
7978
|
+
inFlight.opencodeMessageId,
|
|
7979
|
+
inFlight.evidentMessageId
|
|
7980
|
+
);
|
|
7170
7981
|
try {
|
|
7171
7982
|
await this.markDone(
|
|
7172
7983
|
conv.id,
|
|
@@ -7174,7 +7985,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7174
7985
|
sessionId,
|
|
7175
7986
|
inFlight.opencodeMessageId,
|
|
7176
7987
|
title,
|
|
7177
|
-
usage
|
|
7988
|
+
usage,
|
|
7989
|
+
usageAgentName,
|
|
7990
|
+
subagentInvocations
|
|
7178
7991
|
);
|
|
7179
7992
|
} catch (err) {
|
|
7180
7993
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7207,6 +8020,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7207
8020
|
return;
|
|
7208
8021
|
}
|
|
7209
8022
|
inFlight.done = true;
|
|
8023
|
+
await this.reportSubagentAuthFailures(
|
|
8024
|
+
watcher.conv.id,
|
|
8025
|
+
inFlight.opencodeMessageId,
|
|
8026
|
+
inFlight.evidentMessageId,
|
|
8027
|
+
messages
|
|
8028
|
+
);
|
|
7210
8029
|
}
|
|
7211
8030
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7212
8031
|
}
|
|
@@ -7347,6 +8166,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7347
8166
|
if (state === "failed" && !restartAborted) {
|
|
7348
8167
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
7349
8168
|
const usage = messageUsage(messages, ocId ?? "");
|
|
8169
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
8170
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
8171
|
+
messages,
|
|
8172
|
+
ocId ?? "",
|
|
8173
|
+
row.id
|
|
8174
|
+
);
|
|
7350
8175
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
7351
8176
|
this.log({
|
|
7352
8177
|
level: "error",
|
|
@@ -7355,7 +8180,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7355
8180
|
message_id: row.id
|
|
7356
8181
|
});
|
|
7357
8182
|
try {
|
|
7358
|
-
await this.markFailed(
|
|
8183
|
+
await this.markFailed(
|
|
8184
|
+
row.conversation_id,
|
|
8185
|
+
row.id,
|
|
8186
|
+
sessionId,
|
|
8187
|
+
error2,
|
|
8188
|
+
usage,
|
|
8189
|
+
failure,
|
|
8190
|
+
usageAgentName,
|
|
8191
|
+
subagentInvocations
|
|
8192
|
+
);
|
|
7359
8193
|
} catch (err) {
|
|
7360
8194
|
if (err instanceof ChannelAuthError) throw err;
|
|
7361
8195
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7377,6 +8211,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7377
8211
|
});
|
|
7378
8212
|
return;
|
|
7379
8213
|
}
|
|
8214
|
+
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
7380
8215
|
this.dontRedispatch.delete(row.id);
|
|
7381
8216
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
7382
8217
|
return;
|
|
@@ -7516,7 +8351,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7516
8351
|
try {
|
|
7517
8352
|
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
7518
8353
|
const usage = messageUsage(messages, ocId ?? "");
|
|
7519
|
-
|
|
8354
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
8355
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
8356
|
+
messages,
|
|
8357
|
+
ocId ?? "",
|
|
8358
|
+
row.id
|
|
8359
|
+
);
|
|
8360
|
+
await this.markDone(
|
|
8361
|
+
row.conversation_id,
|
|
8362
|
+
row.id,
|
|
8363
|
+
sessionId,
|
|
8364
|
+
ocId,
|
|
8365
|
+
title,
|
|
8366
|
+
usage,
|
|
8367
|
+
usageAgentName,
|
|
8368
|
+
subagentInvocations
|
|
8369
|
+
);
|
|
7520
8370
|
} catch (err) {
|
|
7521
8371
|
if (err instanceof ChannelAuthError) throw err;
|
|
7522
8372
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7538,6 +8388,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7538
8388
|
});
|
|
7539
8389
|
return;
|
|
7540
8390
|
}
|
|
8391
|
+
if (ocId !== null) {
|
|
8392
|
+
await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
|
|
8393
|
+
}
|
|
7541
8394
|
this.dontRedispatch.delete(row.id);
|
|
7542
8395
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
7543
8396
|
}
|
|
@@ -7631,14 +8484,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7631
8484
|
this.unconfirmedDispatchFailures.delete(row.id);
|
|
7632
8485
|
this.sessions.delete(readoptConv.id);
|
|
7633
8486
|
this.supersede(readoptConv.id, sessionId);
|
|
7634
|
-
const
|
|
8487
|
+
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
8488
|
this.log({
|
|
7636
8489
|
level: "error",
|
|
7637
|
-
message:
|
|
8490
|
+
message: errorMessage3,
|
|
7638
8491
|
conversation_id: row.conversation_id,
|
|
7639
8492
|
message_id: row.id
|
|
7640
8493
|
});
|
|
7641
|
-
await this.markFailed(row.conversation_id, row.id, null,
|
|
8494
|
+
await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
|
|
7642
8495
|
this.log({
|
|
7643
8496
|
level: "warn",
|
|
7644
8497
|
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)}`,
|
|
@@ -7917,6 +8770,166 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7917
8770
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
7918
8771
|
return parent;
|
|
7919
8772
|
}
|
|
8773
|
+
usageAgentName(messages, userMessageId) {
|
|
8774
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
8775
|
+
const mode = reply?.info?.mode;
|
|
8776
|
+
if (typeof mode === "string" && mode.length > 0) return mode;
|
|
8777
|
+
const agent = reply?.info?.agent;
|
|
8778
|
+
return typeof agent === "string" && agent.length > 0 ? agent : null;
|
|
8779
|
+
}
|
|
8780
|
+
async resolveSubagentInvocations(messages, userMessageId, messageId, phase = "terminal") {
|
|
8781
|
+
if (!messages) return void 0;
|
|
8782
|
+
const cache = phase === "prefetch" ? this.subagentInvocationPrefetches : this.subagentInvocationCollections;
|
|
8783
|
+
const cached = cache.get(messageId);
|
|
8784
|
+
if (cached) return cached;
|
|
8785
|
+
const collection = this.buildSubagentInvocations(messages, userMessageId, messageId).catch(
|
|
8786
|
+
(err) => {
|
|
8787
|
+
this.log({
|
|
8788
|
+
level: "warn",
|
|
8789
|
+
message: `Best-effort subagent usage collection failed for message ${messageId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
8790
|
+
message_id: messageId
|
|
8791
|
+
});
|
|
8792
|
+
return void 0;
|
|
8793
|
+
}
|
|
8794
|
+
);
|
|
8795
|
+
cache.set(messageId, collection);
|
|
8796
|
+
const result = await collection;
|
|
8797
|
+
if (result === void 0 && cache.get(messageId) === collection) cache.delete(messageId);
|
|
8798
|
+
return result;
|
|
8799
|
+
}
|
|
8800
|
+
clearSubagentInvocationCaches(messageId) {
|
|
8801
|
+
this.subagentInvocationCollections.delete(messageId);
|
|
8802
|
+
this.subagentInvocationPrefetches.delete(messageId);
|
|
8803
|
+
}
|
|
8804
|
+
async buildSubagentInvocations(messages, userMessageId, messageId) {
|
|
8805
|
+
const rootCalls = collectTaskCalls(messages, userMessageId);
|
|
8806
|
+
if (rootCalls.length === 0) return void 0;
|
|
8807
|
+
const childMessages = /* @__PURE__ */ new Map();
|
|
8808
|
+
const seenCallIds = new Set(rootCalls.map((call) => call.callID));
|
|
8809
|
+
const work = rootCalls.map((call) => ({
|
|
8810
|
+
call,
|
|
8811
|
+
depth: 1
|
|
8812
|
+
}));
|
|
8813
|
+
const payload = [];
|
|
8814
|
+
const fetchChildMessages = (sessionId) => {
|
|
8815
|
+
const cached = childMessages.get(sessionId);
|
|
8816
|
+
if (cached) return cached;
|
|
8817
|
+
const pending = (async () => {
|
|
8818
|
+
try {
|
|
8819
|
+
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
8820
|
+
if (!res.ok) {
|
|
8821
|
+
this.log({
|
|
8822
|
+
level: "warn",
|
|
8823
|
+
message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 omitting invocation telemetry`,
|
|
8824
|
+
message_id: messageId
|
|
8825
|
+
});
|
|
8826
|
+
return null;
|
|
8827
|
+
}
|
|
8828
|
+
const body = await res.json();
|
|
8829
|
+
if (!Array.isArray(body)) throw new Error("response body was not a message array");
|
|
8830
|
+
return body;
|
|
8831
|
+
} catch (err) {
|
|
8832
|
+
this.log({
|
|
8833
|
+
level: "warn",
|
|
8834
|
+
message: `Best-effort subagent session fetch failed for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
8835
|
+
message_id: messageId
|
|
8836
|
+
});
|
|
8837
|
+
return null;
|
|
8838
|
+
}
|
|
8839
|
+
})();
|
|
8840
|
+
childMessages.set(sessionId, pending);
|
|
8841
|
+
return pending;
|
|
8842
|
+
};
|
|
8843
|
+
const fetchChildWithoutBlocking = async (sessionId) => {
|
|
8844
|
+
const pending = fetchChildMessages(sessionId);
|
|
8845
|
+
let timer;
|
|
8846
|
+
const timeout = new Promise((resolve4) => {
|
|
8847
|
+
timer = setTimeout(() => {
|
|
8848
|
+
this.log({
|
|
8849
|
+
level: "warn",
|
|
8850
|
+
message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} was slow \u2014 omitting invocation telemetry without delaying completion`,
|
|
8851
|
+
message_id: messageId
|
|
8852
|
+
});
|
|
8853
|
+
resolve4(null);
|
|
8854
|
+
}, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
|
|
8855
|
+
});
|
|
8856
|
+
try {
|
|
8857
|
+
return await Promise.race([pending, timeout]);
|
|
8858
|
+
} finally {
|
|
8859
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
8860
|
+
}
|
|
8861
|
+
};
|
|
8862
|
+
while (work.length > 0) {
|
|
8863
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8864
|
+
for (const item of work.splice(0)) {
|
|
8865
|
+
const group = groups.get(item.call.childSessionId) ?? [];
|
|
8866
|
+
group.push(item);
|
|
8867
|
+
groups.set(item.call.childSessionId, group);
|
|
8868
|
+
}
|
|
8869
|
+
const groupResults = await Promise.all(
|
|
8870
|
+
[...groups].map(async ([sessionId, items]) => ({
|
|
8871
|
+
sessionId,
|
|
8872
|
+
items,
|
|
8873
|
+
messages: sessionId === null ? [] : await fetchChildWithoutBlocking(sessionId)
|
|
8874
|
+
}))
|
|
8875
|
+
);
|
|
8876
|
+
for (const { sessionId, items, messages: child } of groupResults) {
|
|
8877
|
+
if (sessionId !== null && child === null) continue;
|
|
8878
|
+
const attribution = sessionId === null ? { invocations: [], unattributed: [] } : attributeTaskCallUsage(
|
|
8879
|
+
child,
|
|
8880
|
+
items.map(({ call }) => ({
|
|
8881
|
+
callID: call.callID,
|
|
8882
|
+
timeStart: call.timeStart,
|
|
8883
|
+
timeEnd: call.timeEnd
|
|
8884
|
+
}))
|
|
8885
|
+
);
|
|
8886
|
+
if (sessionId !== null && attribution.unattributed.length > 0) {
|
|
8887
|
+
this.log({
|
|
8888
|
+
level: "warn",
|
|
8889
|
+
message: `Omitted ${attribution.unattributed.length} unattributable assistant message(s) from subagent usage for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} \u2014 assigned to no invocation window`,
|
|
8890
|
+
message_id: messageId
|
|
8891
|
+
});
|
|
8892
|
+
}
|
|
8893
|
+
const usageByCall = new Map(
|
|
8894
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.usage])
|
|
8895
|
+
);
|
|
8896
|
+
const messagesByCall = new Map(
|
|
8897
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.messages])
|
|
8898
|
+
);
|
|
8899
|
+
for (const { call, depth } of items) {
|
|
8900
|
+
const usage = usageByCall.get(call.callID) ?? null;
|
|
8901
|
+
payload.push({
|
|
8902
|
+
tool_call_id: call.callID,
|
|
8903
|
+
agent_name: call.subagentName,
|
|
8904
|
+
opencode_session_id: call.childSessionId,
|
|
8905
|
+
parent_opencode_session_id: call.parentSessionId,
|
|
8906
|
+
depth,
|
|
8907
|
+
status: call.status,
|
|
8908
|
+
started_at: call.timeStart === null ? null : new Date(call.timeStart).toISOString(),
|
|
8909
|
+
ended_at: call.timeEnd === null ? null : new Date(call.timeEnd).toISOString(),
|
|
8910
|
+
usage_provider_id: usage?.usage_provider_id ?? call.model?.providerID ?? null,
|
|
8911
|
+
usage_model_id: usage?.usage_model_id ?? call.model?.modelID ?? null,
|
|
8912
|
+
usage_tokens_input: usage?.usage_tokens_input ?? null,
|
|
8913
|
+
usage_tokens_output: usage?.usage_tokens_output ?? null,
|
|
8914
|
+
usage_tokens_reasoning: usage?.usage_tokens_reasoning ?? null,
|
|
8915
|
+
usage_tokens_cache_read: usage?.usage_tokens_cache_read ?? null,
|
|
8916
|
+
usage_tokens_cache_write: usage?.usage_tokens_cache_write ?? null,
|
|
8917
|
+
usage_cost_usd: usage?.usage_cost_usd ?? null
|
|
8918
|
+
});
|
|
8919
|
+
for (const assigned of messagesByCall.get(call.callID) ?? []) {
|
|
8920
|
+
const parentId = assigned.info?.parentID ?? assigned.parentID;
|
|
8921
|
+
if (!parentId) continue;
|
|
8922
|
+
for (const nested of collectTaskCalls([assigned], parentId)) {
|
|
8923
|
+
if (seenCallIds.has(nested.callID)) continue;
|
|
8924
|
+
seenCallIds.add(nested.callID);
|
|
8925
|
+
work.push({ call: nested, depth: depth + 1 });
|
|
8926
|
+
}
|
|
8927
|
+
}
|
|
8928
|
+
}
|
|
8929
|
+
}
|
|
8930
|
+
}
|
|
8931
|
+
return payload.length > 0 ? payload : void 0;
|
|
8932
|
+
}
|
|
7920
8933
|
/**
|
|
7921
8934
|
* OpenCode's synchronous default session title (e.g.
|
|
7922
8935
|
* `"New session - 1737800000000"`), assigned immediately when a session is
|
|
@@ -8253,6 +9266,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8253
9266
|
throw new Error(`Failed to get pending conversations: HTTP ${res.status}`);
|
|
8254
9267
|
}
|
|
8255
9268
|
const data = await res.json();
|
|
9269
|
+
this.recycleRequestedFlag = data.recycle_requested === true;
|
|
8256
9270
|
let conversations = data.conversations;
|
|
8257
9271
|
if (this.conversationFilter) {
|
|
8258
9272
|
conversations = conversations.filter((c) => c.id === this.conversationFilter);
|
|
@@ -8399,7 +9413,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8399
9413
|
* watcher retries next tick within the
|
|
8400
9414
|
* deadline, Finding 4).
|
|
8401
9415
|
*/
|
|
8402
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
|
|
9416
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
|
|
8403
9417
|
const res = await this.fetchImpl(
|
|
8404
9418
|
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
8405
9419
|
{
|
|
@@ -8415,15 +9429,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8415
9429
|
opencode_session_id: sessionId,
|
|
8416
9430
|
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
8417
9431
|
...title ? { title } : {},
|
|
8418
|
-
...usage ? usage : {}
|
|
9432
|
+
...usage ? usage : {},
|
|
9433
|
+
...usageAgentName ? { usage_agent_name: usageAgentName } : {},
|
|
9434
|
+
...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
|
|
8419
9435
|
})
|
|
8420
9436
|
}
|
|
8421
9437
|
);
|
|
8422
9438
|
this.assertAuth(res, "marking message as done");
|
|
8423
|
-
if (res.ok)
|
|
9439
|
+
if (res.ok) {
|
|
9440
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
9441
|
+
return;
|
|
9442
|
+
}
|
|
8424
9443
|
if (isRetryableStatus(res.status)) {
|
|
8425
9444
|
throw new Error(`marking message as done: HTTP ${res.status}`);
|
|
8426
9445
|
}
|
|
9446
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8427
9447
|
throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
|
|
8428
9448
|
}
|
|
8429
9449
|
/**
|
|
@@ -8438,7 +9458,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8438
9458
|
* exists but is wedged, so the next attempt must get a fresh one
|
|
8439
9459
|
* instead of reusing it — see WI-1's server-side null-clearing PATCH).
|
|
8440
9460
|
*/
|
|
8441
|
-
async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
|
|
9461
|
+
async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
|
|
8442
9462
|
const body = { status: "failed" };
|
|
8443
9463
|
if (sessionId === null) {
|
|
8444
9464
|
body.opencode_session_id = null;
|
|
@@ -8447,23 +9467,33 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8447
9467
|
}
|
|
8448
9468
|
if (error2 !== void 0) body.error = error2;
|
|
8449
9469
|
if (usage) Object.assign(body, usage);
|
|
9470
|
+
if (usageAgentName) body.usage_agent_name = usageAgentName;
|
|
9471
|
+
if (subagentInvocations && subagentInvocations.length > 0) {
|
|
9472
|
+
body.subagent_invocations = subagentInvocations;
|
|
9473
|
+
}
|
|
8450
9474
|
if (failure) {
|
|
8451
9475
|
body.failure_kind = failure.kind;
|
|
8452
9476
|
body.failure_provider_id = failure.providerId;
|
|
8453
9477
|
body.failure_model_id = failure.modelId;
|
|
8454
9478
|
body.failure_reason = failure.reason;
|
|
8455
9479
|
}
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
9480
|
+
try {
|
|
9481
|
+
await this.callWithRetry(
|
|
9482
|
+
"marking message as failed",
|
|
9483
|
+
() => this.fetchImpl(
|
|
9484
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
9485
|
+
{
|
|
9486
|
+
method: "PATCH",
|
|
9487
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
9488
|
+
body: JSON.stringify(body)
|
|
9489
|
+
}
|
|
9490
|
+
)
|
|
9491
|
+
);
|
|
9492
|
+
} catch (err) {
|
|
9493
|
+
if (err instanceof ChannelTerminalError) this.clearSubagentInvocationCaches(messageId);
|
|
9494
|
+
throw err;
|
|
9495
|
+
}
|
|
9496
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8467
9497
|
}
|
|
8468
9498
|
/**
|
|
8469
9499
|
* Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
|
|
@@ -8488,6 +9518,111 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8488
9518
|
reply?.info?.modelID ?? null
|
|
8489
9519
|
);
|
|
8490
9520
|
}
|
|
9521
|
+
async recordSubagentModelAuthFailure(failure, conversationId, messageId) {
|
|
9522
|
+
const providerId = failure.providerId ?? "(unknown)";
|
|
9523
|
+
try {
|
|
9524
|
+
const res = await this.fetchImpl(
|
|
9525
|
+
`${this.apiUrl}/runners/${this.agentId}/model-auth-failures`,
|
|
9526
|
+
{
|
|
9527
|
+
method: "POST",
|
|
9528
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
9529
|
+
body: JSON.stringify({
|
|
9530
|
+
provider_id: failure.providerId,
|
|
9531
|
+
model_id: failure.modelId,
|
|
9532
|
+
reason: failure.reason
|
|
9533
|
+
})
|
|
9534
|
+
}
|
|
9535
|
+
);
|
|
9536
|
+
if (!res.ok) {
|
|
9537
|
+
this.log({
|
|
9538
|
+
level: "warn",
|
|
9539
|
+
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)})`,
|
|
9540
|
+
conversation_id: conversationId,
|
|
9541
|
+
message_id: messageId
|
|
9542
|
+
});
|
|
9543
|
+
}
|
|
9544
|
+
} catch (err) {
|
|
9545
|
+
this.log({
|
|
9546
|
+
level: "warn",
|
|
9547
|
+
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)}`,
|
|
9548
|
+
conversation_id: conversationId,
|
|
9549
|
+
message_id: messageId
|
|
9550
|
+
});
|
|
9551
|
+
}
|
|
9552
|
+
}
|
|
9553
|
+
async clearSubagentModelAuthFailure(providerId, conversationId, messageId) {
|
|
9554
|
+
try {
|
|
9555
|
+
const res = await this.fetchImpl(
|
|
9556
|
+
`${this.apiUrl}/runners/${this.agentId}/model-auth-failures/${encodeURIComponent(providerId)}`,
|
|
9557
|
+
{
|
|
9558
|
+
method: "DELETE",
|
|
9559
|
+
headers: { Authorization: this.getAuthHeader() }
|
|
9560
|
+
}
|
|
9561
|
+
);
|
|
9562
|
+
if (!res.ok) {
|
|
9563
|
+
this.log({
|
|
9564
|
+
level: "warn",
|
|
9565
|
+
message: `Sub-agent model-auth clear for provider ${providerId} returned HTTP ${res.status} (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)})`,
|
|
9566
|
+
conversation_id: conversationId,
|
|
9567
|
+
message_id: messageId
|
|
9568
|
+
});
|
|
9569
|
+
}
|
|
9570
|
+
} catch (err) {
|
|
9571
|
+
this.log({
|
|
9572
|
+
level: "warn",
|
|
9573
|
+
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)}`,
|
|
9574
|
+
conversation_id: conversationId,
|
|
9575
|
+
message_id: messageId
|
|
9576
|
+
});
|
|
9577
|
+
}
|
|
9578
|
+
}
|
|
9579
|
+
async reportSubagentAuthFailures(conversationId, opencodeMessageId, evidentMessageId, messages) {
|
|
9580
|
+
const refs = collectSubagentSessions(messages, opencodeMessageId);
|
|
9581
|
+
if (refs.length === 0) return;
|
|
9582
|
+
const failedProviders = /* @__PURE__ */ new Map();
|
|
9583
|
+
const succeededProviders = /* @__PURE__ */ new Set();
|
|
9584
|
+
for (const ref of refs) {
|
|
9585
|
+
try {
|
|
9586
|
+
const childMessages = await getSessionMessages(this.port, ref.sessionId);
|
|
9587
|
+
if (childMessages === null) {
|
|
9588
|
+
this.log({
|
|
9589
|
+
level: "debug",
|
|
9590
|
+
message: `Could not read sub-agent session ${ref.sessionId} while checking credential failures \u2014 skipping it`,
|
|
9591
|
+
conversation_id: conversationId,
|
|
9592
|
+
message_id: evidentMessageId
|
|
9593
|
+
});
|
|
9594
|
+
continue;
|
|
9595
|
+
}
|
|
9596
|
+
const outcome = findSubagentAuthOutcome(childMessages, ref.startedAtMs);
|
|
9597
|
+
if (!outcome) continue;
|
|
9598
|
+
if (outcome.outcome === "failed") {
|
|
9599
|
+
failedProviders.set(outcome.providerId, outcome.failure);
|
|
9600
|
+
} else {
|
|
9601
|
+
succeededProviders.add(outcome.providerId);
|
|
9602
|
+
}
|
|
9603
|
+
} catch (err) {
|
|
9604
|
+
this.log({
|
|
9605
|
+
level: "warn",
|
|
9606
|
+
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)}`,
|
|
9607
|
+
conversation_id: conversationId,
|
|
9608
|
+
message_id: evidentMessageId
|
|
9609
|
+
});
|
|
9610
|
+
}
|
|
9611
|
+
}
|
|
9612
|
+
for (const [providerId, failure] of failedProviders) {
|
|
9613
|
+
this.log({
|
|
9614
|
+
level: "warn",
|
|
9615
|
+
message: `Sub-agent turn failed on provider ${providerId} (${failure.reason}) \u2014 recording credential evidence`,
|
|
9616
|
+
conversation_id: conversationId,
|
|
9617
|
+
message_id: evidentMessageId
|
|
9618
|
+
});
|
|
9619
|
+
await this.recordSubagentModelAuthFailure(failure, conversationId, evidentMessageId);
|
|
9620
|
+
}
|
|
9621
|
+
for (const providerId of succeededProviders) {
|
|
9622
|
+
if (failedProviders.has(providerId)) continue;
|
|
9623
|
+
await this.clearSubagentModelAuthFailure(providerId, conversationId, evidentMessageId);
|
|
9624
|
+
}
|
|
9625
|
+
}
|
|
8491
9626
|
/**
|
|
8492
9627
|
* Best-effort, SINGLE-ATTEMPT runner→server telemetry ping
|
|
8493
9628
|
* (queued-followup-redrive). `POST .../messages/:id/signal {signal, ...extra}`
|
|
@@ -8641,6 +9776,13 @@ import chalk5 from "chalk";
|
|
|
8641
9776
|
import ora2 from "ora";
|
|
8642
9777
|
import { select as select2 } from "@inquirer/prompts";
|
|
8643
9778
|
var INTERACTIVE_START_TIMEOUT_MS = 3e4;
|
|
9779
|
+
function checkNonInteractivePortConflict(port, isPortInUseFn) {
|
|
9780
|
+
if (isPortInUseFn(port)) {
|
|
9781
|
+
throw new Error(
|
|
9782
|
+
`Port ${port} is already in use by a non-OpenCode process. Free it or pass --port.`
|
|
9783
|
+
);
|
|
9784
|
+
}
|
|
9785
|
+
}
|
|
8644
9786
|
async function ensureOpenCodeRunning(ctx) {
|
|
8645
9787
|
const healthCheck = await checkOpenCodeHealth(ctx.port);
|
|
8646
9788
|
if (healthCheck.healthy) {
|
|
@@ -8688,6 +9830,7 @@ async function ensureOpenCodeRunning(ctx) {
|
|
|
8688
9830
|
}
|
|
8689
9831
|
}
|
|
8690
9832
|
if (!ctx.interactive) {
|
|
9833
|
+
checkNonInteractivePortConflict(ctx.port, isPortInUse);
|
|
8691
9834
|
ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
|
|
8692
9835
|
const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
|
|
8693
9836
|
const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
|
|
@@ -8769,9 +9912,119 @@ Port ${port} is already in use.`));
|
|
|
8769
9912
|
return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
|
|
8770
9913
|
}
|
|
8771
9914
|
|
|
9915
|
+
// src/commands/ensure-opencode-v2.ts
|
|
9916
|
+
import chalk6 from "chalk";
|
|
9917
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
9918
|
+
async function probeOpenCode2WithoutPassword(port) {
|
|
9919
|
+
try {
|
|
9920
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
9921
|
+
signal: AbortSignal.timeout(2e3)
|
|
9922
|
+
});
|
|
9923
|
+
if (response.status === 401) {
|
|
9924
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
9925
|
+
}
|
|
9926
|
+
if (!response.ok) {
|
|
9927
|
+
return { healthy: false, error: `HTTP ${response.status}` };
|
|
9928
|
+
}
|
|
9929
|
+
return { healthy: true };
|
|
9930
|
+
} catch (error2) {
|
|
9931
|
+
return {
|
|
9932
|
+
healthy: false,
|
|
9933
|
+
error: error2 instanceof Error ? error2.message : "Unknown error"
|
|
9934
|
+
};
|
|
9935
|
+
}
|
|
9936
|
+
}
|
|
9937
|
+
function unknownPasswordError(port) {
|
|
9938
|
+
return new Error(
|
|
9939
|
+
`OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
|
|
9940
|
+
);
|
|
9941
|
+
}
|
|
9942
|
+
function v2SessionSupportIncompleteError() {
|
|
9943
|
+
return new Error(
|
|
9944
|
+
"OpenCode V2 (opencode2) session support is incomplete \u2014 see https://github.com/sroze/evident/issues/2075. Not starting a new opencode2 process."
|
|
9945
|
+
);
|
|
9946
|
+
}
|
|
9947
|
+
async function ensureOpenCode2Running(ctx) {
|
|
9948
|
+
const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
|
|
9949
|
+
if (initialHealth.authFailed) {
|
|
9950
|
+
throw unknownPasswordError(ctx.port);
|
|
9951
|
+
}
|
|
9952
|
+
if (initialHealth.healthy) {
|
|
9953
|
+
return {
|
|
9954
|
+
port: ctx.port,
|
|
9955
|
+
process: null,
|
|
9956
|
+
version: null,
|
|
9957
|
+
notReadyReason: null,
|
|
9958
|
+
password: null
|
|
9959
|
+
};
|
|
9960
|
+
}
|
|
9961
|
+
if (!isOpenCode2Installed()) {
|
|
9962
|
+
throw new Error(
|
|
9963
|
+
"OpenCode V2 (opencode2) is not installed. Install it with: npm install -g @opencode-ai/cli@beta"
|
|
9964
|
+
);
|
|
9965
|
+
}
|
|
9966
|
+
let port = ctx.port;
|
|
9967
|
+
if (!ctx.interactive) {
|
|
9968
|
+
checkNonInteractivePortConflict(port, isPortInUse);
|
|
9969
|
+
} else if (isPortInUse(port)) {
|
|
9970
|
+
console.log(chalk6.yellow(`
|
|
9971
|
+
Port ${port} is already in use.`));
|
|
9972
|
+
const alternativePort = findAvailablePort(port + 1);
|
|
9973
|
+
if (alternativePort) {
|
|
9974
|
+
const useAlternative = await select3({
|
|
9975
|
+
message: `Use port ${alternativePort} instead?`,
|
|
9976
|
+
choices: [
|
|
9977
|
+
{ name: `Yes, use port ${alternativePort}`, value: "yes" },
|
|
9978
|
+
{ name: "No, I will free the port manually", value: "no" }
|
|
9979
|
+
]
|
|
9980
|
+
});
|
|
9981
|
+
if (useAlternative === "yes") {
|
|
9982
|
+
port = alternativePort;
|
|
9983
|
+
} else {
|
|
9984
|
+
throw new Error(`Port ${ctx.port} is in use`);
|
|
9985
|
+
}
|
|
9986
|
+
}
|
|
9987
|
+
}
|
|
9988
|
+
if (!ctx.interactive) {
|
|
9989
|
+
throw v2SessionSupportIncompleteError();
|
|
9990
|
+
}
|
|
9991
|
+
console.log(chalk6.yellow(`
|
|
9992
|
+
${v2SessionSupportIncompleteError().message}`));
|
|
9993
|
+
const action = await select3({
|
|
9994
|
+
message: "OpenCode V2 is not running. What would you like to do?",
|
|
9995
|
+
choices: [
|
|
9996
|
+
{
|
|
9997
|
+
name: "Show me the command",
|
|
9998
|
+
value: "manual",
|
|
9999
|
+
description: "Display the command to run manually"
|
|
10000
|
+
},
|
|
10001
|
+
{
|
|
10002
|
+
name: "Continue without OpenCode V2",
|
|
10003
|
+
value: "continue",
|
|
10004
|
+
description: "Requests will fail until OpenCode V2 starts"
|
|
10005
|
+
}
|
|
10006
|
+
]
|
|
10007
|
+
});
|
|
10008
|
+
if (action === "manual") {
|
|
10009
|
+
blank();
|
|
10010
|
+
console.log(chalk6.bold("Run this command in another terminal:"));
|
|
10011
|
+
blank();
|
|
10012
|
+
console.log(` ${chalk6.cyan(`opencode2 serve --port ${port}`)}`);
|
|
10013
|
+
blank();
|
|
10014
|
+
throw new Error("Please start OpenCode V2 manually");
|
|
10015
|
+
}
|
|
10016
|
+
return {
|
|
10017
|
+
port,
|
|
10018
|
+
process: null,
|
|
10019
|
+
version: null,
|
|
10020
|
+
notReadyReason: "you chose to continue without OpenCode V2",
|
|
10021
|
+
password: null
|
|
10022
|
+
};
|
|
10023
|
+
}
|
|
10024
|
+
|
|
8772
10025
|
// src/lib/runner-credentials.ts
|
|
8773
|
-
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
8774
|
-
import { spawn as spawn5 } from "child_process";
|
|
10026
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
10027
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
8775
10028
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
8776
10029
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
8777
10030
|
var GITHUB_PROBE_TIMEOUT_MS = 1e4;
|
|
@@ -9043,11 +10296,11 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
9043
10296
|
}
|
|
9044
10297
|
|
|
9045
10298
|
// 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
|
|
10299
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
10300
|
+
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
10301
|
+
import { isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "node:path";
|
|
9049
10302
|
function isFile(filePath) {
|
|
9050
|
-
return existsSync2(filePath) &&
|
|
10303
|
+
return existsSync2(filePath) && statSync6(filePath).isFile();
|
|
9051
10304
|
}
|
|
9052
10305
|
function applyRunnerOpenCodeConfig({
|
|
9053
10306
|
overlayPath,
|
|
@@ -9059,7 +10312,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9059
10312
|
return;
|
|
9060
10313
|
}
|
|
9061
10314
|
const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
|
|
9062
|
-
const target = isFile(
|
|
10315
|
+
const target = isFile(join9(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
9063
10316
|
if (!isFile(source)) {
|
|
9064
10317
|
log3(
|
|
9065
10318
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -9067,7 +10320,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9067
10320
|
);
|
|
9068
10321
|
return;
|
|
9069
10322
|
}
|
|
9070
|
-
copyFileSync(source,
|
|
10323
|
+
copyFileSync(source, join9(cwd, target));
|
|
9071
10324
|
try {
|
|
9072
10325
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
9073
10326
|
stdio: "ignore"
|
|
@@ -9076,7 +10329,242 @@ function applyRunnerOpenCodeConfig({
|
|
|
9076
10329
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
9077
10330
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
9078
10331
|
}
|
|
9079
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
10332
|
+
log3(`Applied runner OpenCode config ${source} to ${join9(cwd, target)}`);
|
|
10333
|
+
}
|
|
10334
|
+
|
|
10335
|
+
// src/lib/credential-sync.ts
|
|
10336
|
+
import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
10337
|
+
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
10338
|
+
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
10339
|
+
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
10340
|
+
var DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS = 60;
|
|
10341
|
+
var STORES = ["claude", "opencode"];
|
|
10342
|
+
var MAX_FLUSH_PASSES = 2;
|
|
10343
|
+
function outcomesWith(outcome) {
|
|
10344
|
+
return { claude: outcome, opencode: outcome };
|
|
10345
|
+
}
|
|
10346
|
+
function errorMessage2(error2) {
|
|
10347
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
10348
|
+
}
|
|
10349
|
+
function waitForSettlement(promise, timeoutMs) {
|
|
10350
|
+
return new Promise((resolve4) => {
|
|
10351
|
+
let settled = false;
|
|
10352
|
+
const timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
|
|
10353
|
+
const finish = (value) => {
|
|
10354
|
+
if (settled) return;
|
|
10355
|
+
settled = true;
|
|
10356
|
+
clearTimeout(timer);
|
|
10357
|
+
resolve4(value);
|
|
10358
|
+
};
|
|
10359
|
+
promise.then(
|
|
10360
|
+
() => finish(true),
|
|
10361
|
+
() => finish(true)
|
|
10362
|
+
);
|
|
10363
|
+
});
|
|
10364
|
+
}
|
|
10365
|
+
function writeMarker(markerPath, outcomes, log3) {
|
|
10366
|
+
const body = `${STORES.map((store) => `${store}=${outcomes[store]}`).join("\n")}
|
|
10367
|
+
`;
|
|
10368
|
+
const temporaryPath = `${markerPath}.tmp`;
|
|
10369
|
+
try {
|
|
10370
|
+
writeFileSync5(temporaryPath, body, { mode: 384 });
|
|
10371
|
+
renameSync(temporaryPath, markerPath);
|
|
10372
|
+
} catch (error2) {
|
|
10373
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
|
|
10374
|
+
}
|
|
10375
|
+
}
|
|
10376
|
+
function intervalSeconds(env, log3) {
|
|
10377
|
+
const raw = env.CREDS_SYNC_INTERVAL;
|
|
10378
|
+
if (raw === void 0 || /^[1-9][0-9]*$/.test(raw) && Number.isSafeInteger(Number(raw))) {
|
|
10379
|
+
return raw === void 0 ? DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS : Number(raw);
|
|
10380
|
+
}
|
|
10381
|
+
log3(
|
|
10382
|
+
`CREDENTIAL-SYNC-INTERVAL-INVALID: CREDS_SYNC_INTERVAL='${raw}' is not a positive integer; using ${DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS}s`,
|
|
10383
|
+
"warn"
|
|
10384
|
+
);
|
|
10385
|
+
return DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS;
|
|
10386
|
+
}
|
|
10387
|
+
async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
10388
|
+
const remainingMs = deadlineAt - Date.now();
|
|
10389
|
+
if (remainingMs <= 0) return { outcome: "timeout", orphaned: false };
|
|
10390
|
+
const controller = new AbortController();
|
|
10391
|
+
let result;
|
|
10392
|
+
let failed = false;
|
|
10393
|
+
const completion = Promise.resolve().then(
|
|
10394
|
+
() => synchroniserRunner(["sync-once", store], {
|
|
10395
|
+
timeoutMs: remainingMs,
|
|
10396
|
+
env,
|
|
10397
|
+
signal: controller.signal
|
|
10398
|
+
})
|
|
10399
|
+
).then(
|
|
10400
|
+
(value) => {
|
|
10401
|
+
result = value;
|
|
10402
|
+
},
|
|
10403
|
+
(error2) => {
|
|
10404
|
+
failed = true;
|
|
10405
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
|
|
10406
|
+
}
|
|
10407
|
+
);
|
|
10408
|
+
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
10409
|
+
const settledBeforeDeadline = await waitForSettlement(completion, remainingMs);
|
|
10410
|
+
clearTimeout(abortTimer);
|
|
10411
|
+
if (!settledBeforeDeadline) {
|
|
10412
|
+
controller.abort();
|
|
10413
|
+
const settledAfterAbort = await waitForSettlement(completion, CREDENTIAL_FLUSH_ABORT_GRACE_MS);
|
|
10414
|
+
if (!settledAfterAbort) return { outcome: "timeout", orphaned: true };
|
|
10415
|
+
return { outcome: "timeout", orphaned: false };
|
|
10416
|
+
}
|
|
10417
|
+
if (failed || !result) return { outcome: "failed", orphaned: false };
|
|
10418
|
+
if (result.timedOut || Date.now() >= deadlineAt) {
|
|
10419
|
+
return { outcome: "timeout", orphaned: false };
|
|
10420
|
+
}
|
|
10421
|
+
return { outcome: result.code === 0 ? "ok" : "failed", orphaned: false };
|
|
10422
|
+
}
|
|
10423
|
+
function createCredentialSync({
|
|
10424
|
+
markerPath,
|
|
10425
|
+
env,
|
|
10426
|
+
log: log3,
|
|
10427
|
+
synchroniserRunner = runSynchroniser
|
|
10428
|
+
}) {
|
|
10429
|
+
const persistenceDisabled = !env.PERSISTENCE_BUCKET;
|
|
10430
|
+
let disabled = persistenceDisabled;
|
|
10431
|
+
let armed = false;
|
|
10432
|
+
let stopped = false;
|
|
10433
|
+
let timer;
|
|
10434
|
+
let inFlight;
|
|
10435
|
+
let activeTickAbort;
|
|
10436
|
+
let lastTickFailed;
|
|
10437
|
+
let flushPromise;
|
|
10438
|
+
const scheduleTick = (intervalMs, startTick2) => {
|
|
10439
|
+
if (stopped) return;
|
|
10440
|
+
timer = setTimeout(() => {
|
|
10441
|
+
timer = void 0;
|
|
10442
|
+
startTick2();
|
|
10443
|
+
}, intervalMs);
|
|
10444
|
+
};
|
|
10445
|
+
const startTick = (intervalMs) => {
|
|
10446
|
+
if (stopped) return;
|
|
10447
|
+
const controller = new AbortController();
|
|
10448
|
+
activeTickAbort = controller;
|
|
10449
|
+
const tick = (async () => {
|
|
10450
|
+
const outcomes = {
|
|
10451
|
+
claude: "failed",
|
|
10452
|
+
opencode: "failed"
|
|
10453
|
+
};
|
|
10454
|
+
for (const store of STORES) {
|
|
10455
|
+
if (controller.signal.aborted) break;
|
|
10456
|
+
try {
|
|
10457
|
+
const result = await synchroniserRunner(["sync-once", store], {
|
|
10458
|
+
timeoutMs: CREDENTIAL_SYNC_TICK_TIMEOUT_MS,
|
|
10459
|
+
env,
|
|
10460
|
+
signal: controller.signal
|
|
10461
|
+
});
|
|
10462
|
+
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
10463
|
+
} catch (error2) {
|
|
10464
|
+
outcomes[store] = "failed";
|
|
10465
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
|
|
10466
|
+
}
|
|
10467
|
+
}
|
|
10468
|
+
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
10469
|
+
log3(
|
|
10470
|
+
`CREDENTIAL-SYNC-TICK: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
|
|
10471
|
+
"debug"
|
|
10472
|
+
);
|
|
10473
|
+
if (failed && lastTickFailed !== true) {
|
|
10474
|
+
log3(
|
|
10475
|
+
"CREDENTIAL-SYNC-FAILED: an interval credential sync failed; retrying on the next tick",
|
|
10476
|
+
"warn"
|
|
10477
|
+
);
|
|
10478
|
+
} else if (!failed && lastTickFailed === true) {
|
|
10479
|
+
log3("CREDENTIAL-SYNC-RECOVERED: interval credential sync succeeded again", "warn");
|
|
10480
|
+
}
|
|
10481
|
+
lastTickFailed = failed;
|
|
10482
|
+
})().finally(() => {
|
|
10483
|
+
if (activeTickAbort === controller) activeTickAbort = void 0;
|
|
10484
|
+
if (inFlight === tick) inFlight = void 0;
|
|
10485
|
+
scheduleTick(intervalMs, () => startTick(intervalMs));
|
|
10486
|
+
});
|
|
10487
|
+
inFlight = tick;
|
|
10488
|
+
};
|
|
10489
|
+
const performFlush = async () => {
|
|
10490
|
+
stopped = true;
|
|
10491
|
+
if (timer) {
|
|
10492
|
+
clearTimeout(timer);
|
|
10493
|
+
timer = void 0;
|
|
10494
|
+
}
|
|
10495
|
+
const deadlineAt = Date.now() + CREDENTIAL_FLUSH_DEADLINE_MS;
|
|
10496
|
+
if (inFlight) {
|
|
10497
|
+
const settled = await waitForSettlement(inFlight, CREDENTIAL_FLUSH_DEADLINE_MS);
|
|
10498
|
+
if (!settled) {
|
|
10499
|
+
activeTickAbort?.abort();
|
|
10500
|
+
const settledAfterAbort = await waitForSettlement(
|
|
10501
|
+
inFlight,
|
|
10502
|
+
CREDENTIAL_FLUSH_ABORT_GRACE_MS
|
|
10503
|
+
);
|
|
10504
|
+
if (!settledAfterAbort) {
|
|
10505
|
+
log3(
|
|
10506
|
+
"CREDENTIAL-FLUSH-ORPHANED-TICK: a sync-once child could not be proven settled; skipping the boundary flush rather than becoming a second writer",
|
|
10507
|
+
"warn"
|
|
10508
|
+
);
|
|
10509
|
+
return { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
10510
|
+
}
|
|
10511
|
+
}
|
|
10512
|
+
}
|
|
10513
|
+
if (disabled) return { outcomes: outcomesWith("skipped"), orphaned: false };
|
|
10514
|
+
const outcomes = outcomesWith("timeout");
|
|
10515
|
+
for (const store of STORES) {
|
|
10516
|
+
const result = await runFlushStore(store, env, synchroniserRunner, log3, deadlineAt);
|
|
10517
|
+
if (result.orphaned) {
|
|
10518
|
+
log3(
|
|
10519
|
+
"CREDENTIAL-FLUSH-ORPHANED-CHILD: a flush sync-once child could not be proven settled; refusing another flush pass rather than becoming a second writer",
|
|
10520
|
+
"warn"
|
|
10521
|
+
);
|
|
10522
|
+
return { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
10523
|
+
}
|
|
10524
|
+
outcomes[store] = result.outcome;
|
|
10525
|
+
}
|
|
10526
|
+
return { outcomes, orphaned: false };
|
|
10527
|
+
};
|
|
10528
|
+
let flushPasses = 0;
|
|
10529
|
+
let lastFlush;
|
|
10530
|
+
return {
|
|
10531
|
+
arm() {
|
|
10532
|
+
if (stopped || armed) return;
|
|
10533
|
+
armed = true;
|
|
10534
|
+
if (persistenceDisabled) {
|
|
10535
|
+
disabled = true;
|
|
10536
|
+
log3(
|
|
10537
|
+
"CREDENTIAL-SYNC-DISABLED: LITESTREAM_BUCKET/LITESTREAM_PREFIX are not both set; no interval credential sync this boot",
|
|
10538
|
+
"warn"
|
|
10539
|
+
);
|
|
10540
|
+
return;
|
|
10541
|
+
}
|
|
10542
|
+
disabled = false;
|
|
10543
|
+
const intervalMs = intervalSeconds(env, log3) * 1e3;
|
|
10544
|
+
scheduleTick(intervalMs, () => startTick(intervalMs));
|
|
10545
|
+
},
|
|
10546
|
+
async stopAndFlush(publish) {
|
|
10547
|
+
let result;
|
|
10548
|
+
const runningFlush = flushPromise;
|
|
10549
|
+
if (runningFlush) {
|
|
10550
|
+
result = await runningFlush;
|
|
10551
|
+
} else if (flushPasses >= MAX_FLUSH_PASSES || lastFlush?.orphaned) {
|
|
10552
|
+
result = lastFlush ?? { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
10553
|
+
} else {
|
|
10554
|
+
flushPasses++;
|
|
10555
|
+
const currentFlush = performFlush();
|
|
10556
|
+
flushPromise = currentFlush;
|
|
10557
|
+
try {
|
|
10558
|
+
result = await currentFlush;
|
|
10559
|
+
lastFlush = result;
|
|
10560
|
+
} finally {
|
|
10561
|
+
if (flushPromise === currentFlush) flushPromise = void 0;
|
|
10562
|
+
}
|
|
10563
|
+
}
|
|
10564
|
+
if (publish) writeMarker(markerPath, result.outcomes, log3);
|
|
10565
|
+
return result.outcomes;
|
|
10566
|
+
}
|
|
10567
|
+
};
|
|
9080
10568
|
}
|
|
9081
10569
|
|
|
9082
10570
|
// src/commands/run.ts
|
|
@@ -9116,7 +10604,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9116
10604
|
if (trimmed === "") {
|
|
9117
10605
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
9118
10606
|
}
|
|
9119
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
10607
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join10(homeDir, trimmed.slice(2)) : trimmed;
|
|
9120
10608
|
if (!isAbsolute3(expanded)) {
|
|
9121
10609
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
9122
10610
|
}
|
|
@@ -9140,6 +10628,28 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9140
10628
|
var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
|
|
9141
10629
|
var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
|
|
9142
10630
|
var OPENCODE_START_TIMEOUT_ENV = "EVIDENT_OPENCODE_START_TIMEOUT";
|
|
10631
|
+
var OPENCODE_VERSION_ENV = "EVIDENT_OPENCODE_VERSION";
|
|
10632
|
+
function resolveOpenCodeVersion(options, env = process.env) {
|
|
10633
|
+
let raw;
|
|
10634
|
+
let source;
|
|
10635
|
+
if (options.opencodeVersion !== void 0) {
|
|
10636
|
+
raw = options.opencodeVersion;
|
|
10637
|
+
source = "--opencode-version";
|
|
10638
|
+
} else if (env[OPENCODE_VERSION_ENV] !== void 0 && env[OPENCODE_VERSION_ENV] !== "") {
|
|
10639
|
+
raw = env[OPENCODE_VERSION_ENV];
|
|
10640
|
+
source = OPENCODE_VERSION_ENV;
|
|
10641
|
+
} else {
|
|
10642
|
+
return { version: "v1", warnings: [] };
|
|
10643
|
+
}
|
|
10644
|
+
const normalized = raw.trim().toLowerCase();
|
|
10645
|
+
if (normalized !== "v1" && normalized !== "v2") {
|
|
10646
|
+
return {
|
|
10647
|
+
version: "v1",
|
|
10648
|
+
warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or v2; using the default v1`]
|
|
10649
|
+
};
|
|
10650
|
+
}
|
|
10651
|
+
return { version: normalized, warnings: [] };
|
|
10652
|
+
}
|
|
9143
10653
|
function resolveOpenCodeStartTimeoutMs(options, env = process.env) {
|
|
9144
10654
|
const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1e3;
|
|
9145
10655
|
let raw;
|
|
@@ -9206,7 +10716,7 @@ function log2(state, message, level = "info") {
|
|
|
9206
10716
|
})
|
|
9207
10717
|
);
|
|
9208
10718
|
} else if (!state.interactive) {
|
|
9209
|
-
const prefix = level === "error" ?
|
|
10719
|
+
const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
|
|
9210
10720
|
console.log(`${prefix} ${message}`);
|
|
9211
10721
|
}
|
|
9212
10722
|
}
|
|
@@ -9236,7 +10746,7 @@ function logActivity(state, entry) {
|
|
|
9236
10746
|
}
|
|
9237
10747
|
function reportSessionDbRecovery(state) {
|
|
9238
10748
|
try {
|
|
9239
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
10749
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
|
|
9240
10750
|
for (const record of report.records) {
|
|
9241
10751
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
9242
10752
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -9267,18 +10777,18 @@ function reportSessionDbRecoveryRecord(state, record) {
|
|
|
9267
10777
|
function displayStatus(state) {
|
|
9268
10778
|
if (!state.interactive) return;
|
|
9269
10779
|
const attempt = state.connection?.reconnectAttempt ?? 0;
|
|
9270
|
-
const tunnel = state.connected ?
|
|
9271
|
-
const opencode = state.opencodeConnected ?
|
|
9272
|
-
const messages = state.messageCount > 0 ?
|
|
10780
|
+
const tunnel = state.connected ? chalk7.green("tunnel: connected") : attempt > 0 ? chalk7.yellow(`tunnel: reconnecting (#${attempt})`) : chalk7.yellow("tunnel: connecting");
|
|
10781
|
+
const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
|
|
10782
|
+
const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
|
|
9273
10783
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
9274
|
-
const detail = last ?
|
|
10784
|
+
const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
|
|
9275
10785
|
const agent = state.agentName ?? state.agentId;
|
|
9276
10786
|
console.log(
|
|
9277
|
-
`${
|
|
10787
|
+
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
9278
10788
|
);
|
|
9279
10789
|
}
|
|
9280
10790
|
async function promptForLogin(promptMessage, successMessage) {
|
|
9281
|
-
const action = await
|
|
10791
|
+
const action = await select4({
|
|
9282
10792
|
message: promptMessage,
|
|
9283
10793
|
choices: [
|
|
9284
10794
|
{
|
|
@@ -9294,7 +10804,7 @@ async function promptForLogin(promptMessage, successMessage) {
|
|
|
9294
10804
|
]
|
|
9295
10805
|
});
|
|
9296
10806
|
if (action === "exit") {
|
|
9297
|
-
console.log(
|
|
10807
|
+
console.log(chalk7.dim(`
|
|
9298
10808
|
You can log in later by running: ${getCliName()} login`));
|
|
9299
10809
|
process.exit(0);
|
|
9300
10810
|
}
|
|
@@ -9305,7 +10815,7 @@ You can log in later by running: ${getCliName()} login`));
|
|
|
9305
10815
|
process.exit(1);
|
|
9306
10816
|
}
|
|
9307
10817
|
blank();
|
|
9308
|
-
console.log(
|
|
10818
|
+
console.log(chalk7.green(successMessage));
|
|
9309
10819
|
blank();
|
|
9310
10820
|
return { token: credentials2.token, authType: "bearer", user: credentials2.user };
|
|
9311
10821
|
}
|
|
@@ -9318,12 +10828,12 @@ async function handleAuthError(state, error2) {
|
|
|
9318
10828
|
if (state.interactive) displayStatus(state);
|
|
9319
10829
|
if (!state.interactive) {
|
|
9320
10830
|
blank();
|
|
9321
|
-
console.log(
|
|
9322
|
-
console.log(
|
|
10831
|
+
console.log(chalk7.red("Authentication expired"));
|
|
10832
|
+
console.log(chalk7.dim("Your authentication token is no longer valid."));
|
|
9323
10833
|
blank();
|
|
9324
|
-
console.log(
|
|
9325
|
-
console.log(
|
|
9326
|
-
console.log(
|
|
10834
|
+
console.log(chalk7.dim("To fix this:"));
|
|
10835
|
+
console.log(chalk7.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
|
|
10836
|
+
console.log(chalk7.dim(" 2. Restart this command"));
|
|
9327
10837
|
blank();
|
|
9328
10838
|
await cleanup(state);
|
|
9329
10839
|
await shutdownTelemetry();
|
|
@@ -9331,7 +10841,7 @@ async function handleAuthError(state, error2) {
|
|
|
9331
10841
|
return { success: false };
|
|
9332
10842
|
}
|
|
9333
10843
|
blank();
|
|
9334
|
-
console.log(
|
|
10844
|
+
console.log(chalk7.yellow("Your authentication has expired."));
|
|
9335
10845
|
blank();
|
|
9336
10846
|
try {
|
|
9337
10847
|
const credentials2 = await promptForLogin(
|
|
@@ -9376,6 +10886,10 @@ async function driveChannels(state, driver) {
|
|
|
9376
10886
|
consecutiveDrainFailures = 0;
|
|
9377
10887
|
unreachableMs = 0;
|
|
9378
10888
|
state.messageCount += processed;
|
|
10889
|
+
if (driver.recycleRequested) {
|
|
10890
|
+
await beginGracefulShutdown(state, "recycle");
|
|
10891
|
+
return;
|
|
10892
|
+
}
|
|
9379
10893
|
const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
|
|
9380
10894
|
lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
|
|
9381
10895
|
const fileActivitySnapshot = driver.fileSyncActivity();
|
|
@@ -9391,6 +10905,14 @@ async function driveChannels(state, driver) {
|
|
|
9391
10905
|
const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
|
|
9392
10906
|
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
9393
10907
|
if (opencodeAuthApplied) state.openaiUsageRearm?.();
|
|
10908
|
+
if (claudeCredentialApplied || opencodeAuthApplied) {
|
|
10909
|
+
void reloadProviderCache(state.port).catch(
|
|
10910
|
+
(error2) => logActivity(state, {
|
|
10911
|
+
type: "error",
|
|
10912
|
+
error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
10913
|
+
})
|
|
10914
|
+
);
|
|
10915
|
+
}
|
|
9394
10916
|
if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
|
|
9395
10917
|
idlePolls = 0;
|
|
9396
10918
|
idleMs = 0;
|
|
@@ -9418,8 +10940,8 @@ async function driveChannels(state, driver) {
|
|
|
9418
10940
|
state.running = false;
|
|
9419
10941
|
break;
|
|
9420
10942
|
}
|
|
9421
|
-
const
|
|
9422
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
10943
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
|
|
10944
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage3}` });
|
|
9423
10945
|
if (state.interactive) displayStatus(state);
|
|
9424
10946
|
if (driver.hasInFlightWatchers()) {
|
|
9425
10947
|
consecutiveDrainFailures = 0;
|
|
@@ -9457,9 +10979,18 @@ async function driveChannels(state, driver) {
|
|
|
9457
10979
|
}
|
|
9458
10980
|
}
|
|
9459
10981
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
9460
|
-
var SESSION_DB_RECLAIM_MAX_PAGES =
|
|
10982
|
+
var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
|
|
10983
|
+
function shouldWarnForReclaimSkip(reason) {
|
|
10984
|
+
if (reason !== "sqlite-unavailable") return false;
|
|
10985
|
+
const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
|
|
10986
|
+
if (!version2) return false;
|
|
10987
|
+
const major = Number(version2[1]);
|
|
10988
|
+
const minor = Number(version2[2]);
|
|
10989
|
+
const patch = Number(version2[3]);
|
|
10990
|
+
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
10991
|
+
}
|
|
9461
10992
|
function sessionDbPath() {
|
|
9462
|
-
return
|
|
10993
|
+
return join10(homedir6(), ".local", "share", "opencode", "opencode.db");
|
|
9463
10994
|
}
|
|
9464
10995
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
9465
10996
|
const record = {
|
|
@@ -9555,7 +11086,7 @@ async function runSweep(state, driver, config) {
|
|
|
9555
11086
|
} else {
|
|
9556
11087
|
logActivity(state, {
|
|
9557
11088
|
type: "info",
|
|
9558
|
-
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})`
|
|
11089
|
+
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : "")
|
|
9559
11090
|
});
|
|
9560
11091
|
}
|
|
9561
11092
|
} catch (error2) {
|
|
@@ -9578,13 +11109,20 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
9578
11109
|
for (const warning2 of config.warnings) {
|
|
9579
11110
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
9580
11111
|
}
|
|
9581
|
-
const dbBytes = statSessionDbBytes(
|
|
11112
|
+
const dbBytes = statSessionDbBytes(homedir6());
|
|
9582
11113
|
void (async () => {
|
|
9583
|
-
const
|
|
11114
|
+
const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
11115
|
+
if (reclaimAvailability !== null) {
|
|
11116
|
+
logActivity(state, {
|
|
11117
|
+
type: "info",
|
|
11118
|
+
level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
|
|
11119
|
+
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
|
|
11120
|
+
});
|
|
11121
|
+
}
|
|
9584
11122
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
9585
11123
|
dbBytes,
|
|
9586
11124
|
cleanupEnabled: config.enabled,
|
|
9587
|
-
reclaimSkipReason
|
|
11125
|
+
reclaimSkipReason: reclaimAvailability?.reason ?? null
|
|
9588
11126
|
});
|
|
9589
11127
|
if (sizeWarning !== null) {
|
|
9590
11128
|
logActivity(state, { type: "info", level: "warn", message: sizeWarning });
|
|
@@ -9779,7 +11317,8 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
9779
11317
|
});
|
|
9780
11318
|
return;
|
|
9781
11319
|
}
|
|
9782
|
-
const collect = createResourceUsageCollector(
|
|
11320
|
+
const { collect, stop } = createResourceUsageCollector(homedir6());
|
|
11321
|
+
state.stopResourceUsageSampling = stop;
|
|
9783
11322
|
let consecutiveFailures = 0;
|
|
9784
11323
|
const tick = async () => {
|
|
9785
11324
|
try {
|
|
@@ -9875,6 +11414,8 @@ async function cleanup(state, opts = {}) {
|
|
|
9875
11414
|
clearTimeout(timer);
|
|
9876
11415
|
}
|
|
9877
11416
|
state.sessionCleanupTimers = [];
|
|
11417
|
+
state.stopOpenCodeLogTail?.();
|
|
11418
|
+
state.stopOpenCodeLogTail = null;
|
|
9878
11419
|
if (state.claudeUsageTimer) {
|
|
9879
11420
|
clearTimeout(state.claudeUsageTimer);
|
|
9880
11421
|
state.claudeUsageTimer = null;
|
|
@@ -9889,21 +11430,41 @@ async function cleanup(state, opts = {}) {
|
|
|
9889
11430
|
clearTimeout(state.resourceUsageTimer);
|
|
9890
11431
|
state.resourceUsageTimer = null;
|
|
9891
11432
|
}
|
|
11433
|
+
state.stopResourceUsageSampling?.();
|
|
11434
|
+
state.stopResourceUsageSampling = null;
|
|
11435
|
+
const credentialSync = state.credentialSync;
|
|
11436
|
+
const flushCredentials = credentialSync ? async (phase, publish) => {
|
|
11437
|
+
await timeShutdownPhase(state, durations, phase, async () => {
|
|
11438
|
+
const outcomes = await credentialSync.stopAndFlush(publish);
|
|
11439
|
+
const level = Object.values(outcomes).every((outcome) => outcome === "ok") ? "info" : "warn";
|
|
11440
|
+
log2(
|
|
11441
|
+
state,
|
|
11442
|
+
`Credential flush: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
|
|
11443
|
+
level
|
|
11444
|
+
);
|
|
11445
|
+
});
|
|
11446
|
+
} : void 0;
|
|
11447
|
+
let drainSettled = true;
|
|
9892
11448
|
if (opts.graceful && state.channelDriver) {
|
|
9893
11449
|
state.channelDriver.stop();
|
|
11450
|
+
}
|
|
11451
|
+
if (flushCredentials) {
|
|
11452
|
+
await flushCredentials("credential_flush", !(opts.graceful && state.channelDriver));
|
|
11453
|
+
}
|
|
11454
|
+
if (opts.graceful && state.channelDriver) {
|
|
9894
11455
|
log2(state, "Draining in-flight channel work before shutdown...");
|
|
9895
11456
|
if (state.interactive) {
|
|
9896
11457
|
logActivity(state, { type: "info", message: "Draining in-flight work before shutdown..." });
|
|
9897
11458
|
displayStatus(state);
|
|
9898
11459
|
}
|
|
9899
11460
|
const driver = state.channelDriver;
|
|
9900
|
-
|
|
11461
|
+
drainSettled = await timeShutdownPhase(
|
|
9901
11462
|
state,
|
|
9902
11463
|
durations,
|
|
9903
11464
|
"drain",
|
|
9904
11465
|
() => driver.waitForInFlight(SHUTDOWN_DRAIN_TIMEOUT_MS)
|
|
9905
11466
|
);
|
|
9906
|
-
if (!
|
|
11467
|
+
if (!drainSettled) {
|
|
9907
11468
|
logActivity(state, {
|
|
9908
11469
|
type: "info",
|
|
9909
11470
|
message: "Shutdown drain timed out with work still in flight \u2014 leaving it for restart recovery"
|
|
@@ -9911,6 +11472,9 @@ async function cleanup(state, opts = {}) {
|
|
|
9911
11472
|
if (state.interactive) displayStatus(state);
|
|
9912
11473
|
}
|
|
9913
11474
|
}
|
|
11475
|
+
if (opts.graceful && state.channelDriver && flushCredentials && drainSettled) {
|
|
11476
|
+
await flushCredentials("credential_flush_final", true);
|
|
11477
|
+
}
|
|
9914
11478
|
await timeShutdownPhase(state, durations, "offline_notify", () => notifyOffline(state));
|
|
9915
11479
|
if (state.connection) {
|
|
9916
11480
|
const connection = state.connection;
|
|
@@ -9946,13 +11510,51 @@ async function cleanup(state, opts = {}) {
|
|
|
9946
11510
|
}
|
|
9947
11511
|
return durations;
|
|
9948
11512
|
}
|
|
11513
|
+
async function beginGracefulShutdown(state, trigger) {
|
|
11514
|
+
if (state.shuttingDown) return;
|
|
11515
|
+
state.shuttingDown = true;
|
|
11516
|
+
const shutdownStartedAt = Date.now();
|
|
11517
|
+
const shutdownMessage = trigger === "recycle" ? "Recycle requested \u2014 finishing in-flight work, then exiting" : "Shutting down...";
|
|
11518
|
+
if (state.interactive) {
|
|
11519
|
+
logActivity(state, { type: "info", message: shutdownMessage });
|
|
11520
|
+
displayStatus(state);
|
|
11521
|
+
} else {
|
|
11522
|
+
log2(state, shutdownMessage);
|
|
11523
|
+
}
|
|
11524
|
+
const durations = await cleanup(state, { graceful: true });
|
|
11525
|
+
const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
|
|
11526
|
+
await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
|
|
11527
|
+
let timer;
|
|
11528
|
+
const flushed = shutdownTelemetry().then(
|
|
11529
|
+
() => true,
|
|
11530
|
+
(error2) => {
|
|
11531
|
+
log2(
|
|
11532
|
+
state,
|
|
11533
|
+
`Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
11534
|
+
"warn"
|
|
11535
|
+
);
|
|
11536
|
+
return true;
|
|
11537
|
+
}
|
|
11538
|
+
);
|
|
11539
|
+
const timedOut = new Promise((resolve4) => {
|
|
11540
|
+
timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
|
|
11541
|
+
});
|
|
11542
|
+
if (!await Promise.race([flushed, timedOut])) {
|
|
11543
|
+
log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
|
|
11544
|
+
}
|
|
11545
|
+
clearTimeout(timer);
|
|
11546
|
+
});
|
|
11547
|
+
const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
|
|
11548
|
+
log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
|
|
11549
|
+
process.exit(0);
|
|
11550
|
+
}
|
|
9949
11551
|
async function run(options) {
|
|
9950
11552
|
const interactive = isInteractive(options.json);
|
|
9951
11553
|
let logLevel;
|
|
9952
11554
|
let fileSyncDirectories;
|
|
9953
11555
|
try {
|
|
9954
11556
|
logLevel = resolveLogLevel(options);
|
|
9955
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
11557
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
|
|
9956
11558
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
9957
11559
|
throw new Error(
|
|
9958
11560
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -9983,6 +11585,7 @@ async function run(options) {
|
|
|
9983
11585
|
opencodeVersion: null,
|
|
9984
11586
|
sessionDbProvenanceAnomaly: false,
|
|
9985
11587
|
opencodeProcess: null,
|
|
11588
|
+
stopOpenCodeLogTail: null,
|
|
9986
11589
|
litestreamProcess: null,
|
|
9987
11590
|
connection: null,
|
|
9988
11591
|
channelDriver: null,
|
|
@@ -9997,9 +11600,24 @@ async function run(options) {
|
|
|
9997
11600
|
openaiUsageTimer: null,
|
|
9998
11601
|
openaiUsageRearm: null,
|
|
9999
11602
|
resourceUsageTimer: null,
|
|
11603
|
+
stopResourceUsageSampling: null,
|
|
11604
|
+
credentialSync: null,
|
|
10000
11605
|
authHeader: ""
|
|
10001
11606
|
};
|
|
10002
11607
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
11608
|
+
if (options.credentialSyncMarker) {
|
|
11609
|
+
state.credentialSync = createCredentialSync({
|
|
11610
|
+
markerPath: options.credentialSyncMarker,
|
|
11611
|
+
env: process.env,
|
|
11612
|
+
log: (message, level = "info") => {
|
|
11613
|
+
if (level === "error") {
|
|
11614
|
+
logActivity(state, { type: "error", error: message });
|
|
11615
|
+
} else {
|
|
11616
|
+
logActivity(state, { type: "info", level, message });
|
|
11617
|
+
}
|
|
11618
|
+
}
|
|
11619
|
+
});
|
|
11620
|
+
}
|
|
10003
11621
|
if (fileSyncDirectories.length > 0) {
|
|
10004
11622
|
log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
|
|
10005
11623
|
} else {
|
|
@@ -10025,43 +11643,7 @@ async function run(options) {
|
|
|
10025
11643
|
"warn"
|
|
10026
11644
|
);
|
|
10027
11645
|
}
|
|
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
|
-
};
|
|
11646
|
+
const handleSignal = () => beginGracefulShutdown(state, "signal");
|
|
10065
11647
|
process.on("SIGINT", handleSignal);
|
|
10066
11648
|
process.on("SIGTERM", handleSignal);
|
|
10067
11649
|
try {
|
|
@@ -10071,15 +11653,15 @@ async function run(options) {
|
|
|
10071
11653
|
printError("Authentication required");
|
|
10072
11654
|
blank();
|
|
10073
11655
|
console.log(
|
|
10074
|
-
|
|
11656
|
+
chalk7.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
|
|
10075
11657
|
);
|
|
10076
|
-
console.log(
|
|
11658
|
+
console.log(chalk7.dim("Or run `evident login` for interactive authentication"));
|
|
10077
11659
|
blank();
|
|
10078
11660
|
process.exit(1);
|
|
10079
11661
|
return;
|
|
10080
11662
|
}
|
|
10081
11663
|
blank();
|
|
10082
|
-
console.log(
|
|
11664
|
+
console.log(chalk7.yellow("You are not logged in to Evident."));
|
|
10083
11665
|
blank();
|
|
10084
11666
|
credentials2 = await promptForLogin(
|
|
10085
11667
|
"Would you like to log in now?",
|
|
@@ -10129,7 +11711,7 @@ async function run(options) {
|
|
|
10129
11711
|
);
|
|
10130
11712
|
blank();
|
|
10131
11713
|
console.log(
|
|
10132
|
-
|
|
11714
|
+
chalk7.dim(
|
|
10133
11715
|
"Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
|
|
10134
11716
|
)
|
|
10135
11717
|
);
|
|
@@ -10152,15 +11734,15 @@ async function run(options) {
|
|
|
10152
11734
|
);
|
|
10153
11735
|
if (interactive && !state.json) {
|
|
10154
11736
|
blank();
|
|
10155
|
-
console.log(
|
|
10156
|
-
console.log(
|
|
11737
|
+
console.log(chalk7.bold("Evident Run"));
|
|
11738
|
+
console.log(chalk7.dim("-".repeat(40)));
|
|
10157
11739
|
}
|
|
10158
11740
|
const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
|
|
10159
11741
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
10160
11742
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
10161
11743
|
spinner?.fail("Authentication failed");
|
|
10162
11744
|
blank();
|
|
10163
|
-
console.log(
|
|
11745
|
+
console.log(chalk7.yellow("Your authentication token is invalid or expired."));
|
|
10164
11746
|
blank();
|
|
10165
11747
|
credentials2 = await promptForLogin(
|
|
10166
11748
|
"Would you like to log in again?",
|
|
@@ -10207,6 +11789,14 @@ async function run(options) {
|
|
|
10207
11789
|
await restoreCredentialStores(credentialContext);
|
|
10208
11790
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
10209
11791
|
}
|
|
11792
|
+
state.credentialSync?.arm();
|
|
11793
|
+
state.stopOpenCodeLogTail = tailOpenCodeLogFile(
|
|
11794
|
+
resolveOpenCodeLogPath(homedir6(), process.env),
|
|
11795
|
+
createOpenCodeActivityForwarder(() => ({
|
|
11796
|
+
agentId: state.agentId,
|
|
11797
|
+
authHeader: state.authHeader
|
|
11798
|
+
}))
|
|
11799
|
+
).stop;
|
|
10210
11800
|
let sessionDbVerifyFatal = false;
|
|
10211
11801
|
if (!options.restoreSessionDb) {
|
|
10212
11802
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -10256,6 +11846,13 @@ async function run(options) {
|
|
|
10256
11846
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
10257
11847
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
10258
11848
|
}
|
|
11849
|
+
const { version: opencodeVersion, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(
|
|
11850
|
+
options,
|
|
11851
|
+
process.env
|
|
11852
|
+
);
|
|
11853
|
+
for (const warning2 of opencodeVersionWarnings) {
|
|
11854
|
+
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
11855
|
+
}
|
|
10259
11856
|
const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
|
|
10260
11857
|
for (const warning2 of maxActiveSessionsWarnings) {
|
|
10261
11858
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
@@ -10263,7 +11860,14 @@ async function run(options) {
|
|
|
10263
11860
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
10264
11861
|
const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
|
|
10265
11862
|
try {
|
|
10266
|
-
const oc = await
|
|
11863
|
+
const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
|
|
11864
|
+
port: state.port,
|
|
11865
|
+
interactive: state.interactive,
|
|
11866
|
+
agentId: state.agentId,
|
|
11867
|
+
log: (message) => log2(state, message),
|
|
11868
|
+
startTimeoutMs: opencodeStartTimeoutMs,
|
|
11869
|
+
inheritStdio: Boolean(options.opencodePidFile)
|
|
11870
|
+
}) : await ensureOpenCodeRunning({
|
|
10267
11871
|
port: state.port,
|
|
10268
11872
|
interactive: state.interactive,
|
|
10269
11873
|
agentId: state.agentId,
|
|
@@ -10276,7 +11880,7 @@ async function run(options) {
|
|
|
10276
11880
|
state.opencodeVersion = oc.version;
|
|
10277
11881
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
10278
11882
|
try {
|
|
10279
|
-
|
|
11883
|
+
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
10280
11884
|
`, { mode: 384 });
|
|
10281
11885
|
chmodSync3(options.opencodePidFile, 384);
|
|
10282
11886
|
} catch (error2) {
|
|
@@ -10290,7 +11894,7 @@ async function run(options) {
|
|
|
10290
11894
|
const provenance = checkSessionDbProvenance({
|
|
10291
11895
|
dbPath: sessionDbPath(),
|
|
10292
11896
|
currentVersion: state.opencodeVersion,
|
|
10293
|
-
homeDir:
|
|
11897
|
+
homeDir: homedir6(),
|
|
10294
11898
|
env: process.env
|
|
10295
11899
|
});
|
|
10296
11900
|
if (provenance.anomaly) {
|
|
@@ -10317,6 +11921,7 @@ async function run(options) {
|
|
|
10317
11921
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
10318
11922
|
}
|
|
10319
11923
|
}
|
|
11924
|
+
await reloadProviderCache(state.port);
|
|
10320
11925
|
const noProviderWarning = buildNoProviderWarning(
|
|
10321
11926
|
await hasAnyConfiguredProvider(state.port)
|
|
10322
11927
|
);
|
|
@@ -10325,10 +11930,10 @@ async function run(options) {
|
|
|
10325
11930
|
if (state.interactive && !state.json) {
|
|
10326
11931
|
logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
|
|
10327
11932
|
blank();
|
|
10328
|
-
console.log(
|
|
11933
|
+
console.log(chalk7.yellow("\u26A0 No OpenCode model provider is configured."));
|
|
10329
11934
|
console.log(
|
|
10330
|
-
|
|
10331
|
-
`Run ${
|
|
11935
|
+
chalk7.dim(
|
|
11936
|
+
`Run ${chalk7.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
|
|
10332
11937
|
)
|
|
10333
11938
|
);
|
|
10334
11939
|
blank();
|
|
@@ -10392,7 +11997,7 @@ async function run(options) {
|
|
|
10392
11997
|
});
|
|
10393
11998
|
try {
|
|
10394
11999
|
if (litestreamProcess.pid !== void 0) {
|
|
10395
|
-
|
|
12000
|
+
writeFileSync6(options.litestreamPidFile, `${litestreamProcess.pid}
|
|
10396
12001
|
`, {
|
|
10397
12002
|
mode: 384
|
|
10398
12003
|
});
|
|
@@ -10452,7 +12057,7 @@ async function run(options) {
|
|
|
10452
12057
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
10453
12058
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
10454
12059
|
fileSyncDirectories,
|
|
10455
|
-
homeDir:
|
|
12060
|
+
homeDir: homedir6(),
|
|
10456
12061
|
maxActiveSessions,
|
|
10457
12062
|
log: (entry) => (
|
|
10458
12063
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -10578,6 +12183,18 @@ async function run(options) {
|
|
|
10578
12183
|
if (state.interactive) displayStatus(state);
|
|
10579
12184
|
});
|
|
10580
12185
|
},
|
|
12186
|
+
// Both loops are rearmed because `rearm()` is idempotent for the
|
|
12187
|
+
// provider that did not just connect, and is a no-op when reporting is off.
|
|
12188
|
+
onUsageRearmPing: () => {
|
|
12189
|
+
if (!state.running) return;
|
|
12190
|
+
logActivity(state, {
|
|
12191
|
+
type: "info",
|
|
12192
|
+
level: "debug",
|
|
12193
|
+
message: "Usage rearm ping received"
|
|
12194
|
+
});
|
|
12195
|
+
state.claudeUsageRearm?.();
|
|
12196
|
+
state.openaiUsageRearm?.();
|
|
12197
|
+
},
|
|
10581
12198
|
onInfo: (message) => logActivity(state, { type: "info", message })
|
|
10582
12199
|
}
|
|
10583
12200
|
});
|
|
@@ -10598,7 +12215,17 @@ async function run(options) {
|
|
|
10598
12215
|
setTimer: (timer) => {
|
|
10599
12216
|
state.openaiUsageTimer = timer;
|
|
10600
12217
|
},
|
|
10601
|
-
fetchUsage: () =>
|
|
12218
|
+
fetchUsage: async () => {
|
|
12219
|
+
const usage = await getOpenAiUsage(state.port);
|
|
12220
|
+
if (usage.subscription === null) {
|
|
12221
|
+
logActivity(state, {
|
|
12222
|
+
type: "info",
|
|
12223
|
+
level: "debug",
|
|
12224
|
+
message: "OpenAI usage subscription could not be identified from the local credential"
|
|
12225
|
+
});
|
|
12226
|
+
}
|
|
12227
|
+
return usage;
|
|
12228
|
+
},
|
|
10602
12229
|
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
10603
12230
|
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
10604
12231
|
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
@@ -10672,6 +12299,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10672
12299
|
).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
12300
|
"--opencode-start-timeout <seconds>",
|
|
10674
12301
|
"Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
|
|
12302
|
+
).option(
|
|
12303
|
+
"--opencode-version <v1|v2>",
|
|
12304
|
+
"Which OpenCode major version to launch: v1 or v2 (default: v1). Env: EVIDENT_OPENCODE_VERSION"
|
|
10675
12305
|
).option("--json", "Output in JSON format").option(
|
|
10676
12306
|
"--session-cleanup-max-age <duration>",
|
|
10677
12307
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -10722,6 +12352,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10722
12352
|
).option(
|
|
10723
12353
|
"--opencode-config-overlay <path>",
|
|
10724
12354
|
"Apply this runner-provided OpenCode config before starting OpenCode."
|
|
12355
|
+
).option(
|
|
12356
|
+
"--credential-sync-marker <path>",
|
|
12357
|
+
"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
12358
|
).action(
|
|
10726
12359
|
(options) => {
|
|
10727
12360
|
run({
|
|
@@ -10737,6 +12370,7 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10737
12370
|
// Raw string — validation/precedence is single-sourced in run.ts's
|
|
10738
12371
|
// resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).
|
|
10739
12372
|
opencodeStartTimeout: options.opencodeStartTimeout,
|
|
12373
|
+
opencodeVersion: options.opencodeVersion,
|
|
10740
12374
|
json: options.json,
|
|
10741
12375
|
// Raw strings — the resolver in run.ts single-sources parsing (M1).
|
|
10742
12376
|
sessionCleanupMaxAge: options.sessionCleanupMaxAge,
|
|
@@ -10760,7 +12394,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10760
12394
|
sessionDbNoReplicateMarker: options.sessionDbNoReplicateMarker,
|
|
10761
12395
|
restoreSessionDb: options.restoreSessionDb,
|
|
10762
12396
|
restoreRunnerCredentials: options.restoreRunnerCredentials,
|
|
10763
|
-
opencodeConfigOverlay: options.opencodeConfigOverlay
|
|
12397
|
+
opencodeConfigOverlay: options.opencodeConfigOverlay,
|
|
12398
|
+
credentialSyncMarker: options.credentialSyncMarker
|
|
10764
12399
|
});
|
|
10765
12400
|
}
|
|
10766
12401
|
);
|