@evident-ai/cli 3.4.1-dev.fb713ba → 3.4.1-dev.fb92ede
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 +17 -0
- package/dist/index.js +1976 -436
- 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;
|
|
@@ -764,59 +739,25 @@ function toReportedOpenAiWindow(window) {
|
|
|
764
739
|
};
|
|
765
740
|
}
|
|
766
741
|
async function reportOpenAiUsage(agentId, authHeader, snapshot) {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
775
|
-
has_credits: snapshot.hasCredits,
|
|
776
|
-
credits_unlimited: snapshot.creditsUnlimited
|
|
777
|
-
}),
|
|
778
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
779
|
-
});
|
|
780
|
-
if (!response.ok) {
|
|
781
|
-
const serverMessage = await readErrorMessage(response);
|
|
782
|
-
return {
|
|
783
|
-
ok: false,
|
|
784
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
return { ok: true };
|
|
788
|
-
} catch (error2) {
|
|
789
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
790
|
-
}
|
|
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
|
+
});
|
|
791
749
|
}
|
|
792
750
|
async function reportResourceUsage(agentId, authHeader, usage) {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
disk_total_bytes: usage.diskTotalBytes,
|
|
804
|
-
disk_free_bytes: usage.diskFreeBytes,
|
|
805
|
-
opencode_db_bytes: usage.opencodeDbBytes
|
|
806
|
-
}),
|
|
807
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
808
|
-
});
|
|
809
|
-
if (!response.ok) {
|
|
810
|
-
const serverMessage = await readErrorMessage(response);
|
|
811
|
-
return {
|
|
812
|
-
ok: false,
|
|
813
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
return { ok: true };
|
|
817
|
-
} catch (error2) {
|
|
818
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
819
|
-
}
|
|
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
|
+
});
|
|
820
761
|
}
|
|
821
762
|
async function getAgentInfo(agentId, authHeader) {
|
|
822
763
|
const apiUrl = getApiUrlConfig();
|
|
@@ -868,13 +809,6 @@ function authLabelFor(credentials2) {
|
|
|
868
809
|
}
|
|
869
810
|
return "user token";
|
|
870
811
|
}
|
|
871
|
-
function describeFetchError(error2) {
|
|
872
|
-
const name = error2?.name;
|
|
873
|
-
if (name === "TimeoutError" || name === "AbortError") {
|
|
874
|
-
return `timed out after ${STATUS_TIMEOUT_MS}ms waiting for a response`;
|
|
875
|
-
}
|
|
876
|
-
return error2 instanceof Error ? error2.message : String(error2);
|
|
877
|
-
}
|
|
878
812
|
async function checkStatus(jsonMode) {
|
|
879
813
|
const apiUrl = getApiUrlConfig();
|
|
880
814
|
const credentials2 = await getAuthCredentials();
|
|
@@ -902,7 +836,7 @@ async function checkStatus(jsonMode) {
|
|
|
902
836
|
endpoint: apiUrl,
|
|
903
837
|
authLabel: authLabelFor(credentials2),
|
|
904
838
|
reason: "unreachable",
|
|
905
|
-
error: `Could not reach ${apiUrl}: ${
|
|
839
|
+
error: `Could not reach ${apiUrl}: ${describeTimeoutError(error2, STATUS_TIMEOUT_MS)}. The credentials were NOT validated.`,
|
|
906
840
|
exitCode: 75
|
|
907
841
|
};
|
|
908
842
|
}
|
|
@@ -1000,10 +934,10 @@ async function status(options = {}) {
|
|
|
1000
934
|
}
|
|
1001
935
|
|
|
1002
936
|
// src/lib/claude-usage.ts
|
|
1003
|
-
import { execFileSync } from "child_process";
|
|
1004
|
-
import { readFileSync } from "fs";
|
|
1005
|
-
import { homedir } from "os";
|
|
1006
|
-
import { join } from "path";
|
|
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";
|
|
1007
941
|
var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
1008
942
|
var CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
1009
943
|
var CLAUDE_PROFILE_TIMEOUT_MS = 2e3;
|
|
@@ -1088,7 +1022,7 @@ function ownerLookupFailure(error2) {
|
|
|
1088
1022
|
}
|
|
1089
1023
|
async function getClaudeUsageOwner(accessToken) {
|
|
1090
1024
|
if (cachedOwner?.accessToken === accessToken) {
|
|
1091
|
-
return {
|
|
1025
|
+
return { subscription: cachedOwner.owner, ownerLookupError: null };
|
|
1092
1026
|
}
|
|
1093
1027
|
try {
|
|
1094
1028
|
const response = await fetch(CLAUDE_PROFILE_URL, {
|
|
@@ -1100,27 +1034,27 @@ async function getClaudeUsageOwner(accessToken) {
|
|
|
1100
1034
|
signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
|
|
1101
1035
|
});
|
|
1102
1036
|
if (!response.ok) {
|
|
1103
|
-
return {
|
|
1037
|
+
return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
|
|
1104
1038
|
}
|
|
1105
1039
|
let body;
|
|
1106
1040
|
try {
|
|
1107
1041
|
body = await response.json();
|
|
1108
1042
|
} catch (error2) {
|
|
1109
|
-
return {
|
|
1043
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1110
1044
|
}
|
|
1111
1045
|
const profile = body;
|
|
1112
1046
|
if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
|
|
1113
|
-
return {
|
|
1047
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1114
1048
|
}
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1049
|
+
const subscription = {
|
|
1050
|
+
ownerEmail: profile.account.email,
|
|
1117
1051
|
organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
|
|
1118
|
-
|
|
1052
|
+
planType: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
|
|
1119
1053
|
};
|
|
1120
|
-
cachedOwner = { accessToken, owner };
|
|
1121
|
-
return {
|
|
1054
|
+
cachedOwner = { accessToken, owner: subscription };
|
|
1055
|
+
return { subscription, ownerLookupError: null };
|
|
1122
1056
|
} catch (error2) {
|
|
1123
|
-
return {
|
|
1057
|
+
return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
|
|
1124
1058
|
}
|
|
1125
1059
|
}
|
|
1126
1060
|
async function getClaudeUsage() {
|
|
@@ -1149,11 +1083,11 @@ async function getClaudeUsage() {
|
|
|
1149
1083
|
throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
|
|
1150
1084
|
}
|
|
1151
1085
|
const body = await res.json();
|
|
1152
|
-
const {
|
|
1086
|
+
const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
|
|
1153
1087
|
return {
|
|
1154
1088
|
fiveHour: toWindow(body.five_hour),
|
|
1155
1089
|
sevenDay: toWindow(body.seven_day),
|
|
1156
|
-
|
|
1090
|
+
subscription,
|
|
1157
1091
|
ownerLookupError
|
|
1158
1092
|
};
|
|
1159
1093
|
}
|
|
@@ -1183,10 +1117,10 @@ async function claudeUsage() {
|
|
|
1183
1117
|
}
|
|
1184
1118
|
|
|
1185
1119
|
// src/commands/run.ts
|
|
1186
|
-
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as
|
|
1187
|
-
import { homedir as
|
|
1188
|
-
import { isAbsolute as isAbsolute3, join as
|
|
1189
|
-
import
|
|
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";
|
|
1190
1124
|
|
|
1191
1125
|
// ../../packages/types/src/agents/index.ts
|
|
1192
1126
|
var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
|
|
@@ -1207,6 +1141,7 @@ var TelemetryEventTypes = {
|
|
|
1207
1141
|
// ../../packages/types/src/tunnel/index.ts
|
|
1208
1142
|
var MAX_FRAME_BYTES = 256 * 1024;
|
|
1209
1143
|
var TUNNEL_DRAIN_PING_PATH = "/__evident/drain";
|
|
1144
|
+
var TUNNEL_USAGE_REARM_PING_PATH = "/__evident/usage-rearm";
|
|
1210
1145
|
|
|
1211
1146
|
// ../../packages/types/src/runner-files.ts
|
|
1212
1147
|
var MAX_FILE_PUSH_BYTES = 64 * 1024;
|
|
@@ -1244,7 +1179,7 @@ function stripQuery(url) {
|
|
|
1244
1179
|
|
|
1245
1180
|
// src/commands/run.ts
|
|
1246
1181
|
import ora3 from "ora";
|
|
1247
|
-
import { select as
|
|
1182
|
+
import { select as select4 } from "@inquirer/prompts";
|
|
1248
1183
|
|
|
1249
1184
|
// src/lib/telemetry.ts
|
|
1250
1185
|
var CLI_VERSION = (true ? "3.0.0" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
@@ -1417,12 +1352,50 @@ var SEVERITY_BY_LEVEL = {
|
|
|
1417
1352
|
warn: "warning",
|
|
1418
1353
|
error: "error"
|
|
1419
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
|
+
}
|
|
1420
1393
|
var MAX_MESSAGE_LENGTH = 500;
|
|
1421
1394
|
var MAX_METADATA_VALUE_LENGTH = 200;
|
|
1422
1395
|
var MAX_METADATA_ENTRIES = 20;
|
|
1423
1396
|
var TRUNCATION_MARKER = "\u2026";
|
|
1424
1397
|
function redact(message) {
|
|
1425
|
-
return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/https?:\/\/\S+/g, "<url>");
|
|
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>");
|
|
1426
1399
|
}
|
|
1427
1400
|
function truncate(message) {
|
|
1428
1401
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
@@ -1448,43 +1421,47 @@ function sanitiseMetadata(metadata) {
|
|
|
1448
1421
|
}
|
|
1449
1422
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1450
1423
|
var RATE_LIMIT_MAX_EVENTS = 30;
|
|
1451
|
-
var
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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) {
|
|
1457
1433
|
console.error(
|
|
1458
|
-
`[runner-activity-telemetry] rate cap reached: dropped ${windowDroppedCount} ${windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (cap ${RATE_LIMIT_MAX_EVENTS}/min)`
|
|
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}"`
|
|
1459
1435
|
);
|
|
1460
1436
|
}
|
|
1461
|
-
windowStartedAt = now;
|
|
1462
|
-
windowCount = 0;
|
|
1463
|
-
windowDroppedCount = 0;
|
|
1437
|
+
window.windowStartedAt = now;
|
|
1438
|
+
window.windowCount = 0;
|
|
1439
|
+
window.windowDroppedCount = 0;
|
|
1464
1440
|
}
|
|
1465
|
-
if (windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1466
|
-
windowDroppedCount++;
|
|
1467
|
-
if (windowDroppedCount === 1) {
|
|
1441
|
+
if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1442
|
+
window.windowDroppedCount++;
|
|
1443
|
+
if (window.windowDroppedCount === 1) {
|
|
1468
1444
|
console.error(
|
|
1469
|
-
`[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ${RATE_LIMIT_WINDOW_MS / 1e3}s) \u2014 dropping further entries this window`
|
|
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}"`
|
|
1470
1446
|
);
|
|
1471
1447
|
}
|
|
1472
1448
|
return false;
|
|
1473
1449
|
}
|
|
1474
|
-
windowCount++;
|
|
1450
|
+
window.windowCount++;
|
|
1475
1451
|
return true;
|
|
1476
1452
|
}
|
|
1477
1453
|
function forwardRunnerActivity(entry, context) {
|
|
1478
1454
|
try {
|
|
1479
1455
|
if (!FORWARDED_LEVELS.has(entry.level)) return;
|
|
1480
1456
|
if (!context.agentId || !context.authHeader) return;
|
|
1481
|
-
|
|
1457
|
+
const source = entry.source ?? "cli.run";
|
|
1458
|
+
if (!admitUnderRateLimit(source, Date.now())) return;
|
|
1482
1459
|
const rawMessage = entry.error ?? entry.message ?? "";
|
|
1483
1460
|
const message = truncate(redact(rawMessage));
|
|
1484
1461
|
logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
|
|
1485
1462
|
severity: SEVERITY_BY_LEVEL[entry.level],
|
|
1486
1463
|
message,
|
|
1487
|
-
metadata: { ...sanitiseMetadata(entry.metadata), source
|
|
1464
|
+
metadata: { ...sanitiseMetadata(entry.metadata), source },
|
|
1488
1465
|
agentId: context.agentId
|
|
1489
1466
|
});
|
|
1490
1467
|
} catch (err) {
|
|
@@ -1495,8 +1472,8 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1495
1472
|
}
|
|
1496
1473
|
|
|
1497
1474
|
// src/lib/opencode/session-db-recovery-report.ts
|
|
1498
|
-
import { readFileSync as readFileSync2, unlinkSync } from "fs";
|
|
1499
|
-
import { join as join2 } from "path";
|
|
1475
|
+
import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
|
|
1476
|
+
import { join as join2 } from "node:path";
|
|
1500
1477
|
function sessionDbRecoveryReportPath(homeDir, env) {
|
|
1501
1478
|
const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
|
|
1502
1479
|
return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
|
|
@@ -1709,13 +1686,13 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
1709
1686
|
}
|
|
1710
1687
|
|
|
1711
1688
|
// src/lib/opencode/session-db-boot.ts
|
|
1712
|
-
import { spawn as spawn2 } from "child_process";
|
|
1713
|
-
import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "fs";
|
|
1714
|
-
import { homedir as homedir2 } from "os";
|
|
1715
|
-
import { dirname as dirname2, resolve as resolvePath } from "path";
|
|
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";
|
|
1716
1693
|
|
|
1717
1694
|
// src/lib/runner-synchroniser.ts
|
|
1718
|
-
import { spawn } from "child_process";
|
|
1695
|
+
import { spawn } from "node:child_process";
|
|
1719
1696
|
function appendError(stderr, error2) {
|
|
1720
1697
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1721
1698
|
return stderr === "" ? message : `${stderr}
|
|
@@ -1728,10 +1705,14 @@ function runSynchroniser(args, opts) {
|
|
|
1728
1705
|
let stderr = "";
|
|
1729
1706
|
let settled = false;
|
|
1730
1707
|
const timer = {};
|
|
1708
|
+
let abortListener;
|
|
1709
|
+
let spawnListener;
|
|
1731
1710
|
const finish = (result) => {
|
|
1732
1711
|
if (settled) return;
|
|
1733
1712
|
settled = true;
|
|
1734
1713
|
if (timer.handle) clearTimeout(timer.handle);
|
|
1714
|
+
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
1715
|
+
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
1735
1716
|
resolve4(result);
|
|
1736
1717
|
};
|
|
1737
1718
|
try {
|
|
@@ -1757,6 +1738,25 @@ function runSynchroniser(args, opts) {
|
|
|
1757
1738
|
child.once("close", (code) => {
|
|
1758
1739
|
finish({ code, stdout, stderr, timedOut: false });
|
|
1759
1740
|
});
|
|
1741
|
+
if (opts.signal) {
|
|
1742
|
+
const killChild = () => {
|
|
1743
|
+
if (child.pid === void 0) {
|
|
1744
|
+
if (!spawnListener) {
|
|
1745
|
+
spawnListener = killChild;
|
|
1746
|
+
child.once("spawn", spawnListener);
|
|
1747
|
+
}
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
child.kill("SIGKILL");
|
|
1751
|
+
};
|
|
1752
|
+
abortListener = killChild;
|
|
1753
|
+
if (opts.signal.aborted) {
|
|
1754
|
+
abortListener();
|
|
1755
|
+
} else {
|
|
1756
|
+
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
1757
|
+
if (opts.signal.aborted) abortListener();
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
1760
|
timer.handle = setTimeout(
|
|
1761
1761
|
() => {
|
|
1762
1762
|
child.kill("SIGKILL");
|
|
@@ -2217,9 +2217,9 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
2217
2217
|
}
|
|
2218
2218
|
|
|
2219
2219
|
// src/lib/opencode/session-db-provenance.ts
|
|
2220
|
-
import { createRequire } from "module";
|
|
2221
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2222
|
-
import { dirname as dirname3, join as join3 } from "path";
|
|
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";
|
|
2223
2223
|
var require2 = createRequire(import.meta.url);
|
|
2224
2224
|
function readSessionDbMigrationIds(dbPath) {
|
|
2225
2225
|
let db;
|
|
@@ -2413,6 +2413,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
2413
2413
|
|
|
2414
2414
|
// src/lib/opencode/process.ts
|
|
2415
2415
|
var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
|
|
2416
|
+
var VALID_OPENCODE_LOG_LEVELS = /* @__PURE__ */ new Set(["DEBUG", "INFO", "WARN", "ERROR"]);
|
|
2417
|
+
function resolveOpenCodeLogLevel(env) {
|
|
2418
|
+
const raw = env.OPENCODE_LOG_LEVEL;
|
|
2419
|
+
if (!raw) return "INFO";
|
|
2420
|
+
const upper = raw.toUpperCase();
|
|
2421
|
+
if (VALID_OPENCODE_LOG_LEVELS.has(upper)) return upper;
|
|
2422
|
+
console.warn(
|
|
2423
|
+
`startOpenCode: ignoring invalid OPENCODE_LOG_LEVEL "${raw}" (expected DEBUG|INFO|WARN|ERROR) \u2014 using INFO`
|
|
2424
|
+
);
|
|
2425
|
+
return "INFO";
|
|
2426
|
+
}
|
|
2416
2427
|
function getProcessCwd(pid) {
|
|
2417
2428
|
const platform = process.platform;
|
|
2418
2429
|
try {
|
|
@@ -2461,14 +2472,14 @@ function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
2461
2472
|
}
|
|
2462
2473
|
return null;
|
|
2463
2474
|
}
|
|
2464
|
-
function
|
|
2475
|
+
function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
2465
2476
|
const instances = [];
|
|
2466
2477
|
try {
|
|
2467
2478
|
const platform = process.platform;
|
|
2468
2479
|
if (platform === "darwin" || platform === "linux") {
|
|
2469
2480
|
let pids = [];
|
|
2470
2481
|
try {
|
|
2471
|
-
const pgrepOutput = execSync(
|
|
2482
|
+
const pgrepOutput = execSync(`pgrep -f "${pgrepPattern}"`, {
|
|
2472
2483
|
encoding: "utf-8",
|
|
2473
2484
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2474
2485
|
}).trim();
|
|
@@ -2477,7 +2488,7 @@ function findOpenCodeProcesses() {
|
|
|
2477
2488
|
}
|
|
2478
2489
|
} catch {
|
|
2479
2490
|
try {
|
|
2480
|
-
const psOutput = execSync(
|
|
2491
|
+
const psOutput = execSync(`ps aux | grep -E "${psPattern}" | grep -v grep`, {
|
|
2481
2492
|
encoding: "utf-8",
|
|
2482
2493
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2483
2494
|
}).trim();
|
|
@@ -2523,6 +2534,9 @@ function findOpenCodeProcesses() {
|
|
|
2523
2534
|
}
|
|
2524
2535
|
return instances;
|
|
2525
2536
|
}
|
|
2537
|
+
function findOpenCodeProcesses() {
|
|
2538
|
+
return findProcessesByPattern("opencode serve|opencode-serve", "opencode (serve|--port)");
|
|
2539
|
+
}
|
|
2526
2540
|
async function scanPortsForOpenCode() {
|
|
2527
2541
|
const instances = [];
|
|
2528
2542
|
const checks = OPENCODE_PORT_RANGE.map(async (port) => {
|
|
@@ -2569,7 +2583,7 @@ async function findHealthyOpenCodeInstances() {
|
|
|
2569
2583
|
}
|
|
2570
2584
|
async function startOpenCode(port, options = {}) {
|
|
2571
2585
|
let command = "opencode";
|
|
2572
|
-
const printLogs = options.inheritStdio ? ["--print-logs"] : [];
|
|
2586
|
+
const printLogs = options.inheritStdio ? ["--print-logs", "--log-level", resolveOpenCodeLogLevel(process.env)] : [];
|
|
2573
2587
|
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...printLogs];
|
|
2574
2588
|
try {
|
|
2575
2589
|
execSync("which opencode", { stdio: "ignore" });
|
|
@@ -2626,6 +2640,19 @@ function isOpenCodeInstalled() {
|
|
|
2626
2640
|
return false;
|
|
2627
2641
|
}
|
|
2628
2642
|
}
|
|
2643
|
+
function isOpenCode2Installed() {
|
|
2644
|
+
try {
|
|
2645
|
+
const platform = process.platform;
|
|
2646
|
+
if (platform === "win32") {
|
|
2647
|
+
execSync2("where opencode2", { stdio: "ignore" });
|
|
2648
|
+
} else {
|
|
2649
|
+
execSync2("which opencode2", { stdio: "ignore" });
|
|
2650
|
+
}
|
|
2651
|
+
return true;
|
|
2652
|
+
} catch {
|
|
2653
|
+
return false;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2629
2656
|
async function promptOpenCodeInstall(interactive) {
|
|
2630
2657
|
if (!interactive) {
|
|
2631
2658
|
console.log(
|
|
@@ -2635,7 +2662,11 @@ async function promptOpenCodeInstall(interactive) {
|
|
|
2635
2662
|
install_url: OPENCODE_INSTALL_URL,
|
|
2636
2663
|
install_commands: {
|
|
2637
2664
|
npm: "npm install -g opencode-ai",
|
|
2638
|
-
curl: "curl -fsSL https://opencode.ai/install.sh | sh"
|
|
2665
|
+
curl: "curl -fsSL https://opencode.ai/install.sh | sh",
|
|
2666
|
+
v2: {
|
|
2667
|
+
npm: "npm install -g @opencode-ai/cli@beta",
|
|
2668
|
+
curl: "curl -fsSL https://opencode.ai/v2/install | bash"
|
|
2669
|
+
}
|
|
2639
2670
|
}
|
|
2640
2671
|
})
|
|
2641
2672
|
);
|
|
@@ -3119,21 +3150,112 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
3119
3150
|
}
|
|
3120
3151
|
return lastOk ?? last;
|
|
3121
3152
|
}
|
|
3122
|
-
function
|
|
3123
|
-
if (!messages || messages.length === 0) return
|
|
3124
|
-
const
|
|
3125
|
-
(
|
|
3153
|
+
function collectSubagentSessions(messages, userMessageId) {
|
|
3154
|
+
if (!messages || messages.length === 0) return [];
|
|
3155
|
+
const byParent = messages.filter(
|
|
3156
|
+
(message) => roleOf(message) === "assistant" && parentIdOf(message) === userMessageId
|
|
3126
3157
|
);
|
|
3127
|
-
const
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3158
|
+
const assistants = byParent.length > 0 ? byParent : [];
|
|
3159
|
+
if (assistants.length === 0) {
|
|
3160
|
+
const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
|
|
3161
|
+
if (userIndex === -1) return [];
|
|
3162
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
3163
|
+
const message = messages[i];
|
|
3164
|
+
if (roleOf(message) === "user") break;
|
|
3165
|
+
if (roleOf(message) === "assistant") assistants.push(message);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
const refs = [];
|
|
3169
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3170
|
+
for (const message of assistants) {
|
|
3171
|
+
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
3172
|
+
for (const part of parts) {
|
|
3173
|
+
if (!part || typeof part !== "object" || part.type !== "tool" || part.tool !== "task")
|
|
3174
|
+
continue;
|
|
3175
|
+
const state = part.state;
|
|
3176
|
+
if (!state || typeof state !== "object") continue;
|
|
3177
|
+
const metadata = state.metadata;
|
|
3178
|
+
if (!metadata || typeof metadata !== "object") continue;
|
|
3179
|
+
const sessionId = metadata.sessionId;
|
|
3180
|
+
if (typeof sessionId !== "string" || sessionId.length === 0 || seen.has(sessionId)) continue;
|
|
3181
|
+
seen.add(sessionId);
|
|
3182
|
+
const start = state.time?.start;
|
|
3183
|
+
refs.push({
|
|
3184
|
+
sessionId,
|
|
3185
|
+
startedAtMs: typeof start === "number" && Number.isFinite(start) ? start : null
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3135
3188
|
}
|
|
3136
|
-
|
|
3189
|
+
return refs;
|
|
3190
|
+
}
|
|
3191
|
+
function finiteNumber(value) {
|
|
3192
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3193
|
+
}
|
|
3194
|
+
function taskCallModel(value) {
|
|
3195
|
+
if (!value || typeof value !== "object") return null;
|
|
3196
|
+
const model = value;
|
|
3197
|
+
const modelID = typeof model.modelID === "string" ? model.modelID : void 0;
|
|
3198
|
+
const providerID = typeof model.providerID === "string" ? model.providerID : void 0;
|
|
3199
|
+
return modelID || providerID ? { modelID, providerID } : null;
|
|
3200
|
+
}
|
|
3201
|
+
function collectTaskCalls(messages, userMessageId) {
|
|
3202
|
+
if (!messages || messages.length === 0) return [];
|
|
3203
|
+
const calls = [];
|
|
3204
|
+
for (const message of messages) {
|
|
3205
|
+
if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
|
|
3206
|
+
for (const part of message.parts ?? []) {
|
|
3207
|
+
if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
|
|
3208
|
+
continue;
|
|
3209
|
+
}
|
|
3210
|
+
const rawName = part.state.input?.subagent_type;
|
|
3211
|
+
const subagentName = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : rawName === void 0 ? "general" : "unknown";
|
|
3212
|
+
const metadata = part.state.metadata;
|
|
3213
|
+
calls.push({
|
|
3214
|
+
callID: part.callID,
|
|
3215
|
+
subagentName,
|
|
3216
|
+
childSessionId: typeof metadata?.sessionId === "string" ? metadata.sessionId : null,
|
|
3217
|
+
parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
|
|
3218
|
+
model: taskCallModel(metadata?.model),
|
|
3219
|
+
status: part.state.status ?? "unknown",
|
|
3220
|
+
timeStart: finiteNumber(part.state.time?.start),
|
|
3221
|
+
timeEnd: finiteNumber(part.state.time?.end)
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
return calls;
|
|
3226
|
+
}
|
|
3227
|
+
function attributeTaskCallUsage(messages, windows) {
|
|
3228
|
+
const eligibleWindows = windows.filter(
|
|
3229
|
+
(window) => window.timeStart !== null && Number.isFinite(window.timeStart)
|
|
3230
|
+
);
|
|
3231
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
3232
|
+
for (const window of eligibleWindows) assignments.set(window.callID, []);
|
|
3233
|
+
const unattributed = [];
|
|
3234
|
+
for (const message of messages ?? []) {
|
|
3235
|
+
if (roleOf(message) !== "assistant") continue;
|
|
3236
|
+
const created = finiteNumber(createdOf(message));
|
|
3237
|
+
const matching = created === null ? [] : eligibleWindows.filter(
|
|
3238
|
+
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
3239
|
+
);
|
|
3240
|
+
if (matching.length === 0) {
|
|
3241
|
+
unattributed.push(message);
|
|
3242
|
+
continue;
|
|
3243
|
+
}
|
|
3244
|
+
matching.sort((a, b) => a.timeStart - b.timeStart);
|
|
3245
|
+
assignments.get(matching[0].callID)?.push(message);
|
|
3246
|
+
}
|
|
3247
|
+
return {
|
|
3248
|
+
invocations: eligibleWindows.map((window) => {
|
|
3249
|
+
const assigned = assignments.get(window.callID) ?? [];
|
|
3250
|
+
return { callID: window.callID, messages: assigned, usage: sumAssistantUsage(assigned) };
|
|
3251
|
+
}),
|
|
3252
|
+
unattributed
|
|
3253
|
+
};
|
|
3254
|
+
}
|
|
3255
|
+
function sumAssistantUsage(messages) {
|
|
3256
|
+
if (!messages || messages.length === 0) return null;
|
|
3257
|
+
const nonErrored = messages.filter((message) => errorOf(message) == null);
|
|
3258
|
+
const selected = nonErrored.length > 0 ? nonErrored : messages;
|
|
3137
3259
|
let sawAnyUsage = false;
|
|
3138
3260
|
let inputSum = 0;
|
|
3139
3261
|
let outputSum = 0;
|
|
@@ -3144,7 +3266,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
3144
3266
|
let sawCost = false;
|
|
3145
3267
|
let modelId = null;
|
|
3146
3268
|
let providerId = null;
|
|
3147
|
-
for (const m of
|
|
3269
|
+
for (const m of selected) {
|
|
3148
3270
|
const info = m.info;
|
|
3149
3271
|
if (!info) continue;
|
|
3150
3272
|
const tokens = info.tokens;
|
|
@@ -3179,12 +3301,28 @@ function messageUsage(messages, userMessageId) {
|
|
|
3179
3301
|
usage_tokens_reasoning: reasoningSum,
|
|
3180
3302
|
usage_tokens_cache_read: cacheReadSum,
|
|
3181
3303
|
usage_tokens_cache_write: cacheWriteSum,
|
|
3182
|
-
// NULL means
|
|
3183
|
-
//
|
|
3184
|
-
// `sawCost` true with `costSum === 0`.
|
|
3304
|
+
// NULL means OpenCode never reported a cost; it is distinct from a genuine
|
|
3305
|
+
// zero-cost message, which sets `sawCost` with `costSum === 0`.
|
|
3185
3306
|
usage_cost_usd: sawCost ? costSum : null
|
|
3186
3307
|
};
|
|
3187
3308
|
}
|
|
3309
|
+
function messageUsage(messages, userMessageId) {
|
|
3310
|
+
if (!messages || messages.length === 0) return null;
|
|
3311
|
+
const byParentAll = messages.filter(
|
|
3312
|
+
(m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
|
|
3313
|
+
);
|
|
3314
|
+
const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
|
|
3315
|
+
const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
|
|
3316
|
+
let correlated;
|
|
3317
|
+
if (byParent.length > 0) {
|
|
3318
|
+
correlated = byParent;
|
|
3319
|
+
} else {
|
|
3320
|
+
const reply = findAssistantReplyAfter(messages, userMessageId);
|
|
3321
|
+
correlated = reply ? [reply] : [];
|
|
3322
|
+
}
|
|
3323
|
+
if (correlated.length === 0) return null;
|
|
3324
|
+
return sumAssistantUsage(correlated);
|
|
3325
|
+
}
|
|
3188
3326
|
function messageRunState(messages, userMessageId) {
|
|
3189
3327
|
if (!messages || messages.length === 0) return "unknown";
|
|
3190
3328
|
const hasUser = messages.some((m) => idOf(m) === userMessageId);
|
|
@@ -3247,8 +3385,7 @@ function isAbortedTerminalReply(messages, userMessageId) {
|
|
|
3247
3385
|
}
|
|
3248
3386
|
return false;
|
|
3249
3387
|
}
|
|
3250
|
-
function
|
|
3251
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
3388
|
+
function classifyReplyAuthError(reply) {
|
|
3252
3389
|
const error2 = errorOf(reply);
|
|
3253
3390
|
if (error2 == null || typeof error2 !== "object") return null;
|
|
3254
3391
|
const e = error2;
|
|
@@ -3273,6 +3410,32 @@ function messageFailure(messages, userMessageId) {
|
|
|
3273
3410
|
}
|
|
3274
3411
|
return null;
|
|
3275
3412
|
}
|
|
3413
|
+
function messageFailure(messages, userMessageId) {
|
|
3414
|
+
return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
|
|
3415
|
+
}
|
|
3416
|
+
function findLatestSubagentAuthOutcome(messages, sinceMs) {
|
|
3417
|
+
if (!messages || messages.length === 0) return null;
|
|
3418
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3419
|
+
const message = messages[i];
|
|
3420
|
+
if (roleOf(message) !== "assistant") continue;
|
|
3421
|
+
const created = createdOf(message);
|
|
3422
|
+
if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
|
|
3423
|
+
const failure = classifyReplyAuthError(message);
|
|
3424
|
+
if (failure) {
|
|
3425
|
+
if (!failure.providerId) return null;
|
|
3426
|
+
return { providerId: failure.providerId, outcome: "failed", failure };
|
|
3427
|
+
}
|
|
3428
|
+
const providerId = message.info?.providerID;
|
|
3429
|
+
if (errorOf(message) == null && typeof providerId === "string" && providerId.length > 0) {
|
|
3430
|
+
return { providerId, outcome: "succeeded" };
|
|
3431
|
+
}
|
|
3432
|
+
return null;
|
|
3433
|
+
}
|
|
3434
|
+
return null;
|
|
3435
|
+
}
|
|
3436
|
+
function findSubagentAuthOutcome(messages, sinceMs) {
|
|
3437
|
+
return findLatestSubagentAuthOutcome(messages, sinceMs);
|
|
3438
|
+
}
|
|
3276
3439
|
function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProviderId, replyModelId = null) {
|
|
3277
3440
|
if (classified != null) return classified;
|
|
3278
3441
|
if (hasConfiguredProvider !== false) return null;
|
|
@@ -3289,6 +3452,28 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
|
3289
3452
|
(m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
3290
3453
|
);
|
|
3291
3454
|
}
|
|
3455
|
+
function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
|
|
3456
|
+
if (!messages || messages.length === 0) return false;
|
|
3457
|
+
const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
|
|
3458
|
+
if (userIndex === -1) return false;
|
|
3459
|
+
let hasLaterUser = false;
|
|
3460
|
+
let hasStartedLaterUser = false;
|
|
3461
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
3462
|
+
const message = messages[i];
|
|
3463
|
+
if (roleOf(message) !== "user") continue;
|
|
3464
|
+
hasLaterUser = true;
|
|
3465
|
+
const laterUserMessageId = idOf(message);
|
|
3466
|
+
if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
|
|
3467
|
+
return false;
|
|
3468
|
+
}
|
|
3469
|
+
if (messages.some(
|
|
3470
|
+
(candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
|
|
3471
|
+
)) {
|
|
3472
|
+
hasStartedLaterUser = true;
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
return hasLaterUser && hasStartedLaterUser;
|
|
3476
|
+
}
|
|
3292
3477
|
async function hasAnyConfiguredProvider(port) {
|
|
3293
3478
|
try {
|
|
3294
3479
|
const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
|
|
@@ -3320,6 +3505,94 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3320
3505
|
return null;
|
|
3321
3506
|
}
|
|
3322
3507
|
}
|
|
3508
|
+
function sessionErrorReason(error2) {
|
|
3509
|
+
const record = typeof error2 === "object" && error2 !== null ? error2 : null;
|
|
3510
|
+
const data = record?.data;
|
|
3511
|
+
const dataRecord = typeof data === "object" && data !== null ? data : null;
|
|
3512
|
+
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";
|
|
3513
|
+
const reason = rawReason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
3514
|
+
return reason || "OpenCode reported a session error with no details";
|
|
3515
|
+
}
|
|
3516
|
+
function parseSessionErrorFrame(data) {
|
|
3517
|
+
let parsed;
|
|
3518
|
+
try {
|
|
3519
|
+
parsed = JSON.parse(data);
|
|
3520
|
+
} catch (error2) {
|
|
3521
|
+
void error2;
|
|
3522
|
+
return null;
|
|
3523
|
+
}
|
|
3524
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
3525
|
+
const parsedRecord = parsed;
|
|
3526
|
+
const payload = parsedRecord.payload;
|
|
3527
|
+
const event = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : parsedRecord;
|
|
3528
|
+
if (event.type !== "session.error") return null;
|
|
3529
|
+
const properties = event.properties;
|
|
3530
|
+
if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
|
|
3531
|
+
return null;
|
|
3532
|
+
}
|
|
3533
|
+
const propertiesRecord = properties;
|
|
3534
|
+
const sessionId = propertiesRecord.sessionID;
|
|
3535
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
3536
|
+
return {
|
|
3537
|
+
sessionId,
|
|
3538
|
+
reason: sessionErrorReason(propertiesRecord.error)
|
|
3539
|
+
};
|
|
3540
|
+
}
|
|
3541
|
+
async function readSessionErrorStream(port, options) {
|
|
3542
|
+
let reader = null;
|
|
3543
|
+
try {
|
|
3544
|
+
const response = await fetch(`${opencodeBase(port)}/event`, {
|
|
3545
|
+
headers: { accept: "text/event-stream" },
|
|
3546
|
+
signal: options.signal
|
|
3547
|
+
});
|
|
3548
|
+
if (!response.ok || !response.body) {
|
|
3549
|
+
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
3550
|
+
}
|
|
3551
|
+
reader = response.body.getReader();
|
|
3552
|
+
const decoder = new TextDecoder();
|
|
3553
|
+
let buffer = "";
|
|
3554
|
+
const processLine = (line) => {
|
|
3555
|
+
const trimmed = line.trimEnd();
|
|
3556
|
+
if (!trimmed.startsWith("data:")) return;
|
|
3557
|
+
const event = parseSessionErrorFrame(trimmed.slice("data:".length).replace(/^ /, ""));
|
|
3558
|
+
if (event) options.onSessionError(event);
|
|
3559
|
+
};
|
|
3560
|
+
while (true) {
|
|
3561
|
+
const { done, value } = await reader.read();
|
|
3562
|
+
if (done) return { reason: "ended" };
|
|
3563
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3564
|
+
const lines = buffer.split("\n");
|
|
3565
|
+
buffer = lines.pop() ?? "";
|
|
3566
|
+
for (const line of lines) processLine(line);
|
|
3567
|
+
}
|
|
3568
|
+
} catch (err) {
|
|
3569
|
+
if (options.signal.aborted) return { reason: "aborted" };
|
|
3570
|
+
return {
|
|
3571
|
+
reason: "unavailable",
|
|
3572
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
3573
|
+
};
|
|
3574
|
+
} finally {
|
|
3575
|
+
if (reader) void reader.cancel().catch(() => void 0);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
async function reloadProviderCache(port) {
|
|
3579
|
+
try {
|
|
3580
|
+
const res = await timedFetch(`${opencodeBase(port)}/config`, {
|
|
3581
|
+
method: "PATCH",
|
|
3582
|
+
headers: { "Content-Type": "application/json" },
|
|
3583
|
+
body: JSON.stringify({})
|
|
3584
|
+
});
|
|
3585
|
+
if (!res.ok) {
|
|
3586
|
+
console.error(
|
|
3587
|
+
`[reloadProviderCache] PATCH /config returned HTTP ${res.status} (port ${port})`
|
|
3588
|
+
);
|
|
3589
|
+
}
|
|
3590
|
+
} catch (err) {
|
|
3591
|
+
console.error(
|
|
3592
|
+
`[reloadProviderCache] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
3593
|
+
);
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3323
3596
|
|
|
3324
3597
|
// src/lib/opencode/session-cleanup.ts
|
|
3325
3598
|
var DURATION_UNIT_MS = {
|
|
@@ -3426,8 +3699,8 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
3426
3699
|
}
|
|
3427
3700
|
|
|
3428
3701
|
// src/lib/opencode/session-db-size.ts
|
|
3429
|
-
import { statSync as statSync3 } from "fs";
|
|
3430
|
-
import { join as join4 } from "path";
|
|
3702
|
+
import { statSync as statSync3 } from "node:fs";
|
|
3703
|
+
import { join as join4 } from "node:path";
|
|
3431
3704
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
3432
3705
|
function statSessionDbBytes(homeDir) {
|
|
3433
3706
|
const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
@@ -3457,9 +3730,96 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
3457
3730
|
return null;
|
|
3458
3731
|
}
|
|
3459
3732
|
|
|
3733
|
+
// src/lib/opencode/log-tail.ts
|
|
3734
|
+
import { statSync as statSync4 } from "node:fs";
|
|
3735
|
+
import { homedir as homedir3 } from "node:os";
|
|
3736
|
+
import { join as join5 } from "node:path";
|
|
3737
|
+
import { open as open2, stat } from "node:fs/promises";
|
|
3738
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
3739
|
+
function resolveOpenCodeLogPath(homeDir = homedir3(), env = process.env) {
|
|
3740
|
+
const dataDir = env.XDG_DATA_HOME || join5(homeDir, ".local", "share");
|
|
3741
|
+
return join5(dataDir, "opencode", "log", "opencode.log");
|
|
3742
|
+
}
|
|
3743
|
+
function isEnoent(error2) {
|
|
3744
|
+
return error2?.code === "ENOENT";
|
|
3745
|
+
}
|
|
3746
|
+
function reportFailure(operation, logPath, error2) {
|
|
3747
|
+
console.error(
|
|
3748
|
+
`[opencode-log-tail] ${operation} failed for ${logPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3749
|
+
);
|
|
3750
|
+
}
|
|
3751
|
+
function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
3752
|
+
const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
3753
|
+
let offset = 0;
|
|
3754
|
+
let inode = null;
|
|
3755
|
+
let baselineReady = true;
|
|
3756
|
+
try {
|
|
3757
|
+
const initial = statSync4(logPath);
|
|
3758
|
+
offset = initial.size;
|
|
3759
|
+
inode = initial.ino;
|
|
3760
|
+
} catch (error2) {
|
|
3761
|
+
if (!isEnoent(error2)) {
|
|
3762
|
+
reportFailure("initial stat", logPath, error2);
|
|
3763
|
+
baselineReady = false;
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
let polling = false;
|
|
3767
|
+
let stopped = false;
|
|
3768
|
+
const poll = async () => {
|
|
3769
|
+
if (polling || stopped) return;
|
|
3770
|
+
polling = true;
|
|
3771
|
+
try {
|
|
3772
|
+
let current;
|
|
3773
|
+
try {
|
|
3774
|
+
current = await stat(logPath);
|
|
3775
|
+
} catch (error2) {
|
|
3776
|
+
if (!isEnoent(error2)) reportFailure("stat", logPath, error2);
|
|
3777
|
+
return;
|
|
3778
|
+
}
|
|
3779
|
+
if (!baselineReady) {
|
|
3780
|
+
offset = current.size;
|
|
3781
|
+
inode = current.ino;
|
|
3782
|
+
baselineReady = true;
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
if (inode !== null && current.ino !== inode || current.size < offset) {
|
|
3786
|
+
offset = 0;
|
|
3787
|
+
}
|
|
3788
|
+
inode = current.ino;
|
|
3789
|
+
if (current.size === offset) return;
|
|
3790
|
+
const length = current.size - offset;
|
|
3791
|
+
const fh = await open2(logPath, "r");
|
|
3792
|
+
try {
|
|
3793
|
+
const buf = Buffer.alloc(length);
|
|
3794
|
+
const { bytesRead } = await fh.read(buf, 0, length, offset);
|
|
3795
|
+
offset += bytesRead;
|
|
3796
|
+
if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
|
|
3797
|
+
} finally {
|
|
3798
|
+
await fh.close();
|
|
3799
|
+
}
|
|
3800
|
+
} catch (error2) {
|
|
3801
|
+
if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
|
|
3802
|
+
} finally {
|
|
3803
|
+
polling = false;
|
|
3804
|
+
}
|
|
3805
|
+
};
|
|
3806
|
+
const interval = setInterval(() => void poll(), pollIntervalMs);
|
|
3807
|
+
void poll();
|
|
3808
|
+
return {
|
|
3809
|
+
stop: () => {
|
|
3810
|
+
stopped = true;
|
|
3811
|
+
clearInterval(interval);
|
|
3812
|
+
}
|
|
3813
|
+
};
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3460
3816
|
// src/lib/opencode/session-db-reclaim.ts
|
|
3461
|
-
import { statSync as
|
|
3462
|
-
import { dirname as dirname4 } from "path";
|
|
3817
|
+
import { statSync as statSync5, statfsSync } from "node:fs";
|
|
3818
|
+
import { dirname as dirname4 } from "node:path";
|
|
3819
|
+
function errorMessage(error2) {
|
|
3820
|
+
if (!(error2 instanceof Error)) return String(error2);
|
|
3821
|
+
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
3822
|
+
}
|
|
3463
3823
|
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
3464
3824
|
try {
|
|
3465
3825
|
const fsStats = statfsSync(dirname4(dbPath));
|
|
@@ -3485,17 +3845,17 @@ async function probeReclaimAvailability(input) {
|
|
|
3485
3845
|
const { dbPath, requiredBytes } = input;
|
|
3486
3846
|
let sqlite;
|
|
3487
3847
|
try {
|
|
3488
|
-
sqlite = await import("sqlite");
|
|
3848
|
+
sqlite = await import("node:sqlite");
|
|
3489
3849
|
} catch (err) {
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
return "sqlite-unavailable";
|
|
3850
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
3851
|
+
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
3852
|
+
return { reason: "sqlite-unavailable", detail };
|
|
3494
3853
|
}
|
|
3495
3854
|
let autoVacuum = null;
|
|
3496
3855
|
try {
|
|
3497
3856
|
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
3498
3857
|
try {
|
|
3858
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3499
3859
|
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3500
3860
|
} finally {
|
|
3501
3861
|
db.close();
|
|
@@ -3506,23 +3866,25 @@ async function probeReclaimAvailability(input) {
|
|
|
3506
3866
|
);
|
|
3507
3867
|
}
|
|
3508
3868
|
if (autoVacuum !== 0) return null;
|
|
3509
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
|
|
3869
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
3510
3870
|
}
|
|
3511
3871
|
async function reclaimSessionDbSpace(input) {
|
|
3512
3872
|
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
3513
3873
|
let sqlite;
|
|
3514
3874
|
try {
|
|
3515
|
-
sqlite = await import("sqlite");
|
|
3875
|
+
sqlite = await import("node:sqlite");
|
|
3516
3876
|
} catch (err) {
|
|
3877
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
3517
3878
|
console.warn(
|
|
3518
|
-
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${
|
|
3879
|
+
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
3519
3880
|
);
|
|
3520
|
-
return { ok: false, skipped: "sqlite-unavailable" };
|
|
3881
|
+
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
3521
3882
|
}
|
|
3522
3883
|
const { DatabaseSync } = sqlite;
|
|
3523
3884
|
let db;
|
|
3524
3885
|
try {
|
|
3525
3886
|
db = new DatabaseSync(dbPath);
|
|
3887
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3526
3888
|
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3527
3889
|
if (autoVacuum === 0) {
|
|
3528
3890
|
if (!allowFullVacuum) {
|
|
@@ -3531,7 +3893,7 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3531
3893
|
);
|
|
3532
3894
|
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
3533
3895
|
}
|
|
3534
|
-
const fileBytesForGuard =
|
|
3896
|
+
const fileBytesForGuard = statSync5(dbPath).size;
|
|
3535
3897
|
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
3536
3898
|
if (skipReason !== null) {
|
|
3537
3899
|
console.warn(
|
|
@@ -3559,10 +3921,12 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3559
3921
|
);
|
|
3560
3922
|
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
3561
3923
|
} catch (err) {
|
|
3562
|
-
console.error(
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3924
|
+
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
3925
|
+
return {
|
|
3926
|
+
ok: false,
|
|
3927
|
+
skipped: "reclaim-error",
|
|
3928
|
+
detail: errorMessage(err)
|
|
3929
|
+
};
|
|
3566
3930
|
} finally {
|
|
3567
3931
|
db?.close();
|
|
3568
3932
|
}
|
|
@@ -3603,7 +3967,6 @@ var StreamForwarder = class {
|
|
|
3603
3967
|
handleFrame(frame) {
|
|
3604
3968
|
switch (frame.type) {
|
|
3605
3969
|
case "open":
|
|
3606
|
-
this.callbacks.onOpen?.(frame.sid, frame.method, frame.path);
|
|
3607
3970
|
void this.handleOpen(frame);
|
|
3608
3971
|
break;
|
|
3609
3972
|
case "req_data":
|
|
@@ -3639,12 +4002,21 @@ var StreamForwarder = class {
|
|
|
3639
4002
|
const { sid, method, path, headers, has_body } = frame;
|
|
3640
4003
|
const correlationId = headers?.[CORRELATION_ID_HEADER];
|
|
3641
4004
|
const startedAt = Date.now();
|
|
4005
|
+
if (path !== TUNNEL_DRAIN_PING_PATH && path !== TUNNEL_USAGE_REARM_PING_PATH) {
|
|
4006
|
+
this.callbacks.onOpen?.(sid, method, path);
|
|
4007
|
+
}
|
|
3642
4008
|
if (path === TUNNEL_DRAIN_PING_PATH) {
|
|
3643
4009
|
this.callbacks.onDrainPing?.();
|
|
3644
4010
|
this.send({ type: "head", sid, status: 204, headers: {} });
|
|
3645
4011
|
this.send({ type: "res_end", sid });
|
|
3646
4012
|
return;
|
|
3647
4013
|
}
|
|
4014
|
+
if (path === TUNNEL_USAGE_REARM_PING_PATH) {
|
|
4015
|
+
this.callbacks.onUsageRearmPing?.();
|
|
4016
|
+
this.send({ type: "head", sid, status: 204, headers: {} });
|
|
4017
|
+
this.send({ type: "res_end", sid });
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
3648
4020
|
if (process.env.DEBUG) {
|
|
3649
4021
|
log("debug", "agent_request", {
|
|
3650
4022
|
correlation_id: correlationId,
|
|
@@ -3789,7 +4161,8 @@ function connectTunnel(options) {
|
|
|
3789
4161
|
onResponse,
|
|
3790
4162
|
onInfo,
|
|
3791
4163
|
onWarning,
|
|
3792
|
-
onDrainPing
|
|
4164
|
+
onDrainPing,
|
|
4165
|
+
onUsageRearmPing
|
|
3793
4166
|
} = options;
|
|
3794
4167
|
const tunnelUrl = getTunnelUrlConfig();
|
|
3795
4168
|
const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
|
|
@@ -3801,7 +4174,8 @@ function connectTunnel(options) {
|
|
|
3801
4174
|
});
|
|
3802
4175
|
const forwarder = new StreamForwarder(ws, port, {
|
|
3803
4176
|
onHead: () => onResponse?.(),
|
|
3804
|
-
onDrainPing: () => onDrainPing?.()
|
|
4177
|
+
onDrainPing: () => onDrainPing?.(),
|
|
4178
|
+
onUsageRearmPing: () => onUsageRearmPing?.()
|
|
3805
4179
|
});
|
|
3806
4180
|
const connectionTimeout = setTimeout(() => {
|
|
3807
4181
|
ws.close();
|
|
@@ -3844,8 +4218,8 @@ function connectTunnel(options) {
|
|
|
3844
4218
|
try {
|
|
3845
4219
|
message = JSON.parse(data.toString());
|
|
3846
4220
|
} catch (error2) {
|
|
3847
|
-
const
|
|
3848
|
-
onError?.(`Failed to handle message: ${
|
|
4221
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
4222
|
+
onError?.(`Failed to handle message: ${errorMessage3}`);
|
|
3849
4223
|
return;
|
|
3850
4224
|
}
|
|
3851
4225
|
if (isStreamFrame(message)) {
|
|
@@ -3962,6 +4336,7 @@ var RunnerConnection = class {
|
|
|
3962
4336
|
onError: (error2) => events.onError?.(error2),
|
|
3963
4337
|
onResponse: () => events.onResponse?.(),
|
|
3964
4338
|
onDrainPing: () => events.onDrainPing?.(),
|
|
4339
|
+
onUsageRearmPing: () => events.onUsageRearmPing?.(),
|
|
3965
4340
|
onInfo: (message) => events.onInfo?.(message),
|
|
3966
4341
|
onWarning: (message) => events.onWarning?.(message)
|
|
3967
4342
|
});
|
|
@@ -3988,7 +4363,7 @@ var RunnerConnection = class {
|
|
|
3988
4363
|
};
|
|
3989
4364
|
|
|
3990
4365
|
// src/lib/tunnel/ready-marker.ts
|
|
3991
|
-
import { writeFileSync as writeFileSync3 } from "fs";
|
|
4366
|
+
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
3992
4367
|
function writeTunnelReadyMarker(path, agentId) {
|
|
3993
4368
|
try {
|
|
3994
4369
|
writeFileSync3(path, `${agentId}
|
|
@@ -4000,7 +4375,7 @@ function writeTunnelReadyMarker(path, agentId) {
|
|
|
4000
4375
|
}
|
|
4001
4376
|
|
|
4002
4377
|
// src/lib/replication.ts
|
|
4003
|
-
import { spawn as spawn4 } from "child_process";
|
|
4378
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
4004
4379
|
function startSessionDbReplication(configPath) {
|
|
4005
4380
|
return spawn4("litestream", ["replicate", "-config", configPath], {
|
|
4006
4381
|
stdio: "inherit"
|
|
@@ -4016,7 +4391,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
4016
4391
|
}
|
|
4017
4392
|
|
|
4018
4393
|
// src/lib/process-liveness.ts
|
|
4019
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
4394
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
4020
4395
|
function isProcessAlive(pid) {
|
|
4021
4396
|
try {
|
|
4022
4397
|
process.kill(pid, 0);
|
|
@@ -4042,9 +4417,9 @@ function isProcessAlive(pid) {
|
|
|
4042
4417
|
}
|
|
4043
4418
|
|
|
4044
4419
|
// src/lib/openai-usage.ts
|
|
4045
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
4046
|
-
import { homedir as
|
|
4047
|
-
import { join as
|
|
4420
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
4421
|
+
import { homedir as homedir4 } from "node:os";
|
|
4422
|
+
import { join as join6 } from "node:path";
|
|
4048
4423
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4049
4424
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4050
4425
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4058,7 +4433,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4058
4433
|
}
|
|
4059
4434
|
function readOpenCodeChatGptCredentials() {
|
|
4060
4435
|
try {
|
|
4061
|
-
const raw = readFileSync5(
|
|
4436
|
+
const raw = readFileSync5(join6(homedir4(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4062
4437
|
let parsed;
|
|
4063
4438
|
try {
|
|
4064
4439
|
parsed = JSON.parse(raw);
|
|
@@ -4080,6 +4455,23 @@ function readOpenCodeChatGptCredentials() {
|
|
|
4080
4455
|
return null;
|
|
4081
4456
|
}
|
|
4082
4457
|
}
|
|
4458
|
+
function parseChatGptIdentity(accessToken) {
|
|
4459
|
+
const segments = accessToken.split(".");
|
|
4460
|
+
if (segments.length !== 3) return null;
|
|
4461
|
+
let payload;
|
|
4462
|
+
try {
|
|
4463
|
+
const parsed = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf8"));
|
|
4464
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
4465
|
+
payload = parsed;
|
|
4466
|
+
} catch {
|
|
4467
|
+
return null;
|
|
4468
|
+
}
|
|
4469
|
+
const profile = payload["https://api.openai.com/profile"];
|
|
4470
|
+
const auth = payload["https://api.openai.com/auth"];
|
|
4471
|
+
const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
|
|
4472
|
+
const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
|
|
4473
|
+
return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
|
|
4474
|
+
}
|
|
4083
4475
|
function toWindow2(headers, name) {
|
|
4084
4476
|
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
4085
4477
|
const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
|
|
@@ -4155,6 +4547,7 @@ async function getOpenAiUsage(port) {
|
|
|
4155
4547
|
"credentials_expired"
|
|
4156
4548
|
);
|
|
4157
4549
|
}
|
|
4550
|
+
const subscription = parseChatGptIdentity(credentials2.accessToken);
|
|
4158
4551
|
const models = await resolveProbeModels(port);
|
|
4159
4552
|
if (models.length === 0) {
|
|
4160
4553
|
throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
|
|
@@ -4187,7 +4580,7 @@ async function getOpenAiUsage(port) {
|
|
|
4187
4580
|
"no_usable_window"
|
|
4188
4581
|
);
|
|
4189
4582
|
}
|
|
4190
|
-
return usage;
|
|
4583
|
+
return { ...usage, subscription };
|
|
4191
4584
|
}
|
|
4192
4585
|
if (res.status === 401) {
|
|
4193
4586
|
throw new OpenAiUsageError(
|
|
@@ -4259,13 +4652,6 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
|
|
|
4259
4652
|
envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
4260
4653
|
});
|
|
4261
4654
|
}
|
|
4262
|
-
function nextReportDelayMs(random = Math.random) {
|
|
4263
|
-
return usageReportDelayMs(random);
|
|
4264
|
-
}
|
|
4265
|
-
var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
|
|
4266
|
-
function claudeUsageFailureLogLevel(consecutiveFailures) {
|
|
4267
|
-
return usageReportFailureLogLevel(consecutiveFailures);
|
|
4268
|
-
}
|
|
4269
4655
|
|
|
4270
4656
|
// src/lib/openai-usage-reporting.ts
|
|
4271
4657
|
function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
@@ -4302,8 +4688,8 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
4302
4688
|
}
|
|
4303
4689
|
|
|
4304
4690
|
// src/lib/resource-usage.ts
|
|
4305
|
-
import { cpus, totalmem, freemem } from "os";
|
|
4306
|
-
import { statfsSync as statfsSync2 } from "fs";
|
|
4691
|
+
import { cpus, totalmem, freemem } from "node:os";
|
|
4692
|
+
import { statfsSync as statfsSync2 } from "node:fs";
|
|
4307
4693
|
|
|
4308
4694
|
// src/lib/ecs-task-metadata.ts
|
|
4309
4695
|
var ECS_METADATA_TIMEOUT_MS = 2e3;
|
|
@@ -4388,58 +4774,97 @@ function readDisk(homeDir) {
|
|
|
4388
4774
|
};
|
|
4389
4775
|
}
|
|
4390
4776
|
}
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4777
|
+
var CPU_PEAK_WINDOW_MS = 6e4;
|
|
4778
|
+
var CPU_PEAK_WINDOW_SAMPLE_COUNT = 4;
|
|
4779
|
+
var CPU_PEAK_SAMPLE_INTERVAL_MS = CPU_PEAK_WINDOW_MS / CPU_PEAK_WINDOW_SAMPLE_COUNT;
|
|
4780
|
+
function createCpuPeakSampler() {
|
|
4781
|
+
const sampleHistory = new Array(CPU_PEAK_WINDOW_SAMPLE_COUNT);
|
|
4782
|
+
sampleHistory[0] = readCpuSample();
|
|
4783
|
+
let nextSampleIndex = 1;
|
|
4784
|
+
let sampleCount = 1;
|
|
4785
|
+
let peak = null;
|
|
4786
|
+
const timer = setInterval(() => {
|
|
4394
4787
|
const current = readCpuSample();
|
|
4395
|
-
const
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
const warnings = [];
|
|
4402
|
-
if (disk.warning) warnings.push(disk.warning);
|
|
4403
|
-
if (ecsWarning) warnings.push(ecsWarning);
|
|
4404
|
-
let cpuPercent = hostCpuPercent;
|
|
4405
|
-
let cpuCount = hostCpuCount;
|
|
4406
|
-
let memoryTotalBytes = totalmem();
|
|
4407
|
-
let memoryAvailableBytes = freemem();
|
|
4408
|
-
if (limits !== null) {
|
|
4409
|
-
cpuCount = limits.cpuCount;
|
|
4410
|
-
memoryTotalBytes = limits.memoryTotalBytes;
|
|
4411
|
-
memoryAvailableBytes = clamp(
|
|
4412
|
-
limits.memoryTotalBytes - (totalmem() - freemem()),
|
|
4413
|
-
0,
|
|
4414
|
-
limits.memoryTotalBytes
|
|
4415
|
-
);
|
|
4416
|
-
cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
|
|
4788
|
+
const sampleFromWindowAgo = sampleCount === CPU_PEAK_WINDOW_SAMPLE_COUNT ? sampleHistory[nextSampleIndex] : void 0;
|
|
4789
|
+
if (sampleFromWindowAgo !== void 0) {
|
|
4790
|
+
const percentage = cpuPercentBetween(sampleFromWindowAgo, current);
|
|
4791
|
+
if (percentage !== null) {
|
|
4792
|
+
peak = peak === null ? percentage : Math.max(peak, percentage);
|
|
4793
|
+
}
|
|
4417
4794
|
}
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4795
|
+
sampleHistory[nextSampleIndex] = current;
|
|
4796
|
+
nextSampleIndex = (nextSampleIndex + 1) % CPU_PEAK_WINDOW_SAMPLE_COUNT;
|
|
4797
|
+
sampleCount = Math.min(sampleCount + 1, CPU_PEAK_WINDOW_SAMPLE_COUNT);
|
|
4798
|
+
}, CPU_PEAK_SAMPLE_INTERVAL_MS);
|
|
4799
|
+
return {
|
|
4800
|
+
takeAndReset: () => {
|
|
4801
|
+
const currentPeak = peak;
|
|
4802
|
+
peak = null;
|
|
4803
|
+
return currentPeak;
|
|
4804
|
+
},
|
|
4805
|
+
stop: () => clearInterval(timer)
|
|
4806
|
+
};
|
|
4807
|
+
}
|
|
4808
|
+
function createResourceUsageCollector(homeDir) {
|
|
4809
|
+
let previous = readCpuSample();
|
|
4810
|
+
const cpuPeakSampler = createCpuPeakSampler();
|
|
4811
|
+
return {
|
|
4812
|
+
collect: async () => {
|
|
4813
|
+
const current = readCpuSample();
|
|
4814
|
+
const hostCpuPercent = cpuPercentBetween(previous, current);
|
|
4815
|
+
const hostCpuPeakPercent = cpuPeakSampler.takeAndReset();
|
|
4816
|
+
const hostCpuCount = cpus().length;
|
|
4817
|
+
previous = current;
|
|
4818
|
+
const disk = readDisk(homeDir);
|
|
4819
|
+
const opencodeDbBytes = statSessionDbBytes(homeDir);
|
|
4820
|
+
const { limits, warning: ecsWarning } = await readEcsTaskLimits(process.env);
|
|
4821
|
+
const warnings = [];
|
|
4822
|
+
if (disk.warning) warnings.push(disk.warning);
|
|
4823
|
+
if (ecsWarning) warnings.push(ecsWarning);
|
|
4824
|
+
let cpuPercent = hostCpuPercent;
|
|
4825
|
+
let cpuPeakPercent = hostCpuPeakPercent;
|
|
4826
|
+
let cpuCount = hostCpuCount;
|
|
4827
|
+
let memoryTotalBytes = totalmem();
|
|
4828
|
+
let memoryAvailableBytes = freemem();
|
|
4829
|
+
if (limits !== null) {
|
|
4830
|
+
cpuCount = limits.cpuCount;
|
|
4831
|
+
memoryTotalBytes = limits.memoryTotalBytes;
|
|
4832
|
+
memoryAvailableBytes = clamp(
|
|
4833
|
+
limits.memoryTotalBytes - (totalmem() - freemem()),
|
|
4834
|
+
0,
|
|
4835
|
+
limits.memoryTotalBytes
|
|
4836
|
+
);
|
|
4837
|
+
cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
|
|
4838
|
+
cpuPeakPercent = hostCpuPeakPercent === null ? null : clamp(round2(hostCpuPeakPercent * hostCpuCount / limits.cpuCount), 0, 100);
|
|
4839
|
+
}
|
|
4840
|
+
return {
|
|
4841
|
+
usage: {
|
|
4842
|
+
cpuPercent,
|
|
4843
|
+
cpuPeakPercent,
|
|
4844
|
+
cpuCount,
|
|
4845
|
+
memoryTotalBytes,
|
|
4846
|
+
memoryAvailableBytes,
|
|
4847
|
+
diskTotalBytes: disk.totalBytes,
|
|
4848
|
+
diskFreeBytes: disk.freeBytes,
|
|
4849
|
+
opencodeDbBytes
|
|
4850
|
+
},
|
|
4851
|
+
warnings
|
|
4852
|
+
};
|
|
4853
|
+
},
|
|
4854
|
+
stop: cpuPeakSampler.stop
|
|
4430
4855
|
};
|
|
4431
4856
|
}
|
|
4432
4857
|
|
|
4433
4858
|
// src/lib/channels/driver.ts
|
|
4434
|
-
import { homedir as
|
|
4859
|
+
import { homedir as homedir5 } from "node:os";
|
|
4435
4860
|
|
|
4436
4861
|
// src/lib/runner-file-sync.ts
|
|
4437
|
-
import { join as
|
|
4862
|
+
import { join as join8 } from "node:path";
|
|
4438
4863
|
|
|
4439
4864
|
// src/lib/file-push.ts
|
|
4440
|
-
import { randomUUID } from "crypto";
|
|
4441
|
-
import { chmod, mkdir, open as
|
|
4442
|
-
import { basename, dirname as dirname5, isAbsolute, join as
|
|
4865
|
+
import { randomUUID } from "node:crypto";
|
|
4866
|
+
import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
|
|
4867
|
+
import { basename, dirname as dirname5, isAbsolute, join as join7, relative, resolve as resolve2, sep } from "node:path";
|
|
4443
4868
|
var FILE_MODE = 384;
|
|
4444
4869
|
var DIRECTORY_MODE = 448;
|
|
4445
4870
|
async function writePushedFile(request) {
|
|
@@ -4472,7 +4897,7 @@ async function writePushedFile(request) {
|
|
|
4472
4897
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
4473
4898
|
dirname5(candidate)
|
|
4474
4899
|
);
|
|
4475
|
-
const realTarget =
|
|
4900
|
+
const realTarget = join7(existingAncestor, ...missingSegments, basename(candidate));
|
|
4476
4901
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
4477
4902
|
if (allowedDirectory === null) {
|
|
4478
4903
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -4508,7 +4933,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
4508
4933
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
4509
4934
|
return null;
|
|
4510
4935
|
}
|
|
4511
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4936
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4512
4937
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
4513
4938
|
return null;
|
|
4514
4939
|
}
|
|
@@ -4581,16 +5006,16 @@ function contains(realDirectory, realTarget) {
|
|
|
4581
5006
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
4582
5007
|
let current = existingAncestor;
|
|
4583
5008
|
for (const segment of missingSegments) {
|
|
4584
|
-
current =
|
|
5009
|
+
current = join7(current, segment);
|
|
4585
5010
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
4586
5011
|
await chmod(current, DIRECTORY_MODE);
|
|
4587
5012
|
}
|
|
4588
5013
|
}
|
|
4589
5014
|
async function writeAtomically(realTarget, content) {
|
|
4590
|
-
const temporaryPath =
|
|
5015
|
+
const temporaryPath = join7(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
4591
5016
|
let handle;
|
|
4592
5017
|
try {
|
|
4593
|
-
handle = await
|
|
5018
|
+
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
4594
5019
|
await handle.writeFile(content);
|
|
4595
5020
|
await handle.chmod(FILE_MODE);
|
|
4596
5021
|
await handle.close();
|
|
@@ -4717,12 +5142,12 @@ var NOT_APPLIED = {
|
|
|
4717
5142
|
opencodeAuthApplied: false
|
|
4718
5143
|
};
|
|
4719
5144
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
4720
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4721
|
-
return expanded ===
|
|
5145
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5146
|
+
return expanded === join8(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
4722
5147
|
}
|
|
4723
5148
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
4724
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4725
|
-
return expanded ===
|
|
5149
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5150
|
+
return expanded === join8(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
4726
5151
|
}
|
|
4727
5152
|
async function applyOne(options, file) {
|
|
4728
5153
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -4882,6 +5307,10 @@ var DEFAULT_RETRY_POLICY = {
|
|
|
4882
5307
|
baseDelayMs: 500,
|
|
4883
5308
|
maxDelayMs: 3e4
|
|
4884
5309
|
};
|
|
5310
|
+
var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
|
|
5311
|
+
var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
|
|
5312
|
+
var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
5313
|
+
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
4885
5314
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
4886
5315
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
4887
5316
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
@@ -5022,6 +5451,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5022
5451
|
* message; it is removed once its in-flight set empties.
|
|
5023
5452
|
*/
|
|
5024
5453
|
watchers = /* @__PURE__ */ new Map();
|
|
5454
|
+
sessionErrorStream = null;
|
|
5455
|
+
/**
|
|
5456
|
+
* Session-error failures currently being reported; entries are empty at rest
|
|
5457
|
+
* because each handoff deletes its id in `finally`.
|
|
5458
|
+
*/
|
|
5459
|
+
sessionErrorHandled = /* @__PURE__ */ new Set();
|
|
5460
|
+
/**
|
|
5461
|
+
* Session errors that arrived before their dispatch was registered. Bounded FIFO
|
|
5462
|
+
* with a short TTL so an unmatched session cannot retain an event indefinitely.
|
|
5463
|
+
*/
|
|
5464
|
+
bufferedSessionErrors = /* @__PURE__ */ new Map();
|
|
5025
5465
|
/**
|
|
5026
5466
|
* AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
|
|
5027
5467
|
* dispatched and are still in-flight. A message in this set is never
|
|
@@ -5205,6 +5645,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5205
5645
|
* no watcher) can resolve the title.
|
|
5206
5646
|
*/
|
|
5207
5647
|
sessionTitles = /* @__PURE__ */ new Map();
|
|
5648
|
+
/** One best-effort terminal subagent collection per Evident message id. */
|
|
5649
|
+
subagentInvocationCollections = /* @__PURE__ */ new Map();
|
|
5650
|
+
/**
|
|
5651
|
+
* Early snapshots are only liveness hints; they must not become the terminal
|
|
5652
|
+
* collection when the task parts or child transcript have advanced.
|
|
5653
|
+
*/
|
|
5654
|
+
subagentInvocationPrefetches = /* @__PURE__ */ new Map();
|
|
5208
5655
|
/** Serialises drains so a reconnect during a drain doesn't double-process. */
|
|
5209
5656
|
draining = false;
|
|
5210
5657
|
/**
|
|
@@ -5249,6 +5696,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5249
5696
|
* and stops opencode.
|
|
5250
5697
|
*/
|
|
5251
5698
|
stopped = false;
|
|
5699
|
+
recycleRequestedFlag = false;
|
|
5252
5700
|
constructor(config) {
|
|
5253
5701
|
this.agentId = config.agentId;
|
|
5254
5702
|
this.port = config.port;
|
|
@@ -5268,7 +5716,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5268
5716
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
5269
5717
|
this.now = config.now ?? (() => Date.now());
|
|
5270
5718
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
5271
|
-
this.homeDir = config.homeDir ??
|
|
5719
|
+
this.homeDir = config.homeDir ?? homedir5();
|
|
5272
5720
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
5273
5721
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
5274
5722
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
@@ -5354,6 +5802,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5354
5802
|
let dispatched = 0;
|
|
5355
5803
|
try {
|
|
5356
5804
|
const conversations = await this.getPendingConversations();
|
|
5805
|
+
if (this.recycleRequestedFlag) {
|
|
5806
|
+
this.stop();
|
|
5807
|
+
}
|
|
5357
5808
|
if (conversations.length > 0) {
|
|
5358
5809
|
const total = conversations.reduce((sum, c) => sum + (c.pending_message_count ?? 0), 0);
|
|
5359
5810
|
this.log({
|
|
@@ -5415,6 +5866,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5415
5866
|
}
|
|
5416
5867
|
return ids;
|
|
5417
5868
|
}
|
|
5869
|
+
/**
|
|
5870
|
+
* OpenCode user-message ids tracked for other Evident messages in a session.
|
|
5871
|
+
* Excluding this message makes an unattributed later row fail safe; a missing
|
|
5872
|
+
* watcher yields no attributions, per `hasLaterSiblingTurnStarted`'s docblock.
|
|
5873
|
+
*/
|
|
5874
|
+
siblingOpencodeMessageIds(watcher, ownEvidentMessageId) {
|
|
5875
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5876
|
+
if (!watcher) return ids;
|
|
5877
|
+
for (const inFlight of watcher.inFlight.values()) {
|
|
5878
|
+
if (inFlight.evidentMessageId !== ownEvidentMessageId) {
|
|
5879
|
+
ids.add(inFlight.opencodeMessageId);
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
return ids;
|
|
5883
|
+
}
|
|
5418
5884
|
/**
|
|
5419
5885
|
* File-pull work, for `run.ts`'s idle accounting (#559).
|
|
5420
5886
|
*
|
|
@@ -5481,6 +5947,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5481
5947
|
*/
|
|
5482
5948
|
stop() {
|
|
5483
5949
|
this.stopped = true;
|
|
5950
|
+
this.sessionErrorStream?.abort.abort();
|
|
5951
|
+
this.sessionErrorStream = null;
|
|
5952
|
+
}
|
|
5953
|
+
/**
|
|
5954
|
+
* The server clears this request when a new MicroVM identity is recorded, so a
|
|
5955
|
+
* same-VM tunnel reconnect does not consume it. This is a plain read rather
|
|
5956
|
+
* than a consume; `run.ts` guards the action once-only.
|
|
5957
|
+
*/
|
|
5958
|
+
get recycleRequested() {
|
|
5959
|
+
return this.recycleRequestedFlag;
|
|
5484
5960
|
}
|
|
5485
5961
|
/**
|
|
5486
5962
|
* Wait (up to `timeoutMs`) for in-flight watcher work to settle during a
|
|
@@ -5547,6 +6023,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5547
6023
|
*/
|
|
5548
6024
|
async processConversation(conv) {
|
|
5549
6025
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
6026
|
+
this.ensureSessionErrorStream();
|
|
5550
6027
|
const messages = await this.getPendingMessages(conv.id);
|
|
5551
6028
|
let dispatched = 0;
|
|
5552
6029
|
let skippedAlreadyDispatched = 0;
|
|
@@ -5619,7 +6096,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5619
6096
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
5620
6097
|
break;
|
|
5621
6098
|
}
|
|
5622
|
-
const
|
|
6099
|
+
const errorMessage3 = err instanceof Error ? err.message : String(err);
|
|
5623
6100
|
this.sessions.delete(conv.id);
|
|
5624
6101
|
this.supersede(conv.id, sessionId);
|
|
5625
6102
|
this.log({
|
|
@@ -5628,7 +6105,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5628
6105
|
conversation_id: conv.id,
|
|
5629
6106
|
message_id: message.id
|
|
5630
6107
|
});
|
|
5631
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6108
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5632
6109
|
this.log({
|
|
5633
6110
|
level: "warn",
|
|
5634
6111
|
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -5639,7 +6116,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5639
6116
|
});
|
|
5640
6117
|
this.log({
|
|
5641
6118
|
level: "error",
|
|
5642
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
6119
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
|
|
5643
6120
|
conversation_id: conv.id,
|
|
5644
6121
|
message_id: message.id
|
|
5645
6122
|
});
|
|
@@ -5660,14 +6137,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5660
6137
|
this.unconfirmedDispatchFailures.delete(message.id);
|
|
5661
6138
|
this.sessions.delete(conv.id);
|
|
5662
6139
|
this.supersede(conv.id, sessionId);
|
|
5663
|
-
const
|
|
6140
|
+
const errorMessage3 = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
|
|
5664
6141
|
this.log({
|
|
5665
6142
|
level: "error",
|
|
5666
|
-
message:
|
|
6143
|
+
message: errorMessage3,
|
|
5667
6144
|
conversation_id: conv.id,
|
|
5668
6145
|
message_id: message.id
|
|
5669
6146
|
});
|
|
5670
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6147
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5671
6148
|
this.log({
|
|
5672
6149
|
level: "warn",
|
|
5673
6150
|
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -5893,6 +6370,23 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5893
6370
|
if (state === "running" || state === "queued") {
|
|
5894
6371
|
const ongoing = await isSessionOngoing(this.port, sessionId);
|
|
5895
6372
|
if (ongoing === true) {
|
|
6373
|
+
if (state === "queued") {
|
|
6374
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(
|
|
6375
|
+
this.watchers.get(sessionId),
|
|
6376
|
+
message.id
|
|
6377
|
+
);
|
|
6378
|
+
if (hasLaterSiblingTurnStarted(messages, ocId ?? "", siblingOcIds)) {
|
|
6379
|
+
this.log({
|
|
6380
|
+
level: "warn",
|
|
6381
|
+
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`,
|
|
6382
|
+
conversation_id: conv.id,
|
|
6383
|
+
message_id: message.id
|
|
6384
|
+
});
|
|
6385
|
+
this.clearRedriveUnresolved(message.id);
|
|
6386
|
+
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
6387
|
+
return "dispatch";
|
|
6388
|
+
}
|
|
6389
|
+
}
|
|
5896
6390
|
return this.reattachRedrive(conv, sessionId, message, ocId);
|
|
5897
6391
|
}
|
|
5898
6392
|
if (ongoing === false) {
|
|
@@ -5982,16 +6476,37 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5982
6476
|
if (state === "done") {
|
|
5983
6477
|
const title = await this.resolveSessionTitle(sessionId, conv.id);
|
|
5984
6478
|
const usage = messageUsage(messages, ocId ?? "");
|
|
6479
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
6480
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
6481
|
+
messages,
|
|
6482
|
+
ocId ?? "",
|
|
6483
|
+
message.id
|
|
6484
|
+
);
|
|
5985
6485
|
this.log({
|
|
5986
6486
|
level: "info",
|
|
5987
6487
|
message: `Re-drive: message ${message.id.slice(0, 8)} completed while its row was wrongly reclaimed to pending \u2014 marking done instead of re-dispatching`,
|
|
5988
6488
|
conversation_id: conv.id,
|
|
5989
6489
|
message_id: message.id
|
|
5990
6490
|
});
|
|
5991
|
-
await this.markDone(
|
|
6491
|
+
await this.markDone(
|
|
6492
|
+
conv.id,
|
|
6493
|
+
message.id,
|
|
6494
|
+
sessionId,
|
|
6495
|
+
ocId,
|
|
6496
|
+
title,
|
|
6497
|
+
usage,
|
|
6498
|
+
usageAgentName,
|
|
6499
|
+
subagentInvocations
|
|
6500
|
+
);
|
|
5992
6501
|
} else {
|
|
5993
6502
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
5994
6503
|
const usage = messageUsage(messages, ocId ?? "");
|
|
6504
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
6505
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
6506
|
+
messages,
|
|
6507
|
+
ocId ?? "",
|
|
6508
|
+
message.id
|
|
6509
|
+
);
|
|
5995
6510
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
5996
6511
|
this.log({
|
|
5997
6512
|
level: "error",
|
|
@@ -5999,7 +6514,19 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5999
6514
|
conversation_id: conv.id,
|
|
6000
6515
|
message_id: message.id
|
|
6001
6516
|
});
|
|
6002
|
-
await this.markFailed(
|
|
6517
|
+
await this.markFailed(
|
|
6518
|
+
conv.id,
|
|
6519
|
+
message.id,
|
|
6520
|
+
sessionId,
|
|
6521
|
+
error2,
|
|
6522
|
+
usage,
|
|
6523
|
+
failure,
|
|
6524
|
+
usageAgentName,
|
|
6525
|
+
subagentInvocations
|
|
6526
|
+
);
|
|
6527
|
+
}
|
|
6528
|
+
if (ocId !== null) {
|
|
6529
|
+
await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
|
|
6003
6530
|
}
|
|
6004
6531
|
} catch (err) {
|
|
6005
6532
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -6540,6 +7067,24 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6540
7067
|
ambiguousPinnedSinceMs: 0,
|
|
6541
7068
|
ambiguousResolved: false
|
|
6542
7069
|
});
|
|
7070
|
+
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
7071
|
+
if (!buffered) return;
|
|
7072
|
+
this.bufferedSessionErrors.delete(sessionId);
|
|
7073
|
+
if (this.now() - buffered.receivedAt < SESSION_ERROR_BUFFER_TTL_MS) {
|
|
7074
|
+
this.handleSessionError(buffered.event);
|
|
7075
|
+
}
|
|
7076
|
+
}
|
|
7077
|
+
bufferSessionError(event) {
|
|
7078
|
+
this.bufferedSessionErrors.delete(event.sessionId);
|
|
7079
|
+
this.bufferedSessionErrors.set(event.sessionId, {
|
|
7080
|
+
event,
|
|
7081
|
+
receivedAt: this.now()
|
|
7082
|
+
});
|
|
7083
|
+
while (this.bufferedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
|
|
7084
|
+
const oldest = this.bufferedSessionErrors.keys().next().value;
|
|
7085
|
+
if (typeof oldest !== "string") break;
|
|
7086
|
+
this.bufferedSessionErrors.delete(oldest);
|
|
7087
|
+
}
|
|
6543
7088
|
}
|
|
6544
7089
|
/**
|
|
6545
7090
|
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
@@ -6744,6 +7289,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6744
7289
|
ensureWatcherRunning(sessionId) {
|
|
6745
7290
|
const watcher = this.watchers.get(sessionId);
|
|
6746
7291
|
if (!watcher) return;
|
|
7292
|
+
this.ensureSessionErrorStream();
|
|
6747
7293
|
if (watcher.loop) return;
|
|
6748
7294
|
if (watcher.inFlight.size === 0) {
|
|
6749
7295
|
this.watchers.delete(sessionId);
|
|
@@ -6759,6 +7305,154 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6759
7305
|
});
|
|
6760
7306
|
watcher.loop = loop;
|
|
6761
7307
|
}
|
|
7308
|
+
ensureSessionErrorStream() {
|
|
7309
|
+
if (this.sessionErrorStream || this.stopped) return;
|
|
7310
|
+
const abort = new AbortController();
|
|
7311
|
+
const loop = this.runSessionErrorStream(abort.signal);
|
|
7312
|
+
this.sessionErrorStream = { abort, loop };
|
|
7313
|
+
}
|
|
7314
|
+
async runSessionErrorStream(signal) {
|
|
7315
|
+
let attempt = 0;
|
|
7316
|
+
let warned = false;
|
|
7317
|
+
while (!this.stopped && !signal.aborted) {
|
|
7318
|
+
const openedAt = this.now();
|
|
7319
|
+
try {
|
|
7320
|
+
const outcome = await readSessionErrorStream(this.port, {
|
|
7321
|
+
signal,
|
|
7322
|
+
onSessionError: (event) => this.handleSessionError(event)
|
|
7323
|
+
});
|
|
7324
|
+
if (outcome.reason === "aborted" || signal.aborted) return;
|
|
7325
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
7326
|
+
if (outcome.reason === "unavailable" || outcome.reason === "ended") {
|
|
7327
|
+
if (!healthy) {
|
|
7328
|
+
const detail = outcome.reason === "unavailable" ? outcome.detail : "stream ended";
|
|
7329
|
+
this.log({
|
|
7330
|
+
level: warned ? "debug" : "warn",
|
|
7331
|
+
message: `OpenCode session-error stream ${warned ? "still unavailable" : "unavailable"} (${detail}); transcript polling remains the evidence path`
|
|
7332
|
+
});
|
|
7333
|
+
warned = true;
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
if (healthy) {
|
|
7337
|
+
if (warned) {
|
|
7338
|
+
this.log({
|
|
7339
|
+
level: "info",
|
|
7340
|
+
message: "OpenCode session-error stream reconnected; transcript polling remains the evidence path"
|
|
7341
|
+
});
|
|
7342
|
+
warned = false;
|
|
7343
|
+
}
|
|
7344
|
+
attempt = 0;
|
|
7345
|
+
} else {
|
|
7346
|
+
attempt += 1;
|
|
7347
|
+
}
|
|
7348
|
+
if (this.stopped || signal.aborted) return;
|
|
7349
|
+
await this.sleep(backoffDelay(healthy ? 0 : attempt - 1, this.retry));
|
|
7350
|
+
} catch (err) {
|
|
7351
|
+
if (this.stopped || signal.aborted) return;
|
|
7352
|
+
this.log({
|
|
7353
|
+
level: "error",
|
|
7354
|
+
message: `OpenCode session-error stream failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
|
|
7355
|
+
});
|
|
7356
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
7357
|
+
const delayAttempt = healthy ? 0 : attempt;
|
|
7358
|
+
attempt = healthy ? 0 : attempt + 1;
|
|
7359
|
+
try {
|
|
7360
|
+
await this.sleep(backoffDelay(delayAttempt, this.retry));
|
|
7361
|
+
} catch (sleepErr) {
|
|
7362
|
+
this.log({
|
|
7363
|
+
level: "error",
|
|
7364
|
+
message: `OpenCode session-error stream backoff failed unexpectedly: ${sleepErr instanceof Error ? sleepErr.message : String(sleepErr)}`
|
|
7365
|
+
});
|
|
7366
|
+
}
|
|
7367
|
+
}
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
7370
|
+
handleSessionError(event) {
|
|
7371
|
+
try {
|
|
7372
|
+
const watcher = this.watchers.get(event.sessionId);
|
|
7373
|
+
if (!watcher) {
|
|
7374
|
+
this.bufferSessionError(event);
|
|
7375
|
+
this.log({
|
|
7376
|
+
level: "debug",
|
|
7377
|
+
message: `Ignoring session error for unknown session ${event.sessionId.slice(0, 8)}`
|
|
7378
|
+
});
|
|
7379
|
+
return;
|
|
7380
|
+
}
|
|
7381
|
+
if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
|
|
7382
|
+
this.log({
|
|
7383
|
+
level: "debug",
|
|
7384
|
+
message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
|
|
7385
|
+
conversation_id: watcher.conv.id
|
|
7386
|
+
});
|
|
7387
|
+
return;
|
|
7388
|
+
}
|
|
7389
|
+
const inFlight = [...watcher.inFlight.values()].filter((message) => !message.started && !message.done).sort((a, b) => a.dispatchedAt - b.dispatchedAt)[0];
|
|
7390
|
+
if (!inFlight) {
|
|
7391
|
+
this.bufferSessionError(event);
|
|
7392
|
+
this.log({
|
|
7393
|
+
level: "debug",
|
|
7394
|
+
message: `No queued in-flight turn to correlate with session error in ${event.sessionId.slice(0, 8)}`,
|
|
7395
|
+
conversation_id: watcher.conv.id
|
|
7396
|
+
});
|
|
7397
|
+
return;
|
|
7398
|
+
}
|
|
7399
|
+
if (this.sessionErrorHandled.has(inFlight.evidentMessageId)) return;
|
|
7400
|
+
this.sessionErrorHandled.add(inFlight.evidentMessageId);
|
|
7401
|
+
void this.failFromSessionError(watcher, event, inFlight);
|
|
7402
|
+
} catch (err) {
|
|
7403
|
+
this.log({
|
|
7404
|
+
level: "error",
|
|
7405
|
+
message: `Failed to handle OpenCode session error for ${event.sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`
|
|
7406
|
+
});
|
|
7407
|
+
}
|
|
7408
|
+
}
|
|
7409
|
+
async failFromSessionError(watcher, event, inFlight) {
|
|
7410
|
+
try {
|
|
7411
|
+
const messages = await getSessionMessages(this.port, event.sessionId);
|
|
7412
|
+
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7413
|
+
if (state !== "queued") {
|
|
7414
|
+
this.log({
|
|
7415
|
+
level: "debug",
|
|
7416
|
+
message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
|
|
7417
|
+
conversation_id: watcher.conv.id,
|
|
7418
|
+
message_id: inFlight.evidentMessageId
|
|
7419
|
+
});
|
|
7420
|
+
return;
|
|
7421
|
+
}
|
|
7422
|
+
this.log({
|
|
7423
|
+
level: "error",
|
|
7424
|
+
message: `OpenCode could not run message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)}: ${event.reason}`,
|
|
7425
|
+
conversation_id: watcher.conv.id,
|
|
7426
|
+
message_id: inFlight.evidentMessageId
|
|
7427
|
+
});
|
|
7428
|
+
await this.markFailed(
|
|
7429
|
+
watcher.conv.id,
|
|
7430
|
+
inFlight.evidentMessageId,
|
|
7431
|
+
event.sessionId,
|
|
7432
|
+
`OpenCode could not run this turn: ${event.reason}`
|
|
7433
|
+
);
|
|
7434
|
+
inFlight.done = true;
|
|
7435
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7436
|
+
} catch (err) {
|
|
7437
|
+
if (err instanceof ChannelAuthError) {
|
|
7438
|
+
this.log({
|
|
7439
|
+
level: "warn",
|
|
7440
|
+
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`,
|
|
7441
|
+
conversation_id: watcher.conv.id,
|
|
7442
|
+
message_id: inFlight.evidentMessageId
|
|
7443
|
+
});
|
|
7444
|
+
} else {
|
|
7445
|
+
this.log({
|
|
7446
|
+
level: "warn",
|
|
7447
|
+
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`,
|
|
7448
|
+
conversation_id: watcher.conv.id,
|
|
7449
|
+
message_id: inFlight.evidentMessageId
|
|
7450
|
+
});
|
|
7451
|
+
}
|
|
7452
|
+
} finally {
|
|
7453
|
+
this.sessionErrorHandled.delete(inFlight.evidentMessageId);
|
|
7454
|
+
}
|
|
7455
|
+
}
|
|
6762
7456
|
/**
|
|
6763
7457
|
* The per-session polling loop (WI-3). Once per tick it:
|
|
6764
7458
|
* 1. polls `GET /session/:id/message` once and, per in-flight message,
|
|
@@ -6871,6 +7565,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6871
7565
|
const conv = watcher.conv;
|
|
6872
7566
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
6873
7567
|
const id = inFlight.evidentMessageId;
|
|
7568
|
+
if (messages && collectTaskCalls(messages, inFlight.opencodeMessageId).length > 0 && !this.subagentInvocationPrefetches.has(id)) {
|
|
7569
|
+
void this.resolveSubagentInvocations(
|
|
7570
|
+
messages,
|
|
7571
|
+
inFlight.opencodeMessageId,
|
|
7572
|
+
id,
|
|
7573
|
+
"prefetch"
|
|
7574
|
+
).catch((err) => {
|
|
7575
|
+
this.log({
|
|
7576
|
+
level: "warn",
|
|
7577
|
+
message: `Best-effort subagent usage prefetch failed for message ${id.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
7578
|
+
conversation_id: conv.id,
|
|
7579
|
+
message_id: id
|
|
7580
|
+
});
|
|
7581
|
+
});
|
|
7582
|
+
}
|
|
6874
7583
|
if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
|
|
6875
7584
|
else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
|
|
6876
7585
|
if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
|
|
@@ -6924,6 +7633,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6924
7633
|
message_id: inFlight.evidentMessageId
|
|
6925
7634
|
});
|
|
6926
7635
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
7636
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
7637
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
7638
|
+
messages,
|
|
7639
|
+
inFlight.opencodeMessageId,
|
|
7640
|
+
inFlight.evidentMessageId
|
|
7641
|
+
);
|
|
6927
7642
|
const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
|
|
6928
7643
|
try {
|
|
6929
7644
|
await this.markFailed(
|
|
@@ -6932,7 +7647,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6932
7647
|
sessionId,
|
|
6933
7648
|
error2,
|
|
6934
7649
|
usage,
|
|
6935
|
-
failure
|
|
7650
|
+
failure,
|
|
7651
|
+
usageAgentName,
|
|
7652
|
+
subagentInvocations
|
|
6936
7653
|
);
|
|
6937
7654
|
} catch (err) {
|
|
6938
7655
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -6965,13 +7682,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6965
7682
|
return;
|
|
6966
7683
|
}
|
|
6967
7684
|
inFlight.done = true;
|
|
7685
|
+
await this.reportSubagentAuthFailures(
|
|
7686
|
+
watcher.conv.id,
|
|
7687
|
+
inFlight.opencodeMessageId,
|
|
7688
|
+
inFlight.evidentMessageId,
|
|
7689
|
+
messages
|
|
7690
|
+
);
|
|
6968
7691
|
}
|
|
6969
7692
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
6970
7693
|
return;
|
|
6971
7694
|
}
|
|
7695
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
|
|
7696
|
+
const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
|
|
6972
7697
|
const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
|
|
6973
7698
|
const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
|
|
6974
|
-
if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
|
|
7699
|
+
if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
|
|
6975
7700
|
inFlight.stuckReported = true;
|
|
6976
7701
|
void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
|
|
6977
7702
|
stuck_for_ms: this.now() - inFlight.dispatchedAt
|
|
@@ -7132,7 +7857,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7132
7857
|
const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
|
|
7133
7858
|
(sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
|
|
7134
7859
|
);
|
|
7135
|
-
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
7860
|
+
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
|
|
7136
7861
|
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
7137
7862
|
this.log({
|
|
7138
7863
|
level: "debug",
|
|
@@ -7167,6 +7892,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7167
7892
|
});
|
|
7168
7893
|
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
7169
7894
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
7895
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
7896
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
7897
|
+
messages,
|
|
7898
|
+
inFlight.opencodeMessageId,
|
|
7899
|
+
inFlight.evidentMessageId
|
|
7900
|
+
);
|
|
7170
7901
|
try {
|
|
7171
7902
|
await this.markDone(
|
|
7172
7903
|
conv.id,
|
|
@@ -7174,7 +7905,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7174
7905
|
sessionId,
|
|
7175
7906
|
inFlight.opencodeMessageId,
|
|
7176
7907
|
title,
|
|
7177
|
-
usage
|
|
7908
|
+
usage,
|
|
7909
|
+
usageAgentName,
|
|
7910
|
+
subagentInvocations
|
|
7178
7911
|
);
|
|
7179
7912
|
} catch (err) {
|
|
7180
7913
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7207,6 +7940,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7207
7940
|
return;
|
|
7208
7941
|
}
|
|
7209
7942
|
inFlight.done = true;
|
|
7943
|
+
await this.reportSubagentAuthFailures(
|
|
7944
|
+
watcher.conv.id,
|
|
7945
|
+
inFlight.opencodeMessageId,
|
|
7946
|
+
inFlight.evidentMessageId,
|
|
7947
|
+
messages
|
|
7948
|
+
);
|
|
7210
7949
|
}
|
|
7211
7950
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7212
7951
|
}
|
|
@@ -7347,6 +8086,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7347
8086
|
if (state === "failed" && !restartAborted) {
|
|
7348
8087
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
7349
8088
|
const usage = messageUsage(messages, ocId ?? "");
|
|
8089
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
8090
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
8091
|
+
messages,
|
|
8092
|
+
ocId ?? "",
|
|
8093
|
+
row.id
|
|
8094
|
+
);
|
|
7350
8095
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
7351
8096
|
this.log({
|
|
7352
8097
|
level: "error",
|
|
@@ -7355,7 +8100,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7355
8100
|
message_id: row.id
|
|
7356
8101
|
});
|
|
7357
8102
|
try {
|
|
7358
|
-
await this.markFailed(
|
|
8103
|
+
await this.markFailed(
|
|
8104
|
+
row.conversation_id,
|
|
8105
|
+
row.id,
|
|
8106
|
+
sessionId,
|
|
8107
|
+
error2,
|
|
8108
|
+
usage,
|
|
8109
|
+
failure,
|
|
8110
|
+
usageAgentName,
|
|
8111
|
+
subagentInvocations
|
|
8112
|
+
);
|
|
7359
8113
|
} catch (err) {
|
|
7360
8114
|
if (err instanceof ChannelAuthError) throw err;
|
|
7361
8115
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7377,6 +8131,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7377
8131
|
});
|
|
7378
8132
|
return;
|
|
7379
8133
|
}
|
|
8134
|
+
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
7380
8135
|
this.dontRedispatch.delete(row.id);
|
|
7381
8136
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
7382
8137
|
return;
|
|
@@ -7516,7 +8271,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7516
8271
|
try {
|
|
7517
8272
|
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
7518
8273
|
const usage = messageUsage(messages, ocId ?? "");
|
|
7519
|
-
|
|
8274
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
8275
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
8276
|
+
messages,
|
|
8277
|
+
ocId ?? "",
|
|
8278
|
+
row.id
|
|
8279
|
+
);
|
|
8280
|
+
await this.markDone(
|
|
8281
|
+
row.conversation_id,
|
|
8282
|
+
row.id,
|
|
8283
|
+
sessionId,
|
|
8284
|
+
ocId,
|
|
8285
|
+
title,
|
|
8286
|
+
usage,
|
|
8287
|
+
usageAgentName,
|
|
8288
|
+
subagentInvocations
|
|
8289
|
+
);
|
|
7520
8290
|
} catch (err) {
|
|
7521
8291
|
if (err instanceof ChannelAuthError) throw err;
|
|
7522
8292
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7538,6 +8308,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7538
8308
|
});
|
|
7539
8309
|
return;
|
|
7540
8310
|
}
|
|
8311
|
+
if (ocId !== null) {
|
|
8312
|
+
await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
|
|
8313
|
+
}
|
|
7541
8314
|
this.dontRedispatch.delete(row.id);
|
|
7542
8315
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
7543
8316
|
}
|
|
@@ -7631,14 +8404,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7631
8404
|
this.unconfirmedDispatchFailures.delete(row.id);
|
|
7632
8405
|
this.sessions.delete(readoptConv.id);
|
|
7633
8406
|
this.supersede(readoptConv.id, sessionId);
|
|
7634
|
-
const
|
|
8407
|
+
const errorMessage3 = `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
|
|
7635
8408
|
this.log({
|
|
7636
8409
|
level: "error",
|
|
7637
|
-
message:
|
|
8410
|
+
message: errorMessage3,
|
|
7638
8411
|
conversation_id: row.conversation_id,
|
|
7639
8412
|
message_id: row.id
|
|
7640
8413
|
});
|
|
7641
|
-
await this.markFailed(row.conversation_id, row.id, null,
|
|
8414
|
+
await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
|
|
7642
8415
|
this.log({
|
|
7643
8416
|
level: "warn",
|
|
7644
8417
|
message: `markFailed PATCH for message ${row.id.slice(0, 8)} (conversation ${row.conversation_id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -7917,6 +8690,166 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7917
8690
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
7918
8691
|
return parent;
|
|
7919
8692
|
}
|
|
8693
|
+
usageAgentName(messages, userMessageId) {
|
|
8694
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
8695
|
+
const mode = reply?.info?.mode;
|
|
8696
|
+
if (typeof mode === "string" && mode.length > 0) return mode;
|
|
8697
|
+
const agent = reply?.info?.agent;
|
|
8698
|
+
return typeof agent === "string" && agent.length > 0 ? agent : null;
|
|
8699
|
+
}
|
|
8700
|
+
async resolveSubagentInvocations(messages, userMessageId, messageId, phase = "terminal") {
|
|
8701
|
+
if (!messages) return void 0;
|
|
8702
|
+
const cache = phase === "prefetch" ? this.subagentInvocationPrefetches : this.subagentInvocationCollections;
|
|
8703
|
+
const cached = cache.get(messageId);
|
|
8704
|
+
if (cached) return cached;
|
|
8705
|
+
const collection = this.buildSubagentInvocations(messages, userMessageId, messageId).catch(
|
|
8706
|
+
(err) => {
|
|
8707
|
+
this.log({
|
|
8708
|
+
level: "warn",
|
|
8709
|
+
message: `Best-effort subagent usage collection failed for message ${messageId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
8710
|
+
message_id: messageId
|
|
8711
|
+
});
|
|
8712
|
+
return void 0;
|
|
8713
|
+
}
|
|
8714
|
+
);
|
|
8715
|
+
cache.set(messageId, collection);
|
|
8716
|
+
const result = await collection;
|
|
8717
|
+
if (result === void 0 && cache.get(messageId) === collection) cache.delete(messageId);
|
|
8718
|
+
return result;
|
|
8719
|
+
}
|
|
8720
|
+
clearSubagentInvocationCaches(messageId) {
|
|
8721
|
+
this.subagentInvocationCollections.delete(messageId);
|
|
8722
|
+
this.subagentInvocationPrefetches.delete(messageId);
|
|
8723
|
+
}
|
|
8724
|
+
async buildSubagentInvocations(messages, userMessageId, messageId) {
|
|
8725
|
+
const rootCalls = collectTaskCalls(messages, userMessageId);
|
|
8726
|
+
if (rootCalls.length === 0) return void 0;
|
|
8727
|
+
const childMessages = /* @__PURE__ */ new Map();
|
|
8728
|
+
const seenCallIds = new Set(rootCalls.map((call) => call.callID));
|
|
8729
|
+
const work = rootCalls.map((call) => ({
|
|
8730
|
+
call,
|
|
8731
|
+
depth: 1
|
|
8732
|
+
}));
|
|
8733
|
+
const payload = [];
|
|
8734
|
+
const fetchChildMessages = (sessionId) => {
|
|
8735
|
+
const cached = childMessages.get(sessionId);
|
|
8736
|
+
if (cached) return cached;
|
|
8737
|
+
const pending = (async () => {
|
|
8738
|
+
try {
|
|
8739
|
+
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
8740
|
+
if (!res.ok) {
|
|
8741
|
+
this.log({
|
|
8742
|
+
level: "warn",
|
|
8743
|
+
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`,
|
|
8744
|
+
message_id: messageId
|
|
8745
|
+
});
|
|
8746
|
+
return null;
|
|
8747
|
+
}
|
|
8748
|
+
const body = await res.json();
|
|
8749
|
+
if (!Array.isArray(body)) throw new Error("response body was not a message array");
|
|
8750
|
+
return body;
|
|
8751
|
+
} catch (err) {
|
|
8752
|
+
this.log({
|
|
8753
|
+
level: "warn",
|
|
8754
|
+
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)}`,
|
|
8755
|
+
message_id: messageId
|
|
8756
|
+
});
|
|
8757
|
+
return null;
|
|
8758
|
+
}
|
|
8759
|
+
})();
|
|
8760
|
+
childMessages.set(sessionId, pending);
|
|
8761
|
+
return pending;
|
|
8762
|
+
};
|
|
8763
|
+
const fetchChildWithoutBlocking = async (sessionId) => {
|
|
8764
|
+
const pending = fetchChildMessages(sessionId);
|
|
8765
|
+
let timer;
|
|
8766
|
+
const timeout = new Promise((resolve4) => {
|
|
8767
|
+
timer = setTimeout(() => {
|
|
8768
|
+
this.log({
|
|
8769
|
+
level: "warn",
|
|
8770
|
+
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`,
|
|
8771
|
+
message_id: messageId
|
|
8772
|
+
});
|
|
8773
|
+
resolve4(null);
|
|
8774
|
+
}, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
|
|
8775
|
+
});
|
|
8776
|
+
try {
|
|
8777
|
+
return await Promise.race([pending, timeout]);
|
|
8778
|
+
} finally {
|
|
8779
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
8780
|
+
}
|
|
8781
|
+
};
|
|
8782
|
+
while (work.length > 0) {
|
|
8783
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8784
|
+
for (const item of work.splice(0)) {
|
|
8785
|
+
const group = groups.get(item.call.childSessionId) ?? [];
|
|
8786
|
+
group.push(item);
|
|
8787
|
+
groups.set(item.call.childSessionId, group);
|
|
8788
|
+
}
|
|
8789
|
+
const groupResults = await Promise.all(
|
|
8790
|
+
[...groups].map(async ([sessionId, items]) => ({
|
|
8791
|
+
sessionId,
|
|
8792
|
+
items,
|
|
8793
|
+
messages: sessionId === null ? [] : await fetchChildWithoutBlocking(sessionId)
|
|
8794
|
+
}))
|
|
8795
|
+
);
|
|
8796
|
+
for (const { sessionId, items, messages: child } of groupResults) {
|
|
8797
|
+
if (sessionId !== null && child === null) continue;
|
|
8798
|
+
const attribution = sessionId === null ? { invocations: [], unattributed: [] } : attributeTaskCallUsage(
|
|
8799
|
+
child,
|
|
8800
|
+
items.map(({ call }) => ({
|
|
8801
|
+
callID: call.callID,
|
|
8802
|
+
timeStart: call.timeStart,
|
|
8803
|
+
timeEnd: call.timeEnd
|
|
8804
|
+
}))
|
|
8805
|
+
);
|
|
8806
|
+
if (sessionId !== null && attribution.unattributed.length > 0) {
|
|
8807
|
+
this.log({
|
|
8808
|
+
level: "warn",
|
|
8809
|
+
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`,
|
|
8810
|
+
message_id: messageId
|
|
8811
|
+
});
|
|
8812
|
+
}
|
|
8813
|
+
const usageByCall = new Map(
|
|
8814
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.usage])
|
|
8815
|
+
);
|
|
8816
|
+
const messagesByCall = new Map(
|
|
8817
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.messages])
|
|
8818
|
+
);
|
|
8819
|
+
for (const { call, depth } of items) {
|
|
8820
|
+
const usage = usageByCall.get(call.callID) ?? null;
|
|
8821
|
+
payload.push({
|
|
8822
|
+
tool_call_id: call.callID,
|
|
8823
|
+
agent_name: call.subagentName,
|
|
8824
|
+
opencode_session_id: call.childSessionId,
|
|
8825
|
+
parent_opencode_session_id: call.parentSessionId,
|
|
8826
|
+
depth,
|
|
8827
|
+
status: call.status,
|
|
8828
|
+
started_at: call.timeStart === null ? null : new Date(call.timeStart).toISOString(),
|
|
8829
|
+
ended_at: call.timeEnd === null ? null : new Date(call.timeEnd).toISOString(),
|
|
8830
|
+
usage_provider_id: usage?.usage_provider_id ?? call.model?.providerID ?? null,
|
|
8831
|
+
usage_model_id: usage?.usage_model_id ?? call.model?.modelID ?? null,
|
|
8832
|
+
usage_tokens_input: usage?.usage_tokens_input ?? null,
|
|
8833
|
+
usage_tokens_output: usage?.usage_tokens_output ?? null,
|
|
8834
|
+
usage_tokens_reasoning: usage?.usage_tokens_reasoning ?? null,
|
|
8835
|
+
usage_tokens_cache_read: usage?.usage_tokens_cache_read ?? null,
|
|
8836
|
+
usage_tokens_cache_write: usage?.usage_tokens_cache_write ?? null,
|
|
8837
|
+
usage_cost_usd: usage?.usage_cost_usd ?? null
|
|
8838
|
+
});
|
|
8839
|
+
for (const assigned of messagesByCall.get(call.callID) ?? []) {
|
|
8840
|
+
const parentId = assigned.info?.parentID ?? assigned.parentID;
|
|
8841
|
+
if (!parentId) continue;
|
|
8842
|
+
for (const nested of collectTaskCalls([assigned], parentId)) {
|
|
8843
|
+
if (seenCallIds.has(nested.callID)) continue;
|
|
8844
|
+
seenCallIds.add(nested.callID);
|
|
8845
|
+
work.push({ call: nested, depth: depth + 1 });
|
|
8846
|
+
}
|
|
8847
|
+
}
|
|
8848
|
+
}
|
|
8849
|
+
}
|
|
8850
|
+
}
|
|
8851
|
+
return payload.length > 0 ? payload : void 0;
|
|
8852
|
+
}
|
|
7920
8853
|
/**
|
|
7921
8854
|
* OpenCode's synchronous default session title (e.g.
|
|
7922
8855
|
* `"New session - 1737800000000"`), assigned immediately when a session is
|
|
@@ -8253,6 +9186,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8253
9186
|
throw new Error(`Failed to get pending conversations: HTTP ${res.status}`);
|
|
8254
9187
|
}
|
|
8255
9188
|
const data = await res.json();
|
|
9189
|
+
this.recycleRequestedFlag = data.recycle_requested === true;
|
|
8256
9190
|
let conversations = data.conversations;
|
|
8257
9191
|
if (this.conversationFilter) {
|
|
8258
9192
|
conversations = conversations.filter((c) => c.id === this.conversationFilter);
|
|
@@ -8399,7 +9333,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8399
9333
|
* watcher retries next tick within the
|
|
8400
9334
|
* deadline, Finding 4).
|
|
8401
9335
|
*/
|
|
8402
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
|
|
9336
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
|
|
8403
9337
|
const res = await this.fetchImpl(
|
|
8404
9338
|
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
8405
9339
|
{
|
|
@@ -8415,15 +9349,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8415
9349
|
opencode_session_id: sessionId,
|
|
8416
9350
|
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
8417
9351
|
...title ? { title } : {},
|
|
8418
|
-
...usage ? usage : {}
|
|
9352
|
+
...usage ? usage : {},
|
|
9353
|
+
...usageAgentName ? { usage_agent_name: usageAgentName } : {},
|
|
9354
|
+
...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
|
|
8419
9355
|
})
|
|
8420
9356
|
}
|
|
8421
9357
|
);
|
|
8422
9358
|
this.assertAuth(res, "marking message as done");
|
|
8423
|
-
if (res.ok)
|
|
9359
|
+
if (res.ok) {
|
|
9360
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
9361
|
+
return;
|
|
9362
|
+
}
|
|
8424
9363
|
if (isRetryableStatus(res.status)) {
|
|
8425
9364
|
throw new Error(`marking message as done: HTTP ${res.status}`);
|
|
8426
9365
|
}
|
|
9366
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8427
9367
|
throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
|
|
8428
9368
|
}
|
|
8429
9369
|
/**
|
|
@@ -8438,7 +9378,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8438
9378
|
* exists but is wedged, so the next attempt must get a fresh one
|
|
8439
9379
|
* instead of reusing it — see WI-1's server-side null-clearing PATCH).
|
|
8440
9380
|
*/
|
|
8441
|
-
async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
|
|
9381
|
+
async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
|
|
8442
9382
|
const body = { status: "failed" };
|
|
8443
9383
|
if (sessionId === null) {
|
|
8444
9384
|
body.opencode_session_id = null;
|
|
@@ -8447,23 +9387,33 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8447
9387
|
}
|
|
8448
9388
|
if (error2 !== void 0) body.error = error2;
|
|
8449
9389
|
if (usage) Object.assign(body, usage);
|
|
9390
|
+
if (usageAgentName) body.usage_agent_name = usageAgentName;
|
|
9391
|
+
if (subagentInvocations && subagentInvocations.length > 0) {
|
|
9392
|
+
body.subagent_invocations = subagentInvocations;
|
|
9393
|
+
}
|
|
8450
9394
|
if (failure) {
|
|
8451
9395
|
body.failure_kind = failure.kind;
|
|
8452
9396
|
body.failure_provider_id = failure.providerId;
|
|
8453
9397
|
body.failure_model_id = failure.modelId;
|
|
8454
9398
|
body.failure_reason = failure.reason;
|
|
8455
9399
|
}
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
9400
|
+
try {
|
|
9401
|
+
await this.callWithRetry(
|
|
9402
|
+
"marking message as failed",
|
|
9403
|
+
() => this.fetchImpl(
|
|
9404
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
9405
|
+
{
|
|
9406
|
+
method: "PATCH",
|
|
9407
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
9408
|
+
body: JSON.stringify(body)
|
|
9409
|
+
}
|
|
9410
|
+
)
|
|
9411
|
+
);
|
|
9412
|
+
} catch (err) {
|
|
9413
|
+
if (err instanceof ChannelTerminalError) this.clearSubagentInvocationCaches(messageId);
|
|
9414
|
+
throw err;
|
|
9415
|
+
}
|
|
9416
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8467
9417
|
}
|
|
8468
9418
|
/**
|
|
8469
9419
|
* Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
|
|
@@ -8488,14 +9438,119 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8488
9438
|
reply?.info?.modelID ?? null
|
|
8489
9439
|
);
|
|
8490
9440
|
}
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
9441
|
+
async recordSubagentModelAuthFailure(failure, conversationId, messageId) {
|
|
9442
|
+
const providerId = failure.providerId ?? "(unknown)";
|
|
9443
|
+
try {
|
|
9444
|
+
const res = await this.fetchImpl(
|
|
9445
|
+
`${this.apiUrl}/runners/${this.agentId}/model-auth-failures`,
|
|
9446
|
+
{
|
|
9447
|
+
method: "POST",
|
|
9448
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
9449
|
+
body: JSON.stringify({
|
|
9450
|
+
provider_id: failure.providerId,
|
|
9451
|
+
model_id: failure.modelId,
|
|
9452
|
+
reason: failure.reason
|
|
9453
|
+
})
|
|
9454
|
+
}
|
|
9455
|
+
);
|
|
9456
|
+
if (!res.ok) {
|
|
9457
|
+
this.log({
|
|
9458
|
+
level: "warn",
|
|
9459
|
+
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)})`,
|
|
9460
|
+
conversation_id: conversationId,
|
|
9461
|
+
message_id: messageId
|
|
9462
|
+
});
|
|
9463
|
+
}
|
|
9464
|
+
} catch (err) {
|
|
9465
|
+
this.log({
|
|
9466
|
+
level: "warn",
|
|
9467
|
+
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)}`,
|
|
9468
|
+
conversation_id: conversationId,
|
|
9469
|
+
message_id: messageId
|
|
9470
|
+
});
|
|
9471
|
+
}
|
|
9472
|
+
}
|
|
9473
|
+
async clearSubagentModelAuthFailure(providerId, conversationId, messageId) {
|
|
9474
|
+
try {
|
|
9475
|
+
const res = await this.fetchImpl(
|
|
9476
|
+
`${this.apiUrl}/runners/${this.agentId}/model-auth-failures/${encodeURIComponent(providerId)}`,
|
|
9477
|
+
{
|
|
9478
|
+
method: "DELETE",
|
|
9479
|
+
headers: { Authorization: this.getAuthHeader() }
|
|
9480
|
+
}
|
|
9481
|
+
);
|
|
9482
|
+
if (!res.ok) {
|
|
9483
|
+
this.log({
|
|
9484
|
+
level: "warn",
|
|
9485
|
+
message: `Sub-agent model-auth clear for provider ${providerId} returned HTTP ${res.status} (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)})`,
|
|
9486
|
+
conversation_id: conversationId,
|
|
9487
|
+
message_id: messageId
|
|
9488
|
+
});
|
|
9489
|
+
}
|
|
9490
|
+
} catch (err) {
|
|
9491
|
+
this.log({
|
|
9492
|
+
level: "warn",
|
|
9493
|
+
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)}`,
|
|
9494
|
+
conversation_id: conversationId,
|
|
9495
|
+
message_id: messageId
|
|
9496
|
+
});
|
|
9497
|
+
}
|
|
9498
|
+
}
|
|
9499
|
+
async reportSubagentAuthFailures(conversationId, opencodeMessageId, evidentMessageId, messages) {
|
|
9500
|
+
const refs = collectSubagentSessions(messages, opencodeMessageId);
|
|
9501
|
+
if (refs.length === 0) return;
|
|
9502
|
+
const failedProviders = /* @__PURE__ */ new Map();
|
|
9503
|
+
const succeededProviders = /* @__PURE__ */ new Set();
|
|
9504
|
+
for (const ref of refs) {
|
|
9505
|
+
try {
|
|
9506
|
+
const childMessages = await getSessionMessages(this.port, ref.sessionId);
|
|
9507
|
+
if (childMessages === null) {
|
|
9508
|
+
this.log({
|
|
9509
|
+
level: "debug",
|
|
9510
|
+
message: `Could not read sub-agent session ${ref.sessionId} while checking credential failures \u2014 skipping it`,
|
|
9511
|
+
conversation_id: conversationId,
|
|
9512
|
+
message_id: evidentMessageId
|
|
9513
|
+
});
|
|
9514
|
+
continue;
|
|
9515
|
+
}
|
|
9516
|
+
const outcome = findSubagentAuthOutcome(childMessages, ref.startedAtMs);
|
|
9517
|
+
if (!outcome) continue;
|
|
9518
|
+
if (outcome.outcome === "failed") {
|
|
9519
|
+
failedProviders.set(outcome.providerId, outcome.failure);
|
|
9520
|
+
} else {
|
|
9521
|
+
succeededProviders.add(outcome.providerId);
|
|
9522
|
+
}
|
|
9523
|
+
} catch (err) {
|
|
9524
|
+
this.log({
|
|
9525
|
+
level: "warn",
|
|
9526
|
+
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)}`,
|
|
9527
|
+
conversation_id: conversationId,
|
|
9528
|
+
message_id: evidentMessageId
|
|
9529
|
+
});
|
|
9530
|
+
}
|
|
9531
|
+
}
|
|
9532
|
+
for (const [providerId, failure] of failedProviders) {
|
|
9533
|
+
this.log({
|
|
9534
|
+
level: "warn",
|
|
9535
|
+
message: `Sub-agent turn failed on provider ${providerId} (${failure.reason}) \u2014 recording credential evidence`,
|
|
9536
|
+
conversation_id: conversationId,
|
|
9537
|
+
message_id: evidentMessageId
|
|
9538
|
+
});
|
|
9539
|
+
await this.recordSubagentModelAuthFailure(failure, conversationId, evidentMessageId);
|
|
9540
|
+
}
|
|
9541
|
+
for (const providerId of succeededProviders) {
|
|
9542
|
+
if (failedProviders.has(providerId)) continue;
|
|
9543
|
+
await this.clearSubagentModelAuthFailure(providerId, conversationId, evidentMessageId);
|
|
9544
|
+
}
|
|
9545
|
+
}
|
|
9546
|
+
/**
|
|
9547
|
+
* Best-effort, SINGLE-ATTEMPT runner→server telemetry ping
|
|
9548
|
+
* (queued-followup-redrive). `POST .../messages/:id/signal {signal, ...extra}`
|
|
9549
|
+
* — the server records it via `log()` (no DB write, no notification). This is
|
|
9550
|
+
* fire-and-forget: it MUST NEVER throw into the drain or the watcher tick, and
|
|
9551
|
+
* MUST NOT use `callWithRetry` (a telemetry ping must not block the sequential
|
|
9552
|
+
* watcher tick — one attempt is enough). A failure is SWALLOWED but LOGGED with
|
|
9553
|
+
* context (no silent catch, per development-workflow).
|
|
8499
9554
|
*
|
|
8500
9555
|
* Returns whether the POST SUCCEEDED (2xx). Most callers ignore this (pure
|
|
8501
9556
|
* telemetry), but the `paused` liveness-clear uses it to know whether to
|
|
@@ -8641,6 +9696,13 @@ import chalk5 from "chalk";
|
|
|
8641
9696
|
import ora2 from "ora";
|
|
8642
9697
|
import { select as select2 } from "@inquirer/prompts";
|
|
8643
9698
|
var INTERACTIVE_START_TIMEOUT_MS = 3e4;
|
|
9699
|
+
function checkNonInteractivePortConflict(port, isPortInUseFn) {
|
|
9700
|
+
if (isPortInUseFn(port)) {
|
|
9701
|
+
throw new Error(
|
|
9702
|
+
`Port ${port} is already in use by a non-OpenCode process. Free it or pass --port.`
|
|
9703
|
+
);
|
|
9704
|
+
}
|
|
9705
|
+
}
|
|
8644
9706
|
async function ensureOpenCodeRunning(ctx) {
|
|
8645
9707
|
const healthCheck = await checkOpenCodeHealth(ctx.port);
|
|
8646
9708
|
if (healthCheck.healthy) {
|
|
@@ -8688,6 +9750,7 @@ async function ensureOpenCodeRunning(ctx) {
|
|
|
8688
9750
|
}
|
|
8689
9751
|
}
|
|
8690
9752
|
if (!ctx.interactive) {
|
|
9753
|
+
checkNonInteractivePortConflict(ctx.port, isPortInUse);
|
|
8691
9754
|
ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
|
|
8692
9755
|
const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
|
|
8693
9756
|
const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
|
|
@@ -8769,9 +9832,119 @@ Port ${port} is already in use.`));
|
|
|
8769
9832
|
return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
|
|
8770
9833
|
}
|
|
8771
9834
|
|
|
9835
|
+
// src/commands/ensure-opencode-v2.ts
|
|
9836
|
+
import chalk6 from "chalk";
|
|
9837
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
9838
|
+
async function probeOpenCode2WithoutPassword(port) {
|
|
9839
|
+
try {
|
|
9840
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
9841
|
+
signal: AbortSignal.timeout(2e3)
|
|
9842
|
+
});
|
|
9843
|
+
if (response.status === 401) {
|
|
9844
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
9845
|
+
}
|
|
9846
|
+
if (!response.ok) {
|
|
9847
|
+
return { healthy: false, error: `HTTP ${response.status}` };
|
|
9848
|
+
}
|
|
9849
|
+
return { healthy: true };
|
|
9850
|
+
} catch (error2) {
|
|
9851
|
+
return {
|
|
9852
|
+
healthy: false,
|
|
9853
|
+
error: error2 instanceof Error ? error2.message : "Unknown error"
|
|
9854
|
+
};
|
|
9855
|
+
}
|
|
9856
|
+
}
|
|
9857
|
+
function unknownPasswordError(port) {
|
|
9858
|
+
return new Error(
|
|
9859
|
+
`OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
|
|
9860
|
+
);
|
|
9861
|
+
}
|
|
9862
|
+
function v2SessionSupportIncompleteError() {
|
|
9863
|
+
return new Error(
|
|
9864
|
+
"OpenCode V2 (opencode2) session support is incomplete \u2014 see https://github.com/sroze/evident/issues/2075. Not starting a new opencode2 process."
|
|
9865
|
+
);
|
|
9866
|
+
}
|
|
9867
|
+
async function ensureOpenCode2Running(ctx) {
|
|
9868
|
+
const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
|
|
9869
|
+
if (initialHealth.authFailed) {
|
|
9870
|
+
throw unknownPasswordError(ctx.port);
|
|
9871
|
+
}
|
|
9872
|
+
if (initialHealth.healthy) {
|
|
9873
|
+
return {
|
|
9874
|
+
port: ctx.port,
|
|
9875
|
+
process: null,
|
|
9876
|
+
version: null,
|
|
9877
|
+
notReadyReason: null,
|
|
9878
|
+
password: null
|
|
9879
|
+
};
|
|
9880
|
+
}
|
|
9881
|
+
if (!isOpenCode2Installed()) {
|
|
9882
|
+
throw new Error(
|
|
9883
|
+
"OpenCode V2 (opencode2) is not installed. Install it with: npm install -g @opencode-ai/cli@beta"
|
|
9884
|
+
);
|
|
9885
|
+
}
|
|
9886
|
+
let port = ctx.port;
|
|
9887
|
+
if (!ctx.interactive) {
|
|
9888
|
+
checkNonInteractivePortConflict(port, isPortInUse);
|
|
9889
|
+
} else if (isPortInUse(port)) {
|
|
9890
|
+
console.log(chalk6.yellow(`
|
|
9891
|
+
Port ${port} is already in use.`));
|
|
9892
|
+
const alternativePort = findAvailablePort(port + 1);
|
|
9893
|
+
if (alternativePort) {
|
|
9894
|
+
const useAlternative = await select3({
|
|
9895
|
+
message: `Use port ${alternativePort} instead?`,
|
|
9896
|
+
choices: [
|
|
9897
|
+
{ name: `Yes, use port ${alternativePort}`, value: "yes" },
|
|
9898
|
+
{ name: "No, I will free the port manually", value: "no" }
|
|
9899
|
+
]
|
|
9900
|
+
});
|
|
9901
|
+
if (useAlternative === "yes") {
|
|
9902
|
+
port = alternativePort;
|
|
9903
|
+
} else {
|
|
9904
|
+
throw new Error(`Port ${ctx.port} is in use`);
|
|
9905
|
+
}
|
|
9906
|
+
}
|
|
9907
|
+
}
|
|
9908
|
+
if (!ctx.interactive) {
|
|
9909
|
+
throw v2SessionSupportIncompleteError();
|
|
9910
|
+
}
|
|
9911
|
+
console.log(chalk6.yellow(`
|
|
9912
|
+
${v2SessionSupportIncompleteError().message}`));
|
|
9913
|
+
const action = await select3({
|
|
9914
|
+
message: "OpenCode V2 is not running. What would you like to do?",
|
|
9915
|
+
choices: [
|
|
9916
|
+
{
|
|
9917
|
+
name: "Show me the command",
|
|
9918
|
+
value: "manual",
|
|
9919
|
+
description: "Display the command to run manually"
|
|
9920
|
+
},
|
|
9921
|
+
{
|
|
9922
|
+
name: "Continue without OpenCode V2",
|
|
9923
|
+
value: "continue",
|
|
9924
|
+
description: "Requests will fail until OpenCode V2 starts"
|
|
9925
|
+
}
|
|
9926
|
+
]
|
|
9927
|
+
});
|
|
9928
|
+
if (action === "manual") {
|
|
9929
|
+
blank();
|
|
9930
|
+
console.log(chalk6.bold("Run this command in another terminal:"));
|
|
9931
|
+
blank();
|
|
9932
|
+
console.log(` ${chalk6.cyan(`opencode2 serve --port ${port}`)}`);
|
|
9933
|
+
blank();
|
|
9934
|
+
throw new Error("Please start OpenCode V2 manually");
|
|
9935
|
+
}
|
|
9936
|
+
return {
|
|
9937
|
+
port,
|
|
9938
|
+
process: null,
|
|
9939
|
+
version: null,
|
|
9940
|
+
notReadyReason: "you chose to continue without OpenCode V2",
|
|
9941
|
+
password: null
|
|
9942
|
+
};
|
|
9943
|
+
}
|
|
9944
|
+
|
|
8772
9945
|
// src/lib/runner-credentials.ts
|
|
8773
|
-
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
8774
|
-
import { spawn as spawn5 } from "child_process";
|
|
9946
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
9947
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
8775
9948
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
8776
9949
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
8777
9950
|
var GITHUB_PROBE_TIMEOUT_MS = 1e4;
|
|
@@ -9043,11 +10216,11 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
9043
10216
|
}
|
|
9044
10217
|
|
|
9045
10218
|
// src/lib/opencode/config-overlay.ts
|
|
9046
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
9047
|
-
import { copyFileSync, existsSync as existsSync2, statSync as
|
|
9048
|
-
import { isAbsolute as isAbsolute2, join as
|
|
10219
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
10220
|
+
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
10221
|
+
import { isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "node:path";
|
|
9049
10222
|
function isFile(filePath) {
|
|
9050
|
-
return existsSync2(filePath) &&
|
|
10223
|
+
return existsSync2(filePath) && statSync6(filePath).isFile();
|
|
9051
10224
|
}
|
|
9052
10225
|
function applyRunnerOpenCodeConfig({
|
|
9053
10226
|
overlayPath,
|
|
@@ -9059,7 +10232,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9059
10232
|
return;
|
|
9060
10233
|
}
|
|
9061
10234
|
const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
|
|
9062
|
-
const target = isFile(
|
|
10235
|
+
const target = isFile(join9(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
9063
10236
|
if (!isFile(source)) {
|
|
9064
10237
|
log3(
|
|
9065
10238
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -9067,7 +10240,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9067
10240
|
);
|
|
9068
10241
|
return;
|
|
9069
10242
|
}
|
|
9070
|
-
copyFileSync(source,
|
|
10243
|
+
copyFileSync(source, join9(cwd, target));
|
|
9071
10244
|
try {
|
|
9072
10245
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
9073
10246
|
stdio: "ignore"
|
|
@@ -9076,7 +10249,242 @@ function applyRunnerOpenCodeConfig({
|
|
|
9076
10249
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
9077
10250
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
9078
10251
|
}
|
|
9079
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
10252
|
+
log3(`Applied runner OpenCode config ${source} to ${join9(cwd, target)}`);
|
|
10253
|
+
}
|
|
10254
|
+
|
|
10255
|
+
// src/lib/credential-sync.ts
|
|
10256
|
+
import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
10257
|
+
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
10258
|
+
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
10259
|
+
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
10260
|
+
var DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS = 60;
|
|
10261
|
+
var STORES = ["claude", "opencode"];
|
|
10262
|
+
var MAX_FLUSH_PASSES = 2;
|
|
10263
|
+
function outcomesWith(outcome) {
|
|
10264
|
+
return { claude: outcome, opencode: outcome };
|
|
10265
|
+
}
|
|
10266
|
+
function errorMessage2(error2) {
|
|
10267
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
10268
|
+
}
|
|
10269
|
+
function waitForSettlement(promise, timeoutMs) {
|
|
10270
|
+
return new Promise((resolve4) => {
|
|
10271
|
+
let settled = false;
|
|
10272
|
+
const timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
|
|
10273
|
+
const finish = (value) => {
|
|
10274
|
+
if (settled) return;
|
|
10275
|
+
settled = true;
|
|
10276
|
+
clearTimeout(timer);
|
|
10277
|
+
resolve4(value);
|
|
10278
|
+
};
|
|
10279
|
+
promise.then(
|
|
10280
|
+
() => finish(true),
|
|
10281
|
+
() => finish(true)
|
|
10282
|
+
);
|
|
10283
|
+
});
|
|
10284
|
+
}
|
|
10285
|
+
function writeMarker(markerPath, outcomes, log3) {
|
|
10286
|
+
const body = `${STORES.map((store) => `${store}=${outcomes[store]}`).join("\n")}
|
|
10287
|
+
`;
|
|
10288
|
+
const temporaryPath = `${markerPath}.tmp`;
|
|
10289
|
+
try {
|
|
10290
|
+
writeFileSync5(temporaryPath, body, { mode: 384 });
|
|
10291
|
+
renameSync(temporaryPath, markerPath);
|
|
10292
|
+
} catch (error2) {
|
|
10293
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
|
|
10294
|
+
}
|
|
10295
|
+
}
|
|
10296
|
+
function intervalSeconds(env, log3) {
|
|
10297
|
+
const raw = env.CREDS_SYNC_INTERVAL;
|
|
10298
|
+
if (raw === void 0 || /^[1-9][0-9]*$/.test(raw) && Number.isSafeInteger(Number(raw))) {
|
|
10299
|
+
return raw === void 0 ? DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS : Number(raw);
|
|
10300
|
+
}
|
|
10301
|
+
log3(
|
|
10302
|
+
`CREDENTIAL-SYNC-INTERVAL-INVALID: CREDS_SYNC_INTERVAL='${raw}' is not a positive integer; using ${DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS}s`,
|
|
10303
|
+
"warn"
|
|
10304
|
+
);
|
|
10305
|
+
return DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS;
|
|
10306
|
+
}
|
|
10307
|
+
async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
10308
|
+
const remainingMs = deadlineAt - Date.now();
|
|
10309
|
+
if (remainingMs <= 0) return { outcome: "timeout", orphaned: false };
|
|
10310
|
+
const controller = new AbortController();
|
|
10311
|
+
let result;
|
|
10312
|
+
let failed = false;
|
|
10313
|
+
const completion = Promise.resolve().then(
|
|
10314
|
+
() => synchroniserRunner(["sync-once", store], {
|
|
10315
|
+
timeoutMs: remainingMs,
|
|
10316
|
+
env,
|
|
10317
|
+
signal: controller.signal
|
|
10318
|
+
})
|
|
10319
|
+
).then(
|
|
10320
|
+
(value) => {
|
|
10321
|
+
result = value;
|
|
10322
|
+
},
|
|
10323
|
+
(error2) => {
|
|
10324
|
+
failed = true;
|
|
10325
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
|
|
10326
|
+
}
|
|
10327
|
+
);
|
|
10328
|
+
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
10329
|
+
const settledBeforeDeadline = await waitForSettlement(completion, remainingMs);
|
|
10330
|
+
clearTimeout(abortTimer);
|
|
10331
|
+
if (!settledBeforeDeadline) {
|
|
10332
|
+
controller.abort();
|
|
10333
|
+
const settledAfterAbort = await waitForSettlement(completion, CREDENTIAL_FLUSH_ABORT_GRACE_MS);
|
|
10334
|
+
if (!settledAfterAbort) return { outcome: "timeout", orphaned: true };
|
|
10335
|
+
return { outcome: "timeout", orphaned: false };
|
|
10336
|
+
}
|
|
10337
|
+
if (failed || !result) return { outcome: "failed", orphaned: false };
|
|
10338
|
+
if (result.timedOut || Date.now() >= deadlineAt) {
|
|
10339
|
+
return { outcome: "timeout", orphaned: false };
|
|
10340
|
+
}
|
|
10341
|
+
return { outcome: result.code === 0 ? "ok" : "failed", orphaned: false };
|
|
10342
|
+
}
|
|
10343
|
+
function createCredentialSync({
|
|
10344
|
+
markerPath,
|
|
10345
|
+
env,
|
|
10346
|
+
log: log3,
|
|
10347
|
+
synchroniserRunner = runSynchroniser
|
|
10348
|
+
}) {
|
|
10349
|
+
const persistenceDisabled = !env.PERSISTENCE_BUCKET;
|
|
10350
|
+
let disabled = persistenceDisabled;
|
|
10351
|
+
let armed = false;
|
|
10352
|
+
let stopped = false;
|
|
10353
|
+
let timer;
|
|
10354
|
+
let inFlight;
|
|
10355
|
+
let activeTickAbort;
|
|
10356
|
+
let lastTickFailed;
|
|
10357
|
+
let flushPromise;
|
|
10358
|
+
const scheduleTick = (intervalMs, startTick2) => {
|
|
10359
|
+
if (stopped) return;
|
|
10360
|
+
timer = setTimeout(() => {
|
|
10361
|
+
timer = void 0;
|
|
10362
|
+
startTick2();
|
|
10363
|
+
}, intervalMs);
|
|
10364
|
+
};
|
|
10365
|
+
const startTick = (intervalMs) => {
|
|
10366
|
+
if (stopped) return;
|
|
10367
|
+
const controller = new AbortController();
|
|
10368
|
+
activeTickAbort = controller;
|
|
10369
|
+
const tick = (async () => {
|
|
10370
|
+
const outcomes = {
|
|
10371
|
+
claude: "failed",
|
|
10372
|
+
opencode: "failed"
|
|
10373
|
+
};
|
|
10374
|
+
for (const store of STORES) {
|
|
10375
|
+
if (controller.signal.aborted) break;
|
|
10376
|
+
try {
|
|
10377
|
+
const result = await synchroniserRunner(["sync-once", store], {
|
|
10378
|
+
timeoutMs: CREDENTIAL_SYNC_TICK_TIMEOUT_MS,
|
|
10379
|
+
env,
|
|
10380
|
+
signal: controller.signal
|
|
10381
|
+
});
|
|
10382
|
+
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
10383
|
+
} catch (error2) {
|
|
10384
|
+
outcomes[store] = "failed";
|
|
10385
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
|
|
10386
|
+
}
|
|
10387
|
+
}
|
|
10388
|
+
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
10389
|
+
log3(
|
|
10390
|
+
`CREDENTIAL-SYNC-TICK: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
|
|
10391
|
+
"debug"
|
|
10392
|
+
);
|
|
10393
|
+
if (failed && lastTickFailed !== true) {
|
|
10394
|
+
log3(
|
|
10395
|
+
"CREDENTIAL-SYNC-FAILED: an interval credential sync failed; retrying on the next tick",
|
|
10396
|
+
"warn"
|
|
10397
|
+
);
|
|
10398
|
+
} else if (!failed && lastTickFailed === true) {
|
|
10399
|
+
log3("CREDENTIAL-SYNC-RECOVERED: interval credential sync succeeded again", "warn");
|
|
10400
|
+
}
|
|
10401
|
+
lastTickFailed = failed;
|
|
10402
|
+
})().finally(() => {
|
|
10403
|
+
if (activeTickAbort === controller) activeTickAbort = void 0;
|
|
10404
|
+
if (inFlight === tick) inFlight = void 0;
|
|
10405
|
+
scheduleTick(intervalMs, () => startTick(intervalMs));
|
|
10406
|
+
});
|
|
10407
|
+
inFlight = tick;
|
|
10408
|
+
};
|
|
10409
|
+
const performFlush = async () => {
|
|
10410
|
+
stopped = true;
|
|
10411
|
+
if (timer) {
|
|
10412
|
+
clearTimeout(timer);
|
|
10413
|
+
timer = void 0;
|
|
10414
|
+
}
|
|
10415
|
+
const deadlineAt = Date.now() + CREDENTIAL_FLUSH_DEADLINE_MS;
|
|
10416
|
+
if (inFlight) {
|
|
10417
|
+
const settled = await waitForSettlement(inFlight, CREDENTIAL_FLUSH_DEADLINE_MS);
|
|
10418
|
+
if (!settled) {
|
|
10419
|
+
activeTickAbort?.abort();
|
|
10420
|
+
const settledAfterAbort = await waitForSettlement(
|
|
10421
|
+
inFlight,
|
|
10422
|
+
CREDENTIAL_FLUSH_ABORT_GRACE_MS
|
|
10423
|
+
);
|
|
10424
|
+
if (!settledAfterAbort) {
|
|
10425
|
+
log3(
|
|
10426
|
+
"CREDENTIAL-FLUSH-ORPHANED-TICK: a sync-once child could not be proven settled; skipping the boundary flush rather than becoming a second writer",
|
|
10427
|
+
"warn"
|
|
10428
|
+
);
|
|
10429
|
+
return { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
10430
|
+
}
|
|
10431
|
+
}
|
|
10432
|
+
}
|
|
10433
|
+
if (disabled) return { outcomes: outcomesWith("skipped"), orphaned: false };
|
|
10434
|
+
const outcomes = outcomesWith("timeout");
|
|
10435
|
+
for (const store of STORES) {
|
|
10436
|
+
const result = await runFlushStore(store, env, synchroniserRunner, log3, deadlineAt);
|
|
10437
|
+
if (result.orphaned) {
|
|
10438
|
+
log3(
|
|
10439
|
+
"CREDENTIAL-FLUSH-ORPHANED-CHILD: a flush sync-once child could not be proven settled; refusing another flush pass rather than becoming a second writer",
|
|
10440
|
+
"warn"
|
|
10441
|
+
);
|
|
10442
|
+
return { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
10443
|
+
}
|
|
10444
|
+
outcomes[store] = result.outcome;
|
|
10445
|
+
}
|
|
10446
|
+
return { outcomes, orphaned: false };
|
|
10447
|
+
};
|
|
10448
|
+
let flushPasses = 0;
|
|
10449
|
+
let lastFlush;
|
|
10450
|
+
return {
|
|
10451
|
+
arm() {
|
|
10452
|
+
if (stopped || armed) return;
|
|
10453
|
+
armed = true;
|
|
10454
|
+
if (persistenceDisabled) {
|
|
10455
|
+
disabled = true;
|
|
10456
|
+
log3(
|
|
10457
|
+
"CREDENTIAL-SYNC-DISABLED: LITESTREAM_BUCKET/LITESTREAM_PREFIX are not both set; no interval credential sync this boot",
|
|
10458
|
+
"warn"
|
|
10459
|
+
);
|
|
10460
|
+
return;
|
|
10461
|
+
}
|
|
10462
|
+
disabled = false;
|
|
10463
|
+
const intervalMs = intervalSeconds(env, log3) * 1e3;
|
|
10464
|
+
scheduleTick(intervalMs, () => startTick(intervalMs));
|
|
10465
|
+
},
|
|
10466
|
+
async stopAndFlush(publish) {
|
|
10467
|
+
let result;
|
|
10468
|
+
const runningFlush = flushPromise;
|
|
10469
|
+
if (runningFlush) {
|
|
10470
|
+
result = await runningFlush;
|
|
10471
|
+
} else if (flushPasses >= MAX_FLUSH_PASSES || lastFlush?.orphaned) {
|
|
10472
|
+
result = lastFlush ?? { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
10473
|
+
} else {
|
|
10474
|
+
flushPasses++;
|
|
10475
|
+
const currentFlush = performFlush();
|
|
10476
|
+
flushPromise = currentFlush;
|
|
10477
|
+
try {
|
|
10478
|
+
result = await currentFlush;
|
|
10479
|
+
lastFlush = result;
|
|
10480
|
+
} finally {
|
|
10481
|
+
if (flushPromise === currentFlush) flushPromise = void 0;
|
|
10482
|
+
}
|
|
10483
|
+
}
|
|
10484
|
+
if (publish) writeMarker(markerPath, result.outcomes, log3);
|
|
10485
|
+
return result.outcomes;
|
|
10486
|
+
}
|
|
10487
|
+
};
|
|
9080
10488
|
}
|
|
9081
10489
|
|
|
9082
10490
|
// src/commands/run.ts
|
|
@@ -9116,7 +10524,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9116
10524
|
if (trimmed === "") {
|
|
9117
10525
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
9118
10526
|
}
|
|
9119
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
10527
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join10(homeDir, trimmed.slice(2)) : trimmed;
|
|
9120
10528
|
if (!isAbsolute3(expanded)) {
|
|
9121
10529
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
9122
10530
|
}
|
|
@@ -9140,6 +10548,28 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9140
10548
|
var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
|
|
9141
10549
|
var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
|
|
9142
10550
|
var OPENCODE_START_TIMEOUT_ENV = "EVIDENT_OPENCODE_START_TIMEOUT";
|
|
10551
|
+
var OPENCODE_VERSION_ENV = "EVIDENT_OPENCODE_VERSION";
|
|
10552
|
+
function resolveOpenCodeVersion(options, env = process.env) {
|
|
10553
|
+
let raw;
|
|
10554
|
+
let source;
|
|
10555
|
+
if (options.opencodeVersion !== void 0) {
|
|
10556
|
+
raw = options.opencodeVersion;
|
|
10557
|
+
source = "--opencode-version";
|
|
10558
|
+
} else if (env[OPENCODE_VERSION_ENV] !== void 0 && env[OPENCODE_VERSION_ENV] !== "") {
|
|
10559
|
+
raw = env[OPENCODE_VERSION_ENV];
|
|
10560
|
+
source = OPENCODE_VERSION_ENV;
|
|
10561
|
+
} else {
|
|
10562
|
+
return { version: "v1", warnings: [] };
|
|
10563
|
+
}
|
|
10564
|
+
const normalized = raw.trim().toLowerCase();
|
|
10565
|
+
if (normalized !== "v1" && normalized !== "v2") {
|
|
10566
|
+
return {
|
|
10567
|
+
version: "v1",
|
|
10568
|
+
warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or v2; using the default v1`]
|
|
10569
|
+
};
|
|
10570
|
+
}
|
|
10571
|
+
return { version: normalized, warnings: [] };
|
|
10572
|
+
}
|
|
9143
10573
|
function resolveOpenCodeStartTimeoutMs(options, env = process.env) {
|
|
9144
10574
|
const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1e3;
|
|
9145
10575
|
let raw;
|
|
@@ -9206,7 +10636,7 @@ function log2(state, message, level = "info") {
|
|
|
9206
10636
|
})
|
|
9207
10637
|
);
|
|
9208
10638
|
} else if (!state.interactive) {
|
|
9209
|
-
const prefix = level === "error" ?
|
|
10639
|
+
const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
|
|
9210
10640
|
console.log(`${prefix} ${message}`);
|
|
9211
10641
|
}
|
|
9212
10642
|
}
|
|
@@ -9236,7 +10666,7 @@ function logActivity(state, entry) {
|
|
|
9236
10666
|
}
|
|
9237
10667
|
function reportSessionDbRecovery(state) {
|
|
9238
10668
|
try {
|
|
9239
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
10669
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
|
|
9240
10670
|
for (const record of report.records) {
|
|
9241
10671
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
9242
10672
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -9267,18 +10697,18 @@ function reportSessionDbRecoveryRecord(state, record) {
|
|
|
9267
10697
|
function displayStatus(state) {
|
|
9268
10698
|
if (!state.interactive) return;
|
|
9269
10699
|
const attempt = state.connection?.reconnectAttempt ?? 0;
|
|
9270
|
-
const tunnel = state.connected ?
|
|
9271
|
-
const opencode = state.opencodeConnected ?
|
|
9272
|
-
const messages = state.messageCount > 0 ?
|
|
10700
|
+
const tunnel = state.connected ? chalk7.green("tunnel: connected") : attempt > 0 ? chalk7.yellow(`tunnel: reconnecting (#${attempt})`) : chalk7.yellow("tunnel: connecting");
|
|
10701
|
+
const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
|
|
10702
|
+
const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
|
|
9273
10703
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
9274
|
-
const detail = last ?
|
|
10704
|
+
const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
|
|
9275
10705
|
const agent = state.agentName ?? state.agentId;
|
|
9276
10706
|
console.log(
|
|
9277
|
-
`${
|
|
10707
|
+
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
9278
10708
|
);
|
|
9279
10709
|
}
|
|
9280
10710
|
async function promptForLogin(promptMessage, successMessage) {
|
|
9281
|
-
const action = await
|
|
10711
|
+
const action = await select4({
|
|
9282
10712
|
message: promptMessage,
|
|
9283
10713
|
choices: [
|
|
9284
10714
|
{
|
|
@@ -9294,7 +10724,7 @@ async function promptForLogin(promptMessage, successMessage) {
|
|
|
9294
10724
|
]
|
|
9295
10725
|
});
|
|
9296
10726
|
if (action === "exit") {
|
|
9297
|
-
console.log(
|
|
10727
|
+
console.log(chalk7.dim(`
|
|
9298
10728
|
You can log in later by running: ${getCliName()} login`));
|
|
9299
10729
|
process.exit(0);
|
|
9300
10730
|
}
|
|
@@ -9305,7 +10735,7 @@ You can log in later by running: ${getCliName()} login`));
|
|
|
9305
10735
|
process.exit(1);
|
|
9306
10736
|
}
|
|
9307
10737
|
blank();
|
|
9308
|
-
console.log(
|
|
10738
|
+
console.log(chalk7.green(successMessage));
|
|
9309
10739
|
blank();
|
|
9310
10740
|
return { token: credentials2.token, authType: "bearer", user: credentials2.user };
|
|
9311
10741
|
}
|
|
@@ -9318,12 +10748,12 @@ async function handleAuthError(state, error2) {
|
|
|
9318
10748
|
if (state.interactive) displayStatus(state);
|
|
9319
10749
|
if (!state.interactive) {
|
|
9320
10750
|
blank();
|
|
9321
|
-
console.log(
|
|
9322
|
-
console.log(
|
|
10751
|
+
console.log(chalk7.red("Authentication expired"));
|
|
10752
|
+
console.log(chalk7.dim("Your authentication token is no longer valid."));
|
|
9323
10753
|
blank();
|
|
9324
|
-
console.log(
|
|
9325
|
-
console.log(
|
|
9326
|
-
console.log(
|
|
10754
|
+
console.log(chalk7.dim("To fix this:"));
|
|
10755
|
+
console.log(chalk7.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
|
|
10756
|
+
console.log(chalk7.dim(" 2. Restart this command"));
|
|
9327
10757
|
blank();
|
|
9328
10758
|
await cleanup(state);
|
|
9329
10759
|
await shutdownTelemetry();
|
|
@@ -9331,7 +10761,7 @@ async function handleAuthError(state, error2) {
|
|
|
9331
10761
|
return { success: false };
|
|
9332
10762
|
}
|
|
9333
10763
|
blank();
|
|
9334
|
-
console.log(
|
|
10764
|
+
console.log(chalk7.yellow("Your authentication has expired."));
|
|
9335
10765
|
blank();
|
|
9336
10766
|
try {
|
|
9337
10767
|
const credentials2 = await promptForLogin(
|
|
@@ -9376,6 +10806,10 @@ async function driveChannels(state, driver) {
|
|
|
9376
10806
|
consecutiveDrainFailures = 0;
|
|
9377
10807
|
unreachableMs = 0;
|
|
9378
10808
|
state.messageCount += processed;
|
|
10809
|
+
if (driver.recycleRequested) {
|
|
10810
|
+
await beginGracefulShutdown(state, "recycle");
|
|
10811
|
+
return;
|
|
10812
|
+
}
|
|
9379
10813
|
const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
|
|
9380
10814
|
lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
|
|
9381
10815
|
const fileActivitySnapshot = driver.fileSyncActivity();
|
|
@@ -9391,6 +10825,14 @@ async function driveChannels(state, driver) {
|
|
|
9391
10825
|
const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
|
|
9392
10826
|
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
9393
10827
|
if (opencodeAuthApplied) state.openaiUsageRearm?.();
|
|
10828
|
+
if (claudeCredentialApplied || opencodeAuthApplied) {
|
|
10829
|
+
void reloadProviderCache(state.port).catch(
|
|
10830
|
+
(error2) => logActivity(state, {
|
|
10831
|
+
type: "error",
|
|
10832
|
+
error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
10833
|
+
})
|
|
10834
|
+
);
|
|
10835
|
+
}
|
|
9394
10836
|
if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
|
|
9395
10837
|
idlePolls = 0;
|
|
9396
10838
|
idleMs = 0;
|
|
@@ -9418,8 +10860,8 @@ async function driveChannels(state, driver) {
|
|
|
9418
10860
|
state.running = false;
|
|
9419
10861
|
break;
|
|
9420
10862
|
}
|
|
9421
|
-
const
|
|
9422
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
10863
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
|
|
10864
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage3}` });
|
|
9423
10865
|
if (state.interactive) displayStatus(state);
|
|
9424
10866
|
if (driver.hasInFlightWatchers()) {
|
|
9425
10867
|
consecutiveDrainFailures = 0;
|
|
@@ -9457,9 +10899,18 @@ async function driveChannels(state, driver) {
|
|
|
9457
10899
|
}
|
|
9458
10900
|
}
|
|
9459
10901
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
9460
|
-
var SESSION_DB_RECLAIM_MAX_PAGES =
|
|
10902
|
+
var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
|
|
10903
|
+
function shouldWarnForReclaimSkip(reason) {
|
|
10904
|
+
if (reason !== "sqlite-unavailable") return false;
|
|
10905
|
+
const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
|
|
10906
|
+
if (!version2) return false;
|
|
10907
|
+
const major = Number(version2[1]);
|
|
10908
|
+
const minor = Number(version2[2]);
|
|
10909
|
+
const patch = Number(version2[3]);
|
|
10910
|
+
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
10911
|
+
}
|
|
9461
10912
|
function sessionDbPath() {
|
|
9462
|
-
return
|
|
10913
|
+
return join10(homedir6(), ".local", "share", "opencode", "opencode.db");
|
|
9463
10914
|
}
|
|
9464
10915
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
9465
10916
|
const record = {
|
|
@@ -9555,7 +11006,7 @@ async function runSweep(state, driver, config) {
|
|
|
9555
11006
|
} else {
|
|
9556
11007
|
logActivity(state, {
|
|
9557
11008
|
type: "info",
|
|
9558
|
-
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})`
|
|
11009
|
+
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : "")
|
|
9559
11010
|
});
|
|
9560
11011
|
}
|
|
9561
11012
|
} catch (error2) {
|
|
@@ -9578,13 +11029,20 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
9578
11029
|
for (const warning2 of config.warnings) {
|
|
9579
11030
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
9580
11031
|
}
|
|
9581
|
-
const dbBytes = statSessionDbBytes(
|
|
11032
|
+
const dbBytes = statSessionDbBytes(homedir6());
|
|
9582
11033
|
void (async () => {
|
|
9583
|
-
const
|
|
11034
|
+
const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
11035
|
+
if (reclaimAvailability !== null) {
|
|
11036
|
+
logActivity(state, {
|
|
11037
|
+
type: "info",
|
|
11038
|
+
level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
|
|
11039
|
+
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
|
|
11040
|
+
});
|
|
11041
|
+
}
|
|
9584
11042
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
9585
11043
|
dbBytes,
|
|
9586
11044
|
cleanupEnabled: config.enabled,
|
|
9587
|
-
reclaimSkipReason
|
|
11045
|
+
reclaimSkipReason: reclaimAvailability?.reason ?? null
|
|
9588
11046
|
});
|
|
9589
11047
|
if (sizeWarning !== null) {
|
|
9590
11048
|
logActivity(state, { type: "info", level: "warn", message: sizeWarning });
|
|
@@ -9751,14 +11209,11 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
9751
11209
|
report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
|
|
9752
11210
|
isLocalCredentialProblem,
|
|
9753
11211
|
forcedOnHint: "run `claude` to sign in",
|
|
9754
|
-
firstDelayMs:
|
|
9755
|
-
nextDelayMs:
|
|
9756
|
-
failureLogLevel:
|
|
11212
|
+
firstDelayMs: firstReportDelayMs,
|
|
11213
|
+
nextDelayMs: usageReportDelayMs,
|
|
11214
|
+
failureLogLevel: usageReportFailureLogLevel
|
|
9757
11215
|
});
|
|
9758
11216
|
}
|
|
9759
|
-
var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
|
|
9760
|
-
var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
9761
|
-
var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
9762
11217
|
function scheduleResourceUsageReporting(state, options) {
|
|
9763
11218
|
const { enabled, warnings } = resolveResourceUsageReportingEnabled(
|
|
9764
11219
|
options.resourceUsageReporting,
|
|
@@ -9779,7 +11234,8 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
9779
11234
|
});
|
|
9780
11235
|
return;
|
|
9781
11236
|
}
|
|
9782
|
-
const collect = createResourceUsageCollector(
|
|
11237
|
+
const { collect, stop } = createResourceUsageCollector(homedir6());
|
|
11238
|
+
state.stopResourceUsageSampling = stop;
|
|
9783
11239
|
let consecutiveFailures = 0;
|
|
9784
11240
|
const tick = async () => {
|
|
9785
11241
|
try {
|
|
@@ -9810,10 +11266,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
9810
11266
|
consecutiveFailures++;
|
|
9811
11267
|
logActivity(state, {
|
|
9812
11268
|
type: "info",
|
|
9813
|
-
level:
|
|
9814
|
-
consecutiveFailures,
|
|
9815
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
9816
|
-
),
|
|
11269
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
9817
11270
|
message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
|
|
9818
11271
|
});
|
|
9819
11272
|
}
|
|
@@ -9822,20 +11275,11 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
9822
11275
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
9823
11276
|
logActivity(state, {
|
|
9824
11277
|
type: "info",
|
|
9825
|
-
level:
|
|
9826
|
-
consecutiveFailures,
|
|
9827
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
9828
|
-
),
|
|
11278
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
9829
11279
|
message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
|
|
9830
11280
|
});
|
|
9831
11281
|
} finally {
|
|
9832
|
-
state.resourceUsageTimer = setTimeout(
|
|
9833
|
-
() => void tick(),
|
|
9834
|
-
jitteredDelayMs(
|
|
9835
|
-
RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
|
|
9836
|
-
RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
|
|
9837
|
-
)
|
|
9838
|
-
);
|
|
11282
|
+
state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
|
|
9839
11283
|
}
|
|
9840
11284
|
};
|
|
9841
11285
|
state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
|
|
@@ -9875,6 +11319,8 @@ async function cleanup(state, opts = {}) {
|
|
|
9875
11319
|
clearTimeout(timer);
|
|
9876
11320
|
}
|
|
9877
11321
|
state.sessionCleanupTimers = [];
|
|
11322
|
+
state.stopOpenCodeLogTail?.();
|
|
11323
|
+
state.stopOpenCodeLogTail = null;
|
|
9878
11324
|
if (state.claudeUsageTimer) {
|
|
9879
11325
|
clearTimeout(state.claudeUsageTimer);
|
|
9880
11326
|
state.claudeUsageTimer = null;
|
|
@@ -9889,21 +11335,41 @@ async function cleanup(state, opts = {}) {
|
|
|
9889
11335
|
clearTimeout(state.resourceUsageTimer);
|
|
9890
11336
|
state.resourceUsageTimer = null;
|
|
9891
11337
|
}
|
|
11338
|
+
state.stopResourceUsageSampling?.();
|
|
11339
|
+
state.stopResourceUsageSampling = null;
|
|
11340
|
+
const credentialSync = state.credentialSync;
|
|
11341
|
+
const flushCredentials = credentialSync ? async (phase, publish) => {
|
|
11342
|
+
await timeShutdownPhase(state, durations, phase, async () => {
|
|
11343
|
+
const outcomes = await credentialSync.stopAndFlush(publish);
|
|
11344
|
+
const level = Object.values(outcomes).every((outcome) => outcome === "ok") ? "info" : "warn";
|
|
11345
|
+
log2(
|
|
11346
|
+
state,
|
|
11347
|
+
`Credential flush: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
|
|
11348
|
+
level
|
|
11349
|
+
);
|
|
11350
|
+
});
|
|
11351
|
+
} : void 0;
|
|
11352
|
+
let drainSettled = true;
|
|
9892
11353
|
if (opts.graceful && state.channelDriver) {
|
|
9893
11354
|
state.channelDriver.stop();
|
|
11355
|
+
}
|
|
11356
|
+
if (flushCredentials) {
|
|
11357
|
+
await flushCredentials("credential_flush", !(opts.graceful && state.channelDriver));
|
|
11358
|
+
}
|
|
11359
|
+
if (opts.graceful && state.channelDriver) {
|
|
9894
11360
|
log2(state, "Draining in-flight channel work before shutdown...");
|
|
9895
11361
|
if (state.interactive) {
|
|
9896
11362
|
logActivity(state, { type: "info", message: "Draining in-flight work before shutdown..." });
|
|
9897
11363
|
displayStatus(state);
|
|
9898
11364
|
}
|
|
9899
11365
|
const driver = state.channelDriver;
|
|
9900
|
-
|
|
11366
|
+
drainSettled = await timeShutdownPhase(
|
|
9901
11367
|
state,
|
|
9902
11368
|
durations,
|
|
9903
11369
|
"drain",
|
|
9904
11370
|
() => driver.waitForInFlight(SHUTDOWN_DRAIN_TIMEOUT_MS)
|
|
9905
11371
|
);
|
|
9906
|
-
if (!
|
|
11372
|
+
if (!drainSettled) {
|
|
9907
11373
|
logActivity(state, {
|
|
9908
11374
|
type: "info",
|
|
9909
11375
|
message: "Shutdown drain timed out with work still in flight \u2014 leaving it for restart recovery"
|
|
@@ -9911,6 +11377,9 @@ async function cleanup(state, opts = {}) {
|
|
|
9911
11377
|
if (state.interactive) displayStatus(state);
|
|
9912
11378
|
}
|
|
9913
11379
|
}
|
|
11380
|
+
if (opts.graceful && state.channelDriver && flushCredentials && drainSettled) {
|
|
11381
|
+
await flushCredentials("credential_flush_final", true);
|
|
11382
|
+
}
|
|
9914
11383
|
await timeShutdownPhase(state, durations, "offline_notify", () => notifyOffline(state));
|
|
9915
11384
|
if (state.connection) {
|
|
9916
11385
|
const connection = state.connection;
|
|
@@ -9946,13 +11415,51 @@ async function cleanup(state, opts = {}) {
|
|
|
9946
11415
|
}
|
|
9947
11416
|
return durations;
|
|
9948
11417
|
}
|
|
11418
|
+
async function beginGracefulShutdown(state, trigger) {
|
|
11419
|
+
if (state.shuttingDown) return;
|
|
11420
|
+
state.shuttingDown = true;
|
|
11421
|
+
const shutdownStartedAt = Date.now();
|
|
11422
|
+
const shutdownMessage = trigger === "recycle" ? "Recycle requested \u2014 finishing in-flight work, then exiting" : "Shutting down...";
|
|
11423
|
+
if (state.interactive) {
|
|
11424
|
+
logActivity(state, { type: "info", message: shutdownMessage });
|
|
11425
|
+
displayStatus(state);
|
|
11426
|
+
} else {
|
|
11427
|
+
log2(state, shutdownMessage);
|
|
11428
|
+
}
|
|
11429
|
+
const durations = await cleanup(state, { graceful: true });
|
|
11430
|
+
const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
|
|
11431
|
+
await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
|
|
11432
|
+
let timer;
|
|
11433
|
+
const flushed = shutdownTelemetry().then(
|
|
11434
|
+
() => true,
|
|
11435
|
+
(error2) => {
|
|
11436
|
+
log2(
|
|
11437
|
+
state,
|
|
11438
|
+
`Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
11439
|
+
"warn"
|
|
11440
|
+
);
|
|
11441
|
+
return true;
|
|
11442
|
+
}
|
|
11443
|
+
);
|
|
11444
|
+
const timedOut = new Promise((resolve4) => {
|
|
11445
|
+
timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
|
|
11446
|
+
});
|
|
11447
|
+
if (!await Promise.race([flushed, timedOut])) {
|
|
11448
|
+
log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
|
|
11449
|
+
}
|
|
11450
|
+
clearTimeout(timer);
|
|
11451
|
+
});
|
|
11452
|
+
const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
|
|
11453
|
+
log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
|
|
11454
|
+
process.exit(0);
|
|
11455
|
+
}
|
|
9949
11456
|
async function run(options) {
|
|
9950
11457
|
const interactive = isInteractive(options.json);
|
|
9951
11458
|
let logLevel;
|
|
9952
11459
|
let fileSyncDirectories;
|
|
9953
11460
|
try {
|
|
9954
11461
|
logLevel = resolveLogLevel(options);
|
|
9955
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
11462
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
|
|
9956
11463
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
9957
11464
|
throw new Error(
|
|
9958
11465
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -9983,6 +11490,7 @@ async function run(options) {
|
|
|
9983
11490
|
opencodeVersion: null,
|
|
9984
11491
|
sessionDbProvenanceAnomaly: false,
|
|
9985
11492
|
opencodeProcess: null,
|
|
11493
|
+
stopOpenCodeLogTail: null,
|
|
9986
11494
|
litestreamProcess: null,
|
|
9987
11495
|
connection: null,
|
|
9988
11496
|
channelDriver: null,
|
|
@@ -9997,9 +11505,24 @@ async function run(options) {
|
|
|
9997
11505
|
openaiUsageTimer: null,
|
|
9998
11506
|
openaiUsageRearm: null,
|
|
9999
11507
|
resourceUsageTimer: null,
|
|
11508
|
+
stopResourceUsageSampling: null,
|
|
11509
|
+
credentialSync: null,
|
|
10000
11510
|
authHeader: ""
|
|
10001
11511
|
};
|
|
10002
11512
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
11513
|
+
if (options.credentialSyncMarker) {
|
|
11514
|
+
state.credentialSync = createCredentialSync({
|
|
11515
|
+
markerPath: options.credentialSyncMarker,
|
|
11516
|
+
env: process.env,
|
|
11517
|
+
log: (message, level = "info") => {
|
|
11518
|
+
if (level === "error") {
|
|
11519
|
+
logActivity(state, { type: "error", error: message });
|
|
11520
|
+
} else {
|
|
11521
|
+
logActivity(state, { type: "info", level, message });
|
|
11522
|
+
}
|
|
11523
|
+
}
|
|
11524
|
+
});
|
|
11525
|
+
}
|
|
10003
11526
|
if (fileSyncDirectories.length > 0) {
|
|
10004
11527
|
log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
|
|
10005
11528
|
} else {
|
|
@@ -10025,43 +11548,7 @@ async function run(options) {
|
|
|
10025
11548
|
"warn"
|
|
10026
11549
|
);
|
|
10027
11550
|
}
|
|
10028
|
-
const handleSignal =
|
|
10029
|
-
if (state.shuttingDown) return;
|
|
10030
|
-
state.shuttingDown = true;
|
|
10031
|
-
const shutdownStartedAt = Date.now();
|
|
10032
|
-
if (state.interactive) {
|
|
10033
|
-
logActivity(state, { type: "info", message: "Shutting down..." });
|
|
10034
|
-
displayStatus(state);
|
|
10035
|
-
} else {
|
|
10036
|
-
log2(state, "Shutting down...");
|
|
10037
|
-
}
|
|
10038
|
-
const durations = await cleanup(state, { graceful: true });
|
|
10039
|
-
const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
|
|
10040
|
-
await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
|
|
10041
|
-
let timer;
|
|
10042
|
-
const flushed = shutdownTelemetry().then(
|
|
10043
|
-
() => true,
|
|
10044
|
-
(error2) => {
|
|
10045
|
-
log2(
|
|
10046
|
-
state,
|
|
10047
|
-
`Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
10048
|
-
"warn"
|
|
10049
|
-
);
|
|
10050
|
-
return true;
|
|
10051
|
-
}
|
|
10052
|
-
);
|
|
10053
|
-
const timedOut = new Promise((resolve4) => {
|
|
10054
|
-
timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
|
|
10055
|
-
});
|
|
10056
|
-
if (!await Promise.race([flushed, timedOut])) {
|
|
10057
|
-
log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
|
|
10058
|
-
}
|
|
10059
|
-
clearTimeout(timer);
|
|
10060
|
-
});
|
|
10061
|
-
const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
|
|
10062
|
-
log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
|
|
10063
|
-
process.exit(0);
|
|
10064
|
-
};
|
|
11551
|
+
const handleSignal = () => beginGracefulShutdown(state, "signal");
|
|
10065
11552
|
process.on("SIGINT", handleSignal);
|
|
10066
11553
|
process.on("SIGTERM", handleSignal);
|
|
10067
11554
|
try {
|
|
@@ -10071,15 +11558,15 @@ async function run(options) {
|
|
|
10071
11558
|
printError("Authentication required");
|
|
10072
11559
|
blank();
|
|
10073
11560
|
console.log(
|
|
10074
|
-
|
|
11561
|
+
chalk7.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
|
|
10075
11562
|
);
|
|
10076
|
-
console.log(
|
|
11563
|
+
console.log(chalk7.dim("Or run `evident login` for interactive authentication"));
|
|
10077
11564
|
blank();
|
|
10078
11565
|
process.exit(1);
|
|
10079
11566
|
return;
|
|
10080
11567
|
}
|
|
10081
11568
|
blank();
|
|
10082
|
-
console.log(
|
|
11569
|
+
console.log(chalk7.yellow("You are not logged in to Evident."));
|
|
10083
11570
|
blank();
|
|
10084
11571
|
credentials2 = await promptForLogin(
|
|
10085
11572
|
"Would you like to log in now?",
|
|
@@ -10129,7 +11616,7 @@ async function run(options) {
|
|
|
10129
11616
|
);
|
|
10130
11617
|
blank();
|
|
10131
11618
|
console.log(
|
|
10132
|
-
|
|
11619
|
+
chalk7.dim(
|
|
10133
11620
|
"Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
|
|
10134
11621
|
)
|
|
10135
11622
|
);
|
|
@@ -10152,15 +11639,15 @@ async function run(options) {
|
|
|
10152
11639
|
);
|
|
10153
11640
|
if (interactive && !state.json) {
|
|
10154
11641
|
blank();
|
|
10155
|
-
console.log(
|
|
10156
|
-
console.log(
|
|
11642
|
+
console.log(chalk7.bold("Evident Run"));
|
|
11643
|
+
console.log(chalk7.dim("-".repeat(40)));
|
|
10157
11644
|
}
|
|
10158
11645
|
const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
|
|
10159
11646
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
10160
11647
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
10161
11648
|
spinner?.fail("Authentication failed");
|
|
10162
11649
|
blank();
|
|
10163
|
-
console.log(
|
|
11650
|
+
console.log(chalk7.yellow("Your authentication token is invalid or expired."));
|
|
10164
11651
|
blank();
|
|
10165
11652
|
credentials2 = await promptForLogin(
|
|
10166
11653
|
"Would you like to log in again?",
|
|
@@ -10207,6 +11694,14 @@ async function run(options) {
|
|
|
10207
11694
|
await restoreCredentialStores(credentialContext);
|
|
10208
11695
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
10209
11696
|
}
|
|
11697
|
+
state.credentialSync?.arm();
|
|
11698
|
+
state.stopOpenCodeLogTail = tailOpenCodeLogFile(
|
|
11699
|
+
resolveOpenCodeLogPath(homedir6(), process.env),
|
|
11700
|
+
createOpenCodeActivityForwarder(() => ({
|
|
11701
|
+
agentId: state.agentId,
|
|
11702
|
+
authHeader: state.authHeader
|
|
11703
|
+
}))
|
|
11704
|
+
).stop;
|
|
10210
11705
|
let sessionDbVerifyFatal = false;
|
|
10211
11706
|
if (!options.restoreSessionDb) {
|
|
10212
11707
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -10256,6 +11751,13 @@ async function run(options) {
|
|
|
10256
11751
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
10257
11752
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
10258
11753
|
}
|
|
11754
|
+
const { version: opencodeVersion, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(
|
|
11755
|
+
options,
|
|
11756
|
+
process.env
|
|
11757
|
+
);
|
|
11758
|
+
for (const warning2 of opencodeVersionWarnings) {
|
|
11759
|
+
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
11760
|
+
}
|
|
10259
11761
|
const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
|
|
10260
11762
|
for (const warning2 of maxActiveSessionsWarnings) {
|
|
10261
11763
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
@@ -10263,7 +11765,14 @@ async function run(options) {
|
|
|
10263
11765
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
10264
11766
|
const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
|
|
10265
11767
|
try {
|
|
10266
|
-
const oc = await
|
|
11768
|
+
const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
|
|
11769
|
+
port: state.port,
|
|
11770
|
+
interactive: state.interactive,
|
|
11771
|
+
agentId: state.agentId,
|
|
11772
|
+
log: (message) => log2(state, message),
|
|
11773
|
+
startTimeoutMs: opencodeStartTimeoutMs,
|
|
11774
|
+
inheritStdio: Boolean(options.opencodePidFile)
|
|
11775
|
+
}) : await ensureOpenCodeRunning({
|
|
10267
11776
|
port: state.port,
|
|
10268
11777
|
interactive: state.interactive,
|
|
10269
11778
|
agentId: state.agentId,
|
|
@@ -10276,7 +11785,7 @@ async function run(options) {
|
|
|
10276
11785
|
state.opencodeVersion = oc.version;
|
|
10277
11786
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
10278
11787
|
try {
|
|
10279
|
-
|
|
11788
|
+
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
10280
11789
|
`, { mode: 384 });
|
|
10281
11790
|
chmodSync3(options.opencodePidFile, 384);
|
|
10282
11791
|
} catch (error2) {
|
|
@@ -10290,7 +11799,7 @@ async function run(options) {
|
|
|
10290
11799
|
const provenance = checkSessionDbProvenance({
|
|
10291
11800
|
dbPath: sessionDbPath(),
|
|
10292
11801
|
currentVersion: state.opencodeVersion,
|
|
10293
|
-
homeDir:
|
|
11802
|
+
homeDir: homedir6(),
|
|
10294
11803
|
env: process.env
|
|
10295
11804
|
});
|
|
10296
11805
|
if (provenance.anomaly) {
|
|
@@ -10317,6 +11826,7 @@ async function run(options) {
|
|
|
10317
11826
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
10318
11827
|
}
|
|
10319
11828
|
}
|
|
11829
|
+
await reloadProviderCache(state.port);
|
|
10320
11830
|
const noProviderWarning = buildNoProviderWarning(
|
|
10321
11831
|
await hasAnyConfiguredProvider(state.port)
|
|
10322
11832
|
);
|
|
@@ -10325,10 +11835,10 @@ async function run(options) {
|
|
|
10325
11835
|
if (state.interactive && !state.json) {
|
|
10326
11836
|
logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
|
|
10327
11837
|
blank();
|
|
10328
|
-
console.log(
|
|
11838
|
+
console.log(chalk7.yellow("\u26A0 No OpenCode model provider is configured."));
|
|
10329
11839
|
console.log(
|
|
10330
|
-
|
|
10331
|
-
`Run ${
|
|
11840
|
+
chalk7.dim(
|
|
11841
|
+
`Run ${chalk7.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
|
|
10332
11842
|
)
|
|
10333
11843
|
);
|
|
10334
11844
|
blank();
|
|
@@ -10392,7 +11902,7 @@ async function run(options) {
|
|
|
10392
11902
|
});
|
|
10393
11903
|
try {
|
|
10394
11904
|
if (litestreamProcess.pid !== void 0) {
|
|
10395
|
-
|
|
11905
|
+
writeFileSync6(options.litestreamPidFile, `${litestreamProcess.pid}
|
|
10396
11906
|
`, {
|
|
10397
11907
|
mode: 384
|
|
10398
11908
|
});
|
|
@@ -10452,7 +11962,7 @@ async function run(options) {
|
|
|
10452
11962
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
10453
11963
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
10454
11964
|
fileSyncDirectories,
|
|
10455
|
-
homeDir:
|
|
11965
|
+
homeDir: homedir6(),
|
|
10456
11966
|
maxActiveSessions,
|
|
10457
11967
|
log: (entry) => (
|
|
10458
11968
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -10578,6 +12088,18 @@ async function run(options) {
|
|
|
10578
12088
|
if (state.interactive) displayStatus(state);
|
|
10579
12089
|
});
|
|
10580
12090
|
},
|
|
12091
|
+
// Both loops are rearmed because `rearm()` is idempotent for the
|
|
12092
|
+
// provider that did not just connect, and is a no-op when reporting is off.
|
|
12093
|
+
onUsageRearmPing: () => {
|
|
12094
|
+
if (!state.running) return;
|
|
12095
|
+
logActivity(state, {
|
|
12096
|
+
type: "info",
|
|
12097
|
+
level: "debug",
|
|
12098
|
+
message: "Usage rearm ping received"
|
|
12099
|
+
});
|
|
12100
|
+
state.claudeUsageRearm?.();
|
|
12101
|
+
state.openaiUsageRearm?.();
|
|
12102
|
+
},
|
|
10581
12103
|
onInfo: (message) => logActivity(state, { type: "info", message })
|
|
10582
12104
|
}
|
|
10583
12105
|
});
|
|
@@ -10598,7 +12120,17 @@ async function run(options) {
|
|
|
10598
12120
|
setTimer: (timer) => {
|
|
10599
12121
|
state.openaiUsageTimer = timer;
|
|
10600
12122
|
},
|
|
10601
|
-
fetchUsage: () =>
|
|
12123
|
+
fetchUsage: async () => {
|
|
12124
|
+
const usage = await getOpenAiUsage(state.port);
|
|
12125
|
+
if (usage.subscription === null) {
|
|
12126
|
+
logActivity(state, {
|
|
12127
|
+
type: "info",
|
|
12128
|
+
level: "debug",
|
|
12129
|
+
message: "OpenAI usage subscription could not be identified from the local credential"
|
|
12130
|
+
});
|
|
12131
|
+
}
|
|
12132
|
+
return usage;
|
|
12133
|
+
},
|
|
10602
12134
|
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
10603
12135
|
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
10604
12136
|
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
@@ -10672,6 +12204,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10672
12204
|
).option("-v, --verbose", "Alias for --log-level debug (ignored if --log-level is set)").option("-c, --conversation <id>", "Process only this specific conversation").option("--idle-timeout <seconds>", "Exit after N seconds idle").option(
|
|
10673
12205
|
"--opencode-start-timeout <seconds>",
|
|
10674
12206
|
"Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
|
|
12207
|
+
).option(
|
|
12208
|
+
"--opencode-version <v1|v2>",
|
|
12209
|
+
"Which OpenCode major version to launch: v1 or v2 (default: v1). Env: EVIDENT_OPENCODE_VERSION"
|
|
10675
12210
|
).option("--json", "Output in JSON format").option(
|
|
10676
12211
|
"--session-cleanup-max-age <duration>",
|
|
10677
12212
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -10722,6 +12257,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10722
12257
|
).option(
|
|
10723
12258
|
"--opencode-config-overlay <path>",
|
|
10724
12259
|
"Apply this runner-provided OpenCode config before starting OpenCode."
|
|
12260
|
+
).option(
|
|
12261
|
+
"--credential-sync-marker <path>",
|
|
12262
|
+
"Own the interval credential sync and write this marker once the shutdown flush has finished, so the runner image's lifecycle hooks can wait on it."
|
|
10725
12263
|
).action(
|
|
10726
12264
|
(options) => {
|
|
10727
12265
|
run({
|
|
@@ -10737,6 +12275,7 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10737
12275
|
// Raw string — validation/precedence is single-sourced in run.ts's
|
|
10738
12276
|
// resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).
|
|
10739
12277
|
opencodeStartTimeout: options.opencodeStartTimeout,
|
|
12278
|
+
opencodeVersion: options.opencodeVersion,
|
|
10740
12279
|
json: options.json,
|
|
10741
12280
|
// Raw strings — the resolver in run.ts single-sources parsing (M1).
|
|
10742
12281
|
sessionCleanupMaxAge: options.sessionCleanupMaxAge,
|
|
@@ -10760,7 +12299,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10760
12299
|
sessionDbNoReplicateMarker: options.sessionDbNoReplicateMarker,
|
|
10761
12300
|
restoreSessionDb: options.restoreSessionDb,
|
|
10762
12301
|
restoreRunnerCredentials: options.restoreRunnerCredentials,
|
|
10763
|
-
opencodeConfigOverlay: options.opencodeConfigOverlay
|
|
12302
|
+
opencodeConfigOverlay: options.opencodeConfigOverlay,
|
|
12303
|
+
credentialSyncMarker: options.credentialSyncMarker
|
|
10764
12304
|
});
|
|
10765
12305
|
}
|
|
10766
12306
|
);
|