@evident-ai/cli 3.4.1-dev.325472f → 3.4.1-dev.388b76b
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 +12 -0
- package/dist/index.js +2619 -615
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,8 +11,8 @@ import chalk2 from "chalk";
|
|
|
11
11
|
|
|
12
12
|
// src/lib/config.ts
|
|
13
13
|
import Conf from "conf";
|
|
14
|
-
import { chmodSync, existsSync, statSync } from "fs";
|
|
15
|
-
import { dirname } from "path";
|
|
14
|
+
import { chmodSync, existsSync, statSync } from "node:fs";
|
|
15
|
+
import { dirname } from "node:path";
|
|
16
16
|
var PRODUCTION_API_URL = "https://api.production.evident.run/v1";
|
|
17
17
|
var PRODUCTION_TUNNEL_URL = "wss://tunnel.production.evident.run";
|
|
18
18
|
var defaults = {
|
|
@@ -623,6 +623,19 @@ function isInteractive(jsonOutput) {
|
|
|
623
623
|
return true;
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
+
// src/lib/subscription-usage-report.ts
|
|
627
|
+
function toReportedSubscription(collected) {
|
|
628
|
+
if (!collected) return null;
|
|
629
|
+
if (collected.ownerEmail === null && collected.planType === null && collected.organizationName === null) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
owner_email: collected.ownerEmail,
|
|
634
|
+
plan_type: collected.planType,
|
|
635
|
+
organization_name: collected.organizationName
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
626
639
|
// src/commands/agent-lookup.ts
|
|
627
640
|
async function readErrorMessage(response) {
|
|
628
641
|
const text = await response.text().catch(() => "");
|
|
@@ -670,12 +683,15 @@ async function resolveAgentIdFromKey(authHeader) {
|
|
|
670
683
|
}
|
|
671
684
|
}
|
|
672
685
|
var BEST_EFFORT_NOTIFY_TIMEOUT_MS = 2e3;
|
|
673
|
-
async function
|
|
674
|
-
const apiUrl = getApiUrlConfig();
|
|
686
|
+
async function postBestEffort(path, authHeader, body) {
|
|
675
687
|
try {
|
|
676
|
-
const
|
|
688
|
+
const apiUrl = getApiUrlConfig();
|
|
689
|
+
const headers = { Authorization: authHeader };
|
|
690
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
691
|
+
const response = await fetch(`${apiUrl}${path}`, {
|
|
677
692
|
method: "POST",
|
|
678
|
-
headers
|
|
693
|
+
headers,
|
|
694
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
679
695
|
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
680
696
|
});
|
|
681
697
|
if (!response.ok) {
|
|
@@ -687,73 +703,32 @@ async function notifyAgentDisconnected(agentId, authHeader) {
|
|
|
687
703
|
}
|
|
688
704
|
return { ok: true };
|
|
689
705
|
} catch (error2) {
|
|
690
|
-
return { ok: false, error:
|
|
706
|
+
return { ok: false, error: describeTimeoutError(error2, BEST_EFFORT_NOTIFY_TIMEOUT_MS) };
|
|
691
707
|
}
|
|
692
708
|
}
|
|
693
|
-
function
|
|
709
|
+
function describeTimeoutError(error2, timeoutMs) {
|
|
694
710
|
const name = error2?.name;
|
|
695
711
|
if (name === "TimeoutError" || name === "AbortError") {
|
|
696
|
-
return `timed out after ${
|
|
712
|
+
return `timed out after ${timeoutMs}ms`;
|
|
697
713
|
}
|
|
698
714
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
699
715
|
}
|
|
716
|
+
async function notifyAgentDisconnected(agentId, authHeader) {
|
|
717
|
+
return postBestEffort(`/runners/${agentId}/disconnect`, authHeader);
|
|
718
|
+
}
|
|
700
719
|
async function reportMicrovmId(agentId, authHeader, microvmId) {
|
|
701
|
-
|
|
702
|
-
const apiUrl = getApiUrlConfig();
|
|
703
|
-
const response = await fetch(`${apiUrl}/runners/${agentId}/microvm`, {
|
|
704
|
-
method: "POST",
|
|
705
|
-
headers: { Authorization: authHeader, "Content-Type": "application/json" },
|
|
706
|
-
body: JSON.stringify({ microvm_id: microvmId }),
|
|
707
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
708
|
-
});
|
|
709
|
-
if (!response.ok) {
|
|
710
|
-
const serverMessage = await readErrorMessage(response);
|
|
711
|
-
return {
|
|
712
|
-
ok: false,
|
|
713
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
return { ok: true };
|
|
717
|
-
} catch (error2) {
|
|
718
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
719
|
-
}
|
|
720
|
+
return postBestEffort(`/runners/${agentId}/microvm`, authHeader, { microvm_id: microvmId });
|
|
720
721
|
}
|
|
721
722
|
function toReportedWindow(window) {
|
|
722
723
|
if (!window) return null;
|
|
723
724
|
return { utilization: window.utilization, resets_at: window.resetsAt };
|
|
724
725
|
}
|
|
725
|
-
function toReportedOwner(snapshot) {
|
|
726
|
-
if (!snapshot.owner) return null;
|
|
727
|
-
return {
|
|
728
|
-
email: snapshot.owner.email,
|
|
729
|
-
organization_name: snapshot.owner.organizationName,
|
|
730
|
-
rate_limit_tier: snapshot.owner.rateLimitTier
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
726
|
async function reportClaudeUsage(agentId, authHeader, snapshot) {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
body: JSON.stringify({
|
|
740
|
-
five_hour: toReportedWindow(snapshot.fiveHour),
|
|
741
|
-
seven_day: toReportedWindow(snapshot.sevenDay),
|
|
742
|
-
owner: toReportedOwner(snapshot)
|
|
743
|
-
}),
|
|
744
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
745
|
-
});
|
|
746
|
-
if (!response.ok) {
|
|
747
|
-
const serverMessage = await readErrorMessage(response);
|
|
748
|
-
return {
|
|
749
|
-
ok: false,
|
|
750
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
return { ok: true };
|
|
754
|
-
} catch (error2) {
|
|
755
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
756
|
-
}
|
|
727
|
+
return postBestEffort(`/runners/${agentId}/claude-usage`, authHeader, {
|
|
728
|
+
five_hour: toReportedWindow(snapshot.fiveHour),
|
|
729
|
+
seven_day: toReportedWindow(snapshot.sevenDay),
|
|
730
|
+
subscription: toReportedSubscription(snapshot.subscription)
|
|
731
|
+
});
|
|
757
732
|
}
|
|
758
733
|
function toReportedOpenAiWindow(window) {
|
|
759
734
|
if (!window) return null;
|
|
@@ -763,69 +738,26 @@ function toReportedOpenAiWindow(window) {
|
|
|
763
738
|
resets_at: window.resetsAt
|
|
764
739
|
};
|
|
765
740
|
}
|
|
766
|
-
function toReportedOpenAiSubscription(snapshot) {
|
|
767
|
-
if (!snapshot.subscription) return null;
|
|
768
|
-
return {
|
|
769
|
-
owner_email: snapshot.subscription.ownerEmail,
|
|
770
|
-
plan_type: snapshot.subscription.planType
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
741
|
async function reportOpenAiUsage(agentId, authHeader, snapshot) {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
782
|
-
has_credits: snapshot.hasCredits,
|
|
783
|
-
credits_unlimited: snapshot.creditsUnlimited,
|
|
784
|
-
subscription: toReportedOpenAiSubscription(snapshot)
|
|
785
|
-
}),
|
|
786
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
787
|
-
});
|
|
788
|
-
if (!response.ok) {
|
|
789
|
-
const serverMessage = await readErrorMessage(response);
|
|
790
|
-
return {
|
|
791
|
-
ok: false,
|
|
792
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
return { ok: true };
|
|
796
|
-
} catch (error2) {
|
|
797
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
798
|
-
}
|
|
742
|
+
return postBestEffort(`/runners/${agentId}/openai-usage`, authHeader, {
|
|
743
|
+
primary: toReportedOpenAiWindow(snapshot.primary),
|
|
744
|
+
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
745
|
+
has_credits: snapshot.hasCredits,
|
|
746
|
+
credits_unlimited: snapshot.creditsUnlimited,
|
|
747
|
+
subscription: toReportedSubscription(snapshot.subscription)
|
|
748
|
+
});
|
|
799
749
|
}
|
|
800
750
|
async function reportResourceUsage(agentId, authHeader, usage) {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
memory_available_bytes: usage.memoryAvailableBytes,
|
|
812
|
-
disk_total_bytes: usage.diskTotalBytes,
|
|
813
|
-
disk_free_bytes: usage.diskFreeBytes,
|
|
814
|
-
opencode_db_bytes: usage.opencodeDbBytes
|
|
815
|
-
}),
|
|
816
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
817
|
-
});
|
|
818
|
-
if (!response.ok) {
|
|
819
|
-
const serverMessage = await readErrorMessage(response);
|
|
820
|
-
return {
|
|
821
|
-
ok: false,
|
|
822
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
return { ok: true };
|
|
826
|
-
} catch (error2) {
|
|
827
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
828
|
-
}
|
|
751
|
+
return postBestEffort(`/runners/${agentId}/resource-usage`, authHeader, {
|
|
752
|
+
cpu_percent: usage.cpuPercent,
|
|
753
|
+
cpu_peak_percent: usage.cpuPeakPercent,
|
|
754
|
+
cpu_count: usage.cpuCount,
|
|
755
|
+
memory_total_bytes: usage.memoryTotalBytes,
|
|
756
|
+
memory_available_bytes: usage.memoryAvailableBytes,
|
|
757
|
+
disk_total_bytes: usage.diskTotalBytes,
|
|
758
|
+
disk_free_bytes: usage.diskFreeBytes,
|
|
759
|
+
opencode_db_bytes: usage.opencodeDbBytes
|
|
760
|
+
});
|
|
829
761
|
}
|
|
830
762
|
async function getAgentInfo(agentId, authHeader) {
|
|
831
763
|
const apiUrl = getApiUrlConfig();
|
|
@@ -877,13 +809,6 @@ function authLabelFor(credentials2) {
|
|
|
877
809
|
}
|
|
878
810
|
return "user token";
|
|
879
811
|
}
|
|
880
|
-
function describeFetchError(error2) {
|
|
881
|
-
const name = error2?.name;
|
|
882
|
-
if (name === "TimeoutError" || name === "AbortError") {
|
|
883
|
-
return `timed out after ${STATUS_TIMEOUT_MS}ms waiting for a response`;
|
|
884
|
-
}
|
|
885
|
-
return error2 instanceof Error ? error2.message : String(error2);
|
|
886
|
-
}
|
|
887
812
|
async function checkStatus(jsonMode) {
|
|
888
813
|
const apiUrl = getApiUrlConfig();
|
|
889
814
|
const credentials2 = await getAuthCredentials();
|
|
@@ -911,7 +836,7 @@ async function checkStatus(jsonMode) {
|
|
|
911
836
|
endpoint: apiUrl,
|
|
912
837
|
authLabel: authLabelFor(credentials2),
|
|
913
838
|
reason: "unreachable",
|
|
914
|
-
error: `Could not reach ${apiUrl}: ${
|
|
839
|
+
error: `Could not reach ${apiUrl}: ${describeTimeoutError(error2, STATUS_TIMEOUT_MS)}. The credentials were NOT validated.`,
|
|
915
840
|
exitCode: 75
|
|
916
841
|
};
|
|
917
842
|
}
|
|
@@ -1009,10 +934,10 @@ async function status(options = {}) {
|
|
|
1009
934
|
}
|
|
1010
935
|
|
|
1011
936
|
// src/lib/claude-usage.ts
|
|
1012
|
-
import { execFileSync } from "child_process";
|
|
1013
|
-
import { readFileSync } from "fs";
|
|
1014
|
-
import { homedir } from "os";
|
|
1015
|
-
import { join } from "path";
|
|
937
|
+
import { execFileSync } from "node:child_process";
|
|
938
|
+
import { readFileSync } from "node:fs";
|
|
939
|
+
import { homedir } from "node:os";
|
|
940
|
+
import { join } from "node:path";
|
|
1016
941
|
var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
1017
942
|
var CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
1018
943
|
var CLAUDE_PROFILE_TIMEOUT_MS = 2e3;
|
|
@@ -1097,7 +1022,7 @@ function ownerLookupFailure(error2) {
|
|
|
1097
1022
|
}
|
|
1098
1023
|
async function getClaudeUsageOwner(accessToken) {
|
|
1099
1024
|
if (cachedOwner?.accessToken === accessToken) {
|
|
1100
|
-
return {
|
|
1025
|
+
return { subscription: cachedOwner.owner, ownerLookupError: null };
|
|
1101
1026
|
}
|
|
1102
1027
|
try {
|
|
1103
1028
|
const response = await fetch(CLAUDE_PROFILE_URL, {
|
|
@@ -1109,27 +1034,27 @@ async function getClaudeUsageOwner(accessToken) {
|
|
|
1109
1034
|
signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
|
|
1110
1035
|
});
|
|
1111
1036
|
if (!response.ok) {
|
|
1112
|
-
return {
|
|
1037
|
+
return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
|
|
1113
1038
|
}
|
|
1114
1039
|
let body;
|
|
1115
1040
|
try {
|
|
1116
1041
|
body = await response.json();
|
|
1117
1042
|
} catch (error2) {
|
|
1118
|
-
return {
|
|
1043
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1119
1044
|
}
|
|
1120
1045
|
const profile = body;
|
|
1121
1046
|
if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
|
|
1122
|
-
return {
|
|
1047
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1123
1048
|
}
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1049
|
+
const subscription = {
|
|
1050
|
+
ownerEmail: profile.account.email,
|
|
1126
1051
|
organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
|
|
1127
|
-
|
|
1052
|
+
planType: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
|
|
1128
1053
|
};
|
|
1129
|
-
cachedOwner = { accessToken, owner };
|
|
1130
|
-
return {
|
|
1054
|
+
cachedOwner = { accessToken, owner: subscription };
|
|
1055
|
+
return { subscription, ownerLookupError: null };
|
|
1131
1056
|
} catch (error2) {
|
|
1132
|
-
return {
|
|
1057
|
+
return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
|
|
1133
1058
|
}
|
|
1134
1059
|
}
|
|
1135
1060
|
async function getClaudeUsage() {
|
|
@@ -1158,11 +1083,11 @@ async function getClaudeUsage() {
|
|
|
1158
1083
|
throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
|
|
1159
1084
|
}
|
|
1160
1085
|
const body = await res.json();
|
|
1161
|
-
const {
|
|
1086
|
+
const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
|
|
1162
1087
|
return {
|
|
1163
1088
|
fiveHour: toWindow(body.five_hour),
|
|
1164
1089
|
sevenDay: toWindow(body.seven_day),
|
|
1165
|
-
|
|
1090
|
+
subscription,
|
|
1166
1091
|
ownerLookupError
|
|
1167
1092
|
};
|
|
1168
1093
|
}
|
|
@@ -1192,10 +1117,10 @@ async function claudeUsage() {
|
|
|
1192
1117
|
}
|
|
1193
1118
|
|
|
1194
1119
|
// src/commands/run.ts
|
|
1195
|
-
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
1196
|
-
import { homedir as
|
|
1197
|
-
import { isAbsolute as isAbsolute3, join as
|
|
1198
|
-
import
|
|
1120
|
+
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1121
|
+
import { homedir as homedir6 } from "node:os";
|
|
1122
|
+
import { isAbsolute as isAbsolute3, join as join10, parse, resolve as resolvePath2 } from "node:path";
|
|
1123
|
+
import chalk7 from "chalk";
|
|
1199
1124
|
|
|
1200
1125
|
// ../../packages/types/src/agents/index.ts
|
|
1201
1126
|
var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
|
|
@@ -1253,11 +1178,11 @@ function stripQuery(url) {
|
|
|
1253
1178
|
}
|
|
1254
1179
|
|
|
1255
1180
|
// src/commands/run.ts
|
|
1256
|
-
import
|
|
1257
|
-
import { select as
|
|
1181
|
+
import ora4 from "ora";
|
|
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.388b76b" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1261
1186
|
function getCliVersion() {
|
|
1262
1187
|
return CLI_VERSION;
|
|
1263
1188
|
}
|
|
@@ -1427,12 +1352,50 @@ var SEVERITY_BY_LEVEL = {
|
|
|
1427
1352
|
warn: "warning",
|
|
1428
1353
|
error: "error"
|
|
1429
1354
|
};
|
|
1355
|
+
function parseOpenCodeLogLine(line) {
|
|
1356
|
+
const normalisedLine = line.replace(/\r$/, "");
|
|
1357
|
+
const levelMatch = normalisedLine.match(/(?:^|\s)level=(\w+)/i);
|
|
1358
|
+
if (!levelMatch) return null;
|
|
1359
|
+
const level = levelMatch[1].toUpperCase();
|
|
1360
|
+
if (level !== "WARN" && level !== "ERROR") return null;
|
|
1361
|
+
const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
|
|
1362
|
+
return { level: level === "WARN" ? "warn" : "error", sessionID: sessionMatch?.[1] };
|
|
1363
|
+
}
|
|
1364
|
+
var MAX_LINE_BUFFER_BYTES = 16 * 1024;
|
|
1365
|
+
function createOpenCodeActivityForwarder(getContext) {
|
|
1366
|
+
let buffer = Buffer.alloc(0);
|
|
1367
|
+
const flushLine = (line) => {
|
|
1368
|
+
const parsed = parseOpenCodeLogLine(line);
|
|
1369
|
+
if (!parsed) return;
|
|
1370
|
+
forwardRunnerActivity(
|
|
1371
|
+
{
|
|
1372
|
+
level: parsed.level,
|
|
1373
|
+
error: line,
|
|
1374
|
+
metadata: parsed.sessionID ? { sessionID: parsed.sessionID } : void 0,
|
|
1375
|
+
source: "opencode"
|
|
1376
|
+
},
|
|
1377
|
+
getContext()
|
|
1378
|
+
);
|
|
1379
|
+
};
|
|
1380
|
+
return (chunk) => {
|
|
1381
|
+
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
|
|
1382
|
+
let newlineIndex;
|
|
1383
|
+
while ((newlineIndex = buffer.indexOf(10)) !== -1) {
|
|
1384
|
+
flushLine(buffer.subarray(0, newlineIndex).toString("utf-8").replace(/\r$/, ""));
|
|
1385
|
+
buffer = buffer.subarray(newlineIndex + 1);
|
|
1386
|
+
}
|
|
1387
|
+
if (buffer.length > MAX_LINE_BUFFER_BYTES) {
|
|
1388
|
+
flushLine(buffer.toString("utf-8"));
|
|
1389
|
+
buffer = Buffer.alloc(0);
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1430
1393
|
var MAX_MESSAGE_LENGTH = 500;
|
|
1431
1394
|
var MAX_METADATA_VALUE_LENGTH = 200;
|
|
1432
1395
|
var MAX_METADATA_ENTRIES = 20;
|
|
1433
1396
|
var TRUNCATION_MARKER = "\u2026";
|
|
1434
1397
|
function redact(message) {
|
|
1435
|
-
return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/https?:\/\/\S+/g, "<url>");
|
|
1398
|
+
return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/(?<![A-Za-z0-9_-])sk-ant-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-ant-***").replace(/(?<![A-Za-z0-9_-])sk-proj-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-proj-***").replace(/(?<![A-Za-z0-9_-])sk-[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "sk-***").replace(/(?<![A-Za-z0-9_-])(gh[oprsu])_[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "$1_***").replace(/(?<![A-Za-z0-9_-])github_pat_[A-Za-z0-9_]{20,}(?![A-Za-z0-9_-])/g, "github_pat_***").replace(/https?:\/\/\S+/g, "<url>");
|
|
1436
1399
|
}
|
|
1437
1400
|
function truncate(message) {
|
|
1438
1401
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
@@ -1458,43 +1421,47 @@ function sanitiseMetadata(metadata) {
|
|
|
1458
1421
|
}
|
|
1459
1422
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1460
1423
|
var RATE_LIMIT_MAX_EVENTS = 30;
|
|
1461
|
-
var
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1424
|
+
var rateWindows = /* @__PURE__ */ new Map();
|
|
1425
|
+
function admitUnderRateLimit(source, now) {
|
|
1426
|
+
let window = rateWindows.get(source);
|
|
1427
|
+
if (!window) {
|
|
1428
|
+
window = { windowStartedAt: 0, windowCount: 0, windowDroppedCount: 0 };
|
|
1429
|
+
rateWindows.set(source, window);
|
|
1430
|
+
}
|
|
1431
|
+
if (now - window.windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
|
|
1432
|
+
if (window.windowDroppedCount > 0) {
|
|
1467
1433
|
console.error(
|
|
1468
|
-
`[runner-activity-telemetry] rate cap reached: dropped ${windowDroppedCount} ${windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (cap ${RATE_LIMIT_MAX_EVENTS}/min)`
|
|
1434
|
+
`[runner-activity-telemetry] rate cap reached: dropped ${window.windowDroppedCount} ${window.windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (cap ${RATE_LIMIT_MAX_EVENTS}/min) for source "${source}"`
|
|
1469
1435
|
);
|
|
1470
1436
|
}
|
|
1471
|
-
windowStartedAt = now;
|
|
1472
|
-
windowCount = 0;
|
|
1473
|
-
windowDroppedCount = 0;
|
|
1437
|
+
window.windowStartedAt = now;
|
|
1438
|
+
window.windowCount = 0;
|
|
1439
|
+
window.windowDroppedCount = 0;
|
|
1474
1440
|
}
|
|
1475
|
-
if (windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1476
|
-
windowDroppedCount++;
|
|
1477
|
-
if (windowDroppedCount === 1) {
|
|
1441
|
+
if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1442
|
+
window.windowDroppedCount++;
|
|
1443
|
+
if (window.windowDroppedCount === 1) {
|
|
1478
1444
|
console.error(
|
|
1479
|
-
`[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ${RATE_LIMIT_WINDOW_MS / 1e3}s) \u2014 dropping further entries this window`
|
|
1445
|
+
`[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ${RATE_LIMIT_WINDOW_MS / 1e3}s) \u2014 dropping further entries this window for source "${source}"`
|
|
1480
1446
|
);
|
|
1481
1447
|
}
|
|
1482
1448
|
return false;
|
|
1483
1449
|
}
|
|
1484
|
-
windowCount++;
|
|
1450
|
+
window.windowCount++;
|
|
1485
1451
|
return true;
|
|
1486
1452
|
}
|
|
1487
1453
|
function forwardRunnerActivity(entry, context) {
|
|
1488
1454
|
try {
|
|
1489
1455
|
if (!FORWARDED_LEVELS.has(entry.level)) return;
|
|
1490
1456
|
if (!context.agentId || !context.authHeader) return;
|
|
1491
|
-
|
|
1457
|
+
const source = entry.source ?? "cli.run";
|
|
1458
|
+
if (!admitUnderRateLimit(source, Date.now())) return;
|
|
1492
1459
|
const rawMessage = entry.error ?? entry.message ?? "";
|
|
1493
1460
|
const message = truncate(redact(rawMessage));
|
|
1494
1461
|
logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
|
|
1495
1462
|
severity: SEVERITY_BY_LEVEL[entry.level],
|
|
1496
1463
|
message,
|
|
1497
|
-
metadata: { ...sanitiseMetadata(entry.metadata), source
|
|
1464
|
+
metadata: { ...sanitiseMetadata(entry.metadata), source },
|
|
1498
1465
|
agentId: context.agentId
|
|
1499
1466
|
});
|
|
1500
1467
|
} catch (err) {
|
|
@@ -1505,8 +1472,8 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1505
1472
|
}
|
|
1506
1473
|
|
|
1507
1474
|
// src/lib/opencode/session-db-recovery-report.ts
|
|
1508
|
-
import { readFileSync as readFileSync2, unlinkSync } from "fs";
|
|
1509
|
-
import { join as join2 } from "path";
|
|
1475
|
+
import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
|
|
1476
|
+
import { join as join2 } from "node:path";
|
|
1510
1477
|
function sessionDbRecoveryReportPath(homeDir, env) {
|
|
1511
1478
|
const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
|
|
1512
1479
|
return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
|
|
@@ -1689,6 +1656,11 @@ function isSessionDbRecoveryRecord(value) {
|
|
|
1689
1656
|
);
|
|
1690
1657
|
}
|
|
1691
1658
|
|
|
1659
|
+
// src/lib/opencode/auth.ts
|
|
1660
|
+
function buildOpenCodeBasicAuthHeader(password) {
|
|
1661
|
+
return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1692
1664
|
// src/lib/opencode/health.ts
|
|
1693
1665
|
async function checkOpenCodeHealth(port) {
|
|
1694
1666
|
try {
|
|
@@ -1706,6 +1678,27 @@ async function checkOpenCodeHealth(port) {
|
|
|
1706
1678
|
return { healthy: false, error: message };
|
|
1707
1679
|
}
|
|
1708
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
|
+
}
|
|
1709
1702
|
async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
1710
1703
|
const startTime = Date.now();
|
|
1711
1704
|
while (Date.now() - startTime < timeoutMs) {
|
|
@@ -1717,15 +1710,70 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
1717
1710
|
}
|
|
1718
1711
|
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
1719
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
|
+
}
|
|
1720
1768
|
|
|
1721
1769
|
// src/lib/opencode/session-db-boot.ts
|
|
1722
|
-
import { spawn as spawn2 } from "child_process";
|
|
1723
|
-
import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "fs";
|
|
1724
|
-
import { homedir as homedir2 } from "os";
|
|
1725
|
-
import { dirname as dirname2, resolve as resolvePath } from "path";
|
|
1770
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1771
|
+
import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
1772
|
+
import { homedir as homedir2 } from "node:os";
|
|
1773
|
+
import { dirname as dirname2, resolve as resolvePath } from "node:path";
|
|
1726
1774
|
|
|
1727
1775
|
// src/lib/runner-synchroniser.ts
|
|
1728
|
-
import { spawn } from "child_process";
|
|
1776
|
+
import { spawn } from "node:child_process";
|
|
1729
1777
|
function appendError(stderr, error2) {
|
|
1730
1778
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1731
1779
|
return stderr === "" ? message : `${stderr}
|
|
@@ -2250,9 +2298,9 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
2250
2298
|
}
|
|
2251
2299
|
|
|
2252
2300
|
// src/lib/opencode/session-db-provenance.ts
|
|
2253
|
-
import { createRequire } from "module";
|
|
2254
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2255
|
-
import { dirname as dirname3, join as join3 } from "path";
|
|
2301
|
+
import { createRequire } from "node:module";
|
|
2302
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2303
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
2256
2304
|
var require2 = createRequire(import.meta.url);
|
|
2257
2305
|
function readSessionDbMigrationIds(dbPath) {
|
|
2258
2306
|
let db;
|
|
@@ -2380,15 +2428,23 @@ function isQueueValidatedVersion(version2) {
|
|
|
2380
2428
|
if (!version2) return false;
|
|
2381
2429
|
return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version2);
|
|
2382
2430
|
}
|
|
2383
|
-
function buildOpenCodeVersionWarning(version2) {
|
|
2384
|
-
if (
|
|
2385
|
-
const detected = version2 ? `v${version2}` : "unknown";
|
|
2431
|
+
function buildOpenCodeVersionWarning(version2, major) {
|
|
2432
|
+
if (major === "v2") return null;
|
|
2386
2433
|
const validated = QUEUE_VALIDATED_OPENCODE_VERSIONS.map((v) => `v${v}`).join(", ");
|
|
2387
|
-
|
|
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`;
|
|
2388
2443
|
}
|
|
2389
2444
|
|
|
2390
2445
|
// src/lib/opencode/process.ts
|
|
2391
2446
|
import { execSync, spawn as spawn3 } from "child_process";
|
|
2447
|
+
import { randomBytes } from "node:crypto";
|
|
2392
2448
|
|
|
2393
2449
|
// src/lib/process-stop.ts
|
|
2394
2450
|
async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
@@ -2446,6 +2502,38 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
2446
2502
|
|
|
2447
2503
|
// src/lib/opencode/process.ts
|
|
2448
2504
|
var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
|
|
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
|
+
]);
|
|
2517
|
+
function resolveOpenCodeLogLevel(env) {
|
|
2518
|
+
const raw = env.OPENCODE_LOG_LEVEL;
|
|
2519
|
+
if (!raw) return "INFO";
|
|
2520
|
+
const upper = raw.toUpperCase();
|
|
2521
|
+
if (VALID_OPENCODE_LOG_LEVELS.has(upper)) return upper;
|
|
2522
|
+
console.warn(
|
|
2523
|
+
`startOpenCode: ignoring invalid OPENCODE_LOG_LEVEL "${raw}" (expected DEBUG|INFO|WARN|ERROR) \u2014 using INFO`
|
|
2524
|
+
);
|
|
2525
|
+
return "INFO";
|
|
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
|
+
}
|
|
2449
2537
|
function getProcessCwd(pid) {
|
|
2450
2538
|
const platform = process.platform;
|
|
2451
2539
|
try {
|
|
@@ -2494,14 +2582,14 @@ function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
2494
2582
|
}
|
|
2495
2583
|
return null;
|
|
2496
2584
|
}
|
|
2497
|
-
function
|
|
2585
|
+
function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
2498
2586
|
const instances = [];
|
|
2499
2587
|
try {
|
|
2500
2588
|
const platform = process.platform;
|
|
2501
2589
|
if (platform === "darwin" || platform === "linux") {
|
|
2502
2590
|
let pids = [];
|
|
2503
2591
|
try {
|
|
2504
|
-
const pgrepOutput = execSync(
|
|
2592
|
+
const pgrepOutput = execSync(`pgrep -f "${pgrepPattern}"`, {
|
|
2505
2593
|
encoding: "utf-8",
|
|
2506
2594
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2507
2595
|
}).trim();
|
|
@@ -2510,7 +2598,7 @@ function findOpenCodeProcesses() {
|
|
|
2510
2598
|
}
|
|
2511
2599
|
} catch {
|
|
2512
2600
|
try {
|
|
2513
|
-
const psOutput = execSync(
|
|
2601
|
+
const psOutput = execSync(`ps aux | grep -E "${psPattern}" | grep -v grep`, {
|
|
2514
2602
|
encoding: "utf-8",
|
|
2515
2603
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2516
2604
|
}).trim();
|
|
@@ -2556,6 +2644,9 @@ function findOpenCodeProcesses() {
|
|
|
2556
2644
|
}
|
|
2557
2645
|
return instances;
|
|
2558
2646
|
}
|
|
2647
|
+
function findOpenCodeProcesses() {
|
|
2648
|
+
return findProcessesByPattern("opencode serve|opencode-serve", "opencode (serve|--port)");
|
|
2649
|
+
}
|
|
2559
2650
|
async function scanPortsForOpenCode() {
|
|
2560
2651
|
const instances = [];
|
|
2561
2652
|
const checks = OPENCODE_PORT_RANGE.map(async (port) => {
|
|
@@ -2602,7 +2693,7 @@ async function findHealthyOpenCodeInstances() {
|
|
|
2602
2693
|
}
|
|
2603
2694
|
async function startOpenCode(port, options = {}) {
|
|
2604
2695
|
let command = "opencode";
|
|
2605
|
-
const printLogs = options.inheritStdio ? ["--print-logs"] : [];
|
|
2696
|
+
const printLogs = options.inheritStdio ? ["--print-logs", "--log-level", resolveOpenCodeLogLevel(process.env)] : [];
|
|
2606
2697
|
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...printLogs];
|
|
2607
2698
|
try {
|
|
2608
2699
|
execSync("which opencode", { stdio: "ignore" });
|
|
@@ -2625,6 +2716,37 @@ async function startOpenCode(port, options = {}) {
|
|
|
2625
2716
|
});
|
|
2626
2717
|
return child;
|
|
2627
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
|
+
}
|
|
2628
2750
|
function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
|
|
2629
2751
|
const sendSignal = (signal) => {
|
|
2630
2752
|
if (process.platform === "win32") {
|
|
@@ -2659,6 +2781,19 @@ function isOpenCodeInstalled() {
|
|
|
2659
2781
|
return false;
|
|
2660
2782
|
}
|
|
2661
2783
|
}
|
|
2784
|
+
function isOpenCode2Installed() {
|
|
2785
|
+
try {
|
|
2786
|
+
const platform = process.platform;
|
|
2787
|
+
if (platform === "win32") {
|
|
2788
|
+
execSync2("where opencode2", { stdio: "ignore" });
|
|
2789
|
+
} else {
|
|
2790
|
+
execSync2("which opencode2", { stdio: "ignore" });
|
|
2791
|
+
}
|
|
2792
|
+
return true;
|
|
2793
|
+
} catch {
|
|
2794
|
+
return false;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2662
2797
|
async function promptOpenCodeInstall(interactive) {
|
|
2663
2798
|
if (!interactive) {
|
|
2664
2799
|
console.log(
|
|
@@ -2668,7 +2803,11 @@ async function promptOpenCodeInstall(interactive) {
|
|
|
2668
2803
|
install_url: OPENCODE_INSTALL_URL,
|
|
2669
2804
|
install_commands: {
|
|
2670
2805
|
npm: "npm install -g opencode-ai",
|
|
2671
|
-
curl: "curl -fsSL https://opencode.ai/install.sh | sh"
|
|
2806
|
+
curl: "curl -fsSL https://opencode.ai/install.sh | sh",
|
|
2807
|
+
v2: {
|
|
2808
|
+
npm: "npm install -g @opencode-ai/cli@beta",
|
|
2809
|
+
curl: "curl -fsSL https://opencode.ai/v2/install | bash"
|
|
2810
|
+
}
|
|
2672
2811
|
}
|
|
2673
2812
|
})
|
|
2674
2813
|
);
|
|
@@ -2755,61 +2894,534 @@ function buildNoProviderWarning(hasProvider) {
|
|
|
2755
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).";
|
|
2756
2895
|
}
|
|
2757
2896
|
|
|
2758
|
-
// src/lib/
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
|
|
2897
|
+
// src/lib/opencode/session-v2.ts
|
|
2898
|
+
function isRecord(value) {
|
|
2899
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2762
2900
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
function timedFetch(input, init) {
|
|
2766
|
-
return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
|
|
2901
|
+
function finiteNumber(value) {
|
|
2902
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2767
2903
|
}
|
|
2768
|
-
function
|
|
2769
|
-
return
|
|
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
|
+
};
|
|
2770
2913
|
}
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
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;
|
|
2780
2925
|
}
|
|
2926
|
+
return {
|
|
2927
|
+
...input !== void 0 ? { input } : {},
|
|
2928
|
+
...output !== void 0 ? { output } : {},
|
|
2929
|
+
...reasoning !== void 0 ? { reasoning } : {},
|
|
2930
|
+
...cache ? { cache } : {}
|
|
2931
|
+
};
|
|
2781
2932
|
}
|
|
2782
|
-
function
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
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
|
+
};
|
|
2787
2978
|
}
|
|
2788
|
-
function
|
|
2789
|
-
if (!
|
|
2790
|
-
|
|
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 };
|
|
2791
2984
|
}
|
|
2792
|
-
function
|
|
2793
|
-
if (!
|
|
2794
|
-
|
|
2985
|
+
function adaptV2FormWire(value) {
|
|
2986
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
2987
|
+
return null;
|
|
2988
|
+
}
|
|
2989
|
+
return value;
|
|
2795
2990
|
}
|
|
2796
|
-
function
|
|
2797
|
-
if (!
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
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
|
+
};
|
|
2801
3022
|
}
|
|
2802
|
-
function
|
|
2803
|
-
if (!
|
|
2804
|
-
|
|
2805
|
-
const infoParent = m.info?.parentID;
|
|
2806
|
-
return typeof infoParent === "string" ? infoParent : void 0;
|
|
3023
|
+
function adaptV2FormList(value) {
|
|
3024
|
+
if (!isRecord(value) || !Array.isArray(value.data)) return null;
|
|
3025
|
+
return value.data.map(adaptV2Form).filter((form) => form !== null);
|
|
2807
3026
|
}
|
|
2808
|
-
function
|
|
2809
|
-
if (
|
|
2810
|
-
if (
|
|
2811
|
-
|
|
2812
|
-
|
|
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
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
// src/lib/opencode/session.ts
|
|
3370
|
+
var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
3371
|
+
function timedFetch(input, init) {
|
|
3372
|
+
return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
|
|
3373
|
+
}
|
|
3374
|
+
function requestWithClient(port, client, path, init, options) {
|
|
3375
|
+
return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
|
|
3376
|
+
}
|
|
3377
|
+
function opencodeBase(port) {
|
|
3378
|
+
return `http://127.0.0.1:${port}`;
|
|
3379
|
+
}
|
|
3380
|
+
async function getOpenCodeDirectory(port, client) {
|
|
3381
|
+
try {
|
|
3382
|
+
const res = await requestWithClient(port, client, "/path");
|
|
3383
|
+
if (!res.ok) return null;
|
|
3384
|
+
const body = await res.json();
|
|
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;
|
|
3386
|
+
return dir && dir.trim() ? dir.trim() : null;
|
|
3387
|
+
} catch (error2) {
|
|
3388
|
+
console.error(
|
|
3389
|
+
`[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3390
|
+
);
|
|
3391
|
+
return null;
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
function roleOf(m) {
|
|
3395
|
+
if (!m || typeof m !== "object") return void 0;
|
|
3396
|
+
if (typeof m.role === "string") return m.role;
|
|
3397
|
+
const infoRole = m.info?.role;
|
|
3398
|
+
return typeof infoRole === "string" ? infoRole : void 0;
|
|
3399
|
+
}
|
|
3400
|
+
function completedOf(m) {
|
|
3401
|
+
if (!m || typeof m !== "object") return void 0;
|
|
3402
|
+
return m.info?.time?.completed ?? m.time?.completed;
|
|
3403
|
+
}
|
|
3404
|
+
function createdOf(m) {
|
|
3405
|
+
if (!m || typeof m !== "object") return void 0;
|
|
3406
|
+
return m.info?.time?.created ?? m.time?.created;
|
|
3407
|
+
}
|
|
3408
|
+
function idOf(m) {
|
|
3409
|
+
if (!m || typeof m !== "object") return void 0;
|
|
3410
|
+
if (typeof m.id === "string") return m.id;
|
|
3411
|
+
const infoId = m.info?.id;
|
|
3412
|
+
return typeof infoId === "string" ? infoId : void 0;
|
|
3413
|
+
}
|
|
3414
|
+
function parentIdOf(m) {
|
|
3415
|
+
if (!m || typeof m !== "object") return void 0;
|
|
3416
|
+
if (typeof m.parentID === "string") return m.parentID;
|
|
3417
|
+
const infoParent = m.info?.parentID;
|
|
3418
|
+
return typeof infoParent === "string" ? infoParent : void 0;
|
|
3419
|
+
}
|
|
3420
|
+
function finishOf(m) {
|
|
3421
|
+
if (!m || typeof m !== "object") return void 0;
|
|
3422
|
+
if (typeof m.finish === "string") return m.finish;
|
|
3423
|
+
const infoFinish = m.info?.finish;
|
|
3424
|
+
return typeof infoFinish === "string" ? infoFinish : void 0;
|
|
2813
3425
|
}
|
|
2814
3426
|
function errorOf(m) {
|
|
2815
3427
|
if (!m || typeof m !== "object") return void 0;
|
|
@@ -2819,16 +3431,48 @@ function isAssistantInFlight(m) {
|
|
|
2819
3431
|
if (completedOf(m) == null) return true;
|
|
2820
3432
|
return finishOf(m) === "tool-calls";
|
|
2821
3433
|
}
|
|
2822
|
-
async function getSessionMessages(port, sessionId) {
|
|
3434
|
+
async function getSessionMessages(port, sessionId, client) {
|
|
2823
3435
|
try {
|
|
2824
|
-
const
|
|
3436
|
+
const path = `/session/${sessionId}/message`;
|
|
3437
|
+
const res = await requestWithClient(port, client, path);
|
|
2825
3438
|
if (!res.ok) return null;
|
|
2826
3439
|
const body = await res.json();
|
|
2827
3440
|
return Array.isArray(body) ? body : null;
|
|
2828
|
-
} catch {
|
|
3441
|
+
} catch (error2) {
|
|
3442
|
+
console.error(
|
|
3443
|
+
`[getSessionMessages] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3444
|
+
);
|
|
2829
3445
|
return null;
|
|
2830
3446
|
}
|
|
2831
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
|
+
}
|
|
2832
3476
|
function isSessionActivelyGenerating(messages) {
|
|
2833
3477
|
if (!messages || messages.length === 0) return false;
|
|
2834
3478
|
const last = messages[messages.length - 1];
|
|
@@ -2849,27 +3493,37 @@ function sessionLastActivityMs(session) {
|
|
|
2849
3493
|
}
|
|
2850
3494
|
return null;
|
|
2851
3495
|
}
|
|
2852
|
-
async function listSessions(port) {
|
|
3496
|
+
async function listSessions(port, client) {
|
|
3497
|
+
if (client?.version === "v2") return listV2Sessions(client);
|
|
2853
3498
|
try {
|
|
2854
|
-
const res = await
|
|
3499
|
+
const res = await requestWithClient(port, client, "/session");
|
|
2855
3500
|
if (!res.ok) return null;
|
|
2856
3501
|
const body = await res.json();
|
|
2857
3502
|
return Array.isArray(body) ? body : null;
|
|
2858
|
-
} catch {
|
|
3503
|
+
} catch (error2) {
|
|
3504
|
+
console.error(
|
|
3505
|
+
`[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3506
|
+
);
|
|
2859
3507
|
return null;
|
|
2860
3508
|
}
|
|
2861
3509
|
}
|
|
2862
|
-
async function deleteSession(port, id) {
|
|
3510
|
+
async function deleteSession(port, id, client) {
|
|
3511
|
+
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
2863
3512
|
try {
|
|
2864
|
-
const res = await
|
|
3513
|
+
const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
|
|
2865
3514
|
return res.status >= 200 && res.status < 300;
|
|
2866
|
-
} catch {
|
|
3515
|
+
} catch (error2) {
|
|
3516
|
+
console.error(
|
|
3517
|
+
`[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3518
|
+
);
|
|
2867
3519
|
return false;
|
|
2868
3520
|
}
|
|
2869
3521
|
}
|
|
2870
|
-
async function sessionExists(port, id) {
|
|
3522
|
+
async function sessionExists(port, id, client) {
|
|
2871
3523
|
try {
|
|
2872
|
-
const res = await
|
|
3524
|
+
const res = await requestWithClient(port, client, `/session/${id}`, void 0, {
|
|
3525
|
+
allowStatuses: [404]
|
|
3526
|
+
});
|
|
2873
3527
|
if (res.status >= 200 && res.status < 300) return true;
|
|
2874
3528
|
if (res.status === 404) return false;
|
|
2875
3529
|
return null;
|
|
@@ -2877,9 +3531,22 @@ async function sessionExists(port, id) {
|
|
|
2877
3531
|
return null;
|
|
2878
3532
|
}
|
|
2879
3533
|
}
|
|
2880
|
-
async function
|
|
3534
|
+
async function getOpenCodeSession(port, id, client) {
|
|
2881
3535
|
try {
|
|
2882
|
-
const
|
|
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;
|
|
3548
|
+
try {
|
|
3549
|
+
const res = await requestWithClient(port, client, "/session/status");
|
|
2883
3550
|
if (!res.ok) {
|
|
2884
3551
|
console.error(
|
|
2885
3552
|
`[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
|
|
@@ -2901,22 +3568,28 @@ async function getSessionStatuses(port) {
|
|
|
2901
3568
|
return null;
|
|
2902
3569
|
}
|
|
2903
3570
|
}
|
|
2904
|
-
async function isSessionOngoing(port, id) {
|
|
2905
|
-
|
|
3571
|
+
async function isSessionOngoing(port, id, client) {
|
|
3572
|
+
if (client?.version === "v2") return isV2SessionOngoing(client, id);
|
|
3573
|
+
const map = await getSessionStatuses(port, client);
|
|
2906
3574
|
if (map == null) return null;
|
|
2907
3575
|
const entry = map[id];
|
|
2908
3576
|
return entry != null && entry.type !== "idle";
|
|
2909
3577
|
}
|
|
2910
|
-
async function createOpenCodeSession(port, directory) {
|
|
2911
|
-
const
|
|
2912
|
-
if (directory && directory.trim())
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
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
|
+
);
|
|
2920
3593
|
if (!response.ok) {
|
|
2921
3594
|
const text = await response.text().catch(() => "");
|
|
2922
3595
|
throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ""}`);
|
|
@@ -2924,10 +3597,16 @@ async function createOpenCodeSession(port, directory) {
|
|
|
2924
3597
|
const data = await response.json();
|
|
2925
3598
|
return data.id;
|
|
2926
3599
|
}
|
|
2927
|
-
async function getModelAttachmentCapability(port, model) {
|
|
3600
|
+
async function getModelAttachmentCapability(port, model, client) {
|
|
2928
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
|
+
}
|
|
2929
3608
|
try {
|
|
2930
|
-
const res = await
|
|
3609
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
2931
3610
|
if (!res.ok) {
|
|
2932
3611
|
console.error(
|
|
2933
3612
|
`[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
@@ -3047,19 +3726,43 @@ function applyModelOptions(body, options) {
|
|
|
3047
3726
|
}
|
|
3048
3727
|
if (variant) body.variant = variant;
|
|
3049
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
|
+
}
|
|
3050
3753
|
function messageText(m) {
|
|
3051
3754
|
if (!m || !Array.isArray(m.parts)) return "";
|
|
3052
3755
|
return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
3053
3756
|
}
|
|
3054
|
-
async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
3055
|
-
const before = await getSessionMessages(port, sessionId);
|
|
3757
|
+
async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
|
|
3758
|
+
const before = await getSessionMessages(port, sessionId, client);
|
|
3056
3759
|
const knownUserIds = new Set(
|
|
3057
3760
|
(before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
|
|
3058
3761
|
);
|
|
3059
3762
|
const parts = [{ type: "text", text: content }];
|
|
3060
3763
|
let pendingOutcomes = null;
|
|
3061
3764
|
if (attachments && attachments.inputs.length > 0) {
|
|
3062
|
-
const capable = await getModelAttachmentCapability(port, options?.model);
|
|
3765
|
+
const capable = await getModelAttachmentCapability(port, options?.model, client);
|
|
3063
3766
|
const {
|
|
3064
3767
|
parts: fileParts,
|
|
3065
3768
|
outcomes,
|
|
@@ -3072,11 +3775,17 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
3072
3775
|
parts
|
|
3073
3776
|
};
|
|
3074
3777
|
applyModelOptions(body, options);
|
|
3075
|
-
const res = await
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
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
|
+
);
|
|
3080
3789
|
if (res.status < 200 || res.status >= 300) {
|
|
3081
3790
|
const text = await res.text().catch(() => "");
|
|
3082
3791
|
const { variant } = splitModelVariant(options?.model);
|
|
@@ -3087,7 +3796,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
3087
3796
|
const READ_BACK_ATTEMPTS = 5;
|
|
3088
3797
|
const READ_BACK_DELAY_MS = 150;
|
|
3089
3798
|
for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
|
|
3090
|
-
const after = await getSessionMessages(port, sessionId);
|
|
3799
|
+
const after = await getSessionMessages(port, sessionId, client);
|
|
3091
3800
|
if (after) {
|
|
3092
3801
|
let best = null;
|
|
3093
3802
|
for (const m of after) {
|
|
@@ -3190,21 +3899,74 @@ function collectSubagentSessions(messages, userMessageId) {
|
|
|
3190
3899
|
}
|
|
3191
3900
|
return refs;
|
|
3192
3901
|
}
|
|
3193
|
-
function
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
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)
|
|
3197
3941
|
);
|
|
3198
|
-
const
|
|
3199
|
-
const
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
const
|
|
3205
|
-
|
|
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);
|
|
3206
3957
|
}
|
|
3207
|
-
|
|
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;
|
|
3208
3970
|
let sawAnyUsage = false;
|
|
3209
3971
|
let inputSum = 0;
|
|
3210
3972
|
let outputSum = 0;
|
|
@@ -3215,7 +3977,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
3215
3977
|
let sawCost = false;
|
|
3216
3978
|
let modelId = null;
|
|
3217
3979
|
let providerId = null;
|
|
3218
|
-
for (const m of
|
|
3980
|
+
for (const m of selected) {
|
|
3219
3981
|
const info = m.info;
|
|
3220
3982
|
if (!info) continue;
|
|
3221
3983
|
const tokens = info.tokens;
|
|
@@ -3250,12 +4012,28 @@ function messageUsage(messages, userMessageId) {
|
|
|
3250
4012
|
usage_tokens_reasoning: reasoningSum,
|
|
3251
4013
|
usage_tokens_cache_read: cacheReadSum,
|
|
3252
4014
|
usage_tokens_cache_write: cacheWriteSum,
|
|
3253
|
-
// NULL means
|
|
3254
|
-
//
|
|
3255
|
-
// `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`.
|
|
3256
4017
|
usage_cost_usd: sawCost ? costSum : null
|
|
3257
4018
|
};
|
|
3258
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
|
+
}
|
|
3259
4037
|
function messageRunState(messages, userMessageId) {
|
|
3260
4038
|
if (!messages || messages.length === 0) return "unknown";
|
|
3261
4039
|
const hasUser = messages.some((m) => idOf(m) === userMessageId);
|
|
@@ -3385,9 +4163,73 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
|
3385
4163
|
(m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
3386
4164
|
);
|
|
3387
4165
|
}
|
|
3388
|
-
|
|
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
|
+
}
|
|
3389
4231
|
try {
|
|
3390
|
-
const res = await
|
|
4232
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
3391
4233
|
if (!res.ok) {
|
|
3392
4234
|
console.error(
|
|
3393
4235
|
`[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
@@ -3416,6 +4258,99 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3416
4258
|
return null;
|
|
3417
4259
|
}
|
|
3418
4260
|
}
|
|
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;
|
|
4337
|
+
try {
|
|
4338
|
+
const res = await requestWithClient(port, client, "/config", {
|
|
4339
|
+
method: "PATCH",
|
|
4340
|
+
headers: { "Content-Type": "application/json" },
|
|
4341
|
+
body: JSON.stringify({})
|
|
4342
|
+
});
|
|
4343
|
+
if (!res.ok) {
|
|
4344
|
+
console.error(
|
|
4345
|
+
`[reloadProviderCache] PATCH /config returned HTTP ${res.status} (port ${port})`
|
|
4346
|
+
);
|
|
4347
|
+
}
|
|
4348
|
+
} catch (err) {
|
|
4349
|
+
console.error(
|
|
4350
|
+
`[reloadProviderCache] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
4351
|
+
);
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
3419
4354
|
|
|
3420
4355
|
// src/lib/opencode/session-cleanup.ts
|
|
3421
4356
|
var DURATION_UNIT_MS = {
|
|
@@ -3522,8 +4457,8 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
3522
4457
|
}
|
|
3523
4458
|
|
|
3524
4459
|
// src/lib/opencode/session-db-size.ts
|
|
3525
|
-
import { statSync as statSync3 } from "fs";
|
|
3526
|
-
import { join as join4 } from "path";
|
|
4460
|
+
import { statSync as statSync3 } from "node:fs";
|
|
4461
|
+
import { join as join4 } from "node:path";
|
|
3527
4462
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
3528
4463
|
function statSessionDbBytes(homeDir) {
|
|
3529
4464
|
const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
@@ -3553,9 +4488,96 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
3553
4488
|
return null;
|
|
3554
4489
|
}
|
|
3555
4490
|
|
|
4491
|
+
// src/lib/opencode/log-tail.ts
|
|
4492
|
+
import { statSync as statSync4 } from "node:fs";
|
|
4493
|
+
import { homedir as homedir3 } from "node:os";
|
|
4494
|
+
import { join as join5 } from "node:path";
|
|
4495
|
+
import { open as open2, stat } from "node:fs/promises";
|
|
4496
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
4497
|
+
function resolveOpenCodeLogPath(homeDir = homedir3(), env = process.env) {
|
|
4498
|
+
const dataDir = env.XDG_DATA_HOME || join5(homeDir, ".local", "share");
|
|
4499
|
+
return join5(dataDir, "opencode", "log", "opencode.log");
|
|
4500
|
+
}
|
|
4501
|
+
function isEnoent(error2) {
|
|
4502
|
+
return error2?.code === "ENOENT";
|
|
4503
|
+
}
|
|
4504
|
+
function reportFailure(operation, logPath, error2) {
|
|
4505
|
+
console.error(
|
|
4506
|
+
`[opencode-log-tail] ${operation} failed for ${logPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4507
|
+
);
|
|
4508
|
+
}
|
|
4509
|
+
function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
4510
|
+
const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
4511
|
+
let offset = 0;
|
|
4512
|
+
let inode = null;
|
|
4513
|
+
let baselineReady = true;
|
|
4514
|
+
try {
|
|
4515
|
+
const initial = statSync4(logPath);
|
|
4516
|
+
offset = initial.size;
|
|
4517
|
+
inode = initial.ino;
|
|
4518
|
+
} catch (error2) {
|
|
4519
|
+
if (!isEnoent(error2)) {
|
|
4520
|
+
reportFailure("initial stat", logPath, error2);
|
|
4521
|
+
baselineReady = false;
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
let polling = false;
|
|
4525
|
+
let stopped = false;
|
|
4526
|
+
const poll = async () => {
|
|
4527
|
+
if (polling || stopped) return;
|
|
4528
|
+
polling = true;
|
|
4529
|
+
try {
|
|
4530
|
+
let current;
|
|
4531
|
+
try {
|
|
4532
|
+
current = await stat(logPath);
|
|
4533
|
+
} catch (error2) {
|
|
4534
|
+
if (!isEnoent(error2)) reportFailure("stat", logPath, error2);
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
if (!baselineReady) {
|
|
4538
|
+
offset = current.size;
|
|
4539
|
+
inode = current.ino;
|
|
4540
|
+
baselineReady = true;
|
|
4541
|
+
return;
|
|
4542
|
+
}
|
|
4543
|
+
if (inode !== null && current.ino !== inode || current.size < offset) {
|
|
4544
|
+
offset = 0;
|
|
4545
|
+
}
|
|
4546
|
+
inode = current.ino;
|
|
4547
|
+
if (current.size === offset) return;
|
|
4548
|
+
const length = current.size - offset;
|
|
4549
|
+
const fh = await open2(logPath, "r");
|
|
4550
|
+
try {
|
|
4551
|
+
const buf = Buffer.alloc(length);
|
|
4552
|
+
const { bytesRead } = await fh.read(buf, 0, length, offset);
|
|
4553
|
+
offset += bytesRead;
|
|
4554
|
+
if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
|
|
4555
|
+
} finally {
|
|
4556
|
+
await fh.close();
|
|
4557
|
+
}
|
|
4558
|
+
} catch (error2) {
|
|
4559
|
+
if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
|
|
4560
|
+
} finally {
|
|
4561
|
+
polling = false;
|
|
4562
|
+
}
|
|
4563
|
+
};
|
|
4564
|
+
const interval = setInterval(() => void poll(), pollIntervalMs);
|
|
4565
|
+
void poll();
|
|
4566
|
+
return {
|
|
4567
|
+
stop: () => {
|
|
4568
|
+
stopped = true;
|
|
4569
|
+
clearInterval(interval);
|
|
4570
|
+
}
|
|
4571
|
+
};
|
|
4572
|
+
}
|
|
4573
|
+
|
|
3556
4574
|
// src/lib/opencode/session-db-reclaim.ts
|
|
3557
|
-
import { statSync as
|
|
3558
|
-
import { dirname as dirname4 } from "path";
|
|
4575
|
+
import { statSync as statSync5, statfsSync } from "node:fs";
|
|
4576
|
+
import { dirname as dirname4 } from "node:path";
|
|
4577
|
+
function errorMessage(error2) {
|
|
4578
|
+
if (!(error2 instanceof Error)) return String(error2);
|
|
4579
|
+
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
4580
|
+
}
|
|
3559
4581
|
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
3560
4582
|
try {
|
|
3561
4583
|
const fsStats = statfsSync(dirname4(dbPath));
|
|
@@ -3581,17 +4603,17 @@ async function probeReclaimAvailability(input) {
|
|
|
3581
4603
|
const { dbPath, requiredBytes } = input;
|
|
3582
4604
|
let sqlite;
|
|
3583
4605
|
try {
|
|
3584
|
-
sqlite = await import("sqlite");
|
|
4606
|
+
sqlite = await import("node:sqlite");
|
|
3585
4607
|
} catch (err) {
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
return "sqlite-unavailable";
|
|
4608
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
4609
|
+
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
4610
|
+
return { reason: "sqlite-unavailable", detail };
|
|
3590
4611
|
}
|
|
3591
4612
|
let autoVacuum = null;
|
|
3592
4613
|
try {
|
|
3593
4614
|
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
3594
4615
|
try {
|
|
4616
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3595
4617
|
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3596
4618
|
} finally {
|
|
3597
4619
|
db.close();
|
|
@@ -3602,23 +4624,25 @@ async function probeReclaimAvailability(input) {
|
|
|
3602
4624
|
);
|
|
3603
4625
|
}
|
|
3604
4626
|
if (autoVacuum !== 0) return null;
|
|
3605
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
|
|
4627
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
3606
4628
|
}
|
|
3607
4629
|
async function reclaimSessionDbSpace(input) {
|
|
3608
4630
|
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
3609
4631
|
let sqlite;
|
|
3610
4632
|
try {
|
|
3611
|
-
sqlite = await import("sqlite");
|
|
4633
|
+
sqlite = await import("node:sqlite");
|
|
3612
4634
|
} catch (err) {
|
|
4635
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
3613
4636
|
console.warn(
|
|
3614
|
-
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${
|
|
4637
|
+
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
3615
4638
|
);
|
|
3616
|
-
return { ok: false, skipped: "sqlite-unavailable" };
|
|
4639
|
+
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
3617
4640
|
}
|
|
3618
4641
|
const { DatabaseSync } = sqlite;
|
|
3619
4642
|
let db;
|
|
3620
4643
|
try {
|
|
3621
4644
|
db = new DatabaseSync(dbPath);
|
|
4645
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
3622
4646
|
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
3623
4647
|
if (autoVacuum === 0) {
|
|
3624
4648
|
if (!allowFullVacuum) {
|
|
@@ -3627,7 +4651,7 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3627
4651
|
);
|
|
3628
4652
|
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
3629
4653
|
}
|
|
3630
|
-
const fileBytesForGuard =
|
|
4654
|
+
const fileBytesForGuard = statSync5(dbPath).size;
|
|
3631
4655
|
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
3632
4656
|
if (skipReason !== null) {
|
|
3633
4657
|
console.warn(
|
|
@@ -3655,10 +4679,12 @@ async function reclaimSessionDbSpace(input) {
|
|
|
3655
4679
|
);
|
|
3656
4680
|
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
3657
4681
|
} catch (err) {
|
|
3658
|
-
console.error(
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
4682
|
+
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
4683
|
+
return {
|
|
4684
|
+
ok: false,
|
|
4685
|
+
skipped: "reclaim-error",
|
|
4686
|
+
detail: errorMessage(err)
|
|
4687
|
+
};
|
|
3662
4688
|
} finally {
|
|
3663
4689
|
db?.close();
|
|
3664
4690
|
}
|
|
@@ -3687,10 +4713,11 @@ var STRIP_RES = /* @__PURE__ */ new Set([
|
|
|
3687
4713
|
"content-length"
|
|
3688
4714
|
]);
|
|
3689
4715
|
var StreamForwarder = class {
|
|
3690
|
-
constructor(ws, port, callbacks = {}) {
|
|
4716
|
+
constructor(ws, port, callbacks = {}, options = {}) {
|
|
3691
4717
|
this.ws = ws;
|
|
3692
4718
|
this.port = port;
|
|
3693
4719
|
this.callbacks = callbacks;
|
|
4720
|
+
this.options = options;
|
|
3694
4721
|
}
|
|
3695
4722
|
inflight = /* @__PURE__ */ new Map();
|
|
3696
4723
|
/**
|
|
@@ -3774,7 +4801,15 @@ var StreamForwarder = class {
|
|
|
3774
4801
|
}
|
|
3775
4802
|
const fwdHeaders = {};
|
|
3776
4803
|
for (const [k, v] of Object.entries(headers ?? {})) {
|
|
3777
|
-
|
|
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);
|
|
3778
4813
|
}
|
|
3779
4814
|
this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });
|
|
3780
4815
|
const body = bodyPromise ? await bodyPromise : void 0;
|
|
@@ -3887,6 +4922,7 @@ function connectTunnel(options) {
|
|
|
3887
4922
|
agentId,
|
|
3888
4923
|
authHeader,
|
|
3889
4924
|
port,
|
|
4925
|
+
openCodePassword,
|
|
3890
4926
|
onConnected,
|
|
3891
4927
|
onDisconnected,
|
|
3892
4928
|
onError,
|
|
@@ -3904,11 +4940,16 @@ function connectTunnel(options) {
|
|
|
3904
4940
|
Authorization: authHeader
|
|
3905
4941
|
}
|
|
3906
4942
|
});
|
|
3907
|
-
const forwarder = new StreamForwarder(
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
4943
|
+
const forwarder = new StreamForwarder(
|
|
4944
|
+
ws,
|
|
4945
|
+
port,
|
|
4946
|
+
{
|
|
4947
|
+
onHead: () => onResponse?.(),
|
|
4948
|
+
onDrainPing: () => onDrainPing?.(),
|
|
4949
|
+
onUsageRearmPing: () => onUsageRearmPing?.()
|
|
4950
|
+
},
|
|
4951
|
+
{ openCodePassword }
|
|
4952
|
+
);
|
|
3912
4953
|
const connectionTimeout = setTimeout(() => {
|
|
3913
4954
|
ws.close();
|
|
3914
4955
|
reject(new Error("Connection timeout"));
|
|
@@ -3950,8 +4991,8 @@ function connectTunnel(options) {
|
|
|
3950
4991
|
try {
|
|
3951
4992
|
message = JSON.parse(data.toString());
|
|
3952
4993
|
} catch (error2) {
|
|
3953
|
-
const
|
|
3954
|
-
onError?.(`Failed to handle message: ${
|
|
4994
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
4995
|
+
onError?.(`Failed to handle message: ${errorMessage3}`);
|
|
3955
4996
|
return;
|
|
3956
4997
|
}
|
|
3957
4998
|
if (isStreamFrame(message)) {
|
|
@@ -4051,6 +5092,7 @@ var RunnerConnection = class {
|
|
|
4051
5092
|
agentId: this.resolvedAgentId,
|
|
4052
5093
|
authHeader: this.opts.getAuthHeader(),
|
|
4053
5094
|
port: this.opts.port,
|
|
5095
|
+
openCodePassword: this.opts.openCodePassword,
|
|
4054
5096
|
onConnected: (agentId) => {
|
|
4055
5097
|
this.reconnectAttempt = 0;
|
|
4056
5098
|
this.reconnecting = false;
|
|
@@ -4095,7 +5137,7 @@ var RunnerConnection = class {
|
|
|
4095
5137
|
};
|
|
4096
5138
|
|
|
4097
5139
|
// src/lib/tunnel/ready-marker.ts
|
|
4098
|
-
import { writeFileSync as writeFileSync3 } from "fs";
|
|
5140
|
+
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
4099
5141
|
function writeTunnelReadyMarker(path, agentId) {
|
|
4100
5142
|
try {
|
|
4101
5143
|
writeFileSync3(path, `${agentId}
|
|
@@ -4107,7 +5149,7 @@ function writeTunnelReadyMarker(path, agentId) {
|
|
|
4107
5149
|
}
|
|
4108
5150
|
|
|
4109
5151
|
// src/lib/replication.ts
|
|
4110
|
-
import { spawn as spawn4 } from "child_process";
|
|
5152
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
4111
5153
|
function startSessionDbReplication(configPath) {
|
|
4112
5154
|
return spawn4("litestream", ["replicate", "-config", configPath], {
|
|
4113
5155
|
stdio: "inherit"
|
|
@@ -4123,7 +5165,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
4123
5165
|
}
|
|
4124
5166
|
|
|
4125
5167
|
// src/lib/process-liveness.ts
|
|
4126
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
5168
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
4127
5169
|
function isProcessAlive(pid) {
|
|
4128
5170
|
try {
|
|
4129
5171
|
process.kill(pid, 0);
|
|
@@ -4149,9 +5191,9 @@ function isProcessAlive(pid) {
|
|
|
4149
5191
|
}
|
|
4150
5192
|
|
|
4151
5193
|
// src/lib/openai-usage.ts
|
|
4152
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
4153
|
-
import { homedir as
|
|
4154
|
-
import { join as
|
|
5194
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
5195
|
+
import { homedir as homedir4 } from "node:os";
|
|
5196
|
+
import { join as join6 } from "node:path";
|
|
4155
5197
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4156
5198
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4157
5199
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4165,7 +5207,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4165
5207
|
}
|
|
4166
5208
|
function readOpenCodeChatGptCredentials() {
|
|
4167
5209
|
try {
|
|
4168
|
-
const raw = readFileSync5(
|
|
5210
|
+
const raw = readFileSync5(join6(homedir4(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4169
5211
|
let parsed;
|
|
4170
5212
|
try {
|
|
4171
5213
|
parsed = JSON.parse(raw);
|
|
@@ -4202,7 +5244,7 @@ function parseChatGptIdentity(accessToken) {
|
|
|
4202
5244
|
const auth = payload["https://api.openai.com/auth"];
|
|
4203
5245
|
const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
|
|
4204
5246
|
const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
|
|
4205
|
-
return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
|
|
5247
|
+
return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
|
|
4206
5248
|
}
|
|
4207
5249
|
function toWindow2(headers, name) {
|
|
4208
5250
|
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
@@ -4231,33 +5273,73 @@ function parseCodexUsageHeaders(headers) {
|
|
|
4231
5273
|
function normalizeProbeModel(model) {
|
|
4232
5274
|
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
4233
5275
|
}
|
|
4234
|
-
|
|
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) {
|
|
4235
5284
|
try {
|
|
4236
|
-
const
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
if (!res.ok) {
|
|
4241
|
-
console.error(
|
|
4242
|
-
`[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
4243
|
-
);
|
|
4244
|
-
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);
|
|
4245
5289
|
}
|
|
4246
|
-
const
|
|
4247
|
-
|
|
4248
|
-
|
|
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;
|
|
4249
5295
|
const candidates = [
|
|
4250
|
-
...typeof
|
|
5296
|
+
...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
|
|
4251
5297
|
...Object.keys(provider.models)
|
|
4252
5298
|
].map(normalizeProbeModel);
|
|
4253
|
-
return [...new Set(candidates)].slice(0, 4);
|
|
5299
|
+
return { status: "supported", models: [...new Set(candidates)].slice(0, 4) };
|
|
4254
5300
|
} catch (err) {
|
|
4255
|
-
|
|
4256
|
-
`
|
|
5301
|
+
return unsupportedProbeModels(
|
|
5302
|
+
`V1 GET /config/providers failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
5303
|
+
port
|
|
5304
|
+
);
|
|
5305
|
+
}
|
|
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
|
|
4257
5337
|
);
|
|
4258
|
-
return [];
|
|
4259
5338
|
}
|
|
4260
5339
|
}
|
|
5340
|
+
async function resolveProbeModels(port, client = createOpenCodeClient({ port, version: "v1" })) {
|
|
5341
|
+
return client.version === "v2" ? resolveV2ProbeModels(client, port) : resolveV1ProbeModels(client, port);
|
|
5342
|
+
}
|
|
4261
5343
|
function hasPrimaryHeaders(headers) {
|
|
4262
5344
|
return [
|
|
4263
5345
|
"x-codex-primary-used-percent",
|
|
@@ -4265,7 +5347,7 @@ function hasPrimaryHeaders(headers) {
|
|
|
4265
5347
|
"x-codex-primary-reset-at"
|
|
4266
5348
|
].some((name) => headers.has(name));
|
|
4267
5349
|
}
|
|
4268
|
-
async function getOpenAiUsage(port) {
|
|
5350
|
+
async function getOpenAiUsage(port, client) {
|
|
4269
5351
|
const credentials2 = readOpenCodeChatGptCredentials();
|
|
4270
5352
|
if (!credentials2) {
|
|
4271
5353
|
throw new OpenAiUsageError(
|
|
@@ -4280,12 +5362,16 @@ async function getOpenAiUsage(port) {
|
|
|
4280
5362
|
);
|
|
4281
5363
|
}
|
|
4282
5364
|
const subscription = parseChatGptIdentity(credentials2.accessToken);
|
|
4283
|
-
const
|
|
4284
|
-
if (models.length === 0) {
|
|
4285
|
-
|
|
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
|
+
);
|
|
4286
5372
|
}
|
|
4287
5373
|
let lastStatus;
|
|
4288
|
-
for (const model of models) {
|
|
5374
|
+
for (const model of lookup.models) {
|
|
4289
5375
|
let res;
|
|
4290
5376
|
try {
|
|
4291
5377
|
res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
|
|
@@ -4384,13 +5470,6 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
|
|
|
4384
5470
|
envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
4385
5471
|
});
|
|
4386
5472
|
}
|
|
4387
|
-
function nextReportDelayMs(random = Math.random) {
|
|
4388
|
-
return usageReportDelayMs(random);
|
|
4389
|
-
}
|
|
4390
|
-
var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
|
|
4391
|
-
function claudeUsageFailureLogLevel(consecutiveFailures) {
|
|
4392
|
-
return usageReportFailureLogLevel(consecutiveFailures);
|
|
4393
|
-
}
|
|
4394
5473
|
|
|
4395
5474
|
// src/lib/openai-usage-reporting.ts
|
|
4396
5475
|
function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
@@ -4427,8 +5506,8 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
4427
5506
|
}
|
|
4428
5507
|
|
|
4429
5508
|
// src/lib/resource-usage.ts
|
|
4430
|
-
import { cpus, totalmem, freemem } from "os";
|
|
4431
|
-
import { statfsSync as statfsSync2 } from "fs";
|
|
5509
|
+
import { cpus, totalmem, freemem } from "node:os";
|
|
5510
|
+
import { statfsSync as statfsSync2 } from "node:fs";
|
|
4432
5511
|
|
|
4433
5512
|
// src/lib/ecs-task-metadata.ts
|
|
4434
5513
|
var ECS_METADATA_TIMEOUT_MS = 2e3;
|
|
@@ -4595,15 +5674,15 @@ function createResourceUsageCollector(homeDir) {
|
|
|
4595
5674
|
}
|
|
4596
5675
|
|
|
4597
5676
|
// src/lib/channels/driver.ts
|
|
4598
|
-
import { homedir as
|
|
5677
|
+
import { homedir as homedir5 } from "node:os";
|
|
4599
5678
|
|
|
4600
5679
|
// src/lib/runner-file-sync.ts
|
|
4601
|
-
import { join as
|
|
5680
|
+
import { join as join8 } from "node:path";
|
|
4602
5681
|
|
|
4603
5682
|
// src/lib/file-push.ts
|
|
4604
|
-
import { randomUUID } from "crypto";
|
|
4605
|
-
import { chmod, mkdir, open as
|
|
4606
|
-
import { basename, dirname as dirname5, isAbsolute, join as
|
|
5683
|
+
import { randomUUID } from "node:crypto";
|
|
5684
|
+
import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
|
|
5685
|
+
import { basename, dirname as dirname5, isAbsolute, join as join7, relative, resolve as resolve2, sep } from "node:path";
|
|
4607
5686
|
var FILE_MODE = 384;
|
|
4608
5687
|
var DIRECTORY_MODE = 448;
|
|
4609
5688
|
async function writePushedFile(request) {
|
|
@@ -4636,7 +5715,7 @@ async function writePushedFile(request) {
|
|
|
4636
5715
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
4637
5716
|
dirname5(candidate)
|
|
4638
5717
|
);
|
|
4639
|
-
const realTarget =
|
|
5718
|
+
const realTarget = join7(existingAncestor, ...missingSegments, basename(candidate));
|
|
4640
5719
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
4641
5720
|
if (allowedDirectory === null) {
|
|
4642
5721
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -4672,7 +5751,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
4672
5751
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
4673
5752
|
return null;
|
|
4674
5753
|
}
|
|
4675
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
5754
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4676
5755
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
4677
5756
|
return null;
|
|
4678
5757
|
}
|
|
@@ -4745,16 +5824,16 @@ function contains(realDirectory, realTarget) {
|
|
|
4745
5824
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
4746
5825
|
let current = existingAncestor;
|
|
4747
5826
|
for (const segment of missingSegments) {
|
|
4748
|
-
current =
|
|
5827
|
+
current = join7(current, segment);
|
|
4749
5828
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
4750
5829
|
await chmod(current, DIRECTORY_MODE);
|
|
4751
5830
|
}
|
|
4752
5831
|
}
|
|
4753
5832
|
async function writeAtomically(realTarget, content) {
|
|
4754
|
-
const temporaryPath =
|
|
5833
|
+
const temporaryPath = join7(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
4755
5834
|
let handle;
|
|
4756
5835
|
try {
|
|
4757
|
-
handle = await
|
|
5836
|
+
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
4758
5837
|
await handle.writeFile(content);
|
|
4759
5838
|
await handle.chmod(FILE_MODE);
|
|
4760
5839
|
await handle.close();
|
|
@@ -4881,12 +5960,12 @@ var NOT_APPLIED = {
|
|
|
4881
5960
|
opencodeAuthApplied: false
|
|
4882
5961
|
};
|
|
4883
5962
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
4884
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4885
|
-
return expanded ===
|
|
5963
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5964
|
+
return expanded === join8(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
4886
5965
|
}
|
|
4887
5966
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
4888
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4889
|
-
return expanded ===
|
|
5967
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
5968
|
+
return expanded === join8(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
4890
5969
|
}
|
|
4891
5970
|
async function applyOne(options, file) {
|
|
4892
5971
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -5046,6 +6125,10 @@ var DEFAULT_RETRY_POLICY = {
|
|
|
5046
6125
|
baseDelayMs: 500,
|
|
5047
6126
|
maxDelayMs: 3e4
|
|
5048
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;
|
|
5049
6132
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
5050
6133
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
5051
6134
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
@@ -5107,6 +6190,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5107
6190
|
maxActiveSessions;
|
|
5108
6191
|
watcherStallMs;
|
|
5109
6192
|
wedgeWarningIntervalMs;
|
|
6193
|
+
openCodeClient;
|
|
5110
6194
|
/** Cache of conversationId → opencode sessionId. */
|
|
5111
6195
|
sessions = /* @__PURE__ */ new Map();
|
|
5112
6196
|
/**
|
|
@@ -5186,6 +6270,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5186
6270
|
* message; it is removed once its in-flight set empties.
|
|
5187
6271
|
*/
|
|
5188
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();
|
|
5189
6284
|
/**
|
|
5190
6285
|
* AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
|
|
5191
6286
|
* dispatched and are still in-flight. A message in this set is never
|
|
@@ -5204,20 +6299,23 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5204
6299
|
*/
|
|
5205
6300
|
readopted = /* @__PURE__ */ new Set();
|
|
5206
6301
|
/**
|
|
5207
|
-
*
|
|
5208
|
-
*
|
|
5209
|
-
*
|
|
5210
|
-
*
|
|
5211
|
-
*
|
|
5212
|
-
*
|
|
5213
|
-
* CRITICAL (Bugbot #202): this suppresses ONLY the dispatch/re-attach paths, it
|
|
5214
|
-
* does NOT suppress DONE delivery. A row parked here whose reply later COMPLETES
|
|
5215
|
-
* in opencode must still be delivered via `markDone` on the next drain — so
|
|
5216
|
-
* `readoptOne` computes `state` FIRST and this set is checked only on the
|
|
5217
|
-
* non-done path. It is cleared once the row leaves the processing list (cron
|
|
5218
|
-
* 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.
|
|
5219
6308
|
*/
|
|
5220
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();
|
|
5221
6319
|
/**
|
|
5222
6320
|
* "markDone for this row is TERMINALLY undeliverable" (Bug 4). Set ONLY when a
|
|
5223
6321
|
* re-adopted DONE row's `markDone` returned a terminal 4xx (a status that will
|
|
@@ -5340,7 +6438,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5340
6438
|
*/
|
|
5341
6439
|
attachmentsSkippedSignalled = /* @__PURE__ */ new Set();
|
|
5342
6440
|
/**
|
|
5343
|
-
* Cache of the opencode root directory
|
|
6441
|
+
* Cache of the opencode root directory from the selected client's location lookup.
|
|
6442
|
+
* Resolved lazily on
|
|
5344
6443
|
* first session creation so drain-created sessions are rooted at the project
|
|
5345
6444
|
* directory and thus visible in `opencode web`'s session list. `undefined` =
|
|
5346
6445
|
* not yet resolved; `null` = resolved-but-unavailable (don't keep retrying).
|
|
@@ -5369,6 +6468,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5369
6468
|
* no watcher) can resolve the title.
|
|
5370
6469
|
*/
|
|
5371
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();
|
|
5372
6478
|
/** Serialises drains so a reconnect during a drain doesn't double-process. */
|
|
5373
6479
|
draining = false;
|
|
5374
6480
|
/**
|
|
@@ -5427,20 +6533,55 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5427
6533
|
config.fetchImpl ?? fetch,
|
|
5428
6534
|
config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
|
|
5429
6535
|
);
|
|
6536
|
+
this.openCodeClient = config.openCodeClient ?? createOpenCodeClient({
|
|
6537
|
+
port: config.port,
|
|
6538
|
+
version: "v1",
|
|
6539
|
+
fetchImpl: config.fetchImpl
|
|
6540
|
+
});
|
|
5430
6541
|
this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5431
6542
|
this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
|
|
5432
6543
|
this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
|
|
5433
6544
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
5434
6545
|
this.now = config.now ?? (() => Date.now());
|
|
5435
6546
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
5436
|
-
this.homeDir = config.homeDir ??
|
|
6547
|
+
this.homeDir = config.homeDir ?? homedir5();
|
|
5437
6548
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
5438
6549
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
5439
6550
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
5440
6551
|
}
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
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);
|
|
5444
6585
|
}
|
|
5445
6586
|
/**
|
|
5446
6587
|
* Drain all pending channel conversations once: poll → dispatch → register.
|
|
@@ -5518,6 +6659,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5518
6659
|
async runDrain() {
|
|
5519
6660
|
let dispatched = 0;
|
|
5520
6661
|
try {
|
|
6662
|
+
this.pendingMessageIds.clear();
|
|
5521
6663
|
const conversations = await this.getPendingConversations();
|
|
5522
6664
|
if (this.recycleRequestedFlag) {
|
|
5523
6665
|
this.stop();
|
|
@@ -5583,6 +6725,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5583
6725
|
}
|
|
5584
6726
|
return ids;
|
|
5585
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
|
+
}
|
|
5586
6743
|
/**
|
|
5587
6744
|
* File-pull work, for `run.ts`'s idle accounting (#559).
|
|
5588
6745
|
*
|
|
@@ -5649,6 +6806,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5649
6806
|
*/
|
|
5650
6807
|
stop() {
|
|
5651
6808
|
this.stopped = true;
|
|
6809
|
+
this.sessionErrorStream?.abort.abort();
|
|
6810
|
+
this.sessionErrorStream = null;
|
|
5652
6811
|
}
|
|
5653
6812
|
/**
|
|
5654
6813
|
* The server clears this request when a new MicroVM identity is recorded, so a
|
|
@@ -5723,7 +6882,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5723
6882
|
*/
|
|
5724
6883
|
async processConversation(conv) {
|
|
5725
6884
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
6885
|
+
this.ensureSessionErrorStream();
|
|
5726
6886
|
const messages = await this.getPendingMessages(conv.id);
|
|
6887
|
+
for (const message of messages) this.pendingMessageIds.add(message.id);
|
|
5727
6888
|
let dispatched = 0;
|
|
5728
6889
|
let skippedAlreadyDispatched = 0;
|
|
5729
6890
|
if (refusedSessionId && messages.length > 0) {
|
|
@@ -5737,6 +6898,15 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5737
6898
|
skippedAlreadyDispatched += 1;
|
|
5738
6899
|
continue;
|
|
5739
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
|
+
}
|
|
5740
6910
|
const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
|
|
5741
6911
|
if (effectiveOpencodeMessageId) {
|
|
5742
6912
|
const outcome = await this.resolveRedrive(
|
|
@@ -5765,15 +6935,55 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5765
6935
|
conversation_id: conv.id,
|
|
5766
6936
|
message_id: message.id
|
|
5767
6937
|
});
|
|
5768
|
-
const sendAttachments = this.buildSendAttachments(conv, message);
|
|
5769
|
-
|
|
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(
|
|
5770
6953
|
sessionId,
|
|
5771
|
-
() => sendPromptAsync(
|
|
6954
|
+
() => sendPromptAsync(
|
|
6955
|
+
this.port,
|
|
6956
|
+
sessionId,
|
|
6957
|
+
message.content,
|
|
6958
|
+
options,
|
|
6959
|
+
sendAttachments,
|
|
6960
|
+
this.openCodeClient
|
|
6961
|
+
)
|
|
5772
6962
|
);
|
|
5773
6963
|
} catch (err) {
|
|
5774
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
|
+
}
|
|
5775
6985
|
this.dispatched.delete(message.id);
|
|
5776
|
-
const exists = await sessionExists(
|
|
6986
|
+
const exists = await this.sessionExists(sessionId);
|
|
5777
6987
|
if (exists === false) {
|
|
5778
6988
|
this.sessions.delete(conv.id);
|
|
5779
6989
|
this.log({
|
|
@@ -5795,7 +7005,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5795
7005
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
5796
7006
|
break;
|
|
5797
7007
|
}
|
|
5798
|
-
const
|
|
7008
|
+
const errorMessage3 = err instanceof Error ? err.message : String(err);
|
|
5799
7009
|
this.sessions.delete(conv.id);
|
|
5800
7010
|
this.supersede(conv.id, sessionId);
|
|
5801
7011
|
this.log({
|
|
@@ -5804,7 +7014,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5804
7014
|
conversation_id: conv.id,
|
|
5805
7015
|
message_id: message.id
|
|
5806
7016
|
});
|
|
5807
|
-
await this.markFailed(conv.id, message.id, null,
|
|
7017
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5808
7018
|
this.log({
|
|
5809
7019
|
level: "warn",
|
|
5810
7020
|
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -5815,13 +7025,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5815
7025
|
});
|
|
5816
7026
|
this.log({
|
|
5817
7027
|
level: "error",
|
|
5818
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
7028
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
|
|
5819
7029
|
conversation_id: conv.id,
|
|
5820
7030
|
message_id: message.id
|
|
5821
7031
|
});
|
|
5822
7032
|
break;
|
|
5823
7033
|
}
|
|
5824
7034
|
if (opencodeMessageId === null) {
|
|
7035
|
+
if (this.isV2) {
|
|
7036
|
+
throw new Error("V2 prompt dispatch completed without an acknowledged message id");
|
|
7037
|
+
}
|
|
5825
7038
|
const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
|
|
5826
7039
|
if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
5827
7040
|
this.log({
|
|
@@ -5836,14 +7049,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5836
7049
|
this.unconfirmedDispatchFailures.delete(message.id);
|
|
5837
7050
|
this.sessions.delete(conv.id);
|
|
5838
7051
|
this.supersede(conv.id, sessionId);
|
|
5839
|
-
const
|
|
7052
|
+
const errorMessage3 = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
|
|
5840
7053
|
this.log({
|
|
5841
7054
|
level: "error",
|
|
5842
|
-
message:
|
|
7055
|
+
message: errorMessage3,
|
|
5843
7056
|
conversation_id: conv.id,
|
|
5844
7057
|
message_id: message.id
|
|
5845
7058
|
});
|
|
5846
|
-
await this.markFailed(conv.id, message.id, null,
|
|
7059
|
+
await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
|
|
5847
7060
|
this.log({
|
|
5848
7061
|
level: "warn",
|
|
5849
7062
|
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -5969,29 +7182,38 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5969
7182
|
*/
|
|
5970
7183
|
async pollSessionMessagesForRedrive(conv, message, sessionId) {
|
|
5971
7184
|
try {
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
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 };
|
|
5983
7197
|
}
|
|
5984
|
-
const
|
|
5985
|
-
|
|
7198
|
+
const polledV1 = await pollSessionMessagesForRedrive(
|
|
7199
|
+
this.port,
|
|
7200
|
+
sessionId,
|
|
7201
|
+
this.openCodeClient
|
|
7202
|
+
);
|
|
7203
|
+
if (!polledV1.ok) {
|
|
7204
|
+
const normalized = normalizeRedrivePollFailureBody(polledV1.body);
|
|
5986
7205
|
this.log({
|
|
5987
7206
|
level: "warn",
|
|
5988
|
-
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`,
|
|
5989
7208
|
conversation_id: conv.id,
|
|
5990
7209
|
message_id: message.id
|
|
5991
7210
|
});
|
|
5992
|
-
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
|
+
};
|
|
5993
7215
|
}
|
|
5994
|
-
return { ok: true, messages:
|
|
7216
|
+
return { ok: true, messages: polledV1.messages };
|
|
5995
7217
|
} catch (err) {
|
|
5996
7218
|
this.log({
|
|
5997
7219
|
level: "warn",
|
|
@@ -6050,11 +7272,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6050
7272
|
}
|
|
6051
7273
|
const state = messageRunState(messages, ocId ?? "");
|
|
6052
7274
|
if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
|
|
6053
|
-
const ongoing = await isSessionOngoing(
|
|
7275
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
6054
7276
|
if (ongoing === false) {
|
|
6055
7277
|
this.log({
|
|
6056
7278
|
level: "info",
|
|
6057
|
-
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`,
|
|
6058
7280
|
conversation_id: conv.id,
|
|
6059
7281
|
message_id: message.id
|
|
6060
7282
|
});
|
|
@@ -6067,8 +7289,25 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6067
7289
|
return this.settleRedrive(conv, sessionId, message, ocId, messages, state);
|
|
6068
7290
|
}
|
|
6069
7291
|
if (state === "running" || state === "queued") {
|
|
6070
|
-
const ongoing = await isSessionOngoing(
|
|
7292
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
6071
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
|
+
}
|
|
6072
7311
|
return this.reattachRedrive(conv, sessionId, message, ocId);
|
|
6073
7312
|
}
|
|
6074
7313
|
if (ongoing === false) {
|
|
@@ -6158,16 +7397,37 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6158
7397
|
if (state === "done") {
|
|
6159
7398
|
const title = await this.resolveSessionTitle(sessionId, conv.id);
|
|
6160
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
|
+
);
|
|
6161
7406
|
this.log({
|
|
6162
7407
|
level: "info",
|
|
6163
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`,
|
|
6164
7409
|
conversation_id: conv.id,
|
|
6165
7410
|
message_id: message.id
|
|
6166
7411
|
});
|
|
6167
|
-
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
|
+
);
|
|
6168
7422
|
} else {
|
|
6169
7423
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
6170
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
|
+
);
|
|
6171
7431
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
6172
7432
|
this.log({
|
|
6173
7433
|
level: "error",
|
|
@@ -6175,7 +7435,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6175
7435
|
conversation_id: conv.id,
|
|
6176
7436
|
message_id: message.id
|
|
6177
7437
|
});
|
|
6178
|
-
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
|
+
);
|
|
6179
7448
|
}
|
|
6180
7449
|
if (ocId !== null) {
|
|
6181
7450
|
await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
|
|
@@ -6480,7 +7749,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6480
7749
|
};
|
|
6481
7750
|
}
|
|
6482
7751
|
if (bound) {
|
|
6483
|
-
const exists = await sessionExists(
|
|
7752
|
+
const exists = await this.sessionExists(bound);
|
|
6484
7753
|
if (exists === false) {
|
|
6485
7754
|
this.log({
|
|
6486
7755
|
level: "debug",
|
|
@@ -6513,7 +7782,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6513
7782
|
*/
|
|
6514
7783
|
async createAndBindSession(conversationId) {
|
|
6515
7784
|
const directory = await this.resolveOpenCodeDirectory();
|
|
6516
|
-
const sessionId = await createOpenCodeSession(
|
|
7785
|
+
const sessionId = await this.createOpenCodeSession(directory);
|
|
6517
7786
|
this.sessions.set(conversationId, sessionId);
|
|
6518
7787
|
await this.persistSession(conversationId, sessionId).catch((err) => {
|
|
6519
7788
|
this.log({
|
|
@@ -6525,17 +7794,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6525
7794
|
return sessionId;
|
|
6526
7795
|
}
|
|
6527
7796
|
/**
|
|
6528
|
-
* 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.
|
|
6529
7799
|
* Resolved once per driver: `undefined` until first lookup, then the directory
|
|
6530
|
-
* 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).
|
|
6531
7801
|
*/
|
|
6532
7802
|
async resolveOpenCodeDirectory() {
|
|
6533
7803
|
if (this.opencodeDirectory !== void 0) return this.opencodeDirectory;
|
|
6534
|
-
this.opencodeDirectory = await getOpenCodeDirectory(
|
|
7804
|
+
this.opencodeDirectory = await this.getOpenCodeDirectory();
|
|
6535
7805
|
if (!this.opencodeDirectory) {
|
|
6536
7806
|
this.log({
|
|
6537
7807
|
level: "warn",
|
|
6538
|
-
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"
|
|
6539
7809
|
});
|
|
6540
7810
|
}
|
|
6541
7811
|
return this.opencodeDirectory;
|
|
@@ -6719,6 +7989,24 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6719
7989
|
ambiguousPinnedSinceMs: 0,
|
|
6720
7990
|
ambiguousResolved: false
|
|
6721
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
|
+
}
|
|
6722
8010
|
}
|
|
6723
8011
|
/**
|
|
6724
8012
|
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
@@ -6923,6 +8211,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6923
8211
|
ensureWatcherRunning(sessionId) {
|
|
6924
8212
|
const watcher = this.watchers.get(sessionId);
|
|
6925
8213
|
if (!watcher) return;
|
|
8214
|
+
this.ensureSessionErrorStream();
|
|
6926
8215
|
if (watcher.loop) return;
|
|
6927
8216
|
if (watcher.inFlight.size === 0) {
|
|
6928
8217
|
this.watchers.delete(sessionId);
|
|
@@ -6938,6 +8227,154 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6938
8227
|
});
|
|
6939
8228
|
watcher.loop = loop;
|
|
6940
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
|
|
8372
|
+
});
|
|
8373
|
+
}
|
|
8374
|
+
} finally {
|
|
8375
|
+
this.sessionErrorHandled.delete(inFlight.evidentMessageId);
|
|
8376
|
+
}
|
|
8377
|
+
}
|
|
6941
8378
|
/**
|
|
6942
8379
|
* The per-session polling loop (WI-3). Once per tick it:
|
|
6943
8380
|
* 1. polls `GET /session/:id/message` once and, per in-flight message,
|
|
@@ -6945,7 +8382,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6945
8382
|
* markDone (done) exactly once per transition;
|
|
6946
8383
|
* 2. applies the idle-path re-dispatch guard (a dispatched message that never
|
|
6947
8384
|
* APPEARS → re-dispatch — D1 obligation 2);
|
|
6948
|
-
* 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
|
|
6949
8387
|
* NEW ones via `reportInteraction`, carrying the PAUSED message's own
|
|
6950
8388
|
* `source_message_id`;
|
|
6951
8389
|
* 4. drops messages that completed or timed out from the in-flight set.
|
|
@@ -6971,11 +8409,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6971
8409
|
if (watcher.generation !== generation) return;
|
|
6972
8410
|
let messages = null;
|
|
6973
8411
|
try {
|
|
6974
|
-
|
|
6975
|
-
if (res.ok) {
|
|
6976
|
-
const body = await res.json();
|
|
6977
|
-
messages = Array.isArray(body) ? body : null;
|
|
6978
|
-
}
|
|
8412
|
+
messages = await this.getSessionMessages(sessionId);
|
|
6979
8413
|
} catch {
|
|
6980
8414
|
}
|
|
6981
8415
|
if (messages != null && messages.length > 0) {
|
|
@@ -7050,6 +8484,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7050
8484
|
const conv = watcher.conv;
|
|
7051
8485
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7052
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
|
+
}
|
|
7053
8502
|
if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
|
|
7054
8503
|
else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
|
|
7055
8504
|
if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
|
|
@@ -7103,6 +8552,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7103
8552
|
message_id: inFlight.evidentMessageId
|
|
7104
8553
|
});
|
|
7105
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
|
+
);
|
|
7106
8561
|
const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
|
|
7107
8562
|
try {
|
|
7108
8563
|
await this.markFailed(
|
|
@@ -7111,7 +8566,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7111
8566
|
sessionId,
|
|
7112
8567
|
error2,
|
|
7113
8568
|
usage,
|
|
7114
|
-
failure
|
|
8569
|
+
failure,
|
|
8570
|
+
usageAgentName,
|
|
8571
|
+
subagentInvocations
|
|
7115
8572
|
);
|
|
7116
8573
|
} catch (err) {
|
|
7117
8574
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7154,9 +8611,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7154
8611
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7155
8612
|
return;
|
|
7156
8613
|
}
|
|
8614
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
|
|
8615
|
+
const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
|
|
7157
8616
|
const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
|
|
7158
8617
|
const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
|
|
7159
|
-
if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
|
|
8618
|
+
if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
|
|
7160
8619
|
inFlight.stuckReported = true;
|
|
7161
8620
|
void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
|
|
7162
8621
|
stuck_for_ms: this.now() - inFlight.dispatchedAt
|
|
@@ -7183,7 +8642,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7183
8642
|
inFlight.b2LastDescendantCheckMs = this.now();
|
|
7184
8643
|
const [descendantOngoing, rootOngoing] = await Promise.all([
|
|
7185
8644
|
this.isAnyDescendantSessionOngoing(sessionId),
|
|
7186
|
-
isSessionOngoing(
|
|
8645
|
+
this.isSessionOngoing(sessionId)
|
|
7187
8646
|
]);
|
|
7188
8647
|
if (isB2AbandonmentConfirmed({
|
|
7189
8648
|
pinnedForMs,
|
|
@@ -7243,7 +8702,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7243
8702
|
});
|
|
7244
8703
|
}
|
|
7245
8704
|
const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;
|
|
7246
|
-
const ongoing = await isSessionOngoing(
|
|
8705
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
7247
8706
|
if (isAmbiguousFinishResolved({
|
|
7248
8707
|
pinnedForMs,
|
|
7249
8708
|
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
@@ -7317,7 +8776,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7317
8776
|
const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
|
|
7318
8777
|
(sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
|
|
7319
8778
|
);
|
|
7320
|
-
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
8779
|
+
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
|
|
7321
8780
|
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
7322
8781
|
this.log({
|
|
7323
8782
|
level: "debug",
|
|
@@ -7352,6 +8811,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7352
8811
|
});
|
|
7353
8812
|
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
7354
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
|
+
);
|
|
7355
8820
|
try {
|
|
7356
8821
|
await this.markDone(
|
|
7357
8822
|
conv.id,
|
|
@@ -7359,7 +8824,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7359
8824
|
sessionId,
|
|
7360
8825
|
inFlight.opencodeMessageId,
|
|
7361
8826
|
title,
|
|
7362
|
-
usage
|
|
8827
|
+
usage,
|
|
8828
|
+
usageAgentName,
|
|
8829
|
+
subagentInvocations
|
|
7363
8830
|
);
|
|
7364
8831
|
} catch (err) {
|
|
7365
8832
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7418,7 +8885,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7418
8885
|
*/
|
|
7419
8886
|
async readoptProcessing() {
|
|
7420
8887
|
const rows = await this.getProcessingMessages();
|
|
7421
|
-
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) {
|
|
7422
8889
|
const stillProcessing = new Set(rows.map((r) => r.id));
|
|
7423
8890
|
for (const id of [
|
|
7424
8891
|
...this.dontRedispatch,
|
|
@@ -7438,6 +8905,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7438
8905
|
}
|
|
7439
8906
|
}
|
|
7440
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
|
+
}
|
|
7441
8918
|
}
|
|
7442
8919
|
if (rows.length === 0) return;
|
|
7443
8920
|
const bySession = /* @__PURE__ */ new Map();
|
|
@@ -7458,23 +8935,32 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7458
8935
|
for (const [sessionId, sessionRows] of bySession) {
|
|
7459
8936
|
let messages;
|
|
7460
8937
|
try {
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
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;
|
|
7476
8963
|
}
|
|
7477
|
-
messages = body;
|
|
7478
8964
|
} catch (err) {
|
|
7479
8965
|
this.log({
|
|
7480
8966
|
level: "warn",
|
|
@@ -7483,7 +8969,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7483
8969
|
continue;
|
|
7484
8970
|
}
|
|
7485
8971
|
const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
|
|
7486
|
-
const sessionOngoing = anyUntracked ? await isSessionOngoing(
|
|
8972
|
+
const sessionOngoing = anyUntracked ? await this.isSessionOngoing(sessionId) : null;
|
|
7487
8973
|
for (const row of sessionRows) {
|
|
7488
8974
|
await this.readoptOne(sessionId, row, messages, sessionOngoing);
|
|
7489
8975
|
}
|
|
@@ -7530,7 +9016,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7530
9016
|
if (restartAborted) {
|
|
7531
9017
|
this.log({
|
|
7532
9018
|
level: "info",
|
|
7533
|
-
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`,
|
|
7534
9020
|
conversation_id: row.conversation_id,
|
|
7535
9021
|
message_id: row.id
|
|
7536
9022
|
});
|
|
@@ -7538,6 +9024,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7538
9024
|
if (state === "failed" && !restartAborted) {
|
|
7539
9025
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
7540
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
|
+
);
|
|
7541
9033
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
7542
9034
|
this.log({
|
|
7543
9035
|
level: "error",
|
|
@@ -7546,7 +9038,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7546
9038
|
message_id: row.id
|
|
7547
9039
|
});
|
|
7548
9040
|
try {
|
|
7549
|
-
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
|
+
);
|
|
7550
9051
|
} catch (err) {
|
|
7551
9052
|
if (err instanceof ChannelAuthError) throw err;
|
|
7552
9053
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7570,10 +9071,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7570
9071
|
}
|
|
7571
9072
|
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
7572
9073
|
this.dontRedispatch.delete(row.id);
|
|
9074
|
+
this.untrackableAck.delete(row.id);
|
|
7573
9075
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
7574
9076
|
return;
|
|
7575
9077
|
}
|
|
7576
|
-
if (this.dontRedispatch.has(row.id)) {
|
|
9078
|
+
if (this.dontRedispatch.has(row.id) || this.untrackableAck.has(row.id)) {
|
|
7577
9079
|
this.log({
|
|
7578
9080
|
level: "debug",
|
|
7579
9081
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
@@ -7593,7 +9095,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7593
9095
|
const finish = reply?.info?.finish ?? reply?.finish;
|
|
7594
9096
|
this.log({
|
|
7595
9097
|
level: "info",
|
|
7596
|
-
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`,
|
|
7597
9099
|
conversation_id: row.conversation_id,
|
|
7598
9100
|
message_id: row.id
|
|
7599
9101
|
});
|
|
@@ -7602,7 +9104,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7602
9104
|
}
|
|
7603
9105
|
this.log({
|
|
7604
9106
|
level: "info",
|
|
7605
|
-
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)`,
|
|
7606
9108
|
conversation_id: row.conversation_id,
|
|
7607
9109
|
message_id: row.id
|
|
7608
9110
|
});
|
|
@@ -7612,7 +9114,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7612
9114
|
if (ongoing === true) {
|
|
7613
9115
|
this.log({
|
|
7614
9116
|
level: "debug",
|
|
7615
|
-
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)`,
|
|
7616
9118
|
conversation_id: row.conversation_id,
|
|
7617
9119
|
message_id: row.id
|
|
7618
9120
|
});
|
|
@@ -7620,7 +9122,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7620
9122
|
if (shape === "b1") {
|
|
7621
9123
|
this.log({
|
|
7622
9124
|
level: "debug",
|
|
7623
|
-
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`,
|
|
7624
9126
|
conversation_id: row.conversation_id,
|
|
7625
9127
|
message_id: row.id
|
|
7626
9128
|
});
|
|
@@ -7632,7 +9134,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7632
9134
|
}
|
|
7633
9135
|
this.log({
|
|
7634
9136
|
level: "debug",
|
|
7635
|
-
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`,
|
|
7636
9138
|
conversation_id: row.conversation_id,
|
|
7637
9139
|
message_id: row.id
|
|
7638
9140
|
});
|
|
@@ -7682,7 +9184,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7682
9184
|
* extracted (#1493 Task 2.4) so the ambiguous-finish guard above can call the
|
|
7683
9185
|
* SAME delivery instead of duplicating it.
|
|
7684
9186
|
*
|
|
7685
|
-
* EVEN IF the row was previously parked
|
|
9187
|
+
* EVEN IF the row was previously parked by either recovery fence (a give-up stops
|
|
7686
9188
|
* re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY like the watcher's
|
|
7687
9189
|
* `settleMessageDone`: auth re-throws; terminal → park in `doneUndeliverable` +
|
|
7688
9190
|
* leave for cron; transient → log + leave for the next drain (the still-
|
|
@@ -7708,7 +9210,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7708
9210
|
try {
|
|
7709
9211
|
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
7710
9212
|
const usage = messageUsage(messages, ocId ?? "");
|
|
7711
|
-
|
|
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
|
+
);
|
|
7712
9229
|
} catch (err) {
|
|
7713
9230
|
if (err instanceof ChannelAuthError) throw err;
|
|
7714
9231
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7734,6 +9251,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7734
9251
|
await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
|
|
7735
9252
|
}
|
|
7736
9253
|
this.dontRedispatch.delete(row.id);
|
|
9254
|
+
this.untrackableAck.delete(row.id);
|
|
7737
9255
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
7738
9256
|
}
|
|
7739
9257
|
/**
|
|
@@ -7797,19 +9315,90 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7797
9315
|
conversation_id: row.conversation_id,
|
|
7798
9316
|
message_id: row.id
|
|
7799
9317
|
});
|
|
7800
|
-
this.awaitingReadopt.add(row.id);
|
|
9318
|
+
if (!this.isV2) this.awaitingReadopt.add(row.id);
|
|
7801
9319
|
const readoptConv = this.convForRow(sessionId, row);
|
|
7802
9320
|
const readoptMessage = this.queuedMessageForRow(row);
|
|
7803
|
-
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
|
+
}
|
|
7804
9335
|
let ocId;
|
|
7805
9336
|
try {
|
|
7806
|
-
ocId = await this.dispatchLocked(
|
|
9337
|
+
ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
|
|
7807
9338
|
sessionId,
|
|
7808
|
-
() => sendPromptAsync(
|
|
9339
|
+
() => sendPromptAsync(
|
|
9340
|
+
this.port,
|
|
9341
|
+
sessionId,
|
|
9342
|
+
row.content,
|
|
9343
|
+
options,
|
|
9344
|
+
sendAttachments,
|
|
9345
|
+
this.openCodeClient
|
|
9346
|
+
)
|
|
7809
9347
|
);
|
|
7810
9348
|
} catch (err) {
|
|
7811
9349
|
this.awaitingReadopt.delete(row.id);
|
|
7812
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
|
+
}
|
|
7813
9402
|
this.log({
|
|
7814
9403
|
level: "warn",
|
|
7815
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)}`,
|
|
@@ -7826,14 +9415,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7826
9415
|
this.unconfirmedDispatchFailures.delete(row.id);
|
|
7827
9416
|
this.sessions.delete(readoptConv.id);
|
|
7828
9417
|
this.supersede(readoptConv.id, sessionId);
|
|
7829
|
-
const
|
|
9418
|
+
const errorMessage3 = `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
|
|
7830
9419
|
this.log({
|
|
7831
9420
|
level: "error",
|
|
7832
|
-
message:
|
|
9421
|
+
message: errorMessage3,
|
|
7833
9422
|
conversation_id: row.conversation_id,
|
|
7834
9423
|
message_id: row.id
|
|
7835
9424
|
});
|
|
7836
|
-
await this.markFailed(row.conversation_id, row.id, null,
|
|
9425
|
+
await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
|
|
7837
9426
|
this.log({
|
|
7838
9427
|
level: "warn",
|
|
7839
9428
|
message: `markFailed PATCH for message ${row.id.slice(0, 8)} (conversation ${row.conversation_id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -7941,12 +9530,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7941
9530
|
this.dispatched.delete(evidentMessageId);
|
|
7942
9531
|
}
|
|
7943
9532
|
/**
|
|
7944
|
-
* Poll `/question` + `/permission
|
|
7945
|
-
* via `reportInteraction` (Task 3.5),
|
|
7946
|
-
* `source_message_id` so the server @mentions
|
|
7947
|
-
* concurrency. Dedups by interaction id across ticks
|
|
9533
|
+
* Poll V1's global `/question` + `/permission`, or V2's watched-session form and
|
|
9534
|
+
* permission routes, and surface NEW ones via `reportInteraction` (Task 3.5),
|
|
9535
|
+
* carrying the PAUSED message's own `source_message_id` so the server @mentions
|
|
9536
|
+
* the correct person under concurrency. Dedups by interaction id across ticks
|
|
9537
|
+
* (reused per-session sets).
|
|
7948
9538
|
*
|
|
7949
|
-
* The interaction is attributed to the in-flight message it paused on.
|
|
9539
|
+
* The interaction is attributed to the in-flight message it paused on. OpenCode
|
|
7950
9540
|
* stamps a `messageID` on a permission (and `tool.messageID` on a question) =
|
|
7951
9541
|
* the assistant message id, whose `parentID` is the user message id — but the
|
|
7952
9542
|
* simplest robust attribution here is: the single in-flight message that is
|
|
@@ -7969,16 +9559,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7969
9559
|
let permissionsPolledOk = true;
|
|
7970
9560
|
let questions = [];
|
|
7971
9561
|
try {
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
questions = body;
|
|
7977
|
-
} else {
|
|
7978
|
-
questionsPolledOk = false;
|
|
7979
|
-
}
|
|
9562
|
+
if (this.isV2) {
|
|
9563
|
+
const forms = await listV2Forms(this.openCodeClient, sessionId);
|
|
9564
|
+
if (forms === null) questionsPolledOk = false;
|
|
9565
|
+
else questions = forms;
|
|
7980
9566
|
} else {
|
|
7981
|
-
|
|
9567
|
+
const listed = await listOpenCodeQuestions(this.port, this.openCodeClient);
|
|
9568
|
+
if (listed === null) questionsPolledOk = false;
|
|
9569
|
+
else questions = listed;
|
|
7982
9570
|
}
|
|
7983
9571
|
} catch {
|
|
7984
9572
|
questionsPolledOk = false;
|
|
@@ -7998,16 +9586,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7998
9586
|
}
|
|
7999
9587
|
let permissions = [];
|
|
8000
9588
|
try {
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
permissions = body;
|
|
8006
|
-
} else {
|
|
8007
|
-
permissionsPolledOk = false;
|
|
8008
|
-
}
|
|
9589
|
+
if (this.isV2) {
|
|
9590
|
+
const listed = await listV2Permissions(this.openCodeClient, sessionId);
|
|
9591
|
+
if (listed === null) permissionsPolledOk = false;
|
|
9592
|
+
else permissions = listed;
|
|
8009
9593
|
} else {
|
|
8010
|
-
|
|
9594
|
+
const listed = await listOpenCodePermissions(this.port, this.openCodeClient);
|
|
9595
|
+
if (listed === null) permissionsPolledOk = false;
|
|
9596
|
+
else permissions = listed;
|
|
8011
9597
|
}
|
|
8012
9598
|
} catch {
|
|
8013
9599
|
permissionsPolledOk = false;
|
|
@@ -8101,10 +9687,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8101
9687
|
if (cached !== void 0) return cached;
|
|
8102
9688
|
let parent = void 0;
|
|
8103
9689
|
try {
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
9690
|
+
if (this.isV2) {
|
|
9691
|
+
const session = await getV2Session(this.openCodeClient, sessionId);
|
|
9692
|
+
parent = null;
|
|
9693
|
+
const candidate = session.parentID;
|
|
9694
|
+
if (typeof candidate === "string") parent = candidate;
|
|
9695
|
+
} else {
|
|
9696
|
+
const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
|
|
9697
|
+
parent = typeof body?.parentID === "string" ? body.parentID : body === null ? void 0 : null;
|
|
8108
9698
|
}
|
|
8109
9699
|
} catch {
|
|
8110
9700
|
parent = void 0;
|
|
@@ -8112,6 +9702,164 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8112
9702
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
8113
9703
|
return parent;
|
|
8114
9704
|
}
|
|
9705
|
+
usageAgentName(messages, userMessageId) {
|
|
9706
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
9707
|
+
const mode = reply?.info?.mode;
|
|
9708
|
+
if (typeof mode === "string" && mode.length > 0) return mode;
|
|
9709
|
+
const agent = reply?.info?.agent;
|
|
9710
|
+
return typeof agent === "string" && agent.length > 0 ? agent : null;
|
|
9711
|
+
}
|
|
9712
|
+
async resolveSubagentInvocations(messages, userMessageId, messageId, phase = "terminal") {
|
|
9713
|
+
if (!messages) return void 0;
|
|
9714
|
+
const cache = phase === "prefetch" ? this.subagentInvocationPrefetches : this.subagentInvocationCollections;
|
|
9715
|
+
const cached = cache.get(messageId);
|
|
9716
|
+
if (cached) return cached;
|
|
9717
|
+
const collection = this.buildSubagentInvocations(messages, userMessageId, messageId).catch(
|
|
9718
|
+
(err) => {
|
|
9719
|
+
this.log({
|
|
9720
|
+
level: "warn",
|
|
9721
|
+
message: `Best-effort subagent usage collection failed for message ${messageId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
9722
|
+
message_id: messageId
|
|
9723
|
+
});
|
|
9724
|
+
return void 0;
|
|
9725
|
+
}
|
|
9726
|
+
);
|
|
9727
|
+
cache.set(messageId, collection);
|
|
9728
|
+
const result = await collection;
|
|
9729
|
+
if (result === void 0 && cache.get(messageId) === collection) cache.delete(messageId);
|
|
9730
|
+
return result;
|
|
9731
|
+
}
|
|
9732
|
+
clearSubagentInvocationCaches(messageId) {
|
|
9733
|
+
this.subagentInvocationCollections.delete(messageId);
|
|
9734
|
+
this.subagentInvocationPrefetches.delete(messageId);
|
|
9735
|
+
}
|
|
9736
|
+
async buildSubagentInvocations(messages, userMessageId, messageId) {
|
|
9737
|
+
const rootCalls = collectTaskCalls(messages, userMessageId);
|
|
9738
|
+
if (rootCalls.length === 0) return void 0;
|
|
9739
|
+
const childMessages = /* @__PURE__ */ new Map();
|
|
9740
|
+
const seenCallIds = new Set(rootCalls.map((call) => call.callID));
|
|
9741
|
+
const work = rootCalls.map((call) => ({
|
|
9742
|
+
call,
|
|
9743
|
+
depth: 1
|
|
9744
|
+
}));
|
|
9745
|
+
const payload = [];
|
|
9746
|
+
const fetchChildMessages = (sessionId) => {
|
|
9747
|
+
const cached = childMessages.get(sessionId);
|
|
9748
|
+
if (cached) return cached;
|
|
9749
|
+
const pending = (async () => {
|
|
9750
|
+
try {
|
|
9751
|
+
const messages2 = await this.getTelemetrySubagentSessionMessages(sessionId);
|
|
9752
|
+
if (messages2 === null) {
|
|
9753
|
+
this.log({
|
|
9754
|
+
level: "warn",
|
|
9755
|
+
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`,
|
|
9756
|
+
message_id: messageId
|
|
9757
|
+
});
|
|
9758
|
+
return null;
|
|
9759
|
+
}
|
|
9760
|
+
return messages2;
|
|
9761
|
+
} catch (err) {
|
|
9762
|
+
this.log({
|
|
9763
|
+
level: "warn",
|
|
9764
|
+
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)}`,
|
|
9765
|
+
message_id: messageId
|
|
9766
|
+
});
|
|
9767
|
+
return null;
|
|
9768
|
+
}
|
|
9769
|
+
})();
|
|
9770
|
+
childMessages.set(sessionId, pending);
|
|
9771
|
+
return pending;
|
|
9772
|
+
};
|
|
9773
|
+
const fetchChildWithoutBlocking = async (sessionId) => {
|
|
9774
|
+
const pending = fetchChildMessages(sessionId);
|
|
9775
|
+
let timer;
|
|
9776
|
+
const timeout = new Promise((resolve4) => {
|
|
9777
|
+
timer = setTimeout(() => {
|
|
9778
|
+
this.log({
|
|
9779
|
+
level: "warn",
|
|
9780
|
+
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`,
|
|
9781
|
+
message_id: messageId
|
|
9782
|
+
});
|
|
9783
|
+
resolve4(null);
|
|
9784
|
+
}, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
|
|
9785
|
+
});
|
|
9786
|
+
try {
|
|
9787
|
+
return await Promise.race([pending, timeout]);
|
|
9788
|
+
} finally {
|
|
9789
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
9790
|
+
}
|
|
9791
|
+
};
|
|
9792
|
+
while (work.length > 0) {
|
|
9793
|
+
const groups = /* @__PURE__ */ new Map();
|
|
9794
|
+
for (const item of work.splice(0)) {
|
|
9795
|
+
const group = groups.get(item.call.childSessionId) ?? [];
|
|
9796
|
+
group.push(item);
|
|
9797
|
+
groups.set(item.call.childSessionId, group);
|
|
9798
|
+
}
|
|
9799
|
+
const groupResults = await Promise.all(
|
|
9800
|
+
[...groups].map(async ([sessionId, items]) => ({
|
|
9801
|
+
sessionId,
|
|
9802
|
+
items,
|
|
9803
|
+
messages: sessionId === null ? [] : await fetchChildWithoutBlocking(sessionId)
|
|
9804
|
+
}))
|
|
9805
|
+
);
|
|
9806
|
+
for (const { sessionId, items, messages: child } of groupResults) {
|
|
9807
|
+
if (sessionId !== null && child === null) continue;
|
|
9808
|
+
const attribution = sessionId === null ? { invocations: [], unattributed: [] } : attributeTaskCallUsage(
|
|
9809
|
+
child,
|
|
9810
|
+
items.map(({ call }) => ({
|
|
9811
|
+
callID: call.callID,
|
|
9812
|
+
timeStart: call.timeStart,
|
|
9813
|
+
timeEnd: call.timeEnd
|
|
9814
|
+
}))
|
|
9815
|
+
);
|
|
9816
|
+
if (sessionId !== null && attribution.unattributed.length > 0) {
|
|
9817
|
+
this.log({
|
|
9818
|
+
level: "warn",
|
|
9819
|
+
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`,
|
|
9820
|
+
message_id: messageId
|
|
9821
|
+
});
|
|
9822
|
+
}
|
|
9823
|
+
const usageByCall = new Map(
|
|
9824
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.usage])
|
|
9825
|
+
);
|
|
9826
|
+
const messagesByCall = new Map(
|
|
9827
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.messages])
|
|
9828
|
+
);
|
|
9829
|
+
for (const { call, depth } of items) {
|
|
9830
|
+
const usage = usageByCall.get(call.callID) ?? null;
|
|
9831
|
+
payload.push({
|
|
9832
|
+
tool_call_id: call.callID,
|
|
9833
|
+
agent_name: call.subagentName,
|
|
9834
|
+
opencode_session_id: call.childSessionId,
|
|
9835
|
+
parent_opencode_session_id: call.parentSessionId,
|
|
9836
|
+
depth,
|
|
9837
|
+
status: call.status,
|
|
9838
|
+
started_at: call.timeStart === null ? null : new Date(call.timeStart).toISOString(),
|
|
9839
|
+
ended_at: call.timeEnd === null ? null : new Date(call.timeEnd).toISOString(),
|
|
9840
|
+
usage_provider_id: usage?.usage_provider_id ?? call.model?.providerID ?? null,
|
|
9841
|
+
usage_model_id: usage?.usage_model_id ?? call.model?.modelID ?? null,
|
|
9842
|
+
usage_tokens_input: usage?.usage_tokens_input ?? null,
|
|
9843
|
+
usage_tokens_output: usage?.usage_tokens_output ?? null,
|
|
9844
|
+
usage_tokens_reasoning: usage?.usage_tokens_reasoning ?? null,
|
|
9845
|
+
usage_tokens_cache_read: usage?.usage_tokens_cache_read ?? null,
|
|
9846
|
+
usage_tokens_cache_write: usage?.usage_tokens_cache_write ?? null,
|
|
9847
|
+
usage_cost_usd: usage?.usage_cost_usd ?? null
|
|
9848
|
+
});
|
|
9849
|
+
for (const assigned of messagesByCall.get(call.callID) ?? []) {
|
|
9850
|
+
const parentId = assigned.info?.parentID ?? assigned.parentID;
|
|
9851
|
+
if (!parentId) continue;
|
|
9852
|
+
for (const nested of collectTaskCalls([assigned], parentId)) {
|
|
9853
|
+
if (seenCallIds.has(nested.callID)) continue;
|
|
9854
|
+
seenCallIds.add(nested.callID);
|
|
9855
|
+
work.push({ call: nested, depth: depth + 1 });
|
|
9856
|
+
}
|
|
9857
|
+
}
|
|
9858
|
+
}
|
|
9859
|
+
}
|
|
9860
|
+
}
|
|
9861
|
+
return payload.length > 0 ? payload : void 0;
|
|
9862
|
+
}
|
|
8115
9863
|
/**
|
|
8116
9864
|
* OpenCode's synchronous default session title (e.g.
|
|
8117
9865
|
* `"New session - 1737800000000"`), assigned immediately when a session is
|
|
@@ -8150,21 +9898,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8150
9898
|
const cached = this.sessionTitles.get(sessionId);
|
|
8151
9899
|
if (cached != null) return cached;
|
|
8152
9900
|
try {
|
|
8153
|
-
|
|
8154
|
-
if (
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
return title;
|
|
8160
|
-
}
|
|
8161
|
-
return null;
|
|
9901
|
+
let title = "";
|
|
9902
|
+
if (this.isV2) {
|
|
9903
|
+
title = (await getV2Session(this.openCodeClient, sessionId)).title?.trim() ?? "";
|
|
9904
|
+
} else {
|
|
9905
|
+
const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
|
|
9906
|
+
title = typeof body?.title === "string" ? body.title.trim() : "";
|
|
8162
9907
|
}
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
9908
|
+
if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
|
|
9909
|
+
this.sessionTitles.set(sessionId, title);
|
|
9910
|
+
return title;
|
|
9911
|
+
}
|
|
9912
|
+
return null;
|
|
8168
9913
|
} catch (err) {
|
|
8169
9914
|
this.log({
|
|
8170
9915
|
level: "debug",
|
|
@@ -8262,7 +10007,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8262
10007
|
* `SessionStatus` only.
|
|
8263
10008
|
*/
|
|
8264
10009
|
async isAnyDescendantSessionAlive(rootSessionId) {
|
|
8265
|
-
const sessions = await listSessions(
|
|
10010
|
+
const sessions = await this.listSessions();
|
|
8266
10011
|
if (!sessions) {
|
|
8267
10012
|
this.log({
|
|
8268
10013
|
level: "warn",
|
|
@@ -8273,7 +10018,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8273
10018
|
for (const candidate of sessions) {
|
|
8274
10019
|
if (!candidate?.id || candidate.id === rootSessionId) continue;
|
|
8275
10020
|
if (!await this.sessionBelongsTo(candidate.id, rootSessionId)) continue;
|
|
8276
|
-
const childMsgs = await
|
|
10021
|
+
const childMsgs = await this.getSubagentSessionMessages(candidate.id);
|
|
8277
10022
|
if (isSessionActivelyGenerating(childMsgs)) {
|
|
8278
10023
|
return true;
|
|
8279
10024
|
}
|
|
@@ -8335,7 +10080,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8335
10080
|
* `isB2AbandonmentConfirmed`.
|
|
8336
10081
|
*/
|
|
8337
10082
|
async isAnyDescendantSessionOngoing(rootSessionId) {
|
|
8338
|
-
const sessions = await listSessions(
|
|
10083
|
+
const sessions = await this.listSessions();
|
|
8339
10084
|
if (!sessions) {
|
|
8340
10085
|
this.log({
|
|
8341
10086
|
level: "warn",
|
|
@@ -8352,7 +10097,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8352
10097
|
continue;
|
|
8353
10098
|
}
|
|
8354
10099
|
if (membership === false) continue;
|
|
8355
|
-
const ongoing = await isSessionOngoing(
|
|
10100
|
+
const ongoing = await this.isSessionOngoing(candidate.id);
|
|
8356
10101
|
if (ongoing === true) return true;
|
|
8357
10102
|
if (ongoing === null) indeterminate = true;
|
|
8358
10103
|
}
|
|
@@ -8595,7 +10340,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8595
10340
|
* watcher retries next tick within the
|
|
8596
10341
|
* deadline, Finding 4).
|
|
8597
10342
|
*/
|
|
8598
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
|
|
10343
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
|
|
8599
10344
|
const res = await this.fetchImpl(
|
|
8600
10345
|
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
8601
10346
|
{
|
|
@@ -8611,15 +10356,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8611
10356
|
opencode_session_id: sessionId,
|
|
8612
10357
|
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
8613
10358
|
...title ? { title } : {},
|
|
8614
|
-
...usage ? usage : {}
|
|
10359
|
+
...usage ? usage : {},
|
|
10360
|
+
...usageAgentName ? { usage_agent_name: usageAgentName } : {},
|
|
10361
|
+
...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
|
|
8615
10362
|
})
|
|
8616
10363
|
}
|
|
8617
10364
|
);
|
|
8618
10365
|
this.assertAuth(res, "marking message as done");
|
|
8619
|
-
if (res.ok)
|
|
10366
|
+
if (res.ok) {
|
|
10367
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
10368
|
+
return;
|
|
10369
|
+
}
|
|
8620
10370
|
if (isRetryableStatus(res.status)) {
|
|
8621
10371
|
throw new Error(`marking message as done: HTTP ${res.status}`);
|
|
8622
10372
|
}
|
|
10373
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8623
10374
|
throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
|
|
8624
10375
|
}
|
|
8625
10376
|
/**
|
|
@@ -8634,7 +10385,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8634
10385
|
* exists but is wedged, so the next attempt must get a fresh one
|
|
8635
10386
|
* instead of reusing it — see WI-1's server-side null-clearing PATCH).
|
|
8636
10387
|
*/
|
|
8637
|
-
async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
|
|
10388
|
+
async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
|
|
8638
10389
|
const body = { status: "failed" };
|
|
8639
10390
|
if (sessionId === null) {
|
|
8640
10391
|
body.opencode_session_id = null;
|
|
@@ -8643,23 +10394,33 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8643
10394
|
}
|
|
8644
10395
|
if (error2 !== void 0) body.error = error2;
|
|
8645
10396
|
if (usage) Object.assign(body, usage);
|
|
10397
|
+
if (usageAgentName) body.usage_agent_name = usageAgentName;
|
|
10398
|
+
if (subagentInvocations && subagentInvocations.length > 0) {
|
|
10399
|
+
body.subagent_invocations = subagentInvocations;
|
|
10400
|
+
}
|
|
8646
10401
|
if (failure) {
|
|
8647
10402
|
body.failure_kind = failure.kind;
|
|
8648
10403
|
body.failure_provider_id = failure.providerId;
|
|
8649
10404
|
body.failure_model_id = failure.modelId;
|
|
8650
10405
|
body.failure_reason = failure.reason;
|
|
8651
10406
|
}
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
10407
|
+
try {
|
|
10408
|
+
await this.callWithRetry(
|
|
10409
|
+
"marking message as failed",
|
|
10410
|
+
() => this.fetchImpl(
|
|
10411
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
10412
|
+
{
|
|
10413
|
+
method: "PATCH",
|
|
10414
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
10415
|
+
body: JSON.stringify(body)
|
|
10416
|
+
}
|
|
10417
|
+
)
|
|
10418
|
+
);
|
|
10419
|
+
} catch (err) {
|
|
10420
|
+
if (err instanceof ChannelTerminalError) this.clearSubagentInvocationCaches(messageId);
|
|
10421
|
+
throw err;
|
|
10422
|
+
}
|
|
10423
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8663
10424
|
}
|
|
8664
10425
|
/**
|
|
8665
10426
|
* Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
|
|
@@ -8676,7 +10437,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8676
10437
|
const classified = messageFailure(messages, userMessageId);
|
|
8677
10438
|
if (classified != null) return classified;
|
|
8678
10439
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
8679
|
-
const hasProvider = await hasAnyConfiguredProvider(
|
|
10440
|
+
const hasProvider = await this.hasAnyConfiguredProvider();
|
|
8680
10441
|
return applyZeroProviderFallback(
|
|
8681
10442
|
classified,
|
|
8682
10443
|
hasProvider,
|
|
@@ -8749,7 +10510,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8749
10510
|
const succeededProviders = /* @__PURE__ */ new Set();
|
|
8750
10511
|
for (const ref of refs) {
|
|
8751
10512
|
try {
|
|
8752
|
-
const childMessages = await
|
|
10513
|
+
const childMessages = await this.getSubagentSessionMessages(ref.sessionId);
|
|
8753
10514
|
if (childMessages === null) {
|
|
8754
10515
|
this.log({
|
|
8755
10516
|
level: "debug",
|
|
@@ -8942,6 +10703,13 @@ import chalk5 from "chalk";
|
|
|
8942
10703
|
import ora2 from "ora";
|
|
8943
10704
|
import { select as select2 } from "@inquirer/prompts";
|
|
8944
10705
|
var INTERACTIVE_START_TIMEOUT_MS = 3e4;
|
|
10706
|
+
function checkNonInteractivePortConflict(port, isPortInUseFn) {
|
|
10707
|
+
if (isPortInUseFn(port)) {
|
|
10708
|
+
throw new Error(
|
|
10709
|
+
`Port ${port} is already in use by a non-OpenCode process. Free it or pass --port.`
|
|
10710
|
+
);
|
|
10711
|
+
}
|
|
10712
|
+
}
|
|
8945
10713
|
async function ensureOpenCodeRunning(ctx) {
|
|
8946
10714
|
const healthCheck = await checkOpenCodeHealth(ctx.port);
|
|
8947
10715
|
if (healthCheck.healthy) {
|
|
@@ -8989,6 +10757,7 @@ async function ensureOpenCodeRunning(ctx) {
|
|
|
8989
10757
|
}
|
|
8990
10758
|
}
|
|
8991
10759
|
if (!ctx.interactive) {
|
|
10760
|
+
checkNonInteractivePortConflict(ctx.port, isPortInUse);
|
|
8992
10761
|
ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
|
|
8993
10762
|
const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
|
|
8994
10763
|
const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
|
|
@@ -9070,9 +10839,159 @@ Port ${port} is already in use.`));
|
|
|
9070
10839
|
return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
|
|
9071
10840
|
}
|
|
9072
10841
|
|
|
10842
|
+
// src/commands/ensure-opencode-v2.ts
|
|
10843
|
+
import chalk6 from "chalk";
|
|
10844
|
+
import ora3 from "ora";
|
|
10845
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
10846
|
+
async function probeOpenCode2WithoutPassword(port) {
|
|
10847
|
+
try {
|
|
10848
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
10849
|
+
signal: AbortSignal.timeout(2e3)
|
|
10850
|
+
});
|
|
10851
|
+
if (response.status === 401) {
|
|
10852
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
10853
|
+
}
|
|
10854
|
+
if (!response.ok) {
|
|
10855
|
+
return { healthy: false, error: `HTTP ${response.status}` };
|
|
10856
|
+
}
|
|
10857
|
+
return { healthy: true };
|
|
10858
|
+
} catch (error2) {
|
|
10859
|
+
return {
|
|
10860
|
+
healthy: false,
|
|
10861
|
+
error: error2 instanceof Error ? error2.message : "Unknown error"
|
|
10862
|
+
};
|
|
10863
|
+
}
|
|
10864
|
+
}
|
|
10865
|
+
function unknownPasswordError(port) {
|
|
10866
|
+
return new Error(
|
|
10867
|
+
`OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
|
|
10868
|
+
);
|
|
10869
|
+
}
|
|
10870
|
+
var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
|
|
10871
|
+
async function ensureOpenCode2Running(ctx) {
|
|
10872
|
+
const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
|
|
10873
|
+
if (initialHealth.authFailed) {
|
|
10874
|
+
throw unknownPasswordError(ctx.port);
|
|
10875
|
+
}
|
|
10876
|
+
if (initialHealth.healthy) {
|
|
10877
|
+
return {
|
|
10878
|
+
port: ctx.port,
|
|
10879
|
+
process: null,
|
|
10880
|
+
version: null,
|
|
10881
|
+
notReadyReason: null,
|
|
10882
|
+
password: null
|
|
10883
|
+
};
|
|
10884
|
+
}
|
|
10885
|
+
if (!isOpenCode2Installed()) {
|
|
10886
|
+
throw new Error(
|
|
10887
|
+
"OpenCode V2 (opencode2) is not installed. Install it with: npm install -g @opencode-ai/cli@beta"
|
|
10888
|
+
);
|
|
10889
|
+
}
|
|
10890
|
+
let port = ctx.port;
|
|
10891
|
+
if (!ctx.interactive) {
|
|
10892
|
+
checkNonInteractivePortConflict(port, isPortInUse);
|
|
10893
|
+
} else if (isPortInUse(port)) {
|
|
10894
|
+
console.log(chalk6.yellow(`
|
|
10895
|
+
Port ${port} is already in use.`));
|
|
10896
|
+
const alternativePort = findAvailablePort(port + 1);
|
|
10897
|
+
if (alternativePort) {
|
|
10898
|
+
const useAlternative = await select3({
|
|
10899
|
+
message: `Use port ${alternativePort} instead?`,
|
|
10900
|
+
choices: [
|
|
10901
|
+
{ name: `Yes, use port ${alternativePort}`, value: "yes" },
|
|
10902
|
+
{ name: "No, I will free the port manually", value: "no" }
|
|
10903
|
+
]
|
|
10904
|
+
});
|
|
10905
|
+
if (useAlternative === "yes") {
|
|
10906
|
+
port = alternativePort;
|
|
10907
|
+
} else {
|
|
10908
|
+
throw new Error(`Port ${ctx.port} is in use`);
|
|
10909
|
+
}
|
|
10910
|
+
}
|
|
10911
|
+
}
|
|
10912
|
+
if (!ctx.interactive) {
|
|
10913
|
+
ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
|
|
10914
|
+
const { child: proc, password } = await startOpenCode2(port, {
|
|
10915
|
+
inheritStdio: ctx.inheritStdio
|
|
10916
|
+
});
|
|
10917
|
+
const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
|
|
10918
|
+
if (!health.healthy) {
|
|
10919
|
+
return {
|
|
10920
|
+
port,
|
|
10921
|
+
process: proc,
|
|
10922
|
+
version: null,
|
|
10923
|
+
notReadyReason: `it did not become healthy within ${Math.round(ctx.startTimeoutMs / 1e3)}s`,
|
|
10924
|
+
password
|
|
10925
|
+
};
|
|
10926
|
+
}
|
|
10927
|
+
ctx.log(`OpenCode V2 started on port ${port}${health.version ? ` (v${health.version})` : ""}`);
|
|
10928
|
+
return {
|
|
10929
|
+
port,
|
|
10930
|
+
process: proc,
|
|
10931
|
+
version: health.version ?? null,
|
|
10932
|
+
notReadyReason: null,
|
|
10933
|
+
password
|
|
10934
|
+
};
|
|
10935
|
+
}
|
|
10936
|
+
const action = await select3({
|
|
10937
|
+
message: "OpenCode V2 is not running. What would you like to do?",
|
|
10938
|
+
choices: [
|
|
10939
|
+
{
|
|
10940
|
+
name: "Start OpenCode V2 for me",
|
|
10941
|
+
value: "start",
|
|
10942
|
+
description: `Run 'opencode2 serve --port ${port}'`
|
|
10943
|
+
},
|
|
10944
|
+
{
|
|
10945
|
+
name: "Show me the command",
|
|
10946
|
+
value: "manual",
|
|
10947
|
+
description: "Display the command to run manually"
|
|
10948
|
+
},
|
|
10949
|
+
{
|
|
10950
|
+
name: "Continue without OpenCode V2",
|
|
10951
|
+
value: "continue",
|
|
10952
|
+
description: "Requests will fail until OpenCode V2 starts"
|
|
10953
|
+
}
|
|
10954
|
+
]
|
|
10955
|
+
});
|
|
10956
|
+
if (action === "manual") {
|
|
10957
|
+
blank();
|
|
10958
|
+
console.log(chalk6.bold("Run this command in another terminal:"));
|
|
10959
|
+
blank();
|
|
10960
|
+
console.log(` ${chalk6.cyan(`opencode2 serve --port ${port}`)}`);
|
|
10961
|
+
blank();
|
|
10962
|
+
throw new Error("Please start OpenCode V2 manually");
|
|
10963
|
+
}
|
|
10964
|
+
if (action === "start") {
|
|
10965
|
+
const spinner = ora3("Starting OpenCode V2...").start();
|
|
10966
|
+
const { child: proc, password } = await startOpenCode2(port, {
|
|
10967
|
+
inheritStdio: ctx.inheritStdio
|
|
10968
|
+
});
|
|
10969
|
+
const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
|
|
10970
|
+
if (!health.healthy) {
|
|
10971
|
+
spinner.fail("Failed to start OpenCode V2");
|
|
10972
|
+
throw new Error("OpenCode V2 failed to start");
|
|
10973
|
+
}
|
|
10974
|
+
spinner.stop();
|
|
10975
|
+
return {
|
|
10976
|
+
port,
|
|
10977
|
+
process: proc,
|
|
10978
|
+
version: health.version ?? null,
|
|
10979
|
+
notReadyReason: null,
|
|
10980
|
+
password
|
|
10981
|
+
};
|
|
10982
|
+
}
|
|
10983
|
+
return {
|
|
10984
|
+
port,
|
|
10985
|
+
process: null,
|
|
10986
|
+
version: null,
|
|
10987
|
+
notReadyReason: "you chose to continue without OpenCode V2",
|
|
10988
|
+
password: null
|
|
10989
|
+
};
|
|
10990
|
+
}
|
|
10991
|
+
|
|
9073
10992
|
// src/lib/runner-credentials.ts
|
|
9074
|
-
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
9075
|
-
import { spawn as spawn5 } from "child_process";
|
|
10993
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
10994
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
9076
10995
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
9077
10996
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
9078
10997
|
var GITHUB_PROBE_TIMEOUT_MS = 1e4;
|
|
@@ -9344,11 +11263,11 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
9344
11263
|
}
|
|
9345
11264
|
|
|
9346
11265
|
// src/lib/opencode/config-overlay.ts
|
|
9347
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
9348
|
-
import { copyFileSync, existsSync as existsSync2, statSync as
|
|
9349
|
-
import { isAbsolute as isAbsolute2, join as
|
|
11266
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
11267
|
+
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
11268
|
+
import { isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "node:path";
|
|
9350
11269
|
function isFile(filePath) {
|
|
9351
|
-
return existsSync2(filePath) &&
|
|
11270
|
+
return existsSync2(filePath) && statSync6(filePath).isFile();
|
|
9352
11271
|
}
|
|
9353
11272
|
function applyRunnerOpenCodeConfig({
|
|
9354
11273
|
overlayPath,
|
|
@@ -9360,7 +11279,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9360
11279
|
return;
|
|
9361
11280
|
}
|
|
9362
11281
|
const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
|
|
9363
|
-
const target = isFile(
|
|
11282
|
+
const target = isFile(join9(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
9364
11283
|
if (!isFile(source)) {
|
|
9365
11284
|
log3(
|
|
9366
11285
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -9368,7 +11287,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
9368
11287
|
);
|
|
9369
11288
|
return;
|
|
9370
11289
|
}
|
|
9371
|
-
copyFileSync(source,
|
|
11290
|
+
copyFileSync(source, join9(cwd, target));
|
|
9372
11291
|
try {
|
|
9373
11292
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
9374
11293
|
stdio: "ignore"
|
|
@@ -9377,11 +11296,11 @@ function applyRunnerOpenCodeConfig({
|
|
|
9377
11296
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
9378
11297
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
9379
11298
|
}
|
|
9380
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
11299
|
+
log3(`Applied runner OpenCode config ${source} to ${join9(cwd, target)}`);
|
|
9381
11300
|
}
|
|
9382
11301
|
|
|
9383
11302
|
// src/lib/credential-sync.ts
|
|
9384
|
-
import { renameSync, writeFileSync as writeFileSync5 } from "fs";
|
|
11303
|
+
import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
9385
11304
|
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
9386
11305
|
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
9387
11306
|
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
@@ -9391,7 +11310,7 @@ var MAX_FLUSH_PASSES = 2;
|
|
|
9391
11310
|
function outcomesWith(outcome) {
|
|
9392
11311
|
return { claude: outcome, opencode: outcome };
|
|
9393
11312
|
}
|
|
9394
|
-
function
|
|
11313
|
+
function errorMessage2(error2) {
|
|
9395
11314
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
9396
11315
|
}
|
|
9397
11316
|
function waitForSettlement(promise, timeoutMs) {
|
|
@@ -9418,7 +11337,7 @@ function writeMarker(markerPath, outcomes, log3) {
|
|
|
9418
11337
|
writeFileSync5(temporaryPath, body, { mode: 384 });
|
|
9419
11338
|
renameSync(temporaryPath, markerPath);
|
|
9420
11339
|
} catch (error2) {
|
|
9421
|
-
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${
|
|
11340
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
|
|
9422
11341
|
}
|
|
9423
11342
|
}
|
|
9424
11343
|
function intervalSeconds(env, log3) {
|
|
@@ -9450,7 +11369,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
|
9450
11369
|
},
|
|
9451
11370
|
(error2) => {
|
|
9452
11371
|
failed = true;
|
|
9453
|
-
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${
|
|
11372
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
|
|
9454
11373
|
}
|
|
9455
11374
|
);
|
|
9456
11375
|
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
@@ -9510,7 +11429,7 @@ function createCredentialSync({
|
|
|
9510
11429
|
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
9511
11430
|
} catch (error2) {
|
|
9512
11431
|
outcomes[store] = "failed";
|
|
9513
|
-
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${
|
|
11432
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
|
|
9514
11433
|
}
|
|
9515
11434
|
}
|
|
9516
11435
|
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
@@ -9652,7 +11571,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9652
11571
|
if (trimmed === "") {
|
|
9653
11572
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
9654
11573
|
}
|
|
9655
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
11574
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join10(homeDir, trimmed.slice(2)) : trimmed;
|
|
9656
11575
|
if (!isAbsolute3(expanded)) {
|
|
9657
11576
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
9658
11577
|
}
|
|
@@ -9676,6 +11595,28 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
9676
11595
|
var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
|
|
9677
11596
|
var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
|
|
9678
11597
|
var OPENCODE_START_TIMEOUT_ENV = "EVIDENT_OPENCODE_START_TIMEOUT";
|
|
11598
|
+
var OPENCODE_VERSION_ENV = "EVIDENT_OPENCODE_VERSION";
|
|
11599
|
+
function resolveOpenCodeVersion(options, env = process.env) {
|
|
11600
|
+
let raw;
|
|
11601
|
+
let source;
|
|
11602
|
+
if (options.opencodeVersion !== void 0) {
|
|
11603
|
+
raw = options.opencodeVersion;
|
|
11604
|
+
source = "--opencode-version";
|
|
11605
|
+
} else if (env[OPENCODE_VERSION_ENV] !== void 0 && env[OPENCODE_VERSION_ENV] !== "") {
|
|
11606
|
+
raw = env[OPENCODE_VERSION_ENV];
|
|
11607
|
+
source = OPENCODE_VERSION_ENV;
|
|
11608
|
+
} else {
|
|
11609
|
+
return { version: "v1", warnings: [] };
|
|
11610
|
+
}
|
|
11611
|
+
const normalized = raw.trim().toLowerCase();
|
|
11612
|
+
if (normalized !== "v1" && normalized !== "v2") {
|
|
11613
|
+
return {
|
|
11614
|
+
version: "v1",
|
|
11615
|
+
warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or v2; using the default v1`]
|
|
11616
|
+
};
|
|
11617
|
+
}
|
|
11618
|
+
return { version: normalized, warnings: [] };
|
|
11619
|
+
}
|
|
9679
11620
|
function resolveOpenCodeStartTimeoutMs(options, env = process.env) {
|
|
9680
11621
|
const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1e3;
|
|
9681
11622
|
let raw;
|
|
@@ -9742,7 +11683,7 @@ function log2(state, message, level = "info") {
|
|
|
9742
11683
|
})
|
|
9743
11684
|
);
|
|
9744
11685
|
} else if (!state.interactive) {
|
|
9745
|
-
const prefix = level === "error" ?
|
|
11686
|
+
const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
|
|
9746
11687
|
console.log(`${prefix} ${message}`);
|
|
9747
11688
|
}
|
|
9748
11689
|
}
|
|
@@ -9772,7 +11713,7 @@ function logActivity(state, entry) {
|
|
|
9772
11713
|
}
|
|
9773
11714
|
function reportSessionDbRecovery(state) {
|
|
9774
11715
|
try {
|
|
9775
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
11716
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
|
|
9776
11717
|
for (const record of report.records) {
|
|
9777
11718
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
9778
11719
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -9803,18 +11744,18 @@ function reportSessionDbRecoveryRecord(state, record) {
|
|
|
9803
11744
|
function displayStatus(state) {
|
|
9804
11745
|
if (!state.interactive) return;
|
|
9805
11746
|
const attempt = state.connection?.reconnectAttempt ?? 0;
|
|
9806
|
-
const tunnel = state.connected ?
|
|
9807
|
-
const opencode = state.opencodeConnected ?
|
|
9808
|
-
const messages = state.messageCount > 0 ?
|
|
11747
|
+
const tunnel = state.connected ? chalk7.green("tunnel: connected") : attempt > 0 ? chalk7.yellow(`tunnel: reconnecting (#${attempt})`) : chalk7.yellow("tunnel: connecting");
|
|
11748
|
+
const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
|
|
11749
|
+
const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
|
|
9809
11750
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
9810
|
-
const detail = last ?
|
|
11751
|
+
const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
|
|
9811
11752
|
const agent = state.agentName ?? state.agentId;
|
|
9812
11753
|
console.log(
|
|
9813
|
-
`${
|
|
11754
|
+
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
9814
11755
|
);
|
|
9815
11756
|
}
|
|
9816
11757
|
async function promptForLogin(promptMessage, successMessage) {
|
|
9817
|
-
const action = await
|
|
11758
|
+
const action = await select4({
|
|
9818
11759
|
message: promptMessage,
|
|
9819
11760
|
choices: [
|
|
9820
11761
|
{
|
|
@@ -9830,7 +11771,7 @@ async function promptForLogin(promptMessage, successMessage) {
|
|
|
9830
11771
|
]
|
|
9831
11772
|
});
|
|
9832
11773
|
if (action === "exit") {
|
|
9833
|
-
console.log(
|
|
11774
|
+
console.log(chalk7.dim(`
|
|
9834
11775
|
You can log in later by running: ${getCliName()} login`));
|
|
9835
11776
|
process.exit(0);
|
|
9836
11777
|
}
|
|
@@ -9841,7 +11782,7 @@ You can log in later by running: ${getCliName()} login`));
|
|
|
9841
11782
|
process.exit(1);
|
|
9842
11783
|
}
|
|
9843
11784
|
blank();
|
|
9844
|
-
console.log(
|
|
11785
|
+
console.log(chalk7.green(successMessage));
|
|
9845
11786
|
blank();
|
|
9846
11787
|
return { token: credentials2.token, authType: "bearer", user: credentials2.user };
|
|
9847
11788
|
}
|
|
@@ -9854,12 +11795,12 @@ async function handleAuthError(state, error2) {
|
|
|
9854
11795
|
if (state.interactive) displayStatus(state);
|
|
9855
11796
|
if (!state.interactive) {
|
|
9856
11797
|
blank();
|
|
9857
|
-
console.log(
|
|
9858
|
-
console.log(
|
|
11798
|
+
console.log(chalk7.red("Authentication expired"));
|
|
11799
|
+
console.log(chalk7.dim("Your authentication token is no longer valid."));
|
|
9859
11800
|
blank();
|
|
9860
|
-
console.log(
|
|
9861
|
-
console.log(
|
|
9862
|
-
console.log(
|
|
11801
|
+
console.log(chalk7.dim("To fix this:"));
|
|
11802
|
+
console.log(chalk7.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
|
|
11803
|
+
console.log(chalk7.dim(" 2. Restart this command"));
|
|
9863
11804
|
blank();
|
|
9864
11805
|
await cleanup(state);
|
|
9865
11806
|
await shutdownTelemetry();
|
|
@@ -9867,7 +11808,7 @@ async function handleAuthError(state, error2) {
|
|
|
9867
11808
|
return { success: false };
|
|
9868
11809
|
}
|
|
9869
11810
|
blank();
|
|
9870
|
-
console.log(
|
|
11811
|
+
console.log(chalk7.yellow("Your authentication has expired."));
|
|
9871
11812
|
blank();
|
|
9872
11813
|
try {
|
|
9873
11814
|
const credentials2 = await promptForLogin(
|
|
@@ -9931,6 +11872,14 @@ async function driveChannels(state, driver) {
|
|
|
9931
11872
|
const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
|
|
9932
11873
|
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
9933
11874
|
if (opencodeAuthApplied) state.openaiUsageRearm?.();
|
|
11875
|
+
if (claudeCredentialApplied || opencodeAuthApplied) {
|
|
11876
|
+
void reloadProviderCache(state.port, state.opencodeClient ?? void 0).catch(
|
|
11877
|
+
(error2) => logActivity(state, {
|
|
11878
|
+
type: "error",
|
|
11879
|
+
error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
11880
|
+
})
|
|
11881
|
+
);
|
|
11882
|
+
}
|
|
9934
11883
|
if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
|
|
9935
11884
|
idlePolls = 0;
|
|
9936
11885
|
idleMs = 0;
|
|
@@ -9958,8 +11907,8 @@ async function driveChannels(state, driver) {
|
|
|
9958
11907
|
state.running = false;
|
|
9959
11908
|
break;
|
|
9960
11909
|
}
|
|
9961
|
-
const
|
|
9962
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
11910
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
|
|
11911
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage3}` });
|
|
9963
11912
|
if (state.interactive) displayStatus(state);
|
|
9964
11913
|
if (driver.hasInFlightWatchers()) {
|
|
9965
11914
|
consecutiveDrainFailures = 0;
|
|
@@ -9997,9 +11946,18 @@ async function driveChannels(state, driver) {
|
|
|
9997
11946
|
}
|
|
9998
11947
|
}
|
|
9999
11948
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
10000
|
-
var SESSION_DB_RECLAIM_MAX_PAGES =
|
|
11949
|
+
var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
|
|
11950
|
+
function shouldWarnForReclaimSkip(reason) {
|
|
11951
|
+
if (reason !== "sqlite-unavailable") return false;
|
|
11952
|
+
const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
|
|
11953
|
+
if (!version2) return false;
|
|
11954
|
+
const major = Number(version2[1]);
|
|
11955
|
+
const minor = Number(version2[2]);
|
|
11956
|
+
const patch = Number(version2[3]);
|
|
11957
|
+
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
11958
|
+
}
|
|
10001
11959
|
function sessionDbPath() {
|
|
10002
|
-
return
|
|
11960
|
+
return join10(homedir6(), ".local", "share", "opencode", "opencode.db");
|
|
10003
11961
|
}
|
|
10004
11962
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
10005
11963
|
const record = {
|
|
@@ -10040,7 +11998,7 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
|
|
|
10040
11998
|
async function runSweep(state, driver, config) {
|
|
10041
11999
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
10042
12000
|
try {
|
|
10043
|
-
const sessions = await listSessions(state.port);
|
|
12001
|
+
const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
|
|
10044
12002
|
if (sessions === null) {
|
|
10045
12003
|
logActivity(state, {
|
|
10046
12004
|
type: "info",
|
|
@@ -10070,7 +12028,7 @@ async function runSweep(state, driver, config) {
|
|
|
10070
12028
|
});
|
|
10071
12029
|
continue;
|
|
10072
12030
|
}
|
|
10073
|
-
if (await deleteSession(state.port, id)) deleted++;
|
|
12031
|
+
if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
|
|
10074
12032
|
else failed++;
|
|
10075
12033
|
}
|
|
10076
12034
|
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
@@ -10095,7 +12053,7 @@ async function runSweep(state, driver, config) {
|
|
|
10095
12053
|
} else {
|
|
10096
12054
|
logActivity(state, {
|
|
10097
12055
|
type: "info",
|
|
10098
|
-
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})`
|
|
12056
|
+
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : "")
|
|
10099
12057
|
});
|
|
10100
12058
|
}
|
|
10101
12059
|
} catch (error2) {
|
|
@@ -10118,13 +12076,20 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
10118
12076
|
for (const warning2 of config.warnings) {
|
|
10119
12077
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
10120
12078
|
}
|
|
10121
|
-
const dbBytes = statSessionDbBytes(
|
|
12079
|
+
const dbBytes = statSessionDbBytes(homedir6());
|
|
10122
12080
|
void (async () => {
|
|
10123
|
-
const
|
|
12081
|
+
const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
12082
|
+
if (reclaimAvailability !== null) {
|
|
12083
|
+
logActivity(state, {
|
|
12084
|
+
type: "info",
|
|
12085
|
+
level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
|
|
12086
|
+
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
|
|
12087
|
+
});
|
|
12088
|
+
}
|
|
10124
12089
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
10125
12090
|
dbBytes,
|
|
10126
12091
|
cleanupEnabled: config.enabled,
|
|
10127
|
-
reclaimSkipReason
|
|
12092
|
+
reclaimSkipReason: reclaimAvailability?.reason ?? null
|
|
10128
12093
|
});
|
|
10129
12094
|
if (sizeWarning !== null) {
|
|
10130
12095
|
logActivity(state, { type: "info", level: "warn", message: sizeWarning });
|
|
@@ -10291,14 +12256,11 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
10291
12256
|
report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
|
|
10292
12257
|
isLocalCredentialProblem,
|
|
10293
12258
|
forcedOnHint: "run `claude` to sign in",
|
|
10294
|
-
firstDelayMs:
|
|
10295
|
-
nextDelayMs:
|
|
10296
|
-
failureLogLevel:
|
|
12259
|
+
firstDelayMs: firstReportDelayMs,
|
|
12260
|
+
nextDelayMs: usageReportDelayMs,
|
|
12261
|
+
failureLogLevel: usageReportFailureLogLevel
|
|
10297
12262
|
});
|
|
10298
12263
|
}
|
|
10299
|
-
var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
|
|
10300
|
-
var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
10301
|
-
var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
10302
12264
|
function scheduleResourceUsageReporting(state, options) {
|
|
10303
12265
|
const { enabled, warnings } = resolveResourceUsageReportingEnabled(
|
|
10304
12266
|
options.resourceUsageReporting,
|
|
@@ -10319,7 +12281,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10319
12281
|
});
|
|
10320
12282
|
return;
|
|
10321
12283
|
}
|
|
10322
|
-
const { collect, stop } = createResourceUsageCollector(
|
|
12284
|
+
const { collect, stop } = createResourceUsageCollector(homedir6());
|
|
10323
12285
|
state.stopResourceUsageSampling = stop;
|
|
10324
12286
|
let consecutiveFailures = 0;
|
|
10325
12287
|
const tick = async () => {
|
|
@@ -10351,10 +12313,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10351
12313
|
consecutiveFailures++;
|
|
10352
12314
|
logActivity(state, {
|
|
10353
12315
|
type: "info",
|
|
10354
|
-
level:
|
|
10355
|
-
consecutiveFailures,
|
|
10356
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
10357
|
-
),
|
|
12316
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
10358
12317
|
message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
|
|
10359
12318
|
});
|
|
10360
12319
|
}
|
|
@@ -10363,20 +12322,11 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10363
12322
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
10364
12323
|
logActivity(state, {
|
|
10365
12324
|
type: "info",
|
|
10366
|
-
level:
|
|
10367
|
-
consecutiveFailures,
|
|
10368
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
10369
|
-
),
|
|
12325
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
10370
12326
|
message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
|
|
10371
12327
|
});
|
|
10372
12328
|
} finally {
|
|
10373
|
-
state.resourceUsageTimer = setTimeout(
|
|
10374
|
-
() => void tick(),
|
|
10375
|
-
jitteredDelayMs(
|
|
10376
|
-
RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
|
|
10377
|
-
RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
|
|
10378
|
-
)
|
|
10379
|
-
);
|
|
12329
|
+
state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
|
|
10380
12330
|
}
|
|
10381
12331
|
};
|
|
10382
12332
|
state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
|
|
@@ -10416,6 +12366,8 @@ async function cleanup(state, opts = {}) {
|
|
|
10416
12366
|
clearTimeout(timer);
|
|
10417
12367
|
}
|
|
10418
12368
|
state.sessionCleanupTimers = [];
|
|
12369
|
+
state.stopOpenCodeLogTail?.();
|
|
12370
|
+
state.stopOpenCodeLogTail = null;
|
|
10419
12371
|
if (state.claudeUsageTimer) {
|
|
10420
12372
|
clearTimeout(state.claudeUsageTimer);
|
|
10421
12373
|
state.claudeUsageTimer = null;
|
|
@@ -10554,7 +12506,7 @@ async function run(options) {
|
|
|
10554
12506
|
let fileSyncDirectories;
|
|
10555
12507
|
try {
|
|
10556
12508
|
logLevel = resolveLogLevel(options);
|
|
10557
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
12509
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
|
|
10558
12510
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
10559
12511
|
throw new Error(
|
|
10560
12512
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -10583,8 +12535,12 @@ async function run(options) {
|
|
|
10583
12535
|
connected: false,
|
|
10584
12536
|
opencodeConnected: false,
|
|
10585
12537
|
opencodeVersion: null,
|
|
12538
|
+
opencodeApiVersion: "v1",
|
|
12539
|
+
opencodePassword: null,
|
|
12540
|
+
opencodeClient: null,
|
|
10586
12541
|
sessionDbProvenanceAnomaly: false,
|
|
10587
12542
|
opencodeProcess: null,
|
|
12543
|
+
stopOpenCodeLogTail: null,
|
|
10588
12544
|
litestreamProcess: null,
|
|
10589
12545
|
connection: null,
|
|
10590
12546
|
channelDriver: null,
|
|
@@ -10652,15 +12608,15 @@ async function run(options) {
|
|
|
10652
12608
|
printError("Authentication required");
|
|
10653
12609
|
blank();
|
|
10654
12610
|
console.log(
|
|
10655
|
-
|
|
12611
|
+
chalk7.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
|
|
10656
12612
|
);
|
|
10657
|
-
console.log(
|
|
12613
|
+
console.log(chalk7.dim("Or run `evident login` for interactive authentication"));
|
|
10658
12614
|
blank();
|
|
10659
12615
|
process.exit(1);
|
|
10660
12616
|
return;
|
|
10661
12617
|
}
|
|
10662
12618
|
blank();
|
|
10663
|
-
console.log(
|
|
12619
|
+
console.log(chalk7.yellow("You are not logged in to Evident."));
|
|
10664
12620
|
blank();
|
|
10665
12621
|
credentials2 = await promptForLogin(
|
|
10666
12622
|
"Would you like to log in now?",
|
|
@@ -10710,7 +12666,7 @@ async function run(options) {
|
|
|
10710
12666
|
);
|
|
10711
12667
|
blank();
|
|
10712
12668
|
console.log(
|
|
10713
|
-
|
|
12669
|
+
chalk7.dim(
|
|
10714
12670
|
"Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
|
|
10715
12671
|
)
|
|
10716
12672
|
);
|
|
@@ -10733,15 +12689,15 @@ async function run(options) {
|
|
|
10733
12689
|
);
|
|
10734
12690
|
if (interactive && !state.json) {
|
|
10735
12691
|
blank();
|
|
10736
|
-
console.log(
|
|
10737
|
-
console.log(
|
|
12692
|
+
console.log(chalk7.bold("Evident Run"));
|
|
12693
|
+
console.log(chalk7.dim("-".repeat(40)));
|
|
10738
12694
|
}
|
|
10739
|
-
const spinner = interactive && !state.json ?
|
|
12695
|
+
const spinner = interactive && !state.json ? ora4("Validating runner...").start() : null;
|
|
10740
12696
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
10741
12697
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
10742
12698
|
spinner?.fail("Authentication failed");
|
|
10743
12699
|
blank();
|
|
10744
|
-
console.log(
|
|
12700
|
+
console.log(chalk7.yellow("Your authentication token is invalid or expired."));
|
|
10745
12701
|
blank();
|
|
10746
12702
|
credentials2 = await promptForLogin(
|
|
10747
12703
|
"Would you like to log in again?",
|
|
@@ -10789,6 +12745,13 @@ async function run(options) {
|
|
|
10789
12745
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
10790
12746
|
}
|
|
10791
12747
|
state.credentialSync?.arm();
|
|
12748
|
+
state.stopOpenCodeLogTail = tailOpenCodeLogFile(
|
|
12749
|
+
resolveOpenCodeLogPath(homedir6(), process.env),
|
|
12750
|
+
createOpenCodeActivityForwarder(() => ({
|
|
12751
|
+
agentId: state.agentId,
|
|
12752
|
+
authHeader: state.authHeader
|
|
12753
|
+
}))
|
|
12754
|
+
).stop;
|
|
10792
12755
|
let sessionDbVerifyFatal = false;
|
|
10793
12756
|
if (!options.restoreSessionDb) {
|
|
10794
12757
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -10838,14 +12801,28 @@ async function run(options) {
|
|
|
10838
12801
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
10839
12802
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
10840
12803
|
}
|
|
12804
|
+
const { version: opencodeVersion, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(
|
|
12805
|
+
options,
|
|
12806
|
+
process.env
|
|
12807
|
+
);
|
|
12808
|
+
for (const warning2 of opencodeVersionWarnings) {
|
|
12809
|
+
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
12810
|
+
}
|
|
10841
12811
|
const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
|
|
10842
12812
|
for (const warning2 of maxActiveSessionsWarnings) {
|
|
10843
12813
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
10844
12814
|
}
|
|
10845
12815
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
10846
|
-
const ocSpinner = interactive && !state.json ?
|
|
12816
|
+
const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
|
|
10847
12817
|
try {
|
|
10848
|
-
const oc = await
|
|
12818
|
+
const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
|
|
12819
|
+
port: state.port,
|
|
12820
|
+
interactive: state.interactive,
|
|
12821
|
+
agentId: state.agentId,
|
|
12822
|
+
log: (message) => log2(state, message),
|
|
12823
|
+
startTimeoutMs: opencodeStartTimeoutMs,
|
|
12824
|
+
inheritStdio: Boolean(options.opencodePidFile)
|
|
12825
|
+
}) : await ensureOpenCodeRunning({
|
|
10849
12826
|
port: state.port,
|
|
10850
12827
|
interactive: state.interactive,
|
|
10851
12828
|
agentId: state.agentId,
|
|
@@ -10856,6 +12833,19 @@ async function run(options) {
|
|
|
10856
12833
|
state.port = oc.port;
|
|
10857
12834
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
10858
12835
|
state.opencodeVersion = oc.version;
|
|
12836
|
+
state.opencodeApiVersion = opencodeVersion;
|
|
12837
|
+
let opencodePassword = null;
|
|
12838
|
+
if (opencodeVersion === "v2" && "password" in oc) {
|
|
12839
|
+
const value = oc.password;
|
|
12840
|
+
if (typeof value === "string" || value === null) opencodePassword = value;
|
|
12841
|
+
}
|
|
12842
|
+
state.opencodePassword = opencodePassword;
|
|
12843
|
+
const openCodeClient = createOpenCodeClient({
|
|
12844
|
+
port: state.port,
|
|
12845
|
+
version: state.opencodeApiVersion,
|
|
12846
|
+
password: state.opencodePassword
|
|
12847
|
+
});
|
|
12848
|
+
state.opencodeClient = openCodeClient;
|
|
10859
12849
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
10860
12850
|
try {
|
|
10861
12851
|
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
@@ -10872,7 +12862,7 @@ async function run(options) {
|
|
|
10872
12862
|
const provenance = checkSessionDbProvenance({
|
|
10873
12863
|
dbPath: sessionDbPath(),
|
|
10874
12864
|
currentVersion: state.opencodeVersion,
|
|
10875
|
-
homeDir:
|
|
12865
|
+
homeDir: homedir6(),
|
|
10876
12866
|
env: process.env
|
|
10877
12867
|
});
|
|
10878
12868
|
if (provenance.anomaly) {
|
|
@@ -10892,25 +12882,29 @@ async function run(options) {
|
|
|
10892
12882
|
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}).`;
|
|
10893
12883
|
logActivity(state, { type: "info", level: "warn", message });
|
|
10894
12884
|
} else {
|
|
10895
|
-
const versionWarning = buildOpenCodeVersionWarning(
|
|
12885
|
+
const versionWarning = buildOpenCodeVersionWarning(
|
|
12886
|
+
state.opencodeVersion,
|
|
12887
|
+
state.opencodeApiVersion
|
|
12888
|
+
);
|
|
10896
12889
|
if (versionWarning) {
|
|
10897
12890
|
log2(state, versionWarning, "warn");
|
|
10898
12891
|
if (state.interactive && !state.json) {
|
|
10899
12892
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
10900
12893
|
}
|
|
10901
12894
|
}
|
|
12895
|
+
await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
|
|
10902
12896
|
const noProviderWarning = buildNoProviderWarning(
|
|
10903
|
-
await hasAnyConfiguredProvider(state.port)
|
|
12897
|
+
await hasAnyConfiguredProvider(state.port, state.opencodeClient ?? void 0)
|
|
10904
12898
|
);
|
|
10905
12899
|
if (noProviderWarning) {
|
|
10906
12900
|
log2(state, noProviderWarning, "warn");
|
|
10907
12901
|
if (state.interactive && !state.json) {
|
|
10908
12902
|
logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
|
|
10909
12903
|
blank();
|
|
10910
|
-
console.log(
|
|
12904
|
+
console.log(chalk7.yellow("\u26A0 No OpenCode model provider is configured."));
|
|
10911
12905
|
console.log(
|
|
10912
|
-
|
|
10913
|
-
`Run ${
|
|
12906
|
+
chalk7.dim(
|
|
12907
|
+
`Run ${chalk7.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
|
|
10914
12908
|
)
|
|
10915
12909
|
);
|
|
10916
12910
|
blank();
|
|
@@ -11023,18 +13017,19 @@ async function run(options) {
|
|
|
11023
13017
|
});
|
|
11024
13018
|
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
11025
13019
|
}
|
|
11026
|
-
const tunnelSpinner = interactive && !state.json ?
|
|
13020
|
+
const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
|
|
11027
13021
|
const channelDriver = new ChannelDriver({
|
|
11028
13022
|
agentId: state.agentId,
|
|
11029
13023
|
port: state.port,
|
|
11030
13024
|
apiUrl: getApiUrlConfig(),
|
|
13025
|
+
openCodeClient: state.opencodeClient ?? void 0,
|
|
11031
13026
|
getAuthHeader: () => state.authHeader,
|
|
11032
13027
|
conversationFilter: state.conversationFilter,
|
|
11033
13028
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
11034
13029
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
11035
13030
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
11036
13031
|
fileSyncDirectories,
|
|
11037
|
-
homeDir:
|
|
13032
|
+
homeDir: homedir6(),
|
|
11038
13033
|
maxActiveSessions,
|
|
11039
13034
|
log: (entry) => (
|
|
11040
13035
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -11053,6 +13048,7 @@ async function run(options) {
|
|
|
11053
13048
|
agentId: state.agentId,
|
|
11054
13049
|
getAuthHeader: () => state.authHeader,
|
|
11055
13050
|
port: state.port,
|
|
13051
|
+
openCodePassword: state.opencodePassword,
|
|
11056
13052
|
isRunning: () => state.running,
|
|
11057
13053
|
events: {
|
|
11058
13054
|
onConnected: (agentId, isReconnect) => {
|
|
@@ -11077,7 +13073,11 @@ async function run(options) {
|
|
|
11077
13073
|
emitAgentConnected(state.agentId, {
|
|
11078
13074
|
port: state.port,
|
|
11079
13075
|
cli_version: getCliVersion(),
|
|
11080
|
-
opencode_version:
|
|
13076
|
+
opencode_version: reportedOpenCodeVersion({
|
|
13077
|
+
version: state.opencodeVersion,
|
|
13078
|
+
major: state.opencodeApiVersion,
|
|
13079
|
+
connected: state.opencodeConnected
|
|
13080
|
+
})
|
|
11081
13081
|
});
|
|
11082
13082
|
if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
|
|
11083
13083
|
if (state.interactive) displayStatus(state);
|
|
@@ -11193,7 +13193,7 @@ async function run(options) {
|
|
|
11193
13193
|
state.openaiUsageTimer = timer;
|
|
11194
13194
|
},
|
|
11195
13195
|
fetchUsage: async () => {
|
|
11196
|
-
const usage = await getOpenAiUsage(state.port);
|
|
13196
|
+
const usage = await getOpenAiUsage(state.port, state.opencodeClient ?? void 0);
|
|
11197
13197
|
if (usage.subscription === null) {
|
|
11198
13198
|
logActivity(state, {
|
|
11199
13199
|
type: "info",
|
|
@@ -11276,6 +13276,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
11276
13276
|
).option("-v, --verbose", "Alias for --log-level debug (ignored if --log-level is set)").option("-c, --conversation <id>", "Process only this specific conversation").option("--idle-timeout <seconds>", "Exit after N seconds idle").option(
|
|
11277
13277
|
"--opencode-start-timeout <seconds>",
|
|
11278
13278
|
"Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
|
|
13279
|
+
).option(
|
|
13280
|
+
"--opencode-version <v1|v2>",
|
|
13281
|
+
"Which OpenCode major version to launch: v1 or v2 (default: v1). Env: EVIDENT_OPENCODE_VERSION"
|
|
11279
13282
|
).option("--json", "Output in JSON format").option(
|
|
11280
13283
|
"--session-cleanup-max-age <duration>",
|
|
11281
13284
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -11344,6 +13347,7 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
11344
13347
|
// Raw string — validation/precedence is single-sourced in run.ts's
|
|
11345
13348
|
// resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).
|
|
11346
13349
|
opencodeStartTimeout: options.opencodeStartTimeout,
|
|
13350
|
+
opencodeVersion: options.opencodeVersion,
|
|
11347
13351
|
json: options.json,
|
|
11348
13352
|
// Raw strings — the resolver in run.ts single-sources parsing (M1).
|
|
11349
13353
|
sessionCleanupMaxAge: options.sessionCleanupMaxAge,
|