@evident-ai/cli 3.4.1-dev.31006db → 3.4.1-dev.38181ff
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 +20 -0
- package/dist/index.js +2193 -457
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createRequire } from "module";
|
|
4
|
+
import { createRequire as createRequire2 } from "module";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/login.ts
|
|
@@ -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
|
|
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,10 +1179,10 @@ 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
|
-
var CLI_VERSION = (true ? "3.
|
|
1185
|
+
var CLI_VERSION = (true ? "3.4.1-dev.38181ff" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1251
1186
|
function getCliVersion() {
|
|
1252
1187
|
return CLI_VERSION;
|
|
1253
1188
|
}
|
|
@@ -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");
|
|
@@ -1525,7 +1502,14 @@ function drainSessionDbRecoveryReport({
|
|
|
1525
1502
|
skippedLines++;
|
|
1526
1503
|
return [];
|
|
1527
1504
|
}
|
|
1528
|
-
return [
|
|
1505
|
+
return [
|
|
1506
|
+
{
|
|
1507
|
+
...value,
|
|
1508
|
+
provenance_reason: value.provenance_reason ?? null,
|
|
1509
|
+
provenance_migration_delta: value.provenance_migration_delta ?? null,
|
|
1510
|
+
replication_suspended: value.replication_suspended ?? false
|
|
1511
|
+
}
|
|
1512
|
+
];
|
|
1529
1513
|
} catch (error2) {
|
|
1530
1514
|
skippedLines++;
|
|
1531
1515
|
console.error(
|
|
@@ -1628,6 +1612,12 @@ function buildSessionDbRecoveryActivity(record) {
|
|
|
1628
1612
|
metadata: withoutContractFields(record),
|
|
1629
1613
|
message: `This runner did not come online because its damaged session database could not be safely separated from its active backup or proven removed. Backed-up session history remains readable at ${record.quarantine_destination ?? "its original location or the quarantine destination named in the boot logs"}; any local session-database files that remain were left in place and nothing opened or wrote them. See the runner boot logs for SESSION-DB-LOCAL-DISCARD-FAILED details.`
|
|
1630
1614
|
};
|
|
1615
|
+
case "schema_provenance_mismatch":
|
|
1616
|
+
return {
|
|
1617
|
+
level,
|
|
1618
|
+
metadata: withoutContractFields(record),
|
|
1619
|
+
message: `Session database schema provenance mismatch for ${record.dbPath ?? record.db_path ?? "unknown"}: recorded version=${record.recorded_version ?? "unknown"}, current version=${record.current_version ?? "unknown"}, reason=${record.provenance_reason ?? "unknown"}, migration delta=${record.provenance_migration_delta ?? "unknown"}. Inspect the session database and runner backup before continuing.`
|
|
1620
|
+
};
|
|
1631
1621
|
default:
|
|
1632
1622
|
return null;
|
|
1633
1623
|
}
|
|
@@ -1642,7 +1632,8 @@ var OUTCOMES = /* @__PURE__ */ new Set([
|
|
|
1642
1632
|
"fresh_session_db",
|
|
1643
1633
|
"history_rolled_back",
|
|
1644
1634
|
"restore_misconfigured",
|
|
1645
|
-
"session_db_boot_refused"
|
|
1635
|
+
"session_db_boot_refused",
|
|
1636
|
+
"schema_provenance_mismatch"
|
|
1646
1637
|
]);
|
|
1647
1638
|
var STAGES = /* @__PURE__ */ new Set(["restore", "verify"]);
|
|
1648
1639
|
var SEVERITIES = /* @__PURE__ */ new Set(["warning", "error"]);
|
|
@@ -1660,7 +1651,7 @@ var STRING_OR_NULL_FIELDS = ["quarantine_destination", "verified_restore_point"]
|
|
|
1660
1651
|
function isSessionDbRecoveryRecord(value) {
|
|
1661
1652
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1662
1653
|
const record = value;
|
|
1663
|
-
return record.v === 1 && record.event === "session_db_recovery" && typeof record.at === "string" && STAGES.has(record.stage) && OUTCOMES.has(record.outcome) && SEVERITIES.has(record.severity) && typeof record.reason === "string" && (record.replication_suspended === void 0 || typeof record.replication_suspended === "boolean") && NUMBER_FIELDS.every((field) => record[field] === null || Number.isInteger(record[field])) && STRING_OR_NULL_FIELDS.every(
|
|
1654
|
+
return record.v === 1 && record.event === "session_db_recovery" && typeof record.at === "string" && STAGES.has(record.stage) && OUTCOMES.has(record.outcome) && SEVERITIES.has(record.severity) && typeof record.reason === "string" && (record.replication_suspended === void 0 || typeof record.replication_suspended === "boolean") && (record.provenance_reason === void 0 || record.provenance_reason === null || typeof record.provenance_reason === "string") && (record.provenance_migration_delta === void 0 || record.provenance_migration_delta === null || Number.isInteger(record.provenance_migration_delta)) && NUMBER_FIELDS.every((field) => record[field] === null || Number.isInteger(record[field])) && STRING_OR_NULL_FIELDS.every(
|
|
1664
1655
|
(field) => record[field] === null || typeof record[field] === "string"
|
|
1665
1656
|
);
|
|
1666
1657
|
}
|
|
@@ -1695,13 +1686,13 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
1695
1686
|
}
|
|
1696
1687
|
|
|
1697
1688
|
// src/lib/opencode/session-db-boot.ts
|
|
1698
|
-
import { spawn as spawn2 } from "child_process";
|
|
1699
|
-
import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "fs";
|
|
1700
|
-
import { homedir as homedir2 } from "os";
|
|
1701
|
-
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";
|
|
1702
1693
|
|
|
1703
1694
|
// src/lib/runner-synchroniser.ts
|
|
1704
|
-
import { spawn } from "child_process";
|
|
1695
|
+
import { spawn } from "node:child_process";
|
|
1705
1696
|
function appendError(stderr, error2) {
|
|
1706
1697
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1707
1698
|
return stderr === "" ? message : `${stderr}
|
|
@@ -1714,10 +1705,14 @@ function runSynchroniser(args, opts) {
|
|
|
1714
1705
|
let stderr = "";
|
|
1715
1706
|
let settled = false;
|
|
1716
1707
|
const timer = {};
|
|
1708
|
+
let abortListener;
|
|
1709
|
+
let spawnListener;
|
|
1717
1710
|
const finish = (result) => {
|
|
1718
1711
|
if (settled) return;
|
|
1719
1712
|
settled = true;
|
|
1720
1713
|
if (timer.handle) clearTimeout(timer.handle);
|
|
1714
|
+
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
1715
|
+
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
1721
1716
|
resolve4(result);
|
|
1722
1717
|
};
|
|
1723
1718
|
try {
|
|
@@ -1743,6 +1738,25 @@ function runSynchroniser(args, opts) {
|
|
|
1743
1738
|
child.once("close", (code) => {
|
|
1744
1739
|
finish({ code, stdout, stderr, timedOut: false });
|
|
1745
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
|
+
}
|
|
1746
1760
|
timer.handle = setTimeout(
|
|
1747
1761
|
() => {
|
|
1748
1762
|
child.kill("SIGKILL");
|
|
@@ -1780,6 +1794,8 @@ function reportRecord(stage, outcome, reason, litestreamExitCode, options) {
|
|
|
1780
1794
|
quarantined_bytes: null,
|
|
1781
1795
|
verified_restore_point: null,
|
|
1782
1796
|
restore_points_tried: null,
|
|
1797
|
+
provenance_reason: null,
|
|
1798
|
+
provenance_migration_delta: null,
|
|
1783
1799
|
replication_suspended: stage === "restore"
|
|
1784
1800
|
});
|
|
1785
1801
|
}
|
|
@@ -2200,6 +2216,131 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
2200
2216
|
return { verifyFatal: await verifySessionDb(options, configPath, env) };
|
|
2201
2217
|
}
|
|
2202
2218
|
|
|
2219
|
+
// src/lib/opencode/session-db-provenance.ts
|
|
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
|
+
var require2 = createRequire(import.meta.url);
|
|
2224
|
+
function readSessionDbMigrationIds(dbPath) {
|
|
2225
|
+
let db;
|
|
2226
|
+
try {
|
|
2227
|
+
const { DatabaseSync } = require2("node:sqlite");
|
|
2228
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
2229
|
+
const columns = db.prepare("PRAGMA table_info(migration)").all();
|
|
2230
|
+
const hasExpectedShape = columns.length === 2 && columns.some(
|
|
2231
|
+
(column) => column.name === "id" && typeof column.type === "string" && column.type.toUpperCase() === "TEXT" && column.pk === 1
|
|
2232
|
+
) && columns.some(
|
|
2233
|
+
(column) => column.name === "time_completed" && typeof column.type === "string" && column.type.toUpperCase() === "INTEGER" && column.notnull === 1 && column.pk === 0
|
|
2234
|
+
);
|
|
2235
|
+
if (!hasExpectedShape) {
|
|
2236
|
+
console.warn(`[readSessionDbMigrationIds] unsupported migration table shape in ${dbPath}`);
|
|
2237
|
+
return null;
|
|
2238
|
+
}
|
|
2239
|
+
const rows = db.prepare("SELECT id FROM migration ORDER BY id").all();
|
|
2240
|
+
if (rows.some((row) => typeof row.id !== "string")) return null;
|
|
2241
|
+
return rows.map((row) => row.id);
|
|
2242
|
+
} catch (error2) {
|
|
2243
|
+
console.warn(
|
|
2244
|
+
`[readSessionDbMigrationIds] could not read migration history from ${dbPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2245
|
+
);
|
|
2246
|
+
return null;
|
|
2247
|
+
} finally {
|
|
2248
|
+
try {
|
|
2249
|
+
db?.close();
|
|
2250
|
+
} catch (error2) {
|
|
2251
|
+
console.warn(
|
|
2252
|
+
`[readSessionDbMigrationIds] could not close ${dbPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
function sessionDbProvenanceStatePath(homeDir, env) {
|
|
2258
|
+
const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
|
|
2259
|
+
return override || join3(homeDir, ".local", "state", "evident", "session-db-provenance.json");
|
|
2260
|
+
}
|
|
2261
|
+
function loadSessionDbProvenanceState(path) {
|
|
2262
|
+
let value;
|
|
2263
|
+
try {
|
|
2264
|
+
value = JSON.parse(readFileSync3(path, "utf8"));
|
|
2265
|
+
} catch (error2) {
|
|
2266
|
+
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
|
|
2267
|
+
console.error(
|
|
2268
|
+
`[session-db-provenance] could not read ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2269
|
+
);
|
|
2270
|
+
return {};
|
|
2271
|
+
}
|
|
2272
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2273
|
+
console.error(`[session-db-provenance] ignored malformed state in ${path}`);
|
|
2274
|
+
return {};
|
|
2275
|
+
}
|
|
2276
|
+
const state = {};
|
|
2277
|
+
for (const [dbPath, record] of Object.entries(value)) {
|
|
2278
|
+
if (!isSessionDbProvenanceRecord(record)) {
|
|
2279
|
+
console.error(`[session-db-provenance] ignored malformed state in ${path}`);
|
|
2280
|
+
return {};
|
|
2281
|
+
}
|
|
2282
|
+
state[dbPath] = record;
|
|
2283
|
+
}
|
|
2284
|
+
return state;
|
|
2285
|
+
}
|
|
2286
|
+
function saveSessionDbProvenanceState(path, state) {
|
|
2287
|
+
try {
|
|
2288
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
2289
|
+
writeFileSync2(path, `${JSON.stringify(state, null, 2)}
|
|
2290
|
+
`, "utf8");
|
|
2291
|
+
} catch (error2) {
|
|
2292
|
+
console.error(
|
|
2293
|
+
`[session-db-provenance] could not write ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
function evaluateSessionDbProvenance(input) {
|
|
2298
|
+
const { currentVersion, currentIds, previous } = input;
|
|
2299
|
+
if (!previous) return { anomaly: false, reason: null };
|
|
2300
|
+
const current = new Set(currentIds);
|
|
2301
|
+
const prior = new Set(previous.migrationIds);
|
|
2302
|
+
for (const id of prior) {
|
|
2303
|
+
if (!current.has(id)) return { anomaly: true, reason: "migration-history-regressed" };
|
|
2304
|
+
}
|
|
2305
|
+
if (current.size > prior.size && previous.opencodeVersion === currentVersion) {
|
|
2306
|
+
return { anomaly: true, reason: "foreign-version-migrations" };
|
|
2307
|
+
}
|
|
2308
|
+
return { anomaly: false, reason: null };
|
|
2309
|
+
}
|
|
2310
|
+
function checkSessionDbProvenance(input) {
|
|
2311
|
+
const { dbPath, currentVersion, homeDir, env } = input;
|
|
2312
|
+
const path = sessionDbProvenanceStatePath(homeDir, env);
|
|
2313
|
+
const state = loadSessionDbProvenanceState(path);
|
|
2314
|
+
const previous = state[dbPath];
|
|
2315
|
+
const currentIds = readSessionDbMigrationIds(dbPath);
|
|
2316
|
+
if (currentIds === null) {
|
|
2317
|
+
return {
|
|
2318
|
+
anomaly: false,
|
|
2319
|
+
reason: null,
|
|
2320
|
+
recordedVersion: previous?.opencodeVersion ?? null,
|
|
2321
|
+
migrationDelta: null
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
const decision = evaluateSessionDbProvenance({ currentVersion, currentIds, previous });
|
|
2325
|
+
const migrationDelta = previous ? new Set(currentIds).size - new Set(previous.migrationIds).size : null;
|
|
2326
|
+
state[dbPath] = {
|
|
2327
|
+
opencodeVersion: currentVersion,
|
|
2328
|
+
migrationIds: currentIds,
|
|
2329
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2330
|
+
};
|
|
2331
|
+
saveSessionDbProvenanceState(path, state);
|
|
2332
|
+
return {
|
|
2333
|
+
...decision,
|
|
2334
|
+
recordedVersion: previous?.opencodeVersion ?? null,
|
|
2335
|
+
migrationDelta
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
function isSessionDbProvenanceRecord(value) {
|
|
2339
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2340
|
+
const record = value;
|
|
2341
|
+
return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2203
2344
|
// src/lib/opencode/opencode-version-gate.ts
|
|
2204
2345
|
var QUEUE_VALIDATED_OPENCODE_VERSIONS = ["1.17.11", "1.18.3"];
|
|
2205
2346
|
function isQueueValidatedVersion(version2) {
|
|
@@ -2272,6 +2413,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
2272
2413
|
|
|
2273
2414
|
// src/lib/opencode/process.ts
|
|
2274
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
|
+
}
|
|
2275
2427
|
function getProcessCwd(pid) {
|
|
2276
2428
|
const platform = process.platform;
|
|
2277
2429
|
try {
|
|
@@ -2320,14 +2472,14 @@ function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
2320
2472
|
}
|
|
2321
2473
|
return null;
|
|
2322
2474
|
}
|
|
2323
|
-
function
|
|
2475
|
+
function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
2324
2476
|
const instances = [];
|
|
2325
2477
|
try {
|
|
2326
2478
|
const platform = process.platform;
|
|
2327
2479
|
if (platform === "darwin" || platform === "linux") {
|
|
2328
2480
|
let pids = [];
|
|
2329
2481
|
try {
|
|
2330
|
-
const pgrepOutput = execSync(
|
|
2482
|
+
const pgrepOutput = execSync(`pgrep -f "${pgrepPattern}"`, {
|
|
2331
2483
|
encoding: "utf-8",
|
|
2332
2484
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2333
2485
|
}).trim();
|
|
@@ -2336,7 +2488,7 @@ function findOpenCodeProcesses() {
|
|
|
2336
2488
|
}
|
|
2337
2489
|
} catch {
|
|
2338
2490
|
try {
|
|
2339
|
-
const psOutput = execSync(
|
|
2491
|
+
const psOutput = execSync(`ps aux | grep -E "${psPattern}" | grep -v grep`, {
|
|
2340
2492
|
encoding: "utf-8",
|
|
2341
2493
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2342
2494
|
}).trim();
|
|
@@ -2382,6 +2534,9 @@ function findOpenCodeProcesses() {
|
|
|
2382
2534
|
}
|
|
2383
2535
|
return instances;
|
|
2384
2536
|
}
|
|
2537
|
+
function findOpenCodeProcesses() {
|
|
2538
|
+
return findProcessesByPattern("opencode serve|opencode-serve", "opencode (serve|--port)");
|
|
2539
|
+
}
|
|
2385
2540
|
async function scanPortsForOpenCode() {
|
|
2386
2541
|
const instances = [];
|
|
2387
2542
|
const checks = OPENCODE_PORT_RANGE.map(async (port) => {
|
|
@@ -2428,7 +2583,7 @@ async function findHealthyOpenCodeInstances() {
|
|
|
2428
2583
|
}
|
|
2429
2584
|
async function startOpenCode(port, options = {}) {
|
|
2430
2585
|
let command = "opencode";
|
|
2431
|
-
const printLogs = options.inheritStdio ? ["--print-logs"] : [];
|
|
2586
|
+
const printLogs = options.inheritStdio ? ["--print-logs", "--log-level", resolveOpenCodeLogLevel(process.env)] : [];
|
|
2432
2587
|
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...printLogs];
|
|
2433
2588
|
try {
|
|
2434
2589
|
execSync("which opencode", { stdio: "ignore" });
|
|
@@ -2485,6 +2640,19 @@ function isOpenCodeInstalled() {
|
|
|
2485
2640
|
return false;
|
|
2486
2641
|
}
|
|
2487
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
|
+
}
|
|
2488
2656
|
async function promptOpenCodeInstall(interactive) {
|
|
2489
2657
|
if (!interactive) {
|
|
2490
2658
|
console.log(
|
|
@@ -2494,7 +2662,11 @@ async function promptOpenCodeInstall(interactive) {
|
|
|
2494
2662
|
install_url: OPENCODE_INSTALL_URL,
|
|
2495
2663
|
install_commands: {
|
|
2496
2664
|
npm: "npm install -g opencode-ai",
|
|
2497
|
-
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
|
+
}
|
|
2498
2670
|
}
|
|
2499
2671
|
})
|
|
2500
2672
|
);
|
|
@@ -2978,21 +3150,112 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
2978
3150
|
}
|
|
2979
3151
|
return lastOk ?? last;
|
|
2980
3152
|
}
|
|
2981
|
-
function
|
|
2982
|
-
if (!messages || messages.length === 0) return
|
|
2983
|
-
const
|
|
2984
|
-
(
|
|
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
|
|
2985
3157
|
);
|
|
2986
|
-
const
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
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
|
+
}
|
|
2994
3188
|
}
|
|
2995
|
-
|
|
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;
|
|
2996
3259
|
let sawAnyUsage = false;
|
|
2997
3260
|
let inputSum = 0;
|
|
2998
3261
|
let outputSum = 0;
|
|
@@ -3003,7 +3266,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
3003
3266
|
let sawCost = false;
|
|
3004
3267
|
let modelId = null;
|
|
3005
3268
|
let providerId = null;
|
|
3006
|
-
for (const m of
|
|
3269
|
+
for (const m of selected) {
|
|
3007
3270
|
const info = m.info;
|
|
3008
3271
|
if (!info) continue;
|
|
3009
3272
|
const tokens = info.tokens;
|
|
@@ -3038,12 +3301,28 @@ function messageUsage(messages, userMessageId) {
|
|
|
3038
3301
|
usage_tokens_reasoning: reasoningSum,
|
|
3039
3302
|
usage_tokens_cache_read: cacheReadSum,
|
|
3040
3303
|
usage_tokens_cache_write: cacheWriteSum,
|
|
3041
|
-
// NULL means
|
|
3042
|
-
//
|
|
3043
|
-
// `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`.
|
|
3044
3306
|
usage_cost_usd: sawCost ? costSum : null
|
|
3045
3307
|
};
|
|
3046
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
|
+
}
|
|
3047
3326
|
function messageRunState(messages, userMessageId) {
|
|
3048
3327
|
if (!messages || messages.length === 0) return "unknown";
|
|
3049
3328
|
const hasUser = messages.some((m) => idOf(m) === userMessageId);
|
|
@@ -3106,8 +3385,7 @@ function isAbortedTerminalReply(messages, userMessageId) {
|
|
|
3106
3385
|
}
|
|
3107
3386
|
return false;
|
|
3108
3387
|
}
|
|
3109
|
-
function
|
|
3110
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
3388
|
+
function classifyReplyAuthError(reply) {
|
|
3111
3389
|
const error2 = errorOf(reply);
|
|
3112
3390
|
if (error2 == null || typeof error2 !== "object") return null;
|
|
3113
3391
|
const e = error2;
|
|
@@ -3132,6 +3410,32 @@ function messageFailure(messages, userMessageId) {
|
|
|
3132
3410
|
}
|
|
3133
3411
|
return null;
|
|
3134
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
|
+
}
|
|
3135
3439
|
function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProviderId, replyModelId = null) {
|
|
3136
3440
|
if (classified != null) return classified;
|
|
3137
3441
|
if (hasConfiguredProvider !== false) return null;
|
|
@@ -3148,6 +3452,28 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
|
3148
3452
|
(m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
3149
3453
|
);
|
|
3150
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
|
+
}
|
|
3151
3477
|
async function hasAnyConfiguredProvider(port) {
|
|
3152
3478
|
try {
|
|
3153
3479
|
const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
|
|
@@ -3179,16 +3505,104 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3179
3505
|
return null;
|
|
3180
3506
|
}
|
|
3181
3507
|
}
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
}
|
|
3190
|
-
function
|
|
3191
|
-
|
|
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
|
+
}
|
|
3596
|
+
|
|
3597
|
+
// src/lib/opencode/session-cleanup.ts
|
|
3598
|
+
var DURATION_UNIT_MS = {
|
|
3599
|
+
s: 1e3,
|
|
3600
|
+
m: 60 * 1e3,
|
|
3601
|
+
h: 60 * 60 * 1e3,
|
|
3602
|
+
d: 24 * 60 * 60 * 1e3
|
|
3603
|
+
};
|
|
3604
|
+
function parseDurationMs(input) {
|
|
3605
|
+
const trimmed = input.trim();
|
|
3192
3606
|
const match = /^(\d+)([smhd])$/.exec(trimmed);
|
|
3193
3607
|
if (!match) {
|
|
3194
3608
|
throw new Error(
|
|
@@ -3285,11 +3699,11 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
3285
3699
|
}
|
|
3286
3700
|
|
|
3287
3701
|
// src/lib/opencode/session-db-size.ts
|
|
3288
|
-
import { statSync as statSync3 } from "fs";
|
|
3289
|
-
import { join as
|
|
3702
|
+
import { statSync as statSync3 } from "node:fs";
|
|
3703
|
+
import { join as join4 } from "node:path";
|
|
3290
3704
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
3291
3705
|
function statSessionDbBytes(homeDir) {
|
|
3292
|
-
const dbPath =
|
|
3706
|
+
const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
3293
3707
|
try {
|
|
3294
3708
|
return statSync3(dbPath).size;
|
|
3295
3709
|
} catch (err) {
|
|
@@ -3316,12 +3730,99 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
3316
3730
|
return null;
|
|
3317
3731
|
}
|
|
3318
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
|
+
|
|
3319
3816
|
// src/lib/opencode/session-db-reclaim.ts
|
|
3320
|
-
import { statSync as
|
|
3321
|
-
import { dirname as
|
|
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
|
+
}
|
|
3322
3823
|
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
3323
3824
|
try {
|
|
3324
|
-
const fsStats = statfsSync(
|
|
3825
|
+
const fsStats = statfsSync(dirname4(dbPath));
|
|
3325
3826
|
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
3326
3827
|
if (availableBytes < requiredBytes) {
|
|
3327
3828
|
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
@@ -3344,17 +3845,17 @@ async function probeReclaimAvailability(input) {
|
|
|
3344
3845
|
const { dbPath, requiredBytes } = input;
|
|
3345
3846
|
let sqlite;
|
|
3346
3847
|
try {
|
|
3347
|
-
sqlite = await import("sqlite");
|
|
3848
|
+
sqlite = await import("node:sqlite");
|
|
3348
3849
|
} catch (err) {
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
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 };
|
|
3353
3853
|
}
|
|
3354
3854
|
let autoVacuum = null;
|
|
3355
3855
|
try {
|
|
3356
3856
|
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
3357
3857
|
try {
|
|
3858
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3358
3859
|
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3359
3860
|
} finally {
|
|
3360
3861
|
db.close();
|
|
@@ -3365,23 +3866,25 @@ async function probeReclaimAvailability(input) {
|
|
|
3365
3866
|
);
|
|
3366
3867
|
}
|
|
3367
3868
|
if (autoVacuum !== 0) return null;
|
|
3368
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
|
|
3869
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
3369
3870
|
}
|
|
3370
3871
|
async function reclaimSessionDbSpace(input) {
|
|
3371
3872
|
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
3372
3873
|
let sqlite;
|
|
3373
3874
|
try {
|
|
3374
|
-
sqlite = await import("sqlite");
|
|
3875
|
+
sqlite = await import("node:sqlite");
|
|
3375
3876
|
} catch (err) {
|
|
3877
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
3376
3878
|
console.warn(
|
|
3377
|
-
`[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}`
|
|
3378
3880
|
);
|
|
3379
|
-
return { ok: false, skipped: "sqlite-unavailable" };
|
|
3881
|
+
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
3380
3882
|
}
|
|
3381
3883
|
const { DatabaseSync } = sqlite;
|
|
3382
3884
|
let db;
|
|
3383
3885
|
try {
|
|
3384
3886
|
db = new DatabaseSync(dbPath);
|
|
3887
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3385
3888
|
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3386
3889
|
if (autoVacuum === 0) {
|
|
3387
3890
|
if (!allowFullVacuum) {
|
|
@@ -3390,7 +3893,7 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3390
3893
|
);
|
|
3391
3894
|
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
3392
3895
|
}
|
|
3393
|
-
const fileBytesForGuard =
|
|
3896
|
+
const fileBytesForGuard = statSync5(dbPath).size;
|
|
3394
3897
|
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
3395
3898
|
if (skipReason !== null) {
|
|
3396
3899
|
console.warn(
|
|
@@ -3418,10 +3921,12 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3418
3921
|
);
|
|
3419
3922
|
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
3420
3923
|
} catch (err) {
|
|
3421
|
-
console.error(
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3924
|
+
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
3925
|
+
return {
|
|
3926
|
+
ok: false,
|
|
3927
|
+
skipped: "reclaim-error",
|
|
3928
|
+
detail: errorMessage(err)
|
|
3929
|
+
};
|
|
3425
3930
|
} finally {
|
|
3426
3931
|
db?.close();
|
|
3427
3932
|
}
|
|
@@ -3462,7 +3967,6 @@ var StreamForwarder = class {
|
|
|
3462
3967
|
handleFrame(frame) {
|
|
3463
3968
|
switch (frame.type) {
|
|
3464
3969
|
case "open":
|
|
3465
|
-
this.callbacks.onOpen?.(frame.sid, frame.method, frame.path);
|
|
3466
3970
|
void this.handleOpen(frame);
|
|
3467
3971
|
break;
|
|
3468
3972
|
case "req_data":
|
|
@@ -3498,12 +4002,21 @@ var StreamForwarder = class {
|
|
|
3498
4002
|
const { sid, method, path, headers, has_body } = frame;
|
|
3499
4003
|
const correlationId = headers?.[CORRELATION_ID_HEADER];
|
|
3500
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
|
+
}
|
|
3501
4008
|
if (path === TUNNEL_DRAIN_PING_PATH) {
|
|
3502
4009
|
this.callbacks.onDrainPing?.();
|
|
3503
4010
|
this.send({ type: "head", sid, status: 204, headers: {} });
|
|
3504
4011
|
this.send({ type: "res_end", sid });
|
|
3505
4012
|
return;
|
|
3506
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
|
+
}
|
|
3507
4020
|
if (process.env.DEBUG) {
|
|
3508
4021
|
log("debug", "agent_request", {
|
|
3509
4022
|
correlation_id: correlationId,
|
|
@@ -3648,7 +4161,8 @@ function connectTunnel(options) {
|
|
|
3648
4161
|
onResponse,
|
|
3649
4162
|
onInfo,
|
|
3650
4163
|
onWarning,
|
|
3651
|
-
onDrainPing
|
|
4164
|
+
onDrainPing,
|
|
4165
|
+
onUsageRearmPing
|
|
3652
4166
|
} = options;
|
|
3653
4167
|
const tunnelUrl = getTunnelUrlConfig();
|
|
3654
4168
|
const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
|
|
@@ -3660,7 +4174,8 @@ function connectTunnel(options) {
|
|
|
3660
4174
|
});
|
|
3661
4175
|
const forwarder = new StreamForwarder(ws, port, {
|
|
3662
4176
|
onHead: () => onResponse?.(),
|
|
3663
|
-
onDrainPing: () => onDrainPing?.()
|
|
4177
|
+
onDrainPing: () => onDrainPing?.(),
|
|
4178
|
+
onUsageRearmPing: () => onUsageRearmPing?.()
|
|
3664
4179
|
});
|
|
3665
4180
|
const connectionTimeout = setTimeout(() => {
|
|
3666
4181
|
ws.close();
|
|
@@ -3703,8 +4218,8 @@ function connectTunnel(options) {
|
|
|
3703
4218
|
try {
|
|
3704
4219
|
message = JSON.parse(data.toString());
|
|
3705
4220
|
} catch (error2) {
|
|
3706
|
-
const
|
|
3707
|
-
onError?.(`Failed to handle message: ${
|
|
4221
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
4222
|
+
onError?.(`Failed to handle message: ${errorMessage3}`);
|
|
3708
4223
|
return;
|
|
3709
4224
|
}
|
|
3710
4225
|
if (isStreamFrame(message)) {
|
|
@@ -3821,6 +4336,7 @@ var RunnerConnection = class {
|
|
|
3821
4336
|
onError: (error2) => events.onError?.(error2),
|
|
3822
4337
|
onResponse: () => events.onResponse?.(),
|
|
3823
4338
|
onDrainPing: () => events.onDrainPing?.(),
|
|
4339
|
+
onUsageRearmPing: () => events.onUsageRearmPing?.(),
|
|
3824
4340
|
onInfo: (message) => events.onInfo?.(message),
|
|
3825
4341
|
onWarning: (message) => events.onWarning?.(message)
|
|
3826
4342
|
});
|
|
@@ -3847,10 +4363,10 @@ var RunnerConnection = class {
|
|
|
3847
4363
|
};
|
|
3848
4364
|
|
|
3849
4365
|
// src/lib/tunnel/ready-marker.ts
|
|
3850
|
-
import { writeFileSync as
|
|
4366
|
+
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
3851
4367
|
function writeTunnelReadyMarker(path, agentId) {
|
|
3852
4368
|
try {
|
|
3853
|
-
|
|
4369
|
+
writeFileSync3(path, `${agentId}
|
|
3854
4370
|
`);
|
|
3855
4371
|
return { ok: true };
|
|
3856
4372
|
} catch (error2) {
|
|
@@ -3859,7 +4375,7 @@ function writeTunnelReadyMarker(path, agentId) {
|
|
|
3859
4375
|
}
|
|
3860
4376
|
|
|
3861
4377
|
// src/lib/replication.ts
|
|
3862
|
-
import { spawn as spawn4 } from "child_process";
|
|
4378
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
3863
4379
|
function startSessionDbReplication(configPath) {
|
|
3864
4380
|
return spawn4("litestream", ["replicate", "-config", configPath], {
|
|
3865
4381
|
stdio: "inherit"
|
|
@@ -3875,7 +4391,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
3875
4391
|
}
|
|
3876
4392
|
|
|
3877
4393
|
// src/lib/process-liveness.ts
|
|
3878
|
-
import { readFileSync as
|
|
4394
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
3879
4395
|
function isProcessAlive(pid) {
|
|
3880
4396
|
try {
|
|
3881
4397
|
process.kill(pid, 0);
|
|
@@ -3890,7 +4406,7 @@ function isProcessAlive(pid) {
|
|
|
3890
4406
|
}
|
|
3891
4407
|
if (process.platform !== "linux") return true;
|
|
3892
4408
|
try {
|
|
3893
|
-
const status2 =
|
|
4409
|
+
const status2 = readFileSync4(`/proc/${pid}/status`, "utf8");
|
|
3894
4410
|
return !/^State:\s+Z(?:\s|$)/m.test(status2);
|
|
3895
4411
|
} catch (error2) {
|
|
3896
4412
|
console.error(
|
|
@@ -3901,9 +4417,9 @@ function isProcessAlive(pid) {
|
|
|
3901
4417
|
}
|
|
3902
4418
|
|
|
3903
4419
|
// src/lib/openai-usage.ts
|
|
3904
|
-
import { readFileSync as
|
|
3905
|
-
import { homedir as
|
|
3906
|
-
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";
|
|
3907
4423
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
3908
4424
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
3909
4425
|
var OpenAiUsageError = class extends Error {
|
|
@@ -3917,7 +4433,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
3917
4433
|
}
|
|
3918
4434
|
function readOpenCodeChatGptCredentials() {
|
|
3919
4435
|
try {
|
|
3920
|
-
const raw =
|
|
4436
|
+
const raw = readFileSync5(join6(homedir4(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
3921
4437
|
let parsed;
|
|
3922
4438
|
try {
|
|
3923
4439
|
parsed = JSON.parse(raw);
|
|
@@ -3939,6 +4455,23 @@ function readOpenCodeChatGptCredentials() {
|
|
|
3939
4455
|
return null;
|
|
3940
4456
|
}
|
|
3941
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
|
+
}
|
|
3942
4475
|
function toWindow2(headers, name) {
|
|
3943
4476
|
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
3944
4477
|
const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
|
|
@@ -4014,6 +4547,7 @@ async function getOpenAiUsage(port) {
|
|
|
4014
4547
|
"credentials_expired"
|
|
4015
4548
|
);
|
|
4016
4549
|
}
|
|
4550
|
+
const subscription = parseChatGptIdentity(credentials2.accessToken);
|
|
4017
4551
|
const models = await resolveProbeModels(port);
|
|
4018
4552
|
if (models.length === 0) {
|
|
4019
4553
|
throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
|
|
@@ -4046,7 +4580,7 @@ async function getOpenAiUsage(port) {
|
|
|
4046
4580
|
"no_usable_window"
|
|
4047
4581
|
);
|
|
4048
4582
|
}
|
|
4049
|
-
return usage;
|
|
4583
|
+
return { ...usage, subscription };
|
|
4050
4584
|
}
|
|
4051
4585
|
if (res.status === 401) {
|
|
4052
4586
|
throw new OpenAiUsageError(
|
|
@@ -4118,13 +4652,6 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
|
|
|
4118
4652
|
envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
4119
4653
|
});
|
|
4120
4654
|
}
|
|
4121
|
-
function nextReportDelayMs(random = Math.random) {
|
|
4122
|
-
return usageReportDelayMs(random);
|
|
4123
|
-
}
|
|
4124
|
-
var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
|
|
4125
|
-
function claudeUsageFailureLogLevel(consecutiveFailures) {
|
|
4126
|
-
return usageReportFailureLogLevel(consecutiveFailures);
|
|
4127
|
-
}
|
|
4128
4655
|
|
|
4129
4656
|
// src/lib/openai-usage-reporting.ts
|
|
4130
4657
|
function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
@@ -4161,8 +4688,8 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
4161
4688
|
}
|
|
4162
4689
|
|
|
4163
4690
|
// src/lib/resource-usage.ts
|
|
4164
|
-
import { cpus, totalmem, freemem } from "os";
|
|
4165
|
-
import { statfsSync as statfsSync2 } from "fs";
|
|
4691
|
+
import { cpus, totalmem, freemem } from "node:os";
|
|
4692
|
+
import { statfsSync as statfsSync2 } from "node:fs";
|
|
4166
4693
|
|
|
4167
4694
|
// src/lib/ecs-task-metadata.ts
|
|
4168
4695
|
var ECS_METADATA_TIMEOUT_MS = 2e3;
|
|
@@ -4247,58 +4774,97 @@ function readDisk(homeDir) {
|
|
|
4247
4774
|
};
|
|
4248
4775
|
}
|
|
4249
4776
|
}
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
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(() => {
|
|
4253
4787
|
const current = readCpuSample();
|
|
4254
|
-
const
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
const warnings = [];
|
|
4261
|
-
if (disk.warning) warnings.push(disk.warning);
|
|
4262
|
-
if (ecsWarning) warnings.push(ecsWarning);
|
|
4263
|
-
let cpuPercent = hostCpuPercent;
|
|
4264
|
-
let cpuCount = hostCpuCount;
|
|
4265
|
-
let memoryTotalBytes = totalmem();
|
|
4266
|
-
let memoryAvailableBytes = freemem();
|
|
4267
|
-
if (limits !== null) {
|
|
4268
|
-
cpuCount = limits.cpuCount;
|
|
4269
|
-
memoryTotalBytes = limits.memoryTotalBytes;
|
|
4270
|
-
memoryAvailableBytes = clamp(
|
|
4271
|
-
limits.memoryTotalBytes - (totalmem() - freemem()),
|
|
4272
|
-
0,
|
|
4273
|
-
limits.memoryTotalBytes
|
|
4274
|
-
);
|
|
4275
|
-
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
|
+
}
|
|
4276
4794
|
}
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
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
|
|
4289
4855
|
};
|
|
4290
4856
|
}
|
|
4291
4857
|
|
|
4292
4858
|
// src/lib/channels/driver.ts
|
|
4293
|
-
import { homedir as
|
|
4859
|
+
import { homedir as homedir5 } from "node:os";
|
|
4294
4860
|
|
|
4295
4861
|
// src/lib/runner-file-sync.ts
|
|
4296
|
-
import { join as
|
|
4862
|
+
import { join as join8 } from "node:path";
|
|
4297
4863
|
|
|
4298
4864
|
// src/lib/file-push.ts
|
|
4299
|
-
import { randomUUID } from "crypto";
|
|
4300
|
-
import { chmod, mkdir, open as
|
|
4301
|
-
import { basename, dirname 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";
|
|
4302
4868
|
var FILE_MODE = 384;
|
|
4303
4869
|
var DIRECTORY_MODE = 448;
|
|
4304
4870
|
async function writePushedFile(request) {
|
|
@@ -4329,9 +4895,9 @@ async function writePushedFile(request) {
|
|
|
4329
4895
|
}
|
|
4330
4896
|
try {
|
|
4331
4897
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
4332
|
-
|
|
4898
|
+
dirname5(candidate)
|
|
4333
4899
|
);
|
|
4334
|
-
const realTarget =
|
|
4900
|
+
const realTarget = join7(existingAncestor, ...missingSegments, basename(candidate));
|
|
4335
4901
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
4336
4902
|
if (allowedDirectory === null) {
|
|
4337
4903
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -4341,8 +4907,8 @@ async function writePushedFile(request) {
|
|
|
4341
4907
|
}
|
|
4342
4908
|
if (missingSegments.length > 0) {
|
|
4343
4909
|
await createMissingDirectories(existingAncestor, missingSegments);
|
|
4344
|
-
const realParent = await realpath(
|
|
4345
|
-
if (realParent !==
|
|
4910
|
+
const realParent = await realpath(dirname5(realTarget));
|
|
4911
|
+
if (realParent !== dirname5(realTarget) || !contains(allowedDirectory, realTarget)) {
|
|
4346
4912
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
4347
4913
|
path: realTarget,
|
|
4348
4914
|
bytes,
|
|
@@ -4367,7 +4933,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
4367
4933
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
4368
4934
|
return null;
|
|
4369
4935
|
}
|
|
4370
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4936
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4371
4937
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
4372
4938
|
return null;
|
|
4373
4939
|
}
|
|
@@ -4385,7 +4951,7 @@ async function resolveNearestExistingAncestor(directory) {
|
|
|
4385
4951
|
try {
|
|
4386
4952
|
return { existingAncestor: await realpath(current), missingSegments };
|
|
4387
4953
|
} catch (err) {
|
|
4388
|
-
const parent =
|
|
4954
|
+
const parent = dirname5(current);
|
|
4389
4955
|
if (err.code !== "ENOENT" || parent === current) {
|
|
4390
4956
|
throw err;
|
|
4391
4957
|
}
|
|
@@ -4440,16 +5006,16 @@ function contains(realDirectory, realTarget) {
|
|
|
4440
5006
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
4441
5007
|
let current = existingAncestor;
|
|
4442
5008
|
for (const segment of missingSegments) {
|
|
4443
|
-
current =
|
|
5009
|
+
current = join7(current, segment);
|
|
4444
5010
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
4445
5011
|
await chmod(current, DIRECTORY_MODE);
|
|
4446
5012
|
}
|
|
4447
5013
|
}
|
|
4448
5014
|
async function writeAtomically(realTarget, content) {
|
|
4449
|
-
const temporaryPath =
|
|
5015
|
+
const temporaryPath = join7(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
4450
5016
|
let handle;
|
|
4451
5017
|
try {
|
|
4452
|
-
handle = await
|
|
5018
|
+
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
4453
5019
|
await handle.writeFile(content);
|
|
4454
5020
|
await handle.chmod(FILE_MODE);
|
|
4455
5021
|
await handle.close();
|
|
@@ -4576,12 +5142,12 @@ var NOT_APPLIED = {
|
|
|
4576
5142
|
opencodeAuthApplied: false
|
|
4577
5143
|
};
|
|
4578
5144
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
4579
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4580
|
-
return expanded ===
|
|
5145
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5146
|
+
return expanded === join8(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
4581
5147
|
}
|
|
4582
5148
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
4583
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4584
|
-
return expanded ===
|
|
5149
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5150
|
+
return expanded === join8(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
4585
5151
|
}
|
|
4586
5152
|
async function applyOne(options, file) {
|
|
4587
5153
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -4741,6 +5307,10 @@ var DEFAULT_RETRY_POLICY = {
|
|
|
4741
5307
|
baseDelayMs: 500,
|
|
4742
5308
|
maxDelayMs: 3e4
|
|
4743
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;
|
|
4744
5314
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
4745
5315
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
4746
5316
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
@@ -4881,6 +5451,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4881
5451
|
* message; it is removed once its in-flight set empties.
|
|
4882
5452
|
*/
|
|
4883
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();
|
|
4884
5465
|
/**
|
|
4885
5466
|
* AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
|
|
4886
5467
|
* dispatched and are still in-flight. A message in this set is never
|
|
@@ -5064,6 +5645,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5064
5645
|
* no watcher) can resolve the title.
|
|
5065
5646
|
*/
|
|
5066
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();
|
|
5067
5655
|
/** Serialises drains so a reconnect during a drain doesn't double-process. */
|
|
5068
5656
|
draining = false;
|
|
5069
5657
|
/**
|
|
@@ -5108,6 +5696,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5108
5696
|
* and stops opencode.
|
|
5109
5697
|
*/
|
|
5110
5698
|
stopped = false;
|
|
5699
|
+
recycleRequestedFlag = false;
|
|
5111
5700
|
constructor(config) {
|
|
5112
5701
|
this.agentId = config.agentId;
|
|
5113
5702
|
this.port = config.port;
|
|
@@ -5127,7 +5716,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5127
5716
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
5128
5717
|
this.now = config.now ?? (() => Date.now());
|
|
5129
5718
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
5130
|
-
this.homeDir = config.homeDir ??
|
|
5719
|
+
this.homeDir = config.homeDir ?? homedir5();
|
|
5131
5720
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
5132
5721
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
5133
5722
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
@@ -5213,6 +5802,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5213
5802
|
let dispatched = 0;
|
|
5214
5803
|
try {
|
|
5215
5804
|
const conversations = await this.getPendingConversations();
|
|
5805
|
+
if (this.recycleRequestedFlag) {
|
|
5806
|
+
this.stop();
|
|
5807
|
+
}
|
|
5216
5808
|
if (conversations.length > 0) {
|
|
5217
5809
|
const total = conversations.reduce((sum, c) => sum + (c.pending_message_count ?? 0), 0);
|
|
5218
5810
|
this.log({
|
|
@@ -5274,6 +5866,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5274
5866
|
}
|
|
5275
5867
|
return ids;
|
|
5276
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
|
+
}
|
|
5277
5884
|
/**
|
|
5278
5885
|
* File-pull work, for `run.ts`'s idle accounting (#559).
|
|
5279
5886
|
*
|
|
@@ -5340,6 +5947,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5340
5947
|
*/
|
|
5341
5948
|
stop() {
|
|
5342
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;
|
|
5343
5960
|
}
|
|
5344
5961
|
/**
|
|
5345
5962
|
* Wait (up to `timeoutMs`) for in-flight watcher work to settle during a
|
|
@@ -5406,6 +6023,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5406
6023
|
*/
|
|
5407
6024
|
async processConversation(conv) {
|
|
5408
6025
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
6026
|
+
this.ensureSessionErrorStream();
|
|
5409
6027
|
const messages = await this.getPendingMessages(conv.id);
|
|
5410
6028
|
let dispatched = 0;
|
|
5411
6029
|
let skippedAlreadyDispatched = 0;
|
|
@@ -5478,7 +6096,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5478
6096
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
5479
6097
|
break;
|
|
5480
6098
|
}
|
|
5481
|
-
const
|
|
6099
|
+
const errorMessage3 = err instanceof Error ? err.message : String(err);
|
|
5482
6100
|
this.sessions.delete(conv.id);
|
|
5483
6101
|
this.supersede(conv.id, sessionId);
|
|
5484
6102
|
this.log({
|
|
@@ -5487,7 +6105,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5487
6105
|
conversation_id: conv.id,
|
|
5488
6106
|
message_id: message.id
|
|
5489
6107
|
});
|
|
5490
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6108
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5491
6109
|
this.log({
|
|
5492
6110
|
level: "warn",
|
|
5493
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)}`,
|
|
@@ -5498,7 +6116,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5498
6116
|
});
|
|
5499
6117
|
this.log({
|
|
5500
6118
|
level: "error",
|
|
5501
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
6119
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
|
|
5502
6120
|
conversation_id: conv.id,
|
|
5503
6121
|
message_id: message.id
|
|
5504
6122
|
});
|
|
@@ -5519,14 +6137,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5519
6137
|
this.unconfirmedDispatchFailures.delete(message.id);
|
|
5520
6138
|
this.sessions.delete(conv.id);
|
|
5521
6139
|
this.supersede(conv.id, sessionId);
|
|
5522
|
-
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.`;
|
|
5523
6141
|
this.log({
|
|
5524
6142
|
level: "error",
|
|
5525
|
-
message:
|
|
6143
|
+
message: errorMessage3,
|
|
5526
6144
|
conversation_id: conv.id,
|
|
5527
6145
|
message_id: message.id
|
|
5528
6146
|
});
|
|
5529
|
-
await this.markFailed(conv.id, message.id, null,
|
|
6147
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5530
6148
|
this.log({
|
|
5531
6149
|
level: "warn",
|
|
5532
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)}`,
|
|
@@ -5752,6 +6370,23 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5752
6370
|
if (state === "running" || state === "queued") {
|
|
5753
6371
|
const ongoing = await isSessionOngoing(this.port, sessionId);
|
|
5754
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
|
+
}
|
|
5755
6390
|
return this.reattachRedrive(conv, sessionId, message, ocId);
|
|
5756
6391
|
}
|
|
5757
6392
|
if (ongoing === false) {
|
|
@@ -5841,16 +6476,37 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5841
6476
|
if (state === "done") {
|
|
5842
6477
|
const title = await this.resolveSessionTitle(sessionId, conv.id);
|
|
5843
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
|
+
);
|
|
5844
6485
|
this.log({
|
|
5845
6486
|
level: "info",
|
|
5846
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`,
|
|
5847
6488
|
conversation_id: conv.id,
|
|
5848
6489
|
message_id: message.id
|
|
5849
6490
|
});
|
|
5850
|
-
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
|
+
);
|
|
5851
6501
|
} else {
|
|
5852
6502
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
5853
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
|
+
);
|
|
5854
6510
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
5855
6511
|
this.log({
|
|
5856
6512
|
level: "error",
|
|
@@ -5858,7 +6514,19 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5858
6514
|
conversation_id: conv.id,
|
|
5859
6515
|
message_id: message.id
|
|
5860
6516
|
});
|
|
5861
|
-
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);
|
|
5862
6530
|
}
|
|
5863
6531
|
} catch (err) {
|
|
5864
6532
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -6399,6 +7067,24 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6399
7067
|
ambiguousPinnedSinceMs: 0,
|
|
6400
7068
|
ambiguousResolved: false
|
|
6401
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
|
+
}
|
|
6402
7088
|
}
|
|
6403
7089
|
/**
|
|
6404
7090
|
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
@@ -6603,6 +7289,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6603
7289
|
ensureWatcherRunning(sessionId) {
|
|
6604
7290
|
const watcher = this.watchers.get(sessionId);
|
|
6605
7291
|
if (!watcher) return;
|
|
7292
|
+
this.ensureSessionErrorStream();
|
|
6606
7293
|
if (watcher.loop) return;
|
|
6607
7294
|
if (watcher.inFlight.size === 0) {
|
|
6608
7295
|
this.watchers.delete(sessionId);
|
|
@@ -6618,6 +7305,154 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6618
7305
|
});
|
|
6619
7306
|
watcher.loop = loop;
|
|
6620
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
|
+
}
|
|
6621
7456
|
/**
|
|
6622
7457
|
* The per-session polling loop (WI-3). Once per tick it:
|
|
6623
7458
|
* 1. polls `GET /session/:id/message` once and, per in-flight message,
|
|
@@ -6730,6 +7565,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6730
7565
|
const conv = watcher.conv;
|
|
6731
7566
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
6732
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
|
+
}
|
|
6733
7583
|
if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
|
|
6734
7584
|
else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
|
|
6735
7585
|
if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
|
|
@@ -6783,6 +7633,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6783
7633
|
message_id: inFlight.evidentMessageId
|
|
6784
7634
|
});
|
|
6785
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
|
+
);
|
|
6786
7642
|
const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
|
|
6787
7643
|
try {
|
|
6788
7644
|
await this.markFailed(
|
|
@@ -6791,7 +7647,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6791
7647
|
sessionId,
|
|
6792
7648
|
error2,
|
|
6793
7649
|
usage,
|
|
6794
|
-
failure
|
|
7650
|
+
failure,
|
|
7651
|
+
usageAgentName,
|
|
7652
|
+
subagentInvocations
|
|
6795
7653
|
);
|
|
6796
7654
|
} catch (err) {
|
|
6797
7655
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -6824,13 +7682,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6824
7682
|
return;
|
|
6825
7683
|
}
|
|
6826
7684
|
inFlight.done = true;
|
|
7685
|
+
await this.reportSubagentAuthFailures(
|
|
7686
|
+
watcher.conv.id,
|
|
7687
|
+
inFlight.opencodeMessageId,
|
|
7688
|
+
inFlight.evidentMessageId,
|
|
7689
|
+
messages
|
|
7690
|
+
);
|
|
6827
7691
|
}
|
|
6828
7692
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
6829
7693
|
return;
|
|
6830
7694
|
}
|
|
7695
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
|
|
7696
|
+
const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
|
|
6831
7697
|
const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
|
|
6832
7698
|
const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
|
|
6833
|
-
if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
|
|
7699
|
+
if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
|
|
6834
7700
|
inFlight.stuckReported = true;
|
|
6835
7701
|
void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
|
|
6836
7702
|
stuck_for_ms: this.now() - inFlight.dispatchedAt
|
|
@@ -6991,7 +7857,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6991
7857
|
const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
|
|
6992
7858
|
(sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
|
|
6993
7859
|
);
|
|
6994
|
-
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
7860
|
+
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
|
|
6995
7861
|
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
6996
7862
|
this.log({
|
|
6997
7863
|
level: "debug",
|
|
@@ -7026,6 +7892,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7026
7892
|
});
|
|
7027
7893
|
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
7028
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
|
+
);
|
|
7029
7901
|
try {
|
|
7030
7902
|
await this.markDone(
|
|
7031
7903
|
conv.id,
|
|
@@ -7033,7 +7905,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7033
7905
|
sessionId,
|
|
7034
7906
|
inFlight.opencodeMessageId,
|
|
7035
7907
|
title,
|
|
7036
|
-
usage
|
|
7908
|
+
usage,
|
|
7909
|
+
usageAgentName,
|
|
7910
|
+
subagentInvocations
|
|
7037
7911
|
);
|
|
7038
7912
|
} catch (err) {
|
|
7039
7913
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7066,6 +7940,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7066
7940
|
return;
|
|
7067
7941
|
}
|
|
7068
7942
|
inFlight.done = true;
|
|
7943
|
+
await this.reportSubagentAuthFailures(
|
|
7944
|
+
watcher.conv.id,
|
|
7945
|
+
inFlight.opencodeMessageId,
|
|
7946
|
+
inFlight.evidentMessageId,
|
|
7947
|
+
messages
|
|
7948
|
+
);
|
|
7069
7949
|
}
|
|
7070
7950
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7071
7951
|
}
|
|
@@ -7206,6 +8086,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7206
8086
|
if (state === "failed" && !restartAborted) {
|
|
7207
8087
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
7208
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
|
+
);
|
|
7209
8095
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
7210
8096
|
this.log({
|
|
7211
8097
|
level: "error",
|
|
@@ -7214,7 +8100,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7214
8100
|
message_id: row.id
|
|
7215
8101
|
});
|
|
7216
8102
|
try {
|
|
7217
|
-
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
|
+
);
|
|
7218
8113
|
} catch (err) {
|
|
7219
8114
|
if (err instanceof ChannelAuthError) throw err;
|
|
7220
8115
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7236,6 +8131,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7236
8131
|
});
|
|
7237
8132
|
return;
|
|
7238
8133
|
}
|
|
8134
|
+
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
7239
8135
|
this.dontRedispatch.delete(row.id);
|
|
7240
8136
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
7241
8137
|
return;
|
|
@@ -7375,7 +8271,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7375
8271
|
try {
|
|
7376
8272
|
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
7377
8273
|
const usage = messageUsage(messages, ocId ?? "");
|
|
7378
|
-
|
|
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
|
+
);
|
|
7379
8290
|
} catch (err) {
|
|
7380
8291
|
if (err instanceof ChannelAuthError) throw err;
|
|
7381
8292
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7397,6 +8308,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7397
8308
|
});
|
|
7398
8309
|
return;
|
|
7399
8310
|
}
|
|
8311
|
+
if (ocId !== null) {
|
|
8312
|
+
await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
|
|
8313
|
+
}
|
|
7400
8314
|
this.dontRedispatch.delete(row.id);
|
|
7401
8315
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
7402
8316
|
}
|
|
@@ -7490,14 +8404,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7490
8404
|
this.unconfirmedDispatchFailures.delete(row.id);
|
|
7491
8405
|
this.sessions.delete(readoptConv.id);
|
|
7492
8406
|
this.supersede(readoptConv.id, sessionId);
|
|
7493
|
-
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.`;
|
|
7494
8408
|
this.log({
|
|
7495
8409
|
level: "error",
|
|
7496
|
-
message:
|
|
8410
|
+
message: errorMessage3,
|
|
7497
8411
|
conversation_id: row.conversation_id,
|
|
7498
8412
|
message_id: row.id
|
|
7499
8413
|
});
|
|
7500
|
-
await this.markFailed(row.conversation_id, row.id, null,
|
|
8414
|
+
await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
|
|
7501
8415
|
this.log({
|
|
7502
8416
|
level: "warn",
|
|
7503
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)}`,
|
|
@@ -7776,11 +8690,171 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7776
8690
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
7777
8691
|
return parent;
|
|
7778
8692
|
}
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
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
|
+
}
|
|
8853
|
+
/**
|
|
8854
|
+
* OpenCode's synchronous default session title (e.g.
|
|
8855
|
+
* `"New session - 1737800000000"`), assigned immediately when a session is
|
|
8856
|
+
* created — before OpenCode's async LLM-based auto-titling later renames it
|
|
8857
|
+
* mid-turn (#549). Matched by this literal, case-sensitive prefix only; the
|
|
7784
8858
|
* timestamp suffix's exact format is deliberately NOT matched, since the prefix
|
|
7785
8859
|
* alone is the stable, cheap signal and over-anchoring on the timestamp
|
|
7786
8860
|
* representation risks silently breaking if OpenCode ever changes it. Accepted
|
|
@@ -8112,6 +9186,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8112
9186
|
throw new Error(`Failed to get pending conversations: HTTP ${res.status}`);
|
|
8113
9187
|
}
|
|
8114
9188
|
const data = await res.json();
|
|
9189
|
+
this.recycleRequestedFlag = data.recycle_requested === true;
|
|
8115
9190
|
let conversations = data.conversations;
|
|
8116
9191
|
if (this.conversationFilter) {
|
|
8117
9192
|
conversations = conversations.filter((c) => c.id === this.conversationFilter);
|
|
@@ -8258,7 +9333,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8258
9333
|
* watcher retries next tick within the
|
|
8259
9334
|
* deadline, Finding 4).
|
|
8260
9335
|
*/
|
|
8261
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
|
|
9336
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
|
|
8262
9337
|
const res = await this.fetchImpl(
|
|
8263
9338
|
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
8264
9339
|
{
|
|
@@ -8274,15 +9349,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8274
9349
|
opencode_session_id: sessionId,
|
|
8275
9350
|
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
8276
9351
|
...title ? { title } : {},
|
|
8277
|
-
...usage ? usage : {}
|
|
9352
|
+
...usage ? usage : {},
|
|
9353
|
+
...usageAgentName ? { usage_agent_name: usageAgentName } : {},
|
|
9354
|
+
...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
|
|
8278
9355
|
})
|
|
8279
9356
|
}
|
|
8280
9357
|
);
|
|
8281
9358
|
this.assertAuth(res, "marking message as done");
|
|
8282
|
-
if (res.ok)
|
|
9359
|
+
if (res.ok) {
|
|
9360
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
9361
|
+
return;
|
|
9362
|
+
}
|
|
8283
9363
|
if (isRetryableStatus(res.status)) {
|
|
8284
9364
|
throw new Error(`marking message as done: HTTP ${res.status}`);
|
|
8285
9365
|
}
|
|
9366
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8286
9367
|
throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
|
|
8287
9368
|
}
|
|
8288
9369
|
/**
|
|
@@ -8297,7 +9378,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8297
9378
|
* exists but is wedged, so the next attempt must get a fresh one
|
|
8298
9379
|
* instead of reusing it — see WI-1's server-side null-clearing PATCH).
|
|
8299
9380
|
*/
|
|
8300
|
-
async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
|
|
9381
|
+
async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
|
|
8301
9382
|
const body = { status: "failed" };
|
|
8302
9383
|
if (sessionId === null) {
|
|
8303
9384
|
body.opencode_session_id = null;
|
|
@@ -8306,23 +9387,33 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8306
9387
|
}
|
|
8307
9388
|
if (error2 !== void 0) body.error = error2;
|
|
8308
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
|
+
}
|
|
8309
9394
|
if (failure) {
|
|
8310
9395
|
body.failure_kind = failure.kind;
|
|
8311
9396
|
body.failure_provider_id = failure.providerId;
|
|
8312
9397
|
body.failure_model_id = failure.modelId;
|
|
8313
9398
|
body.failure_reason = failure.reason;
|
|
8314
9399
|
}
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
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);
|
|
8326
9417
|
}
|
|
8327
9418
|
/**
|
|
8328
9419
|
* Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
|
|
@@ -8347,6 +9438,111 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8347
9438
|
reply?.info?.modelID ?? null
|
|
8348
9439
|
);
|
|
8349
9440
|
}
|
|
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
|
+
}
|
|
8350
9546
|
/**
|
|
8351
9547
|
* Best-effort, SINGLE-ATTEMPT runner→server telemetry ping
|
|
8352
9548
|
* (queued-followup-redrive). `POST .../messages/:id/signal {signal, ...extra}`
|
|
@@ -8500,6 +9696,13 @@ import chalk5 from "chalk";
|
|
|
8500
9696
|
import ora2 from "ora";
|
|
8501
9697
|
import { select as select2 } from "@inquirer/prompts";
|
|
8502
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
|
+
}
|
|
8503
9706
|
async function ensureOpenCodeRunning(ctx) {
|
|
8504
9707
|
const healthCheck = await checkOpenCodeHealth(ctx.port);
|
|
8505
9708
|
if (healthCheck.healthy) {
|
|
@@ -8547,6 +9750,7 @@ async function ensureOpenCodeRunning(ctx) {
|
|
|
8547
9750
|
}
|
|
8548
9751
|
}
|
|
8549
9752
|
if (!ctx.interactive) {
|
|
9753
|
+
checkNonInteractivePortConflict(ctx.port, isPortInUse);
|
|
8550
9754
|
ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
|
|
8551
9755
|
const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
|
|
8552
9756
|
const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
|
|
@@ -8628,9 +9832,119 @@ Port ${port} is already in use.`));
|
|
|
8628
9832
|
return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
|
|
8629
9833
|
}
|
|
8630
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
|
+
|
|
8631
9945
|
// src/lib/runner-credentials.ts
|
|
8632
|
-
import { chmodSync as chmodSync2, writeFileSync as
|
|
8633
|
-
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";
|
|
8634
9948
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
8635
9949
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
8636
9950
|
var GITHUB_PROBE_TIMEOUT_MS = 1e4;
|
|
@@ -8870,8 +10184,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
8870
10184
|
}
|
|
8871
10185
|
try {
|
|
8872
10186
|
env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
|
|
8873
|
-
|
|
8874
|
-
|
|
10187
|
+
writeFileSync4(GIT_CONFIG_GLOBAL, "");
|
|
10188
|
+
writeFileSync4(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
|
|
8875
10189
|
chmodSync2(GIT_CREDENTIAL_HELPER, 448);
|
|
8876
10190
|
const config = [
|
|
8877
10191
|
["user.name", env.GIT_USER_NAME ?? "evident-bot"],
|
|
@@ -8902,11 +10216,11 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
8902
10216
|
}
|
|
8903
10217
|
|
|
8904
10218
|
// src/lib/opencode/config-overlay.ts
|
|
8905
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
8906
|
-
import { copyFileSync, existsSync as existsSync2, statSync as
|
|
8907
|
-
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";
|
|
8908
10222
|
function isFile(filePath) {
|
|
8909
|
-
return existsSync2(filePath) &&
|
|
10223
|
+
return existsSync2(filePath) && statSync6(filePath).isFile();
|
|
8910
10224
|
}
|
|
8911
10225
|
function applyRunnerOpenCodeConfig({
|
|
8912
10226
|
overlayPath,
|
|
@@ -8918,7 +10232,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
8918
10232
|
return;
|
|
8919
10233
|
}
|
|
8920
10234
|
const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
|
|
8921
|
-
const target = isFile(
|
|
10235
|
+
const target = isFile(join9(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
8922
10236
|
if (!isFile(source)) {
|
|
8923
10237
|
log3(
|
|
8924
10238
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -8926,7 +10240,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
8926
10240
|
);
|
|
8927
10241
|
return;
|
|
8928
10242
|
}
|
|
8929
|
-
copyFileSync(source,
|
|
10243
|
+
copyFileSync(source, join9(cwd, target));
|
|
8930
10244
|
try {
|
|
8931
10245
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
8932
10246
|
stdio: "ignore"
|
|
@@ -8935,7 +10249,242 @@ function applyRunnerOpenCodeConfig({
|
|
|
8935
10249
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
8936
10250
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
8937
10251
|
}
|
|
8938
|
-
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
|
+
};
|
|
8939
10488
|
}
|
|
8940
10489
|
|
|
8941
10490
|
// src/commands/run.ts
|
|
@@ -8975,7 +10524,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
8975
10524
|
if (trimmed === "") {
|
|
8976
10525
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
8977
10526
|
}
|
|
8978
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
10527
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join10(homeDir, trimmed.slice(2)) : trimmed;
|
|
8979
10528
|
if (!isAbsolute3(expanded)) {
|
|
8980
10529
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
8981
10530
|
}
|
|
@@ -8999,6 +10548,28 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
8999
10548
|
var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
|
|
9000
10549
|
var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
|
|
9001
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
|
+
}
|
|
9002
10573
|
function resolveOpenCodeStartTimeoutMs(options, env = process.env) {
|
|
9003
10574
|
const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1e3;
|
|
9004
10575
|
let raw;
|
|
@@ -9065,7 +10636,7 @@ function log2(state, message, level = "info") {
|
|
|
9065
10636
|
})
|
|
9066
10637
|
);
|
|
9067
10638
|
} else if (!state.interactive) {
|
|
9068
|
-
const prefix = level === "error" ?
|
|
10639
|
+
const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
|
|
9069
10640
|
console.log(`${prefix} ${message}`);
|
|
9070
10641
|
}
|
|
9071
10642
|
}
|
|
@@ -9095,7 +10666,7 @@ function logActivity(state, entry) {
|
|
|
9095
10666
|
}
|
|
9096
10667
|
function reportSessionDbRecovery(state) {
|
|
9097
10668
|
try {
|
|
9098
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
10669
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
|
|
9099
10670
|
for (const record of report.records) {
|
|
9100
10671
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
9101
10672
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -9126,18 +10697,18 @@ function reportSessionDbRecoveryRecord(state, record) {
|
|
|
9126
10697
|
function displayStatus(state) {
|
|
9127
10698
|
if (!state.interactive) return;
|
|
9128
10699
|
const attempt = state.connection?.reconnectAttempt ?? 0;
|
|
9129
|
-
const tunnel = state.connected ?
|
|
9130
|
-
const opencode = state.opencodeConnected ?
|
|
9131
|
-
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`) : "";
|
|
9132
10703
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
9133
|
-
const detail = last ?
|
|
10704
|
+
const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
|
|
9134
10705
|
const agent = state.agentName ?? state.agentId;
|
|
9135
10706
|
console.log(
|
|
9136
|
-
`${
|
|
10707
|
+
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
9137
10708
|
);
|
|
9138
10709
|
}
|
|
9139
10710
|
async function promptForLogin(promptMessage, successMessage) {
|
|
9140
|
-
const action = await
|
|
10711
|
+
const action = await select4({
|
|
9141
10712
|
message: promptMessage,
|
|
9142
10713
|
choices: [
|
|
9143
10714
|
{
|
|
@@ -9153,7 +10724,7 @@ async function promptForLogin(promptMessage, successMessage) {
|
|
|
9153
10724
|
]
|
|
9154
10725
|
});
|
|
9155
10726
|
if (action === "exit") {
|
|
9156
|
-
console.log(
|
|
10727
|
+
console.log(chalk7.dim(`
|
|
9157
10728
|
You can log in later by running: ${getCliName()} login`));
|
|
9158
10729
|
process.exit(0);
|
|
9159
10730
|
}
|
|
@@ -9164,7 +10735,7 @@ You can log in later by running: ${getCliName()} login`));
|
|
|
9164
10735
|
process.exit(1);
|
|
9165
10736
|
}
|
|
9166
10737
|
blank();
|
|
9167
|
-
console.log(
|
|
10738
|
+
console.log(chalk7.green(successMessage));
|
|
9168
10739
|
blank();
|
|
9169
10740
|
return { token: credentials2.token, authType: "bearer", user: credentials2.user };
|
|
9170
10741
|
}
|
|
@@ -9177,12 +10748,12 @@ async function handleAuthError(state, error2) {
|
|
|
9177
10748
|
if (state.interactive) displayStatus(state);
|
|
9178
10749
|
if (!state.interactive) {
|
|
9179
10750
|
blank();
|
|
9180
|
-
console.log(
|
|
9181
|
-
console.log(
|
|
10751
|
+
console.log(chalk7.red("Authentication expired"));
|
|
10752
|
+
console.log(chalk7.dim("Your authentication token is no longer valid."));
|
|
9182
10753
|
blank();
|
|
9183
|
-
console.log(
|
|
9184
|
-
console.log(
|
|
9185
|
-
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"));
|
|
9186
10757
|
blank();
|
|
9187
10758
|
await cleanup(state);
|
|
9188
10759
|
await shutdownTelemetry();
|
|
@@ -9190,7 +10761,7 @@ async function handleAuthError(state, error2) {
|
|
|
9190
10761
|
return { success: false };
|
|
9191
10762
|
}
|
|
9192
10763
|
blank();
|
|
9193
|
-
console.log(
|
|
10764
|
+
console.log(chalk7.yellow("Your authentication has expired."));
|
|
9194
10765
|
blank();
|
|
9195
10766
|
try {
|
|
9196
10767
|
const credentials2 = await promptForLogin(
|
|
@@ -9235,6 +10806,10 @@ async function driveChannels(state, driver) {
|
|
|
9235
10806
|
consecutiveDrainFailures = 0;
|
|
9236
10807
|
unreachableMs = 0;
|
|
9237
10808
|
state.messageCount += processed;
|
|
10809
|
+
if (driver.recycleRequested) {
|
|
10810
|
+
await beginGracefulShutdown(state, "recycle");
|
|
10811
|
+
return;
|
|
10812
|
+
}
|
|
9238
10813
|
const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
|
|
9239
10814
|
lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
|
|
9240
10815
|
const fileActivitySnapshot = driver.fileSyncActivity();
|
|
@@ -9250,6 +10825,14 @@ async function driveChannels(state, driver) {
|
|
|
9250
10825
|
const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
|
|
9251
10826
|
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
9252
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
|
+
}
|
|
9253
10836
|
if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
|
|
9254
10837
|
idlePolls = 0;
|
|
9255
10838
|
idleMs = 0;
|
|
@@ -9277,8 +10860,8 @@ async function driveChannels(state, driver) {
|
|
|
9277
10860
|
state.running = false;
|
|
9278
10861
|
break;
|
|
9279
10862
|
}
|
|
9280
|
-
const
|
|
9281
|
-
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}` });
|
|
9282
10865
|
if (state.interactive) displayStatus(state);
|
|
9283
10866
|
if (driver.hasInFlightWatchers()) {
|
|
9284
10867
|
consecutiveDrainFailures = 0;
|
|
@@ -9316,9 +10899,54 @@ async function driveChannels(state, driver) {
|
|
|
9316
10899
|
}
|
|
9317
10900
|
}
|
|
9318
10901
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
9319
|
-
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
|
+
}
|
|
9320
10912
|
function sessionDbPath() {
|
|
9321
|
-
return
|
|
10913
|
+
return join10(homedir6(), ".local", "share", "opencode", "opencode.db");
|
|
10914
|
+
}
|
|
10915
|
+
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
10916
|
+
const record = {
|
|
10917
|
+
v: 1,
|
|
10918
|
+
event: "session_db_recovery",
|
|
10919
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10920
|
+
stage: "verify",
|
|
10921
|
+
outcome: "schema_provenance_mismatch",
|
|
10922
|
+
severity: "error",
|
|
10923
|
+
reason: provenance.reason ?? "schema-provenance-mismatch",
|
|
10924
|
+
litestream_exit_code: null,
|
|
10925
|
+
attempt: null,
|
|
10926
|
+
replica_objects: null,
|
|
10927
|
+
replica_bytes: null,
|
|
10928
|
+
quarantine_destination: null,
|
|
10929
|
+
quarantined_objects: null,
|
|
10930
|
+
quarantine_failed_objects: null,
|
|
10931
|
+
quarantined_bytes: null,
|
|
10932
|
+
verified_restore_point: null,
|
|
10933
|
+
restore_points_tried: null,
|
|
10934
|
+
provenance_reason: provenance.reason,
|
|
10935
|
+
provenance_migration_delta: provenance.migrationDelta,
|
|
10936
|
+
replication_suspended: false,
|
|
10937
|
+
dbPath: sessionDbPath(),
|
|
10938
|
+
recorded_version: provenance.recordedVersion,
|
|
10939
|
+
current_version: currentVersion,
|
|
10940
|
+
provenance_pre_boot_migration_count: preBootMigrationCount
|
|
10941
|
+
};
|
|
10942
|
+
const activity = buildSessionDbRecoveryActivity(record);
|
|
10943
|
+
if (!activity) throw new Error("could not map session-DB provenance activity");
|
|
10944
|
+
logActivity(state, {
|
|
10945
|
+
type: activity.level === "error" ? "error" : "info",
|
|
10946
|
+
level: activity.level,
|
|
10947
|
+
...activity.level === "error" ? { error: activity.message } : { message: activity.message },
|
|
10948
|
+
metadata: activity.metadata
|
|
10949
|
+
});
|
|
9322
10950
|
}
|
|
9323
10951
|
async function runSweep(state, driver, config) {
|
|
9324
10952
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
@@ -9365,7 +10993,7 @@ async function runSweep(state, driver, config) {
|
|
|
9365
10993
|
const reclaimResult = await reclaimSessionDbSpace({
|
|
9366
10994
|
dbPath: sessionDbPath(),
|
|
9367
10995
|
maxPages: SESSION_DB_RECLAIM_MAX_PAGES,
|
|
9368
|
-
allowFullVacuum: protectedNow.size === 0
|
|
10996
|
+
allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
|
|
9369
10997
|
});
|
|
9370
10998
|
if (reclaimResult.ok) {
|
|
9371
10999
|
const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
|
|
@@ -9378,7 +11006,7 @@ async function runSweep(state, driver, config) {
|
|
|
9378
11006
|
} else {
|
|
9379
11007
|
logActivity(state, {
|
|
9380
11008
|
type: "info",
|
|
9381
|
-
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}` : "")
|
|
9382
11010
|
});
|
|
9383
11011
|
}
|
|
9384
11012
|
} catch (error2) {
|
|
@@ -9401,13 +11029,20 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
9401
11029
|
for (const warning2 of config.warnings) {
|
|
9402
11030
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
9403
11031
|
}
|
|
9404
|
-
const dbBytes = statSessionDbBytes(
|
|
11032
|
+
const dbBytes = statSessionDbBytes(homedir6());
|
|
9405
11033
|
void (async () => {
|
|
9406
|
-
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
|
+
}
|
|
9407
11042
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
9408
11043
|
dbBytes,
|
|
9409
11044
|
cleanupEnabled: config.enabled,
|
|
9410
|
-
reclaimSkipReason
|
|
11045
|
+
reclaimSkipReason: reclaimAvailability?.reason ?? null
|
|
9411
11046
|
});
|
|
9412
11047
|
if (sizeWarning !== null) {
|
|
9413
11048
|
logActivity(state, { type: "info", level: "warn", message: sizeWarning });
|
|
@@ -9574,14 +11209,11 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
9574
11209
|
report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
|
|
9575
11210
|
isLocalCredentialProblem,
|
|
9576
11211
|
forcedOnHint: "run `claude` to sign in",
|
|
9577
|
-
firstDelayMs:
|
|
9578
|
-
nextDelayMs:
|
|
9579
|
-
failureLogLevel:
|
|
11212
|
+
firstDelayMs: firstReportDelayMs,
|
|
11213
|
+
nextDelayMs: usageReportDelayMs,
|
|
11214
|
+
failureLogLevel: usageReportFailureLogLevel
|
|
9580
11215
|
});
|
|
9581
11216
|
}
|
|
9582
|
-
var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
|
|
9583
|
-
var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
9584
|
-
var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
9585
11217
|
function scheduleResourceUsageReporting(state, options) {
|
|
9586
11218
|
const { enabled, warnings } = resolveResourceUsageReportingEnabled(
|
|
9587
11219
|
options.resourceUsageReporting,
|
|
@@ -9602,7 +11234,8 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
9602
11234
|
});
|
|
9603
11235
|
return;
|
|
9604
11236
|
}
|
|
9605
|
-
const collect = createResourceUsageCollector(
|
|
11237
|
+
const { collect, stop } = createResourceUsageCollector(homedir6());
|
|
11238
|
+
state.stopResourceUsageSampling = stop;
|
|
9606
11239
|
let consecutiveFailures = 0;
|
|
9607
11240
|
const tick = async () => {
|
|
9608
11241
|
try {
|
|
@@ -9633,10 +11266,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
9633
11266
|
consecutiveFailures++;
|
|
9634
11267
|
logActivity(state, {
|
|
9635
11268
|
type: "info",
|
|
9636
|
-
level:
|
|
9637
|
-
consecutiveFailures,
|
|
9638
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
9639
|
-
),
|
|
11269
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
9640
11270
|
message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
|
|
9641
11271
|
});
|
|
9642
11272
|
}
|
|
@@ -9645,20 +11275,11 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
9645
11275
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
9646
11276
|
logActivity(state, {
|
|
9647
11277
|
type: "info",
|
|
9648
|
-
level:
|
|
9649
|
-
consecutiveFailures,
|
|
9650
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
9651
|
-
),
|
|
11278
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
9652
11279
|
message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
|
|
9653
11280
|
});
|
|
9654
11281
|
} finally {
|
|
9655
|
-
state.resourceUsageTimer = setTimeout(
|
|
9656
|
-
() => void tick(),
|
|
9657
|
-
jitteredDelayMs(
|
|
9658
|
-
RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
|
|
9659
|
-
RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
|
|
9660
|
-
)
|
|
9661
|
-
);
|
|
11282
|
+
state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
|
|
9662
11283
|
}
|
|
9663
11284
|
};
|
|
9664
11285
|
state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
|
|
@@ -9698,6 +11319,8 @@ async function cleanup(state, opts = {}) {
|
|
|
9698
11319
|
clearTimeout(timer);
|
|
9699
11320
|
}
|
|
9700
11321
|
state.sessionCleanupTimers = [];
|
|
11322
|
+
state.stopOpenCodeLogTail?.();
|
|
11323
|
+
state.stopOpenCodeLogTail = null;
|
|
9701
11324
|
if (state.claudeUsageTimer) {
|
|
9702
11325
|
clearTimeout(state.claudeUsageTimer);
|
|
9703
11326
|
state.claudeUsageTimer = null;
|
|
@@ -9712,21 +11335,41 @@ async function cleanup(state, opts = {}) {
|
|
|
9712
11335
|
clearTimeout(state.resourceUsageTimer);
|
|
9713
11336
|
state.resourceUsageTimer = null;
|
|
9714
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;
|
|
9715
11353
|
if (opts.graceful && state.channelDriver) {
|
|
9716
11354
|
state.channelDriver.stop();
|
|
11355
|
+
}
|
|
11356
|
+
if (flushCredentials) {
|
|
11357
|
+
await flushCredentials("credential_flush", !(opts.graceful && state.channelDriver));
|
|
11358
|
+
}
|
|
11359
|
+
if (opts.graceful && state.channelDriver) {
|
|
9717
11360
|
log2(state, "Draining in-flight channel work before shutdown...");
|
|
9718
11361
|
if (state.interactive) {
|
|
9719
11362
|
logActivity(state, { type: "info", message: "Draining in-flight work before shutdown..." });
|
|
9720
11363
|
displayStatus(state);
|
|
9721
11364
|
}
|
|
9722
11365
|
const driver = state.channelDriver;
|
|
9723
|
-
|
|
11366
|
+
drainSettled = await timeShutdownPhase(
|
|
9724
11367
|
state,
|
|
9725
11368
|
durations,
|
|
9726
11369
|
"drain",
|
|
9727
11370
|
() => driver.waitForInFlight(SHUTDOWN_DRAIN_TIMEOUT_MS)
|
|
9728
11371
|
);
|
|
9729
|
-
if (!
|
|
11372
|
+
if (!drainSettled) {
|
|
9730
11373
|
logActivity(state, {
|
|
9731
11374
|
type: "info",
|
|
9732
11375
|
message: "Shutdown drain timed out with work still in flight \u2014 leaving it for restart recovery"
|
|
@@ -9734,6 +11377,9 @@ async function cleanup(state, opts = {}) {
|
|
|
9734
11377
|
if (state.interactive) displayStatus(state);
|
|
9735
11378
|
}
|
|
9736
11379
|
}
|
|
11380
|
+
if (opts.graceful && state.channelDriver && flushCredentials && drainSettled) {
|
|
11381
|
+
await flushCredentials("credential_flush_final", true);
|
|
11382
|
+
}
|
|
9737
11383
|
await timeShutdownPhase(state, durations, "offline_notify", () => notifyOffline(state));
|
|
9738
11384
|
if (state.connection) {
|
|
9739
11385
|
const connection = state.connection;
|
|
@@ -9769,13 +11415,51 @@ async function cleanup(state, opts = {}) {
|
|
|
9769
11415
|
}
|
|
9770
11416
|
return durations;
|
|
9771
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
|
+
}
|
|
9772
11456
|
async function run(options) {
|
|
9773
11457
|
const interactive = isInteractive(options.json);
|
|
9774
11458
|
let logLevel;
|
|
9775
11459
|
let fileSyncDirectories;
|
|
9776
11460
|
try {
|
|
9777
11461
|
logLevel = resolveLogLevel(options);
|
|
9778
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
11462
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
|
|
9779
11463
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
9780
11464
|
throw new Error(
|
|
9781
11465
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -9804,7 +11488,9 @@ async function run(options) {
|
|
|
9804
11488
|
connected: false,
|
|
9805
11489
|
opencodeConnected: false,
|
|
9806
11490
|
opencodeVersion: null,
|
|
11491
|
+
sessionDbProvenanceAnomaly: false,
|
|
9807
11492
|
opencodeProcess: null,
|
|
11493
|
+
stopOpenCodeLogTail: null,
|
|
9808
11494
|
litestreamProcess: null,
|
|
9809
11495
|
connection: null,
|
|
9810
11496
|
channelDriver: null,
|
|
@@ -9819,9 +11505,24 @@ async function run(options) {
|
|
|
9819
11505
|
openaiUsageTimer: null,
|
|
9820
11506
|
openaiUsageRearm: null,
|
|
9821
11507
|
resourceUsageTimer: null,
|
|
11508
|
+
stopResourceUsageSampling: null,
|
|
11509
|
+
credentialSync: null,
|
|
9822
11510
|
authHeader: ""
|
|
9823
11511
|
};
|
|
9824
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
|
+
}
|
|
9825
11526
|
if (fileSyncDirectories.length > 0) {
|
|
9826
11527
|
log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
|
|
9827
11528
|
} else {
|
|
@@ -9847,43 +11548,7 @@ async function run(options) {
|
|
|
9847
11548
|
"warn"
|
|
9848
11549
|
);
|
|
9849
11550
|
}
|
|
9850
|
-
const handleSignal =
|
|
9851
|
-
if (state.shuttingDown) return;
|
|
9852
|
-
state.shuttingDown = true;
|
|
9853
|
-
const shutdownStartedAt = Date.now();
|
|
9854
|
-
if (state.interactive) {
|
|
9855
|
-
logActivity(state, { type: "info", message: "Shutting down..." });
|
|
9856
|
-
displayStatus(state);
|
|
9857
|
-
} else {
|
|
9858
|
-
log2(state, "Shutting down...");
|
|
9859
|
-
}
|
|
9860
|
-
const durations = await cleanup(state, { graceful: true });
|
|
9861
|
-
const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
|
|
9862
|
-
await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
|
|
9863
|
-
let timer;
|
|
9864
|
-
const flushed = shutdownTelemetry().then(
|
|
9865
|
-
() => true,
|
|
9866
|
-
(error2) => {
|
|
9867
|
-
log2(
|
|
9868
|
-
state,
|
|
9869
|
-
`Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
9870
|
-
"warn"
|
|
9871
|
-
);
|
|
9872
|
-
return true;
|
|
9873
|
-
}
|
|
9874
|
-
);
|
|
9875
|
-
const timedOut = new Promise((resolve4) => {
|
|
9876
|
-
timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
|
|
9877
|
-
});
|
|
9878
|
-
if (!await Promise.race([flushed, timedOut])) {
|
|
9879
|
-
log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
|
|
9880
|
-
}
|
|
9881
|
-
clearTimeout(timer);
|
|
9882
|
-
});
|
|
9883
|
-
const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
|
|
9884
|
-
log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
|
|
9885
|
-
process.exit(0);
|
|
9886
|
-
};
|
|
11551
|
+
const handleSignal = () => beginGracefulShutdown(state, "signal");
|
|
9887
11552
|
process.on("SIGINT", handleSignal);
|
|
9888
11553
|
process.on("SIGTERM", handleSignal);
|
|
9889
11554
|
try {
|
|
@@ -9893,15 +11558,15 @@ async function run(options) {
|
|
|
9893
11558
|
printError("Authentication required");
|
|
9894
11559
|
blank();
|
|
9895
11560
|
console.log(
|
|
9896
|
-
|
|
11561
|
+
chalk7.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
|
|
9897
11562
|
);
|
|
9898
|
-
console.log(
|
|
11563
|
+
console.log(chalk7.dim("Or run `evident login` for interactive authentication"));
|
|
9899
11564
|
blank();
|
|
9900
11565
|
process.exit(1);
|
|
9901
11566
|
return;
|
|
9902
11567
|
}
|
|
9903
11568
|
blank();
|
|
9904
|
-
console.log(
|
|
11569
|
+
console.log(chalk7.yellow("You are not logged in to Evident."));
|
|
9905
11570
|
blank();
|
|
9906
11571
|
credentials2 = await promptForLogin(
|
|
9907
11572
|
"Would you like to log in now?",
|
|
@@ -9951,7 +11616,7 @@ async function run(options) {
|
|
|
9951
11616
|
);
|
|
9952
11617
|
blank();
|
|
9953
11618
|
console.log(
|
|
9954
|
-
|
|
11619
|
+
chalk7.dim(
|
|
9955
11620
|
"Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
|
|
9956
11621
|
)
|
|
9957
11622
|
);
|
|
@@ -9974,15 +11639,15 @@ async function run(options) {
|
|
|
9974
11639
|
);
|
|
9975
11640
|
if (interactive && !state.json) {
|
|
9976
11641
|
blank();
|
|
9977
|
-
console.log(
|
|
9978
|
-
console.log(
|
|
11642
|
+
console.log(chalk7.bold("Evident Run"));
|
|
11643
|
+
console.log(chalk7.dim("-".repeat(40)));
|
|
9979
11644
|
}
|
|
9980
11645
|
const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
|
|
9981
11646
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
9982
11647
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
9983
11648
|
spinner?.fail("Authentication failed");
|
|
9984
11649
|
blank();
|
|
9985
|
-
console.log(
|
|
11650
|
+
console.log(chalk7.yellow("Your authentication token is invalid or expired."));
|
|
9986
11651
|
blank();
|
|
9987
11652
|
credentials2 = await promptForLogin(
|
|
9988
11653
|
"Would you like to log in again?",
|
|
@@ -10029,6 +11694,14 @@ async function run(options) {
|
|
|
10029
11694
|
await restoreCredentialStores(credentialContext);
|
|
10030
11695
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
10031
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;
|
|
10032
11705
|
let sessionDbVerifyFatal = false;
|
|
10033
11706
|
if (!options.restoreSessionDb) {
|
|
10034
11707
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -10078,13 +11751,28 @@ async function run(options) {
|
|
|
10078
11751
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
10079
11752
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
10080
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
|
+
}
|
|
10081
11761
|
const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
|
|
10082
11762
|
for (const warning2 of maxActiveSessionsWarnings) {
|
|
10083
11763
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
10084
11764
|
}
|
|
11765
|
+
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
10085
11766
|
const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
|
|
10086
11767
|
try {
|
|
10087
|
-
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({
|
|
10088
11776
|
port: state.port,
|
|
10089
11777
|
interactive: state.interactive,
|
|
10090
11778
|
agentId: state.agentId,
|
|
@@ -10097,7 +11785,7 @@ async function run(options) {
|
|
|
10097
11785
|
state.opencodeVersion = oc.version;
|
|
10098
11786
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
10099
11787
|
try {
|
|
10100
|
-
|
|
11788
|
+
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
10101
11789
|
`, { mode: 384 });
|
|
10102
11790
|
chmodSync3(options.opencodePidFile, 384);
|
|
10103
11791
|
} catch (error2) {
|
|
@@ -10107,6 +11795,23 @@ async function run(options) {
|
|
|
10107
11795
|
});
|
|
10108
11796
|
}
|
|
10109
11797
|
}
|
|
11798
|
+
if (state.opencodeVersion !== null) {
|
|
11799
|
+
const provenance = checkSessionDbProvenance({
|
|
11800
|
+
dbPath: sessionDbPath(),
|
|
11801
|
+
currentVersion: state.opencodeVersion,
|
|
11802
|
+
homeDir: homedir6(),
|
|
11803
|
+
env: process.env
|
|
11804
|
+
});
|
|
11805
|
+
if (provenance.anomaly) {
|
|
11806
|
+
state.sessionDbProvenanceAnomaly = true;
|
|
11807
|
+
logSessionDbProvenanceMismatch(
|
|
11808
|
+
state,
|
|
11809
|
+
provenance,
|
|
11810
|
+
state.opencodeVersion,
|
|
11811
|
+
preBootMigrationIds?.length ?? null
|
|
11812
|
+
);
|
|
11813
|
+
}
|
|
11814
|
+
}
|
|
10110
11815
|
state.opencodeConnected = oc.notReadyReason === null;
|
|
10111
11816
|
const version2 = state.opencodeVersion ? ` (v${state.opencodeVersion})` : "";
|
|
10112
11817
|
ocSpinner?.succeed(`OpenCode running on port ${state.port}${version2}`);
|
|
@@ -10121,6 +11826,7 @@ async function run(options) {
|
|
|
10121
11826
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
10122
11827
|
}
|
|
10123
11828
|
}
|
|
11829
|
+
await reloadProviderCache(state.port);
|
|
10124
11830
|
const noProviderWarning = buildNoProviderWarning(
|
|
10125
11831
|
await hasAnyConfiguredProvider(state.port)
|
|
10126
11832
|
);
|
|
@@ -10129,10 +11835,10 @@ async function run(options) {
|
|
|
10129
11835
|
if (state.interactive && !state.json) {
|
|
10130
11836
|
logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
|
|
10131
11837
|
blank();
|
|
10132
|
-
console.log(
|
|
11838
|
+
console.log(chalk7.yellow("\u26A0 No OpenCode model provider is configured."));
|
|
10133
11839
|
console.log(
|
|
10134
|
-
|
|
10135
|
-
`Run ${
|
|
11840
|
+
chalk7.dim(
|
|
11841
|
+
`Run ${chalk7.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
|
|
10136
11842
|
)
|
|
10137
11843
|
);
|
|
10138
11844
|
blank();
|
|
@@ -10159,7 +11865,7 @@ async function run(options) {
|
|
|
10159
11865
|
let existingPid;
|
|
10160
11866
|
if (existsSync3(options.litestreamPidFile)) {
|
|
10161
11867
|
try {
|
|
10162
|
-
const rawPid =
|
|
11868
|
+
const rawPid = readFileSync6(options.litestreamPidFile, "utf8").trim();
|
|
10163
11869
|
const parsedPid = Number(rawPid);
|
|
10164
11870
|
if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
|
|
10165
11871
|
existingPid = parsedPid;
|
|
@@ -10196,7 +11902,7 @@ async function run(options) {
|
|
|
10196
11902
|
});
|
|
10197
11903
|
try {
|
|
10198
11904
|
if (litestreamProcess.pid !== void 0) {
|
|
10199
|
-
|
|
11905
|
+
writeFileSync6(options.litestreamPidFile, `${litestreamProcess.pid}
|
|
10200
11906
|
`, {
|
|
10201
11907
|
mode: 384
|
|
10202
11908
|
});
|
|
@@ -10256,7 +11962,7 @@ async function run(options) {
|
|
|
10256
11962
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
10257
11963
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
10258
11964
|
fileSyncDirectories,
|
|
10259
|
-
homeDir:
|
|
11965
|
+
homeDir: homedir6(),
|
|
10260
11966
|
maxActiveSessions,
|
|
10261
11967
|
log: (entry) => (
|
|
10262
11968
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -10382,6 +12088,18 @@ async function run(options) {
|
|
|
10382
12088
|
if (state.interactive) displayStatus(state);
|
|
10383
12089
|
});
|
|
10384
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
|
+
},
|
|
10385
12103
|
onInfo: (message) => logActivity(state, { type: "info", message })
|
|
10386
12104
|
}
|
|
10387
12105
|
});
|
|
@@ -10402,7 +12120,17 @@ async function run(options) {
|
|
|
10402
12120
|
setTimer: (timer) => {
|
|
10403
12121
|
state.openaiUsageTimer = timer;
|
|
10404
12122
|
},
|
|
10405
|
-
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
|
+
},
|
|
10406
12134
|
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
10407
12135
|
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
10408
12136
|
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
@@ -10448,7 +12176,7 @@ async function run(options) {
|
|
|
10448
12176
|
}
|
|
10449
12177
|
|
|
10450
12178
|
// src/index.ts
|
|
10451
|
-
var { version } =
|
|
12179
|
+
var { version } = createRequire2(import.meta.url)("../package.json");
|
|
10452
12180
|
var program = new Command();
|
|
10453
12181
|
program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
|
|
10454
12182
|
"--endpoint <url>",
|
|
@@ -10476,6 +12204,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10476
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(
|
|
10477
12205
|
"--opencode-start-timeout <seconds>",
|
|
10478
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"
|
|
10479
12210
|
).option("--json", "Output in JSON format").option(
|
|
10480
12211
|
"--session-cleanup-max-age <duration>",
|
|
10481
12212
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -10526,6 +12257,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10526
12257
|
).option(
|
|
10527
12258
|
"--opencode-config-overlay <path>",
|
|
10528
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."
|
|
10529
12263
|
).action(
|
|
10530
12264
|
(options) => {
|
|
10531
12265
|
run({
|
|
@@ -10541,6 +12275,7 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10541
12275
|
// Raw string — validation/precedence is single-sourced in run.ts's
|
|
10542
12276
|
// resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).
|
|
10543
12277
|
opencodeStartTimeout: options.opencodeStartTimeout,
|
|
12278
|
+
opencodeVersion: options.opencodeVersion,
|
|
10544
12279
|
json: options.json,
|
|
10545
12280
|
// Raw strings — the resolver in run.ts single-sources parsing (M1).
|
|
10546
12281
|
sessionCleanupMaxAge: options.sessionCleanupMaxAge,
|
|
@@ -10564,7 +12299,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10564
12299
|
sessionDbNoReplicateMarker: options.sessionDbNoReplicateMarker,
|
|
10565
12300
|
restoreSessionDb: options.restoreSessionDb,
|
|
10566
12301
|
restoreRunnerCredentials: options.restoreRunnerCredentials,
|
|
10567
|
-
opencodeConfigOverlay: options.opencodeConfigOverlay
|
|
12302
|
+
opencodeConfigOverlay: options.opencodeConfigOverlay,
|
|
12303
|
+
credentialSyncMarker: options.credentialSyncMarker
|
|
10568
12304
|
});
|
|
10569
12305
|
}
|
|
10570
12306
|
);
|