@evident-ai/cli 3.4.1-dev.661a835 → 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 +1608 -405
- 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,68 +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
|
-
disk_total_bytes: usage.diskTotalBytes,
|
|
812
|
-
disk_free_bytes: usage.diskFreeBytes,
|
|
813
|
-
opencode_db_bytes: usage.opencodeDbBytes
|
|
814
|
-
}),
|
|
815
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
816
|
-
});
|
|
817
|
-
if (!response.ok) {
|
|
818
|
-
const serverMessage = await readErrorMessage(response);
|
|
819
|
-
return {
|
|
820
|
-
ok: false,
|
|
821
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
return { ok: true };
|
|
825
|
-
} catch (error2) {
|
|
826
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
827
|
-
}
|
|
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
|
+
});
|
|
828
761
|
}
|
|
829
762
|
async function getAgentInfo(agentId, authHeader) {
|
|
830
763
|
const apiUrl = getApiUrlConfig();
|
|
@@ -876,13 +809,6 @@ function authLabelFor(credentials2) {
|
|
|
876
809
|
}
|
|
877
810
|
return "user token";
|
|
878
811
|
}
|
|
879
|
-
function describeFetchError(error2) {
|
|
880
|
-
const name = error2?.name;
|
|
881
|
-
if (name === "TimeoutError" || name === "AbortError") {
|
|
882
|
-
return `timed out after ${STATUS_TIMEOUT_MS}ms waiting for a response`;
|
|
883
|
-
}
|
|
884
|
-
return error2 instanceof Error ? error2.message : String(error2);
|
|
885
|
-
}
|
|
886
812
|
async function checkStatus(jsonMode) {
|
|
887
813
|
const apiUrl = getApiUrlConfig();
|
|
888
814
|
const credentials2 = await getAuthCredentials();
|
|
@@ -910,7 +836,7 @@ async function checkStatus(jsonMode) {
|
|
|
910
836
|
endpoint: apiUrl,
|
|
911
837
|
authLabel: authLabelFor(credentials2),
|
|
912
838
|
reason: "unreachable",
|
|
913
|
-
error: `Could not reach ${apiUrl}: ${
|
|
839
|
+
error: `Could not reach ${apiUrl}: ${describeTimeoutError(error2, STATUS_TIMEOUT_MS)}. The credentials were NOT validated.`,
|
|
914
840
|
exitCode: 75
|
|
915
841
|
};
|
|
916
842
|
}
|
|
@@ -1008,10 +934,10 @@ async function status(options = {}) {
|
|
|
1008
934
|
}
|
|
1009
935
|
|
|
1010
936
|
// src/lib/claude-usage.ts
|
|
1011
|
-
import { execFileSync } from "child_process";
|
|
1012
|
-
import { readFileSync } from "fs";
|
|
1013
|
-
import { homedir } from "os";
|
|
1014
|
-
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";
|
|
1015
941
|
var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
1016
942
|
var CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
1017
943
|
var CLAUDE_PROFILE_TIMEOUT_MS = 2e3;
|
|
@@ -1096,7 +1022,7 @@ function ownerLookupFailure(error2) {
|
|
|
1096
1022
|
}
|
|
1097
1023
|
async function getClaudeUsageOwner(accessToken) {
|
|
1098
1024
|
if (cachedOwner?.accessToken === accessToken) {
|
|
1099
|
-
return {
|
|
1025
|
+
return { subscription: cachedOwner.owner, ownerLookupError: null };
|
|
1100
1026
|
}
|
|
1101
1027
|
try {
|
|
1102
1028
|
const response = await fetch(CLAUDE_PROFILE_URL, {
|
|
@@ -1108,27 +1034,27 @@ async function getClaudeUsageOwner(accessToken) {
|
|
|
1108
1034
|
signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
|
|
1109
1035
|
});
|
|
1110
1036
|
if (!response.ok) {
|
|
1111
|
-
return {
|
|
1037
|
+
return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
|
|
1112
1038
|
}
|
|
1113
1039
|
let body;
|
|
1114
1040
|
try {
|
|
1115
1041
|
body = await response.json();
|
|
1116
1042
|
} catch (error2) {
|
|
1117
|
-
return {
|
|
1043
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1118
1044
|
}
|
|
1119
1045
|
const profile = body;
|
|
1120
1046
|
if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
|
|
1121
|
-
return {
|
|
1047
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1122
1048
|
}
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1049
|
+
const subscription = {
|
|
1050
|
+
ownerEmail: profile.account.email,
|
|
1125
1051
|
organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
|
|
1126
|
-
|
|
1052
|
+
planType: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
|
|
1127
1053
|
};
|
|
1128
|
-
cachedOwner = { accessToken, owner };
|
|
1129
|
-
return {
|
|
1054
|
+
cachedOwner = { accessToken, owner: subscription };
|
|
1055
|
+
return { subscription, ownerLookupError: null };
|
|
1130
1056
|
} catch (error2) {
|
|
1131
|
-
return {
|
|
1057
|
+
return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
|
|
1132
1058
|
}
|
|
1133
1059
|
}
|
|
1134
1060
|
async function getClaudeUsage() {
|
|
@@ -1157,11 +1083,11 @@ async function getClaudeUsage() {
|
|
|
1157
1083
|
throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
|
|
1158
1084
|
}
|
|
1159
1085
|
const body = await res.json();
|
|
1160
|
-
const {
|
|
1086
|
+
const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
|
|
1161
1087
|
return {
|
|
1162
1088
|
fiveHour: toWindow(body.five_hour),
|
|
1163
1089
|
sevenDay: toWindow(body.seven_day),
|
|
1164
|
-
|
|
1090
|
+
subscription,
|
|
1165
1091
|
ownerLookupError
|
|
1166
1092
|
};
|
|
1167
1093
|
}
|
|
@@ -1191,10 +1117,10 @@ async function claudeUsage() {
|
|
|
1191
1117
|
}
|
|
1192
1118
|
|
|
1193
1119
|
// src/commands/run.ts
|
|
1194
|
-
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
1195
|
-
import { homedir as
|
|
1196
|
-
import { isAbsolute as isAbsolute3, join as
|
|
1197
|
-
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";
|
|
1198
1124
|
|
|
1199
1125
|
// ../../packages/types/src/agents/index.ts
|
|
1200
1126
|
var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
|
|
@@ -1215,6 +1141,7 @@ var TelemetryEventTypes = {
|
|
|
1215
1141
|
// ../../packages/types/src/tunnel/index.ts
|
|
1216
1142
|
var MAX_FRAME_BYTES = 256 * 1024;
|
|
1217
1143
|
var TUNNEL_DRAIN_PING_PATH = "/__evident/drain";
|
|
1144
|
+
var TUNNEL_USAGE_REARM_PING_PATH = "/__evident/usage-rearm";
|
|
1218
1145
|
|
|
1219
1146
|
// ../../packages/types/src/runner-files.ts
|
|
1220
1147
|
var MAX_FILE_PUSH_BYTES = 64 * 1024;
|
|
@@ -1252,10 +1179,10 @@ function stripQuery(url) {
|
|
|
1252
1179
|
|
|
1253
1180
|
// src/commands/run.ts
|
|
1254
1181
|
import ora3 from "ora";
|
|
1255
|
-
import { select as
|
|
1182
|
+
import { select as select4 } from "@inquirer/prompts";
|
|
1256
1183
|
|
|
1257
1184
|
// src/lib/telemetry.ts
|
|
1258
|
-
var CLI_VERSION = (true ? "3.
|
|
1185
|
+
var CLI_VERSION = (true ? "3.4.1-dev.69d24ff" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1259
1186
|
function getCliVersion() {
|
|
1260
1187
|
return CLI_VERSION;
|
|
1261
1188
|
}
|
|
@@ -1425,12 +1352,50 @@ var SEVERITY_BY_LEVEL = {
|
|
|
1425
1352
|
warn: "warning",
|
|
1426
1353
|
error: "error"
|
|
1427
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
|
+
}
|
|
1428
1393
|
var MAX_MESSAGE_LENGTH = 500;
|
|
1429
1394
|
var MAX_METADATA_VALUE_LENGTH = 200;
|
|
1430
1395
|
var MAX_METADATA_ENTRIES = 20;
|
|
1431
1396
|
var TRUNCATION_MARKER = "\u2026";
|
|
1432
1397
|
function redact(message) {
|
|
1433
|
-
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>");
|
|
1434
1399
|
}
|
|
1435
1400
|
function truncate(message) {
|
|
1436
1401
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
@@ -1456,43 +1421,47 @@ function sanitiseMetadata(metadata) {
|
|
|
1456
1421
|
}
|
|
1457
1422
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1458
1423
|
var RATE_LIMIT_MAX_EVENTS = 30;
|
|
1459
|
-
var
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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) {
|
|
1465
1433
|
console.error(
|
|
1466
|
-
`[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}"`
|
|
1467
1435
|
);
|
|
1468
1436
|
}
|
|
1469
|
-
windowStartedAt = now;
|
|
1470
|
-
windowCount = 0;
|
|
1471
|
-
windowDroppedCount = 0;
|
|
1437
|
+
window.windowStartedAt = now;
|
|
1438
|
+
window.windowCount = 0;
|
|
1439
|
+
window.windowDroppedCount = 0;
|
|
1472
1440
|
}
|
|
1473
|
-
if (windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1474
|
-
windowDroppedCount++;
|
|
1475
|
-
if (windowDroppedCount === 1) {
|
|
1441
|
+
if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1442
|
+
window.windowDroppedCount++;
|
|
1443
|
+
if (window.windowDroppedCount === 1) {
|
|
1476
1444
|
console.error(
|
|
1477
|
-
`[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}"`
|
|
1478
1446
|
);
|
|
1479
1447
|
}
|
|
1480
1448
|
return false;
|
|
1481
1449
|
}
|
|
1482
|
-
windowCount++;
|
|
1450
|
+
window.windowCount++;
|
|
1483
1451
|
return true;
|
|
1484
1452
|
}
|
|
1485
1453
|
function forwardRunnerActivity(entry, context) {
|
|
1486
1454
|
try {
|
|
1487
1455
|
if (!FORWARDED_LEVELS.has(entry.level)) return;
|
|
1488
1456
|
if (!context.agentId || !context.authHeader) return;
|
|
1489
|
-
|
|
1457
|
+
const source = entry.source ?? "cli.run";
|
|
1458
|
+
if (!admitUnderRateLimit(source, Date.now())) return;
|
|
1490
1459
|
const rawMessage = entry.error ?? entry.message ?? "";
|
|
1491
1460
|
const message = truncate(redact(rawMessage));
|
|
1492
1461
|
logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
|
|
1493
1462
|
severity: SEVERITY_BY_LEVEL[entry.level],
|
|
1494
1463
|
message,
|
|
1495
|
-
metadata: { ...sanitiseMetadata(entry.metadata), source
|
|
1464
|
+
metadata: { ...sanitiseMetadata(entry.metadata), source },
|
|
1496
1465
|
agentId: context.agentId
|
|
1497
1466
|
});
|
|
1498
1467
|
} catch (err) {
|
|
@@ -1503,8 +1472,8 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1503
1472
|
}
|
|
1504
1473
|
|
|
1505
1474
|
// src/lib/opencode/session-db-recovery-report.ts
|
|
1506
|
-
import { readFileSync as readFileSync2, unlinkSync } from "fs";
|
|
1507
|
-
import { join as join2 } from "path";
|
|
1475
|
+
import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
|
|
1476
|
+
import { join as join2 } from "node:path";
|
|
1508
1477
|
function sessionDbRecoveryReportPath(homeDir, env) {
|
|
1509
1478
|
const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
|
|
1510
1479
|
return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
|
|
@@ -1717,13 +1686,13 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
1717
1686
|
}
|
|
1718
1687
|
|
|
1719
1688
|
// src/lib/opencode/session-db-boot.ts
|
|
1720
|
-
import { spawn as spawn2 } from "child_process";
|
|
1721
|
-
import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "fs";
|
|
1722
|
-
import { homedir as homedir2 } from "os";
|
|
1723
|
-
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";
|
|
1724
1693
|
|
|
1725
1694
|
// src/lib/runner-synchroniser.ts
|
|
1726
|
-
import { spawn } from "child_process";
|
|
1695
|
+
import { spawn } from "node:child_process";
|
|
1727
1696
|
function appendError(stderr, error2) {
|
|
1728
1697
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1729
1698
|
return stderr === "" ? message : `${stderr}
|
|
@@ -2248,9 +2217,9 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
2248
2217
|
}
|
|
2249
2218
|
|
|
2250
2219
|
// src/lib/opencode/session-db-provenance.ts
|
|
2251
|
-
import { createRequire } from "module";
|
|
2252
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2253
|
-
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";
|
|
2254
2223
|
var require2 = createRequire(import.meta.url);
|
|
2255
2224
|
function readSessionDbMigrationIds(dbPath) {
|
|
2256
2225
|
let db;
|
|
@@ -2378,11 +2347,18 @@ function isQueueValidatedVersion(version2) {
|
|
|
2378
2347
|
if (!version2) return false;
|
|
2379
2348
|
return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version2);
|
|
2380
2349
|
}
|
|
2381
|
-
function buildOpenCodeVersionWarning(version2) {
|
|
2382
|
-
if (
|
|
2383
|
-
const detected = version2 ? `v${version2}` : "unknown";
|
|
2350
|
+
function buildOpenCodeVersionWarning(version2, major) {
|
|
2351
|
+
if (major === "v2") return null;
|
|
2384
2352
|
const validated = QUEUE_VALIDATED_OPENCODE_VERSIONS.map((v) => `v${v}`).join(", ");
|
|
2385
|
-
|
|
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`;
|
|
2386
2362
|
}
|
|
2387
2363
|
|
|
2388
2364
|
// src/lib/opencode/process.ts
|
|
@@ -2444,6 +2420,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
2444
2420
|
|
|
2445
2421
|
// src/lib/opencode/process.ts
|
|
2446
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
|
+
}
|
|
2447
2434
|
function getProcessCwd(pid) {
|
|
2448
2435
|
const platform = process.platform;
|
|
2449
2436
|
try {
|
|
@@ -2492,14 +2479,14 @@ function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
2492
2479
|
}
|
|
2493
2480
|
return null;
|
|
2494
2481
|
}
|
|
2495
|
-
function
|
|
2482
|
+
function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
2496
2483
|
const instances = [];
|
|
2497
2484
|
try {
|
|
2498
2485
|
const platform = process.platform;
|
|
2499
2486
|
if (platform === "darwin" || platform === "linux") {
|
|
2500
2487
|
let pids = [];
|
|
2501
2488
|
try {
|
|
2502
|
-
const pgrepOutput = execSync(
|
|
2489
|
+
const pgrepOutput = execSync(`pgrep -f "${pgrepPattern}"`, {
|
|
2503
2490
|
encoding: "utf-8",
|
|
2504
2491
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2505
2492
|
}).trim();
|
|
@@ -2508,7 +2495,7 @@ function findOpenCodeProcesses() {
|
|
|
2508
2495
|
}
|
|
2509
2496
|
} catch {
|
|
2510
2497
|
try {
|
|
2511
|
-
const psOutput = execSync(
|
|
2498
|
+
const psOutput = execSync(`ps aux | grep -E "${psPattern}" | grep -v grep`, {
|
|
2512
2499
|
encoding: "utf-8",
|
|
2513
2500
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2514
2501
|
}).trim();
|
|
@@ -2554,6 +2541,9 @@ function findOpenCodeProcesses() {
|
|
|
2554
2541
|
}
|
|
2555
2542
|
return instances;
|
|
2556
2543
|
}
|
|
2544
|
+
function findOpenCodeProcesses() {
|
|
2545
|
+
return findProcessesByPattern("opencode serve|opencode-serve", "opencode (serve|--port)");
|
|
2546
|
+
}
|
|
2557
2547
|
async function scanPortsForOpenCode() {
|
|
2558
2548
|
const instances = [];
|
|
2559
2549
|
const checks = OPENCODE_PORT_RANGE.map(async (port) => {
|
|
@@ -2600,7 +2590,7 @@ async function findHealthyOpenCodeInstances() {
|
|
|
2600
2590
|
}
|
|
2601
2591
|
async function startOpenCode(port, options = {}) {
|
|
2602
2592
|
let command = "opencode";
|
|
2603
|
-
const printLogs = options.inheritStdio ? ["--print-logs"] : [];
|
|
2593
|
+
const printLogs = options.inheritStdio ? ["--print-logs", "--log-level", resolveOpenCodeLogLevel(process.env)] : [];
|
|
2604
2594
|
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...printLogs];
|
|
2605
2595
|
try {
|
|
2606
2596
|
execSync("which opencode", { stdio: "ignore" });
|
|
@@ -2657,6 +2647,19 @@ function isOpenCodeInstalled() {
|
|
|
2657
2647
|
return false;
|
|
2658
2648
|
}
|
|
2659
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
|
+
}
|
|
2660
2663
|
async function promptOpenCodeInstall(interactive) {
|
|
2661
2664
|
if (!interactive) {
|
|
2662
2665
|
console.log(
|
|
@@ -2666,7 +2669,11 @@ async function promptOpenCodeInstall(interactive) {
|
|
|
2666
2669
|
install_url: OPENCODE_INSTALL_URL,
|
|
2667
2670
|
install_commands: {
|
|
2668
2671
|
npm: "npm install -g opencode-ai",
|
|
2669
|
-
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
|
+
}
|
|
2670
2677
|
}
|
|
2671
2678
|
})
|
|
2672
2679
|
);
|
|
@@ -3150,21 +3157,112 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
3150
3157
|
}
|
|
3151
3158
|
return lastOk ?? last;
|
|
3152
3159
|
}
|
|
3153
|
-
function
|
|
3154
|
-
if (!messages || messages.length === 0) return
|
|
3155
|
-
const
|
|
3156
|
-
(
|
|
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
|
|
3157
3164
|
);
|
|
3158
|
-
const
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
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
|
+
}
|
|
3166
3195
|
}
|
|
3167
|
-
|
|
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;
|
|
3168
3266
|
let sawAnyUsage = false;
|
|
3169
3267
|
let inputSum = 0;
|
|
3170
3268
|
let outputSum = 0;
|
|
@@ -3175,7 +3273,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
3175
3273
|
let sawCost = false;
|
|
3176
3274
|
let modelId = null;
|
|
3177
3275
|
let providerId = null;
|
|
3178
|
-
for (const m of
|
|
3276
|
+
for (const m of selected) {
|
|
3179
3277
|
const info = m.info;
|
|
3180
3278
|
if (!info) continue;
|
|
3181
3279
|
const tokens = info.tokens;
|
|
@@ -3210,12 +3308,28 @@ function messageUsage(messages, userMessageId) {
|
|
|
3210
3308
|
usage_tokens_reasoning: reasoningSum,
|
|
3211
3309
|
usage_tokens_cache_read: cacheReadSum,
|
|
3212
3310
|
usage_tokens_cache_write: cacheWriteSum,
|
|
3213
|
-
// NULL means
|
|
3214
|
-
//
|
|
3215
|
-
// `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`.
|
|
3216
3313
|
usage_cost_usd: sawCost ? costSum : null
|
|
3217
3314
|
};
|
|
3218
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
|
+
}
|
|
3219
3333
|
function messageRunState(messages, userMessageId) {
|
|
3220
3334
|
if (!messages || messages.length === 0) return "unknown";
|
|
3221
3335
|
const hasUser = messages.some((m) => idOf(m) === userMessageId);
|
|
@@ -3278,8 +3392,7 @@ function isAbortedTerminalReply(messages, userMessageId) {
|
|
|
3278
3392
|
}
|
|
3279
3393
|
return false;
|
|
3280
3394
|
}
|
|
3281
|
-
function
|
|
3282
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
3395
|
+
function classifyReplyAuthError(reply) {
|
|
3283
3396
|
const error2 = errorOf(reply);
|
|
3284
3397
|
if (error2 == null || typeof error2 !== "object") return null;
|
|
3285
3398
|
const e = error2;
|
|
@@ -3304,6 +3417,32 @@ function messageFailure(messages, userMessageId) {
|
|
|
3304
3417
|
}
|
|
3305
3418
|
return null;
|
|
3306
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
|
+
}
|
|
3307
3446
|
function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProviderId, replyModelId = null) {
|
|
3308
3447
|
if (classified != null) return classified;
|
|
3309
3448
|
if (hasConfiguredProvider !== false) return null;
|
|
@@ -3320,6 +3459,28 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
|
3320
3459
|
(m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
3321
3460
|
);
|
|
3322
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
|
+
}
|
|
3323
3484
|
async function hasAnyConfiguredProvider(port) {
|
|
3324
3485
|
try {
|
|
3325
3486
|
const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
|
|
@@ -3351,6 +3512,94 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3351
3512
|
return null;
|
|
3352
3513
|
}
|
|
3353
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
|
+
}
|
|
3354
3603
|
|
|
3355
3604
|
// src/lib/opencode/session-cleanup.ts
|
|
3356
3605
|
var DURATION_UNIT_MS = {
|
|
@@ -3457,8 +3706,8 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
3457
3706
|
}
|
|
3458
3707
|
|
|
3459
3708
|
// src/lib/opencode/session-db-size.ts
|
|
3460
|
-
import { statSync as statSync3 } from "fs";
|
|
3461
|
-
import { join as join4 } from "path";
|
|
3709
|
+
import { statSync as statSync3 } from "node:fs";
|
|
3710
|
+
import { join as join4 } from "node:path";
|
|
3462
3711
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
3463
3712
|
function statSessionDbBytes(homeDir) {
|
|
3464
3713
|
const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
@@ -3488,9 +3737,96 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
3488
3737
|
return null;
|
|
3489
3738
|
}
|
|
3490
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
|
+
|
|
3491
3823
|
// src/lib/opencode/session-db-reclaim.ts
|
|
3492
|
-
import { statSync as
|
|
3493
|
-
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
|
+
}
|
|
3494
3830
|
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
3495
3831
|
try {
|
|
3496
3832
|
const fsStats = statfsSync(dirname4(dbPath));
|
|
@@ -3516,17 +3852,17 @@ async function probeReclaimAvailability(input) {
|
|
|
3516
3852
|
const { dbPath, requiredBytes } = input;
|
|
3517
3853
|
let sqlite;
|
|
3518
3854
|
try {
|
|
3519
|
-
sqlite = await import("sqlite");
|
|
3855
|
+
sqlite = await import("node:sqlite");
|
|
3520
3856
|
} catch (err) {
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
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 };
|
|
3525
3860
|
}
|
|
3526
3861
|
let autoVacuum = null;
|
|
3527
3862
|
try {
|
|
3528
3863
|
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
3529
3864
|
try {
|
|
3865
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3530
3866
|
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3531
3867
|
} finally {
|
|
3532
3868
|
db.close();
|
|
@@ -3537,23 +3873,25 @@ async function probeReclaimAvailability(input) {
|
|
|
3537
3873
|
);
|
|
3538
3874
|
}
|
|
3539
3875
|
if (autoVacuum !== 0) return null;
|
|
3540
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
|
|
3876
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
3541
3877
|
}
|
|
3542
3878
|
async function reclaimSessionDbSpace(input) {
|
|
3543
3879
|
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
3544
3880
|
let sqlite;
|
|
3545
3881
|
try {
|
|
3546
|
-
sqlite = await import("sqlite");
|
|
3882
|
+
sqlite = await import("node:sqlite");
|
|
3547
3883
|
} catch (err) {
|
|
3884
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
3548
3885
|
console.warn(
|
|
3549
|
-
`[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}`
|
|
3550
3887
|
);
|
|
3551
|
-
return { ok: false, skipped: "sqlite-unavailable" };
|
|
3888
|
+
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
3552
3889
|
}
|
|
3553
3890
|
const { DatabaseSync } = sqlite;
|
|
3554
3891
|
let db;
|
|
3555
3892
|
try {
|
|
3556
3893
|
db = new DatabaseSync(dbPath);
|
|
3894
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3557
3895
|
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3558
3896
|
if (autoVacuum === 0) {
|
|
3559
3897
|
if (!allowFullVacuum) {
|
|
@@ -3562,7 +3900,7 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3562
3900
|
);
|
|
3563
3901
|
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
3564
3902
|
}
|
|
3565
|
-
const fileBytesForGuard =
|
|
3903
|
+
const fileBytesForGuard = statSync5(dbPath).size;
|
|
3566
3904
|
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
3567
3905
|
if (skipReason !== null) {
|
|
3568
3906
|
console.warn(
|
|
@@ -3590,10 +3928,12 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3590
3928
|
);
|
|
3591
3929
|
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
3592
3930
|
} catch (err) {
|
|
3593
|
-
console.error(
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3931
|
+
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
3932
|
+
return {
|
|
3933
|
+
ok: false,
|
|
3934
|
+
skipped: "reclaim-error",
|
|
3935
|
+
detail: errorMessage(err)
|
|
3936
|
+
};
|
|
3597
3937
|
} finally {
|
|
3598
3938
|
db?.close();
|
|
3599
3939
|
}
|
|
@@ -3634,7 +3974,6 @@ var StreamForwarder = class {
|
|
|
3634
3974
|
handleFrame(frame) {
|
|
3635
3975
|
switch (frame.type) {
|
|
3636
3976
|
case "open":
|
|
3637
|
-
this.callbacks.onOpen?.(frame.sid, frame.method, frame.path);
|
|
3638
3977
|
void this.handleOpen(frame);
|
|
3639
3978
|
break;
|
|
3640
3979
|
case "req_data":
|
|
@@ -3670,12 +4009,21 @@ var StreamForwarder = class {
|
|
|
3670
4009
|
const { sid, method, path, headers, has_body } = frame;
|
|
3671
4010
|
const correlationId = headers?.[CORRELATION_ID_HEADER];
|
|
3672
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
|
+
}
|
|
3673
4015
|
if (path === TUNNEL_DRAIN_PING_PATH) {
|
|
3674
4016
|
this.callbacks.onDrainPing?.();
|
|
3675
4017
|
this.send({ type: "head", sid, status: 204, headers: {} });
|
|
3676
4018
|
this.send({ type: "res_end", sid });
|
|
3677
4019
|
return;
|
|
3678
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
|
+
}
|
|
3679
4027
|
if (process.env.DEBUG) {
|
|
3680
4028
|
log("debug", "agent_request", {
|
|
3681
4029
|
correlation_id: correlationId,
|
|
@@ -3820,7 +4168,8 @@ function connectTunnel(options) {
|
|
|
3820
4168
|
onResponse,
|
|
3821
4169
|
onInfo,
|
|
3822
4170
|
onWarning,
|
|
3823
|
-
onDrainPing
|
|
4171
|
+
onDrainPing,
|
|
4172
|
+
onUsageRearmPing
|
|
3824
4173
|
} = options;
|
|
3825
4174
|
const tunnelUrl = getTunnelUrlConfig();
|
|
3826
4175
|
const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
|
|
@@ -3832,7 +4181,8 @@ function connectTunnel(options) {
|
|
|
3832
4181
|
});
|
|
3833
4182
|
const forwarder = new StreamForwarder(ws, port, {
|
|
3834
4183
|
onHead: () => onResponse?.(),
|
|
3835
|
-
onDrainPing: () => onDrainPing?.()
|
|
4184
|
+
onDrainPing: () => onDrainPing?.(),
|
|
4185
|
+
onUsageRearmPing: () => onUsageRearmPing?.()
|
|
3836
4186
|
});
|
|
3837
4187
|
const connectionTimeout = setTimeout(() => {
|
|
3838
4188
|
ws.close();
|
|
@@ -3875,8 +4225,8 @@ function connectTunnel(options) {
|
|
|
3875
4225
|
try {
|
|
3876
4226
|
message = JSON.parse(data.toString());
|
|
3877
4227
|
} catch (error2) {
|
|
3878
|
-
const
|
|
3879
|
-
onError?.(`Failed to handle message: ${
|
|
4228
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
4229
|
+
onError?.(`Failed to handle message: ${errorMessage3}`);
|
|
3880
4230
|
return;
|
|
3881
4231
|
}
|
|
3882
4232
|
if (isStreamFrame(message)) {
|
|
@@ -3993,6 +4343,7 @@ var RunnerConnection = class {
|
|
|
3993
4343
|
onError: (error2) => events.onError?.(error2),
|
|
3994
4344
|
onResponse: () => events.onResponse?.(),
|
|
3995
4345
|
onDrainPing: () => events.onDrainPing?.(),
|
|
4346
|
+
onUsageRearmPing: () => events.onUsageRearmPing?.(),
|
|
3996
4347
|
onInfo: (message) => events.onInfo?.(message),
|
|
3997
4348
|
onWarning: (message) => events.onWarning?.(message)
|
|
3998
4349
|
});
|
|
@@ -4019,7 +4370,7 @@ var RunnerConnection = class {
|
|
|
4019
4370
|
};
|
|
4020
4371
|
|
|
4021
4372
|
// src/lib/tunnel/ready-marker.ts
|
|
4022
|
-
import { writeFileSync as writeFileSync3 } from "fs";
|
|
4373
|
+
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
4023
4374
|
function writeTunnelReadyMarker(path, agentId) {
|
|
4024
4375
|
try {
|
|
4025
4376
|
writeFileSync3(path, `${agentId}
|
|
@@ -4031,7 +4382,7 @@ function writeTunnelReadyMarker(path, agentId) {
|
|
|
4031
4382
|
}
|
|
4032
4383
|
|
|
4033
4384
|
// src/lib/replication.ts
|
|
4034
|
-
import { spawn as spawn4 } from "child_process";
|
|
4385
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
4035
4386
|
function startSessionDbReplication(configPath) {
|
|
4036
4387
|
return spawn4("litestream", ["replicate", "-config", configPath], {
|
|
4037
4388
|
stdio: "inherit"
|
|
@@ -4047,7 +4398,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
4047
4398
|
}
|
|
4048
4399
|
|
|
4049
4400
|
// src/lib/process-liveness.ts
|
|
4050
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
4401
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
4051
4402
|
function isProcessAlive(pid) {
|
|
4052
4403
|
try {
|
|
4053
4404
|
process.kill(pid, 0);
|
|
@@ -4073,9 +4424,9 @@ function isProcessAlive(pid) {
|
|
|
4073
4424
|
}
|
|
4074
4425
|
|
|
4075
4426
|
// src/lib/openai-usage.ts
|
|
4076
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
4077
|
-
import { homedir as
|
|
4078
|
-
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";
|
|
4079
4430
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4080
4431
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4081
4432
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4089,7 +4440,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4089
4440
|
}
|
|
4090
4441
|
function readOpenCodeChatGptCredentials() {
|
|
4091
4442
|
try {
|
|
4092
|
-
const raw = readFileSync5(
|
|
4443
|
+
const raw = readFileSync5(join6(homedir4(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4093
4444
|
let parsed;
|
|
4094
4445
|
try {
|
|
4095
4446
|
parsed = JSON.parse(raw);
|
|
@@ -4126,7 +4477,7 @@ function parseChatGptIdentity(accessToken) {
|
|
|
4126
4477
|
const auth = payload["https://api.openai.com/auth"];
|
|
4127
4478
|
const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
|
|
4128
4479
|
const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
|
|
4129
|
-
return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
|
|
4480
|
+
return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
|
|
4130
4481
|
}
|
|
4131
4482
|
function toWindow2(headers, name) {
|
|
4132
4483
|
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
@@ -4308,13 +4659,6 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
|
|
|
4308
4659
|
envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
4309
4660
|
});
|
|
4310
4661
|
}
|
|
4311
|
-
function nextReportDelayMs(random = Math.random) {
|
|
4312
|
-
return usageReportDelayMs(random);
|
|
4313
|
-
}
|
|
4314
|
-
var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
|
|
4315
|
-
function claudeUsageFailureLogLevel(consecutiveFailures) {
|
|
4316
|
-
return usageReportFailureLogLevel(consecutiveFailures);
|
|
4317
|
-
}
|
|
4318
4662
|
|
|
4319
4663
|
// src/lib/openai-usage-reporting.ts
|
|
4320
4664
|
function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
@@ -4351,8 +4695,8 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
4351
4695
|
}
|
|
4352
4696
|
|
|
4353
4697
|
// src/lib/resource-usage.ts
|
|
4354
|
-
import { cpus, totalmem, freemem } from "os";
|
|
4355
|
-
import { statfsSync as statfsSync2 } from "fs";
|
|
4698
|
+
import { cpus, totalmem, freemem } from "node:os";
|
|
4699
|
+
import { statfsSync as statfsSync2 } from "node:fs";
|
|
4356
4700
|
|
|
4357
4701
|
// src/lib/ecs-task-metadata.ts
|
|
4358
4702
|
var ECS_METADATA_TIMEOUT_MS = 2e3;
|
|
@@ -4437,58 +4781,97 @@ function readDisk(homeDir) {
|
|
|
4437
4781
|
};
|
|
4438
4782
|
}
|
|
4439
4783
|
}
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4784
|
+
var CPU_PEAK_WINDOW_MS = 6e4;
|
|
4785
|
+
var CPU_PEAK_WINDOW_SAMPLE_COUNT = 4;
|
|
4786
|
+
var CPU_PEAK_SAMPLE_INTERVAL_MS = CPU_PEAK_WINDOW_MS / CPU_PEAK_WINDOW_SAMPLE_COUNT;
|
|
4787
|
+
function createCpuPeakSampler() {
|
|
4788
|
+
const sampleHistory = new Array(CPU_PEAK_WINDOW_SAMPLE_COUNT);
|
|
4789
|
+
sampleHistory[0] = readCpuSample();
|
|
4790
|
+
let nextSampleIndex = 1;
|
|
4791
|
+
let sampleCount = 1;
|
|
4792
|
+
let peak = null;
|
|
4793
|
+
const timer = setInterval(() => {
|
|
4443
4794
|
const current = readCpuSample();
|
|
4444
|
-
const
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
const warnings = [];
|
|
4451
|
-
if (disk.warning) warnings.push(disk.warning);
|
|
4452
|
-
if (ecsWarning) warnings.push(ecsWarning);
|
|
4453
|
-
let cpuPercent = hostCpuPercent;
|
|
4454
|
-
let cpuCount = hostCpuCount;
|
|
4455
|
-
let memoryTotalBytes = totalmem();
|
|
4456
|
-
let memoryAvailableBytes = freemem();
|
|
4457
|
-
if (limits !== null) {
|
|
4458
|
-
cpuCount = limits.cpuCount;
|
|
4459
|
-
memoryTotalBytes = limits.memoryTotalBytes;
|
|
4460
|
-
memoryAvailableBytes = clamp(
|
|
4461
|
-
limits.memoryTotalBytes - (totalmem() - freemem()),
|
|
4462
|
-
0,
|
|
4463
|
-
limits.memoryTotalBytes
|
|
4464
|
-
);
|
|
4465
|
-
cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
|
|
4795
|
+
const sampleFromWindowAgo = sampleCount === CPU_PEAK_WINDOW_SAMPLE_COUNT ? sampleHistory[nextSampleIndex] : void 0;
|
|
4796
|
+
if (sampleFromWindowAgo !== void 0) {
|
|
4797
|
+
const percentage = cpuPercentBetween(sampleFromWindowAgo, current);
|
|
4798
|
+
if (percentage !== null) {
|
|
4799
|
+
peak = peak === null ? percentage : Math.max(peak, percentage);
|
|
4800
|
+
}
|
|
4466
4801
|
}
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4802
|
+
sampleHistory[nextSampleIndex] = current;
|
|
4803
|
+
nextSampleIndex = (nextSampleIndex + 1) % CPU_PEAK_WINDOW_SAMPLE_COUNT;
|
|
4804
|
+
sampleCount = Math.min(sampleCount + 1, CPU_PEAK_WINDOW_SAMPLE_COUNT);
|
|
4805
|
+
}, CPU_PEAK_SAMPLE_INTERVAL_MS);
|
|
4806
|
+
return {
|
|
4807
|
+
takeAndReset: () => {
|
|
4808
|
+
const currentPeak = peak;
|
|
4809
|
+
peak = null;
|
|
4810
|
+
return currentPeak;
|
|
4811
|
+
},
|
|
4812
|
+
stop: () => clearInterval(timer)
|
|
4813
|
+
};
|
|
4814
|
+
}
|
|
4815
|
+
function createResourceUsageCollector(homeDir) {
|
|
4816
|
+
let previous = readCpuSample();
|
|
4817
|
+
const cpuPeakSampler = createCpuPeakSampler();
|
|
4818
|
+
return {
|
|
4819
|
+
collect: async () => {
|
|
4820
|
+
const current = readCpuSample();
|
|
4821
|
+
const hostCpuPercent = cpuPercentBetween(previous, current);
|
|
4822
|
+
const hostCpuPeakPercent = cpuPeakSampler.takeAndReset();
|
|
4823
|
+
const hostCpuCount = cpus().length;
|
|
4824
|
+
previous = current;
|
|
4825
|
+
const disk = readDisk(homeDir);
|
|
4826
|
+
const opencodeDbBytes = statSessionDbBytes(homeDir);
|
|
4827
|
+
const { limits, warning: ecsWarning } = await readEcsTaskLimits(process.env);
|
|
4828
|
+
const warnings = [];
|
|
4829
|
+
if (disk.warning) warnings.push(disk.warning);
|
|
4830
|
+
if (ecsWarning) warnings.push(ecsWarning);
|
|
4831
|
+
let cpuPercent = hostCpuPercent;
|
|
4832
|
+
let cpuPeakPercent = hostCpuPeakPercent;
|
|
4833
|
+
let cpuCount = hostCpuCount;
|
|
4834
|
+
let memoryTotalBytes = totalmem();
|
|
4835
|
+
let memoryAvailableBytes = freemem();
|
|
4836
|
+
if (limits !== null) {
|
|
4837
|
+
cpuCount = limits.cpuCount;
|
|
4838
|
+
memoryTotalBytes = limits.memoryTotalBytes;
|
|
4839
|
+
memoryAvailableBytes = clamp(
|
|
4840
|
+
limits.memoryTotalBytes - (totalmem() - freemem()),
|
|
4841
|
+
0,
|
|
4842
|
+
limits.memoryTotalBytes
|
|
4843
|
+
);
|
|
4844
|
+
cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
|
|
4845
|
+
cpuPeakPercent = hostCpuPeakPercent === null ? null : clamp(round2(hostCpuPeakPercent * hostCpuCount / limits.cpuCount), 0, 100);
|
|
4846
|
+
}
|
|
4847
|
+
return {
|
|
4848
|
+
usage: {
|
|
4849
|
+
cpuPercent,
|
|
4850
|
+
cpuPeakPercent,
|
|
4851
|
+
cpuCount,
|
|
4852
|
+
memoryTotalBytes,
|
|
4853
|
+
memoryAvailableBytes,
|
|
4854
|
+
diskTotalBytes: disk.totalBytes,
|
|
4855
|
+
diskFreeBytes: disk.freeBytes,
|
|
4856
|
+
opencodeDbBytes
|
|
4857
|
+
},
|
|
4858
|
+
warnings
|
|
4859
|
+
};
|
|
4860
|
+
},
|
|
4861
|
+
stop: cpuPeakSampler.stop
|
|
4479
4862
|
};
|
|
4480
4863
|
}
|
|
4481
4864
|
|
|
4482
4865
|
// src/lib/channels/driver.ts
|
|
4483
|
-
import { homedir as
|
|
4866
|
+
import { homedir as homedir5 } from "node:os";
|
|
4484
4867
|
|
|
4485
4868
|
// src/lib/runner-file-sync.ts
|
|
4486
|
-
import { join as
|
|
4869
|
+
import { join as join8 } from "node:path";
|
|
4487
4870
|
|
|
4488
4871
|
// src/lib/file-push.ts
|
|
4489
|
-
import { randomUUID } from "crypto";
|
|
4490
|
-
import { chmod, mkdir, open as
|
|
4491
|
-
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";
|
|
4492
4875
|
var FILE_MODE = 384;
|
|
4493
4876
|
var DIRECTORY_MODE = 448;
|
|
4494
4877
|
async function writePushedFile(request) {
|
|
@@ -4521,7 +4904,7 @@ async function writePushedFile(request) {
|
|
|
4521
4904
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
4522
4905
|
dirname5(candidate)
|
|
4523
4906
|
);
|
|
4524
|
-
const realTarget =
|
|
4907
|
+
const realTarget = join7(existingAncestor, ...missingSegments, basename(candidate));
|
|
4525
4908
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
4526
4909
|
if (allowedDirectory === null) {
|
|
4527
4910
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -4557,7 +4940,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
4557
4940
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
4558
4941
|
return null;
|
|
4559
4942
|
}
|
|
4560
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4943
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4561
4944
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
4562
4945
|
return null;
|
|
4563
4946
|
}
|
|
@@ -4630,16 +5013,16 @@ function contains(realDirectory, realTarget) {
|
|
|
4630
5013
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
4631
5014
|
let current = existingAncestor;
|
|
4632
5015
|
for (const segment of missingSegments) {
|
|
4633
|
-
current =
|
|
5016
|
+
current = join7(current, segment);
|
|
4634
5017
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
4635
5018
|
await chmod(current, DIRECTORY_MODE);
|
|
4636
5019
|
}
|
|
4637
5020
|
}
|
|
4638
5021
|
async function writeAtomically(realTarget, content) {
|
|
4639
|
-
const temporaryPath =
|
|
5022
|
+
const temporaryPath = join7(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
4640
5023
|
let handle;
|
|
4641
5024
|
try {
|
|
4642
|
-
handle = await
|
|
5025
|
+
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
4643
5026
|
await handle.writeFile(content);
|
|
4644
5027
|
await handle.chmod(FILE_MODE);
|
|
4645
5028
|
await handle.close();
|
|
@@ -4766,12 +5149,12 @@ var NOT_APPLIED = {
|
|
|
4766
5149
|
opencodeAuthApplied: false
|
|
4767
5150
|
};
|
|
4768
5151
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
4769
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4770
|
-
return expanded ===
|
|
5152
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5153
|
+
return expanded === join8(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
4771
5154
|
}
|
|
4772
5155
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
4773
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4774
|
-
return expanded ===
|
|
5156
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5157
|
+
return expanded === join8(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
4775
5158
|
}
|
|
4776
5159
|
async function applyOne(options, file) {
|
|
4777
5160
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -4931,6 +5314,10 @@ var DEFAULT_RETRY_POLICY = {
|
|
|
4931
5314
|
baseDelayMs: 500,
|
|
4932
5315
|
maxDelayMs: 3e4
|
|
4933
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;
|
|
4934
5321
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
4935
5322
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
4936
5323
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
@@ -5071,6 +5458,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5071
5458
|
* message; it is removed once its in-flight set empties.
|
|
5072
5459
|
*/
|
|
5073
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();
|
|
5074
5472
|
/**
|
|
5075
5473
|
* AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
|
|
5076
5474
|
* dispatched and are still in-flight. A message in this set is never
|
|
@@ -5254,6 +5652,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5254
5652
|
* no watcher) can resolve the title.
|
|
5255
5653
|
*/
|
|
5256
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();
|
|
5257
5662
|
/** Serialises drains so a reconnect during a drain doesn't double-process. */
|
|
5258
5663
|
draining = false;
|
|
5259
5664
|
/**
|
|
@@ -5318,7 +5723,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5318
5723
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
5319
5724
|
this.now = config.now ?? (() => Date.now());
|
|
5320
5725
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
5321
|
-
this.homeDir = config.homeDir ??
|
|
5726
|
+
this.homeDir = config.homeDir ?? homedir5();
|
|
5322
5727
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
5323
5728
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
5324
5729
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
@@ -5468,6 +5873,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5468
5873
|
}
|
|
5469
5874
|
return ids;
|
|
5470
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
|
+
}
|
|
5471
5891
|
/**
|
|
5472
5892
|
* File-pull work, for `run.ts`'s idle accounting (#559).
|
|
5473
5893
|
*
|
|
@@ -5534,6 +5954,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5534
5954
|
*/
|
|
5535
5955
|
stop() {
|
|
5536
5956
|
this.stopped = true;
|
|
5957
|
+
this.sessionErrorStream?.abort.abort();
|
|
5958
|
+
this.sessionErrorStream = null;
|
|
5537
5959
|
}
|
|
5538
5960
|
/**
|
|
5539
5961
|
* The server clears this request when a new MicroVM identity is recorded, so a
|
|
@@ -5608,6 +6030,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5608
6030
|
*/
|
|
5609
6031
|
async processConversation(conv) {
|
|
5610
6032
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
6033
|
+
this.ensureSessionErrorStream();
|
|
5611
6034
|
const messages = await this.getPendingMessages(conv.id);
|
|
5612
6035
|
let dispatched = 0;
|
|
5613
6036
|
let skippedAlreadyDispatched = 0;
|
|
@@ -5680,7 +6103,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5680
6103
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
5681
6104
|
break;
|
|
5682
6105
|
}
|
|
5683
|
-
const
|
|
6106
|
+
const errorMessage3 = err instanceof Error ? err.message : String(err);
|
|
5684
6107
|
this.sessions.delete(conv.id);
|
|
5685
6108
|
this.supersede(conv.id, sessionId);
|
|
5686
6109
|
this.log({
|
|
@@ -5689,7 +6112,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5689
6112
|
conversation_id: conv.id,
|
|
5690
6113
|
message_id: message.id
|
|
5691
6114
|
});
|
|
5692
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6115
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5693
6116
|
this.log({
|
|
5694
6117
|
level: "warn",
|
|
5695
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)}`,
|
|
@@ -5700,7 +6123,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5700
6123
|
});
|
|
5701
6124
|
this.log({
|
|
5702
6125
|
level: "error",
|
|
5703
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
6126
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
|
|
5704
6127
|
conversation_id: conv.id,
|
|
5705
6128
|
message_id: message.id
|
|
5706
6129
|
});
|
|
@@ -5721,14 +6144,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5721
6144
|
this.unconfirmedDispatchFailures.delete(message.id);
|
|
5722
6145
|
this.sessions.delete(conv.id);
|
|
5723
6146
|
this.supersede(conv.id, sessionId);
|
|
5724
|
-
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.`;
|
|
5725
6148
|
this.log({
|
|
5726
6149
|
level: "error",
|
|
5727
|
-
message:
|
|
6150
|
+
message: errorMessage3,
|
|
5728
6151
|
conversation_id: conv.id,
|
|
5729
6152
|
message_id: message.id
|
|
5730
6153
|
});
|
|
5731
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6154
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5732
6155
|
this.log({
|
|
5733
6156
|
level: "warn",
|
|
5734
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)}`,
|
|
@@ -5954,6 +6377,23 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5954
6377
|
if (state === "running" || state === "queued") {
|
|
5955
6378
|
const ongoing = await isSessionOngoing(this.port, sessionId);
|
|
5956
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
|
+
}
|
|
5957
6397
|
return this.reattachRedrive(conv, sessionId, message, ocId);
|
|
5958
6398
|
}
|
|
5959
6399
|
if (ongoing === false) {
|
|
@@ -6043,16 +6483,37 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6043
6483
|
if (state === "done") {
|
|
6044
6484
|
const title = await this.resolveSessionTitle(sessionId, conv.id);
|
|
6045
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
|
+
);
|
|
6046
6492
|
this.log({
|
|
6047
6493
|
level: "info",
|
|
6048
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`,
|
|
6049
6495
|
conversation_id: conv.id,
|
|
6050
6496
|
message_id: message.id
|
|
6051
6497
|
});
|
|
6052
|
-
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
|
+
);
|
|
6053
6508
|
} else {
|
|
6054
6509
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
6055
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
|
+
);
|
|
6056
6517
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
6057
6518
|
this.log({
|
|
6058
6519
|
level: "error",
|
|
@@ -6060,7 +6521,19 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6060
6521
|
conversation_id: conv.id,
|
|
6061
6522
|
message_id: message.id
|
|
6062
6523
|
});
|
|
6063
|
-
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);
|
|
6064
6537
|
}
|
|
6065
6538
|
} catch (err) {
|
|
6066
6539
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -6601,6 +7074,24 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6601
7074
|
ambiguousPinnedSinceMs: 0,
|
|
6602
7075
|
ambiguousResolved: false
|
|
6603
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
|
+
}
|
|
6604
7095
|
}
|
|
6605
7096
|
/**
|
|
6606
7097
|
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
@@ -6805,6 +7296,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6805
7296
|
ensureWatcherRunning(sessionId) {
|
|
6806
7297
|
const watcher = this.watchers.get(sessionId);
|
|
6807
7298
|
if (!watcher) return;
|
|
7299
|
+
this.ensureSessionErrorStream();
|
|
6808
7300
|
if (watcher.loop) return;
|
|
6809
7301
|
if (watcher.inFlight.size === 0) {
|
|
6810
7302
|
this.watchers.delete(sessionId);
|
|
@@ -6820,6 +7312,154 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6820
7312
|
});
|
|
6821
7313
|
watcher.loop = loop;
|
|
6822
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
|
+
}
|
|
6823
7463
|
/**
|
|
6824
7464
|
* The per-session polling loop (WI-3). Once per tick it:
|
|
6825
7465
|
* 1. polls `GET /session/:id/message` once and, per in-flight message,
|
|
@@ -6932,6 +7572,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6932
7572
|
const conv = watcher.conv;
|
|
6933
7573
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
6934
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
|
+
}
|
|
6935
7590
|
if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
|
|
6936
7591
|
else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
|
|
6937
7592
|
if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
|
|
@@ -6985,6 +7640,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6985
7640
|
message_id: inFlight.evidentMessageId
|
|
6986
7641
|
});
|
|
6987
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
|
+
);
|
|
6988
7649
|
const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
|
|
6989
7650
|
try {
|
|
6990
7651
|
await this.markFailed(
|
|
@@ -6993,7 +7654,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6993
7654
|
sessionId,
|
|
6994
7655
|
error2,
|
|
6995
7656
|
usage,
|
|
6996
|
-
failure
|
|
7657
|
+
failure,
|
|
7658
|
+
usageAgentName,
|
|
7659
|
+
subagentInvocations
|
|
6997
7660
|
);
|
|
6998
7661
|
} catch (err) {
|
|
6999
7662
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7026,13 +7689,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7026
7689
|
return;
|
|
7027
7690
|
}
|
|
7028
7691
|
inFlight.done = true;
|
|
7692
|
+
await this.reportSubagentAuthFailures(
|
|
7693
|
+
watcher.conv.id,
|
|
7694
|
+
inFlight.opencodeMessageId,
|
|
7695
|
+
inFlight.evidentMessageId,
|
|
7696
|
+
messages
|
|
7697
|
+
);
|
|
7029
7698
|
}
|
|
7030
7699
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7031
7700
|
return;
|
|
7032
7701
|
}
|
|
7702
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
|
|
7703
|
+
const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
|
|
7033
7704
|
const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
|
|
7034
7705
|
const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
|
|
7035
|
-
if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
|
|
7706
|
+
if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
|
|
7036
7707
|
inFlight.stuckReported = true;
|
|
7037
7708
|
void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
|
|
7038
7709
|
stuck_for_ms: this.now() - inFlight.dispatchedAt
|
|
@@ -7193,7 +7864,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7193
7864
|
const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
|
|
7194
7865
|
(sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
|
|
7195
7866
|
);
|
|
7196
|
-
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
7867
|
+
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
|
|
7197
7868
|
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
7198
7869
|
this.log({
|
|
7199
7870
|
level: "debug",
|
|
@@ -7228,6 +7899,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7228
7899
|
});
|
|
7229
7900
|
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
7230
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
|
+
);
|
|
7231
7908
|
try {
|
|
7232
7909
|
await this.markDone(
|
|
7233
7910
|
conv.id,
|
|
@@ -7235,7 +7912,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7235
7912
|
sessionId,
|
|
7236
7913
|
inFlight.opencodeMessageId,
|
|
7237
7914
|
title,
|
|
7238
|
-
usage
|
|
7915
|
+
usage,
|
|
7916
|
+
usageAgentName,
|
|
7917
|
+
subagentInvocations
|
|
7239
7918
|
);
|
|
7240
7919
|
} catch (err) {
|
|
7241
7920
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7268,6 +7947,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7268
7947
|
return;
|
|
7269
7948
|
}
|
|
7270
7949
|
inFlight.done = true;
|
|
7950
|
+
await this.reportSubagentAuthFailures(
|
|
7951
|
+
watcher.conv.id,
|
|
7952
|
+
inFlight.opencodeMessageId,
|
|
7953
|
+
inFlight.evidentMessageId,
|
|
7954
|
+
messages
|
|
7955
|
+
);
|
|
7271
7956
|
}
|
|
7272
7957
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7273
7958
|
}
|
|
@@ -7408,6 +8093,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7408
8093
|
if (state === "failed" && !restartAborted) {
|
|
7409
8094
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
7410
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
|
+
);
|
|
7411
8102
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
7412
8103
|
this.log({
|
|
7413
8104
|
level: "error",
|
|
@@ -7416,7 +8107,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7416
8107
|
message_id: row.id
|
|
7417
8108
|
});
|
|
7418
8109
|
try {
|
|
7419
|
-
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
|
+
);
|
|
7420
8120
|
} catch (err) {
|
|
7421
8121
|
if (err instanceof ChannelAuthError) throw err;
|
|
7422
8122
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7438,6 +8138,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7438
8138
|
});
|
|
7439
8139
|
return;
|
|
7440
8140
|
}
|
|
8141
|
+
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
7441
8142
|
this.dontRedispatch.delete(row.id);
|
|
7442
8143
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
7443
8144
|
return;
|
|
@@ -7577,7 +8278,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7577
8278
|
try {
|
|
7578
8279
|
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
7579
8280
|
const usage = messageUsage(messages, ocId ?? "");
|
|
7580
|
-
|
|
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
|
+
);
|
|
7581
8297
|
} catch (err) {
|
|
7582
8298
|
if (err instanceof ChannelAuthError) throw err;
|
|
7583
8299
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7599,6 +8315,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7599
8315
|
});
|
|
7600
8316
|
return;
|
|
7601
8317
|
}
|
|
8318
|
+
if (ocId !== null) {
|
|
8319
|
+
await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
|
|
8320
|
+
}
|
|
7602
8321
|
this.dontRedispatch.delete(row.id);
|
|
7603
8322
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
7604
8323
|
}
|
|
@@ -7692,14 +8411,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7692
8411
|
this.unconfirmedDispatchFailures.delete(row.id);
|
|
7693
8412
|
this.sessions.delete(readoptConv.id);
|
|
7694
8413
|
this.supersede(readoptConv.id, sessionId);
|
|
7695
|
-
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.`;
|
|
7696
8415
|
this.log({
|
|
7697
8416
|
level: "error",
|
|
7698
|
-
message:
|
|
8417
|
+
message: errorMessage3,
|
|
7699
8418
|
conversation_id: row.conversation_id,
|
|
7700
8419
|
message_id: row.id
|
|
7701
8420
|
});
|
|
7702
|
-
await this.markFailed(row.conversation_id, row.id, null,
|
|
8421
|
+
await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
|
|
7703
8422
|
this.log({
|
|
7704
8423
|
level: "warn",
|
|
7705
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)}`,
|
|
@@ -7978,6 +8697,166 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7978
8697
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
7979
8698
|
return parent;
|
|
7980
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
|
+
}
|
|
7981
8860
|
/**
|
|
7982
8861
|
* OpenCode's synchronous default session title (e.g.
|
|
7983
8862
|
* `"New session - 1737800000000"`), assigned immediately when a session is
|
|
@@ -8461,7 +9340,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8461
9340
|
* watcher retries next tick within the
|
|
8462
9341
|
* deadline, Finding 4).
|
|
8463
9342
|
*/
|
|
8464
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
|
|
9343
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
|
|
8465
9344
|
const res = await this.fetchImpl(
|
|
8466
9345
|
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
8467
9346
|
{
|
|
@@ -8477,15 +9356,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8477
9356
|
opencode_session_id: sessionId,
|
|
8478
9357
|
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
8479
9358
|
...title ? { title } : {},
|
|
8480
|
-
...usage ? usage : {}
|
|
9359
|
+
...usage ? usage : {},
|
|
9360
|
+
...usageAgentName ? { usage_agent_name: usageAgentName } : {},
|
|
9361
|
+
...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
|
|
8481
9362
|
})
|
|
8482
9363
|
}
|
|
8483
9364
|
);
|
|
8484
9365
|
this.assertAuth(res, "marking message as done");
|
|
8485
|
-
if (res.ok)
|
|
9366
|
+
if (res.ok) {
|
|
9367
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
9368
|
+
return;
|
|
9369
|
+
}
|
|
8486
9370
|
if (isRetryableStatus(res.status)) {
|
|
8487
9371
|
throw new Error(`marking message as done: HTTP ${res.status}`);
|
|
8488
9372
|
}
|
|
9373
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8489
9374
|
throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
|
|
8490
9375
|
}
|
|
8491
9376
|
/**
|
|
@@ -8500,7 +9385,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8500
9385
|
* exists but is wedged, so the next attempt must get a fresh one
|
|
8501
9386
|
* instead of reusing it — see WI-1's server-side null-clearing PATCH).
|
|
8502
9387
|
*/
|
|
8503
|
-
async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
|
|
9388
|
+
async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
|
|
8504
9389
|
const body = { status: "failed" };
|
|
8505
9390
|
if (sessionId === null) {
|
|
8506
9391
|
body.opencode_session_id = null;
|
|
@@ -8509,23 +9394,33 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8509
9394
|
}
|
|
8510
9395
|
if (error2 !== void 0) body.error = error2;
|
|
8511
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
|
+
}
|
|
8512
9401
|
if (failure) {
|
|
8513
9402
|
body.failure_kind = failure.kind;
|
|
8514
9403
|
body.failure_provider_id = failure.providerId;
|
|
8515
9404
|
body.failure_model_id = failure.modelId;
|
|
8516
9405
|
body.failure_reason = failure.reason;
|
|
8517
9406
|
}
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
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);
|
|
8529
9424
|
}
|
|
8530
9425
|
/**
|
|
8531
9426
|
* Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
|
|
@@ -8550,6 +9445,111 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8550
9445
|
reply?.info?.modelID ?? null
|
|
8551
9446
|
);
|
|
8552
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
|
+
}
|
|
8553
9553
|
/**
|
|
8554
9554
|
* Best-effort, SINGLE-ATTEMPT runner→server telemetry ping
|
|
8555
9555
|
* (queued-followup-redrive). `POST .../messages/:id/signal {signal, ...extra}`
|
|
@@ -8703,6 +9703,13 @@ import chalk5 from "chalk";
|
|
|
8703
9703
|
import ora2 from "ora";
|
|
8704
9704
|
import { select as select2 } from "@inquirer/prompts";
|
|
8705
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
|
+
}
|
|
8706
9713
|
async function ensureOpenCodeRunning(ctx) {
|
|
8707
9714
|
const healthCheck = await checkOpenCodeHealth(ctx.port);
|
|
8708
9715
|
if (healthCheck.healthy) {
|
|
@@ -8750,6 +9757,7 @@ async function ensureOpenCodeRunning(ctx) {
|
|
|
8750
9757
|
}
|
|
8751
9758
|
}
|
|
8752
9759
|
if (!ctx.interactive) {
|
|
9760
|
+
checkNonInteractivePortConflict(ctx.port, isPortInUse);
|
|
8753
9761
|
ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
|
|
8754
9762
|
const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
|
|
8755
9763
|
const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
|
|
@@ -8831,9 +9839,119 @@ Port ${port} is already in use.`));
|
|
|
8831
9839
|
return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
|
|
8832
9840
|
}
|
|
8833
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
|
+
|
|
8834
9952
|
// src/lib/runner-credentials.ts
|
|
8835
|
-
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
8836
|
-
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";
|
|
8837
9955
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
8838
9956
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
8839
9957
|
var GITHUB_PROBE_TIMEOUT_MS = 1e4;
|
|
@@ -9105,11 +10223,11 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
9105
10223
|
}
|
|
9106
10224
|
|
|
9107
10225
|
// src/lib/opencode/config-overlay.ts
|
|
9108
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
9109
|
-
import { copyFileSync, existsSync as existsSync2, statSync as
|
|
9110
|
-
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";
|
|
9111
10229
|
function isFile(filePath) {
|
|
9112
|
-
return existsSync2(filePath) &&
|
|
10230
|
+
return existsSync2(filePath) && statSync6(filePath).isFile();
|
|
9113
10231
|
}
|
|
9114
10232
|
function applyRunnerOpenCodeConfig({
|
|
9115
10233
|
overlayPath,
|
|
@@ -9121,7 +10239,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9121
10239
|
return;
|
|
9122
10240
|
}
|
|
9123
10241
|
const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
|
|
9124
|
-
const target = isFile(
|
|
10242
|
+
const target = isFile(join9(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
9125
10243
|
if (!isFile(source)) {
|
|
9126
10244
|
log3(
|
|
9127
10245
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -9129,7 +10247,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9129
10247
|
);
|
|
9130
10248
|
return;
|
|
9131
10249
|
}
|
|
9132
|
-
copyFileSync(source,
|
|
10250
|
+
copyFileSync(source, join9(cwd, target));
|
|
9133
10251
|
try {
|
|
9134
10252
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
9135
10253
|
stdio: "ignore"
|
|
@@ -9138,11 +10256,11 @@ function applyRunnerOpenCodeConfig({
|
|
|
9138
10256
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
9139
10257
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
9140
10258
|
}
|
|
9141
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
10259
|
+
log3(`Applied runner OpenCode config ${source} to ${join9(cwd, target)}`);
|
|
9142
10260
|
}
|
|
9143
10261
|
|
|
9144
10262
|
// src/lib/credential-sync.ts
|
|
9145
|
-
import { renameSync, writeFileSync as writeFileSync5 } from "fs";
|
|
10263
|
+
import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
9146
10264
|
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
9147
10265
|
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
9148
10266
|
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
@@ -9152,7 +10270,7 @@ var MAX_FLUSH_PASSES = 2;
|
|
|
9152
10270
|
function outcomesWith(outcome) {
|
|
9153
10271
|
return { claude: outcome, opencode: outcome };
|
|
9154
10272
|
}
|
|
9155
|
-
function
|
|
10273
|
+
function errorMessage2(error2) {
|
|
9156
10274
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
9157
10275
|
}
|
|
9158
10276
|
function waitForSettlement(promise, timeoutMs) {
|
|
@@ -9179,7 +10297,7 @@ function writeMarker(markerPath, outcomes, log3) {
|
|
|
9179
10297
|
writeFileSync5(temporaryPath, body, { mode: 384 });
|
|
9180
10298
|
renameSync(temporaryPath, markerPath);
|
|
9181
10299
|
} catch (error2) {
|
|
9182
|
-
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${
|
|
10300
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
|
|
9183
10301
|
}
|
|
9184
10302
|
}
|
|
9185
10303
|
function intervalSeconds(env, log3) {
|
|
@@ -9211,7 +10329,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
|
9211
10329
|
},
|
|
9212
10330
|
(error2) => {
|
|
9213
10331
|
failed = true;
|
|
9214
|
-
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${
|
|
10332
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
|
|
9215
10333
|
}
|
|
9216
10334
|
);
|
|
9217
10335
|
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
@@ -9271,7 +10389,7 @@ function createCredentialSync({
|
|
|
9271
10389
|
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
9272
10390
|
} catch (error2) {
|
|
9273
10391
|
outcomes[store] = "failed";
|
|
9274
|
-
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${
|
|
10392
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
|
|
9275
10393
|
}
|
|
9276
10394
|
}
|
|
9277
10395
|
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
@@ -9413,7 +10531,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9413
10531
|
if (trimmed === "") {
|
|
9414
10532
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
9415
10533
|
}
|
|
9416
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
10534
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join10(homeDir, trimmed.slice(2)) : trimmed;
|
|
9417
10535
|
if (!isAbsolute3(expanded)) {
|
|
9418
10536
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
9419
10537
|
}
|
|
@@ -9437,6 +10555,28 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9437
10555
|
var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
|
|
9438
10556
|
var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
|
|
9439
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
|
+
}
|
|
9440
10580
|
function resolveOpenCodeStartTimeoutMs(options, env = process.env) {
|
|
9441
10581
|
const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1e3;
|
|
9442
10582
|
let raw;
|
|
@@ -9503,7 +10643,7 @@ function log2(state, message, level = "info") {
|
|
|
9503
10643
|
})
|
|
9504
10644
|
);
|
|
9505
10645
|
} else if (!state.interactive) {
|
|
9506
|
-
const prefix = level === "error" ?
|
|
10646
|
+
const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
|
|
9507
10647
|
console.log(`${prefix} ${message}`);
|
|
9508
10648
|
}
|
|
9509
10649
|
}
|
|
@@ -9533,7 +10673,7 @@ function logActivity(state, entry) {
|
|
|
9533
10673
|
}
|
|
9534
10674
|
function reportSessionDbRecovery(state) {
|
|
9535
10675
|
try {
|
|
9536
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
10676
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
|
|
9537
10677
|
for (const record of report.records) {
|
|
9538
10678
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
9539
10679
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -9564,18 +10704,18 @@ function reportSessionDbRecoveryRecord(state, record) {
|
|
|
9564
10704
|
function displayStatus(state) {
|
|
9565
10705
|
if (!state.interactive) return;
|
|
9566
10706
|
const attempt = state.connection?.reconnectAttempt ?? 0;
|
|
9567
|
-
const tunnel = state.connected ?
|
|
9568
|
-
const opencode = state.opencodeConnected ?
|
|
9569
|
-
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`) : "";
|
|
9570
10710
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
9571
|
-
const detail = last ?
|
|
10711
|
+
const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
|
|
9572
10712
|
const agent = state.agentName ?? state.agentId;
|
|
9573
10713
|
console.log(
|
|
9574
|
-
`${
|
|
10714
|
+
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
9575
10715
|
);
|
|
9576
10716
|
}
|
|
9577
10717
|
async function promptForLogin(promptMessage, successMessage) {
|
|
9578
|
-
const action = await
|
|
10718
|
+
const action = await select4({
|
|
9579
10719
|
message: promptMessage,
|
|
9580
10720
|
choices: [
|
|
9581
10721
|
{
|
|
@@ -9591,7 +10731,7 @@ async function promptForLogin(promptMessage, successMessage) {
|
|
|
9591
10731
|
]
|
|
9592
10732
|
});
|
|
9593
10733
|
if (action === "exit") {
|
|
9594
|
-
console.log(
|
|
10734
|
+
console.log(chalk7.dim(`
|
|
9595
10735
|
You can log in later by running: ${getCliName()} login`));
|
|
9596
10736
|
process.exit(0);
|
|
9597
10737
|
}
|
|
@@ -9602,7 +10742,7 @@ You can log in later by running: ${getCliName()} login`));
|
|
|
9602
10742
|
process.exit(1);
|
|
9603
10743
|
}
|
|
9604
10744
|
blank();
|
|
9605
|
-
console.log(
|
|
10745
|
+
console.log(chalk7.green(successMessage));
|
|
9606
10746
|
blank();
|
|
9607
10747
|
return { token: credentials2.token, authType: "bearer", user: credentials2.user };
|
|
9608
10748
|
}
|
|
@@ -9615,12 +10755,12 @@ async function handleAuthError(state, error2) {
|
|
|
9615
10755
|
if (state.interactive) displayStatus(state);
|
|
9616
10756
|
if (!state.interactive) {
|
|
9617
10757
|
blank();
|
|
9618
|
-
console.log(
|
|
9619
|
-
console.log(
|
|
10758
|
+
console.log(chalk7.red("Authentication expired"));
|
|
10759
|
+
console.log(chalk7.dim("Your authentication token is no longer valid."));
|
|
9620
10760
|
blank();
|
|
9621
|
-
console.log(
|
|
9622
|
-
console.log(
|
|
9623
|
-
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"));
|
|
9624
10764
|
blank();
|
|
9625
10765
|
await cleanup(state);
|
|
9626
10766
|
await shutdownTelemetry();
|
|
@@ -9628,7 +10768,7 @@ async function handleAuthError(state, error2) {
|
|
|
9628
10768
|
return { success: false };
|
|
9629
10769
|
}
|
|
9630
10770
|
blank();
|
|
9631
|
-
console.log(
|
|
10771
|
+
console.log(chalk7.yellow("Your authentication has expired."));
|
|
9632
10772
|
blank();
|
|
9633
10773
|
try {
|
|
9634
10774
|
const credentials2 = await promptForLogin(
|
|
@@ -9692,6 +10832,14 @@ async function driveChannels(state, driver) {
|
|
|
9692
10832
|
const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
|
|
9693
10833
|
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
9694
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
|
+
}
|
|
9695
10843
|
if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
|
|
9696
10844
|
idlePolls = 0;
|
|
9697
10845
|
idleMs = 0;
|
|
@@ -9719,8 +10867,8 @@ async function driveChannels(state, driver) {
|
|
|
9719
10867
|
state.running = false;
|
|
9720
10868
|
break;
|
|
9721
10869
|
}
|
|
9722
|
-
const
|
|
9723
|
-
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}` });
|
|
9724
10872
|
if (state.interactive) displayStatus(state);
|
|
9725
10873
|
if (driver.hasInFlightWatchers()) {
|
|
9726
10874
|
consecutiveDrainFailures = 0;
|
|
@@ -9758,9 +10906,18 @@ async function driveChannels(state, driver) {
|
|
|
9758
10906
|
}
|
|
9759
10907
|
}
|
|
9760
10908
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
9761
|
-
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
|
+
}
|
|
9762
10919
|
function sessionDbPath() {
|
|
9763
|
-
return
|
|
10920
|
+
return join10(homedir6(), ".local", "share", "opencode", "opencode.db");
|
|
9764
10921
|
}
|
|
9765
10922
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
9766
10923
|
const record = {
|
|
@@ -9856,7 +11013,7 @@ async function runSweep(state, driver, config) {
|
|
|
9856
11013
|
} else {
|
|
9857
11014
|
logActivity(state, {
|
|
9858
11015
|
type: "info",
|
|
9859
|
-
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}` : "")
|
|
9860
11017
|
});
|
|
9861
11018
|
}
|
|
9862
11019
|
} catch (error2) {
|
|
@@ -9879,13 +11036,20 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
9879
11036
|
for (const warning2 of config.warnings) {
|
|
9880
11037
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
9881
11038
|
}
|
|
9882
|
-
const dbBytes = statSessionDbBytes(
|
|
11039
|
+
const dbBytes = statSessionDbBytes(homedir6());
|
|
9883
11040
|
void (async () => {
|
|
9884
|
-
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
|
+
}
|
|
9885
11049
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
9886
11050
|
dbBytes,
|
|
9887
11051
|
cleanupEnabled: config.enabled,
|
|
9888
|
-
reclaimSkipReason
|
|
11052
|
+
reclaimSkipReason: reclaimAvailability?.reason ?? null
|
|
9889
11053
|
});
|
|
9890
11054
|
if (sizeWarning !== null) {
|
|
9891
11055
|
logActivity(state, { type: "info", level: "warn", message: sizeWarning });
|
|
@@ -10052,14 +11216,11 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
10052
11216
|
report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
|
|
10053
11217
|
isLocalCredentialProblem,
|
|
10054
11218
|
forcedOnHint: "run `claude` to sign in",
|
|
10055
|
-
firstDelayMs:
|
|
10056
|
-
nextDelayMs:
|
|
10057
|
-
failureLogLevel:
|
|
11219
|
+
firstDelayMs: firstReportDelayMs,
|
|
11220
|
+
nextDelayMs: usageReportDelayMs,
|
|
11221
|
+
failureLogLevel: usageReportFailureLogLevel
|
|
10058
11222
|
});
|
|
10059
11223
|
}
|
|
10060
|
-
var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
|
|
10061
|
-
var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
10062
|
-
var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
10063
11224
|
function scheduleResourceUsageReporting(state, options) {
|
|
10064
11225
|
const { enabled, warnings } = resolveResourceUsageReportingEnabled(
|
|
10065
11226
|
options.resourceUsageReporting,
|
|
@@ -10080,7 +11241,8 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10080
11241
|
});
|
|
10081
11242
|
return;
|
|
10082
11243
|
}
|
|
10083
|
-
const collect = createResourceUsageCollector(
|
|
11244
|
+
const { collect, stop } = createResourceUsageCollector(homedir6());
|
|
11245
|
+
state.stopResourceUsageSampling = stop;
|
|
10084
11246
|
let consecutiveFailures = 0;
|
|
10085
11247
|
const tick = async () => {
|
|
10086
11248
|
try {
|
|
@@ -10111,10 +11273,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10111
11273
|
consecutiveFailures++;
|
|
10112
11274
|
logActivity(state, {
|
|
10113
11275
|
type: "info",
|
|
10114
|
-
level:
|
|
10115
|
-
consecutiveFailures,
|
|
10116
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
10117
|
-
),
|
|
11276
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
10118
11277
|
message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
|
|
10119
11278
|
});
|
|
10120
11279
|
}
|
|
@@ -10123,20 +11282,11 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10123
11282
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
10124
11283
|
logActivity(state, {
|
|
10125
11284
|
type: "info",
|
|
10126
|
-
level:
|
|
10127
|
-
consecutiveFailures,
|
|
10128
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
10129
|
-
),
|
|
11285
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
10130
11286
|
message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
|
|
10131
11287
|
});
|
|
10132
11288
|
} finally {
|
|
10133
|
-
state.resourceUsageTimer = setTimeout(
|
|
10134
|
-
() => void tick(),
|
|
10135
|
-
jitteredDelayMs(
|
|
10136
|
-
RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
|
|
10137
|
-
RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
|
|
10138
|
-
)
|
|
10139
|
-
);
|
|
11289
|
+
state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
|
|
10140
11290
|
}
|
|
10141
11291
|
};
|
|
10142
11292
|
state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
|
|
@@ -10176,6 +11326,8 @@ async function cleanup(state, opts = {}) {
|
|
|
10176
11326
|
clearTimeout(timer);
|
|
10177
11327
|
}
|
|
10178
11328
|
state.sessionCleanupTimers = [];
|
|
11329
|
+
state.stopOpenCodeLogTail?.();
|
|
11330
|
+
state.stopOpenCodeLogTail = null;
|
|
10179
11331
|
if (state.claudeUsageTimer) {
|
|
10180
11332
|
clearTimeout(state.claudeUsageTimer);
|
|
10181
11333
|
state.claudeUsageTimer = null;
|
|
@@ -10190,6 +11342,8 @@ async function cleanup(state, opts = {}) {
|
|
|
10190
11342
|
clearTimeout(state.resourceUsageTimer);
|
|
10191
11343
|
state.resourceUsageTimer = null;
|
|
10192
11344
|
}
|
|
11345
|
+
state.stopResourceUsageSampling?.();
|
|
11346
|
+
state.stopResourceUsageSampling = null;
|
|
10193
11347
|
const credentialSync = state.credentialSync;
|
|
10194
11348
|
const flushCredentials = credentialSync ? async (phase, publish) => {
|
|
10195
11349
|
await timeShutdownPhase(state, durations, phase, async () => {
|
|
@@ -10312,7 +11466,7 @@ async function run(options) {
|
|
|
10312
11466
|
let fileSyncDirectories;
|
|
10313
11467
|
try {
|
|
10314
11468
|
logLevel = resolveLogLevel(options);
|
|
10315
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
11469
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
|
|
10316
11470
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
10317
11471
|
throw new Error(
|
|
10318
11472
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -10341,8 +11495,10 @@ async function run(options) {
|
|
|
10341
11495
|
connected: false,
|
|
10342
11496
|
opencodeConnected: false,
|
|
10343
11497
|
opencodeVersion: null,
|
|
11498
|
+
opencodeApiVersion: "v1",
|
|
10344
11499
|
sessionDbProvenanceAnomaly: false,
|
|
10345
11500
|
opencodeProcess: null,
|
|
11501
|
+
stopOpenCodeLogTail: null,
|
|
10346
11502
|
litestreamProcess: null,
|
|
10347
11503
|
connection: null,
|
|
10348
11504
|
channelDriver: null,
|
|
@@ -10357,6 +11513,7 @@ async function run(options) {
|
|
|
10357
11513
|
openaiUsageTimer: null,
|
|
10358
11514
|
openaiUsageRearm: null,
|
|
10359
11515
|
resourceUsageTimer: null,
|
|
11516
|
+
stopResourceUsageSampling: null,
|
|
10360
11517
|
credentialSync: null,
|
|
10361
11518
|
authHeader: ""
|
|
10362
11519
|
};
|
|
@@ -10409,15 +11566,15 @@ async function run(options) {
|
|
|
10409
11566
|
printError("Authentication required");
|
|
10410
11567
|
blank();
|
|
10411
11568
|
console.log(
|
|
10412
|
-
|
|
11569
|
+
chalk7.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
|
|
10413
11570
|
);
|
|
10414
|
-
console.log(
|
|
11571
|
+
console.log(chalk7.dim("Or run `evident login` for interactive authentication"));
|
|
10415
11572
|
blank();
|
|
10416
11573
|
process.exit(1);
|
|
10417
11574
|
return;
|
|
10418
11575
|
}
|
|
10419
11576
|
blank();
|
|
10420
|
-
console.log(
|
|
11577
|
+
console.log(chalk7.yellow("You are not logged in to Evident."));
|
|
10421
11578
|
blank();
|
|
10422
11579
|
credentials2 = await promptForLogin(
|
|
10423
11580
|
"Would you like to log in now?",
|
|
@@ -10467,7 +11624,7 @@ async function run(options) {
|
|
|
10467
11624
|
);
|
|
10468
11625
|
blank();
|
|
10469
11626
|
console.log(
|
|
10470
|
-
|
|
11627
|
+
chalk7.dim(
|
|
10471
11628
|
"Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
|
|
10472
11629
|
)
|
|
10473
11630
|
);
|
|
@@ -10490,15 +11647,15 @@ async function run(options) {
|
|
|
10490
11647
|
);
|
|
10491
11648
|
if (interactive && !state.json) {
|
|
10492
11649
|
blank();
|
|
10493
|
-
console.log(
|
|
10494
|
-
console.log(
|
|
11650
|
+
console.log(chalk7.bold("Evident Run"));
|
|
11651
|
+
console.log(chalk7.dim("-".repeat(40)));
|
|
10495
11652
|
}
|
|
10496
11653
|
const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
|
|
10497
11654
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
10498
11655
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
10499
11656
|
spinner?.fail("Authentication failed");
|
|
10500
11657
|
blank();
|
|
10501
|
-
console.log(
|
|
11658
|
+
console.log(chalk7.yellow("Your authentication token is invalid or expired."));
|
|
10502
11659
|
blank();
|
|
10503
11660
|
credentials2 = await promptForLogin(
|
|
10504
11661
|
"Would you like to log in again?",
|
|
@@ -10546,6 +11703,13 @@ async function run(options) {
|
|
|
10546
11703
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
10547
11704
|
}
|
|
10548
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;
|
|
10549
11713
|
let sessionDbVerifyFatal = false;
|
|
10550
11714
|
if (!options.restoreSessionDb) {
|
|
10551
11715
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -10595,6 +11759,13 @@ async function run(options) {
|
|
|
10595
11759
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
10596
11760
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
10597
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
|
+
}
|
|
10598
11769
|
const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
|
|
10599
11770
|
for (const warning2 of maxActiveSessionsWarnings) {
|
|
10600
11771
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
@@ -10602,7 +11773,14 @@ async function run(options) {
|
|
|
10602
11773
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
10603
11774
|
const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
|
|
10604
11775
|
try {
|
|
10605
|
-
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({
|
|
10606
11784
|
port: state.port,
|
|
10607
11785
|
interactive: state.interactive,
|
|
10608
11786
|
agentId: state.agentId,
|
|
@@ -10613,6 +11791,7 @@ async function run(options) {
|
|
|
10613
11791
|
state.port = oc.port;
|
|
10614
11792
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
10615
11793
|
state.opencodeVersion = oc.version;
|
|
11794
|
+
state.opencodeApiVersion = opencodeVersion;
|
|
10616
11795
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
10617
11796
|
try {
|
|
10618
11797
|
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
@@ -10629,7 +11808,7 @@ async function run(options) {
|
|
|
10629
11808
|
const provenance = checkSessionDbProvenance({
|
|
10630
11809
|
dbPath: sessionDbPath(),
|
|
10631
11810
|
currentVersion: state.opencodeVersion,
|
|
10632
|
-
homeDir:
|
|
11811
|
+
homeDir: homedir6(),
|
|
10633
11812
|
env: process.env
|
|
10634
11813
|
});
|
|
10635
11814
|
if (provenance.anomaly) {
|
|
@@ -10649,13 +11828,17 @@ async function run(options) {
|
|
|
10649
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}).`;
|
|
10650
11829
|
logActivity(state, { type: "info", level: "warn", message });
|
|
10651
11830
|
} else {
|
|
10652
|
-
const versionWarning = buildOpenCodeVersionWarning(
|
|
11831
|
+
const versionWarning = buildOpenCodeVersionWarning(
|
|
11832
|
+
state.opencodeVersion,
|
|
11833
|
+
state.opencodeApiVersion
|
|
11834
|
+
);
|
|
10653
11835
|
if (versionWarning) {
|
|
10654
11836
|
log2(state, versionWarning, "warn");
|
|
10655
11837
|
if (state.interactive && !state.json) {
|
|
10656
11838
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
10657
11839
|
}
|
|
10658
11840
|
}
|
|
11841
|
+
await reloadProviderCache(state.port);
|
|
10659
11842
|
const noProviderWarning = buildNoProviderWarning(
|
|
10660
11843
|
await hasAnyConfiguredProvider(state.port)
|
|
10661
11844
|
);
|
|
@@ -10664,10 +11847,10 @@ async function run(options) {
|
|
|
10664
11847
|
if (state.interactive && !state.json) {
|
|
10665
11848
|
logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
|
|
10666
11849
|
blank();
|
|
10667
|
-
console.log(
|
|
11850
|
+
console.log(chalk7.yellow("\u26A0 No OpenCode model provider is configured."));
|
|
10668
11851
|
console.log(
|
|
10669
|
-
|
|
10670
|
-
`Run ${
|
|
11852
|
+
chalk7.dim(
|
|
11853
|
+
`Run ${chalk7.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
|
|
10671
11854
|
)
|
|
10672
11855
|
);
|
|
10673
11856
|
blank();
|
|
@@ -10791,7 +11974,7 @@ async function run(options) {
|
|
|
10791
11974
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
10792
11975
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
10793
11976
|
fileSyncDirectories,
|
|
10794
|
-
homeDir:
|
|
11977
|
+
homeDir: homedir6(),
|
|
10795
11978
|
maxActiveSessions,
|
|
10796
11979
|
log: (entry) => (
|
|
10797
11980
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -10834,7 +12017,11 @@ async function run(options) {
|
|
|
10834
12017
|
emitAgentConnected(state.agentId, {
|
|
10835
12018
|
port: state.port,
|
|
10836
12019
|
cli_version: getCliVersion(),
|
|
10837
|
-
opencode_version:
|
|
12020
|
+
opencode_version: reportedOpenCodeVersion({
|
|
12021
|
+
version: state.opencodeVersion,
|
|
12022
|
+
major: state.opencodeApiVersion,
|
|
12023
|
+
connected: state.opencodeConnected
|
|
12024
|
+
})
|
|
10838
12025
|
});
|
|
10839
12026
|
if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
|
|
10840
12027
|
if (state.interactive) displayStatus(state);
|
|
@@ -10917,6 +12104,18 @@ async function run(options) {
|
|
|
10917
12104
|
if (state.interactive) displayStatus(state);
|
|
10918
12105
|
});
|
|
10919
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
|
+
},
|
|
10920
12119
|
onInfo: (message) => logActivity(state, { type: "info", message })
|
|
10921
12120
|
}
|
|
10922
12121
|
});
|
|
@@ -11021,6 +12220,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
11021
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(
|
|
11022
12221
|
"--opencode-start-timeout <seconds>",
|
|
11023
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"
|
|
11024
12226
|
).option("--json", "Output in JSON format").option(
|
|
11025
12227
|
"--session-cleanup-max-age <duration>",
|
|
11026
12228
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -11089,6 +12291,7 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
11089
12291
|
// Raw string — validation/precedence is single-sourced in run.ts's
|
|
11090
12292
|
// resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).
|
|
11091
12293
|
opencodeStartTimeout: options.opencodeStartTimeout,
|
|
12294
|
+
opencodeVersion: options.opencodeVersion,
|
|
11092
12295
|
json: options.json,
|
|
11093
12296
|
// Raw strings — the resolver in run.ts single-sources parsing (M1).
|
|
11094
12297
|
sessionCleanupMaxAge: options.sessionCleanupMaxAge,
|