@evident-ai/cli 3.4.1-dev.0b03f1c → 3.4.1-dev.0c8854e
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 +6 -5
- package/dist/index.js +2115 -474
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -623,6 +623,19 @@ function isInteractive(jsonOutput) {
|
|
|
623
623
|
return true;
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
+
// src/lib/subscription-usage-report.ts
|
|
627
|
+
function toReportedSubscription(collected) {
|
|
628
|
+
if (!collected) return null;
|
|
629
|
+
if (collected.ownerEmail === null && collected.planType === null && collected.organizationName === null) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
owner_email: collected.ownerEmail,
|
|
634
|
+
plan_type: collected.planType,
|
|
635
|
+
organization_name: collected.organizationName
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
626
639
|
// src/commands/agent-lookup.ts
|
|
627
640
|
async function readErrorMessage(response) {
|
|
628
641
|
const text = await response.text().catch(() => "");
|
|
@@ -670,12 +683,15 @@ async function resolveAgentIdFromKey(authHeader) {
|
|
|
670
683
|
}
|
|
671
684
|
}
|
|
672
685
|
var BEST_EFFORT_NOTIFY_TIMEOUT_MS = 2e3;
|
|
673
|
-
async function
|
|
674
|
-
const apiUrl = getApiUrlConfig();
|
|
686
|
+
async function postBestEffort(path, authHeader, body) {
|
|
675
687
|
try {
|
|
676
|
-
const
|
|
688
|
+
const apiUrl = getApiUrlConfig();
|
|
689
|
+
const headers = { Authorization: authHeader };
|
|
690
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
691
|
+
const response = await fetch(`${apiUrl}${path}`, {
|
|
677
692
|
method: "POST",
|
|
678
|
-
headers
|
|
693
|
+
headers,
|
|
694
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
679
695
|
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
680
696
|
});
|
|
681
697
|
if (!response.ok) {
|
|
@@ -687,73 +703,32 @@ async function notifyAgentDisconnected(agentId, authHeader) {
|
|
|
687
703
|
}
|
|
688
704
|
return { ok: true };
|
|
689
705
|
} catch (error2) {
|
|
690
|
-
return { ok: false, error:
|
|
706
|
+
return { ok: false, error: describeTimeoutError(error2, BEST_EFFORT_NOTIFY_TIMEOUT_MS) };
|
|
691
707
|
}
|
|
692
708
|
}
|
|
693
|
-
function
|
|
709
|
+
function describeTimeoutError(error2, timeoutMs) {
|
|
694
710
|
const name = error2?.name;
|
|
695
711
|
if (name === "TimeoutError" || name === "AbortError") {
|
|
696
|
-
return `timed out after ${
|
|
712
|
+
return `timed out after ${timeoutMs}ms`;
|
|
697
713
|
}
|
|
698
714
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
699
715
|
}
|
|
716
|
+
async function notifyAgentDisconnected(agentId, authHeader) {
|
|
717
|
+
return postBestEffort(`/runners/${agentId}/disconnect`, authHeader);
|
|
718
|
+
}
|
|
700
719
|
async function reportMicrovmId(agentId, authHeader, microvmId) {
|
|
701
|
-
|
|
702
|
-
const apiUrl = getApiUrlConfig();
|
|
703
|
-
const response = await fetch(`${apiUrl}/runners/${agentId}/microvm`, {
|
|
704
|
-
method: "POST",
|
|
705
|
-
headers: { Authorization: authHeader, "Content-Type": "application/json" },
|
|
706
|
-
body: JSON.stringify({ microvm_id: microvmId }),
|
|
707
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
708
|
-
});
|
|
709
|
-
if (!response.ok) {
|
|
710
|
-
const serverMessage = await readErrorMessage(response);
|
|
711
|
-
return {
|
|
712
|
-
ok: false,
|
|
713
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
return { ok: true };
|
|
717
|
-
} catch (error2) {
|
|
718
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
719
|
-
}
|
|
720
|
+
return postBestEffort(`/runners/${agentId}/microvm`, authHeader, { microvm_id: microvmId });
|
|
720
721
|
}
|
|
721
722
|
function toReportedWindow(window) {
|
|
722
723
|
if (!window) return null;
|
|
723
724
|
return { utilization: window.utilization, resets_at: window.resetsAt };
|
|
724
725
|
}
|
|
725
|
-
function toReportedOwner(snapshot) {
|
|
726
|
-
if (!snapshot.owner) return null;
|
|
727
|
-
return {
|
|
728
|
-
email: snapshot.owner.email,
|
|
729
|
-
organization_name: snapshot.owner.organizationName,
|
|
730
|
-
rate_limit_tier: snapshot.owner.rateLimitTier
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
726
|
async function reportClaudeUsage(agentId, authHeader, snapshot) {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
body: JSON.stringify({
|
|
740
|
-
five_hour: toReportedWindow(snapshot.fiveHour),
|
|
741
|
-
seven_day: toReportedWindow(snapshot.sevenDay),
|
|
742
|
-
owner: toReportedOwner(snapshot)
|
|
743
|
-
}),
|
|
744
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
745
|
-
});
|
|
746
|
-
if (!response.ok) {
|
|
747
|
-
const serverMessage = await readErrorMessage(response);
|
|
748
|
-
return {
|
|
749
|
-
ok: false,
|
|
750
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
return { ok: true };
|
|
754
|
-
} catch (error2) {
|
|
755
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
756
|
-
}
|
|
727
|
+
return postBestEffort(`/runners/${agentId}/claude-usage`, authHeader, {
|
|
728
|
+
five_hour: toReportedWindow(snapshot.fiveHour),
|
|
729
|
+
seven_day: toReportedWindow(snapshot.sevenDay),
|
|
730
|
+
subscription: toReportedSubscription(snapshot.subscription)
|
|
731
|
+
});
|
|
757
732
|
}
|
|
758
733
|
function toReportedOpenAiWindow(window) {
|
|
759
734
|
if (!window) return null;
|
|
@@ -763,69 +738,26 @@ function toReportedOpenAiWindow(window) {
|
|
|
763
738
|
resets_at: window.resetsAt
|
|
764
739
|
};
|
|
765
740
|
}
|
|
766
|
-
function toReportedOpenAiSubscription(snapshot) {
|
|
767
|
-
if (!snapshot.subscription) return null;
|
|
768
|
-
return {
|
|
769
|
-
owner_email: snapshot.subscription.ownerEmail,
|
|
770
|
-
plan_type: snapshot.subscription.planType
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
741
|
async function reportOpenAiUsage(agentId, authHeader, snapshot) {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
782
|
-
has_credits: snapshot.hasCredits,
|
|
783
|
-
credits_unlimited: snapshot.creditsUnlimited,
|
|
784
|
-
subscription: toReportedOpenAiSubscription(snapshot)
|
|
785
|
-
}),
|
|
786
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
787
|
-
});
|
|
788
|
-
if (!response.ok) {
|
|
789
|
-
const serverMessage = await readErrorMessage(response);
|
|
790
|
-
return {
|
|
791
|
-
ok: false,
|
|
792
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
return { ok: true };
|
|
796
|
-
} catch (error2) {
|
|
797
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
798
|
-
}
|
|
742
|
+
return postBestEffort(`/runners/${agentId}/openai-usage`, authHeader, {
|
|
743
|
+
primary: toReportedOpenAiWindow(snapshot.primary),
|
|
744
|
+
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
745
|
+
has_credits: snapshot.hasCredits,
|
|
746
|
+
credits_unlimited: snapshot.creditsUnlimited,
|
|
747
|
+
subscription: toReportedSubscription(snapshot.subscription)
|
|
748
|
+
});
|
|
799
749
|
}
|
|
800
750
|
async function reportResourceUsage(agentId, authHeader, usage) {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
memory_available_bytes: usage.memoryAvailableBytes,
|
|
812
|
-
disk_total_bytes: usage.diskTotalBytes,
|
|
813
|
-
disk_free_bytes: usage.diskFreeBytes,
|
|
814
|
-
opencode_db_bytes: usage.opencodeDbBytes
|
|
815
|
-
}),
|
|
816
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
817
|
-
});
|
|
818
|
-
if (!response.ok) {
|
|
819
|
-
const serverMessage = await readErrorMessage(response);
|
|
820
|
-
return {
|
|
821
|
-
ok: false,
|
|
822
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
return { ok: true };
|
|
826
|
-
} catch (error2) {
|
|
827
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
828
|
-
}
|
|
751
|
+
return postBestEffort(`/runners/${agentId}/resource-usage`, authHeader, {
|
|
752
|
+
cpu_percent: usage.cpuPercent,
|
|
753
|
+
cpu_peak_percent: usage.cpuPeakPercent,
|
|
754
|
+
cpu_count: usage.cpuCount,
|
|
755
|
+
memory_total_bytes: usage.memoryTotalBytes,
|
|
756
|
+
memory_available_bytes: usage.memoryAvailableBytes,
|
|
757
|
+
disk_total_bytes: usage.diskTotalBytes,
|
|
758
|
+
disk_free_bytes: usage.diskFreeBytes,
|
|
759
|
+
opencode_db_bytes: usage.opencodeDbBytes
|
|
760
|
+
});
|
|
829
761
|
}
|
|
830
762
|
async function getAgentInfo(agentId, authHeader) {
|
|
831
763
|
const apiUrl = getApiUrlConfig();
|
|
@@ -877,13 +809,6 @@ function authLabelFor(credentials2) {
|
|
|
877
809
|
}
|
|
878
810
|
return "user token";
|
|
879
811
|
}
|
|
880
|
-
function describeFetchError(error2) {
|
|
881
|
-
const name = error2?.name;
|
|
882
|
-
if (name === "TimeoutError" || name === "AbortError") {
|
|
883
|
-
return `timed out after ${STATUS_TIMEOUT_MS}ms waiting for a response`;
|
|
884
|
-
}
|
|
885
|
-
return error2 instanceof Error ? error2.message : String(error2);
|
|
886
|
-
}
|
|
887
812
|
async function checkStatus(jsonMode) {
|
|
888
813
|
const apiUrl = getApiUrlConfig();
|
|
889
814
|
const credentials2 = await getAuthCredentials();
|
|
@@ -911,7 +836,7 @@ async function checkStatus(jsonMode) {
|
|
|
911
836
|
endpoint: apiUrl,
|
|
912
837
|
authLabel: authLabelFor(credentials2),
|
|
913
838
|
reason: "unreachable",
|
|
914
|
-
error: `Could not reach ${apiUrl}: ${
|
|
839
|
+
error: `Could not reach ${apiUrl}: ${describeTimeoutError(error2, STATUS_TIMEOUT_MS)}. The credentials were NOT validated.`,
|
|
915
840
|
exitCode: 75
|
|
916
841
|
};
|
|
917
842
|
}
|
|
@@ -1097,7 +1022,7 @@ function ownerLookupFailure(error2) {
|
|
|
1097
1022
|
}
|
|
1098
1023
|
async function getClaudeUsageOwner(accessToken) {
|
|
1099
1024
|
if (cachedOwner?.accessToken === accessToken) {
|
|
1100
|
-
return {
|
|
1025
|
+
return { subscription: cachedOwner.owner, ownerLookupError: null };
|
|
1101
1026
|
}
|
|
1102
1027
|
try {
|
|
1103
1028
|
const response = await fetch(CLAUDE_PROFILE_URL, {
|
|
@@ -1109,27 +1034,27 @@ async function getClaudeUsageOwner(accessToken) {
|
|
|
1109
1034
|
signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
|
|
1110
1035
|
});
|
|
1111
1036
|
if (!response.ok) {
|
|
1112
|
-
return {
|
|
1037
|
+
return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
|
|
1113
1038
|
}
|
|
1114
1039
|
let body;
|
|
1115
1040
|
try {
|
|
1116
1041
|
body = await response.json();
|
|
1117
1042
|
} catch (error2) {
|
|
1118
|
-
return {
|
|
1043
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1119
1044
|
}
|
|
1120
1045
|
const profile = body;
|
|
1121
1046
|
if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
|
|
1122
|
-
return {
|
|
1047
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1123
1048
|
}
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1049
|
+
const subscription = {
|
|
1050
|
+
ownerEmail: profile.account.email,
|
|
1126
1051
|
organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
|
|
1127
|
-
|
|
1052
|
+
planType: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
|
|
1128
1053
|
};
|
|
1129
|
-
cachedOwner = { accessToken, owner };
|
|
1130
|
-
return {
|
|
1054
|
+
cachedOwner = { accessToken, owner: subscription };
|
|
1055
|
+
return { subscription, ownerLookupError: null };
|
|
1131
1056
|
} catch (error2) {
|
|
1132
|
-
return {
|
|
1057
|
+
return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
|
|
1133
1058
|
}
|
|
1134
1059
|
}
|
|
1135
1060
|
async function getClaudeUsage() {
|
|
@@ -1158,11 +1083,11 @@ async function getClaudeUsage() {
|
|
|
1158
1083
|
throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
|
|
1159
1084
|
}
|
|
1160
1085
|
const body = await res.json();
|
|
1161
|
-
const {
|
|
1086
|
+
const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
|
|
1162
1087
|
return {
|
|
1163
1088
|
fiveHour: toWindow(body.five_hour),
|
|
1164
1089
|
sevenDay: toWindow(body.seven_day),
|
|
1165
|
-
|
|
1090
|
+
subscription,
|
|
1166
1091
|
ownerLookupError
|
|
1167
1092
|
};
|
|
1168
1093
|
}
|
|
@@ -1253,11 +1178,11 @@ function stripQuery(url) {
|
|
|
1253
1178
|
}
|
|
1254
1179
|
|
|
1255
1180
|
// src/commands/run.ts
|
|
1256
|
-
import
|
|
1181
|
+
import ora4 from "ora";
|
|
1257
1182
|
import { select as select4 } from "@inquirer/prompts";
|
|
1258
1183
|
|
|
1259
1184
|
// src/lib/telemetry.ts
|
|
1260
|
-
var CLI_VERSION = (true ? "3.
|
|
1185
|
+
var CLI_VERSION = (true ? "3.4.1-dev.0c8854e" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1261
1186
|
function getCliVersion() {
|
|
1262
1187
|
return CLI_VERSION;
|
|
1263
1188
|
}
|
|
@@ -1731,6 +1656,11 @@ function isSessionDbRecoveryRecord(value) {
|
|
|
1731
1656
|
);
|
|
1732
1657
|
}
|
|
1733
1658
|
|
|
1659
|
+
// src/lib/opencode/auth.ts
|
|
1660
|
+
function buildOpenCodeBasicAuthHeader(password) {
|
|
1661
|
+
return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1734
1664
|
// src/lib/opencode/health.ts
|
|
1735
1665
|
async function checkOpenCodeHealth(port) {
|
|
1736
1666
|
try {
|
|
@@ -1748,6 +1678,27 @@ async function checkOpenCodeHealth(port) {
|
|
|
1748
1678
|
return { healthy: false, error: message };
|
|
1749
1679
|
}
|
|
1750
1680
|
}
|
|
1681
|
+
async function checkOpenCode2Health(port, password) {
|
|
1682
|
+
try {
|
|
1683
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
1684
|
+
headers: {
|
|
1685
|
+
Authorization: buildOpenCodeBasicAuthHeader(password)
|
|
1686
|
+
},
|
|
1687
|
+
signal: AbortSignal.timeout(2e3)
|
|
1688
|
+
});
|
|
1689
|
+
if (response.status === 401) {
|
|
1690
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
1691
|
+
}
|
|
1692
|
+
if (!response.ok) {
|
|
1693
|
+
return { healthy: false, error: `HTTP ${response.status}` };
|
|
1694
|
+
}
|
|
1695
|
+
const data = await response.json().catch(() => ({}));
|
|
1696
|
+
return { healthy: true, version: data.version };
|
|
1697
|
+
} catch (error2) {
|
|
1698
|
+
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1699
|
+
return { healthy: false, error: message };
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1751
1702
|
async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
1752
1703
|
const startTime = Date.now();
|
|
1753
1704
|
while (Date.now() - startTime < timeoutMs) {
|
|
@@ -1759,6 +1710,61 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
1759
1710
|
}
|
|
1760
1711
|
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
1761
1712
|
}
|
|
1713
|
+
async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
|
|
1714
|
+
const startTime = Date.now();
|
|
1715
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
1716
|
+
const health = await checkOpenCode2Health(port, password);
|
|
1717
|
+
if (health.healthy || health.authFailed) {
|
|
1718
|
+
return health;
|
|
1719
|
+
}
|
|
1720
|
+
await new Promise((resolve4) => setTimeout(resolve4, 1e3));
|
|
1721
|
+
}
|
|
1722
|
+
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/lib/http-timeout.ts
|
|
1726
|
+
var REQUEST_TIMEOUT_MS = 6e4;
|
|
1727
|
+
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
1728
|
+
return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/lib/opencode/client.ts
|
|
1732
|
+
function redactPassword(message, password) {
|
|
1733
|
+
return message.replaceAll(password, "[redacted]");
|
|
1734
|
+
}
|
|
1735
|
+
function createOpenCodeClient(options) {
|
|
1736
|
+
const password = options.password ?? null;
|
|
1737
|
+
const fetchImpl = withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
|
|
1738
|
+
const baseUrl = `http://127.0.0.1:${options.port}`;
|
|
1739
|
+
return {
|
|
1740
|
+
port: options.port,
|
|
1741
|
+
version: options.version,
|
|
1742
|
+
password,
|
|
1743
|
+
async request(path, init, requestOptions) {
|
|
1744
|
+
const requestInit = options.version === "v2" && password !== null ? (() => {
|
|
1745
|
+
const headers = new Headers(init?.headers);
|
|
1746
|
+
headers.set("Authorization", buildOpenCodeBasicAuthHeader(password));
|
|
1747
|
+
return { ...init, headers };
|
|
1748
|
+
})() : init;
|
|
1749
|
+
try {
|
|
1750
|
+
const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
|
|
1751
|
+
if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
|
|
1752
|
+
const body = await response.text();
|
|
1753
|
+
throw new Error(
|
|
1754
|
+
`OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
return response;
|
|
1758
|
+
} catch (error2) {
|
|
1759
|
+
if (options.version === "v2" && password !== null) {
|
|
1760
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1761
|
+
throw new Error(redactPassword(message, password));
|
|
1762
|
+
}
|
|
1763
|
+
throw error2;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1762
1768
|
|
|
1763
1769
|
// src/lib/opencode/session-db-boot.ts
|
|
1764
1770
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -2422,15 +2428,23 @@ function isQueueValidatedVersion(version2) {
|
|
|
2422
2428
|
if (!version2) return false;
|
|
2423
2429
|
return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version2);
|
|
2424
2430
|
}
|
|
2425
|
-
function buildOpenCodeVersionWarning(version2) {
|
|
2426
|
-
if (
|
|
2427
|
-
const detected = version2 ? `v${version2}` : "unknown";
|
|
2431
|
+
function buildOpenCodeVersionWarning(version2, major) {
|
|
2432
|
+
if (major === "v2") return null;
|
|
2428
2433
|
const validated = QUEUE_VALIDATED_OPENCODE_VERSIONS.map((v) => `v${v}`).join(", ");
|
|
2429
|
-
|
|
2434
|
+
if (!version2) {
|
|
2435
|
+
return `Warning: the running opencode's version could not be determined from its health response, so queue validation could not be checked (validated: ${validated}). Compare against \`opencode --version\`; continuing anyway.`;
|
|
2436
|
+
}
|
|
2437
|
+
if (isQueueValidatedVersion(version2)) return null;
|
|
2438
|
+
return `Warning: opencode v${version2} is not a queue-validated version (validated: ${validated}). Native message queuing \u2014 which channel (Slack) message handling relies on \u2014 is unverified on this version; queued/follow-up messages may behave unexpectedly. Continuing anyway. Bumping the validated set requires re-running the queue validation.`;
|
|
2439
|
+
}
|
|
2440
|
+
function reportedOpenCodeVersion(input) {
|
|
2441
|
+
if (!input.connected) return null;
|
|
2442
|
+
return input.version || `${input.major}-unknown`;
|
|
2430
2443
|
}
|
|
2431
2444
|
|
|
2432
2445
|
// src/lib/opencode/process.ts
|
|
2433
2446
|
import { execSync, spawn as spawn3 } from "child_process";
|
|
2447
|
+
import { randomBytes } from "node:crypto";
|
|
2434
2448
|
|
|
2435
2449
|
// src/lib/process-stop.ts
|
|
2436
2450
|
async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
@@ -2489,6 +2503,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
2489
2503
|
// src/lib/opencode/process.ts
|
|
2490
2504
|
var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
|
|
2491
2505
|
var VALID_OPENCODE_LOG_LEVELS = /* @__PURE__ */ new Set(["DEBUG", "INFO", "WARN", "ERROR"]);
|
|
2506
|
+
var VALID_OPENCODE2_LOG_LEVELS = /* @__PURE__ */ new Set([
|
|
2507
|
+
"all",
|
|
2508
|
+
"trace",
|
|
2509
|
+
"debug",
|
|
2510
|
+
"info",
|
|
2511
|
+
"warn",
|
|
2512
|
+
"warning",
|
|
2513
|
+
"error",
|
|
2514
|
+
"fatal",
|
|
2515
|
+
"none"
|
|
2516
|
+
]);
|
|
2492
2517
|
function resolveOpenCodeLogLevel(env) {
|
|
2493
2518
|
const raw = env.OPENCODE_LOG_LEVEL;
|
|
2494
2519
|
if (!raw) return "INFO";
|
|
@@ -2499,6 +2524,16 @@ function resolveOpenCodeLogLevel(env) {
|
|
|
2499
2524
|
);
|
|
2500
2525
|
return "INFO";
|
|
2501
2526
|
}
|
|
2527
|
+
function resolveOpenCode2LogLevel(env) {
|
|
2528
|
+
const raw = env.OPENCODE_LOG_LEVEL;
|
|
2529
|
+
if (!raw) return "info";
|
|
2530
|
+
const lower = raw.toLowerCase();
|
|
2531
|
+
if (VALID_OPENCODE2_LOG_LEVELS.has(lower)) return lower;
|
|
2532
|
+
console.warn(
|
|
2533
|
+
`startOpenCode2: ignoring invalid OPENCODE_LOG_LEVEL "${raw}" (expected all|trace|debug|info|warn|warning|error|fatal|none) \u2014 using info`
|
|
2534
|
+
);
|
|
2535
|
+
return "info";
|
|
2536
|
+
}
|
|
2502
2537
|
function getProcessCwd(pid) {
|
|
2503
2538
|
const platform = process.platform;
|
|
2504
2539
|
try {
|
|
@@ -2681,6 +2716,37 @@ async function startOpenCode(port, options = {}) {
|
|
|
2681
2716
|
});
|
|
2682
2717
|
return child;
|
|
2683
2718
|
}
|
|
2719
|
+
async function startOpenCode2(port, options = {}) {
|
|
2720
|
+
const password = randomBytes(24).toString("hex");
|
|
2721
|
+
let command = "opencode2";
|
|
2722
|
+
const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
|
|
2723
|
+
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
|
|
2724
|
+
try {
|
|
2725
|
+
execSync("which opencode2", { stdio: "ignore" });
|
|
2726
|
+
} catch {
|
|
2727
|
+
command = "npx";
|
|
2728
|
+
args = [
|
|
2729
|
+
"-y",
|
|
2730
|
+
"-p",
|
|
2731
|
+
"@opencode-ai/cli@beta",
|
|
2732
|
+
"--",
|
|
2733
|
+
"opencode2",
|
|
2734
|
+
"serve",
|
|
2735
|
+
"--port",
|
|
2736
|
+
port.toString(),
|
|
2737
|
+
"--hostname",
|
|
2738
|
+
"127.0.0.1",
|
|
2739
|
+
...logLevel
|
|
2740
|
+
];
|
|
2741
|
+
}
|
|
2742
|
+
const child = spawn3(command, args, {
|
|
2743
|
+
env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
|
|
2744
|
+
detached: true,
|
|
2745
|
+
stdio: options.inheritStdio ? "inherit" : "ignore",
|
|
2746
|
+
cwd: process.cwd()
|
|
2747
|
+
});
|
|
2748
|
+
return { child, password };
|
|
2749
|
+
}
|
|
2684
2750
|
function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
|
|
2685
2751
|
const sendSignal = (signal) => {
|
|
2686
2752
|
if (process.platform === "win32") {
|
|
@@ -2828,27 +2894,500 @@ function buildNoProviderWarning(hasProvider) {
|
|
|
2828
2894
|
return "Warning: opencode has no authenticated model provider configured, so it won't be able to answer prompts. Run `opencode auth login` to set one up (see https://opencode.ai for details).";
|
|
2829
2895
|
}
|
|
2830
2896
|
|
|
2831
|
-
// src/lib/
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2897
|
+
// src/lib/opencode/session-v2.ts
|
|
2898
|
+
function isRecord(value) {
|
|
2899
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2900
|
+
}
|
|
2901
|
+
function finiteNumber(value) {
|
|
2902
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2903
|
+
}
|
|
2904
|
+
function adaptTime(value) {
|
|
2905
|
+
if (!isRecord(value)) return void 0;
|
|
2906
|
+
const created = finiteNumber(value.created);
|
|
2907
|
+
const completed = finiteNumber(value.completed);
|
|
2908
|
+
if (created === void 0 && completed === void 0) return void 0;
|
|
2909
|
+
return {
|
|
2910
|
+
...created !== void 0 ? { created } : {},
|
|
2911
|
+
...completed !== void 0 ? { completed } : {}
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
function adaptTokens(value) {
|
|
2915
|
+
if (!isRecord(value)) return void 0;
|
|
2916
|
+
const input = finiteNumber(value.input);
|
|
2917
|
+
const output = finiteNumber(value.output);
|
|
2918
|
+
const reasoning = finiteNumber(value.reasoning);
|
|
2919
|
+
const cache = isRecord(value.cache) ? {
|
|
2920
|
+
...finiteNumber(value.cache.read) !== void 0 ? { read: finiteNumber(value.cache.read) } : {},
|
|
2921
|
+
...finiteNumber(value.cache.write) !== void 0 ? { write: finiteNumber(value.cache.write) } : {}
|
|
2922
|
+
} : void 0;
|
|
2923
|
+
if (input === void 0 && output === void 0 && reasoning === void 0 && !cache) {
|
|
2924
|
+
return void 0;
|
|
2925
|
+
}
|
|
2926
|
+
return {
|
|
2927
|
+
...input !== void 0 ? { input } : {},
|
|
2928
|
+
...output !== void 0 ? { output } : {},
|
|
2929
|
+
...reasoning !== void 0 ? { reasoning } : {},
|
|
2930
|
+
...cache ? { cache } : {}
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
function adaptMessageInfo(value, role) {
|
|
2934
|
+
const info = {
|
|
2935
|
+
id: value.id,
|
|
2936
|
+
role
|
|
2937
|
+
};
|
|
2938
|
+
const time = adaptTime(value.time);
|
|
2939
|
+
if (time) info.time = time;
|
|
2940
|
+
if (typeof value.finish === "string") info.finish = value.finish;
|
|
2941
|
+
if ("error" in value) info.error = value.error;
|
|
2942
|
+
if (typeof value.agent === "string") info.agent = value.agent;
|
|
2943
|
+
if (isRecord(value.model)) {
|
|
2944
|
+
if (typeof value.model.id === "string") info.modelID = value.model.id;
|
|
2945
|
+
if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
|
|
2946
|
+
}
|
|
2947
|
+
if (typeof value.cost === "number" && Number.isFinite(value.cost)) info.cost = value.cost;
|
|
2948
|
+
const tokens = adaptTokens(value.tokens);
|
|
2949
|
+
if (tokens) info.tokens = tokens;
|
|
2950
|
+
return info;
|
|
2951
|
+
}
|
|
2952
|
+
function adaptV2Message(value) {
|
|
2953
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.type !== "string") {
|
|
2954
|
+
return null;
|
|
2955
|
+
}
|
|
2956
|
+
if (value.type === "user") {
|
|
2957
|
+
if (typeof value.text !== "string") return null;
|
|
2958
|
+
return {
|
|
2959
|
+
info: adaptMessageInfo(value, "user"),
|
|
2960
|
+
parts: [{ type: "text", text: value.text }]
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
|
|
2964
|
+
const parts = [];
|
|
2965
|
+
for (const content of value.content) {
|
|
2966
|
+
if (!isRecord(content) || typeof content.type !== "string") return null;
|
|
2967
|
+
if (content.type === "text") {
|
|
2968
|
+
if (typeof content.text !== "string") return null;
|
|
2969
|
+
parts.push({ type: "text", text: content.text });
|
|
2970
|
+
} else {
|
|
2971
|
+
parts.push({ type: content.type });
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
return {
|
|
2975
|
+
info: adaptMessageInfo(value, "assistant"),
|
|
2976
|
+
parts
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
function adaptFormTool(value) {
|
|
2980
|
+
if (!isRecord(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
|
|
2981
|
+
return void 0;
|
|
2982
|
+
}
|
|
2983
|
+
return { messageID: value.messageID, callID: value.id };
|
|
2984
|
+
}
|
|
2985
|
+
function adaptV2FormWire(value) {
|
|
2986
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
2987
|
+
return null;
|
|
2988
|
+
}
|
|
2989
|
+
return value;
|
|
2990
|
+
}
|
|
2991
|
+
function adaptV2FormField(value, header) {
|
|
2992
|
+
if (!isRecord(value)) return null;
|
|
2993
|
+
const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
|
|
2994
|
+
if (!question) return null;
|
|
2995
|
+
const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
|
|
2996
|
+
if (!isRecord(option)) return [];
|
|
2997
|
+
const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
|
|
2998
|
+
if (!label) return [];
|
|
2999
|
+
return [
|
|
3000
|
+
{
|
|
3001
|
+
label,
|
|
3002
|
+
description: typeof option.description === "string" ? option.description : ""
|
|
3003
|
+
}
|
|
3004
|
+
];
|
|
3005
|
+
}) : [];
|
|
3006
|
+
return { question, header, options };
|
|
3007
|
+
}
|
|
3008
|
+
function adaptV2Form(value) {
|
|
3009
|
+
const form = adaptV2FormWire(value);
|
|
3010
|
+
if (!form || !Array.isArray(form.fields)) return null;
|
|
3011
|
+
const header = typeof form.title === "string" ? form.title : "";
|
|
3012
|
+
const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
|
|
3013
|
+
if (questions.length === 0) return null;
|
|
3014
|
+
const tool = isRecord(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
|
|
3015
|
+
return {
|
|
3016
|
+
id: form.id,
|
|
3017
|
+
sessionID: form.sessionID,
|
|
3018
|
+
questions,
|
|
3019
|
+
...tool ? { tool } : {},
|
|
3020
|
+
raw: form
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
function adaptV2FormList(value) {
|
|
3024
|
+
if (!isRecord(value) || !Array.isArray(value.data)) return null;
|
|
3025
|
+
return value.data.map(adaptV2Form).filter((form) => form !== null);
|
|
3026
|
+
}
|
|
3027
|
+
function adaptPattern(value) {
|
|
3028
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
3029
|
+
if (Array.isArray(value) && value.every((pattern) => typeof pattern === "string")) {
|
|
3030
|
+
return value;
|
|
3031
|
+
}
|
|
3032
|
+
return void 0;
|
|
3033
|
+
}
|
|
3034
|
+
function adaptV2PermissionWire(value) {
|
|
3035
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
3036
|
+
return null;
|
|
3037
|
+
}
|
|
3038
|
+
if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
|
|
3039
|
+
return value;
|
|
3040
|
+
}
|
|
3041
|
+
function adaptV2Permission(value) {
|
|
3042
|
+
const permission = adaptV2PermissionWire(value);
|
|
3043
|
+
if (!permission) return null;
|
|
3044
|
+
const type = permission.permission ?? permission.action;
|
|
3045
|
+
if (!type) return null;
|
|
3046
|
+
const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
|
|
3047
|
+
const time = isRecord(permission.time) ? finiteNumber(permission.time.created) !== void 0 ? { created: finiteNumber(permission.time.created) } : void 0 : void 0;
|
|
3048
|
+
return {
|
|
3049
|
+
id: permission.id,
|
|
3050
|
+
type,
|
|
3051
|
+
sessionID: permission.sessionID,
|
|
3052
|
+
metadata: isRecord(permission.metadata) ? permission.metadata : {},
|
|
3053
|
+
raw: permission,
|
|
3054
|
+
...pattern !== void 0 ? { pattern } : {},
|
|
3055
|
+
...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
|
|
3056
|
+
...typeof permission.callID === "string" ? { callID: permission.callID } : {},
|
|
3057
|
+
...typeof permission.title === "string" ? { title: permission.title } : {},
|
|
3058
|
+
...time ? { time } : {}
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
function adaptV2PermissionList(value) {
|
|
3062
|
+
if (!isRecord(value) || !Array.isArray(value.data)) return null;
|
|
3063
|
+
return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
|
|
3064
|
+
}
|
|
3065
|
+
function adaptV2Session(value) {
|
|
3066
|
+
if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) return null;
|
|
3067
|
+
const time = isRecord(value.time) ? {
|
|
3068
|
+
...finiteNumber(value.time.created) !== void 0 ? { created: finiteNumber(value.time.created) } : {},
|
|
3069
|
+
...finiteNumber(value.time.updated) !== void 0 ? { updated: finiteNumber(value.time.updated) } : {}
|
|
3070
|
+
} : void 0;
|
|
3071
|
+
return {
|
|
3072
|
+
id: value.id,
|
|
3073
|
+
...typeof value.title === "string" ? { title: value.title } : {},
|
|
3074
|
+
...typeof value.parentID === "string" ? { parentID: value.parentID } : {},
|
|
3075
|
+
...time && Object.keys(time).length > 0 ? { time } : {}
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
function adaptV2SessionList(value) {
|
|
3079
|
+
if (!isRecord(value) || !Array.isArray(value.data) || !isRecord(value.cursor)) return null;
|
|
3080
|
+
return {
|
|
3081
|
+
data: value.data.map(adaptV2Session).filter((session) => session !== null),
|
|
3082
|
+
cursor: value.cursor
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
function adaptV2Location(value) {
|
|
3086
|
+
const candidates = [
|
|
3087
|
+
value,
|
|
3088
|
+
isRecord(value) ? value.data : void 0,
|
|
3089
|
+
isRecord(value) ? value.location : void 0
|
|
3090
|
+
];
|
|
3091
|
+
for (const candidate of candidates) {
|
|
3092
|
+
if (!isRecord(candidate) || typeof candidate.directory !== "string") continue;
|
|
3093
|
+
const directory = candidate.directory.trim();
|
|
3094
|
+
if (directory) return directory;
|
|
3095
|
+
}
|
|
3096
|
+
return null;
|
|
3097
|
+
}
|
|
3098
|
+
function adaptV2Messages(value) {
|
|
3099
|
+
if (!isRecord(value) || !Array.isArray(value.data)) return [];
|
|
3100
|
+
return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
|
|
3101
|
+
}
|
|
3102
|
+
async function readJson(response) {
|
|
3103
|
+
try {
|
|
3104
|
+
return await response.json();
|
|
3105
|
+
} catch (error2) {
|
|
3106
|
+
throw new Error(
|
|
3107
|
+
`OpenCode V2 response was not valid JSON: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
async function readData(client, path, init) {
|
|
3112
|
+
const response = await client.request(path, init);
|
|
3113
|
+
const body = await readJson(response);
|
|
3114
|
+
if (!isRecord(body) || !("data" in body)) {
|
|
3115
|
+
throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
|
|
3116
|
+
}
|
|
3117
|
+
return body.data;
|
|
3118
|
+
}
|
|
3119
|
+
var OpenCodeV2PromptAckError = class extends Error {
|
|
3120
|
+
constructor(message) {
|
|
3121
|
+
super(message);
|
|
3122
|
+
this.name = "OpenCodeV2PromptAckError";
|
|
3123
|
+
}
|
|
3124
|
+
};
|
|
3125
|
+
async function getOpenCodeDirectoryV2(client) {
|
|
3126
|
+
try {
|
|
3127
|
+
return adaptV2Location(await readJson(await client.request("/api/location")));
|
|
3128
|
+
} catch (error2) {
|
|
3129
|
+
console.error(
|
|
3130
|
+
`[getOpenCodeDirectoryV2] GET /api/location failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3131
|
+
);
|
|
3132
|
+
return null;
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
async function createV2Session(client, directory) {
|
|
3136
|
+
const data = await readData(client, "/api/session", {
|
|
3137
|
+
method: "POST",
|
|
3138
|
+
headers: { "Content-Type": "application/json" },
|
|
3139
|
+
body: JSON.stringify({ location: { directory } })
|
|
3140
|
+
});
|
|
3141
|
+
if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
3142
|
+
throw new Error("OpenCode V2 create session response was missing data.id");
|
|
3143
|
+
}
|
|
3144
|
+
return data.id;
|
|
3145
|
+
}
|
|
3146
|
+
async function getV2Session(client, sessionId) {
|
|
3147
|
+
const data = await readData(client, `/api/session/${encodeURIComponent(sessionId)}`);
|
|
3148
|
+
const session = adaptV2Session(data);
|
|
3149
|
+
if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
|
|
3150
|
+
return session;
|
|
3151
|
+
}
|
|
3152
|
+
async function listV2SessionPage(client, cursor) {
|
|
3153
|
+
const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
|
|
3154
|
+
try {
|
|
3155
|
+
return adaptV2SessionList(await readJson(await client.request(path)));
|
|
3156
|
+
} catch (error2) {
|
|
3157
|
+
console.error(
|
|
3158
|
+
`[listV2SessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3159
|
+
);
|
|
3160
|
+
return null;
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
async function listV2Sessions(client) {
|
|
3164
|
+
const sessions = [];
|
|
3165
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3166
|
+
let cursor;
|
|
3167
|
+
let hasNextPage = true;
|
|
3168
|
+
try {
|
|
3169
|
+
while (hasNextPage) {
|
|
3170
|
+
const page = await listV2SessionPage(client, cursor);
|
|
3171
|
+
if (!page) return null;
|
|
3172
|
+
sessions.push(...page.data);
|
|
3173
|
+
const next = page.cursor.next;
|
|
3174
|
+
if (next === void 0 || next === null) {
|
|
3175
|
+
hasNextPage = false;
|
|
3176
|
+
continue;
|
|
3177
|
+
}
|
|
3178
|
+
if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
|
|
3179
|
+
throw new Error("OpenCode V2 session list contained an invalid next cursor");
|
|
3180
|
+
}
|
|
3181
|
+
seenCursors.add(next);
|
|
3182
|
+
cursor = next;
|
|
3183
|
+
}
|
|
3184
|
+
return sessions;
|
|
3185
|
+
} catch (error2) {
|
|
3186
|
+
console.error(
|
|
3187
|
+
`[listV2Sessions] session pagination failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3188
|
+
);
|
|
3189
|
+
return null;
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
async function deleteV2Session(client, sessionId) {
|
|
3193
|
+
try {
|
|
3194
|
+
await client.request(`/api/session/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
|
|
3195
|
+
return true;
|
|
3196
|
+
} catch (error2) {
|
|
3197
|
+
console.error(
|
|
3198
|
+
`[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3199
|
+
);
|
|
3200
|
+
return false;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
async function v2SessionExists(client, sessionId) {
|
|
3204
|
+
try {
|
|
3205
|
+
const response = await client.request(
|
|
3206
|
+
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
3207
|
+
void 0,
|
|
3208
|
+
{ allowStatuses: [404] }
|
|
3209
|
+
);
|
|
3210
|
+
return response.status === 404 ? false : true;
|
|
3211
|
+
} catch (error2) {
|
|
3212
|
+
console.error(
|
|
3213
|
+
`[v2SessionExists] GET /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3214
|
+
);
|
|
3215
|
+
return null;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
async function sendV2Prompt(client, sessionId, text) {
|
|
3219
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/prompt`;
|
|
3220
|
+
const response = await client.request(path, {
|
|
3221
|
+
method: "POST",
|
|
3222
|
+
headers: { "Content-Type": "application/json" },
|
|
3223
|
+
body: JSON.stringify({ text, delivery: "queue" })
|
|
3224
|
+
});
|
|
3225
|
+
let body;
|
|
3226
|
+
try {
|
|
3227
|
+
body = await readJson(response);
|
|
3228
|
+
} catch (error2) {
|
|
3229
|
+
throw new OpenCodeV2PromptAckError(
|
|
3230
|
+
`OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
const data = isRecord(body) && "data" in body ? body.data : void 0;
|
|
3234
|
+
if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
3235
|
+
throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
|
|
3236
|
+
}
|
|
3237
|
+
return data.id;
|
|
3238
|
+
}
|
|
3239
|
+
async function getV2SessionMessages(client, sessionId) {
|
|
3240
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
|
|
3241
|
+
try {
|
|
3242
|
+
const body = await readJson(await client.request(path));
|
|
3243
|
+
if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) return null;
|
|
3244
|
+
return adaptV2Messages(body);
|
|
3245
|
+
} catch (error2) {
|
|
3246
|
+
console.error(
|
|
3247
|
+
`[getV2SessionMessages] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3248
|
+
);
|
|
3249
|
+
return null;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
async function listV2Forms(client, sessionId) {
|
|
3253
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/form`;
|
|
3254
|
+
try {
|
|
3255
|
+
return adaptV2FormList(await readJson(await client.request(path)));
|
|
3256
|
+
} catch (error2) {
|
|
3257
|
+
console.error(
|
|
3258
|
+
`[listV2Forms] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3259
|
+
);
|
|
3260
|
+
return null;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
async function listV2Permissions(client, sessionId) {
|
|
3264
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/permission`;
|
|
3265
|
+
try {
|
|
3266
|
+
return adaptV2PermissionList(await readJson(await client.request(path)));
|
|
3267
|
+
} catch (error2) {
|
|
3268
|
+
console.error(
|
|
3269
|
+
`[listV2Permissions] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3270
|
+
);
|
|
3271
|
+
return null;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
async function getV2ActiveSessions(client) {
|
|
3275
|
+
try {
|
|
3276
|
+
const body = await readJson(await client.request("/api/session/active"));
|
|
3277
|
+
if (!isRecord(body) || !isRecord(body.data)) return null;
|
|
3278
|
+
return body.data;
|
|
3279
|
+
} catch (error2) {
|
|
3280
|
+
console.error(
|
|
3281
|
+
`[getV2ActiveSessions] GET /api/session/active failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3282
|
+
);
|
|
3283
|
+
return null;
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
async function isV2SessionOngoing(client, sessionId) {
|
|
3287
|
+
const activeSessions = await getV2ActiveSessions(client);
|
|
3288
|
+
if (activeSessions === null) return null;
|
|
3289
|
+
return Object.prototype.hasOwnProperty.call(activeSessions, sessionId);
|
|
3290
|
+
}
|
|
3291
|
+
function sessionErrorReason(value) {
|
|
3292
|
+
if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
|
|
3293
|
+
if (isRecord(value)) {
|
|
3294
|
+
const data = isRecord(value.data) ? value.data : void 0;
|
|
3295
|
+
const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
|
|
3296
|
+
if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
3297
|
+
}
|
|
3298
|
+
return "OpenCode reported a session error with no details";
|
|
3299
|
+
}
|
|
3300
|
+
function adaptV2SessionErrorEvent(value) {
|
|
3301
|
+
let parsed = value;
|
|
3302
|
+
if (typeof value === "string") {
|
|
3303
|
+
try {
|
|
3304
|
+
parsed = JSON.parse(value);
|
|
3305
|
+
} catch (error2) {
|
|
3306
|
+
void error2;
|
|
3307
|
+
return null;
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
if (!isRecord(parsed)) return null;
|
|
3311
|
+
try {
|
|
3312
|
+
const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
|
|
3313
|
+
if (establishedShape) return establishedShape;
|
|
3314
|
+
} catch (error2) {
|
|
3315
|
+
void error2;
|
|
3316
|
+
}
|
|
3317
|
+
const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord);
|
|
3318
|
+
for (const event of candidates) {
|
|
3319
|
+
if (event.type !== "session.error") continue;
|
|
3320
|
+
const properties = [event.properties, event.data, event].find(isRecord);
|
|
3321
|
+
if (!properties) continue;
|
|
3322
|
+
const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
|
|
3323
|
+
if (!sessionId) continue;
|
|
3324
|
+
return {
|
|
3325
|
+
sessionId,
|
|
3326
|
+
reason: sessionErrorReason(properties.error ?? properties)
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
return null;
|
|
3330
|
+
}
|
|
3331
|
+
async function readV2SessionErrorStream(client, options) {
|
|
3332
|
+
let reader = null;
|
|
3333
|
+
try {
|
|
3334
|
+
const response = await client.request("/api/event", {
|
|
3335
|
+
headers: { accept: "text/event-stream" },
|
|
3336
|
+
signal: options.signal
|
|
3337
|
+
});
|
|
3338
|
+
if (!response.ok || !response.body) {
|
|
3339
|
+
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
3340
|
+
}
|
|
3341
|
+
reader = response.body.getReader();
|
|
3342
|
+
const decoder = new TextDecoder();
|
|
3343
|
+
let buffer = "";
|
|
3344
|
+
const processLine = (line) => {
|
|
3345
|
+
const trimmed = line.trimEnd();
|
|
3346
|
+
if (!trimmed.startsWith("data:")) return;
|
|
3347
|
+
const event = adaptV2SessionErrorEvent(trimmed.slice("data:".length).replace(/^ /, ""));
|
|
3348
|
+
if (event) options.onSessionError(event);
|
|
3349
|
+
};
|
|
3350
|
+
while (true) {
|
|
3351
|
+
const { done, value } = await reader.read();
|
|
3352
|
+
if (done) return { reason: "ended" };
|
|
3353
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3354
|
+
const lines = buffer.split("\n");
|
|
3355
|
+
buffer = lines.pop() ?? "";
|
|
3356
|
+
for (const line of lines) processLine(line);
|
|
3357
|
+
}
|
|
3358
|
+
} catch (error2) {
|
|
3359
|
+
if (options.signal.aborted) return { reason: "aborted" };
|
|
3360
|
+
return {
|
|
3361
|
+
reason: "unavailable",
|
|
3362
|
+
detail: error2 instanceof Error ? error2.message : String(error2)
|
|
3363
|
+
};
|
|
3364
|
+
} finally {
|
|
3365
|
+
if (reader) void reader.cancel().catch(() => void 0);
|
|
3366
|
+
}
|
|
2835
3367
|
}
|
|
2836
3368
|
|
|
2837
3369
|
// src/lib/opencode/session.ts
|
|
3370
|
+
var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
2838
3371
|
function timedFetch(input, init) {
|
|
2839
3372
|
return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
|
|
2840
3373
|
}
|
|
3374
|
+
function requestWithClient(port, client, path, init, options) {
|
|
3375
|
+
return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
|
|
3376
|
+
}
|
|
2841
3377
|
function opencodeBase(port) {
|
|
2842
3378
|
return `http://127.0.0.1:${port}`;
|
|
2843
3379
|
}
|
|
2844
|
-
async function getOpenCodeDirectory(port) {
|
|
3380
|
+
async function getOpenCodeDirectory(port, client) {
|
|
2845
3381
|
try {
|
|
2846
|
-
const res = await
|
|
3382
|
+
const res = await requestWithClient(port, client, "/path");
|
|
2847
3383
|
if (!res.ok) return null;
|
|
2848
3384
|
const body = await res.json();
|
|
2849
3385
|
const dir = typeof body.directory === "string" && body.directory || typeof body.worktree === "string" && body.worktree || typeof body.path?.cwd === "string" && body.path.cwd || typeof body.path?.directory === "string" && body.path.directory || null;
|
|
2850
3386
|
return dir && dir.trim() ? dir.trim() : null;
|
|
2851
|
-
} catch {
|
|
3387
|
+
} catch (error2) {
|
|
3388
|
+
console.error(
|
|
3389
|
+
`[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3390
|
+
);
|
|
2852
3391
|
return null;
|
|
2853
3392
|
}
|
|
2854
3393
|
}
|
|
@@ -2892,16 +3431,48 @@ function isAssistantInFlight(m) {
|
|
|
2892
3431
|
if (completedOf(m) == null) return true;
|
|
2893
3432
|
return finishOf(m) === "tool-calls";
|
|
2894
3433
|
}
|
|
2895
|
-
async function getSessionMessages(port, sessionId) {
|
|
3434
|
+
async function getSessionMessages(port, sessionId, client) {
|
|
2896
3435
|
try {
|
|
2897
|
-
const
|
|
3436
|
+
const path = `/session/${sessionId}/message`;
|
|
3437
|
+
const res = await requestWithClient(port, client, path);
|
|
2898
3438
|
if (!res.ok) return null;
|
|
2899
3439
|
const body = await res.json();
|
|
2900
3440
|
return Array.isArray(body) ? body : null;
|
|
2901
|
-
} catch {
|
|
3441
|
+
} catch (error2) {
|
|
3442
|
+
console.error(
|
|
3443
|
+
`[getSessionMessages] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3444
|
+
);
|
|
2902
3445
|
return null;
|
|
2903
3446
|
}
|
|
2904
3447
|
}
|
|
3448
|
+
async function fetchSessionMessages(port, sessionId, client) {
|
|
3449
|
+
const response = await requestWithClient(port, client, `/session/${sessionId}/message`);
|
|
3450
|
+
if (!response.ok) return null;
|
|
3451
|
+
const body = await response.json();
|
|
3452
|
+
return Array.isArray(body) ? body : null;
|
|
3453
|
+
}
|
|
3454
|
+
async function pollSessionMessagesForRedrive(port, sessionId, client) {
|
|
3455
|
+
try {
|
|
3456
|
+
const response = await requestWithClient(
|
|
3457
|
+
port,
|
|
3458
|
+
client,
|
|
3459
|
+
`/session/${sessionId}/message`,
|
|
3460
|
+
void 0,
|
|
3461
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
3462
|
+
);
|
|
3463
|
+
if (!response.ok) {
|
|
3464
|
+
return { ok: false, status: response.status, body: await response.text(), malformed: false };
|
|
3465
|
+
}
|
|
3466
|
+
const body = await response.json();
|
|
3467
|
+
if (!Array.isArray(body)) return { ok: false, status: null, body: "", malformed: true };
|
|
3468
|
+
return { ok: true, messages: body };
|
|
3469
|
+
} catch (error2) {
|
|
3470
|
+
console.error(
|
|
3471
|
+
`[pollSessionMessagesForRedrive] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3472
|
+
);
|
|
3473
|
+
return { ok: false, status: null, body: "", malformed: false };
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
2905
3476
|
function isSessionActivelyGenerating(messages) {
|
|
2906
3477
|
if (!messages || messages.length === 0) return false;
|
|
2907
3478
|
const last = messages[messages.length - 1];
|
|
@@ -2922,27 +3493,37 @@ function sessionLastActivityMs(session) {
|
|
|
2922
3493
|
}
|
|
2923
3494
|
return null;
|
|
2924
3495
|
}
|
|
2925
|
-
async function listSessions(port) {
|
|
3496
|
+
async function listSessions(port, client) {
|
|
3497
|
+
if (client?.version === "v2") return listV2Sessions(client);
|
|
2926
3498
|
try {
|
|
2927
|
-
const res = await
|
|
3499
|
+
const res = await requestWithClient(port, client, "/session");
|
|
2928
3500
|
if (!res.ok) return null;
|
|
2929
3501
|
const body = await res.json();
|
|
2930
3502
|
return Array.isArray(body) ? body : null;
|
|
2931
|
-
} catch {
|
|
3503
|
+
} catch (error2) {
|
|
3504
|
+
console.error(
|
|
3505
|
+
`[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3506
|
+
);
|
|
2932
3507
|
return null;
|
|
2933
3508
|
}
|
|
2934
3509
|
}
|
|
2935
|
-
async function deleteSession(port, id) {
|
|
3510
|
+
async function deleteSession(port, id, client) {
|
|
3511
|
+
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
2936
3512
|
try {
|
|
2937
|
-
const res = await
|
|
3513
|
+
const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
|
|
2938
3514
|
return res.status >= 200 && res.status < 300;
|
|
2939
|
-
} catch {
|
|
3515
|
+
} catch (error2) {
|
|
3516
|
+
console.error(
|
|
3517
|
+
`[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3518
|
+
);
|
|
2940
3519
|
return false;
|
|
2941
3520
|
}
|
|
2942
3521
|
}
|
|
2943
|
-
async function sessionExists(port, id) {
|
|
3522
|
+
async function sessionExists(port, id, client) {
|
|
2944
3523
|
try {
|
|
2945
|
-
const res = await
|
|
3524
|
+
const res = await requestWithClient(port, client, `/session/${id}`, void 0, {
|
|
3525
|
+
allowStatuses: [404]
|
|
3526
|
+
});
|
|
2946
3527
|
if (res.status >= 200 && res.status < 300) return true;
|
|
2947
3528
|
if (res.status === 404) return false;
|
|
2948
3529
|
return null;
|
|
@@ -2950,9 +3531,22 @@ async function sessionExists(port, id) {
|
|
|
2950
3531
|
return null;
|
|
2951
3532
|
}
|
|
2952
3533
|
}
|
|
2953
|
-
async function
|
|
3534
|
+
async function getOpenCodeSession(port, id, client) {
|
|
3535
|
+
try {
|
|
3536
|
+
const response = await requestWithClient(port, client, `/session/${id}`);
|
|
3537
|
+
const body = await response.json();
|
|
3538
|
+
return body && typeof body === "object" && !Array.isArray(body) ? body : null;
|
|
3539
|
+
} catch (error2) {
|
|
3540
|
+
console.error(
|
|
3541
|
+
`[getOpenCodeSession] GET /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3542
|
+
);
|
|
3543
|
+
return null;
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
async function getSessionStatuses(port, client) {
|
|
3547
|
+
if (client?.version === "v2") return null;
|
|
2954
3548
|
try {
|
|
2955
|
-
const res = await
|
|
3549
|
+
const res = await requestWithClient(port, client, "/session/status");
|
|
2956
3550
|
if (!res.ok) {
|
|
2957
3551
|
console.error(
|
|
2958
3552
|
`[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
|
|
@@ -2974,22 +3568,28 @@ async function getSessionStatuses(port) {
|
|
|
2974
3568
|
return null;
|
|
2975
3569
|
}
|
|
2976
3570
|
}
|
|
2977
|
-
async function isSessionOngoing(port, id) {
|
|
2978
|
-
|
|
3571
|
+
async function isSessionOngoing(port, id, client) {
|
|
3572
|
+
if (client?.version === "v2") return isV2SessionOngoing(client, id);
|
|
3573
|
+
const map = await getSessionStatuses(port, client);
|
|
2979
3574
|
if (map == null) return null;
|
|
2980
3575
|
const entry = map[id];
|
|
2981
3576
|
return entry != null && entry.type !== "idle";
|
|
2982
3577
|
}
|
|
2983
|
-
async function createOpenCodeSession(port, directory) {
|
|
2984
|
-
const
|
|
2985
|
-
if (directory && directory.trim())
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3578
|
+
async function createOpenCodeSession(port, directory, client) {
|
|
3579
|
+
const path = new URL(`${opencodeBase(port)}/session`);
|
|
3580
|
+
if (directory && directory.trim()) path.searchParams.set("directory", directory.trim());
|
|
3581
|
+
const requestPath = `${path.pathname}${path.search}`;
|
|
3582
|
+
const response = await requestWithClient(
|
|
3583
|
+
port,
|
|
3584
|
+
client,
|
|
3585
|
+
requestPath,
|
|
3586
|
+
{
|
|
3587
|
+
method: "POST",
|
|
3588
|
+
headers: { "Content-Type": "application/json" },
|
|
3589
|
+
body: JSON.stringify({})
|
|
3590
|
+
},
|
|
3591
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
3592
|
+
);
|
|
2993
3593
|
if (!response.ok) {
|
|
2994
3594
|
const text = await response.text().catch(() => "");
|
|
2995
3595
|
throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ""}`);
|
|
@@ -2997,10 +3597,16 @@ async function createOpenCodeSession(port, directory) {
|
|
|
2997
3597
|
const data = await response.json();
|
|
2998
3598
|
return data.id;
|
|
2999
3599
|
}
|
|
3000
|
-
async function getModelAttachmentCapability(port, model) {
|
|
3600
|
+
async function getModelAttachmentCapability(port, model, client) {
|
|
3001
3601
|
const { model: baseModel } = splitModelVariant(model);
|
|
3602
|
+
if (client?.version === "v2") {
|
|
3603
|
+
console.error(
|
|
3604
|
+
`[getModelAttachmentCapability] V2 provider capabilities are unavailable; using text-only fallback (port ${port})`
|
|
3605
|
+
);
|
|
3606
|
+
return null;
|
|
3607
|
+
}
|
|
3002
3608
|
try {
|
|
3003
|
-
const res = await
|
|
3609
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
3004
3610
|
if (!res.ok) {
|
|
3005
3611
|
console.error(
|
|
3006
3612
|
`[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
@@ -3120,19 +3726,43 @@ function applyModelOptions(body, options) {
|
|
|
3120
3726
|
}
|
|
3121
3727
|
if (variant) body.variant = variant;
|
|
3122
3728
|
}
|
|
3729
|
+
async function listOpenCodeQuestions(port, client) {
|
|
3730
|
+
try {
|
|
3731
|
+
const response = await requestWithClient(port, client, "/question");
|
|
3732
|
+
const body = await response.json();
|
|
3733
|
+
return Array.isArray(body) ? body : null;
|
|
3734
|
+
} catch (error2) {
|
|
3735
|
+
console.error(
|
|
3736
|
+
`[listOpenCodeQuestions] GET /question failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3737
|
+
);
|
|
3738
|
+
return null;
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
async function listOpenCodePermissions(port, client) {
|
|
3742
|
+
try {
|
|
3743
|
+
const response = await requestWithClient(port, client, "/permission");
|
|
3744
|
+
const body = await response.json();
|
|
3745
|
+
return Array.isArray(body) ? body : null;
|
|
3746
|
+
} catch (error2) {
|
|
3747
|
+
console.error(
|
|
3748
|
+
`[listOpenCodePermissions] GET /permission failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3749
|
+
);
|
|
3750
|
+
return null;
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3123
3753
|
function messageText(m) {
|
|
3124
3754
|
if (!m || !Array.isArray(m.parts)) return "";
|
|
3125
3755
|
return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
3126
3756
|
}
|
|
3127
|
-
async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
3128
|
-
const before = await getSessionMessages(port, sessionId);
|
|
3757
|
+
async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
|
|
3758
|
+
const before = await getSessionMessages(port, sessionId, client);
|
|
3129
3759
|
const knownUserIds = new Set(
|
|
3130
3760
|
(before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
|
|
3131
3761
|
);
|
|
3132
3762
|
const parts = [{ type: "text", text: content }];
|
|
3133
3763
|
let pendingOutcomes = null;
|
|
3134
3764
|
if (attachments && attachments.inputs.length > 0) {
|
|
3135
|
-
const capable = await getModelAttachmentCapability(port, options?.model);
|
|
3765
|
+
const capable = await getModelAttachmentCapability(port, options?.model, client);
|
|
3136
3766
|
const {
|
|
3137
3767
|
parts: fileParts,
|
|
3138
3768
|
outcomes,
|
|
@@ -3145,11 +3775,17 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
3145
3775
|
parts
|
|
3146
3776
|
};
|
|
3147
3777
|
applyModelOptions(body, options);
|
|
3148
|
-
const res = await
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3778
|
+
const res = await requestWithClient(
|
|
3779
|
+
port,
|
|
3780
|
+
client,
|
|
3781
|
+
`/session/${sessionId}/prompt_async`,
|
|
3782
|
+
{
|
|
3783
|
+
method: "POST",
|
|
3784
|
+
headers: { "Content-Type": "application/json" },
|
|
3785
|
+
body: JSON.stringify(body)
|
|
3786
|
+
},
|
|
3787
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
3788
|
+
);
|
|
3153
3789
|
if (res.status < 200 || res.status >= 300) {
|
|
3154
3790
|
const text = await res.text().catch(() => "");
|
|
3155
3791
|
const { variant } = splitModelVariant(options?.model);
|
|
@@ -3160,7 +3796,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
3160
3796
|
const READ_BACK_ATTEMPTS = 5;
|
|
3161
3797
|
const READ_BACK_DELAY_MS = 150;
|
|
3162
3798
|
for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
|
|
3163
|
-
const after = await getSessionMessages(port, sessionId);
|
|
3799
|
+
const after = await getSessionMessages(port, sessionId, client);
|
|
3164
3800
|
if (after) {
|
|
3165
3801
|
let best = null;
|
|
3166
3802
|
for (const m of after) {
|
|
@@ -3263,21 +3899,74 @@ function collectSubagentSessions(messages, userMessageId) {
|
|
|
3263
3899
|
}
|
|
3264
3900
|
return refs;
|
|
3265
3901
|
}
|
|
3266
|
-
function
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3902
|
+
function finiteNumber2(value) {
|
|
3903
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3904
|
+
}
|
|
3905
|
+
function taskCallModel(value) {
|
|
3906
|
+
if (!value || typeof value !== "object") return null;
|
|
3907
|
+
const model = value;
|
|
3908
|
+
const modelID = typeof model.modelID === "string" ? model.modelID : void 0;
|
|
3909
|
+
const providerID = typeof model.providerID === "string" ? model.providerID : void 0;
|
|
3910
|
+
return modelID || providerID ? { modelID, providerID } : null;
|
|
3911
|
+
}
|
|
3912
|
+
function collectTaskCalls(messages, userMessageId) {
|
|
3913
|
+
if (!messages || messages.length === 0) return [];
|
|
3914
|
+
const calls = [];
|
|
3915
|
+
for (const message of messages) {
|
|
3916
|
+
if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
|
|
3917
|
+
for (const part of message.parts ?? []) {
|
|
3918
|
+
if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
|
|
3919
|
+
continue;
|
|
3920
|
+
}
|
|
3921
|
+
const rawName = part.state.input?.subagent_type;
|
|
3922
|
+
const subagentName = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : rawName === void 0 ? "general" : "unknown";
|
|
3923
|
+
const metadata = part.state.metadata;
|
|
3924
|
+
calls.push({
|
|
3925
|
+
callID: part.callID,
|
|
3926
|
+
subagentName,
|
|
3927
|
+
childSessionId: typeof metadata?.sessionId === "string" ? metadata.sessionId : null,
|
|
3928
|
+
parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
|
|
3929
|
+
model: taskCallModel(metadata?.model),
|
|
3930
|
+
status: part.state.status ?? "unknown",
|
|
3931
|
+
timeStart: finiteNumber2(part.state.time?.start),
|
|
3932
|
+
timeEnd: finiteNumber2(part.state.time?.end)
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
return calls;
|
|
3937
|
+
}
|
|
3938
|
+
function attributeTaskCallUsage(messages, windows) {
|
|
3939
|
+
const eligibleWindows = windows.filter(
|
|
3940
|
+
(window) => window.timeStart !== null && Number.isFinite(window.timeStart)
|
|
3270
3941
|
);
|
|
3271
|
-
const
|
|
3272
|
-
const
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
const
|
|
3278
|
-
|
|
3942
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
3943
|
+
for (const window of eligibleWindows) assignments.set(window.callID, []);
|
|
3944
|
+
const unattributed = [];
|
|
3945
|
+
for (const message of messages ?? []) {
|
|
3946
|
+
if (roleOf(message) !== "assistant") continue;
|
|
3947
|
+
const created = finiteNumber2(createdOf(message));
|
|
3948
|
+
const matching = created === null ? [] : eligibleWindows.filter(
|
|
3949
|
+
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
3950
|
+
);
|
|
3951
|
+
if (matching.length === 0) {
|
|
3952
|
+
unattributed.push(message);
|
|
3953
|
+
continue;
|
|
3954
|
+
}
|
|
3955
|
+
matching.sort((a, b) => a.timeStart - b.timeStart);
|
|
3956
|
+
assignments.get(matching[0].callID)?.push(message);
|
|
3279
3957
|
}
|
|
3280
|
-
|
|
3958
|
+
return {
|
|
3959
|
+
invocations: eligibleWindows.map((window) => {
|
|
3960
|
+
const assigned = assignments.get(window.callID) ?? [];
|
|
3961
|
+
return { callID: window.callID, messages: assigned, usage: sumAssistantUsage(assigned) };
|
|
3962
|
+
}),
|
|
3963
|
+
unattributed
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
3966
|
+
function sumAssistantUsage(messages) {
|
|
3967
|
+
if (!messages || messages.length === 0) return null;
|
|
3968
|
+
const nonErrored = messages.filter((message) => errorOf(message) == null);
|
|
3969
|
+
const selected = nonErrored.length > 0 ? nonErrored : messages;
|
|
3281
3970
|
let sawAnyUsage = false;
|
|
3282
3971
|
let inputSum = 0;
|
|
3283
3972
|
let outputSum = 0;
|
|
@@ -3288,7 +3977,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
3288
3977
|
let sawCost = false;
|
|
3289
3978
|
let modelId = null;
|
|
3290
3979
|
let providerId = null;
|
|
3291
|
-
for (const m of
|
|
3980
|
+
for (const m of selected) {
|
|
3292
3981
|
const info = m.info;
|
|
3293
3982
|
if (!info) continue;
|
|
3294
3983
|
const tokens = info.tokens;
|
|
@@ -3323,12 +4012,28 @@ function messageUsage(messages, userMessageId) {
|
|
|
3323
4012
|
usage_tokens_reasoning: reasoningSum,
|
|
3324
4013
|
usage_tokens_cache_read: cacheReadSum,
|
|
3325
4014
|
usage_tokens_cache_write: cacheWriteSum,
|
|
3326
|
-
// NULL means
|
|
3327
|
-
//
|
|
3328
|
-
// `sawCost` true with `costSum === 0`.
|
|
4015
|
+
// NULL means OpenCode never reported a cost; it is distinct from a genuine
|
|
4016
|
+
// zero-cost message, which sets `sawCost` with `costSum === 0`.
|
|
3329
4017
|
usage_cost_usd: sawCost ? costSum : null
|
|
3330
4018
|
};
|
|
3331
4019
|
}
|
|
4020
|
+
function messageUsage(messages, userMessageId) {
|
|
4021
|
+
if (!messages || messages.length === 0) return null;
|
|
4022
|
+
const byParentAll = messages.filter(
|
|
4023
|
+
(m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
|
|
4024
|
+
);
|
|
4025
|
+
const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
|
|
4026
|
+
const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
|
|
4027
|
+
let correlated;
|
|
4028
|
+
if (byParent.length > 0) {
|
|
4029
|
+
correlated = byParent;
|
|
4030
|
+
} else {
|
|
4031
|
+
const reply = findAssistantReplyAfter(messages, userMessageId);
|
|
4032
|
+
correlated = reply ? [reply] : [];
|
|
4033
|
+
}
|
|
4034
|
+
if (correlated.length === 0) return null;
|
|
4035
|
+
return sumAssistantUsage(correlated);
|
|
4036
|
+
}
|
|
3332
4037
|
function messageRunState(messages, userMessageId) {
|
|
3333
4038
|
if (!messages || messages.length === 0) return "unknown";
|
|
3334
4039
|
const hasUser = messages.some((m) => idOf(m) === userMessageId);
|
|
@@ -3458,9 +4163,73 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
|
3458
4163
|
(m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
3459
4164
|
);
|
|
3460
4165
|
}
|
|
3461
|
-
|
|
4166
|
+
function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
|
|
4167
|
+
if (!messages || messages.length === 0) return false;
|
|
4168
|
+
const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
|
|
4169
|
+
if (userIndex === -1) return false;
|
|
4170
|
+
let hasLaterUser = false;
|
|
4171
|
+
let hasStartedLaterUser = false;
|
|
4172
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
4173
|
+
const message = messages[i];
|
|
4174
|
+
if (roleOf(message) !== "user") continue;
|
|
4175
|
+
hasLaterUser = true;
|
|
4176
|
+
const laterUserMessageId = idOf(message);
|
|
4177
|
+
if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
|
|
4178
|
+
return false;
|
|
4179
|
+
}
|
|
4180
|
+
if (messages.some(
|
|
4181
|
+
(candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
|
|
4182
|
+
)) {
|
|
4183
|
+
hasStartedLaterUser = true;
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
return hasLaterUser && hasStartedLaterUser;
|
|
4187
|
+
}
|
|
4188
|
+
async function hasAnyConfiguredProvider(port, client) {
|
|
4189
|
+
if (client?.version === "v2") {
|
|
4190
|
+
const directory = await getOpenCodeDirectoryV2(client);
|
|
4191
|
+
if (!directory) {
|
|
4192
|
+
console.error(
|
|
4193
|
+
`[hasAnyConfiguredProvider] V2 working directory was unavailable (port ${port})`
|
|
4194
|
+
);
|
|
4195
|
+
return null;
|
|
4196
|
+
}
|
|
4197
|
+
const path = `/api/integration?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
|
|
4198
|
+
try {
|
|
4199
|
+
const res = await client.request(path);
|
|
4200
|
+
if (!res.ok) {
|
|
4201
|
+
console.error(
|
|
4202
|
+
`[hasAnyConfiguredProvider] GET ${path} returned HTTP ${res.status} (port ${port})`
|
|
4203
|
+
);
|
|
4204
|
+
return null;
|
|
4205
|
+
}
|
|
4206
|
+
const body = await res.json();
|
|
4207
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
|
|
4208
|
+
console.error(
|
|
4209
|
+
`[hasAnyConfiguredProvider] GET ${path} body had no integration data array (port ${port})`
|
|
4210
|
+
);
|
|
4211
|
+
return null;
|
|
4212
|
+
}
|
|
4213
|
+
for (const integration of body.data) {
|
|
4214
|
+
if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
4215
|
+
console.error(
|
|
4216
|
+
`[hasAnyConfiguredProvider] GET ${path} body contained an invalid integration (port ${port})`
|
|
4217
|
+
);
|
|
4218
|
+
return null;
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
return body.data.some(
|
|
4222
|
+
(integration) => Array.isArray(integration.connections) && integration.connections.length > 0
|
|
4223
|
+
);
|
|
4224
|
+
} catch (error2) {
|
|
4225
|
+
console.error(
|
|
4226
|
+
`[hasAnyConfiguredProvider] GET ${path} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4227
|
+
);
|
|
4228
|
+
return null;
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
3462
4231
|
try {
|
|
3463
|
-
const res = await
|
|
4232
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
3464
4233
|
if (!res.ok) {
|
|
3465
4234
|
console.error(
|
|
3466
4235
|
`[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
@@ -3489,9 +4258,84 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3489
4258
|
return null;
|
|
3490
4259
|
}
|
|
3491
4260
|
}
|
|
3492
|
-
|
|
4261
|
+
function sessionErrorReason2(error2) {
|
|
4262
|
+
const record = typeof error2 === "object" && error2 !== null ? error2 : null;
|
|
4263
|
+
const data = record?.data;
|
|
4264
|
+
const dataRecord = typeof data === "object" && data !== null ? data : null;
|
|
4265
|
+
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";
|
|
4266
|
+
const reason = rawReason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
4267
|
+
return reason || "OpenCode reported a session error with no details";
|
|
4268
|
+
}
|
|
4269
|
+
function parseSessionErrorFrame(data) {
|
|
4270
|
+
let parsed;
|
|
4271
|
+
try {
|
|
4272
|
+
parsed = JSON.parse(data);
|
|
4273
|
+
} catch (error2) {
|
|
4274
|
+
void error2;
|
|
4275
|
+
return null;
|
|
4276
|
+
}
|
|
4277
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
4278
|
+
const parsedRecord = parsed;
|
|
4279
|
+
const payload = parsedRecord.payload;
|
|
4280
|
+
const event = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : parsedRecord;
|
|
4281
|
+
if (event.type !== "session.error") return null;
|
|
4282
|
+
const properties = event.properties;
|
|
4283
|
+
if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
|
|
4284
|
+
return null;
|
|
4285
|
+
}
|
|
4286
|
+
const propertiesRecord = properties;
|
|
4287
|
+
const sessionId = propertiesRecord.sessionID;
|
|
4288
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
4289
|
+
return {
|
|
4290
|
+
sessionId,
|
|
4291
|
+
reason: sessionErrorReason2(propertiesRecord.error)
|
|
4292
|
+
};
|
|
4293
|
+
}
|
|
4294
|
+
async function readSessionErrorStream(port, options, client) {
|
|
4295
|
+
if (client?.version === "v2") return readV2SessionErrorStream(client, options);
|
|
4296
|
+
let reader = null;
|
|
4297
|
+
try {
|
|
4298
|
+
const response = await (client?.request("/event", {
|
|
4299
|
+
headers: { accept: "text/event-stream" },
|
|
4300
|
+
signal: options.signal
|
|
4301
|
+
}) ?? fetch(`${opencodeBase(port)}/event`, {
|
|
4302
|
+
headers: { accept: "text/event-stream" },
|
|
4303
|
+
signal: options.signal
|
|
4304
|
+
}));
|
|
4305
|
+
if (!response.ok || !response.body) {
|
|
4306
|
+
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
4307
|
+
}
|
|
4308
|
+
reader = response.body.getReader();
|
|
4309
|
+
const decoder = new TextDecoder();
|
|
4310
|
+
let buffer = "";
|
|
4311
|
+
const processLine = (line) => {
|
|
4312
|
+
const trimmed = line.trimEnd();
|
|
4313
|
+
if (!trimmed.startsWith("data:")) return;
|
|
4314
|
+
const event = parseSessionErrorFrame(trimmed.slice("data:".length).replace(/^ /, ""));
|
|
4315
|
+
if (event) options.onSessionError(event);
|
|
4316
|
+
};
|
|
4317
|
+
while (true) {
|
|
4318
|
+
const { done, value } = await reader.read();
|
|
4319
|
+
if (done) return { reason: "ended" };
|
|
4320
|
+
buffer += decoder.decode(value, { stream: true });
|
|
4321
|
+
const lines = buffer.split("\n");
|
|
4322
|
+
buffer = lines.pop() ?? "";
|
|
4323
|
+
for (const line of lines) processLine(line);
|
|
4324
|
+
}
|
|
4325
|
+
} catch (err) {
|
|
4326
|
+
if (options.signal.aborted) return { reason: "aborted" };
|
|
4327
|
+
return {
|
|
4328
|
+
reason: "unavailable",
|
|
4329
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
4330
|
+
};
|
|
4331
|
+
} finally {
|
|
4332
|
+
if (reader) void reader.cancel().catch(() => void 0);
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
async function reloadProviderCache(port, client) {
|
|
4336
|
+
if (client?.version === "v2") return;
|
|
3493
4337
|
try {
|
|
3494
|
-
const res = await
|
|
4338
|
+
const res = await requestWithClient(port, client, "/config", {
|
|
3495
4339
|
method: "PATCH",
|
|
3496
4340
|
headers: { "Content-Type": "application/json" },
|
|
3497
4341
|
body: JSON.stringify({})
|
|
@@ -3869,10 +4713,11 @@ var STRIP_RES = /* @__PURE__ */ new Set([
|
|
|
3869
4713
|
"content-length"
|
|
3870
4714
|
]);
|
|
3871
4715
|
var StreamForwarder = class {
|
|
3872
|
-
constructor(ws, port, callbacks = {}) {
|
|
4716
|
+
constructor(ws, port, callbacks = {}, options = {}) {
|
|
3873
4717
|
this.ws = ws;
|
|
3874
4718
|
this.port = port;
|
|
3875
4719
|
this.callbacks = callbacks;
|
|
4720
|
+
this.options = options;
|
|
3876
4721
|
}
|
|
3877
4722
|
inflight = /* @__PURE__ */ new Map();
|
|
3878
4723
|
/**
|
|
@@ -3956,7 +4801,15 @@ var StreamForwarder = class {
|
|
|
3956
4801
|
}
|
|
3957
4802
|
const fwdHeaders = {};
|
|
3958
4803
|
for (const [k, v] of Object.entries(headers ?? {})) {
|
|
3959
|
-
|
|
4804
|
+
const lower = k.toLowerCase();
|
|
4805
|
+
if (STRIP_REQ.has(lower)) continue;
|
|
4806
|
+
if (this.options.openCodePassword !== void 0 && this.options.openCodePassword !== null) {
|
|
4807
|
+
if (lower === "authorization") continue;
|
|
4808
|
+
}
|
|
4809
|
+
fwdHeaders[k] = v;
|
|
4810
|
+
}
|
|
4811
|
+
if (this.options.openCodePassword !== void 0 && this.options.openCodePassword !== null) {
|
|
4812
|
+
fwdHeaders.Authorization = buildOpenCodeBasicAuthHeader(this.options.openCodePassword);
|
|
3960
4813
|
}
|
|
3961
4814
|
this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });
|
|
3962
4815
|
const body = bodyPromise ? await bodyPromise : void 0;
|
|
@@ -4069,6 +4922,7 @@ function connectTunnel(options) {
|
|
|
4069
4922
|
agentId,
|
|
4070
4923
|
authHeader,
|
|
4071
4924
|
port,
|
|
4925
|
+
openCodePassword,
|
|
4072
4926
|
onConnected,
|
|
4073
4927
|
onDisconnected,
|
|
4074
4928
|
onError,
|
|
@@ -4086,11 +4940,16 @@ function connectTunnel(options) {
|
|
|
4086
4940
|
Authorization: authHeader
|
|
4087
4941
|
}
|
|
4088
4942
|
});
|
|
4089
|
-
const forwarder = new StreamForwarder(
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4943
|
+
const forwarder = new StreamForwarder(
|
|
4944
|
+
ws,
|
|
4945
|
+
port,
|
|
4946
|
+
{
|
|
4947
|
+
onHead: () => onResponse?.(),
|
|
4948
|
+
onDrainPing: () => onDrainPing?.(),
|
|
4949
|
+
onUsageRearmPing: () => onUsageRearmPing?.()
|
|
4950
|
+
},
|
|
4951
|
+
{ openCodePassword }
|
|
4952
|
+
);
|
|
4094
4953
|
const connectionTimeout = setTimeout(() => {
|
|
4095
4954
|
ws.close();
|
|
4096
4955
|
reject(new Error("Connection timeout"));
|
|
@@ -4233,6 +5092,7 @@ var RunnerConnection = class {
|
|
|
4233
5092
|
agentId: this.resolvedAgentId,
|
|
4234
5093
|
authHeader: this.opts.getAuthHeader(),
|
|
4235
5094
|
port: this.opts.port,
|
|
5095
|
+
openCodePassword: this.opts.openCodePassword,
|
|
4236
5096
|
onConnected: (agentId) => {
|
|
4237
5097
|
this.reconnectAttempt = 0;
|
|
4238
5098
|
this.reconnecting = false;
|
|
@@ -4384,7 +5244,7 @@ function parseChatGptIdentity(accessToken) {
|
|
|
4384
5244
|
const auth = payload["https://api.openai.com/auth"];
|
|
4385
5245
|
const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
|
|
4386
5246
|
const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
|
|
4387
|
-
return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
|
|
5247
|
+
return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
|
|
4388
5248
|
}
|
|
4389
5249
|
function toWindow2(headers, name) {
|
|
4390
5250
|
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
@@ -4413,33 +5273,73 @@ function parseCodexUsageHeaders(headers) {
|
|
|
4413
5273
|
function normalizeProbeModel(model) {
|
|
4414
5274
|
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
4415
5275
|
}
|
|
4416
|
-
|
|
5276
|
+
function isRecord2(value) {
|
|
5277
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5278
|
+
}
|
|
5279
|
+
function unsupportedProbeModels(reason, port) {
|
|
5280
|
+
console.error(`[resolveProbeModels] ${reason} (port ${port})`);
|
|
5281
|
+
return { status: "unsupported", reason };
|
|
5282
|
+
}
|
|
5283
|
+
async function resolveV1ProbeModels(client, port) {
|
|
4417
5284
|
try {
|
|
4418
|
-
const
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
if (!res.ok) {
|
|
4423
|
-
console.error(
|
|
4424
|
-
`[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
4425
|
-
);
|
|
4426
|
-
return [];
|
|
5285
|
+
const response = await client.request("/config/providers");
|
|
5286
|
+
const body = await response.json();
|
|
5287
|
+
if (!isRecord2(body) || !Array.isArray(body.providers)) {
|
|
5288
|
+
return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
|
|
4427
5289
|
}
|
|
4428
|
-
const
|
|
4429
|
-
|
|
4430
|
-
|
|
5290
|
+
const provider = body.providers.find(
|
|
5291
|
+
(candidate) => isRecord2(candidate) && candidate.id === "openai"
|
|
5292
|
+
);
|
|
5293
|
+
if (!provider || !isRecord2(provider.models)) return { status: "supported", models: [] };
|
|
5294
|
+
const defaults2 = isRecord2(body.default) ? body.default : void 0;
|
|
4431
5295
|
const candidates = [
|
|
4432
|
-
...typeof
|
|
5296
|
+
...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
|
|
4433
5297
|
...Object.keys(provider.models)
|
|
4434
5298
|
].map(normalizeProbeModel);
|
|
4435
|
-
return [...new Set(candidates)].slice(0, 4);
|
|
5299
|
+
return { status: "supported", models: [...new Set(candidates)].slice(0, 4) };
|
|
4436
5300
|
} catch (err) {
|
|
4437
|
-
|
|
4438
|
-
`
|
|
5301
|
+
return unsupportedProbeModels(
|
|
5302
|
+
`V1 GET /config/providers failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
5303
|
+
port
|
|
4439
5304
|
);
|
|
4440
|
-
return [];
|
|
4441
5305
|
}
|
|
4442
5306
|
}
|
|
5307
|
+
async function resolveV2ProbeModels(client, port) {
|
|
5308
|
+
const directory = await getOpenCodeDirectoryV2(client);
|
|
5309
|
+
if (!directory) {
|
|
5310
|
+
return unsupportedProbeModels("V2 working directory could not be verified", port);
|
|
5311
|
+
}
|
|
5312
|
+
const path = `/api/provider?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
|
|
5313
|
+
try {
|
|
5314
|
+
const response = await client.request(path);
|
|
5315
|
+
const body = await response.json();
|
|
5316
|
+
if (!isRecord2(body) || !Array.isArray(body.data)) {
|
|
5317
|
+
return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
|
|
5318
|
+
}
|
|
5319
|
+
const provider = body.data.find(
|
|
5320
|
+
(candidate) => isRecord2(candidate) && candidate.id === "openai"
|
|
5321
|
+
);
|
|
5322
|
+
if (!provider) return { status: "supported", models: [] };
|
|
5323
|
+
if (!isRecord2(provider.models)) {
|
|
5324
|
+
return unsupportedProbeModels(
|
|
5325
|
+
"V2 provider response has no safe OpenAI model catalogue",
|
|
5326
|
+
port
|
|
5327
|
+
);
|
|
5328
|
+
}
|
|
5329
|
+
return {
|
|
5330
|
+
status: "supported",
|
|
5331
|
+
models: [...new Set(Object.keys(provider.models).map(normalizeProbeModel))].slice(0, 4)
|
|
5332
|
+
};
|
|
5333
|
+
} catch (err) {
|
|
5334
|
+
return unsupportedProbeModels(
|
|
5335
|
+
`V2 GET ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
5336
|
+
port
|
|
5337
|
+
);
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
async function resolveProbeModels(port, client = createOpenCodeClient({ port, version: "v1" })) {
|
|
5341
|
+
return client.version === "v2" ? resolveV2ProbeModels(client, port) : resolveV1ProbeModels(client, port);
|
|
5342
|
+
}
|
|
4443
5343
|
function hasPrimaryHeaders(headers) {
|
|
4444
5344
|
return [
|
|
4445
5345
|
"x-codex-primary-used-percent",
|
|
@@ -4447,7 +5347,7 @@ function hasPrimaryHeaders(headers) {
|
|
|
4447
5347
|
"x-codex-primary-reset-at"
|
|
4448
5348
|
].some((name) => headers.has(name));
|
|
4449
5349
|
}
|
|
4450
|
-
async function getOpenAiUsage(port) {
|
|
5350
|
+
async function getOpenAiUsage(port, client) {
|
|
4451
5351
|
const credentials2 = readOpenCodeChatGptCredentials();
|
|
4452
5352
|
if (!credentials2) {
|
|
4453
5353
|
throw new OpenAiUsageError(
|
|
@@ -4462,12 +5362,16 @@ async function getOpenAiUsage(port) {
|
|
|
4462
5362
|
);
|
|
4463
5363
|
}
|
|
4464
5364
|
const subscription = parseChatGptIdentity(credentials2.accessToken);
|
|
4465
|
-
const
|
|
4466
|
-
if (models.length === 0) {
|
|
4467
|
-
|
|
5365
|
+
const lookup = await resolveProbeModels(port, client);
|
|
5366
|
+
if (lookup.status === "unsupported" || lookup.models.length === 0) {
|
|
5367
|
+
const detail = lookup.status === "unsupported" ? ` ${lookup.reason}.` : "";
|
|
5368
|
+
throw new OpenAiUsageError(
|
|
5369
|
+
`No supported OpenAI probe model is available.${detail}`,
|
|
5370
|
+
"no_probe_model"
|
|
5371
|
+
);
|
|
4468
5372
|
}
|
|
4469
5373
|
let lastStatus;
|
|
4470
|
-
for (const model of models) {
|
|
5374
|
+
for (const model of lookup.models) {
|
|
4471
5375
|
let res;
|
|
4472
5376
|
try {
|
|
4473
5377
|
res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
|
|
@@ -4566,13 +5470,6 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
|
|
|
4566
5470
|
envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
4567
5471
|
});
|
|
4568
5472
|
}
|
|
4569
|
-
function nextReportDelayMs(random = Math.random) {
|
|
4570
|
-
return usageReportDelayMs(random);
|
|
4571
|
-
}
|
|
4572
|
-
var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
|
|
4573
|
-
function claudeUsageFailureLogLevel(consecutiveFailures) {
|
|
4574
|
-
return usageReportFailureLogLevel(consecutiveFailures);
|
|
4575
|
-
}
|
|
4576
5473
|
|
|
4577
5474
|
// src/lib/openai-usage-reporting.ts
|
|
4578
5475
|
function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
@@ -5228,6 +6125,10 @@ var DEFAULT_RETRY_POLICY = {
|
|
|
5228
6125
|
baseDelayMs: 500,
|
|
5229
6126
|
maxDelayMs: 3e4
|
|
5230
6127
|
};
|
|
6128
|
+
var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
|
|
6129
|
+
var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
|
|
6130
|
+
var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
6131
|
+
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
5231
6132
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
5232
6133
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
5233
6134
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
@@ -5289,6 +6190,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5289
6190
|
maxActiveSessions;
|
|
5290
6191
|
watcherStallMs;
|
|
5291
6192
|
wedgeWarningIntervalMs;
|
|
6193
|
+
openCodeClient;
|
|
5292
6194
|
/** Cache of conversationId → opencode sessionId. */
|
|
5293
6195
|
sessions = /* @__PURE__ */ new Map();
|
|
5294
6196
|
/**
|
|
@@ -5368,6 +6270,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5368
6270
|
* message; it is removed once its in-flight set empties.
|
|
5369
6271
|
*/
|
|
5370
6272
|
watchers = /* @__PURE__ */ new Map();
|
|
6273
|
+
sessionErrorStream = null;
|
|
6274
|
+
/**
|
|
6275
|
+
* Session-error failures currently being reported; entries are empty at rest
|
|
6276
|
+
* because each handoff deletes its id in `finally`.
|
|
6277
|
+
*/
|
|
6278
|
+
sessionErrorHandled = /* @__PURE__ */ new Set();
|
|
6279
|
+
/**
|
|
6280
|
+
* Session errors that arrived before their dispatch was registered. Bounded FIFO
|
|
6281
|
+
* with a short TTL so an unmatched session cannot retain an event indefinitely.
|
|
6282
|
+
*/
|
|
6283
|
+
bufferedSessionErrors = /* @__PURE__ */ new Map();
|
|
5371
6284
|
/**
|
|
5372
6285
|
* AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
|
|
5373
6286
|
* dispatched and are still in-flight. A message in this set is never
|
|
@@ -5386,20 +6299,23 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5386
6299
|
*/
|
|
5387
6300
|
readopted = /* @__PURE__ */ new Set();
|
|
5388
6301
|
/**
|
|
5389
|
-
*
|
|
5390
|
-
*
|
|
5391
|
-
*
|
|
5392
|
-
*
|
|
5393
|
-
*
|
|
5394
|
-
*
|
|
5395
|
-
* CRITICAL (Bugbot #202): this suppresses ONLY the dispatch/re-attach paths, it
|
|
5396
|
-
* does NOT suppress DONE delivery. A row parked here whose reply later COMPLETES
|
|
5397
|
-
* in opencode must still be delivered via `markDone` on the next drain — so
|
|
5398
|
-
* `readoptOne` computes `state` FIRST and this set is checked only on the
|
|
5399
|
-
* non-done path. It is cleared once the row leaves the processing list (cron
|
|
5400
|
-
* reset → it drains normally as `pending`), so it can never leak.
|
|
6302
|
+
* Readopt give-up fence. Set when recovery declines to start or continue a turn
|
|
6303
|
+
* for a row that is still `processing`, so the next drain does not re-dispatch or
|
|
6304
|
+
* re-attach it before the cron safety net acts. It suppresses only non-done
|
|
6305
|
+
* recovery paths; DONE delivery still runs. Clear it when
|
|
6306
|
+
* `!stillProcessing.has(id)`, because leaving `processing` hands the row back to
|
|
6307
|
+
* normal processing.
|
|
5401
6308
|
*/
|
|
5402
6309
|
dontRedispatch = /* @__PURE__ */ new Set();
|
|
6310
|
+
/**
|
|
6311
|
+
* Untrackable-ack fence. Set after OpenCode accepts a prompt without returning a
|
|
6312
|
+
* usable message id, because another POST could create a duplicate turn. Keep it
|
|
6313
|
+
* fenced while the row is `processing` or `pending`; clear it only when the row
|
|
6314
|
+
* is absent from both lists.
|
|
6315
|
+
*/
|
|
6316
|
+
untrackableAck = /* @__PURE__ */ new Set();
|
|
6317
|
+
/** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
|
|
6318
|
+
pendingMessageIds = /* @__PURE__ */ new Set();
|
|
5403
6319
|
/**
|
|
5404
6320
|
* "markDone for this row is TERMINALLY undeliverable" (Bug 4). Set ONLY when a
|
|
5405
6321
|
* re-adopted DONE row's `markDone` returned a terminal 4xx (a status that will
|
|
@@ -5522,7 +6438,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5522
6438
|
*/
|
|
5523
6439
|
attachmentsSkippedSignalled = /* @__PURE__ */ new Set();
|
|
5524
6440
|
/**
|
|
5525
|
-
* Cache of the opencode root directory
|
|
6441
|
+
* Cache of the opencode root directory from the selected client's location lookup.
|
|
6442
|
+
* Resolved lazily on
|
|
5526
6443
|
* first session creation so drain-created sessions are rooted at the project
|
|
5527
6444
|
* directory and thus visible in `opencode web`'s session list. `undefined` =
|
|
5528
6445
|
* not yet resolved; `null` = resolved-but-unavailable (don't keep retrying).
|
|
@@ -5551,6 +6468,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5551
6468
|
* no watcher) can resolve the title.
|
|
5552
6469
|
*/
|
|
5553
6470
|
sessionTitles = /* @__PURE__ */ new Map();
|
|
6471
|
+
/** One best-effort terminal subagent collection per Evident message id. */
|
|
6472
|
+
subagentInvocationCollections = /* @__PURE__ */ new Map();
|
|
6473
|
+
/**
|
|
6474
|
+
* Early snapshots are only liveness hints; they must not become the terminal
|
|
6475
|
+
* collection when the task parts or child transcript have advanced.
|
|
6476
|
+
*/
|
|
6477
|
+
subagentInvocationPrefetches = /* @__PURE__ */ new Map();
|
|
5554
6478
|
/** Serialises drains so a reconnect during a drain doesn't double-process. */
|
|
5555
6479
|
draining = false;
|
|
5556
6480
|
/**
|
|
@@ -5609,6 +6533,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5609
6533
|
config.fetchImpl ?? fetch,
|
|
5610
6534
|
config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
|
|
5611
6535
|
);
|
|
6536
|
+
this.openCodeClient = config.openCodeClient ?? createOpenCodeClient({
|
|
6537
|
+
port: config.port,
|
|
6538
|
+
version: "v1",
|
|
6539
|
+
fetchImpl: config.fetchImpl
|
|
6540
|
+
});
|
|
5612
6541
|
this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5613
6542
|
this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
|
|
5614
6543
|
this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
|
|
@@ -5620,9 +6549,39 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5620
6549
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
5621
6550
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
5622
6551
|
}
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
6552
|
+
get isV2() {
|
|
6553
|
+
return this.openCodeClient.version === "v2";
|
|
6554
|
+
}
|
|
6555
|
+
async getSessionMessages(sessionId) {
|
|
6556
|
+
return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6557
|
+
}
|
|
6558
|
+
async getSubagentSessionMessages(sessionId) {
|
|
6559
|
+
return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : getSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6560
|
+
}
|
|
6561
|
+
async getTelemetrySubagentSessionMessages(sessionId) {
|
|
6562
|
+
if (this.isV2) return getV2SessionMessages(this.openCodeClient, sessionId);
|
|
6563
|
+
return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6564
|
+
}
|
|
6565
|
+
async listSessions() {
|
|
6566
|
+
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient);
|
|
6567
|
+
}
|
|
6568
|
+
async sessionExists(sessionId) {
|
|
6569
|
+
return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
|
|
6570
|
+
}
|
|
6571
|
+
async isSessionOngoing(sessionId) {
|
|
6572
|
+
return this.isV2 ? isV2SessionOngoing(this.openCodeClient, sessionId) : isSessionOngoing(this.port, sessionId, this.openCodeClient);
|
|
6573
|
+
}
|
|
6574
|
+
async getOpenCodeDirectory() {
|
|
6575
|
+
return this.isV2 ? getOpenCodeDirectoryV2(this.openCodeClient) : getOpenCodeDirectory(this.port, this.openCodeClient);
|
|
6576
|
+
}
|
|
6577
|
+
async createOpenCodeSession(directory) {
|
|
6578
|
+
return this.isV2 ? createV2Session(this.openCodeClient, directory) : createOpenCodeSession(this.port, directory, this.openCodeClient);
|
|
6579
|
+
}
|
|
6580
|
+
async hasAnyConfiguredProvider() {
|
|
6581
|
+
return hasAnyConfiguredProvider(this.port, this.openCodeClient);
|
|
6582
|
+
}
|
|
6583
|
+
async readOpenCodeSessionErrorStream(options) {
|
|
6584
|
+
return this.isV2 ? readV2SessionErrorStream(this.openCodeClient, options) : readSessionErrorStream(this.port, options, this.openCodeClient);
|
|
5626
6585
|
}
|
|
5627
6586
|
/**
|
|
5628
6587
|
* Drain all pending channel conversations once: poll → dispatch → register.
|
|
@@ -5700,6 +6659,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5700
6659
|
async runDrain() {
|
|
5701
6660
|
let dispatched = 0;
|
|
5702
6661
|
try {
|
|
6662
|
+
this.pendingMessageIds.clear();
|
|
5703
6663
|
const conversations = await this.getPendingConversations();
|
|
5704
6664
|
if (this.recycleRequestedFlag) {
|
|
5705
6665
|
this.stop();
|
|
@@ -5765,6 +6725,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5765
6725
|
}
|
|
5766
6726
|
return ids;
|
|
5767
6727
|
}
|
|
6728
|
+
/**
|
|
6729
|
+
* OpenCode user-message ids tracked for other Evident messages in a session.
|
|
6730
|
+
* Excluding this message makes an unattributed later row fail safe; a missing
|
|
6731
|
+
* watcher yields no attributions, per `hasLaterSiblingTurnStarted`'s docblock.
|
|
6732
|
+
*/
|
|
6733
|
+
siblingOpencodeMessageIds(watcher, ownEvidentMessageId) {
|
|
6734
|
+
const ids = /* @__PURE__ */ new Set();
|
|
6735
|
+
if (!watcher) return ids;
|
|
6736
|
+
for (const inFlight of watcher.inFlight.values()) {
|
|
6737
|
+
if (inFlight.evidentMessageId !== ownEvidentMessageId) {
|
|
6738
|
+
ids.add(inFlight.opencodeMessageId);
|
|
6739
|
+
}
|
|
6740
|
+
}
|
|
6741
|
+
return ids;
|
|
6742
|
+
}
|
|
5768
6743
|
/**
|
|
5769
6744
|
* File-pull work, for `run.ts`'s idle accounting (#559).
|
|
5770
6745
|
*
|
|
@@ -5831,6 +6806,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5831
6806
|
*/
|
|
5832
6807
|
stop() {
|
|
5833
6808
|
this.stopped = true;
|
|
6809
|
+
this.sessionErrorStream?.abort.abort();
|
|
6810
|
+
this.sessionErrorStream = null;
|
|
5834
6811
|
}
|
|
5835
6812
|
/**
|
|
5836
6813
|
* The server clears this request when a new MicroVM identity is recorded, so a
|
|
@@ -5905,7 +6882,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5905
6882
|
*/
|
|
5906
6883
|
async processConversation(conv) {
|
|
5907
6884
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
6885
|
+
this.ensureSessionErrorStream();
|
|
5908
6886
|
const messages = await this.getPendingMessages(conv.id);
|
|
6887
|
+
for (const message of messages) this.pendingMessageIds.add(message.id);
|
|
5909
6888
|
let dispatched = 0;
|
|
5910
6889
|
let skippedAlreadyDispatched = 0;
|
|
5911
6890
|
if (refusedSessionId && messages.length > 0) {
|
|
@@ -5919,6 +6898,15 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5919
6898
|
skippedAlreadyDispatched += 1;
|
|
5920
6899
|
continue;
|
|
5921
6900
|
}
|
|
6901
|
+
if (this.untrackableAck.has(message.id)) {
|
|
6902
|
+
this.log({
|
|
6903
|
+
level: "warn",
|
|
6904
|
+
message: `Message ${message.id.slice(0, 8)} is fenced after an untrackable OpenCode turn \u2014 skipping re-dispatch`,
|
|
6905
|
+
conversation_id: conv.id,
|
|
6906
|
+
message_id: message.id
|
|
6907
|
+
});
|
|
6908
|
+
break;
|
|
6909
|
+
}
|
|
5922
6910
|
const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
|
|
5923
6911
|
if (effectiveOpencodeMessageId) {
|
|
5924
6912
|
const outcome = await this.resolveRedrive(
|
|
@@ -5947,15 +6935,55 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5947
6935
|
conversation_id: conv.id,
|
|
5948
6936
|
message_id: message.id
|
|
5949
6937
|
});
|
|
5950
|
-
const sendAttachments = this.buildSendAttachments(conv, message);
|
|
5951
|
-
|
|
6938
|
+
const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(conv, message);
|
|
6939
|
+
if (this.isV2 && message.attachments && message.attachments.length > 0) {
|
|
6940
|
+
this.signalAttachmentsSkipped(
|
|
6941
|
+
conv.id,
|
|
6942
|
+
message.id,
|
|
6943
|
+
message.attachments.map((attachment, index) => ({
|
|
6944
|
+
index,
|
|
6945
|
+
mime: attachment.mime,
|
|
6946
|
+
...attachment.filename ? { filename: attachment.filename } : {},
|
|
6947
|
+
status: "skipped"
|
|
6948
|
+
})),
|
|
6949
|
+
false
|
|
6950
|
+
);
|
|
6951
|
+
}
|
|
6952
|
+
opencodeMessageId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, message.content) : await this.dispatchLocked(
|
|
5952
6953
|
sessionId,
|
|
5953
|
-
() => sendPromptAsync(
|
|
6954
|
+
() => sendPromptAsync(
|
|
6955
|
+
this.port,
|
|
6956
|
+
sessionId,
|
|
6957
|
+
message.content,
|
|
6958
|
+
options,
|
|
6959
|
+
sendAttachments,
|
|
6960
|
+
this.openCodeClient
|
|
6961
|
+
)
|
|
5954
6962
|
);
|
|
5955
6963
|
} catch (err) {
|
|
5956
6964
|
if (err instanceof ChannelAuthError) throw err;
|
|
6965
|
+
if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
|
|
6966
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
6967
|
+
this.untrackableAck.add(message.id);
|
|
6968
|
+
this.log({
|
|
6969
|
+
level: "error",
|
|
6970
|
+
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
|
|
6971
|
+
conversation_id: conv.id,
|
|
6972
|
+
message_id: message.id
|
|
6973
|
+
});
|
|
6974
|
+
await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
|
|
6975
|
+
this.log({
|
|
6976
|
+
level: "warn",
|
|
6977
|
+
message: `markFailed PATCH for V2 dispatch failure on message ${message.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
6978
|
+
conversation_id: conv.id,
|
|
6979
|
+
message_id: message.id
|
|
6980
|
+
});
|
|
6981
|
+
void this.postSignal(conv.id, message.id, "ack_untrackable");
|
|
6982
|
+
});
|
|
6983
|
+
break;
|
|
6984
|
+
}
|
|
5957
6985
|
this.dispatched.delete(message.id);
|
|
5958
|
-
const exists = await sessionExists(
|
|
6986
|
+
const exists = await this.sessionExists(sessionId);
|
|
5959
6987
|
if (exists === false) {
|
|
5960
6988
|
this.sessions.delete(conv.id);
|
|
5961
6989
|
this.log({
|
|
@@ -6004,6 +7032,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6004
7032
|
break;
|
|
6005
7033
|
}
|
|
6006
7034
|
if (opencodeMessageId === null) {
|
|
7035
|
+
if (this.isV2) {
|
|
7036
|
+
throw new Error("V2 prompt dispatch completed without an acknowledged message id");
|
|
7037
|
+
}
|
|
6007
7038
|
const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
|
|
6008
7039
|
if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
6009
7040
|
this.log({
|
|
@@ -6151,29 +7182,38 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6151
7182
|
*/
|
|
6152
7183
|
async pollSessionMessagesForRedrive(conv, message, sessionId) {
|
|
6153
7184
|
try {
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
7185
|
+
if (this.isV2) {
|
|
7186
|
+
const messages = await this.getSessionMessages(sessionId);
|
|
7187
|
+
if (messages === null) {
|
|
7188
|
+
this.log({
|
|
7189
|
+
level: "warn",
|
|
7190
|
+
message: `Re-drive: failed to poll V2 session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} \u2014 treating as unreadable this tick`,
|
|
7191
|
+
conversation_id: conv.id,
|
|
7192
|
+
message_id: message.id
|
|
7193
|
+
});
|
|
7194
|
+
return { ok: false, signature: null };
|
|
7195
|
+
}
|
|
7196
|
+
return { ok: true, messages };
|
|
6165
7197
|
}
|
|
6166
|
-
const
|
|
6167
|
-
|
|
7198
|
+
const polledV1 = await pollSessionMessagesForRedrive(
|
|
7199
|
+
this.port,
|
|
7200
|
+
sessionId,
|
|
7201
|
+
this.openCodeClient
|
|
7202
|
+
);
|
|
7203
|
+
if (!polledV1.ok) {
|
|
7204
|
+
const normalized = normalizeRedrivePollFailureBody(polledV1.body);
|
|
6168
7205
|
this.log({
|
|
6169
7206
|
level: "warn",
|
|
6170
|
-
message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned a non-array message body \u2014 treating as unreadable this tick`,
|
|
7207
|
+
message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned ${polledV1.malformed ? "a non-array message body" : `HTTP ${polledV1.status ?? "a network error"}${normalized ? `: ${normalized}` : ""}`} \u2014 treating as unreadable this tick`,
|
|
6171
7208
|
conversation_id: conv.id,
|
|
6172
7209
|
message_id: message.id
|
|
6173
7210
|
});
|
|
6174
|
-
return {
|
|
7211
|
+
return {
|
|
7212
|
+
ok: false,
|
|
7213
|
+
signature: polledV1.status === null && !polledV1.malformed ? null : polledV1.malformed ? "non-array message body" : `HTTP ${polledV1.status}${normalized ? `: ${normalized}` : ""}`
|
|
7214
|
+
};
|
|
6175
7215
|
}
|
|
6176
|
-
return { ok: true, messages:
|
|
7216
|
+
return { ok: true, messages: polledV1.messages };
|
|
6177
7217
|
} catch (err) {
|
|
6178
7218
|
this.log({
|
|
6179
7219
|
level: "warn",
|
|
@@ -6232,11 +7272,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6232
7272
|
}
|
|
6233
7273
|
const state = messageRunState(messages, ocId ?? "");
|
|
6234
7274
|
if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
|
|
6235
|
-
const ongoing = await isSessionOngoing(
|
|
7275
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
6236
7276
|
if (ongoing === false) {
|
|
6237
7277
|
this.log({
|
|
6238
7278
|
level: "info",
|
|
6239
|
-
message: `Re-drive: message ${message.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per
|
|
7279
|
+
message: `Re-drive: message ${message.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check \u2014 restart orphan, re-dispatching instead of marking it permanently failed`,
|
|
6240
7280
|
conversation_id: conv.id,
|
|
6241
7281
|
message_id: message.id
|
|
6242
7282
|
});
|
|
@@ -6249,8 +7289,25 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6249
7289
|
return this.settleRedrive(conv, sessionId, message, ocId, messages, state);
|
|
6250
7290
|
}
|
|
6251
7291
|
if (state === "running" || state === "queued") {
|
|
6252
|
-
const ongoing = await isSessionOngoing(
|
|
7292
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
6253
7293
|
if (ongoing === true) {
|
|
7294
|
+
if (state === "queued") {
|
|
7295
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(
|
|
7296
|
+
this.watchers.get(sessionId),
|
|
7297
|
+
message.id
|
|
7298
|
+
);
|
|
7299
|
+
if (hasLaterSiblingTurnStarted(messages, ocId ?? "", siblingOcIds)) {
|
|
7300
|
+
this.log({
|
|
7301
|
+
level: "warn",
|
|
7302
|
+
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`,
|
|
7303
|
+
conversation_id: conv.id,
|
|
7304
|
+
message_id: message.id
|
|
7305
|
+
});
|
|
7306
|
+
this.clearRedriveUnresolved(message.id);
|
|
7307
|
+
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
7308
|
+
return "dispatch";
|
|
7309
|
+
}
|
|
7310
|
+
}
|
|
6254
7311
|
return this.reattachRedrive(conv, sessionId, message, ocId);
|
|
6255
7312
|
}
|
|
6256
7313
|
if (ongoing === false) {
|
|
@@ -6340,16 +7397,37 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6340
7397
|
if (state === "done") {
|
|
6341
7398
|
const title = await this.resolveSessionTitle(sessionId, conv.id);
|
|
6342
7399
|
const usage = messageUsage(messages, ocId ?? "");
|
|
7400
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
7401
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
7402
|
+
messages,
|
|
7403
|
+
ocId ?? "",
|
|
7404
|
+
message.id
|
|
7405
|
+
);
|
|
6343
7406
|
this.log({
|
|
6344
7407
|
level: "info",
|
|
6345
7408
|
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`,
|
|
6346
7409
|
conversation_id: conv.id,
|
|
6347
7410
|
message_id: message.id
|
|
6348
7411
|
});
|
|
6349
|
-
await this.markDone(
|
|
7412
|
+
await this.markDone(
|
|
7413
|
+
conv.id,
|
|
7414
|
+
message.id,
|
|
7415
|
+
sessionId,
|
|
7416
|
+
ocId,
|
|
7417
|
+
title,
|
|
7418
|
+
usage,
|
|
7419
|
+
usageAgentName,
|
|
7420
|
+
subagentInvocations
|
|
7421
|
+
);
|
|
6350
7422
|
} else {
|
|
6351
7423
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
6352
7424
|
const usage = messageUsage(messages, ocId ?? "");
|
|
7425
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
7426
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
7427
|
+
messages,
|
|
7428
|
+
ocId ?? "",
|
|
7429
|
+
message.id
|
|
7430
|
+
);
|
|
6353
7431
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
6354
7432
|
this.log({
|
|
6355
7433
|
level: "error",
|
|
@@ -6357,7 +7435,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6357
7435
|
conversation_id: conv.id,
|
|
6358
7436
|
message_id: message.id
|
|
6359
7437
|
});
|
|
6360
|
-
await this.markFailed(
|
|
7438
|
+
await this.markFailed(
|
|
7439
|
+
conv.id,
|
|
7440
|
+
message.id,
|
|
7441
|
+
sessionId,
|
|
7442
|
+
error2,
|
|
7443
|
+
usage,
|
|
7444
|
+
failure,
|
|
7445
|
+
usageAgentName,
|
|
7446
|
+
subagentInvocations
|
|
7447
|
+
);
|
|
6361
7448
|
}
|
|
6362
7449
|
if (ocId !== null) {
|
|
6363
7450
|
await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
|
|
@@ -6662,7 +7749,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6662
7749
|
};
|
|
6663
7750
|
}
|
|
6664
7751
|
if (bound) {
|
|
6665
|
-
const exists = await sessionExists(
|
|
7752
|
+
const exists = await this.sessionExists(bound);
|
|
6666
7753
|
if (exists === false) {
|
|
6667
7754
|
this.log({
|
|
6668
7755
|
level: "debug",
|
|
@@ -6695,7 +7782,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6695
7782
|
*/
|
|
6696
7783
|
async createAndBindSession(conversationId) {
|
|
6697
7784
|
const directory = await this.resolveOpenCodeDirectory();
|
|
6698
|
-
const sessionId = await createOpenCodeSession(
|
|
7785
|
+
const sessionId = await this.createOpenCodeSession(directory);
|
|
6699
7786
|
this.sessions.set(conversationId, sessionId);
|
|
6700
7787
|
await this.persistSession(conversationId, sessionId).catch((err) => {
|
|
6701
7788
|
this.log({
|
|
@@ -6707,17 +7794,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6707
7794
|
return sessionId;
|
|
6708
7795
|
}
|
|
6709
7796
|
/**
|
|
6710
|
-
* Lazily resolve (and cache) opencode's root directory via
|
|
7797
|
+
* Lazily resolve (and cache) opencode's root directory via the selected client's
|
|
7798
|
+
* location lookup.
|
|
6711
7799
|
* Resolved once per driver: `undefined` until first lookup, then the directory
|
|
6712
|
-
* string or `null` if unavailable (we don't keep retrying a
|
|
7800
|
+
* string or `null` if unavailable (we don't keep retrying a failed lookup).
|
|
6713
7801
|
*/
|
|
6714
7802
|
async resolveOpenCodeDirectory() {
|
|
6715
7803
|
if (this.opencodeDirectory !== void 0) return this.opencodeDirectory;
|
|
6716
|
-
this.opencodeDirectory = await getOpenCodeDirectory(
|
|
7804
|
+
this.opencodeDirectory = await this.getOpenCodeDirectory();
|
|
6717
7805
|
if (!this.opencodeDirectory) {
|
|
6718
7806
|
this.log({
|
|
6719
7807
|
level: "warn",
|
|
6720
|
-
message: "Could not determine opencode directory (
|
|
7808
|
+
message: "Could not determine opencode directory (location lookup failed) \u2014 new sessions may not appear in opencode web"
|
|
6721
7809
|
});
|
|
6722
7810
|
}
|
|
6723
7811
|
return this.opencodeDirectory;
|
|
@@ -6901,6 +7989,24 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6901
7989
|
ambiguousPinnedSinceMs: 0,
|
|
6902
7990
|
ambiguousResolved: false
|
|
6903
7991
|
});
|
|
7992
|
+
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
7993
|
+
if (!buffered) return;
|
|
7994
|
+
this.bufferedSessionErrors.delete(sessionId);
|
|
7995
|
+
if (this.now() - buffered.receivedAt < SESSION_ERROR_BUFFER_TTL_MS) {
|
|
7996
|
+
this.handleSessionError(buffered.event);
|
|
7997
|
+
}
|
|
7998
|
+
}
|
|
7999
|
+
bufferSessionError(event) {
|
|
8000
|
+
this.bufferedSessionErrors.delete(event.sessionId);
|
|
8001
|
+
this.bufferedSessionErrors.set(event.sessionId, {
|
|
8002
|
+
event,
|
|
8003
|
+
receivedAt: this.now()
|
|
8004
|
+
});
|
|
8005
|
+
while (this.bufferedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
|
|
8006
|
+
const oldest = this.bufferedSessionErrors.keys().next().value;
|
|
8007
|
+
if (typeof oldest !== "string") break;
|
|
8008
|
+
this.bufferedSessionErrors.delete(oldest);
|
|
8009
|
+
}
|
|
6904
8010
|
}
|
|
6905
8011
|
/**
|
|
6906
8012
|
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
@@ -7079,47 +8185,196 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7079
8185
|
this.ensureWatcherRunning(sessionId);
|
|
7080
8186
|
this.log({
|
|
7081
8187
|
level: "warn",
|
|
7082
|
-
message: `Watchdog: session ${sessionId.slice(0, 8)}'s watcher loop had not ticked in ${stalledForMs}ms \u2014 restarted under generation ${watcher.generation} (${watcher.consecutiveStallRestarts}/${MAX_WATCHER_STALL_RESTARTS})`,
|
|
7083
|
-
conversation_id: watcher.conv.id
|
|
8188
|
+
message: `Watchdog: session ${sessionId.slice(0, 8)}'s watcher loop had not ticked in ${stalledForMs}ms \u2014 restarted under generation ${watcher.generation} (${watcher.consecutiveStallRestarts}/${MAX_WATCHER_STALL_RESTARTS})`,
|
|
8189
|
+
conversation_id: watcher.conv.id
|
|
8190
|
+
});
|
|
8191
|
+
for (const evidentMessageId of watcher.inFlight.keys()) {
|
|
8192
|
+
void this.postSignal(watcher.conv.id, evidentMessageId, "watcher_recovered", {
|
|
8193
|
+
recovery: "loop_stalled"
|
|
8194
|
+
});
|
|
8195
|
+
}
|
|
8196
|
+
}
|
|
8197
|
+
}
|
|
8198
|
+
}
|
|
8199
|
+
/**
|
|
8200
|
+
* Start (but do NOT await) the per-session watcher loop if it has in-flight
|
|
8201
|
+
* work and is not already running. Single-flight per session. The loop is
|
|
8202
|
+
* tracked on the watcher and cleared when it settles; it never rejects (fully
|
|
8203
|
+
* guarded), so a failed poll/callback can never crash the run loop — the cron
|
|
8204
|
+
* stays as the safety net.
|
|
8205
|
+
*
|
|
8206
|
+
* The generation started here (#1618) is captured in the `.finally` closure
|
|
8207
|
+
* so a RETIRED loop settling late — after `reconcileWatchers` has already
|
|
8208
|
+
* restarted this watcher under a newer generation — can neither null the new
|
|
8209
|
+
* loop's handle nor delete a watcher that still has live work.
|
|
8210
|
+
*/
|
|
8211
|
+
ensureWatcherRunning(sessionId) {
|
|
8212
|
+
const watcher = this.watchers.get(sessionId);
|
|
8213
|
+
if (!watcher) return;
|
|
8214
|
+
this.ensureSessionErrorStream();
|
|
8215
|
+
if (watcher.loop) return;
|
|
8216
|
+
if (watcher.inFlight.size === 0) {
|
|
8217
|
+
this.watchers.delete(sessionId);
|
|
8218
|
+
return;
|
|
8219
|
+
}
|
|
8220
|
+
const generation = watcher.generation;
|
|
8221
|
+
const loop = this.runWatcherLoop(sessionId, watcher, generation).finally(() => {
|
|
8222
|
+
if (watcher.generation !== generation) return;
|
|
8223
|
+
watcher.loop = null;
|
|
8224
|
+
if (watcher.inFlight.size === 0) {
|
|
8225
|
+
this.watchers.delete(sessionId);
|
|
8226
|
+
}
|
|
8227
|
+
});
|
|
8228
|
+
watcher.loop = loop;
|
|
8229
|
+
}
|
|
8230
|
+
ensureSessionErrorStream() {
|
|
8231
|
+
if (this.sessionErrorStream || this.stopped) return;
|
|
8232
|
+
const abort = new AbortController();
|
|
8233
|
+
const loop = this.runSessionErrorStream(abort.signal);
|
|
8234
|
+
this.sessionErrorStream = { abort, loop };
|
|
8235
|
+
}
|
|
8236
|
+
async runSessionErrorStream(signal) {
|
|
8237
|
+
let attempt = 0;
|
|
8238
|
+
let warned = false;
|
|
8239
|
+
while (!this.stopped && !signal.aborted) {
|
|
8240
|
+
const openedAt = this.now();
|
|
8241
|
+
try {
|
|
8242
|
+
const outcome = await this.readOpenCodeSessionErrorStream({
|
|
8243
|
+
signal,
|
|
8244
|
+
onSessionError: (event) => this.handleSessionError(event)
|
|
8245
|
+
});
|
|
8246
|
+
if (outcome.reason === "aborted" || signal.aborted) return;
|
|
8247
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
8248
|
+
if (outcome.reason === "unavailable" || outcome.reason === "ended") {
|
|
8249
|
+
if (!healthy) {
|
|
8250
|
+
const detail = outcome.reason === "unavailable" ? outcome.detail : "stream ended";
|
|
8251
|
+
this.log({
|
|
8252
|
+
level: warned ? "debug" : "warn",
|
|
8253
|
+
message: `OpenCode session-error stream ${warned ? "still unavailable" : "unavailable"} (${detail}); transcript polling remains the evidence path`
|
|
8254
|
+
});
|
|
8255
|
+
warned = true;
|
|
8256
|
+
}
|
|
8257
|
+
}
|
|
8258
|
+
if (healthy) {
|
|
8259
|
+
if (warned) {
|
|
8260
|
+
this.log({
|
|
8261
|
+
level: "info",
|
|
8262
|
+
message: "OpenCode session-error stream reconnected; transcript polling remains the evidence path"
|
|
8263
|
+
});
|
|
8264
|
+
warned = false;
|
|
8265
|
+
}
|
|
8266
|
+
attempt = 0;
|
|
8267
|
+
} else {
|
|
8268
|
+
attempt += 1;
|
|
8269
|
+
}
|
|
8270
|
+
if (this.stopped || signal.aborted) return;
|
|
8271
|
+
await this.sleep(backoffDelay(healthy ? 0 : attempt - 1, this.retry));
|
|
8272
|
+
} catch (err) {
|
|
8273
|
+
if (this.stopped || signal.aborted) return;
|
|
8274
|
+
this.log({
|
|
8275
|
+
level: "error",
|
|
8276
|
+
message: `OpenCode session-error stream failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
|
|
8277
|
+
});
|
|
8278
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
8279
|
+
const delayAttempt = healthy ? 0 : attempt;
|
|
8280
|
+
attempt = healthy ? 0 : attempt + 1;
|
|
8281
|
+
try {
|
|
8282
|
+
await this.sleep(backoffDelay(delayAttempt, this.retry));
|
|
8283
|
+
} catch (sleepErr) {
|
|
8284
|
+
this.log({
|
|
8285
|
+
level: "error",
|
|
8286
|
+
message: `OpenCode session-error stream backoff failed unexpectedly: ${sleepErr instanceof Error ? sleepErr.message : String(sleepErr)}`
|
|
8287
|
+
});
|
|
8288
|
+
}
|
|
8289
|
+
}
|
|
8290
|
+
}
|
|
8291
|
+
}
|
|
8292
|
+
handleSessionError(event) {
|
|
8293
|
+
try {
|
|
8294
|
+
const watcher = this.watchers.get(event.sessionId);
|
|
8295
|
+
if (!watcher) {
|
|
8296
|
+
this.bufferSessionError(event);
|
|
8297
|
+
this.log({
|
|
8298
|
+
level: "debug",
|
|
8299
|
+
message: `Ignoring session error for unknown session ${event.sessionId.slice(0, 8)}`
|
|
8300
|
+
});
|
|
8301
|
+
return;
|
|
8302
|
+
}
|
|
8303
|
+
if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
|
|
8304
|
+
this.log({
|
|
8305
|
+
level: "debug",
|
|
8306
|
+
message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
|
|
8307
|
+
conversation_id: watcher.conv.id
|
|
8308
|
+
});
|
|
8309
|
+
return;
|
|
8310
|
+
}
|
|
8311
|
+
const inFlight = [...watcher.inFlight.values()].filter((message) => !message.started && !message.done).sort((a, b) => a.dispatchedAt - b.dispatchedAt)[0];
|
|
8312
|
+
if (!inFlight) {
|
|
8313
|
+
this.bufferSessionError(event);
|
|
8314
|
+
this.log({
|
|
8315
|
+
level: "debug",
|
|
8316
|
+
message: `No queued in-flight turn to correlate with session error in ${event.sessionId.slice(0, 8)}`,
|
|
8317
|
+
conversation_id: watcher.conv.id
|
|
8318
|
+
});
|
|
8319
|
+
return;
|
|
8320
|
+
}
|
|
8321
|
+
if (this.sessionErrorHandled.has(inFlight.evidentMessageId)) return;
|
|
8322
|
+
this.sessionErrorHandled.add(inFlight.evidentMessageId);
|
|
8323
|
+
void this.failFromSessionError(watcher, event, inFlight);
|
|
8324
|
+
} catch (err) {
|
|
8325
|
+
this.log({
|
|
8326
|
+
level: "error",
|
|
8327
|
+
message: `Failed to handle OpenCode session error for ${event.sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`
|
|
8328
|
+
});
|
|
8329
|
+
}
|
|
8330
|
+
}
|
|
8331
|
+
async failFromSessionError(watcher, event, inFlight) {
|
|
8332
|
+
try {
|
|
8333
|
+
const messages = await this.getSessionMessages(event.sessionId);
|
|
8334
|
+
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
8335
|
+
if (state !== "queued") {
|
|
8336
|
+
this.log({
|
|
8337
|
+
level: "debug",
|
|
8338
|
+
message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
|
|
8339
|
+
conversation_id: watcher.conv.id,
|
|
8340
|
+
message_id: inFlight.evidentMessageId
|
|
8341
|
+
});
|
|
8342
|
+
return;
|
|
8343
|
+
}
|
|
8344
|
+
this.log({
|
|
8345
|
+
level: "error",
|
|
8346
|
+
message: `OpenCode could not run message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)}: ${event.reason}`,
|
|
8347
|
+
conversation_id: watcher.conv.id,
|
|
8348
|
+
message_id: inFlight.evidentMessageId
|
|
8349
|
+
});
|
|
8350
|
+
await this.markFailed(
|
|
8351
|
+
watcher.conv.id,
|
|
8352
|
+
inFlight.evidentMessageId,
|
|
8353
|
+
event.sessionId,
|
|
8354
|
+
`OpenCode could not run this turn: ${event.reason}`
|
|
8355
|
+
);
|
|
8356
|
+
inFlight.done = true;
|
|
8357
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
8358
|
+
} catch (err) {
|
|
8359
|
+
if (err instanceof ChannelAuthError) {
|
|
8360
|
+
this.log({
|
|
8361
|
+
level: "warn",
|
|
8362
|
+
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`,
|
|
8363
|
+
conversation_id: watcher.conv.id,
|
|
8364
|
+
message_id: inFlight.evidentMessageId
|
|
8365
|
+
});
|
|
8366
|
+
} else {
|
|
8367
|
+
this.log({
|
|
8368
|
+
level: "warn",
|
|
8369
|
+
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`,
|
|
8370
|
+
conversation_id: watcher.conv.id,
|
|
8371
|
+
message_id: inFlight.evidentMessageId
|
|
7084
8372
|
});
|
|
7085
|
-
for (const evidentMessageId of watcher.inFlight.keys()) {
|
|
7086
|
-
void this.postSignal(watcher.conv.id, evidentMessageId, "watcher_recovered", {
|
|
7087
|
-
recovery: "loop_stalled"
|
|
7088
|
-
});
|
|
7089
|
-
}
|
|
7090
8373
|
}
|
|
8374
|
+
} finally {
|
|
8375
|
+
this.sessionErrorHandled.delete(inFlight.evidentMessageId);
|
|
7091
8376
|
}
|
|
7092
8377
|
}
|
|
7093
|
-
/**
|
|
7094
|
-
* Start (but do NOT await) the per-session watcher loop if it has in-flight
|
|
7095
|
-
* work and is not already running. Single-flight per session. The loop is
|
|
7096
|
-
* tracked on the watcher and cleared when it settles; it never rejects (fully
|
|
7097
|
-
* guarded), so a failed poll/callback can never crash the run loop — the cron
|
|
7098
|
-
* stays as the safety net.
|
|
7099
|
-
*
|
|
7100
|
-
* The generation started here (#1618) is captured in the `.finally` closure
|
|
7101
|
-
* so a RETIRED loop settling late — after `reconcileWatchers` has already
|
|
7102
|
-
* restarted this watcher under a newer generation — can neither null the new
|
|
7103
|
-
* loop's handle nor delete a watcher that still has live work.
|
|
7104
|
-
*/
|
|
7105
|
-
ensureWatcherRunning(sessionId) {
|
|
7106
|
-
const watcher = this.watchers.get(sessionId);
|
|
7107
|
-
if (!watcher) return;
|
|
7108
|
-
if (watcher.loop) return;
|
|
7109
|
-
if (watcher.inFlight.size === 0) {
|
|
7110
|
-
this.watchers.delete(sessionId);
|
|
7111
|
-
return;
|
|
7112
|
-
}
|
|
7113
|
-
const generation = watcher.generation;
|
|
7114
|
-
const loop = this.runWatcherLoop(sessionId, watcher, generation).finally(() => {
|
|
7115
|
-
if (watcher.generation !== generation) return;
|
|
7116
|
-
watcher.loop = null;
|
|
7117
|
-
if (watcher.inFlight.size === 0) {
|
|
7118
|
-
this.watchers.delete(sessionId);
|
|
7119
|
-
}
|
|
7120
|
-
});
|
|
7121
|
-
watcher.loop = loop;
|
|
7122
|
-
}
|
|
7123
8378
|
/**
|
|
7124
8379
|
* The per-session polling loop (WI-3). Once per tick it:
|
|
7125
8380
|
* 1. polls `GET /session/:id/message` once and, per in-flight message,
|
|
@@ -7127,7 +8382,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7127
8382
|
* markDone (done) exactly once per transition;
|
|
7128
8383
|
* 2. applies the idle-path re-dispatch guard (a dispatched message that never
|
|
7129
8384
|
* APPEARS → re-dispatch — D1 obligation 2);
|
|
7130
|
-
* 3. polls `/question` + `/permission
|
|
8385
|
+
* 3. polls V1's global `/question` + `/permission`, or V2's
|
|
8386
|
+
* `/api/session/:id/form` + `/api/session/:id/permission`, and surfaces
|
|
7131
8387
|
* NEW ones via `reportInteraction`, carrying the PAUSED message's own
|
|
7132
8388
|
* `source_message_id`;
|
|
7133
8389
|
* 4. drops messages that completed or timed out from the in-flight set.
|
|
@@ -7153,11 +8409,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7153
8409
|
if (watcher.generation !== generation) return;
|
|
7154
8410
|
let messages = null;
|
|
7155
8411
|
try {
|
|
7156
|
-
|
|
7157
|
-
if (res.ok) {
|
|
7158
|
-
const body = await res.json();
|
|
7159
|
-
messages = Array.isArray(body) ? body : null;
|
|
7160
|
-
}
|
|
8412
|
+
messages = await this.getSessionMessages(sessionId);
|
|
7161
8413
|
} catch {
|
|
7162
8414
|
}
|
|
7163
8415
|
if (messages != null && messages.length > 0) {
|
|
@@ -7232,6 +8484,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7232
8484
|
const conv = watcher.conv;
|
|
7233
8485
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7234
8486
|
const id = inFlight.evidentMessageId;
|
|
8487
|
+
if (messages && collectTaskCalls(messages, inFlight.opencodeMessageId).length > 0 && !this.subagentInvocationPrefetches.has(id)) {
|
|
8488
|
+
void this.resolveSubagentInvocations(
|
|
8489
|
+
messages,
|
|
8490
|
+
inFlight.opencodeMessageId,
|
|
8491
|
+
id,
|
|
8492
|
+
"prefetch"
|
|
8493
|
+
).catch((err) => {
|
|
8494
|
+
this.log({
|
|
8495
|
+
level: "warn",
|
|
8496
|
+
message: `Best-effort subagent usage prefetch failed for message ${id.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
8497
|
+
conversation_id: conv.id,
|
|
8498
|
+
message_id: id
|
|
8499
|
+
});
|
|
8500
|
+
});
|
|
8501
|
+
}
|
|
7235
8502
|
if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
|
|
7236
8503
|
else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
|
|
7237
8504
|
if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
|
|
@@ -7285,6 +8552,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7285
8552
|
message_id: inFlight.evidentMessageId
|
|
7286
8553
|
});
|
|
7287
8554
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
8555
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
8556
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
8557
|
+
messages,
|
|
8558
|
+
inFlight.opencodeMessageId,
|
|
8559
|
+
inFlight.evidentMessageId
|
|
8560
|
+
);
|
|
7288
8561
|
const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
|
|
7289
8562
|
try {
|
|
7290
8563
|
await this.markFailed(
|
|
@@ -7293,7 +8566,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7293
8566
|
sessionId,
|
|
7294
8567
|
error2,
|
|
7295
8568
|
usage,
|
|
7296
|
-
failure
|
|
8569
|
+
failure,
|
|
8570
|
+
usageAgentName,
|
|
8571
|
+
subagentInvocations
|
|
7297
8572
|
);
|
|
7298
8573
|
} catch (err) {
|
|
7299
8574
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7336,9 +8611,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7336
8611
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7337
8612
|
return;
|
|
7338
8613
|
}
|
|
8614
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
|
|
8615
|
+
const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
|
|
7339
8616
|
const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
|
|
7340
8617
|
const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
|
|
7341
|
-
if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
|
|
8618
|
+
if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
|
|
7342
8619
|
inFlight.stuckReported = true;
|
|
7343
8620
|
void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
|
|
7344
8621
|
stuck_for_ms: this.now() - inFlight.dispatchedAt
|
|
@@ -7365,7 +8642,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7365
8642
|
inFlight.b2LastDescendantCheckMs = this.now();
|
|
7366
8643
|
const [descendantOngoing, rootOngoing] = await Promise.all([
|
|
7367
8644
|
this.isAnyDescendantSessionOngoing(sessionId),
|
|
7368
|
-
isSessionOngoing(
|
|
8645
|
+
this.isSessionOngoing(sessionId)
|
|
7369
8646
|
]);
|
|
7370
8647
|
if (isB2AbandonmentConfirmed({
|
|
7371
8648
|
pinnedForMs,
|
|
@@ -7425,7 +8702,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7425
8702
|
});
|
|
7426
8703
|
}
|
|
7427
8704
|
const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;
|
|
7428
|
-
const ongoing = await isSessionOngoing(
|
|
8705
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
7429
8706
|
if (isAmbiguousFinishResolved({
|
|
7430
8707
|
pinnedForMs,
|
|
7431
8708
|
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
@@ -7499,7 +8776,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7499
8776
|
const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
|
|
7500
8777
|
(sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
|
|
7501
8778
|
);
|
|
7502
|
-
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
8779
|
+
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
|
|
7503
8780
|
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
7504
8781
|
this.log({
|
|
7505
8782
|
level: "debug",
|
|
@@ -7534,6 +8811,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7534
8811
|
});
|
|
7535
8812
|
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
7536
8813
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
8814
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
8815
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
8816
|
+
messages,
|
|
8817
|
+
inFlight.opencodeMessageId,
|
|
8818
|
+
inFlight.evidentMessageId
|
|
8819
|
+
);
|
|
7537
8820
|
try {
|
|
7538
8821
|
await this.markDone(
|
|
7539
8822
|
conv.id,
|
|
@@ -7541,7 +8824,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7541
8824
|
sessionId,
|
|
7542
8825
|
inFlight.opencodeMessageId,
|
|
7543
8826
|
title,
|
|
7544
|
-
usage
|
|
8827
|
+
usage,
|
|
8828
|
+
usageAgentName,
|
|
8829
|
+
subagentInvocations
|
|
7545
8830
|
);
|
|
7546
8831
|
} catch (err) {
|
|
7547
8832
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7600,7 +8885,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7600
8885
|
*/
|
|
7601
8886
|
async readoptProcessing() {
|
|
7602
8887
|
const rows = await this.getProcessingMessages();
|
|
7603
|
-
if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
|
|
8888
|
+
if (this.dontRedispatch.size > 0 || this.untrackableAck.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
|
|
7604
8889
|
const stillProcessing = new Set(rows.map((r) => r.id));
|
|
7605
8890
|
for (const id of [
|
|
7606
8891
|
...this.dontRedispatch,
|
|
@@ -7620,6 +8905,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7620
8905
|
}
|
|
7621
8906
|
}
|
|
7622
8907
|
}
|
|
8908
|
+
for (const id of [...this.untrackableAck]) {
|
|
8909
|
+
if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
|
|
8910
|
+
this.untrackableAck.delete(id);
|
|
8911
|
+
this.log({
|
|
8912
|
+
level: "debug",
|
|
8913
|
+
message: `Re-adopt: message ${id.slice(0, 8)} left processing and pending \u2014 cleared untrackable-ack fence`,
|
|
8914
|
+
message_id: id
|
|
8915
|
+
});
|
|
8916
|
+
}
|
|
8917
|
+
}
|
|
7623
8918
|
}
|
|
7624
8919
|
if (rows.length === 0) return;
|
|
7625
8920
|
const bySession = /* @__PURE__ */ new Map();
|
|
@@ -7640,23 +8935,32 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7640
8935
|
for (const [sessionId, sessionRows] of bySession) {
|
|
7641
8936
|
let messages;
|
|
7642
8937
|
try {
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
8938
|
+
if (this.isV2) {
|
|
8939
|
+
const snapshot = await this.getSessionMessages(sessionId);
|
|
8940
|
+
if (snapshot === null) {
|
|
8941
|
+
this.log({
|
|
8942
|
+
level: "warn",
|
|
8943
|
+
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
|
|
8944
|
+
});
|
|
8945
|
+
continue;
|
|
8946
|
+
}
|
|
8947
|
+
messages = snapshot;
|
|
8948
|
+
} else {
|
|
8949
|
+
const polled = await pollSessionMessagesForRedrive(
|
|
8950
|
+
this.port,
|
|
8951
|
+
sessionId,
|
|
8952
|
+
this.openCodeClient
|
|
8953
|
+
);
|
|
8954
|
+
if (!polled.ok) {
|
|
8955
|
+
const normalized = normalizeRedrivePollFailureBody(polled.body);
|
|
8956
|
+
this.log({
|
|
8957
|
+
level: "warn",
|
|
8958
|
+
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned ${polled.malformed ? "a non-array message body" : `HTTP ${polled.status ?? "a network error"}${normalized ? `: ${normalized}` : ""}`} \u2014 skipping this session this tick`
|
|
8959
|
+
});
|
|
8960
|
+
continue;
|
|
8961
|
+
}
|
|
8962
|
+
messages = polled.messages;
|
|
7658
8963
|
}
|
|
7659
|
-
messages = body;
|
|
7660
8964
|
} catch (err) {
|
|
7661
8965
|
this.log({
|
|
7662
8966
|
level: "warn",
|
|
@@ -7665,7 +8969,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7665
8969
|
continue;
|
|
7666
8970
|
}
|
|
7667
8971
|
const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
|
|
7668
|
-
const sessionOngoing = anyUntracked ? await isSessionOngoing(
|
|
8972
|
+
const sessionOngoing = anyUntracked ? await this.isSessionOngoing(sessionId) : null;
|
|
7669
8973
|
for (const row of sessionRows) {
|
|
7670
8974
|
await this.readoptOne(sessionId, row, messages, sessionOngoing);
|
|
7671
8975
|
}
|
|
@@ -7712,7 +9016,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7712
9016
|
if (restartAborted) {
|
|
7713
9017
|
this.log({
|
|
7714
9018
|
level: "info",
|
|
7715
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per
|
|
9019
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check \u2014 restart orphan, re-dispatching instead of marking it permanently failed`,
|
|
7716
9020
|
conversation_id: row.conversation_id,
|
|
7717
9021
|
message_id: row.id
|
|
7718
9022
|
});
|
|
@@ -7720,6 +9024,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7720
9024
|
if (state === "failed" && !restartAborted) {
|
|
7721
9025
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
7722
9026
|
const usage = messageUsage(messages, ocId ?? "");
|
|
9027
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
9028
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
9029
|
+
messages,
|
|
9030
|
+
ocId ?? "",
|
|
9031
|
+
row.id
|
|
9032
|
+
);
|
|
7723
9033
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
7724
9034
|
this.log({
|
|
7725
9035
|
level: "error",
|
|
@@ -7728,7 +9038,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7728
9038
|
message_id: row.id
|
|
7729
9039
|
});
|
|
7730
9040
|
try {
|
|
7731
|
-
await this.markFailed(
|
|
9041
|
+
await this.markFailed(
|
|
9042
|
+
row.conversation_id,
|
|
9043
|
+
row.id,
|
|
9044
|
+
sessionId,
|
|
9045
|
+
error2,
|
|
9046
|
+
usage,
|
|
9047
|
+
failure,
|
|
9048
|
+
usageAgentName,
|
|
9049
|
+
subagentInvocations
|
|
9050
|
+
);
|
|
7732
9051
|
} catch (err) {
|
|
7733
9052
|
if (err instanceof ChannelAuthError) throw err;
|
|
7734
9053
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7752,10 +9071,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7752
9071
|
}
|
|
7753
9072
|
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
7754
9073
|
this.dontRedispatch.delete(row.id);
|
|
9074
|
+
this.untrackableAck.delete(row.id);
|
|
7755
9075
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
7756
9076
|
return;
|
|
7757
9077
|
}
|
|
7758
|
-
if (this.dontRedispatch.has(row.id)) {
|
|
9078
|
+
if (this.dontRedispatch.has(row.id) || this.untrackableAck.has(row.id)) {
|
|
7759
9079
|
this.log({
|
|
7760
9080
|
level: "debug",
|
|
7761
9081
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
@@ -7775,7 +9095,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7775
9095
|
const finish = reply?.info?.finish ?? reply?.finish;
|
|
7776
9096
|
this.log({
|
|
7777
9097
|
level: "info",
|
|
7778
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} has an ambiguous finish ("${finish ?? "(absent)"}") but session ${sessionId.slice(0, 8)} is confirmed not-ongoing per
|
|
9098
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} has an ambiguous finish ("${finish ?? "(absent)"}") but session ${sessionId.slice(0, 8)} is confirmed not-ongoing per the active-session status check \u2014 delivering the existing reply instead of re-dispatching`,
|
|
7779
9099
|
conversation_id: row.conversation_id,
|
|
7780
9100
|
message_id: row.id
|
|
7781
9101
|
});
|
|
@@ -7784,7 +9104,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7784
9104
|
}
|
|
7785
9105
|
this.log({
|
|
7786
9106
|
level: "info",
|
|
7787
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per
|
|
9107
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check (absent/idle) \u2014 re-dispatching from scratch (status-gated recovery)`,
|
|
7788
9108
|
conversation_id: row.conversation_id,
|
|
7789
9109
|
message_id: row.id
|
|
7790
9110
|
});
|
|
@@ -7794,7 +9114,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7794
9114
|
if (ongoing === true) {
|
|
7795
9115
|
this.log({
|
|
7796
9116
|
level: "debug",
|
|
7797
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per
|
|
9117
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per the active-session status check (busy/retry) \u2014 re-attaching watcher (no re-dispatch)`,
|
|
7798
9118
|
conversation_id: row.conversation_id,
|
|
7799
9119
|
message_id: row.id
|
|
7800
9120
|
});
|
|
@@ -7802,7 +9122,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7802
9122
|
if (shape === "b1") {
|
|
7803
9123
|
this.log({
|
|
7804
9124
|
level: "debug",
|
|
7805
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but
|
|
9125
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but the active-session status check was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 NOT latching a b1 row on a transient status blip; leaving it un-tracked to re-evaluate on the next drain`,
|
|
7806
9126
|
conversation_id: row.conversation_id,
|
|
7807
9127
|
message_id: row.id
|
|
7808
9128
|
});
|
|
@@ -7814,7 +9134,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7814
9134
|
}
|
|
7815
9135
|
this.log({
|
|
7816
9136
|
level: "debug",
|
|
7817
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but
|
|
9137
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but the active-session status check was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 falling back to the #253 preamble + descendant cross-check`,
|
|
7818
9138
|
conversation_id: row.conversation_id,
|
|
7819
9139
|
message_id: row.id
|
|
7820
9140
|
});
|
|
@@ -7864,7 +9184,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7864
9184
|
* extracted (#1493 Task 2.4) so the ambiguous-finish guard above can call the
|
|
7865
9185
|
* SAME delivery instead of duplicating it.
|
|
7866
9186
|
*
|
|
7867
|
-
* EVEN IF the row was previously parked
|
|
9187
|
+
* EVEN IF the row was previously parked by either recovery fence (a give-up stops
|
|
7868
9188
|
* re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY like the watcher's
|
|
7869
9189
|
* `settleMessageDone`: auth re-throws; terminal → park in `doneUndeliverable` +
|
|
7870
9190
|
* leave for cron; transient → log + leave for the next drain (the still-
|
|
@@ -7890,7 +9210,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7890
9210
|
try {
|
|
7891
9211
|
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
7892
9212
|
const usage = messageUsage(messages, ocId ?? "");
|
|
7893
|
-
|
|
9213
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
9214
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
9215
|
+
messages,
|
|
9216
|
+
ocId ?? "",
|
|
9217
|
+
row.id
|
|
9218
|
+
);
|
|
9219
|
+
await this.markDone(
|
|
9220
|
+
row.conversation_id,
|
|
9221
|
+
row.id,
|
|
9222
|
+
sessionId,
|
|
9223
|
+
ocId,
|
|
9224
|
+
title,
|
|
9225
|
+
usage,
|
|
9226
|
+
usageAgentName,
|
|
9227
|
+
subagentInvocations
|
|
9228
|
+
);
|
|
7894
9229
|
} catch (err) {
|
|
7895
9230
|
if (err instanceof ChannelAuthError) throw err;
|
|
7896
9231
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7916,6 +9251,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7916
9251
|
await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
|
|
7917
9252
|
}
|
|
7918
9253
|
this.dontRedispatch.delete(row.id);
|
|
9254
|
+
this.untrackableAck.delete(row.id);
|
|
7919
9255
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
7920
9256
|
}
|
|
7921
9257
|
/**
|
|
@@ -7979,19 +9315,90 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7979
9315
|
conversation_id: row.conversation_id,
|
|
7980
9316
|
message_id: row.id
|
|
7981
9317
|
});
|
|
7982
|
-
this.awaitingReadopt.add(row.id);
|
|
9318
|
+
if (!this.isV2) this.awaitingReadopt.add(row.id);
|
|
7983
9319
|
const readoptConv = this.convForRow(sessionId, row);
|
|
7984
9320
|
const readoptMessage = this.queuedMessageForRow(row);
|
|
7985
|
-
const sendAttachments = this.buildSendAttachments(readoptConv, readoptMessage);
|
|
9321
|
+
const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(readoptConv, readoptMessage);
|
|
9322
|
+
if (this.isV2 && row.attachments && row.attachments.length > 0) {
|
|
9323
|
+
this.signalAttachmentsSkipped(
|
|
9324
|
+
row.conversation_id,
|
|
9325
|
+
row.id,
|
|
9326
|
+
row.attachments.map((attachment, index) => ({
|
|
9327
|
+
index,
|
|
9328
|
+
mime: attachment.mime,
|
|
9329
|
+
...attachment.filename ? { filename: attachment.filename } : {},
|
|
9330
|
+
status: "skipped"
|
|
9331
|
+
})),
|
|
9332
|
+
false
|
|
9333
|
+
);
|
|
9334
|
+
}
|
|
7986
9335
|
let ocId;
|
|
7987
9336
|
try {
|
|
7988
|
-
ocId = await this.dispatchLocked(
|
|
9337
|
+
ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
|
|
7989
9338
|
sessionId,
|
|
7990
|
-
() => sendPromptAsync(
|
|
9339
|
+
() => sendPromptAsync(
|
|
9340
|
+
this.port,
|
|
9341
|
+
sessionId,
|
|
9342
|
+
row.content,
|
|
9343
|
+
options,
|
|
9344
|
+
sendAttachments,
|
|
9345
|
+
this.openCodeClient
|
|
9346
|
+
)
|
|
7991
9347
|
);
|
|
7992
9348
|
} catch (err) {
|
|
7993
9349
|
this.awaitingReadopt.delete(row.id);
|
|
7994
9350
|
if (err instanceof ChannelAuthError) throw err;
|
|
9351
|
+
if (this.isV2) {
|
|
9352
|
+
const errorMessage3 = err instanceof Error ? err.message : String(err);
|
|
9353
|
+
const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
|
|
9354
|
+
if (!invalidPromptAck) {
|
|
9355
|
+
const exists = await this.sessionExists(sessionId);
|
|
9356
|
+
if (exists === false) {
|
|
9357
|
+
this.log({
|
|
9358
|
+
level: "warn",
|
|
9359
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was deleted mid-dispatch \u2014 deferring to the next drain: ${errorMessage3}`,
|
|
9360
|
+
conversation_id: row.conversation_id,
|
|
9361
|
+
message_id: row.id
|
|
9362
|
+
});
|
|
9363
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
9364
|
+
return;
|
|
9365
|
+
}
|
|
9366
|
+
if (exists === null) {
|
|
9367
|
+
this.log({
|
|
9368
|
+
level: "warn",
|
|
9369
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) existence could not be confirmed \u2014 deferring to the next drain: ${errorMessage3}`,
|
|
9370
|
+
conversation_id: row.conversation_id,
|
|
9371
|
+
message_id: row.id
|
|
9372
|
+
});
|
|
9373
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
9374
|
+
return;
|
|
9375
|
+
}
|
|
9376
|
+
} else {
|
|
9377
|
+
this.untrackableAck.add(row.id);
|
|
9378
|
+
}
|
|
9379
|
+
this.log({
|
|
9380
|
+
level: "error",
|
|
9381
|
+
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage3}`,
|
|
9382
|
+
conversation_id: row.conversation_id,
|
|
9383
|
+
message_id: row.id
|
|
9384
|
+
});
|
|
9385
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
|
|
9386
|
+
(markErr) => {
|
|
9387
|
+
this.log({
|
|
9388
|
+
level: "warn",
|
|
9389
|
+
message: `markFailed PATCH for V2 re-adopt dispatch failure on message ${row.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
9390
|
+
conversation_id: row.conversation_id,
|
|
9391
|
+
message_id: row.id
|
|
9392
|
+
});
|
|
9393
|
+
if (invalidPromptAck) {
|
|
9394
|
+
void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
|
|
9395
|
+
} else {
|
|
9396
|
+
this.signalDispatchNotStarted(readoptConv, readoptMessage, "failure_unreported");
|
|
9397
|
+
}
|
|
9398
|
+
}
|
|
9399
|
+
);
|
|
9400
|
+
return;
|
|
9401
|
+
}
|
|
7995
9402
|
this.log({
|
|
7996
9403
|
level: "warn",
|
|
7997
9404
|
message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -8002,6 +9409,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8002
9409
|
return;
|
|
8003
9410
|
}
|
|
8004
9411
|
if (ocId === null) {
|
|
9412
|
+
if (this.isV2) {
|
|
9413
|
+
this.untrackableAck.add(row.id);
|
|
9414
|
+
this.log({
|
|
9415
|
+
level: "error",
|
|
9416
|
+
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} completed without an acknowledged message id`,
|
|
9417
|
+
conversation_id: row.conversation_id,
|
|
9418
|
+
message_id: row.id
|
|
9419
|
+
});
|
|
9420
|
+
void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
|
|
9421
|
+
return;
|
|
9422
|
+
}
|
|
8005
9423
|
this.awaitingReadopt.delete(row.id);
|
|
8006
9424
|
const streak = this.recordUnconfirmedDispatch(row.id, sessionId);
|
|
8007
9425
|
if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
@@ -8123,12 +9541,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8123
9541
|
this.dispatched.delete(evidentMessageId);
|
|
8124
9542
|
}
|
|
8125
9543
|
/**
|
|
8126
|
-
* Poll `/question` + `/permission
|
|
8127
|
-
* via `reportInteraction` (Task 3.5),
|
|
8128
|
-
* `source_message_id` so the server @mentions
|
|
8129
|
-
* concurrency. Dedups by interaction id across ticks
|
|
9544
|
+
* Poll V1's global `/question` + `/permission`, or V2's watched-session form and
|
|
9545
|
+
* permission routes, and surface NEW ones via `reportInteraction` (Task 3.5),
|
|
9546
|
+
* carrying the PAUSED message's own `source_message_id` so the server @mentions
|
|
9547
|
+
* the correct person under concurrency. Dedups by interaction id across ticks
|
|
9548
|
+
* (reused per-session sets).
|
|
8130
9549
|
*
|
|
8131
|
-
* The interaction is attributed to the in-flight message it paused on.
|
|
9550
|
+
* The interaction is attributed to the in-flight message it paused on. OpenCode
|
|
8132
9551
|
* stamps a `messageID` on a permission (and `tool.messageID` on a question) =
|
|
8133
9552
|
* the assistant message id, whose `parentID` is the user message id — but the
|
|
8134
9553
|
* simplest robust attribution here is: the single in-flight message that is
|
|
@@ -8151,16 +9570,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8151
9570
|
let permissionsPolledOk = true;
|
|
8152
9571
|
let questions = [];
|
|
8153
9572
|
try {
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
questions = body;
|
|
8159
|
-
} else {
|
|
8160
|
-
questionsPolledOk = false;
|
|
8161
|
-
}
|
|
9573
|
+
if (this.isV2) {
|
|
9574
|
+
const forms = await listV2Forms(this.openCodeClient, sessionId);
|
|
9575
|
+
if (forms === null) questionsPolledOk = false;
|
|
9576
|
+
else questions = forms;
|
|
8162
9577
|
} else {
|
|
8163
|
-
|
|
9578
|
+
const listed = await listOpenCodeQuestions(this.port, this.openCodeClient);
|
|
9579
|
+
if (listed === null) questionsPolledOk = false;
|
|
9580
|
+
else questions = listed;
|
|
8164
9581
|
}
|
|
8165
9582
|
} catch {
|
|
8166
9583
|
questionsPolledOk = false;
|
|
@@ -8180,16 +9597,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8180
9597
|
}
|
|
8181
9598
|
let permissions = [];
|
|
8182
9599
|
try {
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
permissions = body;
|
|
8188
|
-
} else {
|
|
8189
|
-
permissionsPolledOk = false;
|
|
8190
|
-
}
|
|
9600
|
+
if (this.isV2) {
|
|
9601
|
+
const listed = await listV2Permissions(this.openCodeClient, sessionId);
|
|
9602
|
+
if (listed === null) permissionsPolledOk = false;
|
|
9603
|
+
else permissions = listed;
|
|
8191
9604
|
} else {
|
|
8192
|
-
|
|
9605
|
+
const listed = await listOpenCodePermissions(this.port, this.openCodeClient);
|
|
9606
|
+
if (listed === null) permissionsPolledOk = false;
|
|
9607
|
+
else permissions = listed;
|
|
8193
9608
|
}
|
|
8194
9609
|
} catch {
|
|
8195
9610
|
permissionsPolledOk = false;
|
|
@@ -8283,10 +9698,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8283
9698
|
if (cached !== void 0) return cached;
|
|
8284
9699
|
let parent = void 0;
|
|
8285
9700
|
try {
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
9701
|
+
if (this.isV2) {
|
|
9702
|
+
const session = await getV2Session(this.openCodeClient, sessionId);
|
|
9703
|
+
parent = null;
|
|
9704
|
+
const candidate = session.parentID;
|
|
9705
|
+
if (typeof candidate === "string") parent = candidate;
|
|
9706
|
+
} else {
|
|
9707
|
+
const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
|
|
9708
|
+
parent = typeof body?.parentID === "string" ? body.parentID : body === null ? void 0 : null;
|
|
8290
9709
|
}
|
|
8291
9710
|
} catch {
|
|
8292
9711
|
parent = void 0;
|
|
@@ -8294,6 +9713,164 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8294
9713
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
8295
9714
|
return parent;
|
|
8296
9715
|
}
|
|
9716
|
+
usageAgentName(messages, userMessageId) {
|
|
9717
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
9718
|
+
const mode = reply?.info?.mode;
|
|
9719
|
+
if (typeof mode === "string" && mode.length > 0) return mode;
|
|
9720
|
+
const agent = reply?.info?.agent;
|
|
9721
|
+
return typeof agent === "string" && agent.length > 0 ? agent : null;
|
|
9722
|
+
}
|
|
9723
|
+
async resolveSubagentInvocations(messages, userMessageId, messageId, phase = "terminal") {
|
|
9724
|
+
if (!messages) return void 0;
|
|
9725
|
+
const cache = phase === "prefetch" ? this.subagentInvocationPrefetches : this.subagentInvocationCollections;
|
|
9726
|
+
const cached = cache.get(messageId);
|
|
9727
|
+
if (cached) return cached;
|
|
9728
|
+
const collection = this.buildSubagentInvocations(messages, userMessageId, messageId).catch(
|
|
9729
|
+
(err) => {
|
|
9730
|
+
this.log({
|
|
9731
|
+
level: "warn",
|
|
9732
|
+
message: `Best-effort subagent usage collection failed for message ${messageId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
9733
|
+
message_id: messageId
|
|
9734
|
+
});
|
|
9735
|
+
return void 0;
|
|
9736
|
+
}
|
|
9737
|
+
);
|
|
9738
|
+
cache.set(messageId, collection);
|
|
9739
|
+
const result = await collection;
|
|
9740
|
+
if (result === void 0 && cache.get(messageId) === collection) cache.delete(messageId);
|
|
9741
|
+
return result;
|
|
9742
|
+
}
|
|
9743
|
+
clearSubagentInvocationCaches(messageId) {
|
|
9744
|
+
this.subagentInvocationCollections.delete(messageId);
|
|
9745
|
+
this.subagentInvocationPrefetches.delete(messageId);
|
|
9746
|
+
}
|
|
9747
|
+
async buildSubagentInvocations(messages, userMessageId, messageId) {
|
|
9748
|
+
const rootCalls = collectTaskCalls(messages, userMessageId);
|
|
9749
|
+
if (rootCalls.length === 0) return void 0;
|
|
9750
|
+
const childMessages = /* @__PURE__ */ new Map();
|
|
9751
|
+
const seenCallIds = new Set(rootCalls.map((call) => call.callID));
|
|
9752
|
+
const work = rootCalls.map((call) => ({
|
|
9753
|
+
call,
|
|
9754
|
+
depth: 1
|
|
9755
|
+
}));
|
|
9756
|
+
const payload = [];
|
|
9757
|
+
const fetchChildMessages = (sessionId) => {
|
|
9758
|
+
const cached = childMessages.get(sessionId);
|
|
9759
|
+
if (cached) return cached;
|
|
9760
|
+
const pending = (async () => {
|
|
9761
|
+
try {
|
|
9762
|
+
const messages2 = await this.getTelemetrySubagentSessionMessages(sessionId);
|
|
9763
|
+
if (messages2 === null) {
|
|
9764
|
+
this.log({
|
|
9765
|
+
level: "warn",
|
|
9766
|
+
message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} was unreadable \u2014 omitting invocation telemetry`,
|
|
9767
|
+
message_id: messageId
|
|
9768
|
+
});
|
|
9769
|
+
return null;
|
|
9770
|
+
}
|
|
9771
|
+
return messages2;
|
|
9772
|
+
} catch (err) {
|
|
9773
|
+
this.log({
|
|
9774
|
+
level: "warn",
|
|
9775
|
+
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)}`,
|
|
9776
|
+
message_id: messageId
|
|
9777
|
+
});
|
|
9778
|
+
return null;
|
|
9779
|
+
}
|
|
9780
|
+
})();
|
|
9781
|
+
childMessages.set(sessionId, pending);
|
|
9782
|
+
return pending;
|
|
9783
|
+
};
|
|
9784
|
+
const fetchChildWithoutBlocking = async (sessionId) => {
|
|
9785
|
+
const pending = fetchChildMessages(sessionId);
|
|
9786
|
+
let timer;
|
|
9787
|
+
const timeout = new Promise((resolve4) => {
|
|
9788
|
+
timer = setTimeout(() => {
|
|
9789
|
+
this.log({
|
|
9790
|
+
level: "warn",
|
|
9791
|
+
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`,
|
|
9792
|
+
message_id: messageId
|
|
9793
|
+
});
|
|
9794
|
+
resolve4(null);
|
|
9795
|
+
}, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
|
|
9796
|
+
});
|
|
9797
|
+
try {
|
|
9798
|
+
return await Promise.race([pending, timeout]);
|
|
9799
|
+
} finally {
|
|
9800
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
9801
|
+
}
|
|
9802
|
+
};
|
|
9803
|
+
while (work.length > 0) {
|
|
9804
|
+
const groups = /* @__PURE__ */ new Map();
|
|
9805
|
+
for (const item of work.splice(0)) {
|
|
9806
|
+
const group = groups.get(item.call.childSessionId) ?? [];
|
|
9807
|
+
group.push(item);
|
|
9808
|
+
groups.set(item.call.childSessionId, group);
|
|
9809
|
+
}
|
|
9810
|
+
const groupResults = await Promise.all(
|
|
9811
|
+
[...groups].map(async ([sessionId, items]) => ({
|
|
9812
|
+
sessionId,
|
|
9813
|
+
items,
|
|
9814
|
+
messages: sessionId === null ? [] : await fetchChildWithoutBlocking(sessionId)
|
|
9815
|
+
}))
|
|
9816
|
+
);
|
|
9817
|
+
for (const { sessionId, items, messages: child } of groupResults) {
|
|
9818
|
+
if (sessionId !== null && child === null) continue;
|
|
9819
|
+
const attribution = sessionId === null ? { invocations: [], unattributed: [] } : attributeTaskCallUsage(
|
|
9820
|
+
child,
|
|
9821
|
+
items.map(({ call }) => ({
|
|
9822
|
+
callID: call.callID,
|
|
9823
|
+
timeStart: call.timeStart,
|
|
9824
|
+
timeEnd: call.timeEnd
|
|
9825
|
+
}))
|
|
9826
|
+
);
|
|
9827
|
+
if (sessionId !== null && attribution.unattributed.length > 0) {
|
|
9828
|
+
this.log({
|
|
9829
|
+
level: "warn",
|
|
9830
|
+
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`,
|
|
9831
|
+
message_id: messageId
|
|
9832
|
+
});
|
|
9833
|
+
}
|
|
9834
|
+
const usageByCall = new Map(
|
|
9835
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.usage])
|
|
9836
|
+
);
|
|
9837
|
+
const messagesByCall = new Map(
|
|
9838
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.messages])
|
|
9839
|
+
);
|
|
9840
|
+
for (const { call, depth } of items) {
|
|
9841
|
+
const usage = usageByCall.get(call.callID) ?? null;
|
|
9842
|
+
payload.push({
|
|
9843
|
+
tool_call_id: call.callID,
|
|
9844
|
+
agent_name: call.subagentName,
|
|
9845
|
+
opencode_session_id: call.childSessionId,
|
|
9846
|
+
parent_opencode_session_id: call.parentSessionId,
|
|
9847
|
+
depth,
|
|
9848
|
+
status: call.status,
|
|
9849
|
+
started_at: call.timeStart === null ? null : new Date(call.timeStart).toISOString(),
|
|
9850
|
+
ended_at: call.timeEnd === null ? null : new Date(call.timeEnd).toISOString(),
|
|
9851
|
+
usage_provider_id: usage?.usage_provider_id ?? call.model?.providerID ?? null,
|
|
9852
|
+
usage_model_id: usage?.usage_model_id ?? call.model?.modelID ?? null,
|
|
9853
|
+
usage_tokens_input: usage?.usage_tokens_input ?? null,
|
|
9854
|
+
usage_tokens_output: usage?.usage_tokens_output ?? null,
|
|
9855
|
+
usage_tokens_reasoning: usage?.usage_tokens_reasoning ?? null,
|
|
9856
|
+
usage_tokens_cache_read: usage?.usage_tokens_cache_read ?? null,
|
|
9857
|
+
usage_tokens_cache_write: usage?.usage_tokens_cache_write ?? null,
|
|
9858
|
+
usage_cost_usd: usage?.usage_cost_usd ?? null
|
|
9859
|
+
});
|
|
9860
|
+
for (const assigned of messagesByCall.get(call.callID) ?? []) {
|
|
9861
|
+
const parentId = assigned.info?.parentID ?? assigned.parentID;
|
|
9862
|
+
if (!parentId) continue;
|
|
9863
|
+
for (const nested of collectTaskCalls([assigned], parentId)) {
|
|
9864
|
+
if (seenCallIds.has(nested.callID)) continue;
|
|
9865
|
+
seenCallIds.add(nested.callID);
|
|
9866
|
+
work.push({ call: nested, depth: depth + 1 });
|
|
9867
|
+
}
|
|
9868
|
+
}
|
|
9869
|
+
}
|
|
9870
|
+
}
|
|
9871
|
+
}
|
|
9872
|
+
return payload.length > 0 ? payload : void 0;
|
|
9873
|
+
}
|
|
8297
9874
|
/**
|
|
8298
9875
|
* OpenCode's synchronous default session title (e.g.
|
|
8299
9876
|
* `"New session - 1737800000000"`), assigned immediately when a session is
|
|
@@ -8332,21 +9909,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8332
9909
|
const cached = this.sessionTitles.get(sessionId);
|
|
8333
9910
|
if (cached != null) return cached;
|
|
8334
9911
|
try {
|
|
8335
|
-
|
|
8336
|
-
if (
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
return title;
|
|
8342
|
-
}
|
|
8343
|
-
return null;
|
|
9912
|
+
let title = "";
|
|
9913
|
+
if (this.isV2) {
|
|
9914
|
+
title = (await getV2Session(this.openCodeClient, sessionId)).title?.trim() ?? "";
|
|
9915
|
+
} else {
|
|
9916
|
+
const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
|
|
9917
|
+
title = typeof body?.title === "string" ? body.title.trim() : "";
|
|
8344
9918
|
}
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
9919
|
+
if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
|
|
9920
|
+
this.sessionTitles.set(sessionId, title);
|
|
9921
|
+
return title;
|
|
9922
|
+
}
|
|
9923
|
+
return null;
|
|
8350
9924
|
} catch (err) {
|
|
8351
9925
|
this.log({
|
|
8352
9926
|
level: "debug",
|
|
@@ -8444,7 +10018,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8444
10018
|
* `SessionStatus` only.
|
|
8445
10019
|
*/
|
|
8446
10020
|
async isAnyDescendantSessionAlive(rootSessionId) {
|
|
8447
|
-
const sessions = await listSessions(
|
|
10021
|
+
const sessions = await this.listSessions();
|
|
8448
10022
|
if (!sessions) {
|
|
8449
10023
|
this.log({
|
|
8450
10024
|
level: "warn",
|
|
@@ -8455,7 +10029,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8455
10029
|
for (const candidate of sessions) {
|
|
8456
10030
|
if (!candidate?.id || candidate.id === rootSessionId) continue;
|
|
8457
10031
|
if (!await this.sessionBelongsTo(candidate.id, rootSessionId)) continue;
|
|
8458
|
-
const childMsgs = await
|
|
10032
|
+
const childMsgs = await this.getSubagentSessionMessages(candidate.id);
|
|
8459
10033
|
if (isSessionActivelyGenerating(childMsgs)) {
|
|
8460
10034
|
return true;
|
|
8461
10035
|
}
|
|
@@ -8517,7 +10091,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8517
10091
|
* `isB2AbandonmentConfirmed`.
|
|
8518
10092
|
*/
|
|
8519
10093
|
async isAnyDescendantSessionOngoing(rootSessionId) {
|
|
8520
|
-
const sessions = await listSessions(
|
|
10094
|
+
const sessions = await this.listSessions();
|
|
8521
10095
|
if (!sessions) {
|
|
8522
10096
|
this.log({
|
|
8523
10097
|
level: "warn",
|
|
@@ -8534,7 +10108,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8534
10108
|
continue;
|
|
8535
10109
|
}
|
|
8536
10110
|
if (membership === false) continue;
|
|
8537
|
-
const ongoing = await isSessionOngoing(
|
|
10111
|
+
const ongoing = await this.isSessionOngoing(candidate.id);
|
|
8538
10112
|
if (ongoing === true) return true;
|
|
8539
10113
|
if (ongoing === null) indeterminate = true;
|
|
8540
10114
|
}
|
|
@@ -8777,7 +10351,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8777
10351
|
* watcher retries next tick within the
|
|
8778
10352
|
* deadline, Finding 4).
|
|
8779
10353
|
*/
|
|
8780
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
|
|
10354
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
|
|
8781
10355
|
const res = await this.fetchImpl(
|
|
8782
10356
|
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
8783
10357
|
{
|
|
@@ -8793,15 +10367,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8793
10367
|
opencode_session_id: sessionId,
|
|
8794
10368
|
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
8795
10369
|
...title ? { title } : {},
|
|
8796
|
-
...usage ? usage : {}
|
|
10370
|
+
...usage ? usage : {},
|
|
10371
|
+
...usageAgentName ? { usage_agent_name: usageAgentName } : {},
|
|
10372
|
+
...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
|
|
8797
10373
|
})
|
|
8798
10374
|
}
|
|
8799
10375
|
);
|
|
8800
10376
|
this.assertAuth(res, "marking message as done");
|
|
8801
|
-
if (res.ok)
|
|
10377
|
+
if (res.ok) {
|
|
10378
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
10379
|
+
return;
|
|
10380
|
+
}
|
|
8802
10381
|
if (isRetryableStatus(res.status)) {
|
|
8803
10382
|
throw new Error(`marking message as done: HTTP ${res.status}`);
|
|
8804
10383
|
}
|
|
10384
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8805
10385
|
throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
|
|
8806
10386
|
}
|
|
8807
10387
|
/**
|
|
@@ -8816,7 +10396,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8816
10396
|
* exists but is wedged, so the next attempt must get a fresh one
|
|
8817
10397
|
* instead of reusing it — see WI-1's server-side null-clearing PATCH).
|
|
8818
10398
|
*/
|
|
8819
|
-
async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
|
|
10399
|
+
async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
|
|
8820
10400
|
const body = { status: "failed" };
|
|
8821
10401
|
if (sessionId === null) {
|
|
8822
10402
|
body.opencode_session_id = null;
|
|
@@ -8825,23 +10405,33 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8825
10405
|
}
|
|
8826
10406
|
if (error2 !== void 0) body.error = error2;
|
|
8827
10407
|
if (usage) Object.assign(body, usage);
|
|
10408
|
+
if (usageAgentName) body.usage_agent_name = usageAgentName;
|
|
10409
|
+
if (subagentInvocations && subagentInvocations.length > 0) {
|
|
10410
|
+
body.subagent_invocations = subagentInvocations;
|
|
10411
|
+
}
|
|
8828
10412
|
if (failure) {
|
|
8829
10413
|
body.failure_kind = failure.kind;
|
|
8830
10414
|
body.failure_provider_id = failure.providerId;
|
|
8831
10415
|
body.failure_model_id = failure.modelId;
|
|
8832
10416
|
body.failure_reason = failure.reason;
|
|
8833
10417
|
}
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
10418
|
+
try {
|
|
10419
|
+
await this.callWithRetry(
|
|
10420
|
+
"marking message as failed",
|
|
10421
|
+
() => this.fetchImpl(
|
|
10422
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
10423
|
+
{
|
|
10424
|
+
method: "PATCH",
|
|
10425
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
10426
|
+
body: JSON.stringify(body)
|
|
10427
|
+
}
|
|
10428
|
+
)
|
|
10429
|
+
);
|
|
10430
|
+
} catch (err) {
|
|
10431
|
+
if (err instanceof ChannelTerminalError) this.clearSubagentInvocationCaches(messageId);
|
|
10432
|
+
throw err;
|
|
10433
|
+
}
|
|
10434
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8845
10435
|
}
|
|
8846
10436
|
/**
|
|
8847
10437
|
* Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
|
|
@@ -8858,7 +10448,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8858
10448
|
const classified = messageFailure(messages, userMessageId);
|
|
8859
10449
|
if (classified != null) return classified;
|
|
8860
10450
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
8861
|
-
const hasProvider = await hasAnyConfiguredProvider(
|
|
10451
|
+
const hasProvider = await this.hasAnyConfiguredProvider();
|
|
8862
10452
|
return applyZeroProviderFallback(
|
|
8863
10453
|
classified,
|
|
8864
10454
|
hasProvider,
|
|
@@ -8931,7 +10521,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8931
10521
|
const succeededProviders = /* @__PURE__ */ new Set();
|
|
8932
10522
|
for (const ref of refs) {
|
|
8933
10523
|
try {
|
|
8934
|
-
const childMessages = await
|
|
10524
|
+
const childMessages = await this.getSubagentSessionMessages(ref.sessionId);
|
|
8935
10525
|
if (childMessages === null) {
|
|
8936
10526
|
this.log({
|
|
8937
10527
|
level: "debug",
|
|
@@ -9262,6 +10852,7 @@ Port ${port} is already in use.`));
|
|
|
9262
10852
|
|
|
9263
10853
|
// src/commands/ensure-opencode-v2.ts
|
|
9264
10854
|
import chalk6 from "chalk";
|
|
10855
|
+
import ora3 from "ora";
|
|
9265
10856
|
import { select as select3 } from "@inquirer/prompts";
|
|
9266
10857
|
async function probeOpenCode2WithoutPassword(port) {
|
|
9267
10858
|
try {
|
|
@@ -9287,11 +10878,7 @@ function unknownPasswordError(port) {
|
|
|
9287
10878
|
`OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
|
|
9288
10879
|
);
|
|
9289
10880
|
}
|
|
9290
|
-
|
|
9291
|
-
return new Error(
|
|
9292
|
-
"OpenCode V2 (opencode2) session support is incomplete \u2014 see https://github.com/sroze/evident/issues/2075. Not starting a new opencode2 process."
|
|
9293
|
-
);
|
|
9294
|
-
}
|
|
10881
|
+
var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
|
|
9295
10882
|
async function ensureOpenCode2Running(ctx) {
|
|
9296
10883
|
const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
|
|
9297
10884
|
if (initialHealth.authFailed) {
|
|
@@ -9334,13 +10921,37 @@ Port ${port} is already in use.`));
|
|
|
9334
10921
|
}
|
|
9335
10922
|
}
|
|
9336
10923
|
if (!ctx.interactive) {
|
|
9337
|
-
|
|
10924
|
+
ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
|
|
10925
|
+
const { child: proc, password } = await startOpenCode2(port, {
|
|
10926
|
+
inheritStdio: ctx.inheritStdio
|
|
10927
|
+
});
|
|
10928
|
+
const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
|
|
10929
|
+
if (!health.healthy) {
|
|
10930
|
+
return {
|
|
10931
|
+
port,
|
|
10932
|
+
process: proc,
|
|
10933
|
+
version: null,
|
|
10934
|
+
notReadyReason: `it did not become healthy within ${Math.round(ctx.startTimeoutMs / 1e3)}s`,
|
|
10935
|
+
password
|
|
10936
|
+
};
|
|
10937
|
+
}
|
|
10938
|
+
ctx.log(`OpenCode V2 started on port ${port}${health.version ? ` (v${health.version})` : ""}`);
|
|
10939
|
+
return {
|
|
10940
|
+
port,
|
|
10941
|
+
process: proc,
|
|
10942
|
+
version: health.version ?? null,
|
|
10943
|
+
notReadyReason: null,
|
|
10944
|
+
password
|
|
10945
|
+
};
|
|
9338
10946
|
}
|
|
9339
|
-
console.log(chalk6.yellow(`
|
|
9340
|
-
${v2SessionSupportIncompleteError().message}`));
|
|
9341
10947
|
const action = await select3({
|
|
9342
10948
|
message: "OpenCode V2 is not running. What would you like to do?",
|
|
9343
10949
|
choices: [
|
|
10950
|
+
{
|
|
10951
|
+
name: "Start OpenCode V2 for me",
|
|
10952
|
+
value: "start",
|
|
10953
|
+
description: `Run 'opencode2 serve --port ${port}'`
|
|
10954
|
+
},
|
|
9344
10955
|
{
|
|
9345
10956
|
name: "Show me the command",
|
|
9346
10957
|
value: "manual",
|
|
@@ -9361,6 +10972,25 @@ ${v2SessionSupportIncompleteError().message}`));
|
|
|
9361
10972
|
blank();
|
|
9362
10973
|
throw new Error("Please start OpenCode V2 manually");
|
|
9363
10974
|
}
|
|
10975
|
+
if (action === "start") {
|
|
10976
|
+
const spinner = ora3("Starting OpenCode V2...").start();
|
|
10977
|
+
const { child: proc, password } = await startOpenCode2(port, {
|
|
10978
|
+
inheritStdio: ctx.inheritStdio
|
|
10979
|
+
});
|
|
10980
|
+
const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
|
|
10981
|
+
if (!health.healthy) {
|
|
10982
|
+
spinner.fail("Failed to start OpenCode V2");
|
|
10983
|
+
throw new Error("OpenCode V2 failed to start");
|
|
10984
|
+
}
|
|
10985
|
+
spinner.stop();
|
|
10986
|
+
return {
|
|
10987
|
+
port,
|
|
10988
|
+
process: proc,
|
|
10989
|
+
version: health.version ?? null,
|
|
10990
|
+
notReadyReason: null,
|
|
10991
|
+
password
|
|
10992
|
+
};
|
|
10993
|
+
}
|
|
9364
10994
|
return {
|
|
9365
10995
|
port,
|
|
9366
10996
|
process: null,
|
|
@@ -10254,7 +11884,7 @@ async function driveChannels(state, driver) {
|
|
|
10254
11884
|
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
10255
11885
|
if (opencodeAuthApplied) state.openaiUsageRearm?.();
|
|
10256
11886
|
if (claudeCredentialApplied || opencodeAuthApplied) {
|
|
10257
|
-
void reloadProviderCache(state.port).catch(
|
|
11887
|
+
void reloadProviderCache(state.port, state.opencodeClient ?? void 0).catch(
|
|
10258
11888
|
(error2) => logActivity(state, {
|
|
10259
11889
|
type: "error",
|
|
10260
11890
|
error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
@@ -10379,7 +12009,7 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
|
|
|
10379
12009
|
async function runSweep(state, driver, config) {
|
|
10380
12010
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
10381
12011
|
try {
|
|
10382
|
-
const sessions = await listSessions(state.port);
|
|
12012
|
+
const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
|
|
10383
12013
|
if (sessions === null) {
|
|
10384
12014
|
logActivity(state, {
|
|
10385
12015
|
type: "info",
|
|
@@ -10409,7 +12039,7 @@ async function runSweep(state, driver, config) {
|
|
|
10409
12039
|
});
|
|
10410
12040
|
continue;
|
|
10411
12041
|
}
|
|
10412
|
-
if (await deleteSession(state.port, id)) deleted++;
|
|
12042
|
+
if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
|
|
10413
12043
|
else failed++;
|
|
10414
12044
|
}
|
|
10415
12045
|
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
@@ -10637,14 +12267,11 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
10637
12267
|
report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
|
|
10638
12268
|
isLocalCredentialProblem,
|
|
10639
12269
|
forcedOnHint: "run `claude` to sign in",
|
|
10640
|
-
firstDelayMs:
|
|
10641
|
-
nextDelayMs:
|
|
10642
|
-
failureLogLevel:
|
|
12270
|
+
firstDelayMs: firstReportDelayMs,
|
|
12271
|
+
nextDelayMs: usageReportDelayMs,
|
|
12272
|
+
failureLogLevel: usageReportFailureLogLevel
|
|
10643
12273
|
});
|
|
10644
12274
|
}
|
|
10645
|
-
var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
|
|
10646
|
-
var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
10647
|
-
var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
10648
12275
|
function scheduleResourceUsageReporting(state, options) {
|
|
10649
12276
|
const { enabled, warnings } = resolveResourceUsageReportingEnabled(
|
|
10650
12277
|
options.resourceUsageReporting,
|
|
@@ -10697,10 +12324,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10697
12324
|
consecutiveFailures++;
|
|
10698
12325
|
logActivity(state, {
|
|
10699
12326
|
type: "info",
|
|
10700
|
-
level:
|
|
10701
|
-
consecutiveFailures,
|
|
10702
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
10703
|
-
),
|
|
12327
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
10704
12328
|
message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
|
|
10705
12329
|
});
|
|
10706
12330
|
}
|
|
@@ -10709,20 +12333,11 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10709
12333
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
10710
12334
|
logActivity(state, {
|
|
10711
12335
|
type: "info",
|
|
10712
|
-
level:
|
|
10713
|
-
consecutiveFailures,
|
|
10714
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
10715
|
-
),
|
|
12336
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
10716
12337
|
message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
|
|
10717
12338
|
});
|
|
10718
12339
|
} finally {
|
|
10719
|
-
state.resourceUsageTimer = setTimeout(
|
|
10720
|
-
() => void tick(),
|
|
10721
|
-
jitteredDelayMs(
|
|
10722
|
-
RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
|
|
10723
|
-
RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
|
|
10724
|
-
)
|
|
10725
|
-
);
|
|
12340
|
+
state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
|
|
10726
12341
|
}
|
|
10727
12342
|
};
|
|
10728
12343
|
state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
|
|
@@ -10931,6 +12546,9 @@ async function run(options) {
|
|
|
10931
12546
|
connected: false,
|
|
10932
12547
|
opencodeConnected: false,
|
|
10933
12548
|
opencodeVersion: null,
|
|
12549
|
+
opencodeApiVersion: "v1",
|
|
12550
|
+
opencodePassword: null,
|
|
12551
|
+
opencodeClient: null,
|
|
10934
12552
|
sessionDbProvenanceAnomaly: false,
|
|
10935
12553
|
opencodeProcess: null,
|
|
10936
12554
|
stopOpenCodeLogTail: null,
|
|
@@ -11085,7 +12703,7 @@ async function run(options) {
|
|
|
11085
12703
|
console.log(chalk7.bold("Evident Run"));
|
|
11086
12704
|
console.log(chalk7.dim("-".repeat(40)));
|
|
11087
12705
|
}
|
|
11088
|
-
const spinner = interactive && !state.json ?
|
|
12706
|
+
const spinner = interactive && !state.json ? ora4("Validating runner...").start() : null;
|
|
11089
12707
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
11090
12708
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
11091
12709
|
spinner?.fail("Authentication failed");
|
|
@@ -11206,7 +12824,7 @@ async function run(options) {
|
|
|
11206
12824
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
11207
12825
|
}
|
|
11208
12826
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
11209
|
-
const ocSpinner = interactive && !state.json ?
|
|
12827
|
+
const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
|
|
11210
12828
|
try {
|
|
11211
12829
|
const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
|
|
11212
12830
|
port: state.port,
|
|
@@ -11226,6 +12844,19 @@ async function run(options) {
|
|
|
11226
12844
|
state.port = oc.port;
|
|
11227
12845
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
11228
12846
|
state.opencodeVersion = oc.version;
|
|
12847
|
+
state.opencodeApiVersion = opencodeVersion;
|
|
12848
|
+
let opencodePassword = null;
|
|
12849
|
+
if (opencodeVersion === "v2" && "password" in oc) {
|
|
12850
|
+
const value = oc.password;
|
|
12851
|
+
if (typeof value === "string" || value === null) opencodePassword = value;
|
|
12852
|
+
}
|
|
12853
|
+
state.opencodePassword = opencodePassword;
|
|
12854
|
+
const openCodeClient = createOpenCodeClient({
|
|
12855
|
+
port: state.port,
|
|
12856
|
+
version: state.opencodeApiVersion,
|
|
12857
|
+
password: state.opencodePassword
|
|
12858
|
+
});
|
|
12859
|
+
state.opencodeClient = openCodeClient;
|
|
11229
12860
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
11230
12861
|
try {
|
|
11231
12862
|
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
@@ -11262,15 +12893,19 @@ async function run(options) {
|
|
|
11262
12893
|
const message = `OpenCode is not ready on port ${state.port}: ${oc.notReadyReason}. The runner will still come online, but messages will fail until opencode answers \u2014 raise the wait with --opencode-start-timeout <seconds> (env ${OPENCODE_START_TIMEOUT_ENV}).`;
|
|
11263
12894
|
logActivity(state, { type: "info", level: "warn", message });
|
|
11264
12895
|
} else {
|
|
11265
|
-
const versionWarning = buildOpenCodeVersionWarning(
|
|
12896
|
+
const versionWarning = buildOpenCodeVersionWarning(
|
|
12897
|
+
state.opencodeVersion,
|
|
12898
|
+
state.opencodeApiVersion
|
|
12899
|
+
);
|
|
11266
12900
|
if (versionWarning) {
|
|
11267
12901
|
log2(state, versionWarning, "warn");
|
|
11268
12902
|
if (state.interactive && !state.json) {
|
|
11269
12903
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
11270
12904
|
}
|
|
11271
12905
|
}
|
|
12906
|
+
await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
|
|
11272
12907
|
const noProviderWarning = buildNoProviderWarning(
|
|
11273
|
-
await hasAnyConfiguredProvider(state.port)
|
|
12908
|
+
await hasAnyConfiguredProvider(state.port, state.opencodeClient ?? void 0)
|
|
11274
12909
|
);
|
|
11275
12910
|
if (noProviderWarning) {
|
|
11276
12911
|
log2(state, noProviderWarning, "warn");
|
|
@@ -11393,11 +13028,12 @@ async function run(options) {
|
|
|
11393
13028
|
});
|
|
11394
13029
|
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
11395
13030
|
}
|
|
11396
|
-
const tunnelSpinner = interactive && !state.json ?
|
|
13031
|
+
const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
|
|
11397
13032
|
const channelDriver = new ChannelDriver({
|
|
11398
13033
|
agentId: state.agentId,
|
|
11399
13034
|
port: state.port,
|
|
11400
13035
|
apiUrl: getApiUrlConfig(),
|
|
13036
|
+
openCodeClient: state.opencodeClient ?? void 0,
|
|
11401
13037
|
getAuthHeader: () => state.authHeader,
|
|
11402
13038
|
conversationFilter: state.conversationFilter,
|
|
11403
13039
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
@@ -11423,6 +13059,7 @@ async function run(options) {
|
|
|
11423
13059
|
agentId: state.agentId,
|
|
11424
13060
|
getAuthHeader: () => state.authHeader,
|
|
11425
13061
|
port: state.port,
|
|
13062
|
+
openCodePassword: state.opencodePassword,
|
|
11426
13063
|
isRunning: () => state.running,
|
|
11427
13064
|
events: {
|
|
11428
13065
|
onConnected: (agentId, isReconnect) => {
|
|
@@ -11447,7 +13084,11 @@ async function run(options) {
|
|
|
11447
13084
|
emitAgentConnected(state.agentId, {
|
|
11448
13085
|
port: state.port,
|
|
11449
13086
|
cli_version: getCliVersion(),
|
|
11450
|
-
opencode_version:
|
|
13087
|
+
opencode_version: reportedOpenCodeVersion({
|
|
13088
|
+
version: state.opencodeVersion,
|
|
13089
|
+
major: state.opencodeApiVersion,
|
|
13090
|
+
connected: state.opencodeConnected
|
|
13091
|
+
})
|
|
11451
13092
|
});
|
|
11452
13093
|
if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
|
|
11453
13094
|
if (state.interactive) displayStatus(state);
|
|
@@ -11563,7 +13204,7 @@ async function run(options) {
|
|
|
11563
13204
|
state.openaiUsageTimer = timer;
|
|
11564
13205
|
},
|
|
11565
13206
|
fetchUsage: async () => {
|
|
11566
|
-
const usage = await getOpenAiUsage(state.port);
|
|
13207
|
+
const usage = await getOpenAiUsage(state.port, state.opencodeClient ?? void 0);
|
|
11567
13208
|
if (usage.subscription === null) {
|
|
11568
13209
|
logActivity(state, {
|
|
11569
13210
|
type: "info",
|