@evident-ai/cli 3.4.1-dev.5eccf39 → 3.4.1-dev.69d24ff
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/index.js +1528 -369
- 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(() => "");
|
|
@@ -670,12 +683,15 @@ async function resolveAgentIdFromKey(authHeader) {
|
|
|
670
683
|
}
|
|
671
684
|
}
|
|
672
685
|
var BEST_EFFORT_NOTIFY_TIMEOUT_MS = 2e3;
|
|
673
|
-
async function
|
|
674
|
-
const apiUrl = getApiUrlConfig();
|
|
686
|
+
async function postBestEffort(path, authHeader, body) {
|
|
675
687
|
try {
|
|
676
|
-
const
|
|
688
|
+
const apiUrl = getApiUrlConfig();
|
|
689
|
+
const headers = { Authorization: authHeader };
|
|
690
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
691
|
+
const response = await fetch(`${apiUrl}${path}`, {
|
|
677
692
|
method: "POST",
|
|
678
|
-
headers
|
|
693
|
+
headers,
|
|
694
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
679
695
|
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
680
696
|
});
|
|
681
697
|
if (!response.ok) {
|
|
@@ -687,73 +703,32 @@ async function notifyAgentDisconnected(agentId, authHeader) {
|
|
|
687
703
|
}
|
|
688
704
|
return { ok: true };
|
|
689
705
|
} catch (error2) {
|
|
690
|
-
return { ok: false, error:
|
|
706
|
+
return { ok: false, error: describeTimeoutError(error2, BEST_EFFORT_NOTIFY_TIMEOUT_MS) };
|
|
691
707
|
}
|
|
692
708
|
}
|
|
693
|
-
function
|
|
709
|
+
function describeTimeoutError(error2, timeoutMs) {
|
|
694
710
|
const name = error2?.name;
|
|
695
711
|
if (name === "TimeoutError" || name === "AbortError") {
|
|
696
|
-
return `timed out after ${
|
|
712
|
+
return `timed out after ${timeoutMs}ms`;
|
|
697
713
|
}
|
|
698
714
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
699
715
|
}
|
|
716
|
+
async function notifyAgentDisconnected(agentId, authHeader) {
|
|
717
|
+
return postBestEffort(`/runners/${agentId}/disconnect`, authHeader);
|
|
718
|
+
}
|
|
700
719
|
async function reportMicrovmId(agentId, authHeader, microvmId) {
|
|
701
|
-
|
|
702
|
-
const apiUrl = getApiUrlConfig();
|
|
703
|
-
const response = await fetch(`${apiUrl}/runners/${agentId}/microvm`, {
|
|
704
|
-
method: "POST",
|
|
705
|
-
headers: { Authorization: authHeader, "Content-Type": "application/json" },
|
|
706
|
-
body: JSON.stringify({ microvm_id: microvmId }),
|
|
707
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
708
|
-
});
|
|
709
|
-
if (!response.ok) {
|
|
710
|
-
const serverMessage = await readErrorMessage(response);
|
|
711
|
-
return {
|
|
712
|
-
ok: false,
|
|
713
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
return { ok: true };
|
|
717
|
-
} catch (error2) {
|
|
718
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
719
|
-
}
|
|
720
|
+
return postBestEffort(`/runners/${agentId}/microvm`, authHeader, { microvm_id: microvmId });
|
|
720
721
|
}
|
|
721
722
|
function toReportedWindow(window) {
|
|
722
723
|
if (!window) return null;
|
|
723
724
|
return { utilization: window.utilization, resets_at: window.resetsAt };
|
|
724
725
|
}
|
|
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
726
|
async function reportClaudeUsage(agentId, authHeader, snapshot) {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
body: JSON.stringify({
|
|
740
|
-
five_hour: toReportedWindow(snapshot.fiveHour),
|
|
741
|
-
seven_day: toReportedWindow(snapshot.sevenDay),
|
|
742
|
-
owner: toReportedOwner(snapshot)
|
|
743
|
-
}),
|
|
744
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
745
|
-
});
|
|
746
|
-
if (!response.ok) {
|
|
747
|
-
const serverMessage = await readErrorMessage(response);
|
|
748
|
-
return {
|
|
749
|
-
ok: false,
|
|
750
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
return { ok: true };
|
|
754
|
-
} catch (error2) {
|
|
755
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
756
|
-
}
|
|
727
|
+
return postBestEffort(`/runners/${agentId}/claude-usage`, authHeader, {
|
|
728
|
+
five_hour: toReportedWindow(snapshot.fiveHour),
|
|
729
|
+
seven_day: toReportedWindow(snapshot.sevenDay),
|
|
730
|
+
subscription: toReportedSubscription(snapshot.subscription)
|
|
731
|
+
});
|
|
757
732
|
}
|
|
758
733
|
function toReportedOpenAiWindow(window) {
|
|
759
734
|
if (!window) return null;
|
|
@@ -763,69 +738,26 @@ function toReportedOpenAiWindow(window) {
|
|
|
763
738
|
resets_at: window.resetsAt
|
|
764
739
|
};
|
|
765
740
|
}
|
|
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
741
|
async function reportOpenAiUsage(agentId, authHeader, snapshot) {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
782
|
-
has_credits: snapshot.hasCredits,
|
|
783
|
-
credits_unlimited: snapshot.creditsUnlimited,
|
|
784
|
-
subscription: toReportedOpenAiSubscription(snapshot)
|
|
785
|
-
}),
|
|
786
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
787
|
-
});
|
|
788
|
-
if (!response.ok) {
|
|
789
|
-
const serverMessage = await readErrorMessage(response);
|
|
790
|
-
return {
|
|
791
|
-
ok: false,
|
|
792
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
return { ok: true };
|
|
796
|
-
} catch (error2) {
|
|
797
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
798
|
-
}
|
|
742
|
+
return postBestEffort(`/runners/${agentId}/openai-usage`, authHeader, {
|
|
743
|
+
primary: toReportedOpenAiWindow(snapshot.primary),
|
|
744
|
+
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
745
|
+
has_credits: snapshot.hasCredits,
|
|
746
|
+
credits_unlimited: snapshot.creditsUnlimited,
|
|
747
|
+
subscription: toReportedSubscription(snapshot.subscription)
|
|
748
|
+
});
|
|
799
749
|
}
|
|
800
750
|
async function reportResourceUsage(agentId, authHeader, usage) {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
memory_available_bytes: usage.memoryAvailableBytes,
|
|
812
|
-
disk_total_bytes: usage.diskTotalBytes,
|
|
813
|
-
disk_free_bytes: usage.diskFreeBytes,
|
|
814
|
-
opencode_db_bytes: usage.opencodeDbBytes
|
|
815
|
-
}),
|
|
816
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
817
|
-
});
|
|
818
|
-
if (!response.ok) {
|
|
819
|
-
const serverMessage = await readErrorMessage(response);
|
|
820
|
-
return {
|
|
821
|
-
ok: false,
|
|
822
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
return { ok: true };
|
|
826
|
-
} catch (error2) {
|
|
827
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
828
|
-
}
|
|
751
|
+
return postBestEffort(`/runners/${agentId}/resource-usage`, authHeader, {
|
|
752
|
+
cpu_percent: usage.cpuPercent,
|
|
753
|
+
cpu_peak_percent: usage.cpuPeakPercent,
|
|
754
|
+
cpu_count: usage.cpuCount,
|
|
755
|
+
memory_total_bytes: usage.memoryTotalBytes,
|
|
756
|
+
memory_available_bytes: usage.memoryAvailableBytes,
|
|
757
|
+
disk_total_bytes: usage.diskTotalBytes,
|
|
758
|
+
disk_free_bytes: usage.diskFreeBytes,
|
|
759
|
+
opencode_db_bytes: usage.opencodeDbBytes
|
|
760
|
+
});
|
|
829
761
|
}
|
|
830
762
|
async function getAgentInfo(agentId, authHeader) {
|
|
831
763
|
const apiUrl = getApiUrlConfig();
|
|
@@ -877,13 +809,6 @@ function authLabelFor(credentials2) {
|
|
|
877
809
|
}
|
|
878
810
|
return "user token";
|
|
879
811
|
}
|
|
880
|
-
function describeFetchError(error2) {
|
|
881
|
-
const name = error2?.name;
|
|
882
|
-
if (name === "TimeoutError" || name === "AbortError") {
|
|
883
|
-
return `timed out after ${STATUS_TIMEOUT_MS}ms waiting for a response`;
|
|
884
|
-
}
|
|
885
|
-
return error2 instanceof Error ? error2.message : String(error2);
|
|
886
|
-
}
|
|
887
812
|
async function checkStatus(jsonMode) {
|
|
888
813
|
const apiUrl = getApiUrlConfig();
|
|
889
814
|
const credentials2 = await getAuthCredentials();
|
|
@@ -911,7 +836,7 @@ async function checkStatus(jsonMode) {
|
|
|
911
836
|
endpoint: apiUrl,
|
|
912
837
|
authLabel: authLabelFor(credentials2),
|
|
913
838
|
reason: "unreachable",
|
|
914
|
-
error: `Could not reach ${apiUrl}: ${
|
|
839
|
+
error: `Could not reach ${apiUrl}: ${describeTimeoutError(error2, STATUS_TIMEOUT_MS)}. The credentials were NOT validated.`,
|
|
915
840
|
exitCode: 75
|
|
916
841
|
};
|
|
917
842
|
}
|
|
@@ -1009,10 +934,10 @@ async function status(options = {}) {
|
|
|
1009
934
|
}
|
|
1010
935
|
|
|
1011
936
|
// 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";
|
|
937
|
+
import { execFileSync } from "node:child_process";
|
|
938
|
+
import { readFileSync } from "node:fs";
|
|
939
|
+
import { homedir } from "node:os";
|
|
940
|
+
import { join } from "node:path";
|
|
1016
941
|
var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
1017
942
|
var CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
1018
943
|
var CLAUDE_PROFILE_TIMEOUT_MS = 2e3;
|
|
@@ -1097,7 +1022,7 @@ function ownerLookupFailure(error2) {
|
|
|
1097
1022
|
}
|
|
1098
1023
|
async function getClaudeUsageOwner(accessToken) {
|
|
1099
1024
|
if (cachedOwner?.accessToken === accessToken) {
|
|
1100
|
-
return {
|
|
1025
|
+
return { subscription: cachedOwner.owner, ownerLookupError: null };
|
|
1101
1026
|
}
|
|
1102
1027
|
try {
|
|
1103
1028
|
const response = await fetch(CLAUDE_PROFILE_URL, {
|
|
@@ -1109,27 +1034,27 @@ async function getClaudeUsageOwner(accessToken) {
|
|
|
1109
1034
|
signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
|
|
1110
1035
|
});
|
|
1111
1036
|
if (!response.ok) {
|
|
1112
|
-
return {
|
|
1037
|
+
return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
|
|
1113
1038
|
}
|
|
1114
1039
|
let body;
|
|
1115
1040
|
try {
|
|
1116
1041
|
body = await response.json();
|
|
1117
1042
|
} catch (error2) {
|
|
1118
|
-
return {
|
|
1043
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1119
1044
|
}
|
|
1120
1045
|
const profile = body;
|
|
1121
1046
|
if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
|
|
1122
|
-
return {
|
|
1047
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1123
1048
|
}
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1049
|
+
const subscription = {
|
|
1050
|
+
ownerEmail: profile.account.email,
|
|
1126
1051
|
organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
|
|
1127
|
-
|
|
1052
|
+
planType: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
|
|
1128
1053
|
};
|
|
1129
|
-
cachedOwner = { accessToken, owner };
|
|
1130
|
-
return {
|
|
1054
|
+
cachedOwner = { accessToken, owner: subscription };
|
|
1055
|
+
return { subscription, ownerLookupError: null };
|
|
1131
1056
|
} catch (error2) {
|
|
1132
|
-
return {
|
|
1057
|
+
return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
|
|
1133
1058
|
}
|
|
1134
1059
|
}
|
|
1135
1060
|
async function getClaudeUsage() {
|
|
@@ -1158,11 +1083,11 @@ async function getClaudeUsage() {
|
|
|
1158
1083
|
throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
|
|
1159
1084
|
}
|
|
1160
1085
|
const body = await res.json();
|
|
1161
|
-
const {
|
|
1086
|
+
const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
|
|
1162
1087
|
return {
|
|
1163
1088
|
fiveHour: toWindow(body.five_hour),
|
|
1164
1089
|
sevenDay: toWindow(body.seven_day),
|
|
1165
|
-
|
|
1090
|
+
subscription,
|
|
1166
1091
|
ownerLookupError
|
|
1167
1092
|
};
|
|
1168
1093
|
}
|
|
@@ -1192,10 +1117,10 @@ async function claudeUsage() {
|
|
|
1192
1117
|
}
|
|
1193
1118
|
|
|
1194
1119
|
// 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
|
|
1120
|
+
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1121
|
+
import { homedir as homedir6 } from "node:os";
|
|
1122
|
+
import { isAbsolute as isAbsolute3, join as join10, parse, resolve as resolvePath2 } from "node:path";
|
|
1123
|
+
import chalk7 from "chalk";
|
|
1199
1124
|
|
|
1200
1125
|
// ../../packages/types/src/agents/index.ts
|
|
1201
1126
|
var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
|
|
@@ -1216,6 +1141,7 @@ var TelemetryEventTypes = {
|
|
|
1216
1141
|
// ../../packages/types/src/tunnel/index.ts
|
|
1217
1142
|
var MAX_FRAME_BYTES = 256 * 1024;
|
|
1218
1143
|
var TUNNEL_DRAIN_PING_PATH = "/__evident/drain";
|
|
1144
|
+
var TUNNEL_USAGE_REARM_PING_PATH = "/__evident/usage-rearm";
|
|
1219
1145
|
|
|
1220
1146
|
// ../../packages/types/src/runner-files.ts
|
|
1221
1147
|
var MAX_FILE_PUSH_BYTES = 64 * 1024;
|
|
@@ -1253,10 +1179,10 @@ function stripQuery(url) {
|
|
|
1253
1179
|
|
|
1254
1180
|
// src/commands/run.ts
|
|
1255
1181
|
import ora3 from "ora";
|
|
1256
|
-
import { select as
|
|
1182
|
+
import { select as select4 } from "@inquirer/prompts";
|
|
1257
1183
|
|
|
1258
1184
|
// src/lib/telemetry.ts
|
|
1259
|
-
var CLI_VERSION = (true ? "3.
|
|
1185
|
+
var CLI_VERSION = (true ? "3.4.1-dev.69d24ff" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1260
1186
|
function getCliVersion() {
|
|
1261
1187
|
return CLI_VERSION;
|
|
1262
1188
|
}
|
|
@@ -1426,12 +1352,50 @@ var SEVERITY_BY_LEVEL = {
|
|
|
1426
1352
|
warn: "warning",
|
|
1427
1353
|
error: "error"
|
|
1428
1354
|
};
|
|
1355
|
+
function parseOpenCodeLogLine(line) {
|
|
1356
|
+
const normalisedLine = line.replace(/\r$/, "");
|
|
1357
|
+
const levelMatch = normalisedLine.match(/(?:^|\s)level=(\w+)/i);
|
|
1358
|
+
if (!levelMatch) return null;
|
|
1359
|
+
const level = levelMatch[1].toUpperCase();
|
|
1360
|
+
if (level !== "WARN" && level !== "ERROR") return null;
|
|
1361
|
+
const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
|
|
1362
|
+
return { level: level === "WARN" ? "warn" : "error", sessionID: sessionMatch?.[1] };
|
|
1363
|
+
}
|
|
1364
|
+
var MAX_LINE_BUFFER_BYTES = 16 * 1024;
|
|
1365
|
+
function createOpenCodeActivityForwarder(getContext) {
|
|
1366
|
+
let buffer = Buffer.alloc(0);
|
|
1367
|
+
const flushLine = (line) => {
|
|
1368
|
+
const parsed = parseOpenCodeLogLine(line);
|
|
1369
|
+
if (!parsed) return;
|
|
1370
|
+
forwardRunnerActivity(
|
|
1371
|
+
{
|
|
1372
|
+
level: parsed.level,
|
|
1373
|
+
error: line,
|
|
1374
|
+
metadata: parsed.sessionID ? { sessionID: parsed.sessionID } : void 0,
|
|
1375
|
+
source: "opencode"
|
|
1376
|
+
},
|
|
1377
|
+
getContext()
|
|
1378
|
+
);
|
|
1379
|
+
};
|
|
1380
|
+
return (chunk) => {
|
|
1381
|
+
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
|
|
1382
|
+
let newlineIndex;
|
|
1383
|
+
while ((newlineIndex = buffer.indexOf(10)) !== -1) {
|
|
1384
|
+
flushLine(buffer.subarray(0, newlineIndex).toString("utf-8").replace(/\r$/, ""));
|
|
1385
|
+
buffer = buffer.subarray(newlineIndex + 1);
|
|
1386
|
+
}
|
|
1387
|
+
if (buffer.length > MAX_LINE_BUFFER_BYTES) {
|
|
1388
|
+
flushLine(buffer.toString("utf-8"));
|
|
1389
|
+
buffer = Buffer.alloc(0);
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1429
1393
|
var MAX_MESSAGE_LENGTH = 500;
|
|
1430
1394
|
var MAX_METADATA_VALUE_LENGTH = 200;
|
|
1431
1395
|
var MAX_METADATA_ENTRIES = 20;
|
|
1432
1396
|
var TRUNCATION_MARKER = "\u2026";
|
|
1433
1397
|
function redact(message) {
|
|
1434
|
-
return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/https?:\/\/\S+/g, "<url>");
|
|
1398
|
+
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>");
|
|
1435
1399
|
}
|
|
1436
1400
|
function truncate(message) {
|
|
1437
1401
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
@@ -1457,43 +1421,47 @@ function sanitiseMetadata(metadata) {
|
|
|
1457
1421
|
}
|
|
1458
1422
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1459
1423
|
var RATE_LIMIT_MAX_EVENTS = 30;
|
|
1460
|
-
var
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1424
|
+
var rateWindows = /* @__PURE__ */ new Map();
|
|
1425
|
+
function admitUnderRateLimit(source, now) {
|
|
1426
|
+
let window = rateWindows.get(source);
|
|
1427
|
+
if (!window) {
|
|
1428
|
+
window = { windowStartedAt: 0, windowCount: 0, windowDroppedCount: 0 };
|
|
1429
|
+
rateWindows.set(source, window);
|
|
1430
|
+
}
|
|
1431
|
+
if (now - window.windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
|
|
1432
|
+
if (window.windowDroppedCount > 0) {
|
|
1466
1433
|
console.error(
|
|
1467
|
-
`[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)`
|
|
1434
|
+
`[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}"`
|
|
1468
1435
|
);
|
|
1469
1436
|
}
|
|
1470
|
-
windowStartedAt = now;
|
|
1471
|
-
windowCount = 0;
|
|
1472
|
-
windowDroppedCount = 0;
|
|
1437
|
+
window.windowStartedAt = now;
|
|
1438
|
+
window.windowCount = 0;
|
|
1439
|
+
window.windowDroppedCount = 0;
|
|
1473
1440
|
}
|
|
1474
|
-
if (windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1475
|
-
windowDroppedCount++;
|
|
1476
|
-
if (windowDroppedCount === 1) {
|
|
1441
|
+
if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1442
|
+
window.windowDroppedCount++;
|
|
1443
|
+
if (window.windowDroppedCount === 1) {
|
|
1477
1444
|
console.error(
|
|
1478
|
-
`[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ${RATE_LIMIT_WINDOW_MS / 1e3}s) \u2014 dropping further entries this window`
|
|
1445
|
+
`[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}"`
|
|
1479
1446
|
);
|
|
1480
1447
|
}
|
|
1481
1448
|
return false;
|
|
1482
1449
|
}
|
|
1483
|
-
windowCount++;
|
|
1450
|
+
window.windowCount++;
|
|
1484
1451
|
return true;
|
|
1485
1452
|
}
|
|
1486
1453
|
function forwardRunnerActivity(entry, context) {
|
|
1487
1454
|
try {
|
|
1488
1455
|
if (!FORWARDED_LEVELS.has(entry.level)) return;
|
|
1489
1456
|
if (!context.agentId || !context.authHeader) return;
|
|
1490
|
-
|
|
1457
|
+
const source = entry.source ?? "cli.run";
|
|
1458
|
+
if (!admitUnderRateLimit(source, Date.now())) return;
|
|
1491
1459
|
const rawMessage = entry.error ?? entry.message ?? "";
|
|
1492
1460
|
const message = truncate(redact(rawMessage));
|
|
1493
1461
|
logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
|
|
1494
1462
|
severity: SEVERITY_BY_LEVEL[entry.level],
|
|
1495
1463
|
message,
|
|
1496
|
-
metadata: { ...sanitiseMetadata(entry.metadata), source
|
|
1464
|
+
metadata: { ...sanitiseMetadata(entry.metadata), source },
|
|
1497
1465
|
agentId: context.agentId
|
|
1498
1466
|
});
|
|
1499
1467
|
} catch (err) {
|
|
@@ -1504,8 +1472,8 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1504
1472
|
}
|
|
1505
1473
|
|
|
1506
1474
|
// src/lib/opencode/session-db-recovery-report.ts
|
|
1507
|
-
import { readFileSync as readFileSync2, unlinkSync } from "fs";
|
|
1508
|
-
import { join as join2 } from "path";
|
|
1475
|
+
import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
|
|
1476
|
+
import { join as join2 } from "node:path";
|
|
1509
1477
|
function sessionDbRecoveryReportPath(homeDir, env) {
|
|
1510
1478
|
const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
|
|
1511
1479
|
return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
|
|
@@ -1718,13 +1686,13 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
1718
1686
|
}
|
|
1719
1687
|
|
|
1720
1688
|
// src/lib/opencode/session-db-boot.ts
|
|
1721
|
-
import { spawn as spawn2 } from "child_process";
|
|
1722
|
-
import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "fs";
|
|
1723
|
-
import { homedir as homedir2 } from "os";
|
|
1724
|
-
import { dirname as dirname2, resolve as resolvePath } from "path";
|
|
1689
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1690
|
+
import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
1691
|
+
import { homedir as homedir2 } from "node:os";
|
|
1692
|
+
import { dirname as dirname2, resolve as resolvePath } from "node:path";
|
|
1725
1693
|
|
|
1726
1694
|
// src/lib/runner-synchroniser.ts
|
|
1727
|
-
import { spawn } from "child_process";
|
|
1695
|
+
import { spawn } from "node:child_process";
|
|
1728
1696
|
function appendError(stderr, error2) {
|
|
1729
1697
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1730
1698
|
return stderr === "" ? message : `${stderr}
|
|
@@ -2249,9 +2217,9 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
2249
2217
|
}
|
|
2250
2218
|
|
|
2251
2219
|
// src/lib/opencode/session-db-provenance.ts
|
|
2252
|
-
import { createRequire } from "module";
|
|
2253
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2254
|
-
import { dirname as dirname3, join as join3 } from "path";
|
|
2220
|
+
import { createRequire } from "node:module";
|
|
2221
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2222
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
2255
2223
|
var require2 = createRequire(import.meta.url);
|
|
2256
2224
|
function readSessionDbMigrationIds(dbPath) {
|
|
2257
2225
|
let db;
|
|
@@ -2379,11 +2347,18 @@ function isQueueValidatedVersion(version2) {
|
|
|
2379
2347
|
if (!version2) return false;
|
|
2380
2348
|
return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version2);
|
|
2381
2349
|
}
|
|
2382
|
-
function buildOpenCodeVersionWarning(version2) {
|
|
2383
|
-
if (
|
|
2384
|
-
const detected = version2 ? `v${version2}` : "unknown";
|
|
2350
|
+
function buildOpenCodeVersionWarning(version2, major) {
|
|
2351
|
+
if (major === "v2") return null;
|
|
2385
2352
|
const validated = QUEUE_VALIDATED_OPENCODE_VERSIONS.map((v) => `v${v}`).join(", ");
|
|
2386
|
-
|
|
2353
|
+
if (!version2) {
|
|
2354
|
+
return `Warning: the running opencode's version could not be determined from its health response, so queue validation could not be checked (validated: ${validated}). Compare against \`opencode --version\`; continuing anyway.`;
|
|
2355
|
+
}
|
|
2356
|
+
if (isQueueValidatedVersion(version2)) return null;
|
|
2357
|
+
return `Warning: opencode v${version2} is not a queue-validated version (validated: ${validated}). Native message queuing \u2014 which channel (Slack) message handling relies on \u2014 is unverified on this version; queued/follow-up messages may behave unexpectedly. Continuing anyway. Bumping the validated set requires re-running the queue validation.`;
|
|
2358
|
+
}
|
|
2359
|
+
function reportedOpenCodeVersion(input) {
|
|
2360
|
+
if (!input.connected) return null;
|
|
2361
|
+
return input.version || `${input.major}-unknown`;
|
|
2387
2362
|
}
|
|
2388
2363
|
|
|
2389
2364
|
// src/lib/opencode/process.ts
|
|
@@ -2445,6 +2420,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
2445
2420
|
|
|
2446
2421
|
// src/lib/opencode/process.ts
|
|
2447
2422
|
var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
|
|
2423
|
+
var VALID_OPENCODE_LOG_LEVELS = /* @__PURE__ */ new Set(["DEBUG", "INFO", "WARN", "ERROR"]);
|
|
2424
|
+
function resolveOpenCodeLogLevel(env) {
|
|
2425
|
+
const raw = env.OPENCODE_LOG_LEVEL;
|
|
2426
|
+
if (!raw) return "INFO";
|
|
2427
|
+
const upper = raw.toUpperCase();
|
|
2428
|
+
if (VALID_OPENCODE_LOG_LEVELS.has(upper)) return upper;
|
|
2429
|
+
console.warn(
|
|
2430
|
+
`startOpenCode: ignoring invalid OPENCODE_LOG_LEVEL "${raw}" (expected DEBUG|INFO|WARN|ERROR) \u2014 using INFO`
|
|
2431
|
+
);
|
|
2432
|
+
return "INFO";
|
|
2433
|
+
}
|
|
2448
2434
|
function getProcessCwd(pid) {
|
|
2449
2435
|
const platform = process.platform;
|
|
2450
2436
|
try {
|
|
@@ -2493,14 +2479,14 @@ function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
2493
2479
|
}
|
|
2494
2480
|
return null;
|
|
2495
2481
|
}
|
|
2496
|
-
function
|
|
2482
|
+
function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
2497
2483
|
const instances = [];
|
|
2498
2484
|
try {
|
|
2499
2485
|
const platform = process.platform;
|
|
2500
2486
|
if (platform === "darwin" || platform === "linux") {
|
|
2501
2487
|
let pids = [];
|
|
2502
2488
|
try {
|
|
2503
|
-
const pgrepOutput = execSync(
|
|
2489
|
+
const pgrepOutput = execSync(`pgrep -f "${pgrepPattern}"`, {
|
|
2504
2490
|
encoding: "utf-8",
|
|
2505
2491
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2506
2492
|
}).trim();
|
|
@@ -2509,7 +2495,7 @@ function findOpenCodeProcesses() {
|
|
|
2509
2495
|
}
|
|
2510
2496
|
} catch {
|
|
2511
2497
|
try {
|
|
2512
|
-
const psOutput = execSync(
|
|
2498
|
+
const psOutput = execSync(`ps aux | grep -E "${psPattern}" | grep -v grep`, {
|
|
2513
2499
|
encoding: "utf-8",
|
|
2514
2500
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2515
2501
|
}).trim();
|
|
@@ -2555,6 +2541,9 @@ function findOpenCodeProcesses() {
|
|
|
2555
2541
|
}
|
|
2556
2542
|
return instances;
|
|
2557
2543
|
}
|
|
2544
|
+
function findOpenCodeProcesses() {
|
|
2545
|
+
return findProcessesByPattern("opencode serve|opencode-serve", "opencode (serve|--port)");
|
|
2546
|
+
}
|
|
2558
2547
|
async function scanPortsForOpenCode() {
|
|
2559
2548
|
const instances = [];
|
|
2560
2549
|
const checks = OPENCODE_PORT_RANGE.map(async (port) => {
|
|
@@ -2601,7 +2590,7 @@ async function findHealthyOpenCodeInstances() {
|
|
|
2601
2590
|
}
|
|
2602
2591
|
async function startOpenCode(port, options = {}) {
|
|
2603
2592
|
let command = "opencode";
|
|
2604
|
-
const printLogs = options.inheritStdio ? ["--print-logs"] : [];
|
|
2593
|
+
const printLogs = options.inheritStdio ? ["--print-logs", "--log-level", resolveOpenCodeLogLevel(process.env)] : [];
|
|
2605
2594
|
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...printLogs];
|
|
2606
2595
|
try {
|
|
2607
2596
|
execSync("which opencode", { stdio: "ignore" });
|
|
@@ -2658,6 +2647,19 @@ function isOpenCodeInstalled() {
|
|
|
2658
2647
|
return false;
|
|
2659
2648
|
}
|
|
2660
2649
|
}
|
|
2650
|
+
function isOpenCode2Installed() {
|
|
2651
|
+
try {
|
|
2652
|
+
const platform = process.platform;
|
|
2653
|
+
if (platform === "win32") {
|
|
2654
|
+
execSync2("where opencode2", { stdio: "ignore" });
|
|
2655
|
+
} else {
|
|
2656
|
+
execSync2("which opencode2", { stdio: "ignore" });
|
|
2657
|
+
}
|
|
2658
|
+
return true;
|
|
2659
|
+
} catch {
|
|
2660
|
+
return false;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2661
2663
|
async function promptOpenCodeInstall(interactive) {
|
|
2662
2664
|
if (!interactive) {
|
|
2663
2665
|
console.log(
|
|
@@ -2667,7 +2669,11 @@ async function promptOpenCodeInstall(interactive) {
|
|
|
2667
2669
|
install_url: OPENCODE_INSTALL_URL,
|
|
2668
2670
|
install_commands: {
|
|
2669
2671
|
npm: "npm install -g opencode-ai",
|
|
2670
|
-
curl: "curl -fsSL https://opencode.ai/install.sh | sh"
|
|
2672
|
+
curl: "curl -fsSL https://opencode.ai/install.sh | sh",
|
|
2673
|
+
v2: {
|
|
2674
|
+
npm: "npm install -g @opencode-ai/cli@beta",
|
|
2675
|
+
curl: "curl -fsSL https://opencode.ai/v2/install | bash"
|
|
2676
|
+
}
|
|
2671
2677
|
}
|
|
2672
2678
|
})
|
|
2673
2679
|
);
|
|
@@ -3151,21 +3157,112 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
3151
3157
|
}
|
|
3152
3158
|
return lastOk ?? last;
|
|
3153
3159
|
}
|
|
3154
|
-
function
|
|
3155
|
-
if (!messages || messages.length === 0) return
|
|
3156
|
-
const
|
|
3157
|
-
(
|
|
3160
|
+
function collectSubagentSessions(messages, userMessageId) {
|
|
3161
|
+
if (!messages || messages.length === 0) return [];
|
|
3162
|
+
const byParent = messages.filter(
|
|
3163
|
+
(message) => roleOf(message) === "assistant" && parentIdOf(message) === userMessageId
|
|
3158
3164
|
);
|
|
3159
|
-
const
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3165
|
+
const assistants = byParent.length > 0 ? byParent : [];
|
|
3166
|
+
if (assistants.length === 0) {
|
|
3167
|
+
const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
|
|
3168
|
+
if (userIndex === -1) return [];
|
|
3169
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
3170
|
+
const message = messages[i];
|
|
3171
|
+
if (roleOf(message) === "user") break;
|
|
3172
|
+
if (roleOf(message) === "assistant") assistants.push(message);
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
const refs = [];
|
|
3176
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3177
|
+
for (const message of assistants) {
|
|
3178
|
+
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
3179
|
+
for (const part of parts) {
|
|
3180
|
+
if (!part || typeof part !== "object" || part.type !== "tool" || part.tool !== "task")
|
|
3181
|
+
continue;
|
|
3182
|
+
const state = part.state;
|
|
3183
|
+
if (!state || typeof state !== "object") continue;
|
|
3184
|
+
const metadata = state.metadata;
|
|
3185
|
+
if (!metadata || typeof metadata !== "object") continue;
|
|
3186
|
+
const sessionId = metadata.sessionId;
|
|
3187
|
+
if (typeof sessionId !== "string" || sessionId.length === 0 || seen.has(sessionId)) continue;
|
|
3188
|
+
seen.add(sessionId);
|
|
3189
|
+
const start = state.time?.start;
|
|
3190
|
+
refs.push({
|
|
3191
|
+
sessionId,
|
|
3192
|
+
startedAtMs: typeof start === "number" && Number.isFinite(start) ? start : null
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3167
3195
|
}
|
|
3168
|
-
|
|
3196
|
+
return refs;
|
|
3197
|
+
}
|
|
3198
|
+
function finiteNumber(value) {
|
|
3199
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3200
|
+
}
|
|
3201
|
+
function taskCallModel(value) {
|
|
3202
|
+
if (!value || typeof value !== "object") return null;
|
|
3203
|
+
const model = value;
|
|
3204
|
+
const modelID = typeof model.modelID === "string" ? model.modelID : void 0;
|
|
3205
|
+
const providerID = typeof model.providerID === "string" ? model.providerID : void 0;
|
|
3206
|
+
return modelID || providerID ? { modelID, providerID } : null;
|
|
3207
|
+
}
|
|
3208
|
+
function collectTaskCalls(messages, userMessageId) {
|
|
3209
|
+
if (!messages || messages.length === 0) return [];
|
|
3210
|
+
const calls = [];
|
|
3211
|
+
for (const message of messages) {
|
|
3212
|
+
if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
|
|
3213
|
+
for (const part of message.parts ?? []) {
|
|
3214
|
+
if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
|
|
3215
|
+
continue;
|
|
3216
|
+
}
|
|
3217
|
+
const rawName = part.state.input?.subagent_type;
|
|
3218
|
+
const subagentName = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : rawName === void 0 ? "general" : "unknown";
|
|
3219
|
+
const metadata = part.state.metadata;
|
|
3220
|
+
calls.push({
|
|
3221
|
+
callID: part.callID,
|
|
3222
|
+
subagentName,
|
|
3223
|
+
childSessionId: typeof metadata?.sessionId === "string" ? metadata.sessionId : null,
|
|
3224
|
+
parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
|
|
3225
|
+
model: taskCallModel(metadata?.model),
|
|
3226
|
+
status: part.state.status ?? "unknown",
|
|
3227
|
+
timeStart: finiteNumber(part.state.time?.start),
|
|
3228
|
+
timeEnd: finiteNumber(part.state.time?.end)
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
return calls;
|
|
3233
|
+
}
|
|
3234
|
+
function attributeTaskCallUsage(messages, windows) {
|
|
3235
|
+
const eligibleWindows = windows.filter(
|
|
3236
|
+
(window) => window.timeStart !== null && Number.isFinite(window.timeStart)
|
|
3237
|
+
);
|
|
3238
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
3239
|
+
for (const window of eligibleWindows) assignments.set(window.callID, []);
|
|
3240
|
+
const unattributed = [];
|
|
3241
|
+
for (const message of messages ?? []) {
|
|
3242
|
+
if (roleOf(message) !== "assistant") continue;
|
|
3243
|
+
const created = finiteNumber(createdOf(message));
|
|
3244
|
+
const matching = created === null ? [] : eligibleWindows.filter(
|
|
3245
|
+
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
3246
|
+
);
|
|
3247
|
+
if (matching.length === 0) {
|
|
3248
|
+
unattributed.push(message);
|
|
3249
|
+
continue;
|
|
3250
|
+
}
|
|
3251
|
+
matching.sort((a, b) => a.timeStart - b.timeStart);
|
|
3252
|
+
assignments.get(matching[0].callID)?.push(message);
|
|
3253
|
+
}
|
|
3254
|
+
return {
|
|
3255
|
+
invocations: eligibleWindows.map((window) => {
|
|
3256
|
+
const assigned = assignments.get(window.callID) ?? [];
|
|
3257
|
+
return { callID: window.callID, messages: assigned, usage: sumAssistantUsage(assigned) };
|
|
3258
|
+
}),
|
|
3259
|
+
unattributed
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
function sumAssistantUsage(messages) {
|
|
3263
|
+
if (!messages || messages.length === 0) return null;
|
|
3264
|
+
const nonErrored = messages.filter((message) => errorOf(message) == null);
|
|
3265
|
+
const selected = nonErrored.length > 0 ? nonErrored : messages;
|
|
3169
3266
|
let sawAnyUsage = false;
|
|
3170
3267
|
let inputSum = 0;
|
|
3171
3268
|
let outputSum = 0;
|
|
@@ -3176,7 +3273,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
3176
3273
|
let sawCost = false;
|
|
3177
3274
|
let modelId = null;
|
|
3178
3275
|
let providerId = null;
|
|
3179
|
-
for (const m of
|
|
3276
|
+
for (const m of selected) {
|
|
3180
3277
|
const info = m.info;
|
|
3181
3278
|
if (!info) continue;
|
|
3182
3279
|
const tokens = info.tokens;
|
|
@@ -3211,12 +3308,28 @@ function messageUsage(messages, userMessageId) {
|
|
|
3211
3308
|
usage_tokens_reasoning: reasoningSum,
|
|
3212
3309
|
usage_tokens_cache_read: cacheReadSum,
|
|
3213
3310
|
usage_tokens_cache_write: cacheWriteSum,
|
|
3214
|
-
// NULL means
|
|
3215
|
-
//
|
|
3216
|
-
// `sawCost` true with `costSum === 0`.
|
|
3311
|
+
// NULL means OpenCode never reported a cost; it is distinct from a genuine
|
|
3312
|
+
// zero-cost message, which sets `sawCost` with `costSum === 0`.
|
|
3217
3313
|
usage_cost_usd: sawCost ? costSum : null
|
|
3218
3314
|
};
|
|
3219
3315
|
}
|
|
3316
|
+
function messageUsage(messages, userMessageId) {
|
|
3317
|
+
if (!messages || messages.length === 0) return null;
|
|
3318
|
+
const byParentAll = messages.filter(
|
|
3319
|
+
(m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
|
|
3320
|
+
);
|
|
3321
|
+
const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
|
|
3322
|
+
const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
|
|
3323
|
+
let correlated;
|
|
3324
|
+
if (byParent.length > 0) {
|
|
3325
|
+
correlated = byParent;
|
|
3326
|
+
} else {
|
|
3327
|
+
const reply = findAssistantReplyAfter(messages, userMessageId);
|
|
3328
|
+
correlated = reply ? [reply] : [];
|
|
3329
|
+
}
|
|
3330
|
+
if (correlated.length === 0) return null;
|
|
3331
|
+
return sumAssistantUsage(correlated);
|
|
3332
|
+
}
|
|
3220
3333
|
function messageRunState(messages, userMessageId) {
|
|
3221
3334
|
if (!messages || messages.length === 0) return "unknown";
|
|
3222
3335
|
const hasUser = messages.some((m) => idOf(m) === userMessageId);
|
|
@@ -3279,8 +3392,7 @@ function isAbortedTerminalReply(messages, userMessageId) {
|
|
|
3279
3392
|
}
|
|
3280
3393
|
return false;
|
|
3281
3394
|
}
|
|
3282
|
-
function
|
|
3283
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
3395
|
+
function classifyReplyAuthError(reply) {
|
|
3284
3396
|
const error2 = errorOf(reply);
|
|
3285
3397
|
if (error2 == null || typeof error2 !== "object") return null;
|
|
3286
3398
|
const e = error2;
|
|
@@ -3305,6 +3417,32 @@ function messageFailure(messages, userMessageId) {
|
|
|
3305
3417
|
}
|
|
3306
3418
|
return null;
|
|
3307
3419
|
}
|
|
3420
|
+
function messageFailure(messages, userMessageId) {
|
|
3421
|
+
return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
|
|
3422
|
+
}
|
|
3423
|
+
function findLatestSubagentAuthOutcome(messages, sinceMs) {
|
|
3424
|
+
if (!messages || messages.length === 0) return null;
|
|
3425
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3426
|
+
const message = messages[i];
|
|
3427
|
+
if (roleOf(message) !== "assistant") continue;
|
|
3428
|
+
const created = createdOf(message);
|
|
3429
|
+
if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
|
|
3430
|
+
const failure = classifyReplyAuthError(message);
|
|
3431
|
+
if (failure) {
|
|
3432
|
+
if (!failure.providerId) return null;
|
|
3433
|
+
return { providerId: failure.providerId, outcome: "failed", failure };
|
|
3434
|
+
}
|
|
3435
|
+
const providerId = message.info?.providerID;
|
|
3436
|
+
if (errorOf(message) == null && typeof providerId === "string" && providerId.length > 0) {
|
|
3437
|
+
return { providerId, outcome: "succeeded" };
|
|
3438
|
+
}
|
|
3439
|
+
return null;
|
|
3440
|
+
}
|
|
3441
|
+
return null;
|
|
3442
|
+
}
|
|
3443
|
+
function findSubagentAuthOutcome(messages, sinceMs) {
|
|
3444
|
+
return findLatestSubagentAuthOutcome(messages, sinceMs);
|
|
3445
|
+
}
|
|
3308
3446
|
function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProviderId, replyModelId = null) {
|
|
3309
3447
|
if (classified != null) return classified;
|
|
3310
3448
|
if (hasConfiguredProvider !== false) return null;
|
|
@@ -3321,6 +3459,28 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
|
3321
3459
|
(m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
3322
3460
|
);
|
|
3323
3461
|
}
|
|
3462
|
+
function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
|
|
3463
|
+
if (!messages || messages.length === 0) return false;
|
|
3464
|
+
const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
|
|
3465
|
+
if (userIndex === -1) return false;
|
|
3466
|
+
let hasLaterUser = false;
|
|
3467
|
+
let hasStartedLaterUser = false;
|
|
3468
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
3469
|
+
const message = messages[i];
|
|
3470
|
+
if (roleOf(message) !== "user") continue;
|
|
3471
|
+
hasLaterUser = true;
|
|
3472
|
+
const laterUserMessageId = idOf(message);
|
|
3473
|
+
if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
|
|
3474
|
+
return false;
|
|
3475
|
+
}
|
|
3476
|
+
if (messages.some(
|
|
3477
|
+
(candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
|
|
3478
|
+
)) {
|
|
3479
|
+
hasStartedLaterUser = true;
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
return hasLaterUser && hasStartedLaterUser;
|
|
3483
|
+
}
|
|
3324
3484
|
async function hasAnyConfiguredProvider(port) {
|
|
3325
3485
|
try {
|
|
3326
3486
|
const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
|
|
@@ -3352,6 +3512,94 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3352
3512
|
return null;
|
|
3353
3513
|
}
|
|
3354
3514
|
}
|
|
3515
|
+
function sessionErrorReason(error2) {
|
|
3516
|
+
const record = typeof error2 === "object" && error2 !== null ? error2 : null;
|
|
3517
|
+
const data = record?.data;
|
|
3518
|
+
const dataRecord = typeof data === "object" && data !== null ? data : null;
|
|
3519
|
+
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";
|
|
3520
|
+
const reason = rawReason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
3521
|
+
return reason || "OpenCode reported a session error with no details";
|
|
3522
|
+
}
|
|
3523
|
+
function parseSessionErrorFrame(data) {
|
|
3524
|
+
let parsed;
|
|
3525
|
+
try {
|
|
3526
|
+
parsed = JSON.parse(data);
|
|
3527
|
+
} catch (error2) {
|
|
3528
|
+
void error2;
|
|
3529
|
+
return null;
|
|
3530
|
+
}
|
|
3531
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
3532
|
+
const parsedRecord = parsed;
|
|
3533
|
+
const payload = parsedRecord.payload;
|
|
3534
|
+
const event = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : parsedRecord;
|
|
3535
|
+
if (event.type !== "session.error") return null;
|
|
3536
|
+
const properties = event.properties;
|
|
3537
|
+
if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
|
|
3538
|
+
return null;
|
|
3539
|
+
}
|
|
3540
|
+
const propertiesRecord = properties;
|
|
3541
|
+
const sessionId = propertiesRecord.sessionID;
|
|
3542
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
3543
|
+
return {
|
|
3544
|
+
sessionId,
|
|
3545
|
+
reason: sessionErrorReason(propertiesRecord.error)
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
async function readSessionErrorStream(port, options) {
|
|
3549
|
+
let reader = null;
|
|
3550
|
+
try {
|
|
3551
|
+
const response = await fetch(`${opencodeBase(port)}/event`, {
|
|
3552
|
+
headers: { accept: "text/event-stream" },
|
|
3553
|
+
signal: options.signal
|
|
3554
|
+
});
|
|
3555
|
+
if (!response.ok || !response.body) {
|
|
3556
|
+
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
3557
|
+
}
|
|
3558
|
+
reader = response.body.getReader();
|
|
3559
|
+
const decoder = new TextDecoder();
|
|
3560
|
+
let buffer = "";
|
|
3561
|
+
const processLine = (line) => {
|
|
3562
|
+
const trimmed = line.trimEnd();
|
|
3563
|
+
if (!trimmed.startsWith("data:")) return;
|
|
3564
|
+
const event = parseSessionErrorFrame(trimmed.slice("data:".length).replace(/^ /, ""));
|
|
3565
|
+
if (event) options.onSessionError(event);
|
|
3566
|
+
};
|
|
3567
|
+
while (true) {
|
|
3568
|
+
const { done, value } = await reader.read();
|
|
3569
|
+
if (done) return { reason: "ended" };
|
|
3570
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3571
|
+
const lines = buffer.split("\n");
|
|
3572
|
+
buffer = lines.pop() ?? "";
|
|
3573
|
+
for (const line of lines) processLine(line);
|
|
3574
|
+
}
|
|
3575
|
+
} catch (err) {
|
|
3576
|
+
if (options.signal.aborted) return { reason: "aborted" };
|
|
3577
|
+
return {
|
|
3578
|
+
reason: "unavailable",
|
|
3579
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
3580
|
+
};
|
|
3581
|
+
} finally {
|
|
3582
|
+
if (reader) void reader.cancel().catch(() => void 0);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
async function reloadProviderCache(port) {
|
|
3586
|
+
try {
|
|
3587
|
+
const res = await timedFetch(`${opencodeBase(port)}/config`, {
|
|
3588
|
+
method: "PATCH",
|
|
3589
|
+
headers: { "Content-Type": "application/json" },
|
|
3590
|
+
body: JSON.stringify({})
|
|
3591
|
+
});
|
|
3592
|
+
if (!res.ok) {
|
|
3593
|
+
console.error(
|
|
3594
|
+
`[reloadProviderCache] PATCH /config returned HTTP ${res.status} (port ${port})`
|
|
3595
|
+
);
|
|
3596
|
+
}
|
|
3597
|
+
} catch (err) {
|
|
3598
|
+
console.error(
|
|
3599
|
+
`[reloadProviderCache] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
3600
|
+
);
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3355
3603
|
|
|
3356
3604
|
// src/lib/opencode/session-cleanup.ts
|
|
3357
3605
|
var DURATION_UNIT_MS = {
|
|
@@ -3458,8 +3706,8 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
3458
3706
|
}
|
|
3459
3707
|
|
|
3460
3708
|
// src/lib/opencode/session-db-size.ts
|
|
3461
|
-
import { statSync as statSync3 } from "fs";
|
|
3462
|
-
import { join as join4 } from "path";
|
|
3709
|
+
import { statSync as statSync3 } from "node:fs";
|
|
3710
|
+
import { join as join4 } from "node:path";
|
|
3463
3711
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
3464
3712
|
function statSessionDbBytes(homeDir) {
|
|
3465
3713
|
const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
@@ -3489,9 +3737,96 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
3489
3737
|
return null;
|
|
3490
3738
|
}
|
|
3491
3739
|
|
|
3740
|
+
// src/lib/opencode/log-tail.ts
|
|
3741
|
+
import { statSync as statSync4 } from "node:fs";
|
|
3742
|
+
import { homedir as homedir3 } from "node:os";
|
|
3743
|
+
import { join as join5 } from "node:path";
|
|
3744
|
+
import { open as open2, stat } from "node:fs/promises";
|
|
3745
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
3746
|
+
function resolveOpenCodeLogPath(homeDir = homedir3(), env = process.env) {
|
|
3747
|
+
const dataDir = env.XDG_DATA_HOME || join5(homeDir, ".local", "share");
|
|
3748
|
+
return join5(dataDir, "opencode", "log", "opencode.log");
|
|
3749
|
+
}
|
|
3750
|
+
function isEnoent(error2) {
|
|
3751
|
+
return error2?.code === "ENOENT";
|
|
3752
|
+
}
|
|
3753
|
+
function reportFailure(operation, logPath, error2) {
|
|
3754
|
+
console.error(
|
|
3755
|
+
`[opencode-log-tail] ${operation} failed for ${logPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3756
|
+
);
|
|
3757
|
+
}
|
|
3758
|
+
function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
3759
|
+
const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
3760
|
+
let offset = 0;
|
|
3761
|
+
let inode = null;
|
|
3762
|
+
let baselineReady = true;
|
|
3763
|
+
try {
|
|
3764
|
+
const initial = statSync4(logPath);
|
|
3765
|
+
offset = initial.size;
|
|
3766
|
+
inode = initial.ino;
|
|
3767
|
+
} catch (error2) {
|
|
3768
|
+
if (!isEnoent(error2)) {
|
|
3769
|
+
reportFailure("initial stat", logPath, error2);
|
|
3770
|
+
baselineReady = false;
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
let polling = false;
|
|
3774
|
+
let stopped = false;
|
|
3775
|
+
const poll = async () => {
|
|
3776
|
+
if (polling || stopped) return;
|
|
3777
|
+
polling = true;
|
|
3778
|
+
try {
|
|
3779
|
+
let current;
|
|
3780
|
+
try {
|
|
3781
|
+
current = await stat(logPath);
|
|
3782
|
+
} catch (error2) {
|
|
3783
|
+
if (!isEnoent(error2)) reportFailure("stat", logPath, error2);
|
|
3784
|
+
return;
|
|
3785
|
+
}
|
|
3786
|
+
if (!baselineReady) {
|
|
3787
|
+
offset = current.size;
|
|
3788
|
+
inode = current.ino;
|
|
3789
|
+
baselineReady = true;
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
if (inode !== null && current.ino !== inode || current.size < offset) {
|
|
3793
|
+
offset = 0;
|
|
3794
|
+
}
|
|
3795
|
+
inode = current.ino;
|
|
3796
|
+
if (current.size === offset) return;
|
|
3797
|
+
const length = current.size - offset;
|
|
3798
|
+
const fh = await open2(logPath, "r");
|
|
3799
|
+
try {
|
|
3800
|
+
const buf = Buffer.alloc(length);
|
|
3801
|
+
const { bytesRead } = await fh.read(buf, 0, length, offset);
|
|
3802
|
+
offset += bytesRead;
|
|
3803
|
+
if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
|
|
3804
|
+
} finally {
|
|
3805
|
+
await fh.close();
|
|
3806
|
+
}
|
|
3807
|
+
} catch (error2) {
|
|
3808
|
+
if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
|
|
3809
|
+
} finally {
|
|
3810
|
+
polling = false;
|
|
3811
|
+
}
|
|
3812
|
+
};
|
|
3813
|
+
const interval = setInterval(() => void poll(), pollIntervalMs);
|
|
3814
|
+
void poll();
|
|
3815
|
+
return {
|
|
3816
|
+
stop: () => {
|
|
3817
|
+
stopped = true;
|
|
3818
|
+
clearInterval(interval);
|
|
3819
|
+
}
|
|
3820
|
+
};
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3492
3823
|
// src/lib/opencode/session-db-reclaim.ts
|
|
3493
|
-
import { statSync as
|
|
3494
|
-
import { dirname as dirname4 } from "path";
|
|
3824
|
+
import { statSync as statSync5, statfsSync } from "node:fs";
|
|
3825
|
+
import { dirname as dirname4 } from "node:path";
|
|
3826
|
+
function errorMessage(error2) {
|
|
3827
|
+
if (!(error2 instanceof Error)) return String(error2);
|
|
3828
|
+
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
3829
|
+
}
|
|
3495
3830
|
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
3496
3831
|
try {
|
|
3497
3832
|
const fsStats = statfsSync(dirname4(dbPath));
|
|
@@ -3517,17 +3852,17 @@ async function probeReclaimAvailability(input) {
|
|
|
3517
3852
|
const { dbPath, requiredBytes } = input;
|
|
3518
3853
|
let sqlite;
|
|
3519
3854
|
try {
|
|
3520
|
-
sqlite = await import("sqlite");
|
|
3855
|
+
sqlite = await import("node:sqlite");
|
|
3521
3856
|
} catch (err) {
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
return "sqlite-unavailable";
|
|
3857
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
3858
|
+
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
3859
|
+
return { reason: "sqlite-unavailable", detail };
|
|
3526
3860
|
}
|
|
3527
3861
|
let autoVacuum = null;
|
|
3528
3862
|
try {
|
|
3529
3863
|
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
3530
3864
|
try {
|
|
3865
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3531
3866
|
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3532
3867
|
} finally {
|
|
3533
3868
|
db.close();
|
|
@@ -3538,23 +3873,25 @@ async function probeReclaimAvailability(input) {
|
|
|
3538
3873
|
);
|
|
3539
3874
|
}
|
|
3540
3875
|
if (autoVacuum !== 0) return null;
|
|
3541
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
|
|
3876
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
3542
3877
|
}
|
|
3543
3878
|
async function reclaimSessionDbSpace(input) {
|
|
3544
3879
|
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
3545
3880
|
let sqlite;
|
|
3546
3881
|
try {
|
|
3547
|
-
sqlite = await import("sqlite");
|
|
3882
|
+
sqlite = await import("node:sqlite");
|
|
3548
3883
|
} catch (err) {
|
|
3884
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
3549
3885
|
console.warn(
|
|
3550
|
-
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${
|
|
3886
|
+
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
3551
3887
|
);
|
|
3552
|
-
return { ok: false, skipped: "sqlite-unavailable" };
|
|
3888
|
+
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
3553
3889
|
}
|
|
3554
3890
|
const { DatabaseSync } = sqlite;
|
|
3555
3891
|
let db;
|
|
3556
3892
|
try {
|
|
3557
3893
|
db = new DatabaseSync(dbPath);
|
|
3894
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3558
3895
|
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3559
3896
|
if (autoVacuum === 0) {
|
|
3560
3897
|
if (!allowFullVacuum) {
|
|
@@ -3563,7 +3900,7 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3563
3900
|
);
|
|
3564
3901
|
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
3565
3902
|
}
|
|
3566
|
-
const fileBytesForGuard =
|
|
3903
|
+
const fileBytesForGuard = statSync5(dbPath).size;
|
|
3567
3904
|
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
3568
3905
|
if (skipReason !== null) {
|
|
3569
3906
|
console.warn(
|
|
@@ -3591,10 +3928,12 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3591
3928
|
);
|
|
3592
3929
|
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
3593
3930
|
} catch (err) {
|
|
3594
|
-
console.error(
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3931
|
+
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
3932
|
+
return {
|
|
3933
|
+
ok: false,
|
|
3934
|
+
skipped: "reclaim-error",
|
|
3935
|
+
detail: errorMessage(err)
|
|
3936
|
+
};
|
|
3598
3937
|
} finally {
|
|
3599
3938
|
db?.close();
|
|
3600
3939
|
}
|
|
@@ -3635,7 +3974,6 @@ var StreamForwarder = class {
|
|
|
3635
3974
|
handleFrame(frame) {
|
|
3636
3975
|
switch (frame.type) {
|
|
3637
3976
|
case "open":
|
|
3638
|
-
this.callbacks.onOpen?.(frame.sid, frame.method, frame.path);
|
|
3639
3977
|
void this.handleOpen(frame);
|
|
3640
3978
|
break;
|
|
3641
3979
|
case "req_data":
|
|
@@ -3671,12 +4009,21 @@ var StreamForwarder = class {
|
|
|
3671
4009
|
const { sid, method, path, headers, has_body } = frame;
|
|
3672
4010
|
const correlationId = headers?.[CORRELATION_ID_HEADER];
|
|
3673
4011
|
const startedAt = Date.now();
|
|
4012
|
+
if (path !== TUNNEL_DRAIN_PING_PATH && path !== TUNNEL_USAGE_REARM_PING_PATH) {
|
|
4013
|
+
this.callbacks.onOpen?.(sid, method, path);
|
|
4014
|
+
}
|
|
3674
4015
|
if (path === TUNNEL_DRAIN_PING_PATH) {
|
|
3675
4016
|
this.callbacks.onDrainPing?.();
|
|
3676
4017
|
this.send({ type: "head", sid, status: 204, headers: {} });
|
|
3677
4018
|
this.send({ type: "res_end", sid });
|
|
3678
4019
|
return;
|
|
3679
4020
|
}
|
|
4021
|
+
if (path === TUNNEL_USAGE_REARM_PING_PATH) {
|
|
4022
|
+
this.callbacks.onUsageRearmPing?.();
|
|
4023
|
+
this.send({ type: "head", sid, status: 204, headers: {} });
|
|
4024
|
+
this.send({ type: "res_end", sid });
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
3680
4027
|
if (process.env.DEBUG) {
|
|
3681
4028
|
log("debug", "agent_request", {
|
|
3682
4029
|
correlation_id: correlationId,
|
|
@@ -3821,7 +4168,8 @@ function connectTunnel(options) {
|
|
|
3821
4168
|
onResponse,
|
|
3822
4169
|
onInfo,
|
|
3823
4170
|
onWarning,
|
|
3824
|
-
onDrainPing
|
|
4171
|
+
onDrainPing,
|
|
4172
|
+
onUsageRearmPing
|
|
3825
4173
|
} = options;
|
|
3826
4174
|
const tunnelUrl = getTunnelUrlConfig();
|
|
3827
4175
|
const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
|
|
@@ -3833,7 +4181,8 @@ function connectTunnel(options) {
|
|
|
3833
4181
|
});
|
|
3834
4182
|
const forwarder = new StreamForwarder(ws, port, {
|
|
3835
4183
|
onHead: () => onResponse?.(),
|
|
3836
|
-
onDrainPing: () => onDrainPing?.()
|
|
4184
|
+
onDrainPing: () => onDrainPing?.(),
|
|
4185
|
+
onUsageRearmPing: () => onUsageRearmPing?.()
|
|
3837
4186
|
});
|
|
3838
4187
|
const connectionTimeout = setTimeout(() => {
|
|
3839
4188
|
ws.close();
|
|
@@ -3876,8 +4225,8 @@ function connectTunnel(options) {
|
|
|
3876
4225
|
try {
|
|
3877
4226
|
message = JSON.parse(data.toString());
|
|
3878
4227
|
} catch (error2) {
|
|
3879
|
-
const
|
|
3880
|
-
onError?.(`Failed to handle message: ${
|
|
4228
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
4229
|
+
onError?.(`Failed to handle message: ${errorMessage3}`);
|
|
3881
4230
|
return;
|
|
3882
4231
|
}
|
|
3883
4232
|
if (isStreamFrame(message)) {
|
|
@@ -3994,6 +4343,7 @@ var RunnerConnection = class {
|
|
|
3994
4343
|
onError: (error2) => events.onError?.(error2),
|
|
3995
4344
|
onResponse: () => events.onResponse?.(),
|
|
3996
4345
|
onDrainPing: () => events.onDrainPing?.(),
|
|
4346
|
+
onUsageRearmPing: () => events.onUsageRearmPing?.(),
|
|
3997
4347
|
onInfo: (message) => events.onInfo?.(message),
|
|
3998
4348
|
onWarning: (message) => events.onWarning?.(message)
|
|
3999
4349
|
});
|
|
@@ -4020,7 +4370,7 @@ var RunnerConnection = class {
|
|
|
4020
4370
|
};
|
|
4021
4371
|
|
|
4022
4372
|
// src/lib/tunnel/ready-marker.ts
|
|
4023
|
-
import { writeFileSync as writeFileSync3 } from "fs";
|
|
4373
|
+
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
4024
4374
|
function writeTunnelReadyMarker(path, agentId) {
|
|
4025
4375
|
try {
|
|
4026
4376
|
writeFileSync3(path, `${agentId}
|
|
@@ -4032,7 +4382,7 @@ function writeTunnelReadyMarker(path, agentId) {
|
|
|
4032
4382
|
}
|
|
4033
4383
|
|
|
4034
4384
|
// src/lib/replication.ts
|
|
4035
|
-
import { spawn as spawn4 } from "child_process";
|
|
4385
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
4036
4386
|
function startSessionDbReplication(configPath) {
|
|
4037
4387
|
return spawn4("litestream", ["replicate", "-config", configPath], {
|
|
4038
4388
|
stdio: "inherit"
|
|
@@ -4048,7 +4398,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
4048
4398
|
}
|
|
4049
4399
|
|
|
4050
4400
|
// src/lib/process-liveness.ts
|
|
4051
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
4401
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
4052
4402
|
function isProcessAlive(pid) {
|
|
4053
4403
|
try {
|
|
4054
4404
|
process.kill(pid, 0);
|
|
@@ -4074,9 +4424,9 @@ function isProcessAlive(pid) {
|
|
|
4074
4424
|
}
|
|
4075
4425
|
|
|
4076
4426
|
// src/lib/openai-usage.ts
|
|
4077
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
4078
|
-
import { homedir as
|
|
4079
|
-
import { join as
|
|
4427
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
4428
|
+
import { homedir as homedir4 } from "node:os";
|
|
4429
|
+
import { join as join6 } from "node:path";
|
|
4080
4430
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4081
4431
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4082
4432
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4090,7 +4440,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4090
4440
|
}
|
|
4091
4441
|
function readOpenCodeChatGptCredentials() {
|
|
4092
4442
|
try {
|
|
4093
|
-
const raw = readFileSync5(
|
|
4443
|
+
const raw = readFileSync5(join6(homedir4(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4094
4444
|
let parsed;
|
|
4095
4445
|
try {
|
|
4096
4446
|
parsed = JSON.parse(raw);
|
|
@@ -4127,7 +4477,7 @@ function parseChatGptIdentity(accessToken) {
|
|
|
4127
4477
|
const auth = payload["https://api.openai.com/auth"];
|
|
4128
4478
|
const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
|
|
4129
4479
|
const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
|
|
4130
|
-
return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
|
|
4480
|
+
return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
|
|
4131
4481
|
}
|
|
4132
4482
|
function toWindow2(headers, name) {
|
|
4133
4483
|
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
@@ -4309,13 +4659,6 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
|
|
|
4309
4659
|
envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
4310
4660
|
});
|
|
4311
4661
|
}
|
|
4312
|
-
function nextReportDelayMs(random = Math.random) {
|
|
4313
|
-
return usageReportDelayMs(random);
|
|
4314
|
-
}
|
|
4315
|
-
var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
|
|
4316
|
-
function claudeUsageFailureLogLevel(consecutiveFailures) {
|
|
4317
|
-
return usageReportFailureLogLevel(consecutiveFailures);
|
|
4318
|
-
}
|
|
4319
4662
|
|
|
4320
4663
|
// src/lib/openai-usage-reporting.ts
|
|
4321
4664
|
function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
@@ -4352,8 +4695,8 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
4352
4695
|
}
|
|
4353
4696
|
|
|
4354
4697
|
// src/lib/resource-usage.ts
|
|
4355
|
-
import { cpus, totalmem, freemem } from "os";
|
|
4356
|
-
import { statfsSync as statfsSync2 } from "fs";
|
|
4698
|
+
import { cpus, totalmem, freemem } from "node:os";
|
|
4699
|
+
import { statfsSync as statfsSync2 } from "node:fs";
|
|
4357
4700
|
|
|
4358
4701
|
// src/lib/ecs-task-metadata.ts
|
|
4359
4702
|
var ECS_METADATA_TIMEOUT_MS = 2e3;
|
|
@@ -4520,15 +4863,15 @@ function createResourceUsageCollector(homeDir) {
|
|
|
4520
4863
|
}
|
|
4521
4864
|
|
|
4522
4865
|
// src/lib/channels/driver.ts
|
|
4523
|
-
import { homedir as
|
|
4866
|
+
import { homedir as homedir5 } from "node:os";
|
|
4524
4867
|
|
|
4525
4868
|
// src/lib/runner-file-sync.ts
|
|
4526
|
-
import { join as
|
|
4869
|
+
import { join as join8 } from "node:path";
|
|
4527
4870
|
|
|
4528
4871
|
// src/lib/file-push.ts
|
|
4529
|
-
import { randomUUID } from "crypto";
|
|
4530
|
-
import { chmod, mkdir, open as
|
|
4531
|
-
import { basename, dirname as dirname5, isAbsolute, join as
|
|
4872
|
+
import { randomUUID } from "node:crypto";
|
|
4873
|
+
import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
|
|
4874
|
+
import { basename, dirname as dirname5, isAbsolute, join as join7, relative, resolve as resolve2, sep } from "node:path";
|
|
4532
4875
|
var FILE_MODE = 384;
|
|
4533
4876
|
var DIRECTORY_MODE = 448;
|
|
4534
4877
|
async function writePushedFile(request) {
|
|
@@ -4561,7 +4904,7 @@ async function writePushedFile(request) {
|
|
|
4561
4904
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
4562
4905
|
dirname5(candidate)
|
|
4563
4906
|
);
|
|
4564
|
-
const realTarget =
|
|
4907
|
+
const realTarget = join7(existingAncestor, ...missingSegments, basename(candidate));
|
|
4565
4908
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
4566
4909
|
if (allowedDirectory === null) {
|
|
4567
4910
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -4597,7 +4940,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
4597
4940
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
4598
4941
|
return null;
|
|
4599
4942
|
}
|
|
4600
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4943
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4601
4944
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
4602
4945
|
return null;
|
|
4603
4946
|
}
|
|
@@ -4670,16 +5013,16 @@ function contains(realDirectory, realTarget) {
|
|
|
4670
5013
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
4671
5014
|
let current = existingAncestor;
|
|
4672
5015
|
for (const segment of missingSegments) {
|
|
4673
|
-
current =
|
|
5016
|
+
current = join7(current, segment);
|
|
4674
5017
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
4675
5018
|
await chmod(current, DIRECTORY_MODE);
|
|
4676
5019
|
}
|
|
4677
5020
|
}
|
|
4678
5021
|
async function writeAtomically(realTarget, content) {
|
|
4679
|
-
const temporaryPath =
|
|
5022
|
+
const temporaryPath = join7(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
4680
5023
|
let handle;
|
|
4681
5024
|
try {
|
|
4682
|
-
handle = await
|
|
5025
|
+
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
4683
5026
|
await handle.writeFile(content);
|
|
4684
5027
|
await handle.chmod(FILE_MODE);
|
|
4685
5028
|
await handle.close();
|
|
@@ -4806,12 +5149,12 @@ var NOT_APPLIED = {
|
|
|
4806
5149
|
opencodeAuthApplied: false
|
|
4807
5150
|
};
|
|
4808
5151
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
4809
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4810
|
-
return expanded ===
|
|
5152
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5153
|
+
return expanded === join8(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
4811
5154
|
}
|
|
4812
5155
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
4813
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4814
|
-
return expanded ===
|
|
5156
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5157
|
+
return expanded === join8(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
4815
5158
|
}
|
|
4816
5159
|
async function applyOne(options, file) {
|
|
4817
5160
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -4971,6 +5314,10 @@ var DEFAULT_RETRY_POLICY = {
|
|
|
4971
5314
|
baseDelayMs: 500,
|
|
4972
5315
|
maxDelayMs: 3e4
|
|
4973
5316
|
};
|
|
5317
|
+
var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
|
|
5318
|
+
var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
|
|
5319
|
+
var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
5320
|
+
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
4974
5321
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
4975
5322
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
4976
5323
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
@@ -5111,6 +5458,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5111
5458
|
* message; it is removed once its in-flight set empties.
|
|
5112
5459
|
*/
|
|
5113
5460
|
watchers = /* @__PURE__ */ new Map();
|
|
5461
|
+
sessionErrorStream = null;
|
|
5462
|
+
/**
|
|
5463
|
+
* Session-error failures currently being reported; entries are empty at rest
|
|
5464
|
+
* because each handoff deletes its id in `finally`.
|
|
5465
|
+
*/
|
|
5466
|
+
sessionErrorHandled = /* @__PURE__ */ new Set();
|
|
5467
|
+
/**
|
|
5468
|
+
* Session errors that arrived before their dispatch was registered. Bounded FIFO
|
|
5469
|
+
* with a short TTL so an unmatched session cannot retain an event indefinitely.
|
|
5470
|
+
*/
|
|
5471
|
+
bufferedSessionErrors = /* @__PURE__ */ new Map();
|
|
5114
5472
|
/**
|
|
5115
5473
|
* AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
|
|
5116
5474
|
* dispatched and are still in-flight. A message in this set is never
|
|
@@ -5294,6 +5652,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5294
5652
|
* no watcher) can resolve the title.
|
|
5295
5653
|
*/
|
|
5296
5654
|
sessionTitles = /* @__PURE__ */ new Map();
|
|
5655
|
+
/** One best-effort terminal subagent collection per Evident message id. */
|
|
5656
|
+
subagentInvocationCollections = /* @__PURE__ */ new Map();
|
|
5657
|
+
/**
|
|
5658
|
+
* Early snapshots are only liveness hints; they must not become the terminal
|
|
5659
|
+
* collection when the task parts or child transcript have advanced.
|
|
5660
|
+
*/
|
|
5661
|
+
subagentInvocationPrefetches = /* @__PURE__ */ new Map();
|
|
5297
5662
|
/** Serialises drains so a reconnect during a drain doesn't double-process. */
|
|
5298
5663
|
draining = false;
|
|
5299
5664
|
/**
|
|
@@ -5358,7 +5723,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5358
5723
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
5359
5724
|
this.now = config.now ?? (() => Date.now());
|
|
5360
5725
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
5361
|
-
this.homeDir = config.homeDir ??
|
|
5726
|
+
this.homeDir = config.homeDir ?? homedir5();
|
|
5362
5727
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
5363
5728
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
5364
5729
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
@@ -5508,6 +5873,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5508
5873
|
}
|
|
5509
5874
|
return ids;
|
|
5510
5875
|
}
|
|
5876
|
+
/**
|
|
5877
|
+
* OpenCode user-message ids tracked for other Evident messages in a session.
|
|
5878
|
+
* Excluding this message makes an unattributed later row fail safe; a missing
|
|
5879
|
+
* watcher yields no attributions, per `hasLaterSiblingTurnStarted`'s docblock.
|
|
5880
|
+
*/
|
|
5881
|
+
siblingOpencodeMessageIds(watcher, ownEvidentMessageId) {
|
|
5882
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5883
|
+
if (!watcher) return ids;
|
|
5884
|
+
for (const inFlight of watcher.inFlight.values()) {
|
|
5885
|
+
if (inFlight.evidentMessageId !== ownEvidentMessageId) {
|
|
5886
|
+
ids.add(inFlight.opencodeMessageId);
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
return ids;
|
|
5890
|
+
}
|
|
5511
5891
|
/**
|
|
5512
5892
|
* File-pull work, for `run.ts`'s idle accounting (#559).
|
|
5513
5893
|
*
|
|
@@ -5574,6 +5954,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5574
5954
|
*/
|
|
5575
5955
|
stop() {
|
|
5576
5956
|
this.stopped = true;
|
|
5957
|
+
this.sessionErrorStream?.abort.abort();
|
|
5958
|
+
this.sessionErrorStream = null;
|
|
5577
5959
|
}
|
|
5578
5960
|
/**
|
|
5579
5961
|
* The server clears this request when a new MicroVM identity is recorded, so a
|
|
@@ -5648,6 +6030,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5648
6030
|
*/
|
|
5649
6031
|
async processConversation(conv) {
|
|
5650
6032
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
6033
|
+
this.ensureSessionErrorStream();
|
|
5651
6034
|
const messages = await this.getPendingMessages(conv.id);
|
|
5652
6035
|
let dispatched = 0;
|
|
5653
6036
|
let skippedAlreadyDispatched = 0;
|
|
@@ -5720,7 +6103,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5720
6103
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
5721
6104
|
break;
|
|
5722
6105
|
}
|
|
5723
|
-
const
|
|
6106
|
+
const errorMessage3 = err instanceof Error ? err.message : String(err);
|
|
5724
6107
|
this.sessions.delete(conv.id);
|
|
5725
6108
|
this.supersede(conv.id, sessionId);
|
|
5726
6109
|
this.log({
|
|
@@ -5729,7 +6112,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5729
6112
|
conversation_id: conv.id,
|
|
5730
6113
|
message_id: message.id
|
|
5731
6114
|
});
|
|
5732
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6115
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5733
6116
|
this.log({
|
|
5734
6117
|
level: "warn",
|
|
5735
6118
|
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)}`,
|
|
@@ -5740,7 +6123,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5740
6123
|
});
|
|
5741
6124
|
this.log({
|
|
5742
6125
|
level: "error",
|
|
5743
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
6126
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
|
|
5744
6127
|
conversation_id: conv.id,
|
|
5745
6128
|
message_id: message.id
|
|
5746
6129
|
});
|
|
@@ -5761,14 +6144,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5761
6144
|
this.unconfirmedDispatchFailures.delete(message.id);
|
|
5762
6145
|
this.sessions.delete(conv.id);
|
|
5763
6146
|
this.supersede(conv.id, sessionId);
|
|
5764
|
-
const
|
|
6147
|
+
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.`;
|
|
5765
6148
|
this.log({
|
|
5766
6149
|
level: "error",
|
|
5767
|
-
message:
|
|
6150
|
+
message: errorMessage3,
|
|
5768
6151
|
conversation_id: conv.id,
|
|
5769
6152
|
message_id: message.id
|
|
5770
6153
|
});
|
|
5771
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6154
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5772
6155
|
this.log({
|
|
5773
6156
|
level: "warn",
|
|
5774
6157
|
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)}`,
|
|
@@ -5994,6 +6377,23 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5994
6377
|
if (state === "running" || state === "queued") {
|
|
5995
6378
|
const ongoing = await isSessionOngoing(this.port, sessionId);
|
|
5996
6379
|
if (ongoing === true) {
|
|
6380
|
+
if (state === "queued") {
|
|
6381
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(
|
|
6382
|
+
this.watchers.get(sessionId),
|
|
6383
|
+
message.id
|
|
6384
|
+
);
|
|
6385
|
+
if (hasLaterSiblingTurnStarted(messages, ocId ?? "", siblingOcIds)) {
|
|
6386
|
+
this.log({
|
|
6387
|
+
level: "warn",
|
|
6388
|
+
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`,
|
|
6389
|
+
conversation_id: conv.id,
|
|
6390
|
+
message_id: message.id
|
|
6391
|
+
});
|
|
6392
|
+
this.clearRedriveUnresolved(message.id);
|
|
6393
|
+
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
6394
|
+
return "dispatch";
|
|
6395
|
+
}
|
|
6396
|
+
}
|
|
5997
6397
|
return this.reattachRedrive(conv, sessionId, message, ocId);
|
|
5998
6398
|
}
|
|
5999
6399
|
if (ongoing === false) {
|
|
@@ -6083,16 +6483,37 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6083
6483
|
if (state === "done") {
|
|
6084
6484
|
const title = await this.resolveSessionTitle(sessionId, conv.id);
|
|
6085
6485
|
const usage = messageUsage(messages, ocId ?? "");
|
|
6486
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
6487
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
6488
|
+
messages,
|
|
6489
|
+
ocId ?? "",
|
|
6490
|
+
message.id
|
|
6491
|
+
);
|
|
6086
6492
|
this.log({
|
|
6087
6493
|
level: "info",
|
|
6088
6494
|
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`,
|
|
6089
6495
|
conversation_id: conv.id,
|
|
6090
6496
|
message_id: message.id
|
|
6091
6497
|
});
|
|
6092
|
-
await this.markDone(
|
|
6498
|
+
await this.markDone(
|
|
6499
|
+
conv.id,
|
|
6500
|
+
message.id,
|
|
6501
|
+
sessionId,
|
|
6502
|
+
ocId,
|
|
6503
|
+
title,
|
|
6504
|
+
usage,
|
|
6505
|
+
usageAgentName,
|
|
6506
|
+
subagentInvocations
|
|
6507
|
+
);
|
|
6093
6508
|
} else {
|
|
6094
6509
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
6095
6510
|
const usage = messageUsage(messages, ocId ?? "");
|
|
6511
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
6512
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
6513
|
+
messages,
|
|
6514
|
+
ocId ?? "",
|
|
6515
|
+
message.id
|
|
6516
|
+
);
|
|
6096
6517
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
6097
6518
|
this.log({
|
|
6098
6519
|
level: "error",
|
|
@@ -6100,7 +6521,19 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6100
6521
|
conversation_id: conv.id,
|
|
6101
6522
|
message_id: message.id
|
|
6102
6523
|
});
|
|
6103
|
-
await this.markFailed(
|
|
6524
|
+
await this.markFailed(
|
|
6525
|
+
conv.id,
|
|
6526
|
+
message.id,
|
|
6527
|
+
sessionId,
|
|
6528
|
+
error2,
|
|
6529
|
+
usage,
|
|
6530
|
+
failure,
|
|
6531
|
+
usageAgentName,
|
|
6532
|
+
subagentInvocations
|
|
6533
|
+
);
|
|
6534
|
+
}
|
|
6535
|
+
if (ocId !== null) {
|
|
6536
|
+
await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
|
|
6104
6537
|
}
|
|
6105
6538
|
} catch (err) {
|
|
6106
6539
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -6641,6 +7074,24 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6641
7074
|
ambiguousPinnedSinceMs: 0,
|
|
6642
7075
|
ambiguousResolved: false
|
|
6643
7076
|
});
|
|
7077
|
+
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
7078
|
+
if (!buffered) return;
|
|
7079
|
+
this.bufferedSessionErrors.delete(sessionId);
|
|
7080
|
+
if (this.now() - buffered.receivedAt < SESSION_ERROR_BUFFER_TTL_MS) {
|
|
7081
|
+
this.handleSessionError(buffered.event);
|
|
7082
|
+
}
|
|
7083
|
+
}
|
|
7084
|
+
bufferSessionError(event) {
|
|
7085
|
+
this.bufferedSessionErrors.delete(event.sessionId);
|
|
7086
|
+
this.bufferedSessionErrors.set(event.sessionId, {
|
|
7087
|
+
event,
|
|
7088
|
+
receivedAt: this.now()
|
|
7089
|
+
});
|
|
7090
|
+
while (this.bufferedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
|
|
7091
|
+
const oldest = this.bufferedSessionErrors.keys().next().value;
|
|
7092
|
+
if (typeof oldest !== "string") break;
|
|
7093
|
+
this.bufferedSessionErrors.delete(oldest);
|
|
7094
|
+
}
|
|
6644
7095
|
}
|
|
6645
7096
|
/**
|
|
6646
7097
|
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
@@ -6845,6 +7296,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6845
7296
|
ensureWatcherRunning(sessionId) {
|
|
6846
7297
|
const watcher = this.watchers.get(sessionId);
|
|
6847
7298
|
if (!watcher) return;
|
|
7299
|
+
this.ensureSessionErrorStream();
|
|
6848
7300
|
if (watcher.loop) return;
|
|
6849
7301
|
if (watcher.inFlight.size === 0) {
|
|
6850
7302
|
this.watchers.delete(sessionId);
|
|
@@ -6860,6 +7312,154 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6860
7312
|
});
|
|
6861
7313
|
watcher.loop = loop;
|
|
6862
7314
|
}
|
|
7315
|
+
ensureSessionErrorStream() {
|
|
7316
|
+
if (this.sessionErrorStream || this.stopped) return;
|
|
7317
|
+
const abort = new AbortController();
|
|
7318
|
+
const loop = this.runSessionErrorStream(abort.signal);
|
|
7319
|
+
this.sessionErrorStream = { abort, loop };
|
|
7320
|
+
}
|
|
7321
|
+
async runSessionErrorStream(signal) {
|
|
7322
|
+
let attempt = 0;
|
|
7323
|
+
let warned = false;
|
|
7324
|
+
while (!this.stopped && !signal.aborted) {
|
|
7325
|
+
const openedAt = this.now();
|
|
7326
|
+
try {
|
|
7327
|
+
const outcome = await readSessionErrorStream(this.port, {
|
|
7328
|
+
signal,
|
|
7329
|
+
onSessionError: (event) => this.handleSessionError(event)
|
|
7330
|
+
});
|
|
7331
|
+
if (outcome.reason === "aborted" || signal.aborted) return;
|
|
7332
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
7333
|
+
if (outcome.reason === "unavailable" || outcome.reason === "ended") {
|
|
7334
|
+
if (!healthy) {
|
|
7335
|
+
const detail = outcome.reason === "unavailable" ? outcome.detail : "stream ended";
|
|
7336
|
+
this.log({
|
|
7337
|
+
level: warned ? "debug" : "warn",
|
|
7338
|
+
message: `OpenCode session-error stream ${warned ? "still unavailable" : "unavailable"} (${detail}); transcript polling remains the evidence path`
|
|
7339
|
+
});
|
|
7340
|
+
warned = true;
|
|
7341
|
+
}
|
|
7342
|
+
}
|
|
7343
|
+
if (healthy) {
|
|
7344
|
+
if (warned) {
|
|
7345
|
+
this.log({
|
|
7346
|
+
level: "info",
|
|
7347
|
+
message: "OpenCode session-error stream reconnected; transcript polling remains the evidence path"
|
|
7348
|
+
});
|
|
7349
|
+
warned = false;
|
|
7350
|
+
}
|
|
7351
|
+
attempt = 0;
|
|
7352
|
+
} else {
|
|
7353
|
+
attempt += 1;
|
|
7354
|
+
}
|
|
7355
|
+
if (this.stopped || signal.aborted) return;
|
|
7356
|
+
await this.sleep(backoffDelay(healthy ? 0 : attempt - 1, this.retry));
|
|
7357
|
+
} catch (err) {
|
|
7358
|
+
if (this.stopped || signal.aborted) return;
|
|
7359
|
+
this.log({
|
|
7360
|
+
level: "error",
|
|
7361
|
+
message: `OpenCode session-error stream failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
|
|
7362
|
+
});
|
|
7363
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
7364
|
+
const delayAttempt = healthy ? 0 : attempt;
|
|
7365
|
+
attempt = healthy ? 0 : attempt + 1;
|
|
7366
|
+
try {
|
|
7367
|
+
await this.sleep(backoffDelay(delayAttempt, this.retry));
|
|
7368
|
+
} catch (sleepErr) {
|
|
7369
|
+
this.log({
|
|
7370
|
+
level: "error",
|
|
7371
|
+
message: `OpenCode session-error stream backoff failed unexpectedly: ${sleepErr instanceof Error ? sleepErr.message : String(sleepErr)}`
|
|
7372
|
+
});
|
|
7373
|
+
}
|
|
7374
|
+
}
|
|
7375
|
+
}
|
|
7376
|
+
}
|
|
7377
|
+
handleSessionError(event) {
|
|
7378
|
+
try {
|
|
7379
|
+
const watcher = this.watchers.get(event.sessionId);
|
|
7380
|
+
if (!watcher) {
|
|
7381
|
+
this.bufferSessionError(event);
|
|
7382
|
+
this.log({
|
|
7383
|
+
level: "debug",
|
|
7384
|
+
message: `Ignoring session error for unknown session ${event.sessionId.slice(0, 8)}`
|
|
7385
|
+
});
|
|
7386
|
+
return;
|
|
7387
|
+
}
|
|
7388
|
+
if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
|
|
7389
|
+
this.log({
|
|
7390
|
+
level: "debug",
|
|
7391
|
+
message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
|
|
7392
|
+
conversation_id: watcher.conv.id
|
|
7393
|
+
});
|
|
7394
|
+
return;
|
|
7395
|
+
}
|
|
7396
|
+
const inFlight = [...watcher.inFlight.values()].filter((message) => !message.started && !message.done).sort((a, b) => a.dispatchedAt - b.dispatchedAt)[0];
|
|
7397
|
+
if (!inFlight) {
|
|
7398
|
+
this.bufferSessionError(event);
|
|
7399
|
+
this.log({
|
|
7400
|
+
level: "debug",
|
|
7401
|
+
message: `No queued in-flight turn to correlate with session error in ${event.sessionId.slice(0, 8)}`,
|
|
7402
|
+
conversation_id: watcher.conv.id
|
|
7403
|
+
});
|
|
7404
|
+
return;
|
|
7405
|
+
}
|
|
7406
|
+
if (this.sessionErrorHandled.has(inFlight.evidentMessageId)) return;
|
|
7407
|
+
this.sessionErrorHandled.add(inFlight.evidentMessageId);
|
|
7408
|
+
void this.failFromSessionError(watcher, event, inFlight);
|
|
7409
|
+
} catch (err) {
|
|
7410
|
+
this.log({
|
|
7411
|
+
level: "error",
|
|
7412
|
+
message: `Failed to handle OpenCode session error for ${event.sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`
|
|
7413
|
+
});
|
|
7414
|
+
}
|
|
7415
|
+
}
|
|
7416
|
+
async failFromSessionError(watcher, event, inFlight) {
|
|
7417
|
+
try {
|
|
7418
|
+
const messages = await getSessionMessages(this.port, event.sessionId);
|
|
7419
|
+
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7420
|
+
if (state !== "queued") {
|
|
7421
|
+
this.log({
|
|
7422
|
+
level: "debug",
|
|
7423
|
+
message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
|
|
7424
|
+
conversation_id: watcher.conv.id,
|
|
7425
|
+
message_id: inFlight.evidentMessageId
|
|
7426
|
+
});
|
|
7427
|
+
return;
|
|
7428
|
+
}
|
|
7429
|
+
this.log({
|
|
7430
|
+
level: "error",
|
|
7431
|
+
message: `OpenCode could not run message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)}: ${event.reason}`,
|
|
7432
|
+
conversation_id: watcher.conv.id,
|
|
7433
|
+
message_id: inFlight.evidentMessageId
|
|
7434
|
+
});
|
|
7435
|
+
await this.markFailed(
|
|
7436
|
+
watcher.conv.id,
|
|
7437
|
+
inFlight.evidentMessageId,
|
|
7438
|
+
event.sessionId,
|
|
7439
|
+
`OpenCode could not run this turn: ${event.reason}`
|
|
7440
|
+
);
|
|
7441
|
+
inFlight.done = true;
|
|
7442
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7443
|
+
} catch (err) {
|
|
7444
|
+
if (err instanceof ChannelAuthError) {
|
|
7445
|
+
this.log({
|
|
7446
|
+
level: "warn",
|
|
7447
|
+
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`,
|
|
7448
|
+
conversation_id: watcher.conv.id,
|
|
7449
|
+
message_id: inFlight.evidentMessageId
|
|
7450
|
+
});
|
|
7451
|
+
} else {
|
|
7452
|
+
this.log({
|
|
7453
|
+
level: "warn",
|
|
7454
|
+
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`,
|
|
7455
|
+
conversation_id: watcher.conv.id,
|
|
7456
|
+
message_id: inFlight.evidentMessageId
|
|
7457
|
+
});
|
|
7458
|
+
}
|
|
7459
|
+
} finally {
|
|
7460
|
+
this.sessionErrorHandled.delete(inFlight.evidentMessageId);
|
|
7461
|
+
}
|
|
7462
|
+
}
|
|
6863
7463
|
/**
|
|
6864
7464
|
* The per-session polling loop (WI-3). Once per tick it:
|
|
6865
7465
|
* 1. polls `GET /session/:id/message` once and, per in-flight message,
|
|
@@ -6972,6 +7572,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6972
7572
|
const conv = watcher.conv;
|
|
6973
7573
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
6974
7574
|
const id = inFlight.evidentMessageId;
|
|
7575
|
+
if (messages && collectTaskCalls(messages, inFlight.opencodeMessageId).length > 0 && !this.subagentInvocationPrefetches.has(id)) {
|
|
7576
|
+
void this.resolveSubagentInvocations(
|
|
7577
|
+
messages,
|
|
7578
|
+
inFlight.opencodeMessageId,
|
|
7579
|
+
id,
|
|
7580
|
+
"prefetch"
|
|
7581
|
+
).catch((err) => {
|
|
7582
|
+
this.log({
|
|
7583
|
+
level: "warn",
|
|
7584
|
+
message: `Best-effort subagent usage prefetch failed for message ${id.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
7585
|
+
conversation_id: conv.id,
|
|
7586
|
+
message_id: id
|
|
7587
|
+
});
|
|
7588
|
+
});
|
|
7589
|
+
}
|
|
6975
7590
|
if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
|
|
6976
7591
|
else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
|
|
6977
7592
|
if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
|
|
@@ -7025,6 +7640,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7025
7640
|
message_id: inFlight.evidentMessageId
|
|
7026
7641
|
});
|
|
7027
7642
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
7643
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
7644
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
7645
|
+
messages,
|
|
7646
|
+
inFlight.opencodeMessageId,
|
|
7647
|
+
inFlight.evidentMessageId
|
|
7648
|
+
);
|
|
7028
7649
|
const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
|
|
7029
7650
|
try {
|
|
7030
7651
|
await this.markFailed(
|
|
@@ -7033,7 +7654,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7033
7654
|
sessionId,
|
|
7034
7655
|
error2,
|
|
7035
7656
|
usage,
|
|
7036
|
-
failure
|
|
7657
|
+
failure,
|
|
7658
|
+
usageAgentName,
|
|
7659
|
+
subagentInvocations
|
|
7037
7660
|
);
|
|
7038
7661
|
} catch (err) {
|
|
7039
7662
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7066,13 +7689,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7066
7689
|
return;
|
|
7067
7690
|
}
|
|
7068
7691
|
inFlight.done = true;
|
|
7692
|
+
await this.reportSubagentAuthFailures(
|
|
7693
|
+
watcher.conv.id,
|
|
7694
|
+
inFlight.opencodeMessageId,
|
|
7695
|
+
inFlight.evidentMessageId,
|
|
7696
|
+
messages
|
|
7697
|
+
);
|
|
7069
7698
|
}
|
|
7070
7699
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7071
7700
|
return;
|
|
7072
7701
|
}
|
|
7702
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
|
|
7703
|
+
const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
|
|
7073
7704
|
const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
|
|
7074
7705
|
const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
|
|
7075
|
-
if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
|
|
7706
|
+
if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
|
|
7076
7707
|
inFlight.stuckReported = true;
|
|
7077
7708
|
void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
|
|
7078
7709
|
stuck_for_ms: this.now() - inFlight.dispatchedAt
|
|
@@ -7233,7 +7864,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7233
7864
|
const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
|
|
7234
7865
|
(sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
|
|
7235
7866
|
);
|
|
7236
|
-
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
7867
|
+
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
|
|
7237
7868
|
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
7238
7869
|
this.log({
|
|
7239
7870
|
level: "debug",
|
|
@@ -7268,6 +7899,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7268
7899
|
});
|
|
7269
7900
|
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
7270
7901
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
7902
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
7903
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
7904
|
+
messages,
|
|
7905
|
+
inFlight.opencodeMessageId,
|
|
7906
|
+
inFlight.evidentMessageId
|
|
7907
|
+
);
|
|
7271
7908
|
try {
|
|
7272
7909
|
await this.markDone(
|
|
7273
7910
|
conv.id,
|
|
@@ -7275,7 +7912,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7275
7912
|
sessionId,
|
|
7276
7913
|
inFlight.opencodeMessageId,
|
|
7277
7914
|
title,
|
|
7278
|
-
usage
|
|
7915
|
+
usage,
|
|
7916
|
+
usageAgentName,
|
|
7917
|
+
subagentInvocations
|
|
7279
7918
|
);
|
|
7280
7919
|
} catch (err) {
|
|
7281
7920
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7308,6 +7947,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7308
7947
|
return;
|
|
7309
7948
|
}
|
|
7310
7949
|
inFlight.done = true;
|
|
7950
|
+
await this.reportSubagentAuthFailures(
|
|
7951
|
+
watcher.conv.id,
|
|
7952
|
+
inFlight.opencodeMessageId,
|
|
7953
|
+
inFlight.evidentMessageId,
|
|
7954
|
+
messages
|
|
7955
|
+
);
|
|
7311
7956
|
}
|
|
7312
7957
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7313
7958
|
}
|
|
@@ -7448,6 +8093,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7448
8093
|
if (state === "failed" && !restartAborted) {
|
|
7449
8094
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
7450
8095
|
const usage = messageUsage(messages, ocId ?? "");
|
|
8096
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
8097
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
8098
|
+
messages,
|
|
8099
|
+
ocId ?? "",
|
|
8100
|
+
row.id
|
|
8101
|
+
);
|
|
7451
8102
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
7452
8103
|
this.log({
|
|
7453
8104
|
level: "error",
|
|
@@ -7456,7 +8107,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7456
8107
|
message_id: row.id
|
|
7457
8108
|
});
|
|
7458
8109
|
try {
|
|
7459
|
-
await this.markFailed(
|
|
8110
|
+
await this.markFailed(
|
|
8111
|
+
row.conversation_id,
|
|
8112
|
+
row.id,
|
|
8113
|
+
sessionId,
|
|
8114
|
+
error2,
|
|
8115
|
+
usage,
|
|
8116
|
+
failure,
|
|
8117
|
+
usageAgentName,
|
|
8118
|
+
subagentInvocations
|
|
8119
|
+
);
|
|
7460
8120
|
} catch (err) {
|
|
7461
8121
|
if (err instanceof ChannelAuthError) throw err;
|
|
7462
8122
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7478,6 +8138,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7478
8138
|
});
|
|
7479
8139
|
return;
|
|
7480
8140
|
}
|
|
8141
|
+
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
7481
8142
|
this.dontRedispatch.delete(row.id);
|
|
7482
8143
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
7483
8144
|
return;
|
|
@@ -7617,7 +8278,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7617
8278
|
try {
|
|
7618
8279
|
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
7619
8280
|
const usage = messageUsage(messages, ocId ?? "");
|
|
7620
|
-
|
|
8281
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
8282
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
8283
|
+
messages,
|
|
8284
|
+
ocId ?? "",
|
|
8285
|
+
row.id
|
|
8286
|
+
);
|
|
8287
|
+
await this.markDone(
|
|
8288
|
+
row.conversation_id,
|
|
8289
|
+
row.id,
|
|
8290
|
+
sessionId,
|
|
8291
|
+
ocId,
|
|
8292
|
+
title,
|
|
8293
|
+
usage,
|
|
8294
|
+
usageAgentName,
|
|
8295
|
+
subagentInvocations
|
|
8296
|
+
);
|
|
7621
8297
|
} catch (err) {
|
|
7622
8298
|
if (err instanceof ChannelAuthError) throw err;
|
|
7623
8299
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7639,6 +8315,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7639
8315
|
});
|
|
7640
8316
|
return;
|
|
7641
8317
|
}
|
|
8318
|
+
if (ocId !== null) {
|
|
8319
|
+
await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
|
|
8320
|
+
}
|
|
7642
8321
|
this.dontRedispatch.delete(row.id);
|
|
7643
8322
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
7644
8323
|
}
|
|
@@ -7732,14 +8411,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7732
8411
|
this.unconfirmedDispatchFailures.delete(row.id);
|
|
7733
8412
|
this.sessions.delete(readoptConv.id);
|
|
7734
8413
|
this.supersede(readoptConv.id, sessionId);
|
|
7735
|
-
const
|
|
8414
|
+
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.`;
|
|
7736
8415
|
this.log({
|
|
7737
8416
|
level: "error",
|
|
7738
|
-
message:
|
|
8417
|
+
message: errorMessage3,
|
|
7739
8418
|
conversation_id: row.conversation_id,
|
|
7740
8419
|
message_id: row.id
|
|
7741
8420
|
});
|
|
7742
|
-
await this.markFailed(row.conversation_id, row.id, null,
|
|
8421
|
+
await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
|
|
7743
8422
|
this.log({
|
|
7744
8423
|
level: "warn",
|
|
7745
8424
|
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)}`,
|
|
@@ -8018,6 +8697,166 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8018
8697
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
8019
8698
|
return parent;
|
|
8020
8699
|
}
|
|
8700
|
+
usageAgentName(messages, userMessageId) {
|
|
8701
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
8702
|
+
const mode = reply?.info?.mode;
|
|
8703
|
+
if (typeof mode === "string" && mode.length > 0) return mode;
|
|
8704
|
+
const agent = reply?.info?.agent;
|
|
8705
|
+
return typeof agent === "string" && agent.length > 0 ? agent : null;
|
|
8706
|
+
}
|
|
8707
|
+
async resolveSubagentInvocations(messages, userMessageId, messageId, phase = "terminal") {
|
|
8708
|
+
if (!messages) return void 0;
|
|
8709
|
+
const cache = phase === "prefetch" ? this.subagentInvocationPrefetches : this.subagentInvocationCollections;
|
|
8710
|
+
const cached = cache.get(messageId);
|
|
8711
|
+
if (cached) return cached;
|
|
8712
|
+
const collection = this.buildSubagentInvocations(messages, userMessageId, messageId).catch(
|
|
8713
|
+
(err) => {
|
|
8714
|
+
this.log({
|
|
8715
|
+
level: "warn",
|
|
8716
|
+
message: `Best-effort subagent usage collection failed for message ${messageId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
8717
|
+
message_id: messageId
|
|
8718
|
+
});
|
|
8719
|
+
return void 0;
|
|
8720
|
+
}
|
|
8721
|
+
);
|
|
8722
|
+
cache.set(messageId, collection);
|
|
8723
|
+
const result = await collection;
|
|
8724
|
+
if (result === void 0 && cache.get(messageId) === collection) cache.delete(messageId);
|
|
8725
|
+
return result;
|
|
8726
|
+
}
|
|
8727
|
+
clearSubagentInvocationCaches(messageId) {
|
|
8728
|
+
this.subagentInvocationCollections.delete(messageId);
|
|
8729
|
+
this.subagentInvocationPrefetches.delete(messageId);
|
|
8730
|
+
}
|
|
8731
|
+
async buildSubagentInvocations(messages, userMessageId, messageId) {
|
|
8732
|
+
const rootCalls = collectTaskCalls(messages, userMessageId);
|
|
8733
|
+
if (rootCalls.length === 0) return void 0;
|
|
8734
|
+
const childMessages = /* @__PURE__ */ new Map();
|
|
8735
|
+
const seenCallIds = new Set(rootCalls.map((call) => call.callID));
|
|
8736
|
+
const work = rootCalls.map((call) => ({
|
|
8737
|
+
call,
|
|
8738
|
+
depth: 1
|
|
8739
|
+
}));
|
|
8740
|
+
const payload = [];
|
|
8741
|
+
const fetchChildMessages = (sessionId) => {
|
|
8742
|
+
const cached = childMessages.get(sessionId);
|
|
8743
|
+
if (cached) return cached;
|
|
8744
|
+
const pending = (async () => {
|
|
8745
|
+
try {
|
|
8746
|
+
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
8747
|
+
if (!res.ok) {
|
|
8748
|
+
this.log({
|
|
8749
|
+
level: "warn",
|
|
8750
|
+
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`,
|
|
8751
|
+
message_id: messageId
|
|
8752
|
+
});
|
|
8753
|
+
return null;
|
|
8754
|
+
}
|
|
8755
|
+
const body = await res.json();
|
|
8756
|
+
if (!Array.isArray(body)) throw new Error("response body was not a message array");
|
|
8757
|
+
return body;
|
|
8758
|
+
} catch (err) {
|
|
8759
|
+
this.log({
|
|
8760
|
+
level: "warn",
|
|
8761
|
+
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)}`,
|
|
8762
|
+
message_id: messageId
|
|
8763
|
+
});
|
|
8764
|
+
return null;
|
|
8765
|
+
}
|
|
8766
|
+
})();
|
|
8767
|
+
childMessages.set(sessionId, pending);
|
|
8768
|
+
return pending;
|
|
8769
|
+
};
|
|
8770
|
+
const fetchChildWithoutBlocking = async (sessionId) => {
|
|
8771
|
+
const pending = fetchChildMessages(sessionId);
|
|
8772
|
+
let timer;
|
|
8773
|
+
const timeout = new Promise((resolve4) => {
|
|
8774
|
+
timer = setTimeout(() => {
|
|
8775
|
+
this.log({
|
|
8776
|
+
level: "warn",
|
|
8777
|
+
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`,
|
|
8778
|
+
message_id: messageId
|
|
8779
|
+
});
|
|
8780
|
+
resolve4(null);
|
|
8781
|
+
}, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
|
|
8782
|
+
});
|
|
8783
|
+
try {
|
|
8784
|
+
return await Promise.race([pending, timeout]);
|
|
8785
|
+
} finally {
|
|
8786
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
8787
|
+
}
|
|
8788
|
+
};
|
|
8789
|
+
while (work.length > 0) {
|
|
8790
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8791
|
+
for (const item of work.splice(0)) {
|
|
8792
|
+
const group = groups.get(item.call.childSessionId) ?? [];
|
|
8793
|
+
group.push(item);
|
|
8794
|
+
groups.set(item.call.childSessionId, group);
|
|
8795
|
+
}
|
|
8796
|
+
const groupResults = await Promise.all(
|
|
8797
|
+
[...groups].map(async ([sessionId, items]) => ({
|
|
8798
|
+
sessionId,
|
|
8799
|
+
items,
|
|
8800
|
+
messages: sessionId === null ? [] : await fetchChildWithoutBlocking(sessionId)
|
|
8801
|
+
}))
|
|
8802
|
+
);
|
|
8803
|
+
for (const { sessionId, items, messages: child } of groupResults) {
|
|
8804
|
+
if (sessionId !== null && child === null) continue;
|
|
8805
|
+
const attribution = sessionId === null ? { invocations: [], unattributed: [] } : attributeTaskCallUsage(
|
|
8806
|
+
child,
|
|
8807
|
+
items.map(({ call }) => ({
|
|
8808
|
+
callID: call.callID,
|
|
8809
|
+
timeStart: call.timeStart,
|
|
8810
|
+
timeEnd: call.timeEnd
|
|
8811
|
+
}))
|
|
8812
|
+
);
|
|
8813
|
+
if (sessionId !== null && attribution.unattributed.length > 0) {
|
|
8814
|
+
this.log({
|
|
8815
|
+
level: "warn",
|
|
8816
|
+
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`,
|
|
8817
|
+
message_id: messageId
|
|
8818
|
+
});
|
|
8819
|
+
}
|
|
8820
|
+
const usageByCall = new Map(
|
|
8821
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.usage])
|
|
8822
|
+
);
|
|
8823
|
+
const messagesByCall = new Map(
|
|
8824
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.messages])
|
|
8825
|
+
);
|
|
8826
|
+
for (const { call, depth } of items) {
|
|
8827
|
+
const usage = usageByCall.get(call.callID) ?? null;
|
|
8828
|
+
payload.push({
|
|
8829
|
+
tool_call_id: call.callID,
|
|
8830
|
+
agent_name: call.subagentName,
|
|
8831
|
+
opencode_session_id: call.childSessionId,
|
|
8832
|
+
parent_opencode_session_id: call.parentSessionId,
|
|
8833
|
+
depth,
|
|
8834
|
+
status: call.status,
|
|
8835
|
+
started_at: call.timeStart === null ? null : new Date(call.timeStart).toISOString(),
|
|
8836
|
+
ended_at: call.timeEnd === null ? null : new Date(call.timeEnd).toISOString(),
|
|
8837
|
+
usage_provider_id: usage?.usage_provider_id ?? call.model?.providerID ?? null,
|
|
8838
|
+
usage_model_id: usage?.usage_model_id ?? call.model?.modelID ?? null,
|
|
8839
|
+
usage_tokens_input: usage?.usage_tokens_input ?? null,
|
|
8840
|
+
usage_tokens_output: usage?.usage_tokens_output ?? null,
|
|
8841
|
+
usage_tokens_reasoning: usage?.usage_tokens_reasoning ?? null,
|
|
8842
|
+
usage_tokens_cache_read: usage?.usage_tokens_cache_read ?? null,
|
|
8843
|
+
usage_tokens_cache_write: usage?.usage_tokens_cache_write ?? null,
|
|
8844
|
+
usage_cost_usd: usage?.usage_cost_usd ?? null
|
|
8845
|
+
});
|
|
8846
|
+
for (const assigned of messagesByCall.get(call.callID) ?? []) {
|
|
8847
|
+
const parentId = assigned.info?.parentID ?? assigned.parentID;
|
|
8848
|
+
if (!parentId) continue;
|
|
8849
|
+
for (const nested of collectTaskCalls([assigned], parentId)) {
|
|
8850
|
+
if (seenCallIds.has(nested.callID)) continue;
|
|
8851
|
+
seenCallIds.add(nested.callID);
|
|
8852
|
+
work.push({ call: nested, depth: depth + 1 });
|
|
8853
|
+
}
|
|
8854
|
+
}
|
|
8855
|
+
}
|
|
8856
|
+
}
|
|
8857
|
+
}
|
|
8858
|
+
return payload.length > 0 ? payload : void 0;
|
|
8859
|
+
}
|
|
8021
8860
|
/**
|
|
8022
8861
|
* OpenCode's synchronous default session title (e.g.
|
|
8023
8862
|
* `"New session - 1737800000000"`), assigned immediately when a session is
|
|
@@ -8501,7 +9340,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8501
9340
|
* watcher retries next tick within the
|
|
8502
9341
|
* deadline, Finding 4).
|
|
8503
9342
|
*/
|
|
8504
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
|
|
9343
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
|
|
8505
9344
|
const res = await this.fetchImpl(
|
|
8506
9345
|
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
8507
9346
|
{
|
|
@@ -8517,15 +9356,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8517
9356
|
opencode_session_id: sessionId,
|
|
8518
9357
|
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
8519
9358
|
...title ? { title } : {},
|
|
8520
|
-
...usage ? usage : {}
|
|
9359
|
+
...usage ? usage : {},
|
|
9360
|
+
...usageAgentName ? { usage_agent_name: usageAgentName } : {},
|
|
9361
|
+
...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
|
|
8521
9362
|
})
|
|
8522
9363
|
}
|
|
8523
9364
|
);
|
|
8524
9365
|
this.assertAuth(res, "marking message as done");
|
|
8525
|
-
if (res.ok)
|
|
9366
|
+
if (res.ok) {
|
|
9367
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
9368
|
+
return;
|
|
9369
|
+
}
|
|
8526
9370
|
if (isRetryableStatus(res.status)) {
|
|
8527
9371
|
throw new Error(`marking message as done: HTTP ${res.status}`);
|
|
8528
9372
|
}
|
|
9373
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8529
9374
|
throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
|
|
8530
9375
|
}
|
|
8531
9376
|
/**
|
|
@@ -8540,7 +9385,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8540
9385
|
* exists but is wedged, so the next attempt must get a fresh one
|
|
8541
9386
|
* instead of reusing it — see WI-1's server-side null-clearing PATCH).
|
|
8542
9387
|
*/
|
|
8543
|
-
async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
|
|
9388
|
+
async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
|
|
8544
9389
|
const body = { status: "failed" };
|
|
8545
9390
|
if (sessionId === null) {
|
|
8546
9391
|
body.opencode_session_id = null;
|
|
@@ -8549,23 +9394,33 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8549
9394
|
}
|
|
8550
9395
|
if (error2 !== void 0) body.error = error2;
|
|
8551
9396
|
if (usage) Object.assign(body, usage);
|
|
9397
|
+
if (usageAgentName) body.usage_agent_name = usageAgentName;
|
|
9398
|
+
if (subagentInvocations && subagentInvocations.length > 0) {
|
|
9399
|
+
body.subagent_invocations = subagentInvocations;
|
|
9400
|
+
}
|
|
8552
9401
|
if (failure) {
|
|
8553
9402
|
body.failure_kind = failure.kind;
|
|
8554
9403
|
body.failure_provider_id = failure.providerId;
|
|
8555
9404
|
body.failure_model_id = failure.modelId;
|
|
8556
9405
|
body.failure_reason = failure.reason;
|
|
8557
9406
|
}
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
9407
|
+
try {
|
|
9408
|
+
await this.callWithRetry(
|
|
9409
|
+
"marking message as failed",
|
|
9410
|
+
() => this.fetchImpl(
|
|
9411
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
9412
|
+
{
|
|
9413
|
+
method: "PATCH",
|
|
9414
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
9415
|
+
body: JSON.stringify(body)
|
|
9416
|
+
}
|
|
9417
|
+
)
|
|
9418
|
+
);
|
|
9419
|
+
} catch (err) {
|
|
9420
|
+
if (err instanceof ChannelTerminalError) this.clearSubagentInvocationCaches(messageId);
|
|
9421
|
+
throw err;
|
|
9422
|
+
}
|
|
9423
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8569
9424
|
}
|
|
8570
9425
|
/**
|
|
8571
9426
|
* Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
|
|
@@ -8590,6 +9445,111 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8590
9445
|
reply?.info?.modelID ?? null
|
|
8591
9446
|
);
|
|
8592
9447
|
}
|
|
9448
|
+
async recordSubagentModelAuthFailure(failure, conversationId, messageId) {
|
|
9449
|
+
const providerId = failure.providerId ?? "(unknown)";
|
|
9450
|
+
try {
|
|
9451
|
+
const res = await this.fetchImpl(
|
|
9452
|
+
`${this.apiUrl}/runners/${this.agentId}/model-auth-failures`,
|
|
9453
|
+
{
|
|
9454
|
+
method: "POST",
|
|
9455
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
9456
|
+
body: JSON.stringify({
|
|
9457
|
+
provider_id: failure.providerId,
|
|
9458
|
+
model_id: failure.modelId,
|
|
9459
|
+
reason: failure.reason
|
|
9460
|
+
})
|
|
9461
|
+
}
|
|
9462
|
+
);
|
|
9463
|
+
if (!res.ok) {
|
|
9464
|
+
this.log({
|
|
9465
|
+
level: "warn",
|
|
9466
|
+
message: `Sub-agent model-auth report for provider ${providerId} (${failure.reason}) returned HTTP ${res.status} (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)})`,
|
|
9467
|
+
conversation_id: conversationId,
|
|
9468
|
+
message_id: messageId
|
|
9469
|
+
});
|
|
9470
|
+
}
|
|
9471
|
+
} catch (err) {
|
|
9472
|
+
this.log({
|
|
9473
|
+
level: "warn",
|
|
9474
|
+
message: `Sub-agent model-auth report for provider ${providerId} (${failure.reason}) failed with status network-error (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)}): ${err instanceof Error ? err.message : String(err)}`,
|
|
9475
|
+
conversation_id: conversationId,
|
|
9476
|
+
message_id: messageId
|
|
9477
|
+
});
|
|
9478
|
+
}
|
|
9479
|
+
}
|
|
9480
|
+
async clearSubagentModelAuthFailure(providerId, conversationId, messageId) {
|
|
9481
|
+
try {
|
|
9482
|
+
const res = await this.fetchImpl(
|
|
9483
|
+
`${this.apiUrl}/runners/${this.agentId}/model-auth-failures/${encodeURIComponent(providerId)}`,
|
|
9484
|
+
{
|
|
9485
|
+
method: "DELETE",
|
|
9486
|
+
headers: { Authorization: this.getAuthHeader() }
|
|
9487
|
+
}
|
|
9488
|
+
);
|
|
9489
|
+
if (!res.ok) {
|
|
9490
|
+
this.log({
|
|
9491
|
+
level: "warn",
|
|
9492
|
+
message: `Sub-agent model-auth clear for provider ${providerId} returned HTTP ${res.status} (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)})`,
|
|
9493
|
+
conversation_id: conversationId,
|
|
9494
|
+
message_id: messageId
|
|
9495
|
+
});
|
|
9496
|
+
}
|
|
9497
|
+
} catch (err) {
|
|
9498
|
+
this.log({
|
|
9499
|
+
level: "warn",
|
|
9500
|
+
message: `Sub-agent model-auth clear for provider ${providerId} failed with status network-error (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)}): ${err instanceof Error ? err.message : String(err)}`,
|
|
9501
|
+
conversation_id: conversationId,
|
|
9502
|
+
message_id: messageId
|
|
9503
|
+
});
|
|
9504
|
+
}
|
|
9505
|
+
}
|
|
9506
|
+
async reportSubagentAuthFailures(conversationId, opencodeMessageId, evidentMessageId, messages) {
|
|
9507
|
+
const refs = collectSubagentSessions(messages, opencodeMessageId);
|
|
9508
|
+
if (refs.length === 0) return;
|
|
9509
|
+
const failedProviders = /* @__PURE__ */ new Map();
|
|
9510
|
+
const succeededProviders = /* @__PURE__ */ new Set();
|
|
9511
|
+
for (const ref of refs) {
|
|
9512
|
+
try {
|
|
9513
|
+
const childMessages = await getSessionMessages(this.port, ref.sessionId);
|
|
9514
|
+
if (childMessages === null) {
|
|
9515
|
+
this.log({
|
|
9516
|
+
level: "debug",
|
|
9517
|
+
message: `Could not read sub-agent session ${ref.sessionId} while checking credential failures \u2014 skipping it`,
|
|
9518
|
+
conversation_id: conversationId,
|
|
9519
|
+
message_id: evidentMessageId
|
|
9520
|
+
});
|
|
9521
|
+
continue;
|
|
9522
|
+
}
|
|
9523
|
+
const outcome = findSubagentAuthOutcome(childMessages, ref.startedAtMs);
|
|
9524
|
+
if (!outcome) continue;
|
|
9525
|
+
if (outcome.outcome === "failed") {
|
|
9526
|
+
failedProviders.set(outcome.providerId, outcome.failure);
|
|
9527
|
+
} else {
|
|
9528
|
+
succeededProviders.add(outcome.providerId);
|
|
9529
|
+
}
|
|
9530
|
+
} catch (err) {
|
|
9531
|
+
this.log({
|
|
9532
|
+
level: "warn",
|
|
9533
|
+
message: `Failed to inspect sub-agent session ${ref.sessionId} for credential failures (conversation ${conversationId.slice(0, 8)}, message ${evidentMessageId.slice(0, 8)}): ${err instanceof Error ? err.message : String(err)}`,
|
|
9534
|
+
conversation_id: conversationId,
|
|
9535
|
+
message_id: evidentMessageId
|
|
9536
|
+
});
|
|
9537
|
+
}
|
|
9538
|
+
}
|
|
9539
|
+
for (const [providerId, failure] of failedProviders) {
|
|
9540
|
+
this.log({
|
|
9541
|
+
level: "warn",
|
|
9542
|
+
message: `Sub-agent turn failed on provider ${providerId} (${failure.reason}) \u2014 recording credential evidence`,
|
|
9543
|
+
conversation_id: conversationId,
|
|
9544
|
+
message_id: evidentMessageId
|
|
9545
|
+
});
|
|
9546
|
+
await this.recordSubagentModelAuthFailure(failure, conversationId, evidentMessageId);
|
|
9547
|
+
}
|
|
9548
|
+
for (const providerId of succeededProviders) {
|
|
9549
|
+
if (failedProviders.has(providerId)) continue;
|
|
9550
|
+
await this.clearSubagentModelAuthFailure(providerId, conversationId, evidentMessageId);
|
|
9551
|
+
}
|
|
9552
|
+
}
|
|
8593
9553
|
/**
|
|
8594
9554
|
* Best-effort, SINGLE-ATTEMPT runner→server telemetry ping
|
|
8595
9555
|
* (queued-followup-redrive). `POST .../messages/:id/signal {signal, ...extra}`
|
|
@@ -8743,6 +9703,13 @@ import chalk5 from "chalk";
|
|
|
8743
9703
|
import ora2 from "ora";
|
|
8744
9704
|
import { select as select2 } from "@inquirer/prompts";
|
|
8745
9705
|
var INTERACTIVE_START_TIMEOUT_MS = 3e4;
|
|
9706
|
+
function checkNonInteractivePortConflict(port, isPortInUseFn) {
|
|
9707
|
+
if (isPortInUseFn(port)) {
|
|
9708
|
+
throw new Error(
|
|
9709
|
+
`Port ${port} is already in use by a non-OpenCode process. Free it or pass --port.`
|
|
9710
|
+
);
|
|
9711
|
+
}
|
|
9712
|
+
}
|
|
8746
9713
|
async function ensureOpenCodeRunning(ctx) {
|
|
8747
9714
|
const healthCheck = await checkOpenCodeHealth(ctx.port);
|
|
8748
9715
|
if (healthCheck.healthy) {
|
|
@@ -8790,6 +9757,7 @@ async function ensureOpenCodeRunning(ctx) {
|
|
|
8790
9757
|
}
|
|
8791
9758
|
}
|
|
8792
9759
|
if (!ctx.interactive) {
|
|
9760
|
+
checkNonInteractivePortConflict(ctx.port, isPortInUse);
|
|
8793
9761
|
ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
|
|
8794
9762
|
const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
|
|
8795
9763
|
const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
|
|
@@ -8871,9 +9839,119 @@ Port ${port} is already in use.`));
|
|
|
8871
9839
|
return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
|
|
8872
9840
|
}
|
|
8873
9841
|
|
|
9842
|
+
// src/commands/ensure-opencode-v2.ts
|
|
9843
|
+
import chalk6 from "chalk";
|
|
9844
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
9845
|
+
async function probeOpenCode2WithoutPassword(port) {
|
|
9846
|
+
try {
|
|
9847
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
9848
|
+
signal: AbortSignal.timeout(2e3)
|
|
9849
|
+
});
|
|
9850
|
+
if (response.status === 401) {
|
|
9851
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
9852
|
+
}
|
|
9853
|
+
if (!response.ok) {
|
|
9854
|
+
return { healthy: false, error: `HTTP ${response.status}` };
|
|
9855
|
+
}
|
|
9856
|
+
return { healthy: true };
|
|
9857
|
+
} catch (error2) {
|
|
9858
|
+
return {
|
|
9859
|
+
healthy: false,
|
|
9860
|
+
error: error2 instanceof Error ? error2.message : "Unknown error"
|
|
9861
|
+
};
|
|
9862
|
+
}
|
|
9863
|
+
}
|
|
9864
|
+
function unknownPasswordError(port) {
|
|
9865
|
+
return new Error(
|
|
9866
|
+
`OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
|
|
9867
|
+
);
|
|
9868
|
+
}
|
|
9869
|
+
function v2SessionSupportIncompleteError() {
|
|
9870
|
+
return new Error(
|
|
9871
|
+
"OpenCode V2 (opencode2) session support is incomplete \u2014 see https://github.com/sroze/evident/issues/2075. Not starting a new opencode2 process."
|
|
9872
|
+
);
|
|
9873
|
+
}
|
|
9874
|
+
async function ensureOpenCode2Running(ctx) {
|
|
9875
|
+
const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
|
|
9876
|
+
if (initialHealth.authFailed) {
|
|
9877
|
+
throw unknownPasswordError(ctx.port);
|
|
9878
|
+
}
|
|
9879
|
+
if (initialHealth.healthy) {
|
|
9880
|
+
return {
|
|
9881
|
+
port: ctx.port,
|
|
9882
|
+
process: null,
|
|
9883
|
+
version: null,
|
|
9884
|
+
notReadyReason: null,
|
|
9885
|
+
password: null
|
|
9886
|
+
};
|
|
9887
|
+
}
|
|
9888
|
+
if (!isOpenCode2Installed()) {
|
|
9889
|
+
throw new Error(
|
|
9890
|
+
"OpenCode V2 (opencode2) is not installed. Install it with: npm install -g @opencode-ai/cli@beta"
|
|
9891
|
+
);
|
|
9892
|
+
}
|
|
9893
|
+
let port = ctx.port;
|
|
9894
|
+
if (!ctx.interactive) {
|
|
9895
|
+
checkNonInteractivePortConflict(port, isPortInUse);
|
|
9896
|
+
} else if (isPortInUse(port)) {
|
|
9897
|
+
console.log(chalk6.yellow(`
|
|
9898
|
+
Port ${port} is already in use.`));
|
|
9899
|
+
const alternativePort = findAvailablePort(port + 1);
|
|
9900
|
+
if (alternativePort) {
|
|
9901
|
+
const useAlternative = await select3({
|
|
9902
|
+
message: `Use port ${alternativePort} instead?`,
|
|
9903
|
+
choices: [
|
|
9904
|
+
{ name: `Yes, use port ${alternativePort}`, value: "yes" },
|
|
9905
|
+
{ name: "No, I will free the port manually", value: "no" }
|
|
9906
|
+
]
|
|
9907
|
+
});
|
|
9908
|
+
if (useAlternative === "yes") {
|
|
9909
|
+
port = alternativePort;
|
|
9910
|
+
} else {
|
|
9911
|
+
throw new Error(`Port ${ctx.port} is in use`);
|
|
9912
|
+
}
|
|
9913
|
+
}
|
|
9914
|
+
}
|
|
9915
|
+
if (!ctx.interactive) {
|
|
9916
|
+
throw v2SessionSupportIncompleteError();
|
|
9917
|
+
}
|
|
9918
|
+
console.log(chalk6.yellow(`
|
|
9919
|
+
${v2SessionSupportIncompleteError().message}`));
|
|
9920
|
+
const action = await select3({
|
|
9921
|
+
message: "OpenCode V2 is not running. What would you like to do?",
|
|
9922
|
+
choices: [
|
|
9923
|
+
{
|
|
9924
|
+
name: "Show me the command",
|
|
9925
|
+
value: "manual",
|
|
9926
|
+
description: "Display the command to run manually"
|
|
9927
|
+
},
|
|
9928
|
+
{
|
|
9929
|
+
name: "Continue without OpenCode V2",
|
|
9930
|
+
value: "continue",
|
|
9931
|
+
description: "Requests will fail until OpenCode V2 starts"
|
|
9932
|
+
}
|
|
9933
|
+
]
|
|
9934
|
+
});
|
|
9935
|
+
if (action === "manual") {
|
|
9936
|
+
blank();
|
|
9937
|
+
console.log(chalk6.bold("Run this command in another terminal:"));
|
|
9938
|
+
blank();
|
|
9939
|
+
console.log(` ${chalk6.cyan(`opencode2 serve --port ${port}`)}`);
|
|
9940
|
+
blank();
|
|
9941
|
+
throw new Error("Please start OpenCode V2 manually");
|
|
9942
|
+
}
|
|
9943
|
+
return {
|
|
9944
|
+
port,
|
|
9945
|
+
process: null,
|
|
9946
|
+
version: null,
|
|
9947
|
+
notReadyReason: "you chose to continue without OpenCode V2",
|
|
9948
|
+
password: null
|
|
9949
|
+
};
|
|
9950
|
+
}
|
|
9951
|
+
|
|
8874
9952
|
// src/lib/runner-credentials.ts
|
|
8875
|
-
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
8876
|
-
import { spawn as spawn5 } from "child_process";
|
|
9953
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
9954
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
8877
9955
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
8878
9956
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
8879
9957
|
var GITHUB_PROBE_TIMEOUT_MS = 1e4;
|
|
@@ -9145,11 +10223,11 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
9145
10223
|
}
|
|
9146
10224
|
|
|
9147
10225
|
// src/lib/opencode/config-overlay.ts
|
|
9148
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
9149
|
-
import { copyFileSync, existsSync as existsSync2, statSync as
|
|
9150
|
-
import { isAbsolute as isAbsolute2, join as
|
|
10226
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
10227
|
+
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
10228
|
+
import { isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "node:path";
|
|
9151
10229
|
function isFile(filePath) {
|
|
9152
|
-
return existsSync2(filePath) &&
|
|
10230
|
+
return existsSync2(filePath) && statSync6(filePath).isFile();
|
|
9153
10231
|
}
|
|
9154
10232
|
function applyRunnerOpenCodeConfig({
|
|
9155
10233
|
overlayPath,
|
|
@@ -9161,7 +10239,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9161
10239
|
return;
|
|
9162
10240
|
}
|
|
9163
10241
|
const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
|
|
9164
|
-
const target = isFile(
|
|
10242
|
+
const target = isFile(join9(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
9165
10243
|
if (!isFile(source)) {
|
|
9166
10244
|
log3(
|
|
9167
10245
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -9169,7 +10247,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9169
10247
|
);
|
|
9170
10248
|
return;
|
|
9171
10249
|
}
|
|
9172
|
-
copyFileSync(source,
|
|
10250
|
+
copyFileSync(source, join9(cwd, target));
|
|
9173
10251
|
try {
|
|
9174
10252
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
9175
10253
|
stdio: "ignore"
|
|
@@ -9178,11 +10256,11 @@ function applyRunnerOpenCodeConfig({
|
|
|
9178
10256
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
9179
10257
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
9180
10258
|
}
|
|
9181
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
10259
|
+
log3(`Applied runner OpenCode config ${source} to ${join9(cwd, target)}`);
|
|
9182
10260
|
}
|
|
9183
10261
|
|
|
9184
10262
|
// src/lib/credential-sync.ts
|
|
9185
|
-
import { renameSync, writeFileSync as writeFileSync5 } from "fs";
|
|
10263
|
+
import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
9186
10264
|
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
9187
10265
|
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
9188
10266
|
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
@@ -9192,7 +10270,7 @@ var MAX_FLUSH_PASSES = 2;
|
|
|
9192
10270
|
function outcomesWith(outcome) {
|
|
9193
10271
|
return { claude: outcome, opencode: outcome };
|
|
9194
10272
|
}
|
|
9195
|
-
function
|
|
10273
|
+
function errorMessage2(error2) {
|
|
9196
10274
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
9197
10275
|
}
|
|
9198
10276
|
function waitForSettlement(promise, timeoutMs) {
|
|
@@ -9219,7 +10297,7 @@ function writeMarker(markerPath, outcomes, log3) {
|
|
|
9219
10297
|
writeFileSync5(temporaryPath, body, { mode: 384 });
|
|
9220
10298
|
renameSync(temporaryPath, markerPath);
|
|
9221
10299
|
} catch (error2) {
|
|
9222
|
-
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${
|
|
10300
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
|
|
9223
10301
|
}
|
|
9224
10302
|
}
|
|
9225
10303
|
function intervalSeconds(env, log3) {
|
|
@@ -9251,7 +10329,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
|
9251
10329
|
},
|
|
9252
10330
|
(error2) => {
|
|
9253
10331
|
failed = true;
|
|
9254
|
-
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${
|
|
10332
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
|
|
9255
10333
|
}
|
|
9256
10334
|
);
|
|
9257
10335
|
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
@@ -9311,7 +10389,7 @@ function createCredentialSync({
|
|
|
9311
10389
|
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
9312
10390
|
} catch (error2) {
|
|
9313
10391
|
outcomes[store] = "failed";
|
|
9314
|
-
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${
|
|
10392
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
|
|
9315
10393
|
}
|
|
9316
10394
|
}
|
|
9317
10395
|
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
@@ -9453,7 +10531,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9453
10531
|
if (trimmed === "") {
|
|
9454
10532
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
9455
10533
|
}
|
|
9456
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
10534
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join10(homeDir, trimmed.slice(2)) : trimmed;
|
|
9457
10535
|
if (!isAbsolute3(expanded)) {
|
|
9458
10536
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
9459
10537
|
}
|
|
@@ -9477,6 +10555,28 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9477
10555
|
var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
|
|
9478
10556
|
var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
|
|
9479
10557
|
var OPENCODE_START_TIMEOUT_ENV = "EVIDENT_OPENCODE_START_TIMEOUT";
|
|
10558
|
+
var OPENCODE_VERSION_ENV = "EVIDENT_OPENCODE_VERSION";
|
|
10559
|
+
function resolveOpenCodeVersion(options, env = process.env) {
|
|
10560
|
+
let raw;
|
|
10561
|
+
let source;
|
|
10562
|
+
if (options.opencodeVersion !== void 0) {
|
|
10563
|
+
raw = options.opencodeVersion;
|
|
10564
|
+
source = "--opencode-version";
|
|
10565
|
+
} else if (env[OPENCODE_VERSION_ENV] !== void 0 && env[OPENCODE_VERSION_ENV] !== "") {
|
|
10566
|
+
raw = env[OPENCODE_VERSION_ENV];
|
|
10567
|
+
source = OPENCODE_VERSION_ENV;
|
|
10568
|
+
} else {
|
|
10569
|
+
return { version: "v1", warnings: [] };
|
|
10570
|
+
}
|
|
10571
|
+
const normalized = raw.trim().toLowerCase();
|
|
10572
|
+
if (normalized !== "v1" && normalized !== "v2") {
|
|
10573
|
+
return {
|
|
10574
|
+
version: "v1",
|
|
10575
|
+
warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or v2; using the default v1`]
|
|
10576
|
+
};
|
|
10577
|
+
}
|
|
10578
|
+
return { version: normalized, warnings: [] };
|
|
10579
|
+
}
|
|
9480
10580
|
function resolveOpenCodeStartTimeoutMs(options, env = process.env) {
|
|
9481
10581
|
const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1e3;
|
|
9482
10582
|
let raw;
|
|
@@ -9543,7 +10643,7 @@ function log2(state, message, level = "info") {
|
|
|
9543
10643
|
})
|
|
9544
10644
|
);
|
|
9545
10645
|
} else if (!state.interactive) {
|
|
9546
|
-
const prefix = level === "error" ?
|
|
10646
|
+
const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
|
|
9547
10647
|
console.log(`${prefix} ${message}`);
|
|
9548
10648
|
}
|
|
9549
10649
|
}
|
|
@@ -9573,7 +10673,7 @@ function logActivity(state, entry) {
|
|
|
9573
10673
|
}
|
|
9574
10674
|
function reportSessionDbRecovery(state) {
|
|
9575
10675
|
try {
|
|
9576
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
10676
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
|
|
9577
10677
|
for (const record of report.records) {
|
|
9578
10678
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
9579
10679
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -9604,18 +10704,18 @@ function reportSessionDbRecoveryRecord(state, record) {
|
|
|
9604
10704
|
function displayStatus(state) {
|
|
9605
10705
|
if (!state.interactive) return;
|
|
9606
10706
|
const attempt = state.connection?.reconnectAttempt ?? 0;
|
|
9607
|
-
const tunnel = state.connected ?
|
|
9608
|
-
const opencode = state.opencodeConnected ?
|
|
9609
|
-
const messages = state.messageCount > 0 ?
|
|
10707
|
+
const tunnel = state.connected ? chalk7.green("tunnel: connected") : attempt > 0 ? chalk7.yellow(`tunnel: reconnecting (#${attempt})`) : chalk7.yellow("tunnel: connecting");
|
|
10708
|
+
const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
|
|
10709
|
+
const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
|
|
9610
10710
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
9611
|
-
const detail = last ?
|
|
10711
|
+
const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
|
|
9612
10712
|
const agent = state.agentName ?? state.agentId;
|
|
9613
10713
|
console.log(
|
|
9614
|
-
`${
|
|
10714
|
+
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
9615
10715
|
);
|
|
9616
10716
|
}
|
|
9617
10717
|
async function promptForLogin(promptMessage, successMessage) {
|
|
9618
|
-
const action = await
|
|
10718
|
+
const action = await select4({
|
|
9619
10719
|
message: promptMessage,
|
|
9620
10720
|
choices: [
|
|
9621
10721
|
{
|
|
@@ -9631,7 +10731,7 @@ async function promptForLogin(promptMessage, successMessage) {
|
|
|
9631
10731
|
]
|
|
9632
10732
|
});
|
|
9633
10733
|
if (action === "exit") {
|
|
9634
|
-
console.log(
|
|
10734
|
+
console.log(chalk7.dim(`
|
|
9635
10735
|
You can log in later by running: ${getCliName()} login`));
|
|
9636
10736
|
process.exit(0);
|
|
9637
10737
|
}
|
|
@@ -9642,7 +10742,7 @@ You can log in later by running: ${getCliName()} login`));
|
|
|
9642
10742
|
process.exit(1);
|
|
9643
10743
|
}
|
|
9644
10744
|
blank();
|
|
9645
|
-
console.log(
|
|
10745
|
+
console.log(chalk7.green(successMessage));
|
|
9646
10746
|
blank();
|
|
9647
10747
|
return { token: credentials2.token, authType: "bearer", user: credentials2.user };
|
|
9648
10748
|
}
|
|
@@ -9655,12 +10755,12 @@ async function handleAuthError(state, error2) {
|
|
|
9655
10755
|
if (state.interactive) displayStatus(state);
|
|
9656
10756
|
if (!state.interactive) {
|
|
9657
10757
|
blank();
|
|
9658
|
-
console.log(
|
|
9659
|
-
console.log(
|
|
10758
|
+
console.log(chalk7.red("Authentication expired"));
|
|
10759
|
+
console.log(chalk7.dim("Your authentication token is no longer valid."));
|
|
9660
10760
|
blank();
|
|
9661
|
-
console.log(
|
|
9662
|
-
console.log(
|
|
9663
|
-
console.log(
|
|
10761
|
+
console.log(chalk7.dim("To fix this:"));
|
|
10762
|
+
console.log(chalk7.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
|
|
10763
|
+
console.log(chalk7.dim(" 2. Restart this command"));
|
|
9664
10764
|
blank();
|
|
9665
10765
|
await cleanup(state);
|
|
9666
10766
|
await shutdownTelemetry();
|
|
@@ -9668,7 +10768,7 @@ async function handleAuthError(state, error2) {
|
|
|
9668
10768
|
return { success: false };
|
|
9669
10769
|
}
|
|
9670
10770
|
blank();
|
|
9671
|
-
console.log(
|
|
10771
|
+
console.log(chalk7.yellow("Your authentication has expired."));
|
|
9672
10772
|
blank();
|
|
9673
10773
|
try {
|
|
9674
10774
|
const credentials2 = await promptForLogin(
|
|
@@ -9732,6 +10832,14 @@ async function driveChannels(state, driver) {
|
|
|
9732
10832
|
const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
|
|
9733
10833
|
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
9734
10834
|
if (opencodeAuthApplied) state.openaiUsageRearm?.();
|
|
10835
|
+
if (claudeCredentialApplied || opencodeAuthApplied) {
|
|
10836
|
+
void reloadProviderCache(state.port).catch(
|
|
10837
|
+
(error2) => logActivity(state, {
|
|
10838
|
+
type: "error",
|
|
10839
|
+
error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
10840
|
+
})
|
|
10841
|
+
);
|
|
10842
|
+
}
|
|
9735
10843
|
if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
|
|
9736
10844
|
idlePolls = 0;
|
|
9737
10845
|
idleMs = 0;
|
|
@@ -9759,8 +10867,8 @@ async function driveChannels(state, driver) {
|
|
|
9759
10867
|
state.running = false;
|
|
9760
10868
|
break;
|
|
9761
10869
|
}
|
|
9762
|
-
const
|
|
9763
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
10870
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
|
|
10871
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage3}` });
|
|
9764
10872
|
if (state.interactive) displayStatus(state);
|
|
9765
10873
|
if (driver.hasInFlightWatchers()) {
|
|
9766
10874
|
consecutiveDrainFailures = 0;
|
|
@@ -9798,9 +10906,18 @@ async function driveChannels(state, driver) {
|
|
|
9798
10906
|
}
|
|
9799
10907
|
}
|
|
9800
10908
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
9801
|
-
var SESSION_DB_RECLAIM_MAX_PAGES =
|
|
10909
|
+
var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
|
|
10910
|
+
function shouldWarnForReclaimSkip(reason) {
|
|
10911
|
+
if (reason !== "sqlite-unavailable") return false;
|
|
10912
|
+
const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
|
|
10913
|
+
if (!version2) return false;
|
|
10914
|
+
const major = Number(version2[1]);
|
|
10915
|
+
const minor = Number(version2[2]);
|
|
10916
|
+
const patch = Number(version2[3]);
|
|
10917
|
+
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
10918
|
+
}
|
|
9802
10919
|
function sessionDbPath() {
|
|
9803
|
-
return
|
|
10920
|
+
return join10(homedir6(), ".local", "share", "opencode", "opencode.db");
|
|
9804
10921
|
}
|
|
9805
10922
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
9806
10923
|
const record = {
|
|
@@ -9896,7 +11013,7 @@ async function runSweep(state, driver, config) {
|
|
|
9896
11013
|
} else {
|
|
9897
11014
|
logActivity(state, {
|
|
9898
11015
|
type: "info",
|
|
9899
|
-
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})`
|
|
11016
|
+
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : "")
|
|
9900
11017
|
});
|
|
9901
11018
|
}
|
|
9902
11019
|
} catch (error2) {
|
|
@@ -9919,13 +11036,20 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
9919
11036
|
for (const warning2 of config.warnings) {
|
|
9920
11037
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
9921
11038
|
}
|
|
9922
|
-
const dbBytes = statSessionDbBytes(
|
|
11039
|
+
const dbBytes = statSessionDbBytes(homedir6());
|
|
9923
11040
|
void (async () => {
|
|
9924
|
-
const
|
|
11041
|
+
const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
11042
|
+
if (reclaimAvailability !== null) {
|
|
11043
|
+
logActivity(state, {
|
|
11044
|
+
type: "info",
|
|
11045
|
+
level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
|
|
11046
|
+
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
|
|
11047
|
+
});
|
|
11048
|
+
}
|
|
9925
11049
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
9926
11050
|
dbBytes,
|
|
9927
11051
|
cleanupEnabled: config.enabled,
|
|
9928
|
-
reclaimSkipReason
|
|
11052
|
+
reclaimSkipReason: reclaimAvailability?.reason ?? null
|
|
9929
11053
|
});
|
|
9930
11054
|
if (sizeWarning !== null) {
|
|
9931
11055
|
logActivity(state, { type: "info", level: "warn", message: sizeWarning });
|
|
@@ -10092,14 +11216,11 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
10092
11216
|
report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
|
|
10093
11217
|
isLocalCredentialProblem,
|
|
10094
11218
|
forcedOnHint: "run `claude` to sign in",
|
|
10095
|
-
firstDelayMs:
|
|
10096
|
-
nextDelayMs:
|
|
10097
|
-
failureLogLevel:
|
|
11219
|
+
firstDelayMs: firstReportDelayMs,
|
|
11220
|
+
nextDelayMs: usageReportDelayMs,
|
|
11221
|
+
failureLogLevel: usageReportFailureLogLevel
|
|
10098
11222
|
});
|
|
10099
11223
|
}
|
|
10100
|
-
var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
|
|
10101
|
-
var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
10102
|
-
var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
10103
11224
|
function scheduleResourceUsageReporting(state, options) {
|
|
10104
11225
|
const { enabled, warnings } = resolveResourceUsageReportingEnabled(
|
|
10105
11226
|
options.resourceUsageReporting,
|
|
@@ -10120,7 +11241,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10120
11241
|
});
|
|
10121
11242
|
return;
|
|
10122
11243
|
}
|
|
10123
|
-
const { collect, stop } = createResourceUsageCollector(
|
|
11244
|
+
const { collect, stop } = createResourceUsageCollector(homedir6());
|
|
10124
11245
|
state.stopResourceUsageSampling = stop;
|
|
10125
11246
|
let consecutiveFailures = 0;
|
|
10126
11247
|
const tick = async () => {
|
|
@@ -10152,10 +11273,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10152
11273
|
consecutiveFailures++;
|
|
10153
11274
|
logActivity(state, {
|
|
10154
11275
|
type: "info",
|
|
10155
|
-
level:
|
|
10156
|
-
consecutiveFailures,
|
|
10157
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
10158
|
-
),
|
|
11276
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
10159
11277
|
message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
|
|
10160
11278
|
});
|
|
10161
11279
|
}
|
|
@@ -10164,20 +11282,11 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10164
11282
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
10165
11283
|
logActivity(state, {
|
|
10166
11284
|
type: "info",
|
|
10167
|
-
level:
|
|
10168
|
-
consecutiveFailures,
|
|
10169
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
10170
|
-
),
|
|
11285
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
10171
11286
|
message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
|
|
10172
11287
|
});
|
|
10173
11288
|
} finally {
|
|
10174
|
-
state.resourceUsageTimer = setTimeout(
|
|
10175
|
-
() => void tick(),
|
|
10176
|
-
jitteredDelayMs(
|
|
10177
|
-
RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
|
|
10178
|
-
RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
|
|
10179
|
-
)
|
|
10180
|
-
);
|
|
11289
|
+
state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
|
|
10181
11290
|
}
|
|
10182
11291
|
};
|
|
10183
11292
|
state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
|
|
@@ -10217,6 +11326,8 @@ async function cleanup(state, opts = {}) {
|
|
|
10217
11326
|
clearTimeout(timer);
|
|
10218
11327
|
}
|
|
10219
11328
|
state.sessionCleanupTimers = [];
|
|
11329
|
+
state.stopOpenCodeLogTail?.();
|
|
11330
|
+
state.stopOpenCodeLogTail = null;
|
|
10220
11331
|
if (state.claudeUsageTimer) {
|
|
10221
11332
|
clearTimeout(state.claudeUsageTimer);
|
|
10222
11333
|
state.claudeUsageTimer = null;
|
|
@@ -10355,7 +11466,7 @@ async function run(options) {
|
|
|
10355
11466
|
let fileSyncDirectories;
|
|
10356
11467
|
try {
|
|
10357
11468
|
logLevel = resolveLogLevel(options);
|
|
10358
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
11469
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
|
|
10359
11470
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
10360
11471
|
throw new Error(
|
|
10361
11472
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -10384,8 +11495,10 @@ async function run(options) {
|
|
|
10384
11495
|
connected: false,
|
|
10385
11496
|
opencodeConnected: false,
|
|
10386
11497
|
opencodeVersion: null,
|
|
11498
|
+
opencodeApiVersion: "v1",
|
|
10387
11499
|
sessionDbProvenanceAnomaly: false,
|
|
10388
11500
|
opencodeProcess: null,
|
|
11501
|
+
stopOpenCodeLogTail: null,
|
|
10389
11502
|
litestreamProcess: null,
|
|
10390
11503
|
connection: null,
|
|
10391
11504
|
channelDriver: null,
|
|
@@ -10453,15 +11566,15 @@ async function run(options) {
|
|
|
10453
11566
|
printError("Authentication required");
|
|
10454
11567
|
blank();
|
|
10455
11568
|
console.log(
|
|
10456
|
-
|
|
11569
|
+
chalk7.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
|
|
10457
11570
|
);
|
|
10458
|
-
console.log(
|
|
11571
|
+
console.log(chalk7.dim("Or run `evident login` for interactive authentication"));
|
|
10459
11572
|
blank();
|
|
10460
11573
|
process.exit(1);
|
|
10461
11574
|
return;
|
|
10462
11575
|
}
|
|
10463
11576
|
blank();
|
|
10464
|
-
console.log(
|
|
11577
|
+
console.log(chalk7.yellow("You are not logged in to Evident."));
|
|
10465
11578
|
blank();
|
|
10466
11579
|
credentials2 = await promptForLogin(
|
|
10467
11580
|
"Would you like to log in now?",
|
|
@@ -10511,7 +11624,7 @@ async function run(options) {
|
|
|
10511
11624
|
);
|
|
10512
11625
|
blank();
|
|
10513
11626
|
console.log(
|
|
10514
|
-
|
|
11627
|
+
chalk7.dim(
|
|
10515
11628
|
"Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
|
|
10516
11629
|
)
|
|
10517
11630
|
);
|
|
@@ -10534,15 +11647,15 @@ async function run(options) {
|
|
|
10534
11647
|
);
|
|
10535
11648
|
if (interactive && !state.json) {
|
|
10536
11649
|
blank();
|
|
10537
|
-
console.log(
|
|
10538
|
-
console.log(
|
|
11650
|
+
console.log(chalk7.bold("Evident Run"));
|
|
11651
|
+
console.log(chalk7.dim("-".repeat(40)));
|
|
10539
11652
|
}
|
|
10540
11653
|
const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
|
|
10541
11654
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
10542
11655
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
10543
11656
|
spinner?.fail("Authentication failed");
|
|
10544
11657
|
blank();
|
|
10545
|
-
console.log(
|
|
11658
|
+
console.log(chalk7.yellow("Your authentication token is invalid or expired."));
|
|
10546
11659
|
blank();
|
|
10547
11660
|
credentials2 = await promptForLogin(
|
|
10548
11661
|
"Would you like to log in again?",
|
|
@@ -10590,6 +11703,13 @@ async function run(options) {
|
|
|
10590
11703
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
10591
11704
|
}
|
|
10592
11705
|
state.credentialSync?.arm();
|
|
11706
|
+
state.stopOpenCodeLogTail = tailOpenCodeLogFile(
|
|
11707
|
+
resolveOpenCodeLogPath(homedir6(), process.env),
|
|
11708
|
+
createOpenCodeActivityForwarder(() => ({
|
|
11709
|
+
agentId: state.agentId,
|
|
11710
|
+
authHeader: state.authHeader
|
|
11711
|
+
}))
|
|
11712
|
+
).stop;
|
|
10593
11713
|
let sessionDbVerifyFatal = false;
|
|
10594
11714
|
if (!options.restoreSessionDb) {
|
|
10595
11715
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -10639,6 +11759,13 @@ async function run(options) {
|
|
|
10639
11759
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
10640
11760
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
10641
11761
|
}
|
|
11762
|
+
const { version: opencodeVersion, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(
|
|
11763
|
+
options,
|
|
11764
|
+
process.env
|
|
11765
|
+
);
|
|
11766
|
+
for (const warning2 of opencodeVersionWarnings) {
|
|
11767
|
+
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
11768
|
+
}
|
|
10642
11769
|
const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
|
|
10643
11770
|
for (const warning2 of maxActiveSessionsWarnings) {
|
|
10644
11771
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
@@ -10646,7 +11773,14 @@ async function run(options) {
|
|
|
10646
11773
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
10647
11774
|
const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
|
|
10648
11775
|
try {
|
|
10649
|
-
const oc = await
|
|
11776
|
+
const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
|
|
11777
|
+
port: state.port,
|
|
11778
|
+
interactive: state.interactive,
|
|
11779
|
+
agentId: state.agentId,
|
|
11780
|
+
log: (message) => log2(state, message),
|
|
11781
|
+
startTimeoutMs: opencodeStartTimeoutMs,
|
|
11782
|
+
inheritStdio: Boolean(options.opencodePidFile)
|
|
11783
|
+
}) : await ensureOpenCodeRunning({
|
|
10650
11784
|
port: state.port,
|
|
10651
11785
|
interactive: state.interactive,
|
|
10652
11786
|
agentId: state.agentId,
|
|
@@ -10657,6 +11791,7 @@ async function run(options) {
|
|
|
10657
11791
|
state.port = oc.port;
|
|
10658
11792
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
10659
11793
|
state.opencodeVersion = oc.version;
|
|
11794
|
+
state.opencodeApiVersion = opencodeVersion;
|
|
10660
11795
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
10661
11796
|
try {
|
|
10662
11797
|
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
@@ -10673,7 +11808,7 @@ async function run(options) {
|
|
|
10673
11808
|
const provenance = checkSessionDbProvenance({
|
|
10674
11809
|
dbPath: sessionDbPath(),
|
|
10675
11810
|
currentVersion: state.opencodeVersion,
|
|
10676
|
-
homeDir:
|
|
11811
|
+
homeDir: homedir6(),
|
|
10677
11812
|
env: process.env
|
|
10678
11813
|
});
|
|
10679
11814
|
if (provenance.anomaly) {
|
|
@@ -10693,13 +11828,17 @@ async function run(options) {
|
|
|
10693
11828
|
const message = `OpenCode is not ready on port ${state.port}: ${oc.notReadyReason}. The runner will still come online, but messages will fail until opencode answers \u2014 raise the wait with --opencode-start-timeout <seconds> (env ${OPENCODE_START_TIMEOUT_ENV}).`;
|
|
10694
11829
|
logActivity(state, { type: "info", level: "warn", message });
|
|
10695
11830
|
} else {
|
|
10696
|
-
const versionWarning = buildOpenCodeVersionWarning(
|
|
11831
|
+
const versionWarning = buildOpenCodeVersionWarning(
|
|
11832
|
+
state.opencodeVersion,
|
|
11833
|
+
state.opencodeApiVersion
|
|
11834
|
+
);
|
|
10697
11835
|
if (versionWarning) {
|
|
10698
11836
|
log2(state, versionWarning, "warn");
|
|
10699
11837
|
if (state.interactive && !state.json) {
|
|
10700
11838
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
10701
11839
|
}
|
|
10702
11840
|
}
|
|
11841
|
+
await reloadProviderCache(state.port);
|
|
10703
11842
|
const noProviderWarning = buildNoProviderWarning(
|
|
10704
11843
|
await hasAnyConfiguredProvider(state.port)
|
|
10705
11844
|
);
|
|
@@ -10708,10 +11847,10 @@ async function run(options) {
|
|
|
10708
11847
|
if (state.interactive && !state.json) {
|
|
10709
11848
|
logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
|
|
10710
11849
|
blank();
|
|
10711
|
-
console.log(
|
|
11850
|
+
console.log(chalk7.yellow("\u26A0 No OpenCode model provider is configured."));
|
|
10712
11851
|
console.log(
|
|
10713
|
-
|
|
10714
|
-
`Run ${
|
|
11852
|
+
chalk7.dim(
|
|
11853
|
+
`Run ${chalk7.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
|
|
10715
11854
|
)
|
|
10716
11855
|
);
|
|
10717
11856
|
blank();
|
|
@@ -10835,7 +11974,7 @@ async function run(options) {
|
|
|
10835
11974
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
10836
11975
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
10837
11976
|
fileSyncDirectories,
|
|
10838
|
-
homeDir:
|
|
11977
|
+
homeDir: homedir6(),
|
|
10839
11978
|
maxActiveSessions,
|
|
10840
11979
|
log: (entry) => (
|
|
10841
11980
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -10878,7 +12017,11 @@ async function run(options) {
|
|
|
10878
12017
|
emitAgentConnected(state.agentId, {
|
|
10879
12018
|
port: state.port,
|
|
10880
12019
|
cli_version: getCliVersion(),
|
|
10881
|
-
opencode_version:
|
|
12020
|
+
opencode_version: reportedOpenCodeVersion({
|
|
12021
|
+
version: state.opencodeVersion,
|
|
12022
|
+
major: state.opencodeApiVersion,
|
|
12023
|
+
connected: state.opencodeConnected
|
|
12024
|
+
})
|
|
10882
12025
|
});
|
|
10883
12026
|
if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
|
|
10884
12027
|
if (state.interactive) displayStatus(state);
|
|
@@ -10961,6 +12104,18 @@ async function run(options) {
|
|
|
10961
12104
|
if (state.interactive) displayStatus(state);
|
|
10962
12105
|
});
|
|
10963
12106
|
},
|
|
12107
|
+
// Both loops are rearmed because `rearm()` is idempotent for the
|
|
12108
|
+
// provider that did not just connect, and is a no-op when reporting is off.
|
|
12109
|
+
onUsageRearmPing: () => {
|
|
12110
|
+
if (!state.running) return;
|
|
12111
|
+
logActivity(state, {
|
|
12112
|
+
type: "info",
|
|
12113
|
+
level: "debug",
|
|
12114
|
+
message: "Usage rearm ping received"
|
|
12115
|
+
});
|
|
12116
|
+
state.claudeUsageRearm?.();
|
|
12117
|
+
state.openaiUsageRearm?.();
|
|
12118
|
+
},
|
|
10964
12119
|
onInfo: (message) => logActivity(state, { type: "info", message })
|
|
10965
12120
|
}
|
|
10966
12121
|
});
|
|
@@ -11065,6 +12220,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
11065
12220
|
).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(
|
|
11066
12221
|
"--opencode-start-timeout <seconds>",
|
|
11067
12222
|
"Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
|
|
12223
|
+
).option(
|
|
12224
|
+
"--opencode-version <v1|v2>",
|
|
12225
|
+
"Which OpenCode major version to launch: v1 or v2 (default: v1). Env: EVIDENT_OPENCODE_VERSION"
|
|
11068
12226
|
).option("--json", "Output in JSON format").option(
|
|
11069
12227
|
"--session-cleanup-max-age <duration>",
|
|
11070
12228
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -11133,6 +12291,7 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
11133
12291
|
// Raw string — validation/precedence is single-sourced in run.ts's
|
|
11134
12292
|
// resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).
|
|
11135
12293
|
opencodeStartTimeout: options.opencodeStartTimeout,
|
|
12294
|
+
opencodeVersion: options.opencodeVersion,
|
|
11136
12295
|
json: options.json,
|
|
11137
12296
|
// Raw strings — the resolver in run.ts single-sources parsing (M1).
|
|
11138
12297
|
sessionCleanupMaxAge: options.sessionCleanupMaxAge,
|