@evident-ai/cli 3.4.1-dev.7f4a466 → 3.4.1-dev.80d5a20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/index.js +1254 -249
- 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
|
});
|
|
@@ -763,13 +768,6 @@ function toReportedOpenAiWindow(window) {
|
|
|
763
768
|
resets_at: window.resetsAt
|
|
764
769
|
};
|
|
765
770
|
}
|
|
766
|
-
function toReportedOpenAiSubscription(snapshot) {
|
|
767
|
-
if (!snapshot.subscription) return null;
|
|
768
|
-
return {
|
|
769
|
-
owner_email: snapshot.subscription.ownerEmail,
|
|
770
|
-
plan_type: snapshot.subscription.planType
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
771
|
async function reportOpenAiUsage(agentId, authHeader, snapshot) {
|
|
774
772
|
try {
|
|
775
773
|
const apiUrl = getApiUrlConfig();
|
|
@@ -781,7 +779,7 @@ async function reportOpenAiUsage(agentId, authHeader, snapshot) {
|
|
|
781
779
|
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
782
780
|
has_credits: snapshot.hasCredits,
|
|
783
781
|
credits_unlimited: snapshot.creditsUnlimited,
|
|
784
|
-
subscription:
|
|
782
|
+
subscription: toReportedSubscription(snapshot.subscription)
|
|
785
783
|
}),
|
|
786
784
|
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
787
785
|
});
|
|
@@ -1009,10 +1007,10 @@ async function status(options = {}) {
|
|
|
1009
1007
|
}
|
|
1010
1008
|
|
|
1011
1009
|
// src/lib/claude-usage.ts
|
|
1012
|
-
import { execFileSync } from "child_process";
|
|
1013
|
-
import { readFileSync } from "fs";
|
|
1014
|
-
import { homedir } from "os";
|
|
1015
|
-
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";
|
|
1016
1014
|
var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
1017
1015
|
var CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
1018
1016
|
var CLAUDE_PROFILE_TIMEOUT_MS = 2e3;
|
|
@@ -1097,7 +1095,7 @@ function ownerLookupFailure(error2) {
|
|
|
1097
1095
|
}
|
|
1098
1096
|
async function getClaudeUsageOwner(accessToken) {
|
|
1099
1097
|
if (cachedOwner?.accessToken === accessToken) {
|
|
1100
|
-
return {
|
|
1098
|
+
return { subscription: cachedOwner.owner, ownerLookupError: null };
|
|
1101
1099
|
}
|
|
1102
1100
|
try {
|
|
1103
1101
|
const response = await fetch(CLAUDE_PROFILE_URL, {
|
|
@@ -1109,27 +1107,27 @@ async function getClaudeUsageOwner(accessToken) {
|
|
|
1109
1107
|
signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
|
|
1110
1108
|
});
|
|
1111
1109
|
if (!response.ok) {
|
|
1112
|
-
return {
|
|
1110
|
+
return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
|
|
1113
1111
|
}
|
|
1114
1112
|
let body;
|
|
1115
1113
|
try {
|
|
1116
1114
|
body = await response.json();
|
|
1117
1115
|
} catch (error2) {
|
|
1118
|
-
return {
|
|
1116
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1119
1117
|
}
|
|
1120
1118
|
const profile = body;
|
|
1121
1119
|
if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
|
|
1122
|
-
return {
|
|
1120
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1123
1121
|
}
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1122
|
+
const subscription = {
|
|
1123
|
+
ownerEmail: profile.account.email,
|
|
1126
1124
|
organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
|
|
1127
|
-
|
|
1125
|
+
planType: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
|
|
1128
1126
|
};
|
|
1129
|
-
cachedOwner = { accessToken, owner };
|
|
1130
|
-
return {
|
|
1127
|
+
cachedOwner = { accessToken, owner: subscription };
|
|
1128
|
+
return { subscription, ownerLookupError: null };
|
|
1131
1129
|
} catch (error2) {
|
|
1132
|
-
return {
|
|
1130
|
+
return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
|
|
1133
1131
|
}
|
|
1134
1132
|
}
|
|
1135
1133
|
async function getClaudeUsage() {
|
|
@@ -1158,11 +1156,11 @@ async function getClaudeUsage() {
|
|
|
1158
1156
|
throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
|
|
1159
1157
|
}
|
|
1160
1158
|
const body = await res.json();
|
|
1161
|
-
const {
|
|
1159
|
+
const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
|
|
1162
1160
|
return {
|
|
1163
1161
|
fiveHour: toWindow(body.five_hour),
|
|
1164
1162
|
sevenDay: toWindow(body.seven_day),
|
|
1165
|
-
|
|
1163
|
+
subscription,
|
|
1166
1164
|
ownerLookupError
|
|
1167
1165
|
};
|
|
1168
1166
|
}
|
|
@@ -1192,10 +1190,10 @@ async function claudeUsage() {
|
|
|
1192
1190
|
}
|
|
1193
1191
|
|
|
1194
1192
|
// src/commands/run.ts
|
|
1195
|
-
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
1196
|
-
import { homedir as
|
|
1197
|
-
import { isAbsolute as isAbsolute3, join as
|
|
1198
|
-
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";
|
|
1199
1197
|
|
|
1200
1198
|
// ../../packages/types/src/agents/index.ts
|
|
1201
1199
|
var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
|
|
@@ -1254,7 +1252,7 @@ function stripQuery(url) {
|
|
|
1254
1252
|
|
|
1255
1253
|
// src/commands/run.ts
|
|
1256
1254
|
import ora3 from "ora";
|
|
1257
|
-
import { select as
|
|
1255
|
+
import { select as select4 } from "@inquirer/prompts";
|
|
1258
1256
|
|
|
1259
1257
|
// src/lib/telemetry.ts
|
|
1260
1258
|
var CLI_VERSION = (true ? "3.0.0" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
@@ -1427,12 +1425,50 @@ var SEVERITY_BY_LEVEL = {
|
|
|
1427
1425
|
warn: "warning",
|
|
1428
1426
|
error: "error"
|
|
1429
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
|
+
}
|
|
1430
1466
|
var MAX_MESSAGE_LENGTH = 500;
|
|
1431
1467
|
var MAX_METADATA_VALUE_LENGTH = 200;
|
|
1432
1468
|
var MAX_METADATA_ENTRIES = 20;
|
|
1433
1469
|
var TRUNCATION_MARKER = "\u2026";
|
|
1434
1470
|
function redact(message) {
|
|
1435
|
-
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>");
|
|
1436
1472
|
}
|
|
1437
1473
|
function truncate(message) {
|
|
1438
1474
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
@@ -1458,43 +1494,47 @@ function sanitiseMetadata(metadata) {
|
|
|
1458
1494
|
}
|
|
1459
1495
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1460
1496
|
var RATE_LIMIT_MAX_EVENTS = 30;
|
|
1461
|
-
var
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
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) {
|
|
1467
1506
|
console.error(
|
|
1468
|
-
`[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}"`
|
|
1469
1508
|
);
|
|
1470
1509
|
}
|
|
1471
|
-
windowStartedAt = now;
|
|
1472
|
-
windowCount = 0;
|
|
1473
|
-
windowDroppedCount = 0;
|
|
1510
|
+
window.windowStartedAt = now;
|
|
1511
|
+
window.windowCount = 0;
|
|
1512
|
+
window.windowDroppedCount = 0;
|
|
1474
1513
|
}
|
|
1475
|
-
if (windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1476
|
-
windowDroppedCount++;
|
|
1477
|
-
if (windowDroppedCount === 1) {
|
|
1514
|
+
if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1515
|
+
window.windowDroppedCount++;
|
|
1516
|
+
if (window.windowDroppedCount === 1) {
|
|
1478
1517
|
console.error(
|
|
1479
|
-
`[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}"`
|
|
1480
1519
|
);
|
|
1481
1520
|
}
|
|
1482
1521
|
return false;
|
|
1483
1522
|
}
|
|
1484
|
-
windowCount++;
|
|
1523
|
+
window.windowCount++;
|
|
1485
1524
|
return true;
|
|
1486
1525
|
}
|
|
1487
1526
|
function forwardRunnerActivity(entry, context) {
|
|
1488
1527
|
try {
|
|
1489
1528
|
if (!FORWARDED_LEVELS.has(entry.level)) return;
|
|
1490
1529
|
if (!context.agentId || !context.authHeader) return;
|
|
1491
|
-
|
|
1530
|
+
const source = entry.source ?? "cli.run";
|
|
1531
|
+
if (!admitUnderRateLimit(source, Date.now())) return;
|
|
1492
1532
|
const rawMessage = entry.error ?? entry.message ?? "";
|
|
1493
1533
|
const message = truncate(redact(rawMessage));
|
|
1494
1534
|
logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
|
|
1495
1535
|
severity: SEVERITY_BY_LEVEL[entry.level],
|
|
1496
1536
|
message,
|
|
1497
|
-
metadata: { ...sanitiseMetadata(entry.metadata), source
|
|
1537
|
+
metadata: { ...sanitiseMetadata(entry.metadata), source },
|
|
1498
1538
|
agentId: context.agentId
|
|
1499
1539
|
});
|
|
1500
1540
|
} catch (err) {
|
|
@@ -1505,8 +1545,8 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1505
1545
|
}
|
|
1506
1546
|
|
|
1507
1547
|
// src/lib/opencode/session-db-recovery-report.ts
|
|
1508
|
-
import { readFileSync as readFileSync2, unlinkSync } from "fs";
|
|
1509
|
-
import { join as join2 } from "path";
|
|
1548
|
+
import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
|
|
1549
|
+
import { join as join2 } from "node:path";
|
|
1510
1550
|
function sessionDbRecoveryReportPath(homeDir, env) {
|
|
1511
1551
|
const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
|
|
1512
1552
|
return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
|
|
@@ -1719,13 +1759,13 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
1719
1759
|
}
|
|
1720
1760
|
|
|
1721
1761
|
// src/lib/opencode/session-db-boot.ts
|
|
1722
|
-
import { spawn as spawn2 } from "child_process";
|
|
1723
|
-
import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "fs";
|
|
1724
|
-
import { homedir as homedir2 } from "os";
|
|
1725
|
-
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";
|
|
1726
1766
|
|
|
1727
1767
|
// src/lib/runner-synchroniser.ts
|
|
1728
|
-
import { spawn } from "child_process";
|
|
1768
|
+
import { spawn } from "node:child_process";
|
|
1729
1769
|
function appendError(stderr, error2) {
|
|
1730
1770
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1731
1771
|
return stderr === "" ? message : `${stderr}
|
|
@@ -2250,9 +2290,9 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
2250
2290
|
}
|
|
2251
2291
|
|
|
2252
2292
|
// src/lib/opencode/session-db-provenance.ts
|
|
2253
|
-
import { createRequire } from "module";
|
|
2254
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2255
|
-
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";
|
|
2256
2296
|
var require2 = createRequire(import.meta.url);
|
|
2257
2297
|
function readSessionDbMigrationIds(dbPath) {
|
|
2258
2298
|
let db;
|
|
@@ -2446,6 +2486,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
2446
2486
|
|
|
2447
2487
|
// src/lib/opencode/process.ts
|
|
2448
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
|
+
}
|
|
2449
2500
|
function getProcessCwd(pid) {
|
|
2450
2501
|
const platform = process.platform;
|
|
2451
2502
|
try {
|
|
@@ -2494,14 +2545,14 @@ function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
2494
2545
|
}
|
|
2495
2546
|
return null;
|
|
2496
2547
|
}
|
|
2497
|
-
function
|
|
2548
|
+
function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
2498
2549
|
const instances = [];
|
|
2499
2550
|
try {
|
|
2500
2551
|
const platform = process.platform;
|
|
2501
2552
|
if (platform === "darwin" || platform === "linux") {
|
|
2502
2553
|
let pids = [];
|
|
2503
2554
|
try {
|
|
2504
|
-
const pgrepOutput = execSync(
|
|
2555
|
+
const pgrepOutput = execSync(`pgrep -f "${pgrepPattern}"`, {
|
|
2505
2556
|
encoding: "utf-8",
|
|
2506
2557
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2507
2558
|
}).trim();
|
|
@@ -2510,7 +2561,7 @@ function findOpenCodeProcesses() {
|
|
|
2510
2561
|
}
|
|
2511
2562
|
} catch {
|
|
2512
2563
|
try {
|
|
2513
|
-
const psOutput = execSync(
|
|
2564
|
+
const psOutput = execSync(`ps aux | grep -E "${psPattern}" | grep -v grep`, {
|
|
2514
2565
|
encoding: "utf-8",
|
|
2515
2566
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2516
2567
|
}).trim();
|
|
@@ -2556,6 +2607,9 @@ function findOpenCodeProcesses() {
|
|
|
2556
2607
|
}
|
|
2557
2608
|
return instances;
|
|
2558
2609
|
}
|
|
2610
|
+
function findOpenCodeProcesses() {
|
|
2611
|
+
return findProcessesByPattern("opencode serve|opencode-serve", "opencode (serve|--port)");
|
|
2612
|
+
}
|
|
2559
2613
|
async function scanPortsForOpenCode() {
|
|
2560
2614
|
const instances = [];
|
|
2561
2615
|
const checks = OPENCODE_PORT_RANGE.map(async (port) => {
|
|
@@ -2602,7 +2656,7 @@ async function findHealthyOpenCodeInstances() {
|
|
|
2602
2656
|
}
|
|
2603
2657
|
async function startOpenCode(port, options = {}) {
|
|
2604
2658
|
let command = "opencode";
|
|
2605
|
-
const printLogs = options.inheritStdio ? ["--print-logs"] : [];
|
|
2659
|
+
const printLogs = options.inheritStdio ? ["--print-logs", "--log-level", resolveOpenCodeLogLevel(process.env)] : [];
|
|
2606
2660
|
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...printLogs];
|
|
2607
2661
|
try {
|
|
2608
2662
|
execSync("which opencode", { stdio: "ignore" });
|
|
@@ -2659,6 +2713,19 @@ function isOpenCodeInstalled() {
|
|
|
2659
2713
|
return false;
|
|
2660
2714
|
}
|
|
2661
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
|
+
}
|
|
2662
2729
|
async function promptOpenCodeInstall(interactive) {
|
|
2663
2730
|
if (!interactive) {
|
|
2664
2731
|
console.log(
|
|
@@ -2668,7 +2735,11 @@ async function promptOpenCodeInstall(interactive) {
|
|
|
2668
2735
|
install_url: OPENCODE_INSTALL_URL,
|
|
2669
2736
|
install_commands: {
|
|
2670
2737
|
npm: "npm install -g opencode-ai",
|
|
2671
|
-
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
|
+
}
|
|
2672
2743
|
}
|
|
2673
2744
|
})
|
|
2674
2745
|
);
|
|
@@ -3190,21 +3261,74 @@ function collectSubagentSessions(messages, userMessageId) {
|
|
|
3190
3261
|
}
|
|
3191
3262
|
return refs;
|
|
3192
3263
|
}
|
|
3193
|
-
function
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
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)
|
|
3197
3303
|
);
|
|
3198
|
-
const
|
|
3199
|
-
const
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
const
|
|
3205
|
-
|
|
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);
|
|
3206
3319
|
}
|
|
3207
|
-
|
|
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;
|
|
3208
3332
|
let sawAnyUsage = false;
|
|
3209
3333
|
let inputSum = 0;
|
|
3210
3334
|
let outputSum = 0;
|
|
@@ -3215,7 +3339,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
3215
3339
|
let sawCost = false;
|
|
3216
3340
|
let modelId = null;
|
|
3217
3341
|
let providerId = null;
|
|
3218
|
-
for (const m of
|
|
3342
|
+
for (const m of selected) {
|
|
3219
3343
|
const info = m.info;
|
|
3220
3344
|
if (!info) continue;
|
|
3221
3345
|
const tokens = info.tokens;
|
|
@@ -3250,12 +3374,28 @@ function messageUsage(messages, userMessageId) {
|
|
|
3250
3374
|
usage_tokens_reasoning: reasoningSum,
|
|
3251
3375
|
usage_tokens_cache_read: cacheReadSum,
|
|
3252
3376
|
usage_tokens_cache_write: cacheWriteSum,
|
|
3253
|
-
// NULL means
|
|
3254
|
-
//
|
|
3255
|
-
// `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`.
|
|
3256
3379
|
usage_cost_usd: sawCost ? costSum : null
|
|
3257
3380
|
};
|
|
3258
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
|
+
}
|
|
3259
3399
|
function messageRunState(messages, userMessageId) {
|
|
3260
3400
|
if (!messages || messages.length === 0) return "unknown";
|
|
3261
3401
|
const hasUser = messages.some((m) => idOf(m) === userMessageId);
|
|
@@ -3385,6 +3525,28 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
|
3385
3525
|
(m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
3386
3526
|
);
|
|
3387
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
|
+
}
|
|
3388
3550
|
async function hasAnyConfiguredProvider(port) {
|
|
3389
3551
|
try {
|
|
3390
3552
|
const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
|
|
@@ -3416,6 +3578,94 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3416
3578
|
return null;
|
|
3417
3579
|
}
|
|
3418
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
|
+
}
|
|
3419
3669
|
|
|
3420
3670
|
// src/lib/opencode/session-cleanup.ts
|
|
3421
3671
|
var DURATION_UNIT_MS = {
|
|
@@ -3522,8 +3772,8 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
3522
3772
|
}
|
|
3523
3773
|
|
|
3524
3774
|
// src/lib/opencode/session-db-size.ts
|
|
3525
|
-
import { statSync as statSync3 } from "fs";
|
|
3526
|
-
import { join as join4 } from "path";
|
|
3775
|
+
import { statSync as statSync3 } from "node:fs";
|
|
3776
|
+
import { join as join4 } from "node:path";
|
|
3527
3777
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
3528
3778
|
function statSessionDbBytes(homeDir) {
|
|
3529
3779
|
const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
@@ -3553,9 +3803,96 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
3553
3803
|
return null;
|
|
3554
3804
|
}
|
|
3555
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
|
+
|
|
3556
3889
|
// src/lib/opencode/session-db-reclaim.ts
|
|
3557
|
-
import { statSync as
|
|
3558
|
-
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
|
+
}
|
|
3559
3896
|
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
3560
3897
|
try {
|
|
3561
3898
|
const fsStats = statfsSync(dirname4(dbPath));
|
|
@@ -3581,17 +3918,17 @@ async function probeReclaimAvailability(input) {
|
|
|
3581
3918
|
const { dbPath, requiredBytes } = input;
|
|
3582
3919
|
let sqlite;
|
|
3583
3920
|
try {
|
|
3584
|
-
sqlite = await import("sqlite");
|
|
3921
|
+
sqlite = await import("node:sqlite");
|
|
3585
3922
|
} catch (err) {
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
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 };
|
|
3590
3926
|
}
|
|
3591
3927
|
let autoVacuum = null;
|
|
3592
3928
|
try {
|
|
3593
3929
|
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
3594
3930
|
try {
|
|
3931
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3595
3932
|
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3596
3933
|
} finally {
|
|
3597
3934
|
db.close();
|
|
@@ -3602,23 +3939,25 @@ async function probeReclaimAvailability(input) {
|
|
|
3602
3939
|
);
|
|
3603
3940
|
}
|
|
3604
3941
|
if (autoVacuum !== 0) return null;
|
|
3605
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
|
|
3942
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
3606
3943
|
}
|
|
3607
3944
|
async function reclaimSessionDbSpace(input) {
|
|
3608
3945
|
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
3609
3946
|
let sqlite;
|
|
3610
3947
|
try {
|
|
3611
|
-
sqlite = await import("sqlite");
|
|
3948
|
+
sqlite = await import("node:sqlite");
|
|
3612
3949
|
} catch (err) {
|
|
3950
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
3613
3951
|
console.warn(
|
|
3614
|
-
`[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}`
|
|
3615
3953
|
);
|
|
3616
|
-
return { ok: false, skipped: "sqlite-unavailable" };
|
|
3954
|
+
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
3617
3955
|
}
|
|
3618
3956
|
const { DatabaseSync } = sqlite;
|
|
3619
3957
|
let db;
|
|
3620
3958
|
try {
|
|
3621
3959
|
db = new DatabaseSync(dbPath);
|
|
3960
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3622
3961
|
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3623
3962
|
if (autoVacuum === 0) {
|
|
3624
3963
|
if (!allowFullVacuum) {
|
|
@@ -3627,7 +3966,7 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3627
3966
|
);
|
|
3628
3967
|
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
3629
3968
|
}
|
|
3630
|
-
const fileBytesForGuard =
|
|
3969
|
+
const fileBytesForGuard = statSync5(dbPath).size;
|
|
3631
3970
|
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
3632
3971
|
if (skipReason !== null) {
|
|
3633
3972
|
console.warn(
|
|
@@ -3655,10 +3994,12 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3655
3994
|
);
|
|
3656
3995
|
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
3657
3996
|
} catch (err) {
|
|
3658
|
-
console.error(
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3997
|
+
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
3998
|
+
return {
|
|
3999
|
+
ok: false,
|
|
4000
|
+
skipped: "reclaim-error",
|
|
4001
|
+
detail: errorMessage(err)
|
|
4002
|
+
};
|
|
3662
4003
|
} finally {
|
|
3663
4004
|
db?.close();
|
|
3664
4005
|
}
|
|
@@ -3950,8 +4291,8 @@ function connectTunnel(options) {
|
|
|
3950
4291
|
try {
|
|
3951
4292
|
message = JSON.parse(data.toString());
|
|
3952
4293
|
} catch (error2) {
|
|
3953
|
-
const
|
|
3954
|
-
onError?.(`Failed to handle message: ${
|
|
4294
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
4295
|
+
onError?.(`Failed to handle message: ${errorMessage3}`);
|
|
3955
4296
|
return;
|
|
3956
4297
|
}
|
|
3957
4298
|
if (isStreamFrame(message)) {
|
|
@@ -4095,7 +4436,7 @@ var RunnerConnection = class {
|
|
|
4095
4436
|
};
|
|
4096
4437
|
|
|
4097
4438
|
// src/lib/tunnel/ready-marker.ts
|
|
4098
|
-
import { writeFileSync as writeFileSync3 } from "fs";
|
|
4439
|
+
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
4099
4440
|
function writeTunnelReadyMarker(path, agentId) {
|
|
4100
4441
|
try {
|
|
4101
4442
|
writeFileSync3(path, `${agentId}
|
|
@@ -4107,7 +4448,7 @@ function writeTunnelReadyMarker(path, agentId) {
|
|
|
4107
4448
|
}
|
|
4108
4449
|
|
|
4109
4450
|
// src/lib/replication.ts
|
|
4110
|
-
import { spawn as spawn4 } from "child_process";
|
|
4451
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
4111
4452
|
function startSessionDbReplication(configPath) {
|
|
4112
4453
|
return spawn4("litestream", ["replicate", "-config", configPath], {
|
|
4113
4454
|
stdio: "inherit"
|
|
@@ -4123,7 +4464,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
4123
4464
|
}
|
|
4124
4465
|
|
|
4125
4466
|
// src/lib/process-liveness.ts
|
|
4126
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
4467
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
4127
4468
|
function isProcessAlive(pid) {
|
|
4128
4469
|
try {
|
|
4129
4470
|
process.kill(pid, 0);
|
|
@@ -4149,9 +4490,9 @@ function isProcessAlive(pid) {
|
|
|
4149
4490
|
}
|
|
4150
4491
|
|
|
4151
4492
|
// src/lib/openai-usage.ts
|
|
4152
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
4153
|
-
import { homedir as
|
|
4154
|
-
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";
|
|
4155
4496
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4156
4497
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4157
4498
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4165,7 +4506,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4165
4506
|
}
|
|
4166
4507
|
function readOpenCodeChatGptCredentials() {
|
|
4167
4508
|
try {
|
|
4168
|
-
const raw = readFileSync5(
|
|
4509
|
+
const raw = readFileSync5(join6(homedir4(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4169
4510
|
let parsed;
|
|
4170
4511
|
try {
|
|
4171
4512
|
parsed = JSON.parse(raw);
|
|
@@ -4202,7 +4543,7 @@ function parseChatGptIdentity(accessToken) {
|
|
|
4202
4543
|
const auth = payload["https://api.openai.com/auth"];
|
|
4203
4544
|
const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
|
|
4204
4545
|
const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
|
|
4205
|
-
return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
|
|
4546
|
+
return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
|
|
4206
4547
|
}
|
|
4207
4548
|
function toWindow2(headers, name) {
|
|
4208
4549
|
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
@@ -4384,13 +4725,6 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
|
|
|
4384
4725
|
envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
4385
4726
|
});
|
|
4386
4727
|
}
|
|
4387
|
-
function nextReportDelayMs(random = Math.random) {
|
|
4388
|
-
return usageReportDelayMs(random);
|
|
4389
|
-
}
|
|
4390
|
-
var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
|
|
4391
|
-
function claudeUsageFailureLogLevel(consecutiveFailures) {
|
|
4392
|
-
return usageReportFailureLogLevel(consecutiveFailures);
|
|
4393
|
-
}
|
|
4394
4728
|
|
|
4395
4729
|
// src/lib/openai-usage-reporting.ts
|
|
4396
4730
|
function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
@@ -4427,8 +4761,8 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
4427
4761
|
}
|
|
4428
4762
|
|
|
4429
4763
|
// src/lib/resource-usage.ts
|
|
4430
|
-
import { cpus, totalmem, freemem } from "os";
|
|
4431
|
-
import { statfsSync as statfsSync2 } from "fs";
|
|
4764
|
+
import { cpus, totalmem, freemem } from "node:os";
|
|
4765
|
+
import { statfsSync as statfsSync2 } from "node:fs";
|
|
4432
4766
|
|
|
4433
4767
|
// src/lib/ecs-task-metadata.ts
|
|
4434
4768
|
var ECS_METADATA_TIMEOUT_MS = 2e3;
|
|
@@ -4595,15 +4929,15 @@ function createResourceUsageCollector(homeDir) {
|
|
|
4595
4929
|
}
|
|
4596
4930
|
|
|
4597
4931
|
// src/lib/channels/driver.ts
|
|
4598
|
-
import { homedir as
|
|
4932
|
+
import { homedir as homedir5 } from "node:os";
|
|
4599
4933
|
|
|
4600
4934
|
// src/lib/runner-file-sync.ts
|
|
4601
|
-
import { join as
|
|
4935
|
+
import { join as join8 } from "node:path";
|
|
4602
4936
|
|
|
4603
4937
|
// src/lib/file-push.ts
|
|
4604
|
-
import { randomUUID } from "crypto";
|
|
4605
|
-
import { chmod, mkdir, open as
|
|
4606
|
-
import { basename, dirname as dirname5, isAbsolute, join as
|
|
4938
|
+
import { randomUUID } from "node:crypto";
|
|
4939
|
+
import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
|
|
4940
|
+
import { basename, dirname as dirname5, isAbsolute, join as join7, relative, resolve as resolve2, sep } from "node:path";
|
|
4607
4941
|
var FILE_MODE = 384;
|
|
4608
4942
|
var DIRECTORY_MODE = 448;
|
|
4609
4943
|
async function writePushedFile(request) {
|
|
@@ -4636,7 +4970,7 @@ async function writePushedFile(request) {
|
|
|
4636
4970
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
4637
4971
|
dirname5(candidate)
|
|
4638
4972
|
);
|
|
4639
|
-
const realTarget =
|
|
4973
|
+
const realTarget = join7(existingAncestor, ...missingSegments, basename(candidate));
|
|
4640
4974
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
4641
4975
|
if (allowedDirectory === null) {
|
|
4642
4976
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -4672,7 +5006,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
4672
5006
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
4673
5007
|
return null;
|
|
4674
5008
|
}
|
|
4675
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
5009
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4676
5010
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
4677
5011
|
return null;
|
|
4678
5012
|
}
|
|
@@ -4745,16 +5079,16 @@ function contains(realDirectory, realTarget) {
|
|
|
4745
5079
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
4746
5080
|
let current = existingAncestor;
|
|
4747
5081
|
for (const segment of missingSegments) {
|
|
4748
|
-
current =
|
|
5082
|
+
current = join7(current, segment);
|
|
4749
5083
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
4750
5084
|
await chmod(current, DIRECTORY_MODE);
|
|
4751
5085
|
}
|
|
4752
5086
|
}
|
|
4753
5087
|
async function writeAtomically(realTarget, content) {
|
|
4754
|
-
const temporaryPath =
|
|
5088
|
+
const temporaryPath = join7(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
4755
5089
|
let handle;
|
|
4756
5090
|
try {
|
|
4757
|
-
handle = await
|
|
5091
|
+
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
4758
5092
|
await handle.writeFile(content);
|
|
4759
5093
|
await handle.chmod(FILE_MODE);
|
|
4760
5094
|
await handle.close();
|
|
@@ -4881,12 +5215,12 @@ var NOT_APPLIED = {
|
|
|
4881
5215
|
opencodeAuthApplied: false
|
|
4882
5216
|
};
|
|
4883
5217
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
4884
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4885
|
-
return expanded ===
|
|
5218
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5219
|
+
return expanded === join8(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
4886
5220
|
}
|
|
4887
5221
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
4888
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4889
|
-
return expanded ===
|
|
5222
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5223
|
+
return expanded === join8(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
4890
5224
|
}
|
|
4891
5225
|
async function applyOne(options, file) {
|
|
4892
5226
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -5046,6 +5380,10 @@ var DEFAULT_RETRY_POLICY = {
|
|
|
5046
5380
|
baseDelayMs: 500,
|
|
5047
5381
|
maxDelayMs: 3e4
|
|
5048
5382
|
};
|
|
5383
|
+
var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
|
|
5384
|
+
var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
|
|
5385
|
+
var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
5386
|
+
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
5049
5387
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
5050
5388
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
5051
5389
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
@@ -5186,6 +5524,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5186
5524
|
* message; it is removed once its in-flight set empties.
|
|
5187
5525
|
*/
|
|
5188
5526
|
watchers = /* @__PURE__ */ new Map();
|
|
5527
|
+
sessionErrorStream = null;
|
|
5528
|
+
/**
|
|
5529
|
+
* Session-error failures currently being reported; entries are empty at rest
|
|
5530
|
+
* because each handoff deletes its id in `finally`.
|
|
5531
|
+
*/
|
|
5532
|
+
sessionErrorHandled = /* @__PURE__ */ new Set();
|
|
5533
|
+
/**
|
|
5534
|
+
* Session errors that arrived before their dispatch was registered. Bounded FIFO
|
|
5535
|
+
* with a short TTL so an unmatched session cannot retain an event indefinitely.
|
|
5536
|
+
*/
|
|
5537
|
+
bufferedSessionErrors = /* @__PURE__ */ new Map();
|
|
5189
5538
|
/**
|
|
5190
5539
|
* AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
|
|
5191
5540
|
* dispatched and are still in-flight. A message in this set is never
|
|
@@ -5369,6 +5718,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5369
5718
|
* no watcher) can resolve the title.
|
|
5370
5719
|
*/
|
|
5371
5720
|
sessionTitles = /* @__PURE__ */ new Map();
|
|
5721
|
+
/** One best-effort terminal subagent collection per Evident message id. */
|
|
5722
|
+
subagentInvocationCollections = /* @__PURE__ */ new Map();
|
|
5723
|
+
/**
|
|
5724
|
+
* Early snapshots are only liveness hints; they must not become the terminal
|
|
5725
|
+
* collection when the task parts or child transcript have advanced.
|
|
5726
|
+
*/
|
|
5727
|
+
subagentInvocationPrefetches = /* @__PURE__ */ new Map();
|
|
5372
5728
|
/** Serialises drains so a reconnect during a drain doesn't double-process. */
|
|
5373
5729
|
draining = false;
|
|
5374
5730
|
/**
|
|
@@ -5433,7 +5789,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5433
5789
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
5434
5790
|
this.now = config.now ?? (() => Date.now());
|
|
5435
5791
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
5436
|
-
this.homeDir = config.homeDir ??
|
|
5792
|
+
this.homeDir = config.homeDir ?? homedir5();
|
|
5437
5793
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
5438
5794
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
5439
5795
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
@@ -5583,6 +5939,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5583
5939
|
}
|
|
5584
5940
|
return ids;
|
|
5585
5941
|
}
|
|
5942
|
+
/**
|
|
5943
|
+
* OpenCode user-message ids tracked for other Evident messages in a session.
|
|
5944
|
+
* Excluding this message makes an unattributed later row fail safe; a missing
|
|
5945
|
+
* watcher yields no attributions, per `hasLaterSiblingTurnStarted`'s docblock.
|
|
5946
|
+
*/
|
|
5947
|
+
siblingOpencodeMessageIds(watcher, ownEvidentMessageId) {
|
|
5948
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5949
|
+
if (!watcher) return ids;
|
|
5950
|
+
for (const inFlight of watcher.inFlight.values()) {
|
|
5951
|
+
if (inFlight.evidentMessageId !== ownEvidentMessageId) {
|
|
5952
|
+
ids.add(inFlight.opencodeMessageId);
|
|
5953
|
+
}
|
|
5954
|
+
}
|
|
5955
|
+
return ids;
|
|
5956
|
+
}
|
|
5586
5957
|
/**
|
|
5587
5958
|
* File-pull work, for `run.ts`'s idle accounting (#559).
|
|
5588
5959
|
*
|
|
@@ -5649,6 +6020,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5649
6020
|
*/
|
|
5650
6021
|
stop() {
|
|
5651
6022
|
this.stopped = true;
|
|
6023
|
+
this.sessionErrorStream?.abort.abort();
|
|
6024
|
+
this.sessionErrorStream = null;
|
|
5652
6025
|
}
|
|
5653
6026
|
/**
|
|
5654
6027
|
* The server clears this request when a new MicroVM identity is recorded, so a
|
|
@@ -5723,6 +6096,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5723
6096
|
*/
|
|
5724
6097
|
async processConversation(conv) {
|
|
5725
6098
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
6099
|
+
this.ensureSessionErrorStream();
|
|
5726
6100
|
const messages = await this.getPendingMessages(conv.id);
|
|
5727
6101
|
let dispatched = 0;
|
|
5728
6102
|
let skippedAlreadyDispatched = 0;
|
|
@@ -5795,7 +6169,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5795
6169
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
5796
6170
|
break;
|
|
5797
6171
|
}
|
|
5798
|
-
const
|
|
6172
|
+
const errorMessage3 = err instanceof Error ? err.message : String(err);
|
|
5799
6173
|
this.sessions.delete(conv.id);
|
|
5800
6174
|
this.supersede(conv.id, sessionId);
|
|
5801
6175
|
this.log({
|
|
@@ -5804,7 +6178,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5804
6178
|
conversation_id: conv.id,
|
|
5805
6179
|
message_id: message.id
|
|
5806
6180
|
});
|
|
5807
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6181
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5808
6182
|
this.log({
|
|
5809
6183
|
level: "warn",
|
|
5810
6184
|
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)}`,
|
|
@@ -5815,7 +6189,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5815
6189
|
});
|
|
5816
6190
|
this.log({
|
|
5817
6191
|
level: "error",
|
|
5818
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
6192
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
|
|
5819
6193
|
conversation_id: conv.id,
|
|
5820
6194
|
message_id: message.id
|
|
5821
6195
|
});
|
|
@@ -5836,14 +6210,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5836
6210
|
this.unconfirmedDispatchFailures.delete(message.id);
|
|
5837
6211
|
this.sessions.delete(conv.id);
|
|
5838
6212
|
this.supersede(conv.id, sessionId);
|
|
5839
|
-
const
|
|
6213
|
+
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.`;
|
|
5840
6214
|
this.log({
|
|
5841
6215
|
level: "error",
|
|
5842
|
-
message:
|
|
6216
|
+
message: errorMessage3,
|
|
5843
6217
|
conversation_id: conv.id,
|
|
5844
6218
|
message_id: message.id
|
|
5845
6219
|
});
|
|
5846
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6220
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5847
6221
|
this.log({
|
|
5848
6222
|
level: "warn",
|
|
5849
6223
|
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)}`,
|
|
@@ -6069,6 +6443,23 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6069
6443
|
if (state === "running" || state === "queued") {
|
|
6070
6444
|
const ongoing = await isSessionOngoing(this.port, sessionId);
|
|
6071
6445
|
if (ongoing === true) {
|
|
6446
|
+
if (state === "queued") {
|
|
6447
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(
|
|
6448
|
+
this.watchers.get(sessionId),
|
|
6449
|
+
message.id
|
|
6450
|
+
);
|
|
6451
|
+
if (hasLaterSiblingTurnStarted(messages, ocId ?? "", siblingOcIds)) {
|
|
6452
|
+
this.log({
|
|
6453
|
+
level: "warn",
|
|
6454
|
+
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`,
|
|
6455
|
+
conversation_id: conv.id,
|
|
6456
|
+
message_id: message.id
|
|
6457
|
+
});
|
|
6458
|
+
this.clearRedriveUnresolved(message.id);
|
|
6459
|
+
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
6460
|
+
return "dispatch";
|
|
6461
|
+
}
|
|
6462
|
+
}
|
|
6072
6463
|
return this.reattachRedrive(conv, sessionId, message, ocId);
|
|
6073
6464
|
}
|
|
6074
6465
|
if (ongoing === false) {
|
|
@@ -6158,16 +6549,37 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6158
6549
|
if (state === "done") {
|
|
6159
6550
|
const title = await this.resolveSessionTitle(sessionId, conv.id);
|
|
6160
6551
|
const usage = messageUsage(messages, ocId ?? "");
|
|
6552
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
6553
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
6554
|
+
messages,
|
|
6555
|
+
ocId ?? "",
|
|
6556
|
+
message.id
|
|
6557
|
+
);
|
|
6161
6558
|
this.log({
|
|
6162
6559
|
level: "info",
|
|
6163
6560
|
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`,
|
|
6164
6561
|
conversation_id: conv.id,
|
|
6165
6562
|
message_id: message.id
|
|
6166
6563
|
});
|
|
6167
|
-
await this.markDone(
|
|
6564
|
+
await this.markDone(
|
|
6565
|
+
conv.id,
|
|
6566
|
+
message.id,
|
|
6567
|
+
sessionId,
|
|
6568
|
+
ocId,
|
|
6569
|
+
title,
|
|
6570
|
+
usage,
|
|
6571
|
+
usageAgentName,
|
|
6572
|
+
subagentInvocations
|
|
6573
|
+
);
|
|
6168
6574
|
} else {
|
|
6169
6575
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
6170
6576
|
const usage = messageUsage(messages, ocId ?? "");
|
|
6577
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
6578
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
6579
|
+
messages,
|
|
6580
|
+
ocId ?? "",
|
|
6581
|
+
message.id
|
|
6582
|
+
);
|
|
6171
6583
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
6172
6584
|
this.log({
|
|
6173
6585
|
level: "error",
|
|
@@ -6175,7 +6587,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6175
6587
|
conversation_id: conv.id,
|
|
6176
6588
|
message_id: message.id
|
|
6177
6589
|
});
|
|
6178
|
-
await this.markFailed(
|
|
6590
|
+
await this.markFailed(
|
|
6591
|
+
conv.id,
|
|
6592
|
+
message.id,
|
|
6593
|
+
sessionId,
|
|
6594
|
+
error2,
|
|
6595
|
+
usage,
|
|
6596
|
+
failure,
|
|
6597
|
+
usageAgentName,
|
|
6598
|
+
subagentInvocations
|
|
6599
|
+
);
|
|
6179
6600
|
}
|
|
6180
6601
|
if (ocId !== null) {
|
|
6181
6602
|
await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
|
|
@@ -6719,6 +7140,24 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6719
7140
|
ambiguousPinnedSinceMs: 0,
|
|
6720
7141
|
ambiguousResolved: false
|
|
6721
7142
|
});
|
|
7143
|
+
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
7144
|
+
if (!buffered) return;
|
|
7145
|
+
this.bufferedSessionErrors.delete(sessionId);
|
|
7146
|
+
if (this.now() - buffered.receivedAt < SESSION_ERROR_BUFFER_TTL_MS) {
|
|
7147
|
+
this.handleSessionError(buffered.event);
|
|
7148
|
+
}
|
|
7149
|
+
}
|
|
7150
|
+
bufferSessionError(event) {
|
|
7151
|
+
this.bufferedSessionErrors.delete(event.sessionId);
|
|
7152
|
+
this.bufferedSessionErrors.set(event.sessionId, {
|
|
7153
|
+
event,
|
|
7154
|
+
receivedAt: this.now()
|
|
7155
|
+
});
|
|
7156
|
+
while (this.bufferedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
|
|
7157
|
+
const oldest = this.bufferedSessionErrors.keys().next().value;
|
|
7158
|
+
if (typeof oldest !== "string") break;
|
|
7159
|
+
this.bufferedSessionErrors.delete(oldest);
|
|
7160
|
+
}
|
|
6722
7161
|
}
|
|
6723
7162
|
/**
|
|
6724
7163
|
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
@@ -6923,6 +7362,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6923
7362
|
ensureWatcherRunning(sessionId) {
|
|
6924
7363
|
const watcher = this.watchers.get(sessionId);
|
|
6925
7364
|
if (!watcher) return;
|
|
7365
|
+
this.ensureSessionErrorStream();
|
|
6926
7366
|
if (watcher.loop) return;
|
|
6927
7367
|
if (watcher.inFlight.size === 0) {
|
|
6928
7368
|
this.watchers.delete(sessionId);
|
|
@@ -6938,6 +7378,154 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6938
7378
|
});
|
|
6939
7379
|
watcher.loop = loop;
|
|
6940
7380
|
}
|
|
7381
|
+
ensureSessionErrorStream() {
|
|
7382
|
+
if (this.sessionErrorStream || this.stopped) return;
|
|
7383
|
+
const abort = new AbortController();
|
|
7384
|
+
const loop = this.runSessionErrorStream(abort.signal);
|
|
7385
|
+
this.sessionErrorStream = { abort, loop };
|
|
7386
|
+
}
|
|
7387
|
+
async runSessionErrorStream(signal) {
|
|
7388
|
+
let attempt = 0;
|
|
7389
|
+
let warned = false;
|
|
7390
|
+
while (!this.stopped && !signal.aborted) {
|
|
7391
|
+
const openedAt = this.now();
|
|
7392
|
+
try {
|
|
7393
|
+
const outcome = await readSessionErrorStream(this.port, {
|
|
7394
|
+
signal,
|
|
7395
|
+
onSessionError: (event) => this.handleSessionError(event)
|
|
7396
|
+
});
|
|
7397
|
+
if (outcome.reason === "aborted" || signal.aborted) return;
|
|
7398
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
7399
|
+
if (outcome.reason === "unavailable" || outcome.reason === "ended") {
|
|
7400
|
+
if (!healthy) {
|
|
7401
|
+
const detail = outcome.reason === "unavailable" ? outcome.detail : "stream ended";
|
|
7402
|
+
this.log({
|
|
7403
|
+
level: warned ? "debug" : "warn",
|
|
7404
|
+
message: `OpenCode session-error stream ${warned ? "still unavailable" : "unavailable"} (${detail}); transcript polling remains the evidence path`
|
|
7405
|
+
});
|
|
7406
|
+
warned = true;
|
|
7407
|
+
}
|
|
7408
|
+
}
|
|
7409
|
+
if (healthy) {
|
|
7410
|
+
if (warned) {
|
|
7411
|
+
this.log({
|
|
7412
|
+
level: "info",
|
|
7413
|
+
message: "OpenCode session-error stream reconnected; transcript polling remains the evidence path"
|
|
7414
|
+
});
|
|
7415
|
+
warned = false;
|
|
7416
|
+
}
|
|
7417
|
+
attempt = 0;
|
|
7418
|
+
} else {
|
|
7419
|
+
attempt += 1;
|
|
7420
|
+
}
|
|
7421
|
+
if (this.stopped || signal.aborted) return;
|
|
7422
|
+
await this.sleep(backoffDelay(healthy ? 0 : attempt - 1, this.retry));
|
|
7423
|
+
} catch (err) {
|
|
7424
|
+
if (this.stopped || signal.aborted) return;
|
|
7425
|
+
this.log({
|
|
7426
|
+
level: "error",
|
|
7427
|
+
message: `OpenCode session-error stream failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
|
|
7428
|
+
});
|
|
7429
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
7430
|
+
const delayAttempt = healthy ? 0 : attempt;
|
|
7431
|
+
attempt = healthy ? 0 : attempt + 1;
|
|
7432
|
+
try {
|
|
7433
|
+
await this.sleep(backoffDelay(delayAttempt, this.retry));
|
|
7434
|
+
} catch (sleepErr) {
|
|
7435
|
+
this.log({
|
|
7436
|
+
level: "error",
|
|
7437
|
+
message: `OpenCode session-error stream backoff failed unexpectedly: ${sleepErr instanceof Error ? sleepErr.message : String(sleepErr)}`
|
|
7438
|
+
});
|
|
7439
|
+
}
|
|
7440
|
+
}
|
|
7441
|
+
}
|
|
7442
|
+
}
|
|
7443
|
+
handleSessionError(event) {
|
|
7444
|
+
try {
|
|
7445
|
+
const watcher = this.watchers.get(event.sessionId);
|
|
7446
|
+
if (!watcher) {
|
|
7447
|
+
this.bufferSessionError(event);
|
|
7448
|
+
this.log({
|
|
7449
|
+
level: "debug",
|
|
7450
|
+
message: `Ignoring session error for unknown session ${event.sessionId.slice(0, 8)}`
|
|
7451
|
+
});
|
|
7452
|
+
return;
|
|
7453
|
+
}
|
|
7454
|
+
if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
|
|
7455
|
+
this.log({
|
|
7456
|
+
level: "debug",
|
|
7457
|
+
message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
|
|
7458
|
+
conversation_id: watcher.conv.id
|
|
7459
|
+
});
|
|
7460
|
+
return;
|
|
7461
|
+
}
|
|
7462
|
+
const inFlight = [...watcher.inFlight.values()].filter((message) => !message.started && !message.done).sort((a, b) => a.dispatchedAt - b.dispatchedAt)[0];
|
|
7463
|
+
if (!inFlight) {
|
|
7464
|
+
this.bufferSessionError(event);
|
|
7465
|
+
this.log({
|
|
7466
|
+
level: "debug",
|
|
7467
|
+
message: `No queued in-flight turn to correlate with session error in ${event.sessionId.slice(0, 8)}`,
|
|
7468
|
+
conversation_id: watcher.conv.id
|
|
7469
|
+
});
|
|
7470
|
+
return;
|
|
7471
|
+
}
|
|
7472
|
+
if (this.sessionErrorHandled.has(inFlight.evidentMessageId)) return;
|
|
7473
|
+
this.sessionErrorHandled.add(inFlight.evidentMessageId);
|
|
7474
|
+
void this.failFromSessionError(watcher, event, inFlight);
|
|
7475
|
+
} catch (err) {
|
|
7476
|
+
this.log({
|
|
7477
|
+
level: "error",
|
|
7478
|
+
message: `Failed to handle OpenCode session error for ${event.sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`
|
|
7479
|
+
});
|
|
7480
|
+
}
|
|
7481
|
+
}
|
|
7482
|
+
async failFromSessionError(watcher, event, inFlight) {
|
|
7483
|
+
try {
|
|
7484
|
+
const messages = await getSessionMessages(this.port, event.sessionId);
|
|
7485
|
+
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7486
|
+
if (state !== "queued") {
|
|
7487
|
+
this.log({
|
|
7488
|
+
level: "debug",
|
|
7489
|
+
message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
|
|
7490
|
+
conversation_id: watcher.conv.id,
|
|
7491
|
+
message_id: inFlight.evidentMessageId
|
|
7492
|
+
});
|
|
7493
|
+
return;
|
|
7494
|
+
}
|
|
7495
|
+
this.log({
|
|
7496
|
+
level: "error",
|
|
7497
|
+
message: `OpenCode could not run message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)}: ${event.reason}`,
|
|
7498
|
+
conversation_id: watcher.conv.id,
|
|
7499
|
+
message_id: inFlight.evidentMessageId
|
|
7500
|
+
});
|
|
7501
|
+
await this.markFailed(
|
|
7502
|
+
watcher.conv.id,
|
|
7503
|
+
inFlight.evidentMessageId,
|
|
7504
|
+
event.sessionId,
|
|
7505
|
+
`OpenCode could not run this turn: ${event.reason}`
|
|
7506
|
+
);
|
|
7507
|
+
inFlight.done = true;
|
|
7508
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7509
|
+
} catch (err) {
|
|
7510
|
+
if (err instanceof ChannelAuthError) {
|
|
7511
|
+
this.log({
|
|
7512
|
+
level: "warn",
|
|
7513
|
+
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`,
|
|
7514
|
+
conversation_id: watcher.conv.id,
|
|
7515
|
+
message_id: inFlight.evidentMessageId
|
|
7516
|
+
});
|
|
7517
|
+
} else {
|
|
7518
|
+
this.log({
|
|
7519
|
+
level: "warn",
|
|
7520
|
+
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`,
|
|
7521
|
+
conversation_id: watcher.conv.id,
|
|
7522
|
+
message_id: inFlight.evidentMessageId
|
|
7523
|
+
});
|
|
7524
|
+
}
|
|
7525
|
+
} finally {
|
|
7526
|
+
this.sessionErrorHandled.delete(inFlight.evidentMessageId);
|
|
7527
|
+
}
|
|
7528
|
+
}
|
|
6941
7529
|
/**
|
|
6942
7530
|
* The per-session polling loop (WI-3). Once per tick it:
|
|
6943
7531
|
* 1. polls `GET /session/:id/message` once and, per in-flight message,
|
|
@@ -7050,6 +7638,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7050
7638
|
const conv = watcher.conv;
|
|
7051
7639
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7052
7640
|
const id = inFlight.evidentMessageId;
|
|
7641
|
+
if (messages && collectTaskCalls(messages, inFlight.opencodeMessageId).length > 0 && !this.subagentInvocationPrefetches.has(id)) {
|
|
7642
|
+
void this.resolveSubagentInvocations(
|
|
7643
|
+
messages,
|
|
7644
|
+
inFlight.opencodeMessageId,
|
|
7645
|
+
id,
|
|
7646
|
+
"prefetch"
|
|
7647
|
+
).catch((err) => {
|
|
7648
|
+
this.log({
|
|
7649
|
+
level: "warn",
|
|
7650
|
+
message: `Best-effort subagent usage prefetch failed for message ${id.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
7651
|
+
conversation_id: conv.id,
|
|
7652
|
+
message_id: id
|
|
7653
|
+
});
|
|
7654
|
+
});
|
|
7655
|
+
}
|
|
7053
7656
|
if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
|
|
7054
7657
|
else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
|
|
7055
7658
|
if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
|
|
@@ -7103,6 +7706,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7103
7706
|
message_id: inFlight.evidentMessageId
|
|
7104
7707
|
});
|
|
7105
7708
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
7709
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
7710
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
7711
|
+
messages,
|
|
7712
|
+
inFlight.opencodeMessageId,
|
|
7713
|
+
inFlight.evidentMessageId
|
|
7714
|
+
);
|
|
7106
7715
|
const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
|
|
7107
7716
|
try {
|
|
7108
7717
|
await this.markFailed(
|
|
@@ -7111,7 +7720,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7111
7720
|
sessionId,
|
|
7112
7721
|
error2,
|
|
7113
7722
|
usage,
|
|
7114
|
-
failure
|
|
7723
|
+
failure,
|
|
7724
|
+
usageAgentName,
|
|
7725
|
+
subagentInvocations
|
|
7115
7726
|
);
|
|
7116
7727
|
} catch (err) {
|
|
7117
7728
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7154,9 +7765,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7154
7765
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7155
7766
|
return;
|
|
7156
7767
|
}
|
|
7768
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
|
|
7769
|
+
const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
|
|
7157
7770
|
const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
|
|
7158
7771
|
const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
|
|
7159
|
-
if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
|
|
7772
|
+
if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
|
|
7160
7773
|
inFlight.stuckReported = true;
|
|
7161
7774
|
void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
|
|
7162
7775
|
stuck_for_ms: this.now() - inFlight.dispatchedAt
|
|
@@ -7317,7 +7930,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7317
7930
|
const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
|
|
7318
7931
|
(sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
|
|
7319
7932
|
);
|
|
7320
|
-
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
7933
|
+
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
|
|
7321
7934
|
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
7322
7935
|
this.log({
|
|
7323
7936
|
level: "debug",
|
|
@@ -7352,6 +7965,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7352
7965
|
});
|
|
7353
7966
|
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
7354
7967
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
7968
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
7969
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
7970
|
+
messages,
|
|
7971
|
+
inFlight.opencodeMessageId,
|
|
7972
|
+
inFlight.evidentMessageId
|
|
7973
|
+
);
|
|
7355
7974
|
try {
|
|
7356
7975
|
await this.markDone(
|
|
7357
7976
|
conv.id,
|
|
@@ -7359,7 +7978,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7359
7978
|
sessionId,
|
|
7360
7979
|
inFlight.opencodeMessageId,
|
|
7361
7980
|
title,
|
|
7362
|
-
usage
|
|
7981
|
+
usage,
|
|
7982
|
+
usageAgentName,
|
|
7983
|
+
subagentInvocations
|
|
7363
7984
|
);
|
|
7364
7985
|
} catch (err) {
|
|
7365
7986
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7538,6 +8159,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7538
8159
|
if (state === "failed" && !restartAborted) {
|
|
7539
8160
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
7540
8161
|
const usage = messageUsage(messages, ocId ?? "");
|
|
8162
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
8163
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
8164
|
+
messages,
|
|
8165
|
+
ocId ?? "",
|
|
8166
|
+
row.id
|
|
8167
|
+
);
|
|
7541
8168
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
7542
8169
|
this.log({
|
|
7543
8170
|
level: "error",
|
|
@@ -7546,7 +8173,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7546
8173
|
message_id: row.id
|
|
7547
8174
|
});
|
|
7548
8175
|
try {
|
|
7549
|
-
await this.markFailed(
|
|
8176
|
+
await this.markFailed(
|
|
8177
|
+
row.conversation_id,
|
|
8178
|
+
row.id,
|
|
8179
|
+
sessionId,
|
|
8180
|
+
error2,
|
|
8181
|
+
usage,
|
|
8182
|
+
failure,
|
|
8183
|
+
usageAgentName,
|
|
8184
|
+
subagentInvocations
|
|
8185
|
+
);
|
|
7550
8186
|
} catch (err) {
|
|
7551
8187
|
if (err instanceof ChannelAuthError) throw err;
|
|
7552
8188
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7708,7 +8344,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7708
8344
|
try {
|
|
7709
8345
|
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
7710
8346
|
const usage = messageUsage(messages, ocId ?? "");
|
|
7711
|
-
|
|
8347
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
8348
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
8349
|
+
messages,
|
|
8350
|
+
ocId ?? "",
|
|
8351
|
+
row.id
|
|
8352
|
+
);
|
|
8353
|
+
await this.markDone(
|
|
8354
|
+
row.conversation_id,
|
|
8355
|
+
row.id,
|
|
8356
|
+
sessionId,
|
|
8357
|
+
ocId,
|
|
8358
|
+
title,
|
|
8359
|
+
usage,
|
|
8360
|
+
usageAgentName,
|
|
8361
|
+
subagentInvocations
|
|
8362
|
+
);
|
|
7712
8363
|
} catch (err) {
|
|
7713
8364
|
if (err instanceof ChannelAuthError) throw err;
|
|
7714
8365
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7826,14 +8477,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7826
8477
|
this.unconfirmedDispatchFailures.delete(row.id);
|
|
7827
8478
|
this.sessions.delete(readoptConv.id);
|
|
7828
8479
|
this.supersede(readoptConv.id, sessionId);
|
|
7829
|
-
const
|
|
8480
|
+
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.`;
|
|
7830
8481
|
this.log({
|
|
7831
8482
|
level: "error",
|
|
7832
|
-
message:
|
|
8483
|
+
message: errorMessage3,
|
|
7833
8484
|
conversation_id: row.conversation_id,
|
|
7834
8485
|
message_id: row.id
|
|
7835
8486
|
});
|
|
7836
|
-
await this.markFailed(row.conversation_id, row.id, null,
|
|
8487
|
+
await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
|
|
7837
8488
|
this.log({
|
|
7838
8489
|
level: "warn",
|
|
7839
8490
|
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)}`,
|
|
@@ -8112,6 +8763,166 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8112
8763
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
8113
8764
|
return parent;
|
|
8114
8765
|
}
|
|
8766
|
+
usageAgentName(messages, userMessageId) {
|
|
8767
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
8768
|
+
const mode = reply?.info?.mode;
|
|
8769
|
+
if (typeof mode === "string" && mode.length > 0) return mode;
|
|
8770
|
+
const agent = reply?.info?.agent;
|
|
8771
|
+
return typeof agent === "string" && agent.length > 0 ? agent : null;
|
|
8772
|
+
}
|
|
8773
|
+
async resolveSubagentInvocations(messages, userMessageId, messageId, phase = "terminal") {
|
|
8774
|
+
if (!messages) return void 0;
|
|
8775
|
+
const cache = phase === "prefetch" ? this.subagentInvocationPrefetches : this.subagentInvocationCollections;
|
|
8776
|
+
const cached = cache.get(messageId);
|
|
8777
|
+
if (cached) return cached;
|
|
8778
|
+
const collection = this.buildSubagentInvocations(messages, userMessageId, messageId).catch(
|
|
8779
|
+
(err) => {
|
|
8780
|
+
this.log({
|
|
8781
|
+
level: "warn",
|
|
8782
|
+
message: `Best-effort subagent usage collection failed for message ${messageId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
8783
|
+
message_id: messageId
|
|
8784
|
+
});
|
|
8785
|
+
return void 0;
|
|
8786
|
+
}
|
|
8787
|
+
);
|
|
8788
|
+
cache.set(messageId, collection);
|
|
8789
|
+
const result = await collection;
|
|
8790
|
+
if (result === void 0 && cache.get(messageId) === collection) cache.delete(messageId);
|
|
8791
|
+
return result;
|
|
8792
|
+
}
|
|
8793
|
+
clearSubagentInvocationCaches(messageId) {
|
|
8794
|
+
this.subagentInvocationCollections.delete(messageId);
|
|
8795
|
+
this.subagentInvocationPrefetches.delete(messageId);
|
|
8796
|
+
}
|
|
8797
|
+
async buildSubagentInvocations(messages, userMessageId, messageId) {
|
|
8798
|
+
const rootCalls = collectTaskCalls(messages, userMessageId);
|
|
8799
|
+
if (rootCalls.length === 0) return void 0;
|
|
8800
|
+
const childMessages = /* @__PURE__ */ new Map();
|
|
8801
|
+
const seenCallIds = new Set(rootCalls.map((call) => call.callID));
|
|
8802
|
+
const work = rootCalls.map((call) => ({
|
|
8803
|
+
call,
|
|
8804
|
+
depth: 1
|
|
8805
|
+
}));
|
|
8806
|
+
const payload = [];
|
|
8807
|
+
const fetchChildMessages = (sessionId) => {
|
|
8808
|
+
const cached = childMessages.get(sessionId);
|
|
8809
|
+
if (cached) return cached;
|
|
8810
|
+
const pending = (async () => {
|
|
8811
|
+
try {
|
|
8812
|
+
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
8813
|
+
if (!res.ok) {
|
|
8814
|
+
this.log({
|
|
8815
|
+
level: "warn",
|
|
8816
|
+
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`,
|
|
8817
|
+
message_id: messageId
|
|
8818
|
+
});
|
|
8819
|
+
return null;
|
|
8820
|
+
}
|
|
8821
|
+
const body = await res.json();
|
|
8822
|
+
if (!Array.isArray(body)) throw new Error("response body was not a message array");
|
|
8823
|
+
return body;
|
|
8824
|
+
} catch (err) {
|
|
8825
|
+
this.log({
|
|
8826
|
+
level: "warn",
|
|
8827
|
+
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)}`,
|
|
8828
|
+
message_id: messageId
|
|
8829
|
+
});
|
|
8830
|
+
return null;
|
|
8831
|
+
}
|
|
8832
|
+
})();
|
|
8833
|
+
childMessages.set(sessionId, pending);
|
|
8834
|
+
return pending;
|
|
8835
|
+
};
|
|
8836
|
+
const fetchChildWithoutBlocking = async (sessionId) => {
|
|
8837
|
+
const pending = fetchChildMessages(sessionId);
|
|
8838
|
+
let timer;
|
|
8839
|
+
const timeout = new Promise((resolve4) => {
|
|
8840
|
+
timer = setTimeout(() => {
|
|
8841
|
+
this.log({
|
|
8842
|
+
level: "warn",
|
|
8843
|
+
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`,
|
|
8844
|
+
message_id: messageId
|
|
8845
|
+
});
|
|
8846
|
+
resolve4(null);
|
|
8847
|
+
}, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
|
|
8848
|
+
});
|
|
8849
|
+
try {
|
|
8850
|
+
return await Promise.race([pending, timeout]);
|
|
8851
|
+
} finally {
|
|
8852
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
8853
|
+
}
|
|
8854
|
+
};
|
|
8855
|
+
while (work.length > 0) {
|
|
8856
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8857
|
+
for (const item of work.splice(0)) {
|
|
8858
|
+
const group = groups.get(item.call.childSessionId) ?? [];
|
|
8859
|
+
group.push(item);
|
|
8860
|
+
groups.set(item.call.childSessionId, group);
|
|
8861
|
+
}
|
|
8862
|
+
const groupResults = await Promise.all(
|
|
8863
|
+
[...groups].map(async ([sessionId, items]) => ({
|
|
8864
|
+
sessionId,
|
|
8865
|
+
items,
|
|
8866
|
+
messages: sessionId === null ? [] : await fetchChildWithoutBlocking(sessionId)
|
|
8867
|
+
}))
|
|
8868
|
+
);
|
|
8869
|
+
for (const { sessionId, items, messages: child } of groupResults) {
|
|
8870
|
+
if (sessionId !== null && child === null) continue;
|
|
8871
|
+
const attribution = sessionId === null ? { invocations: [], unattributed: [] } : attributeTaskCallUsage(
|
|
8872
|
+
child,
|
|
8873
|
+
items.map(({ call }) => ({
|
|
8874
|
+
callID: call.callID,
|
|
8875
|
+
timeStart: call.timeStart,
|
|
8876
|
+
timeEnd: call.timeEnd
|
|
8877
|
+
}))
|
|
8878
|
+
);
|
|
8879
|
+
if (sessionId !== null && attribution.unattributed.length > 0) {
|
|
8880
|
+
this.log({
|
|
8881
|
+
level: "warn",
|
|
8882
|
+
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`,
|
|
8883
|
+
message_id: messageId
|
|
8884
|
+
});
|
|
8885
|
+
}
|
|
8886
|
+
const usageByCall = new Map(
|
|
8887
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.usage])
|
|
8888
|
+
);
|
|
8889
|
+
const messagesByCall = new Map(
|
|
8890
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.messages])
|
|
8891
|
+
);
|
|
8892
|
+
for (const { call, depth } of items) {
|
|
8893
|
+
const usage = usageByCall.get(call.callID) ?? null;
|
|
8894
|
+
payload.push({
|
|
8895
|
+
tool_call_id: call.callID,
|
|
8896
|
+
agent_name: call.subagentName,
|
|
8897
|
+
opencode_session_id: call.childSessionId,
|
|
8898
|
+
parent_opencode_session_id: call.parentSessionId,
|
|
8899
|
+
depth,
|
|
8900
|
+
status: call.status,
|
|
8901
|
+
started_at: call.timeStart === null ? null : new Date(call.timeStart).toISOString(),
|
|
8902
|
+
ended_at: call.timeEnd === null ? null : new Date(call.timeEnd).toISOString(),
|
|
8903
|
+
usage_provider_id: usage?.usage_provider_id ?? call.model?.providerID ?? null,
|
|
8904
|
+
usage_model_id: usage?.usage_model_id ?? call.model?.modelID ?? null,
|
|
8905
|
+
usage_tokens_input: usage?.usage_tokens_input ?? null,
|
|
8906
|
+
usage_tokens_output: usage?.usage_tokens_output ?? null,
|
|
8907
|
+
usage_tokens_reasoning: usage?.usage_tokens_reasoning ?? null,
|
|
8908
|
+
usage_tokens_cache_read: usage?.usage_tokens_cache_read ?? null,
|
|
8909
|
+
usage_tokens_cache_write: usage?.usage_tokens_cache_write ?? null,
|
|
8910
|
+
usage_cost_usd: usage?.usage_cost_usd ?? null
|
|
8911
|
+
});
|
|
8912
|
+
for (const assigned of messagesByCall.get(call.callID) ?? []) {
|
|
8913
|
+
const parentId = assigned.info?.parentID ?? assigned.parentID;
|
|
8914
|
+
if (!parentId) continue;
|
|
8915
|
+
for (const nested of collectTaskCalls([assigned], parentId)) {
|
|
8916
|
+
if (seenCallIds.has(nested.callID)) continue;
|
|
8917
|
+
seenCallIds.add(nested.callID);
|
|
8918
|
+
work.push({ call: nested, depth: depth + 1 });
|
|
8919
|
+
}
|
|
8920
|
+
}
|
|
8921
|
+
}
|
|
8922
|
+
}
|
|
8923
|
+
}
|
|
8924
|
+
return payload.length > 0 ? payload : void 0;
|
|
8925
|
+
}
|
|
8115
8926
|
/**
|
|
8116
8927
|
* OpenCode's synchronous default session title (e.g.
|
|
8117
8928
|
* `"New session - 1737800000000"`), assigned immediately when a session is
|
|
@@ -8595,7 +9406,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8595
9406
|
* watcher retries next tick within the
|
|
8596
9407
|
* deadline, Finding 4).
|
|
8597
9408
|
*/
|
|
8598
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
|
|
9409
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
|
|
8599
9410
|
const res = await this.fetchImpl(
|
|
8600
9411
|
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
8601
9412
|
{
|
|
@@ -8611,15 +9422,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8611
9422
|
opencode_session_id: sessionId,
|
|
8612
9423
|
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
8613
9424
|
...title ? { title } : {},
|
|
8614
|
-
...usage ? usage : {}
|
|
9425
|
+
...usage ? usage : {},
|
|
9426
|
+
...usageAgentName ? { usage_agent_name: usageAgentName } : {},
|
|
9427
|
+
...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
|
|
8615
9428
|
})
|
|
8616
9429
|
}
|
|
8617
9430
|
);
|
|
8618
9431
|
this.assertAuth(res, "marking message as done");
|
|
8619
|
-
if (res.ok)
|
|
9432
|
+
if (res.ok) {
|
|
9433
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
9434
|
+
return;
|
|
9435
|
+
}
|
|
8620
9436
|
if (isRetryableStatus(res.status)) {
|
|
8621
9437
|
throw new Error(`marking message as done: HTTP ${res.status}`);
|
|
8622
9438
|
}
|
|
9439
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8623
9440
|
throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
|
|
8624
9441
|
}
|
|
8625
9442
|
/**
|
|
@@ -8634,7 +9451,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8634
9451
|
* exists but is wedged, so the next attempt must get a fresh one
|
|
8635
9452
|
* instead of reusing it — see WI-1's server-side null-clearing PATCH).
|
|
8636
9453
|
*/
|
|
8637
|
-
async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
|
|
9454
|
+
async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
|
|
8638
9455
|
const body = { status: "failed" };
|
|
8639
9456
|
if (sessionId === null) {
|
|
8640
9457
|
body.opencode_session_id = null;
|
|
@@ -8643,23 +9460,33 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8643
9460
|
}
|
|
8644
9461
|
if (error2 !== void 0) body.error = error2;
|
|
8645
9462
|
if (usage) Object.assign(body, usage);
|
|
9463
|
+
if (usageAgentName) body.usage_agent_name = usageAgentName;
|
|
9464
|
+
if (subagentInvocations && subagentInvocations.length > 0) {
|
|
9465
|
+
body.subagent_invocations = subagentInvocations;
|
|
9466
|
+
}
|
|
8646
9467
|
if (failure) {
|
|
8647
9468
|
body.failure_kind = failure.kind;
|
|
8648
9469
|
body.failure_provider_id = failure.providerId;
|
|
8649
9470
|
body.failure_model_id = failure.modelId;
|
|
8650
9471
|
body.failure_reason = failure.reason;
|
|
8651
9472
|
}
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
9473
|
+
try {
|
|
9474
|
+
await this.callWithRetry(
|
|
9475
|
+
"marking message as failed",
|
|
9476
|
+
() => this.fetchImpl(
|
|
9477
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
9478
|
+
{
|
|
9479
|
+
method: "PATCH",
|
|
9480
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
9481
|
+
body: JSON.stringify(body)
|
|
9482
|
+
}
|
|
9483
|
+
)
|
|
9484
|
+
);
|
|
9485
|
+
} catch (err) {
|
|
9486
|
+
if (err instanceof ChannelTerminalError) this.clearSubagentInvocationCaches(messageId);
|
|
9487
|
+
throw err;
|
|
9488
|
+
}
|
|
9489
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8663
9490
|
}
|
|
8664
9491
|
/**
|
|
8665
9492
|
* Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
|
|
@@ -8942,6 +9769,13 @@ import chalk5 from "chalk";
|
|
|
8942
9769
|
import ora2 from "ora";
|
|
8943
9770
|
import { select as select2 } from "@inquirer/prompts";
|
|
8944
9771
|
var INTERACTIVE_START_TIMEOUT_MS = 3e4;
|
|
9772
|
+
function checkNonInteractivePortConflict(port, isPortInUseFn) {
|
|
9773
|
+
if (isPortInUseFn(port)) {
|
|
9774
|
+
throw new Error(
|
|
9775
|
+
`Port ${port} is already in use by a non-OpenCode process. Free it or pass --port.`
|
|
9776
|
+
);
|
|
9777
|
+
}
|
|
9778
|
+
}
|
|
8945
9779
|
async function ensureOpenCodeRunning(ctx) {
|
|
8946
9780
|
const healthCheck = await checkOpenCodeHealth(ctx.port);
|
|
8947
9781
|
if (healthCheck.healthy) {
|
|
@@ -8989,6 +9823,7 @@ async function ensureOpenCodeRunning(ctx) {
|
|
|
8989
9823
|
}
|
|
8990
9824
|
}
|
|
8991
9825
|
if (!ctx.interactive) {
|
|
9826
|
+
checkNonInteractivePortConflict(ctx.port, isPortInUse);
|
|
8992
9827
|
ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
|
|
8993
9828
|
const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
|
|
8994
9829
|
const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
|
|
@@ -9070,9 +9905,119 @@ Port ${port} is already in use.`));
|
|
|
9070
9905
|
return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
|
|
9071
9906
|
}
|
|
9072
9907
|
|
|
9908
|
+
// src/commands/ensure-opencode-v2.ts
|
|
9909
|
+
import chalk6 from "chalk";
|
|
9910
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
9911
|
+
async function probeOpenCode2WithoutPassword(port) {
|
|
9912
|
+
try {
|
|
9913
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
9914
|
+
signal: AbortSignal.timeout(2e3)
|
|
9915
|
+
});
|
|
9916
|
+
if (response.status === 401) {
|
|
9917
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
9918
|
+
}
|
|
9919
|
+
if (!response.ok) {
|
|
9920
|
+
return { healthy: false, error: `HTTP ${response.status}` };
|
|
9921
|
+
}
|
|
9922
|
+
return { healthy: true };
|
|
9923
|
+
} catch (error2) {
|
|
9924
|
+
return {
|
|
9925
|
+
healthy: false,
|
|
9926
|
+
error: error2 instanceof Error ? error2.message : "Unknown error"
|
|
9927
|
+
};
|
|
9928
|
+
}
|
|
9929
|
+
}
|
|
9930
|
+
function unknownPasswordError(port) {
|
|
9931
|
+
return new Error(
|
|
9932
|
+
`OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
|
|
9933
|
+
);
|
|
9934
|
+
}
|
|
9935
|
+
function v2SessionSupportIncompleteError() {
|
|
9936
|
+
return new Error(
|
|
9937
|
+
"OpenCode V2 (opencode2) session support is incomplete \u2014 see https://github.com/sroze/evident/issues/2075. Not starting a new opencode2 process."
|
|
9938
|
+
);
|
|
9939
|
+
}
|
|
9940
|
+
async function ensureOpenCode2Running(ctx) {
|
|
9941
|
+
const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
|
|
9942
|
+
if (initialHealth.authFailed) {
|
|
9943
|
+
throw unknownPasswordError(ctx.port);
|
|
9944
|
+
}
|
|
9945
|
+
if (initialHealth.healthy) {
|
|
9946
|
+
return {
|
|
9947
|
+
port: ctx.port,
|
|
9948
|
+
process: null,
|
|
9949
|
+
version: null,
|
|
9950
|
+
notReadyReason: null,
|
|
9951
|
+
password: null
|
|
9952
|
+
};
|
|
9953
|
+
}
|
|
9954
|
+
if (!isOpenCode2Installed()) {
|
|
9955
|
+
throw new Error(
|
|
9956
|
+
"OpenCode V2 (opencode2) is not installed. Install it with: npm install -g @opencode-ai/cli@beta"
|
|
9957
|
+
);
|
|
9958
|
+
}
|
|
9959
|
+
let port = ctx.port;
|
|
9960
|
+
if (!ctx.interactive) {
|
|
9961
|
+
checkNonInteractivePortConflict(port, isPortInUse);
|
|
9962
|
+
} else if (isPortInUse(port)) {
|
|
9963
|
+
console.log(chalk6.yellow(`
|
|
9964
|
+
Port ${port} is already in use.`));
|
|
9965
|
+
const alternativePort = findAvailablePort(port + 1);
|
|
9966
|
+
if (alternativePort) {
|
|
9967
|
+
const useAlternative = await select3({
|
|
9968
|
+
message: `Use port ${alternativePort} instead?`,
|
|
9969
|
+
choices: [
|
|
9970
|
+
{ name: `Yes, use port ${alternativePort}`, value: "yes" },
|
|
9971
|
+
{ name: "No, I will free the port manually", value: "no" }
|
|
9972
|
+
]
|
|
9973
|
+
});
|
|
9974
|
+
if (useAlternative === "yes") {
|
|
9975
|
+
port = alternativePort;
|
|
9976
|
+
} else {
|
|
9977
|
+
throw new Error(`Port ${ctx.port} is in use`);
|
|
9978
|
+
}
|
|
9979
|
+
}
|
|
9980
|
+
}
|
|
9981
|
+
if (!ctx.interactive) {
|
|
9982
|
+
throw v2SessionSupportIncompleteError();
|
|
9983
|
+
}
|
|
9984
|
+
console.log(chalk6.yellow(`
|
|
9985
|
+
${v2SessionSupportIncompleteError().message}`));
|
|
9986
|
+
const action = await select3({
|
|
9987
|
+
message: "OpenCode V2 is not running. What would you like to do?",
|
|
9988
|
+
choices: [
|
|
9989
|
+
{
|
|
9990
|
+
name: "Show me the command",
|
|
9991
|
+
value: "manual",
|
|
9992
|
+
description: "Display the command to run manually"
|
|
9993
|
+
},
|
|
9994
|
+
{
|
|
9995
|
+
name: "Continue without OpenCode V2",
|
|
9996
|
+
value: "continue",
|
|
9997
|
+
description: "Requests will fail until OpenCode V2 starts"
|
|
9998
|
+
}
|
|
9999
|
+
]
|
|
10000
|
+
});
|
|
10001
|
+
if (action === "manual") {
|
|
10002
|
+
blank();
|
|
10003
|
+
console.log(chalk6.bold("Run this command in another terminal:"));
|
|
10004
|
+
blank();
|
|
10005
|
+
console.log(` ${chalk6.cyan(`opencode2 serve --port ${port}`)}`);
|
|
10006
|
+
blank();
|
|
10007
|
+
throw new Error("Please start OpenCode V2 manually");
|
|
10008
|
+
}
|
|
10009
|
+
return {
|
|
10010
|
+
port,
|
|
10011
|
+
process: null,
|
|
10012
|
+
version: null,
|
|
10013
|
+
notReadyReason: "you chose to continue without OpenCode V2",
|
|
10014
|
+
password: null
|
|
10015
|
+
};
|
|
10016
|
+
}
|
|
10017
|
+
|
|
9073
10018
|
// src/lib/runner-credentials.ts
|
|
9074
|
-
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
9075
|
-
import { spawn as spawn5 } from "child_process";
|
|
10019
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
10020
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
9076
10021
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
9077
10022
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
9078
10023
|
var GITHUB_PROBE_TIMEOUT_MS = 1e4;
|
|
@@ -9344,11 +10289,11 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
9344
10289
|
}
|
|
9345
10290
|
|
|
9346
10291
|
// src/lib/opencode/config-overlay.ts
|
|
9347
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
9348
|
-
import { copyFileSync, existsSync as existsSync2, statSync as
|
|
9349
|
-
import { isAbsolute as isAbsolute2, join as
|
|
10292
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
10293
|
+
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
10294
|
+
import { isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "node:path";
|
|
9350
10295
|
function isFile(filePath) {
|
|
9351
|
-
return existsSync2(filePath) &&
|
|
10296
|
+
return existsSync2(filePath) && statSync6(filePath).isFile();
|
|
9352
10297
|
}
|
|
9353
10298
|
function applyRunnerOpenCodeConfig({
|
|
9354
10299
|
overlayPath,
|
|
@@ -9360,7 +10305,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9360
10305
|
return;
|
|
9361
10306
|
}
|
|
9362
10307
|
const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
|
|
9363
|
-
const target = isFile(
|
|
10308
|
+
const target = isFile(join9(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
9364
10309
|
if (!isFile(source)) {
|
|
9365
10310
|
log3(
|
|
9366
10311
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -9368,7 +10313,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9368
10313
|
);
|
|
9369
10314
|
return;
|
|
9370
10315
|
}
|
|
9371
|
-
copyFileSync(source,
|
|
10316
|
+
copyFileSync(source, join9(cwd, target));
|
|
9372
10317
|
try {
|
|
9373
10318
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
9374
10319
|
stdio: "ignore"
|
|
@@ -9377,11 +10322,11 @@ function applyRunnerOpenCodeConfig({
|
|
|
9377
10322
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
9378
10323
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
9379
10324
|
}
|
|
9380
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
10325
|
+
log3(`Applied runner OpenCode config ${source} to ${join9(cwd, target)}`);
|
|
9381
10326
|
}
|
|
9382
10327
|
|
|
9383
10328
|
// src/lib/credential-sync.ts
|
|
9384
|
-
import { renameSync, writeFileSync as writeFileSync5 } from "fs";
|
|
10329
|
+
import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
9385
10330
|
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
9386
10331
|
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
9387
10332
|
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
@@ -9391,7 +10336,7 @@ var MAX_FLUSH_PASSES = 2;
|
|
|
9391
10336
|
function outcomesWith(outcome) {
|
|
9392
10337
|
return { claude: outcome, opencode: outcome };
|
|
9393
10338
|
}
|
|
9394
|
-
function
|
|
10339
|
+
function errorMessage2(error2) {
|
|
9395
10340
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
9396
10341
|
}
|
|
9397
10342
|
function waitForSettlement(promise, timeoutMs) {
|
|
@@ -9418,7 +10363,7 @@ function writeMarker(markerPath, outcomes, log3) {
|
|
|
9418
10363
|
writeFileSync5(temporaryPath, body, { mode: 384 });
|
|
9419
10364
|
renameSync(temporaryPath, markerPath);
|
|
9420
10365
|
} catch (error2) {
|
|
9421
|
-
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${
|
|
10366
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
|
|
9422
10367
|
}
|
|
9423
10368
|
}
|
|
9424
10369
|
function intervalSeconds(env, log3) {
|
|
@@ -9450,7 +10395,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
|
9450
10395
|
},
|
|
9451
10396
|
(error2) => {
|
|
9452
10397
|
failed = true;
|
|
9453
|
-
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${
|
|
10398
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
|
|
9454
10399
|
}
|
|
9455
10400
|
);
|
|
9456
10401
|
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
@@ -9510,7 +10455,7 @@ function createCredentialSync({
|
|
|
9510
10455
|
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
9511
10456
|
} catch (error2) {
|
|
9512
10457
|
outcomes[store] = "failed";
|
|
9513
|
-
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${
|
|
10458
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
|
|
9514
10459
|
}
|
|
9515
10460
|
}
|
|
9516
10461
|
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
@@ -9652,7 +10597,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9652
10597
|
if (trimmed === "") {
|
|
9653
10598
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
9654
10599
|
}
|
|
9655
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
10600
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join10(homeDir, trimmed.slice(2)) : trimmed;
|
|
9656
10601
|
if (!isAbsolute3(expanded)) {
|
|
9657
10602
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
9658
10603
|
}
|
|
@@ -9676,6 +10621,28 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9676
10621
|
var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
|
|
9677
10622
|
var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
|
|
9678
10623
|
var OPENCODE_START_TIMEOUT_ENV = "EVIDENT_OPENCODE_START_TIMEOUT";
|
|
10624
|
+
var OPENCODE_VERSION_ENV = "EVIDENT_OPENCODE_VERSION";
|
|
10625
|
+
function resolveOpenCodeVersion(options, env = process.env) {
|
|
10626
|
+
let raw;
|
|
10627
|
+
let source;
|
|
10628
|
+
if (options.opencodeVersion !== void 0) {
|
|
10629
|
+
raw = options.opencodeVersion;
|
|
10630
|
+
source = "--opencode-version";
|
|
10631
|
+
} else if (env[OPENCODE_VERSION_ENV] !== void 0 && env[OPENCODE_VERSION_ENV] !== "") {
|
|
10632
|
+
raw = env[OPENCODE_VERSION_ENV];
|
|
10633
|
+
source = OPENCODE_VERSION_ENV;
|
|
10634
|
+
} else {
|
|
10635
|
+
return { version: "v1", warnings: [] };
|
|
10636
|
+
}
|
|
10637
|
+
const normalized = raw.trim().toLowerCase();
|
|
10638
|
+
if (normalized !== "v1" && normalized !== "v2") {
|
|
10639
|
+
return {
|
|
10640
|
+
version: "v1",
|
|
10641
|
+
warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or v2; using the default v1`]
|
|
10642
|
+
};
|
|
10643
|
+
}
|
|
10644
|
+
return { version: normalized, warnings: [] };
|
|
10645
|
+
}
|
|
9679
10646
|
function resolveOpenCodeStartTimeoutMs(options, env = process.env) {
|
|
9680
10647
|
const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1e3;
|
|
9681
10648
|
let raw;
|
|
@@ -9742,7 +10709,7 @@ function log2(state, message, level = "info") {
|
|
|
9742
10709
|
})
|
|
9743
10710
|
);
|
|
9744
10711
|
} else if (!state.interactive) {
|
|
9745
|
-
const prefix = level === "error" ?
|
|
10712
|
+
const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
|
|
9746
10713
|
console.log(`${prefix} ${message}`);
|
|
9747
10714
|
}
|
|
9748
10715
|
}
|
|
@@ -9772,7 +10739,7 @@ function logActivity(state, entry) {
|
|
|
9772
10739
|
}
|
|
9773
10740
|
function reportSessionDbRecovery(state) {
|
|
9774
10741
|
try {
|
|
9775
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
10742
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
|
|
9776
10743
|
for (const record of report.records) {
|
|
9777
10744
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
9778
10745
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -9803,18 +10770,18 @@ function reportSessionDbRecoveryRecord(state, record) {
|
|
|
9803
10770
|
function displayStatus(state) {
|
|
9804
10771
|
if (!state.interactive) return;
|
|
9805
10772
|
const attempt = state.connection?.reconnectAttempt ?? 0;
|
|
9806
|
-
const tunnel = state.connected ?
|
|
9807
|
-
const opencode = state.opencodeConnected ?
|
|
9808
|
-
const messages = state.messageCount > 0 ?
|
|
10773
|
+
const tunnel = state.connected ? chalk7.green("tunnel: connected") : attempt > 0 ? chalk7.yellow(`tunnel: reconnecting (#${attempt})`) : chalk7.yellow("tunnel: connecting");
|
|
10774
|
+
const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
|
|
10775
|
+
const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
|
|
9809
10776
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
9810
|
-
const detail = last ?
|
|
10777
|
+
const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
|
|
9811
10778
|
const agent = state.agentName ?? state.agentId;
|
|
9812
10779
|
console.log(
|
|
9813
|
-
`${
|
|
10780
|
+
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
9814
10781
|
);
|
|
9815
10782
|
}
|
|
9816
10783
|
async function promptForLogin(promptMessage, successMessage) {
|
|
9817
|
-
const action = await
|
|
10784
|
+
const action = await select4({
|
|
9818
10785
|
message: promptMessage,
|
|
9819
10786
|
choices: [
|
|
9820
10787
|
{
|
|
@@ -9830,7 +10797,7 @@ async function promptForLogin(promptMessage, successMessage) {
|
|
|
9830
10797
|
]
|
|
9831
10798
|
});
|
|
9832
10799
|
if (action === "exit") {
|
|
9833
|
-
console.log(
|
|
10800
|
+
console.log(chalk7.dim(`
|
|
9834
10801
|
You can log in later by running: ${getCliName()} login`));
|
|
9835
10802
|
process.exit(0);
|
|
9836
10803
|
}
|
|
@@ -9841,7 +10808,7 @@ You can log in later by running: ${getCliName()} login`));
|
|
|
9841
10808
|
process.exit(1);
|
|
9842
10809
|
}
|
|
9843
10810
|
blank();
|
|
9844
|
-
console.log(
|
|
10811
|
+
console.log(chalk7.green(successMessage));
|
|
9845
10812
|
blank();
|
|
9846
10813
|
return { token: credentials2.token, authType: "bearer", user: credentials2.user };
|
|
9847
10814
|
}
|
|
@@ -9854,12 +10821,12 @@ async function handleAuthError(state, error2) {
|
|
|
9854
10821
|
if (state.interactive) displayStatus(state);
|
|
9855
10822
|
if (!state.interactive) {
|
|
9856
10823
|
blank();
|
|
9857
|
-
console.log(
|
|
9858
|
-
console.log(
|
|
10824
|
+
console.log(chalk7.red("Authentication expired"));
|
|
10825
|
+
console.log(chalk7.dim("Your authentication token is no longer valid."));
|
|
9859
10826
|
blank();
|
|
9860
|
-
console.log(
|
|
9861
|
-
console.log(
|
|
9862
|
-
console.log(
|
|
10827
|
+
console.log(chalk7.dim("To fix this:"));
|
|
10828
|
+
console.log(chalk7.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
|
|
10829
|
+
console.log(chalk7.dim(" 2. Restart this command"));
|
|
9863
10830
|
blank();
|
|
9864
10831
|
await cleanup(state);
|
|
9865
10832
|
await shutdownTelemetry();
|
|
@@ -9867,7 +10834,7 @@ async function handleAuthError(state, error2) {
|
|
|
9867
10834
|
return { success: false };
|
|
9868
10835
|
}
|
|
9869
10836
|
blank();
|
|
9870
|
-
console.log(
|
|
10837
|
+
console.log(chalk7.yellow("Your authentication has expired."));
|
|
9871
10838
|
blank();
|
|
9872
10839
|
try {
|
|
9873
10840
|
const credentials2 = await promptForLogin(
|
|
@@ -9931,6 +10898,14 @@ async function driveChannels(state, driver) {
|
|
|
9931
10898
|
const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
|
|
9932
10899
|
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
9933
10900
|
if (opencodeAuthApplied) state.openaiUsageRearm?.();
|
|
10901
|
+
if (claudeCredentialApplied || opencodeAuthApplied) {
|
|
10902
|
+
void reloadProviderCache(state.port).catch(
|
|
10903
|
+
(error2) => logActivity(state, {
|
|
10904
|
+
type: "error",
|
|
10905
|
+
error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
10906
|
+
})
|
|
10907
|
+
);
|
|
10908
|
+
}
|
|
9934
10909
|
if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
|
|
9935
10910
|
idlePolls = 0;
|
|
9936
10911
|
idleMs = 0;
|
|
@@ -9958,8 +10933,8 @@ async function driveChannels(state, driver) {
|
|
|
9958
10933
|
state.running = false;
|
|
9959
10934
|
break;
|
|
9960
10935
|
}
|
|
9961
|
-
const
|
|
9962
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
10936
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
|
|
10937
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage3}` });
|
|
9963
10938
|
if (state.interactive) displayStatus(state);
|
|
9964
10939
|
if (driver.hasInFlightWatchers()) {
|
|
9965
10940
|
consecutiveDrainFailures = 0;
|
|
@@ -9997,9 +10972,18 @@ async function driveChannels(state, driver) {
|
|
|
9997
10972
|
}
|
|
9998
10973
|
}
|
|
9999
10974
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
10000
|
-
var SESSION_DB_RECLAIM_MAX_PAGES =
|
|
10975
|
+
var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
|
|
10976
|
+
function shouldWarnForReclaimSkip(reason) {
|
|
10977
|
+
if (reason !== "sqlite-unavailable") return false;
|
|
10978
|
+
const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
|
|
10979
|
+
if (!version2) return false;
|
|
10980
|
+
const major = Number(version2[1]);
|
|
10981
|
+
const minor = Number(version2[2]);
|
|
10982
|
+
const patch = Number(version2[3]);
|
|
10983
|
+
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
10984
|
+
}
|
|
10001
10985
|
function sessionDbPath() {
|
|
10002
|
-
return
|
|
10986
|
+
return join10(homedir6(), ".local", "share", "opencode", "opencode.db");
|
|
10003
10987
|
}
|
|
10004
10988
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
10005
10989
|
const record = {
|
|
@@ -10095,7 +11079,7 @@ async function runSweep(state, driver, config) {
|
|
|
10095
11079
|
} else {
|
|
10096
11080
|
logActivity(state, {
|
|
10097
11081
|
type: "info",
|
|
10098
|
-
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})`
|
|
11082
|
+
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : "")
|
|
10099
11083
|
});
|
|
10100
11084
|
}
|
|
10101
11085
|
} catch (error2) {
|
|
@@ -10118,13 +11102,20 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
10118
11102
|
for (const warning2 of config.warnings) {
|
|
10119
11103
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
10120
11104
|
}
|
|
10121
|
-
const dbBytes = statSessionDbBytes(
|
|
11105
|
+
const dbBytes = statSessionDbBytes(homedir6());
|
|
10122
11106
|
void (async () => {
|
|
10123
|
-
const
|
|
11107
|
+
const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
11108
|
+
if (reclaimAvailability !== null) {
|
|
11109
|
+
logActivity(state, {
|
|
11110
|
+
type: "info",
|
|
11111
|
+
level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
|
|
11112
|
+
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
|
|
11113
|
+
});
|
|
11114
|
+
}
|
|
10124
11115
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
10125
11116
|
dbBytes,
|
|
10126
11117
|
cleanupEnabled: config.enabled,
|
|
10127
|
-
reclaimSkipReason
|
|
11118
|
+
reclaimSkipReason: reclaimAvailability?.reason ?? null
|
|
10128
11119
|
});
|
|
10129
11120
|
if (sizeWarning !== null) {
|
|
10130
11121
|
logActivity(state, { type: "info", level: "warn", message: sizeWarning });
|
|
@@ -10291,14 +11282,11 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
10291
11282
|
report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
|
|
10292
11283
|
isLocalCredentialProblem,
|
|
10293
11284
|
forcedOnHint: "run `claude` to sign in",
|
|
10294
|
-
firstDelayMs:
|
|
10295
|
-
nextDelayMs:
|
|
10296
|
-
failureLogLevel:
|
|
11285
|
+
firstDelayMs: firstReportDelayMs,
|
|
11286
|
+
nextDelayMs: usageReportDelayMs,
|
|
11287
|
+
failureLogLevel: usageReportFailureLogLevel
|
|
10297
11288
|
});
|
|
10298
11289
|
}
|
|
10299
|
-
var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
|
|
10300
|
-
var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
10301
|
-
var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
10302
11290
|
function scheduleResourceUsageReporting(state, options) {
|
|
10303
11291
|
const { enabled, warnings } = resolveResourceUsageReportingEnabled(
|
|
10304
11292
|
options.resourceUsageReporting,
|
|
@@ -10319,7 +11307,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10319
11307
|
});
|
|
10320
11308
|
return;
|
|
10321
11309
|
}
|
|
10322
|
-
const { collect, stop } = createResourceUsageCollector(
|
|
11310
|
+
const { collect, stop } = createResourceUsageCollector(homedir6());
|
|
10323
11311
|
state.stopResourceUsageSampling = stop;
|
|
10324
11312
|
let consecutiveFailures = 0;
|
|
10325
11313
|
const tick = async () => {
|
|
@@ -10351,10 +11339,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10351
11339
|
consecutiveFailures++;
|
|
10352
11340
|
logActivity(state, {
|
|
10353
11341
|
type: "info",
|
|
10354
|
-
level:
|
|
10355
|
-
consecutiveFailures,
|
|
10356
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
10357
|
-
),
|
|
11342
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
10358
11343
|
message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
|
|
10359
11344
|
});
|
|
10360
11345
|
}
|
|
@@ -10363,20 +11348,11 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10363
11348
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
10364
11349
|
logActivity(state, {
|
|
10365
11350
|
type: "info",
|
|
10366
|
-
level:
|
|
10367
|
-
consecutiveFailures,
|
|
10368
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
10369
|
-
),
|
|
11351
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
10370
11352
|
message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
|
|
10371
11353
|
});
|
|
10372
11354
|
} finally {
|
|
10373
|
-
state.resourceUsageTimer = setTimeout(
|
|
10374
|
-
() => void tick(),
|
|
10375
|
-
jitteredDelayMs(
|
|
10376
|
-
RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
|
|
10377
|
-
RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
|
|
10378
|
-
)
|
|
10379
|
-
);
|
|
11355
|
+
state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
|
|
10380
11356
|
}
|
|
10381
11357
|
};
|
|
10382
11358
|
state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
|
|
@@ -10416,6 +11392,8 @@ async function cleanup(state, opts = {}) {
|
|
|
10416
11392
|
clearTimeout(timer);
|
|
10417
11393
|
}
|
|
10418
11394
|
state.sessionCleanupTimers = [];
|
|
11395
|
+
state.stopOpenCodeLogTail?.();
|
|
11396
|
+
state.stopOpenCodeLogTail = null;
|
|
10419
11397
|
if (state.claudeUsageTimer) {
|
|
10420
11398
|
clearTimeout(state.claudeUsageTimer);
|
|
10421
11399
|
state.claudeUsageTimer = null;
|
|
@@ -10554,7 +11532,7 @@ async function run(options) {
|
|
|
10554
11532
|
let fileSyncDirectories;
|
|
10555
11533
|
try {
|
|
10556
11534
|
logLevel = resolveLogLevel(options);
|
|
10557
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
11535
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
|
|
10558
11536
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
10559
11537
|
throw new Error(
|
|
10560
11538
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -10585,6 +11563,7 @@ async function run(options) {
|
|
|
10585
11563
|
opencodeVersion: null,
|
|
10586
11564
|
sessionDbProvenanceAnomaly: false,
|
|
10587
11565
|
opencodeProcess: null,
|
|
11566
|
+
stopOpenCodeLogTail: null,
|
|
10588
11567
|
litestreamProcess: null,
|
|
10589
11568
|
connection: null,
|
|
10590
11569
|
channelDriver: null,
|
|
@@ -10652,15 +11631,15 @@ async function run(options) {
|
|
|
10652
11631
|
printError("Authentication required");
|
|
10653
11632
|
blank();
|
|
10654
11633
|
console.log(
|
|
10655
|
-
|
|
11634
|
+
chalk7.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
|
|
10656
11635
|
);
|
|
10657
|
-
console.log(
|
|
11636
|
+
console.log(chalk7.dim("Or run `evident login` for interactive authentication"));
|
|
10658
11637
|
blank();
|
|
10659
11638
|
process.exit(1);
|
|
10660
11639
|
return;
|
|
10661
11640
|
}
|
|
10662
11641
|
blank();
|
|
10663
|
-
console.log(
|
|
11642
|
+
console.log(chalk7.yellow("You are not logged in to Evident."));
|
|
10664
11643
|
blank();
|
|
10665
11644
|
credentials2 = await promptForLogin(
|
|
10666
11645
|
"Would you like to log in now?",
|
|
@@ -10710,7 +11689,7 @@ async function run(options) {
|
|
|
10710
11689
|
);
|
|
10711
11690
|
blank();
|
|
10712
11691
|
console.log(
|
|
10713
|
-
|
|
11692
|
+
chalk7.dim(
|
|
10714
11693
|
"Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
|
|
10715
11694
|
)
|
|
10716
11695
|
);
|
|
@@ -10733,15 +11712,15 @@ async function run(options) {
|
|
|
10733
11712
|
);
|
|
10734
11713
|
if (interactive && !state.json) {
|
|
10735
11714
|
blank();
|
|
10736
|
-
console.log(
|
|
10737
|
-
console.log(
|
|
11715
|
+
console.log(chalk7.bold("Evident Run"));
|
|
11716
|
+
console.log(chalk7.dim("-".repeat(40)));
|
|
10738
11717
|
}
|
|
10739
11718
|
const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
|
|
10740
11719
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
10741
11720
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
10742
11721
|
spinner?.fail("Authentication failed");
|
|
10743
11722
|
blank();
|
|
10744
|
-
console.log(
|
|
11723
|
+
console.log(chalk7.yellow("Your authentication token is invalid or expired."));
|
|
10745
11724
|
blank();
|
|
10746
11725
|
credentials2 = await promptForLogin(
|
|
10747
11726
|
"Would you like to log in again?",
|
|
@@ -10789,6 +11768,13 @@ async function run(options) {
|
|
|
10789
11768
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
10790
11769
|
}
|
|
10791
11770
|
state.credentialSync?.arm();
|
|
11771
|
+
state.stopOpenCodeLogTail = tailOpenCodeLogFile(
|
|
11772
|
+
resolveOpenCodeLogPath(homedir6(), process.env),
|
|
11773
|
+
createOpenCodeActivityForwarder(() => ({
|
|
11774
|
+
agentId: state.agentId,
|
|
11775
|
+
authHeader: state.authHeader
|
|
11776
|
+
}))
|
|
11777
|
+
).stop;
|
|
10792
11778
|
let sessionDbVerifyFatal = false;
|
|
10793
11779
|
if (!options.restoreSessionDb) {
|
|
10794
11780
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -10838,6 +11824,13 @@ async function run(options) {
|
|
|
10838
11824
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
10839
11825
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
10840
11826
|
}
|
|
11827
|
+
const { version: opencodeVersion, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(
|
|
11828
|
+
options,
|
|
11829
|
+
process.env
|
|
11830
|
+
);
|
|
11831
|
+
for (const warning2 of opencodeVersionWarnings) {
|
|
11832
|
+
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
11833
|
+
}
|
|
10841
11834
|
const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
|
|
10842
11835
|
for (const warning2 of maxActiveSessionsWarnings) {
|
|
10843
11836
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
@@ -10845,7 +11838,14 @@ async function run(options) {
|
|
|
10845
11838
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
10846
11839
|
const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
|
|
10847
11840
|
try {
|
|
10848
|
-
const oc = await
|
|
11841
|
+
const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
|
|
11842
|
+
port: state.port,
|
|
11843
|
+
interactive: state.interactive,
|
|
11844
|
+
agentId: state.agentId,
|
|
11845
|
+
log: (message) => log2(state, message),
|
|
11846
|
+
startTimeoutMs: opencodeStartTimeoutMs,
|
|
11847
|
+
inheritStdio: Boolean(options.opencodePidFile)
|
|
11848
|
+
}) : await ensureOpenCodeRunning({
|
|
10849
11849
|
port: state.port,
|
|
10850
11850
|
interactive: state.interactive,
|
|
10851
11851
|
agentId: state.agentId,
|
|
@@ -10872,7 +11872,7 @@ async function run(options) {
|
|
|
10872
11872
|
const provenance = checkSessionDbProvenance({
|
|
10873
11873
|
dbPath: sessionDbPath(),
|
|
10874
11874
|
currentVersion: state.opencodeVersion,
|
|
10875
|
-
homeDir:
|
|
11875
|
+
homeDir: homedir6(),
|
|
10876
11876
|
env: process.env
|
|
10877
11877
|
});
|
|
10878
11878
|
if (provenance.anomaly) {
|
|
@@ -10899,6 +11899,7 @@ async function run(options) {
|
|
|
10899
11899
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
10900
11900
|
}
|
|
10901
11901
|
}
|
|
11902
|
+
await reloadProviderCache(state.port);
|
|
10902
11903
|
const noProviderWarning = buildNoProviderWarning(
|
|
10903
11904
|
await hasAnyConfiguredProvider(state.port)
|
|
10904
11905
|
);
|
|
@@ -10907,10 +11908,10 @@ async function run(options) {
|
|
|
10907
11908
|
if (state.interactive && !state.json) {
|
|
10908
11909
|
logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
|
|
10909
11910
|
blank();
|
|
10910
|
-
console.log(
|
|
11911
|
+
console.log(chalk7.yellow("\u26A0 No OpenCode model provider is configured."));
|
|
10911
11912
|
console.log(
|
|
10912
|
-
|
|
10913
|
-
`Run ${
|
|
11913
|
+
chalk7.dim(
|
|
11914
|
+
`Run ${chalk7.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
|
|
10914
11915
|
)
|
|
10915
11916
|
);
|
|
10916
11917
|
blank();
|
|
@@ -11034,7 +12035,7 @@ async function run(options) {
|
|
|
11034
12035
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
11035
12036
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
11036
12037
|
fileSyncDirectories,
|
|
11037
|
-
homeDir:
|
|
12038
|
+
homeDir: homedir6(),
|
|
11038
12039
|
maxActiveSessions,
|
|
11039
12040
|
log: (entry) => (
|
|
11040
12041
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -11276,6 +12277,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
11276
12277
|
).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(
|
|
11277
12278
|
"--opencode-start-timeout <seconds>",
|
|
11278
12279
|
"Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
|
|
12280
|
+
).option(
|
|
12281
|
+
"--opencode-version <v1|v2>",
|
|
12282
|
+
"Which OpenCode major version to launch: v1 or v2 (default: v1). Env: EVIDENT_OPENCODE_VERSION"
|
|
11279
12283
|
).option("--json", "Output in JSON format").option(
|
|
11280
12284
|
"--session-cleanup-max-age <duration>",
|
|
11281
12285
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -11344,6 +12348,7 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
11344
12348
|
// Raw string — validation/precedence is single-sourced in run.ts's
|
|
11345
12349
|
// resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).
|
|
11346
12350
|
opencodeStartTimeout: options.opencodeStartTimeout,
|
|
12351
|
+
opencodeVersion: options.opencodeVersion,
|
|
11347
12352
|
json: options.json,
|
|
11348
12353
|
// Raw strings — the resolver in run.ts single-sources parsing (M1).
|
|
11349
12354
|
sessionCleanupMaxAge: options.sessionCleanupMaxAge,
|