@evident-ai/cli 3.4.1-dev.444e897 → 3.4.1-dev.471ed2e
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/dist/index.js +764 -206
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -623,6 +623,19 @@ function isInteractive(jsonOutput) {
|
|
|
623
623
|
return true;
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
+
// src/lib/subscription-usage-report.ts
|
|
627
|
+
function toReportedSubscription(collected) {
|
|
628
|
+
if (!collected) return null;
|
|
629
|
+
if (collected.ownerEmail === null && collected.planType === null && collected.organizationName === null) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
owner_email: collected.ownerEmail,
|
|
634
|
+
plan_type: collected.planType,
|
|
635
|
+
organization_name: collected.organizationName
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
626
639
|
// src/commands/agent-lookup.ts
|
|
627
640
|
async function readErrorMessage(response) {
|
|
628
641
|
const text = await response.text().catch(() => "");
|
|
@@ -670,12 +683,15 @@ async function resolveAgentIdFromKey(authHeader) {
|
|
|
670
683
|
}
|
|
671
684
|
}
|
|
672
685
|
var BEST_EFFORT_NOTIFY_TIMEOUT_MS = 2e3;
|
|
673
|
-
async function
|
|
674
|
-
const apiUrl = getApiUrlConfig();
|
|
686
|
+
async function postBestEffort(path, authHeader, body) {
|
|
675
687
|
try {
|
|
676
|
-
const
|
|
688
|
+
const apiUrl = getApiUrlConfig();
|
|
689
|
+
const headers = { Authorization: authHeader };
|
|
690
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
691
|
+
const response = await fetch(`${apiUrl}${path}`, {
|
|
677
692
|
method: "POST",
|
|
678
|
-
headers
|
|
693
|
+
headers,
|
|
694
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
679
695
|
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
680
696
|
});
|
|
681
697
|
if (!response.ok) {
|
|
@@ -687,73 +703,32 @@ async function notifyAgentDisconnected(agentId, authHeader) {
|
|
|
687
703
|
}
|
|
688
704
|
return { ok: true };
|
|
689
705
|
} catch (error2) {
|
|
690
|
-
return { ok: false, error:
|
|
706
|
+
return { ok: false, error: describeTimeoutError(error2, BEST_EFFORT_NOTIFY_TIMEOUT_MS) };
|
|
691
707
|
}
|
|
692
708
|
}
|
|
693
|
-
function
|
|
709
|
+
function describeTimeoutError(error2, timeoutMs) {
|
|
694
710
|
const name = error2?.name;
|
|
695
711
|
if (name === "TimeoutError" || name === "AbortError") {
|
|
696
|
-
return `timed out after ${
|
|
712
|
+
return `timed out after ${timeoutMs}ms`;
|
|
697
713
|
}
|
|
698
714
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
699
715
|
}
|
|
716
|
+
async function notifyAgentDisconnected(agentId, authHeader) {
|
|
717
|
+
return postBestEffort(`/runners/${agentId}/disconnect`, authHeader);
|
|
718
|
+
}
|
|
700
719
|
async function reportMicrovmId(agentId, authHeader, microvmId) {
|
|
701
|
-
|
|
702
|
-
const apiUrl = getApiUrlConfig();
|
|
703
|
-
const response = await fetch(`${apiUrl}/runners/${agentId}/microvm`, {
|
|
704
|
-
method: "POST",
|
|
705
|
-
headers: { Authorization: authHeader, "Content-Type": "application/json" },
|
|
706
|
-
body: JSON.stringify({ microvm_id: microvmId }),
|
|
707
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
708
|
-
});
|
|
709
|
-
if (!response.ok) {
|
|
710
|
-
const serverMessage = await readErrorMessage(response);
|
|
711
|
-
return {
|
|
712
|
-
ok: false,
|
|
713
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
return { ok: true };
|
|
717
|
-
} catch (error2) {
|
|
718
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
719
|
-
}
|
|
720
|
+
return postBestEffort(`/runners/${agentId}/microvm`, authHeader, { microvm_id: microvmId });
|
|
720
721
|
}
|
|
721
722
|
function toReportedWindow(window) {
|
|
722
723
|
if (!window) return null;
|
|
723
724
|
return { utilization: window.utilization, resets_at: window.resetsAt };
|
|
724
725
|
}
|
|
725
|
-
function toReportedOwner(snapshot) {
|
|
726
|
-
if (!snapshot.owner) return null;
|
|
727
|
-
return {
|
|
728
|
-
email: snapshot.owner.email,
|
|
729
|
-
organization_name: snapshot.owner.organizationName,
|
|
730
|
-
rate_limit_tier: snapshot.owner.rateLimitTier
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
726
|
async function reportClaudeUsage(agentId, authHeader, snapshot) {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
body: JSON.stringify({
|
|
740
|
-
five_hour: toReportedWindow(snapshot.fiveHour),
|
|
741
|
-
seven_day: toReportedWindow(snapshot.sevenDay),
|
|
742
|
-
owner: toReportedOwner(snapshot)
|
|
743
|
-
}),
|
|
744
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
745
|
-
});
|
|
746
|
-
if (!response.ok) {
|
|
747
|
-
const serverMessage = await readErrorMessage(response);
|
|
748
|
-
return {
|
|
749
|
-
ok: false,
|
|
750
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
return { ok: true };
|
|
754
|
-
} catch (error2) {
|
|
755
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
756
|
-
}
|
|
727
|
+
return postBestEffort(`/runners/${agentId}/claude-usage`, authHeader, {
|
|
728
|
+
five_hour: toReportedWindow(snapshot.fiveHour),
|
|
729
|
+
seven_day: toReportedWindow(snapshot.sevenDay),
|
|
730
|
+
subscription: toReportedSubscription(snapshot.subscription)
|
|
731
|
+
});
|
|
757
732
|
}
|
|
758
733
|
function toReportedOpenAiWindow(window) {
|
|
759
734
|
if (!window) return null;
|
|
@@ -763,69 +738,26 @@ function toReportedOpenAiWindow(window) {
|
|
|
763
738
|
resets_at: window.resetsAt
|
|
764
739
|
};
|
|
765
740
|
}
|
|
766
|
-
function toReportedOpenAiSubscription(snapshot) {
|
|
767
|
-
if (!snapshot.subscription) return null;
|
|
768
|
-
return {
|
|
769
|
-
owner_email: snapshot.subscription.ownerEmail,
|
|
770
|
-
plan_type: snapshot.subscription.planType
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
741
|
async function reportOpenAiUsage(agentId, authHeader, snapshot) {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
782
|
-
has_credits: snapshot.hasCredits,
|
|
783
|
-
credits_unlimited: snapshot.creditsUnlimited,
|
|
784
|
-
subscription: toReportedOpenAiSubscription(snapshot)
|
|
785
|
-
}),
|
|
786
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
787
|
-
});
|
|
788
|
-
if (!response.ok) {
|
|
789
|
-
const serverMessage = await readErrorMessage(response);
|
|
790
|
-
return {
|
|
791
|
-
ok: false,
|
|
792
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
return { ok: true };
|
|
796
|
-
} catch (error2) {
|
|
797
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
798
|
-
}
|
|
742
|
+
return postBestEffort(`/runners/${agentId}/openai-usage`, authHeader, {
|
|
743
|
+
primary: toReportedOpenAiWindow(snapshot.primary),
|
|
744
|
+
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
745
|
+
has_credits: snapshot.hasCredits,
|
|
746
|
+
credits_unlimited: snapshot.creditsUnlimited,
|
|
747
|
+
subscription: toReportedSubscription(snapshot.subscription)
|
|
748
|
+
});
|
|
799
749
|
}
|
|
800
750
|
async function reportResourceUsage(agentId, authHeader, usage) {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
memory_available_bytes: usage.memoryAvailableBytes,
|
|
812
|
-
disk_total_bytes: usage.diskTotalBytes,
|
|
813
|
-
disk_free_bytes: usage.diskFreeBytes,
|
|
814
|
-
opencode_db_bytes: usage.opencodeDbBytes
|
|
815
|
-
}),
|
|
816
|
-
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
817
|
-
});
|
|
818
|
-
if (!response.ok) {
|
|
819
|
-
const serverMessage = await readErrorMessage(response);
|
|
820
|
-
return {
|
|
821
|
-
ok: false,
|
|
822
|
-
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
return { ok: true };
|
|
826
|
-
} catch (error2) {
|
|
827
|
-
return { ok: false, error: describeBestEffortError(error2) };
|
|
828
|
-
}
|
|
751
|
+
return postBestEffort(`/runners/${agentId}/resource-usage`, authHeader, {
|
|
752
|
+
cpu_percent: usage.cpuPercent,
|
|
753
|
+
cpu_peak_percent: usage.cpuPeakPercent,
|
|
754
|
+
cpu_count: usage.cpuCount,
|
|
755
|
+
memory_total_bytes: usage.memoryTotalBytes,
|
|
756
|
+
memory_available_bytes: usage.memoryAvailableBytes,
|
|
757
|
+
disk_total_bytes: usage.diskTotalBytes,
|
|
758
|
+
disk_free_bytes: usage.diskFreeBytes,
|
|
759
|
+
opencode_db_bytes: usage.opencodeDbBytes
|
|
760
|
+
});
|
|
829
761
|
}
|
|
830
762
|
async function getAgentInfo(agentId, authHeader) {
|
|
831
763
|
const apiUrl = getApiUrlConfig();
|
|
@@ -877,13 +809,6 @@ function authLabelFor(credentials2) {
|
|
|
877
809
|
}
|
|
878
810
|
return "user token";
|
|
879
811
|
}
|
|
880
|
-
function describeFetchError(error2) {
|
|
881
|
-
const name = error2?.name;
|
|
882
|
-
if (name === "TimeoutError" || name === "AbortError") {
|
|
883
|
-
return `timed out after ${STATUS_TIMEOUT_MS}ms waiting for a response`;
|
|
884
|
-
}
|
|
885
|
-
return error2 instanceof Error ? error2.message : String(error2);
|
|
886
|
-
}
|
|
887
812
|
async function checkStatus(jsonMode) {
|
|
888
813
|
const apiUrl = getApiUrlConfig();
|
|
889
814
|
const credentials2 = await getAuthCredentials();
|
|
@@ -911,7 +836,7 @@ async function checkStatus(jsonMode) {
|
|
|
911
836
|
endpoint: apiUrl,
|
|
912
837
|
authLabel: authLabelFor(credentials2),
|
|
913
838
|
reason: "unreachable",
|
|
914
|
-
error: `Could not reach ${apiUrl}: ${
|
|
839
|
+
error: `Could not reach ${apiUrl}: ${describeTimeoutError(error2, STATUS_TIMEOUT_MS)}. The credentials were NOT validated.`,
|
|
915
840
|
exitCode: 75
|
|
916
841
|
};
|
|
917
842
|
}
|
|
@@ -1097,7 +1022,7 @@ function ownerLookupFailure(error2) {
|
|
|
1097
1022
|
}
|
|
1098
1023
|
async function getClaudeUsageOwner(accessToken) {
|
|
1099
1024
|
if (cachedOwner?.accessToken === accessToken) {
|
|
1100
|
-
return {
|
|
1025
|
+
return { subscription: cachedOwner.owner, ownerLookupError: null };
|
|
1101
1026
|
}
|
|
1102
1027
|
try {
|
|
1103
1028
|
const response = await fetch(CLAUDE_PROFILE_URL, {
|
|
@@ -1109,27 +1034,27 @@ async function getClaudeUsageOwner(accessToken) {
|
|
|
1109
1034
|
signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
|
|
1110
1035
|
});
|
|
1111
1036
|
if (!response.ok) {
|
|
1112
|
-
return {
|
|
1037
|
+
return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
|
|
1113
1038
|
}
|
|
1114
1039
|
let body;
|
|
1115
1040
|
try {
|
|
1116
1041
|
body = await response.json();
|
|
1117
1042
|
} catch (error2) {
|
|
1118
|
-
return {
|
|
1043
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1119
1044
|
}
|
|
1120
1045
|
const profile = body;
|
|
1121
1046
|
if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
|
|
1122
|
-
return {
|
|
1047
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1123
1048
|
}
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1049
|
+
const subscription = {
|
|
1050
|
+
ownerEmail: profile.account.email,
|
|
1126
1051
|
organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
|
|
1127
|
-
|
|
1052
|
+
planType: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
|
|
1128
1053
|
};
|
|
1129
|
-
cachedOwner = { accessToken, owner };
|
|
1130
|
-
return {
|
|
1054
|
+
cachedOwner = { accessToken, owner: subscription };
|
|
1055
|
+
return { subscription, ownerLookupError: null };
|
|
1131
1056
|
} catch (error2) {
|
|
1132
|
-
return {
|
|
1057
|
+
return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
|
|
1133
1058
|
}
|
|
1134
1059
|
}
|
|
1135
1060
|
async function getClaudeUsage() {
|
|
@@ -1158,11 +1083,11 @@ async function getClaudeUsage() {
|
|
|
1158
1083
|
throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
|
|
1159
1084
|
}
|
|
1160
1085
|
const body = await res.json();
|
|
1161
|
-
const {
|
|
1086
|
+
const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
|
|
1162
1087
|
return {
|
|
1163
1088
|
fiveHour: toWindow(body.five_hour),
|
|
1164
1089
|
sevenDay: toWindow(body.seven_day),
|
|
1165
|
-
|
|
1090
|
+
subscription,
|
|
1166
1091
|
ownerLookupError
|
|
1167
1092
|
};
|
|
1168
1093
|
}
|
|
@@ -3263,21 +3188,74 @@ function collectSubagentSessions(messages, userMessageId) {
|
|
|
3263
3188
|
}
|
|
3264
3189
|
return refs;
|
|
3265
3190
|
}
|
|
3266
|
-
function
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3191
|
+
function finiteNumber(value) {
|
|
3192
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3193
|
+
}
|
|
3194
|
+
function taskCallModel(value) {
|
|
3195
|
+
if (!value || typeof value !== "object") return null;
|
|
3196
|
+
const model = value;
|
|
3197
|
+
const modelID = typeof model.modelID === "string" ? model.modelID : void 0;
|
|
3198
|
+
const providerID = typeof model.providerID === "string" ? model.providerID : void 0;
|
|
3199
|
+
return modelID || providerID ? { modelID, providerID } : null;
|
|
3200
|
+
}
|
|
3201
|
+
function collectTaskCalls(messages, userMessageId) {
|
|
3202
|
+
if (!messages || messages.length === 0) return [];
|
|
3203
|
+
const calls = [];
|
|
3204
|
+
for (const message of messages) {
|
|
3205
|
+
if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
|
|
3206
|
+
for (const part of message.parts ?? []) {
|
|
3207
|
+
if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
|
|
3208
|
+
continue;
|
|
3209
|
+
}
|
|
3210
|
+
const rawName = part.state.input?.subagent_type;
|
|
3211
|
+
const subagentName = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : rawName === void 0 ? "general" : "unknown";
|
|
3212
|
+
const metadata = part.state.metadata;
|
|
3213
|
+
calls.push({
|
|
3214
|
+
callID: part.callID,
|
|
3215
|
+
subagentName,
|
|
3216
|
+
childSessionId: typeof metadata?.sessionId === "string" ? metadata.sessionId : null,
|
|
3217
|
+
parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
|
|
3218
|
+
model: taskCallModel(metadata?.model),
|
|
3219
|
+
status: part.state.status ?? "unknown",
|
|
3220
|
+
timeStart: finiteNumber(part.state.time?.start),
|
|
3221
|
+
timeEnd: finiteNumber(part.state.time?.end)
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
return calls;
|
|
3226
|
+
}
|
|
3227
|
+
function attributeTaskCallUsage(messages, windows) {
|
|
3228
|
+
const eligibleWindows = windows.filter(
|
|
3229
|
+
(window) => window.timeStart !== null && Number.isFinite(window.timeStart)
|
|
3270
3230
|
);
|
|
3271
|
-
const
|
|
3272
|
-
const
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
const
|
|
3278
|
-
|
|
3231
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
3232
|
+
for (const window of eligibleWindows) assignments.set(window.callID, []);
|
|
3233
|
+
const unattributed = [];
|
|
3234
|
+
for (const message of messages ?? []) {
|
|
3235
|
+
if (roleOf(message) !== "assistant") continue;
|
|
3236
|
+
const created = finiteNumber(createdOf(message));
|
|
3237
|
+
const matching = created === null ? [] : eligibleWindows.filter(
|
|
3238
|
+
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
3239
|
+
);
|
|
3240
|
+
if (matching.length === 0) {
|
|
3241
|
+
unattributed.push(message);
|
|
3242
|
+
continue;
|
|
3243
|
+
}
|
|
3244
|
+
matching.sort((a, b) => a.timeStart - b.timeStart);
|
|
3245
|
+
assignments.get(matching[0].callID)?.push(message);
|
|
3279
3246
|
}
|
|
3280
|
-
|
|
3247
|
+
return {
|
|
3248
|
+
invocations: eligibleWindows.map((window) => {
|
|
3249
|
+
const assigned = assignments.get(window.callID) ?? [];
|
|
3250
|
+
return { callID: window.callID, messages: assigned, usage: sumAssistantUsage(assigned) };
|
|
3251
|
+
}),
|
|
3252
|
+
unattributed
|
|
3253
|
+
};
|
|
3254
|
+
}
|
|
3255
|
+
function sumAssistantUsage(messages) {
|
|
3256
|
+
if (!messages || messages.length === 0) return null;
|
|
3257
|
+
const nonErrored = messages.filter((message) => errorOf(message) == null);
|
|
3258
|
+
const selected = nonErrored.length > 0 ? nonErrored : messages;
|
|
3281
3259
|
let sawAnyUsage = false;
|
|
3282
3260
|
let inputSum = 0;
|
|
3283
3261
|
let outputSum = 0;
|
|
@@ -3288,7 +3266,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
3288
3266
|
let sawCost = false;
|
|
3289
3267
|
let modelId = null;
|
|
3290
3268
|
let providerId = null;
|
|
3291
|
-
for (const m of
|
|
3269
|
+
for (const m of selected) {
|
|
3292
3270
|
const info = m.info;
|
|
3293
3271
|
if (!info) continue;
|
|
3294
3272
|
const tokens = info.tokens;
|
|
@@ -3323,12 +3301,28 @@ function messageUsage(messages, userMessageId) {
|
|
|
3323
3301
|
usage_tokens_reasoning: reasoningSum,
|
|
3324
3302
|
usage_tokens_cache_read: cacheReadSum,
|
|
3325
3303
|
usage_tokens_cache_write: cacheWriteSum,
|
|
3326
|
-
// NULL means
|
|
3327
|
-
//
|
|
3328
|
-
// `sawCost` true with `costSum === 0`.
|
|
3304
|
+
// NULL means OpenCode never reported a cost; it is distinct from a genuine
|
|
3305
|
+
// zero-cost message, which sets `sawCost` with `costSum === 0`.
|
|
3329
3306
|
usage_cost_usd: sawCost ? costSum : null
|
|
3330
3307
|
};
|
|
3331
3308
|
}
|
|
3309
|
+
function messageUsage(messages, userMessageId) {
|
|
3310
|
+
if (!messages || messages.length === 0) return null;
|
|
3311
|
+
const byParentAll = messages.filter(
|
|
3312
|
+
(m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
|
|
3313
|
+
);
|
|
3314
|
+
const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
|
|
3315
|
+
const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
|
|
3316
|
+
let correlated;
|
|
3317
|
+
if (byParent.length > 0) {
|
|
3318
|
+
correlated = byParent;
|
|
3319
|
+
} else {
|
|
3320
|
+
const reply = findAssistantReplyAfter(messages, userMessageId);
|
|
3321
|
+
correlated = reply ? [reply] : [];
|
|
3322
|
+
}
|
|
3323
|
+
if (correlated.length === 0) return null;
|
|
3324
|
+
return sumAssistantUsage(correlated);
|
|
3325
|
+
}
|
|
3332
3326
|
function messageRunState(messages, userMessageId) {
|
|
3333
3327
|
if (!messages || messages.length === 0) return "unknown";
|
|
3334
3328
|
const hasUser = messages.some((m) => idOf(m) === userMessageId);
|
|
@@ -3458,6 +3452,28 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
|
3458
3452
|
(m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
3459
3453
|
);
|
|
3460
3454
|
}
|
|
3455
|
+
function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
|
|
3456
|
+
if (!messages || messages.length === 0) return false;
|
|
3457
|
+
const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
|
|
3458
|
+
if (userIndex === -1) return false;
|
|
3459
|
+
let hasLaterUser = false;
|
|
3460
|
+
let hasStartedLaterUser = false;
|
|
3461
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
3462
|
+
const message = messages[i];
|
|
3463
|
+
if (roleOf(message) !== "user") continue;
|
|
3464
|
+
hasLaterUser = true;
|
|
3465
|
+
const laterUserMessageId = idOf(message);
|
|
3466
|
+
if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
|
|
3467
|
+
return false;
|
|
3468
|
+
}
|
|
3469
|
+
if (messages.some(
|
|
3470
|
+
(candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
|
|
3471
|
+
)) {
|
|
3472
|
+
hasStartedLaterUser = true;
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
return hasLaterUser && hasStartedLaterUser;
|
|
3476
|
+
}
|
|
3461
3477
|
async function hasAnyConfiguredProvider(port) {
|
|
3462
3478
|
try {
|
|
3463
3479
|
const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
|
|
@@ -3489,6 +3505,76 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3489
3505
|
return null;
|
|
3490
3506
|
}
|
|
3491
3507
|
}
|
|
3508
|
+
function sessionErrorReason(error2) {
|
|
3509
|
+
const record = typeof error2 === "object" && error2 !== null ? error2 : null;
|
|
3510
|
+
const data = record?.data;
|
|
3511
|
+
const dataRecord = typeof data === "object" && data !== null ? data : null;
|
|
3512
|
+
const rawReason = typeof dataRecord?.message === "string" && dataRecord.message || typeof record?.message === "string" && record.message || typeof error2 === "string" && error2 || typeof record?.name === "string" && record.name || "OpenCode reported a session error with no details";
|
|
3513
|
+
const reason = rawReason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
3514
|
+
return reason || "OpenCode reported a session error with no details";
|
|
3515
|
+
}
|
|
3516
|
+
function parseSessionErrorFrame(data) {
|
|
3517
|
+
let parsed;
|
|
3518
|
+
try {
|
|
3519
|
+
parsed = JSON.parse(data);
|
|
3520
|
+
} catch (error2) {
|
|
3521
|
+
void error2;
|
|
3522
|
+
return null;
|
|
3523
|
+
}
|
|
3524
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
3525
|
+
const parsedRecord = parsed;
|
|
3526
|
+
const payload = parsedRecord.payload;
|
|
3527
|
+
const event = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : parsedRecord;
|
|
3528
|
+
if (event.type !== "session.error") return null;
|
|
3529
|
+
const properties = event.properties;
|
|
3530
|
+
if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
|
|
3531
|
+
return null;
|
|
3532
|
+
}
|
|
3533
|
+
const propertiesRecord = properties;
|
|
3534
|
+
const sessionId = propertiesRecord.sessionID;
|
|
3535
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
3536
|
+
return {
|
|
3537
|
+
sessionId,
|
|
3538
|
+
reason: sessionErrorReason(propertiesRecord.error)
|
|
3539
|
+
};
|
|
3540
|
+
}
|
|
3541
|
+
async function readSessionErrorStream(port, options) {
|
|
3542
|
+
let reader = null;
|
|
3543
|
+
try {
|
|
3544
|
+
const response = await fetch(`${opencodeBase(port)}/event`, {
|
|
3545
|
+
headers: { accept: "text/event-stream" },
|
|
3546
|
+
signal: options.signal
|
|
3547
|
+
});
|
|
3548
|
+
if (!response.ok || !response.body) {
|
|
3549
|
+
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
3550
|
+
}
|
|
3551
|
+
reader = response.body.getReader();
|
|
3552
|
+
const decoder = new TextDecoder();
|
|
3553
|
+
let buffer = "";
|
|
3554
|
+
const processLine = (line) => {
|
|
3555
|
+
const trimmed = line.trimEnd();
|
|
3556
|
+
if (!trimmed.startsWith("data:")) return;
|
|
3557
|
+
const event = parseSessionErrorFrame(trimmed.slice("data:".length).replace(/^ /, ""));
|
|
3558
|
+
if (event) options.onSessionError(event);
|
|
3559
|
+
};
|
|
3560
|
+
while (true) {
|
|
3561
|
+
const { done, value } = await reader.read();
|
|
3562
|
+
if (done) return { reason: "ended" };
|
|
3563
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3564
|
+
const lines = buffer.split("\n");
|
|
3565
|
+
buffer = lines.pop() ?? "";
|
|
3566
|
+
for (const line of lines) processLine(line);
|
|
3567
|
+
}
|
|
3568
|
+
} catch (err) {
|
|
3569
|
+
if (options.signal.aborted) return { reason: "aborted" };
|
|
3570
|
+
return {
|
|
3571
|
+
reason: "unavailable",
|
|
3572
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
3573
|
+
};
|
|
3574
|
+
} finally {
|
|
3575
|
+
if (reader) void reader.cancel().catch(() => void 0);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3492
3578
|
async function reloadProviderCache(port) {
|
|
3493
3579
|
try {
|
|
3494
3580
|
const res = await timedFetch(`${opencodeBase(port)}/config`, {
|
|
@@ -4384,7 +4470,7 @@ function parseChatGptIdentity(accessToken) {
|
|
|
4384
4470
|
const auth = payload["https://api.openai.com/auth"];
|
|
4385
4471
|
const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
|
|
4386
4472
|
const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
|
|
4387
|
-
return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
|
|
4473
|
+
return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
|
|
4388
4474
|
}
|
|
4389
4475
|
function toWindow2(headers, name) {
|
|
4390
4476
|
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
@@ -4566,13 +4652,6 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
|
|
|
4566
4652
|
envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
4567
4653
|
});
|
|
4568
4654
|
}
|
|
4569
|
-
function nextReportDelayMs(random = Math.random) {
|
|
4570
|
-
return usageReportDelayMs(random);
|
|
4571
|
-
}
|
|
4572
|
-
var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
|
|
4573
|
-
function claudeUsageFailureLogLevel(consecutiveFailures) {
|
|
4574
|
-
return usageReportFailureLogLevel(consecutiveFailures);
|
|
4575
|
-
}
|
|
4576
4655
|
|
|
4577
4656
|
// src/lib/openai-usage-reporting.ts
|
|
4578
4657
|
function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
@@ -5228,6 +5307,10 @@ var DEFAULT_RETRY_POLICY = {
|
|
|
5228
5307
|
baseDelayMs: 500,
|
|
5229
5308
|
maxDelayMs: 3e4
|
|
5230
5309
|
};
|
|
5310
|
+
var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
|
|
5311
|
+
var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
|
|
5312
|
+
var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
5313
|
+
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
5231
5314
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
5232
5315
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
5233
5316
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
@@ -5368,6 +5451,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5368
5451
|
* message; it is removed once its in-flight set empties.
|
|
5369
5452
|
*/
|
|
5370
5453
|
watchers = /* @__PURE__ */ new Map();
|
|
5454
|
+
sessionErrorStream = null;
|
|
5455
|
+
/**
|
|
5456
|
+
* Session-error failures currently being reported; entries are empty at rest
|
|
5457
|
+
* because each handoff deletes its id in `finally`.
|
|
5458
|
+
*/
|
|
5459
|
+
sessionErrorHandled = /* @__PURE__ */ new Set();
|
|
5460
|
+
/**
|
|
5461
|
+
* Session errors that arrived before their dispatch was registered. Bounded FIFO
|
|
5462
|
+
* with a short TTL so an unmatched session cannot retain an event indefinitely.
|
|
5463
|
+
*/
|
|
5464
|
+
bufferedSessionErrors = /* @__PURE__ */ new Map();
|
|
5371
5465
|
/**
|
|
5372
5466
|
* AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
|
|
5373
5467
|
* dispatched and are still in-flight. A message in this set is never
|
|
@@ -5551,6 +5645,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5551
5645
|
* no watcher) can resolve the title.
|
|
5552
5646
|
*/
|
|
5553
5647
|
sessionTitles = /* @__PURE__ */ new Map();
|
|
5648
|
+
/** One best-effort terminal subagent collection per Evident message id. */
|
|
5649
|
+
subagentInvocationCollections = /* @__PURE__ */ new Map();
|
|
5650
|
+
/**
|
|
5651
|
+
* Early snapshots are only liveness hints; they must not become the terminal
|
|
5652
|
+
* collection when the task parts or child transcript have advanced.
|
|
5653
|
+
*/
|
|
5654
|
+
subagentInvocationPrefetches = /* @__PURE__ */ new Map();
|
|
5554
5655
|
/** Serialises drains so a reconnect during a drain doesn't double-process. */
|
|
5555
5656
|
draining = false;
|
|
5556
5657
|
/**
|
|
@@ -5765,6 +5866,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5765
5866
|
}
|
|
5766
5867
|
return ids;
|
|
5767
5868
|
}
|
|
5869
|
+
/**
|
|
5870
|
+
* OpenCode user-message ids tracked for other Evident messages in a session.
|
|
5871
|
+
* Excluding this message makes an unattributed later row fail safe; a missing
|
|
5872
|
+
* watcher yields no attributions, per `hasLaterSiblingTurnStarted`'s docblock.
|
|
5873
|
+
*/
|
|
5874
|
+
siblingOpencodeMessageIds(watcher, ownEvidentMessageId) {
|
|
5875
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5876
|
+
if (!watcher) return ids;
|
|
5877
|
+
for (const inFlight of watcher.inFlight.values()) {
|
|
5878
|
+
if (inFlight.evidentMessageId !== ownEvidentMessageId) {
|
|
5879
|
+
ids.add(inFlight.opencodeMessageId);
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
return ids;
|
|
5883
|
+
}
|
|
5768
5884
|
/**
|
|
5769
5885
|
* File-pull work, for `run.ts`'s idle accounting (#559).
|
|
5770
5886
|
*
|
|
@@ -5831,6 +5947,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5831
5947
|
*/
|
|
5832
5948
|
stop() {
|
|
5833
5949
|
this.stopped = true;
|
|
5950
|
+
this.sessionErrorStream?.abort.abort();
|
|
5951
|
+
this.sessionErrorStream = null;
|
|
5834
5952
|
}
|
|
5835
5953
|
/**
|
|
5836
5954
|
* The server clears this request when a new MicroVM identity is recorded, so a
|
|
@@ -5905,6 +6023,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5905
6023
|
*/
|
|
5906
6024
|
async processConversation(conv) {
|
|
5907
6025
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
6026
|
+
this.ensureSessionErrorStream();
|
|
5908
6027
|
const messages = await this.getPendingMessages(conv.id);
|
|
5909
6028
|
let dispatched = 0;
|
|
5910
6029
|
let skippedAlreadyDispatched = 0;
|
|
@@ -6251,6 +6370,23 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6251
6370
|
if (state === "running" || state === "queued") {
|
|
6252
6371
|
const ongoing = await isSessionOngoing(this.port, sessionId);
|
|
6253
6372
|
if (ongoing === true) {
|
|
6373
|
+
if (state === "queued") {
|
|
6374
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(
|
|
6375
|
+
this.watchers.get(sessionId),
|
|
6376
|
+
message.id
|
|
6377
|
+
);
|
|
6378
|
+
if (hasLaterSiblingTurnStarted(messages, ocId ?? "", siblingOcIds)) {
|
|
6379
|
+
this.log({
|
|
6380
|
+
level: "warn",
|
|
6381
|
+
message: `Re-drive: OpenCode already served a later, different Evident message's turn in session ${sessionId.slice(0, 8)} while message ${message.id.slice(0, 8)} produced no reply \u2014 re-dispatching instead of reattaching to someone else's turn`,
|
|
6382
|
+
conversation_id: conv.id,
|
|
6383
|
+
message_id: message.id
|
|
6384
|
+
});
|
|
6385
|
+
this.clearRedriveUnresolved(message.id);
|
|
6386
|
+
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
6387
|
+
return "dispatch";
|
|
6388
|
+
}
|
|
6389
|
+
}
|
|
6254
6390
|
return this.reattachRedrive(conv, sessionId, message, ocId);
|
|
6255
6391
|
}
|
|
6256
6392
|
if (ongoing === false) {
|
|
@@ -6340,16 +6476,37 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6340
6476
|
if (state === "done") {
|
|
6341
6477
|
const title = await this.resolveSessionTitle(sessionId, conv.id);
|
|
6342
6478
|
const usage = messageUsage(messages, ocId ?? "");
|
|
6479
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
6480
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
6481
|
+
messages,
|
|
6482
|
+
ocId ?? "",
|
|
6483
|
+
message.id
|
|
6484
|
+
);
|
|
6343
6485
|
this.log({
|
|
6344
6486
|
level: "info",
|
|
6345
6487
|
message: `Re-drive: message ${message.id.slice(0, 8)} completed while its row was wrongly reclaimed to pending \u2014 marking done instead of re-dispatching`,
|
|
6346
6488
|
conversation_id: conv.id,
|
|
6347
6489
|
message_id: message.id
|
|
6348
6490
|
});
|
|
6349
|
-
await this.markDone(
|
|
6491
|
+
await this.markDone(
|
|
6492
|
+
conv.id,
|
|
6493
|
+
message.id,
|
|
6494
|
+
sessionId,
|
|
6495
|
+
ocId,
|
|
6496
|
+
title,
|
|
6497
|
+
usage,
|
|
6498
|
+
usageAgentName,
|
|
6499
|
+
subagentInvocations
|
|
6500
|
+
);
|
|
6350
6501
|
} else {
|
|
6351
6502
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
6352
6503
|
const usage = messageUsage(messages, ocId ?? "");
|
|
6504
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
6505
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
6506
|
+
messages,
|
|
6507
|
+
ocId ?? "",
|
|
6508
|
+
message.id
|
|
6509
|
+
);
|
|
6353
6510
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
6354
6511
|
this.log({
|
|
6355
6512
|
level: "error",
|
|
@@ -6357,7 +6514,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6357
6514
|
conversation_id: conv.id,
|
|
6358
6515
|
message_id: message.id
|
|
6359
6516
|
});
|
|
6360
|
-
await this.markFailed(
|
|
6517
|
+
await this.markFailed(
|
|
6518
|
+
conv.id,
|
|
6519
|
+
message.id,
|
|
6520
|
+
sessionId,
|
|
6521
|
+
error2,
|
|
6522
|
+
usage,
|
|
6523
|
+
failure,
|
|
6524
|
+
usageAgentName,
|
|
6525
|
+
subagentInvocations
|
|
6526
|
+
);
|
|
6361
6527
|
}
|
|
6362
6528
|
if (ocId !== null) {
|
|
6363
6529
|
await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
|
|
@@ -6901,6 +7067,24 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6901
7067
|
ambiguousPinnedSinceMs: 0,
|
|
6902
7068
|
ambiguousResolved: false
|
|
6903
7069
|
});
|
|
7070
|
+
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
7071
|
+
if (!buffered) return;
|
|
7072
|
+
this.bufferedSessionErrors.delete(sessionId);
|
|
7073
|
+
if (this.now() - buffered.receivedAt < SESSION_ERROR_BUFFER_TTL_MS) {
|
|
7074
|
+
this.handleSessionError(buffered.event);
|
|
7075
|
+
}
|
|
7076
|
+
}
|
|
7077
|
+
bufferSessionError(event) {
|
|
7078
|
+
this.bufferedSessionErrors.delete(event.sessionId);
|
|
7079
|
+
this.bufferedSessionErrors.set(event.sessionId, {
|
|
7080
|
+
event,
|
|
7081
|
+
receivedAt: this.now()
|
|
7082
|
+
});
|
|
7083
|
+
while (this.bufferedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
|
|
7084
|
+
const oldest = this.bufferedSessionErrors.keys().next().value;
|
|
7085
|
+
if (typeof oldest !== "string") break;
|
|
7086
|
+
this.bufferedSessionErrors.delete(oldest);
|
|
7087
|
+
}
|
|
6904
7088
|
}
|
|
6905
7089
|
/**
|
|
6906
7090
|
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
@@ -7105,6 +7289,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7105
7289
|
ensureWatcherRunning(sessionId) {
|
|
7106
7290
|
const watcher = this.watchers.get(sessionId);
|
|
7107
7291
|
if (!watcher) return;
|
|
7292
|
+
this.ensureSessionErrorStream();
|
|
7108
7293
|
if (watcher.loop) return;
|
|
7109
7294
|
if (watcher.inFlight.size === 0) {
|
|
7110
7295
|
this.watchers.delete(sessionId);
|
|
@@ -7120,6 +7305,154 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7120
7305
|
});
|
|
7121
7306
|
watcher.loop = loop;
|
|
7122
7307
|
}
|
|
7308
|
+
ensureSessionErrorStream() {
|
|
7309
|
+
if (this.sessionErrorStream || this.stopped) return;
|
|
7310
|
+
const abort = new AbortController();
|
|
7311
|
+
const loop = this.runSessionErrorStream(abort.signal);
|
|
7312
|
+
this.sessionErrorStream = { abort, loop };
|
|
7313
|
+
}
|
|
7314
|
+
async runSessionErrorStream(signal) {
|
|
7315
|
+
let attempt = 0;
|
|
7316
|
+
let warned = false;
|
|
7317
|
+
while (!this.stopped && !signal.aborted) {
|
|
7318
|
+
const openedAt = this.now();
|
|
7319
|
+
try {
|
|
7320
|
+
const outcome = await readSessionErrorStream(this.port, {
|
|
7321
|
+
signal,
|
|
7322
|
+
onSessionError: (event) => this.handleSessionError(event)
|
|
7323
|
+
});
|
|
7324
|
+
if (outcome.reason === "aborted" || signal.aborted) return;
|
|
7325
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
7326
|
+
if (outcome.reason === "unavailable" || outcome.reason === "ended") {
|
|
7327
|
+
if (!healthy) {
|
|
7328
|
+
const detail = outcome.reason === "unavailable" ? outcome.detail : "stream ended";
|
|
7329
|
+
this.log({
|
|
7330
|
+
level: warned ? "debug" : "warn",
|
|
7331
|
+
message: `OpenCode session-error stream ${warned ? "still unavailable" : "unavailable"} (${detail}); transcript polling remains the evidence path`
|
|
7332
|
+
});
|
|
7333
|
+
warned = true;
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
if (healthy) {
|
|
7337
|
+
if (warned) {
|
|
7338
|
+
this.log({
|
|
7339
|
+
level: "info",
|
|
7340
|
+
message: "OpenCode session-error stream reconnected; transcript polling remains the evidence path"
|
|
7341
|
+
});
|
|
7342
|
+
warned = false;
|
|
7343
|
+
}
|
|
7344
|
+
attempt = 0;
|
|
7345
|
+
} else {
|
|
7346
|
+
attempt += 1;
|
|
7347
|
+
}
|
|
7348
|
+
if (this.stopped || signal.aborted) return;
|
|
7349
|
+
await this.sleep(backoffDelay(healthy ? 0 : attempt - 1, this.retry));
|
|
7350
|
+
} catch (err) {
|
|
7351
|
+
if (this.stopped || signal.aborted) return;
|
|
7352
|
+
this.log({
|
|
7353
|
+
level: "error",
|
|
7354
|
+
message: `OpenCode session-error stream failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
|
|
7355
|
+
});
|
|
7356
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
7357
|
+
const delayAttempt = healthy ? 0 : attempt;
|
|
7358
|
+
attempt = healthy ? 0 : attempt + 1;
|
|
7359
|
+
try {
|
|
7360
|
+
await this.sleep(backoffDelay(delayAttempt, this.retry));
|
|
7361
|
+
} catch (sleepErr) {
|
|
7362
|
+
this.log({
|
|
7363
|
+
level: "error",
|
|
7364
|
+
message: `OpenCode session-error stream backoff failed unexpectedly: ${sleepErr instanceof Error ? sleepErr.message : String(sleepErr)}`
|
|
7365
|
+
});
|
|
7366
|
+
}
|
|
7367
|
+
}
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
7370
|
+
handleSessionError(event) {
|
|
7371
|
+
try {
|
|
7372
|
+
const watcher = this.watchers.get(event.sessionId);
|
|
7373
|
+
if (!watcher) {
|
|
7374
|
+
this.bufferSessionError(event);
|
|
7375
|
+
this.log({
|
|
7376
|
+
level: "debug",
|
|
7377
|
+
message: `Ignoring session error for unknown session ${event.sessionId.slice(0, 8)}`
|
|
7378
|
+
});
|
|
7379
|
+
return;
|
|
7380
|
+
}
|
|
7381
|
+
if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
|
|
7382
|
+
this.log({
|
|
7383
|
+
level: "debug",
|
|
7384
|
+
message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
|
|
7385
|
+
conversation_id: watcher.conv.id
|
|
7386
|
+
});
|
|
7387
|
+
return;
|
|
7388
|
+
}
|
|
7389
|
+
const inFlight = [...watcher.inFlight.values()].filter((message) => !message.started && !message.done).sort((a, b) => a.dispatchedAt - b.dispatchedAt)[0];
|
|
7390
|
+
if (!inFlight) {
|
|
7391
|
+
this.bufferSessionError(event);
|
|
7392
|
+
this.log({
|
|
7393
|
+
level: "debug",
|
|
7394
|
+
message: `No queued in-flight turn to correlate with session error in ${event.sessionId.slice(0, 8)}`,
|
|
7395
|
+
conversation_id: watcher.conv.id
|
|
7396
|
+
});
|
|
7397
|
+
return;
|
|
7398
|
+
}
|
|
7399
|
+
if (this.sessionErrorHandled.has(inFlight.evidentMessageId)) return;
|
|
7400
|
+
this.sessionErrorHandled.add(inFlight.evidentMessageId);
|
|
7401
|
+
void this.failFromSessionError(watcher, event, inFlight);
|
|
7402
|
+
} catch (err) {
|
|
7403
|
+
this.log({
|
|
7404
|
+
level: "error",
|
|
7405
|
+
message: `Failed to handle OpenCode session error for ${event.sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`
|
|
7406
|
+
});
|
|
7407
|
+
}
|
|
7408
|
+
}
|
|
7409
|
+
async failFromSessionError(watcher, event, inFlight) {
|
|
7410
|
+
try {
|
|
7411
|
+
const messages = await getSessionMessages(this.port, event.sessionId);
|
|
7412
|
+
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7413
|
+
if (state !== "queued") {
|
|
7414
|
+
this.log({
|
|
7415
|
+
level: "debug",
|
|
7416
|
+
message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
|
|
7417
|
+
conversation_id: watcher.conv.id,
|
|
7418
|
+
message_id: inFlight.evidentMessageId
|
|
7419
|
+
});
|
|
7420
|
+
return;
|
|
7421
|
+
}
|
|
7422
|
+
this.log({
|
|
7423
|
+
level: "error",
|
|
7424
|
+
message: `OpenCode could not run message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)}: ${event.reason}`,
|
|
7425
|
+
conversation_id: watcher.conv.id,
|
|
7426
|
+
message_id: inFlight.evidentMessageId
|
|
7427
|
+
});
|
|
7428
|
+
await this.markFailed(
|
|
7429
|
+
watcher.conv.id,
|
|
7430
|
+
inFlight.evidentMessageId,
|
|
7431
|
+
event.sessionId,
|
|
7432
|
+
`OpenCode could not run this turn: ${event.reason}`
|
|
7433
|
+
);
|
|
7434
|
+
inFlight.done = true;
|
|
7435
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7436
|
+
} catch (err) {
|
|
7437
|
+
if (err instanceof ChannelAuthError) {
|
|
7438
|
+
this.log({
|
|
7439
|
+
level: "warn",
|
|
7440
|
+
message: `OpenCode session error could not mark message ${inFlight.evidentMessageId.slice(0, 8)} failed because authentication failed: ${err.message}; leaving it to transcript polling / the existing give-up path`,
|
|
7441
|
+
conversation_id: watcher.conv.id,
|
|
7442
|
+
message_id: inFlight.evidentMessageId
|
|
7443
|
+
});
|
|
7444
|
+
} else {
|
|
7445
|
+
this.log({
|
|
7446
|
+
level: "warn",
|
|
7447
|
+
message: `OpenCode session error could not mark message ${inFlight.evidentMessageId.slice(0, 8)} failed: ${err instanceof Error ? err.message : String(err)}; leaving it to transcript polling / the existing give-up path`,
|
|
7448
|
+
conversation_id: watcher.conv.id,
|
|
7449
|
+
message_id: inFlight.evidentMessageId
|
|
7450
|
+
});
|
|
7451
|
+
}
|
|
7452
|
+
} finally {
|
|
7453
|
+
this.sessionErrorHandled.delete(inFlight.evidentMessageId);
|
|
7454
|
+
}
|
|
7455
|
+
}
|
|
7123
7456
|
/**
|
|
7124
7457
|
* The per-session polling loop (WI-3). Once per tick it:
|
|
7125
7458
|
* 1. polls `GET /session/:id/message` once and, per in-flight message,
|
|
@@ -7232,6 +7565,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7232
7565
|
const conv = watcher.conv;
|
|
7233
7566
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7234
7567
|
const id = inFlight.evidentMessageId;
|
|
7568
|
+
if (messages && collectTaskCalls(messages, inFlight.opencodeMessageId).length > 0 && !this.subagentInvocationPrefetches.has(id)) {
|
|
7569
|
+
void this.resolveSubagentInvocations(
|
|
7570
|
+
messages,
|
|
7571
|
+
inFlight.opencodeMessageId,
|
|
7572
|
+
id,
|
|
7573
|
+
"prefetch"
|
|
7574
|
+
).catch((err) => {
|
|
7575
|
+
this.log({
|
|
7576
|
+
level: "warn",
|
|
7577
|
+
message: `Best-effort subagent usage prefetch failed for message ${id.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
7578
|
+
conversation_id: conv.id,
|
|
7579
|
+
message_id: id
|
|
7580
|
+
});
|
|
7581
|
+
});
|
|
7582
|
+
}
|
|
7235
7583
|
if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
|
|
7236
7584
|
else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
|
|
7237
7585
|
if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
|
|
@@ -7285,6 +7633,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7285
7633
|
message_id: inFlight.evidentMessageId
|
|
7286
7634
|
});
|
|
7287
7635
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
7636
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
7637
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
7638
|
+
messages,
|
|
7639
|
+
inFlight.opencodeMessageId,
|
|
7640
|
+
inFlight.evidentMessageId
|
|
7641
|
+
);
|
|
7288
7642
|
const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
|
|
7289
7643
|
try {
|
|
7290
7644
|
await this.markFailed(
|
|
@@ -7293,7 +7647,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7293
7647
|
sessionId,
|
|
7294
7648
|
error2,
|
|
7295
7649
|
usage,
|
|
7296
|
-
failure
|
|
7650
|
+
failure,
|
|
7651
|
+
usageAgentName,
|
|
7652
|
+
subagentInvocations
|
|
7297
7653
|
);
|
|
7298
7654
|
} catch (err) {
|
|
7299
7655
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7336,9 +7692,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7336
7692
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7337
7693
|
return;
|
|
7338
7694
|
}
|
|
7695
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
|
|
7696
|
+
const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
|
|
7339
7697
|
const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
|
|
7340
7698
|
const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
|
|
7341
|
-
if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
|
|
7699
|
+
if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
|
|
7342
7700
|
inFlight.stuckReported = true;
|
|
7343
7701
|
void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
|
|
7344
7702
|
stuck_for_ms: this.now() - inFlight.dispatchedAt
|
|
@@ -7499,7 +7857,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7499
7857
|
const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
|
|
7500
7858
|
(sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
|
|
7501
7859
|
);
|
|
7502
|
-
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
7860
|
+
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
|
|
7503
7861
|
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
7504
7862
|
this.log({
|
|
7505
7863
|
level: "debug",
|
|
@@ -7534,6 +7892,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7534
7892
|
});
|
|
7535
7893
|
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
7536
7894
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
7895
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
7896
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
7897
|
+
messages,
|
|
7898
|
+
inFlight.opencodeMessageId,
|
|
7899
|
+
inFlight.evidentMessageId
|
|
7900
|
+
);
|
|
7537
7901
|
try {
|
|
7538
7902
|
await this.markDone(
|
|
7539
7903
|
conv.id,
|
|
@@ -7541,7 +7905,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7541
7905
|
sessionId,
|
|
7542
7906
|
inFlight.opencodeMessageId,
|
|
7543
7907
|
title,
|
|
7544
|
-
usage
|
|
7908
|
+
usage,
|
|
7909
|
+
usageAgentName,
|
|
7910
|
+
subagentInvocations
|
|
7545
7911
|
);
|
|
7546
7912
|
} catch (err) {
|
|
7547
7913
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7720,6 +8086,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7720
8086
|
if (state === "failed" && !restartAborted) {
|
|
7721
8087
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
7722
8088
|
const usage = messageUsage(messages, ocId ?? "");
|
|
8089
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
8090
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
8091
|
+
messages,
|
|
8092
|
+
ocId ?? "",
|
|
8093
|
+
row.id
|
|
8094
|
+
);
|
|
7723
8095
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
7724
8096
|
this.log({
|
|
7725
8097
|
level: "error",
|
|
@@ -7728,7 +8100,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7728
8100
|
message_id: row.id
|
|
7729
8101
|
});
|
|
7730
8102
|
try {
|
|
7731
|
-
await this.markFailed(
|
|
8103
|
+
await this.markFailed(
|
|
8104
|
+
row.conversation_id,
|
|
8105
|
+
row.id,
|
|
8106
|
+
sessionId,
|
|
8107
|
+
error2,
|
|
8108
|
+
usage,
|
|
8109
|
+
failure,
|
|
8110
|
+
usageAgentName,
|
|
8111
|
+
subagentInvocations
|
|
8112
|
+
);
|
|
7732
8113
|
} catch (err) {
|
|
7733
8114
|
if (err instanceof ChannelAuthError) throw err;
|
|
7734
8115
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7890,7 +8271,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7890
8271
|
try {
|
|
7891
8272
|
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
7892
8273
|
const usage = messageUsage(messages, ocId ?? "");
|
|
7893
|
-
|
|
8274
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
8275
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
8276
|
+
messages,
|
|
8277
|
+
ocId ?? "",
|
|
8278
|
+
row.id
|
|
8279
|
+
);
|
|
8280
|
+
await this.markDone(
|
|
8281
|
+
row.conversation_id,
|
|
8282
|
+
row.id,
|
|
8283
|
+
sessionId,
|
|
8284
|
+
ocId,
|
|
8285
|
+
title,
|
|
8286
|
+
usage,
|
|
8287
|
+
usageAgentName,
|
|
8288
|
+
subagentInvocations
|
|
8289
|
+
);
|
|
7894
8290
|
} catch (err) {
|
|
7895
8291
|
if (err instanceof ChannelAuthError) throw err;
|
|
7896
8292
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -8294,6 +8690,166 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8294
8690
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
8295
8691
|
return parent;
|
|
8296
8692
|
}
|
|
8693
|
+
usageAgentName(messages, userMessageId) {
|
|
8694
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
8695
|
+
const mode = reply?.info?.mode;
|
|
8696
|
+
if (typeof mode === "string" && mode.length > 0) return mode;
|
|
8697
|
+
const agent = reply?.info?.agent;
|
|
8698
|
+
return typeof agent === "string" && agent.length > 0 ? agent : null;
|
|
8699
|
+
}
|
|
8700
|
+
async resolveSubagentInvocations(messages, userMessageId, messageId, phase = "terminal") {
|
|
8701
|
+
if (!messages) return void 0;
|
|
8702
|
+
const cache = phase === "prefetch" ? this.subagentInvocationPrefetches : this.subagentInvocationCollections;
|
|
8703
|
+
const cached = cache.get(messageId);
|
|
8704
|
+
if (cached) return cached;
|
|
8705
|
+
const collection = this.buildSubagentInvocations(messages, userMessageId, messageId).catch(
|
|
8706
|
+
(err) => {
|
|
8707
|
+
this.log({
|
|
8708
|
+
level: "warn",
|
|
8709
|
+
message: `Best-effort subagent usage collection failed for message ${messageId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
8710
|
+
message_id: messageId
|
|
8711
|
+
});
|
|
8712
|
+
return void 0;
|
|
8713
|
+
}
|
|
8714
|
+
);
|
|
8715
|
+
cache.set(messageId, collection);
|
|
8716
|
+
const result = await collection;
|
|
8717
|
+
if (result === void 0 && cache.get(messageId) === collection) cache.delete(messageId);
|
|
8718
|
+
return result;
|
|
8719
|
+
}
|
|
8720
|
+
clearSubagentInvocationCaches(messageId) {
|
|
8721
|
+
this.subagentInvocationCollections.delete(messageId);
|
|
8722
|
+
this.subagentInvocationPrefetches.delete(messageId);
|
|
8723
|
+
}
|
|
8724
|
+
async buildSubagentInvocations(messages, userMessageId, messageId) {
|
|
8725
|
+
const rootCalls = collectTaskCalls(messages, userMessageId);
|
|
8726
|
+
if (rootCalls.length === 0) return void 0;
|
|
8727
|
+
const childMessages = /* @__PURE__ */ new Map();
|
|
8728
|
+
const seenCallIds = new Set(rootCalls.map((call) => call.callID));
|
|
8729
|
+
const work = rootCalls.map((call) => ({
|
|
8730
|
+
call,
|
|
8731
|
+
depth: 1
|
|
8732
|
+
}));
|
|
8733
|
+
const payload = [];
|
|
8734
|
+
const fetchChildMessages = (sessionId) => {
|
|
8735
|
+
const cached = childMessages.get(sessionId);
|
|
8736
|
+
if (cached) return cached;
|
|
8737
|
+
const pending = (async () => {
|
|
8738
|
+
try {
|
|
8739
|
+
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
8740
|
+
if (!res.ok) {
|
|
8741
|
+
this.log({
|
|
8742
|
+
level: "warn",
|
|
8743
|
+
message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 omitting invocation telemetry`,
|
|
8744
|
+
message_id: messageId
|
|
8745
|
+
});
|
|
8746
|
+
return null;
|
|
8747
|
+
}
|
|
8748
|
+
const body = await res.json();
|
|
8749
|
+
if (!Array.isArray(body)) throw new Error("response body was not a message array");
|
|
8750
|
+
return body;
|
|
8751
|
+
} catch (err) {
|
|
8752
|
+
this.log({
|
|
8753
|
+
level: "warn",
|
|
8754
|
+
message: `Best-effort subagent session fetch failed for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
8755
|
+
message_id: messageId
|
|
8756
|
+
});
|
|
8757
|
+
return null;
|
|
8758
|
+
}
|
|
8759
|
+
})();
|
|
8760
|
+
childMessages.set(sessionId, pending);
|
|
8761
|
+
return pending;
|
|
8762
|
+
};
|
|
8763
|
+
const fetchChildWithoutBlocking = async (sessionId) => {
|
|
8764
|
+
const pending = fetchChildMessages(sessionId);
|
|
8765
|
+
let timer;
|
|
8766
|
+
const timeout = new Promise((resolve4) => {
|
|
8767
|
+
timer = setTimeout(() => {
|
|
8768
|
+
this.log({
|
|
8769
|
+
level: "warn",
|
|
8770
|
+
message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} was slow \u2014 omitting invocation telemetry without delaying completion`,
|
|
8771
|
+
message_id: messageId
|
|
8772
|
+
});
|
|
8773
|
+
resolve4(null);
|
|
8774
|
+
}, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
|
|
8775
|
+
});
|
|
8776
|
+
try {
|
|
8777
|
+
return await Promise.race([pending, timeout]);
|
|
8778
|
+
} finally {
|
|
8779
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
8780
|
+
}
|
|
8781
|
+
};
|
|
8782
|
+
while (work.length > 0) {
|
|
8783
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8784
|
+
for (const item of work.splice(0)) {
|
|
8785
|
+
const group = groups.get(item.call.childSessionId) ?? [];
|
|
8786
|
+
group.push(item);
|
|
8787
|
+
groups.set(item.call.childSessionId, group);
|
|
8788
|
+
}
|
|
8789
|
+
const groupResults = await Promise.all(
|
|
8790
|
+
[...groups].map(async ([sessionId, items]) => ({
|
|
8791
|
+
sessionId,
|
|
8792
|
+
items,
|
|
8793
|
+
messages: sessionId === null ? [] : await fetchChildWithoutBlocking(sessionId)
|
|
8794
|
+
}))
|
|
8795
|
+
);
|
|
8796
|
+
for (const { sessionId, items, messages: child } of groupResults) {
|
|
8797
|
+
if (sessionId !== null && child === null) continue;
|
|
8798
|
+
const attribution = sessionId === null ? { invocations: [], unattributed: [] } : attributeTaskCallUsage(
|
|
8799
|
+
child,
|
|
8800
|
+
items.map(({ call }) => ({
|
|
8801
|
+
callID: call.callID,
|
|
8802
|
+
timeStart: call.timeStart,
|
|
8803
|
+
timeEnd: call.timeEnd
|
|
8804
|
+
}))
|
|
8805
|
+
);
|
|
8806
|
+
if (sessionId !== null && attribution.unattributed.length > 0) {
|
|
8807
|
+
this.log({
|
|
8808
|
+
level: "warn",
|
|
8809
|
+
message: `Omitted ${attribution.unattributed.length} unattributable assistant message(s) from subagent usage for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} \u2014 assigned to no invocation window`,
|
|
8810
|
+
message_id: messageId
|
|
8811
|
+
});
|
|
8812
|
+
}
|
|
8813
|
+
const usageByCall = new Map(
|
|
8814
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.usage])
|
|
8815
|
+
);
|
|
8816
|
+
const messagesByCall = new Map(
|
|
8817
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.messages])
|
|
8818
|
+
);
|
|
8819
|
+
for (const { call, depth } of items) {
|
|
8820
|
+
const usage = usageByCall.get(call.callID) ?? null;
|
|
8821
|
+
payload.push({
|
|
8822
|
+
tool_call_id: call.callID,
|
|
8823
|
+
agent_name: call.subagentName,
|
|
8824
|
+
opencode_session_id: call.childSessionId,
|
|
8825
|
+
parent_opencode_session_id: call.parentSessionId,
|
|
8826
|
+
depth,
|
|
8827
|
+
status: call.status,
|
|
8828
|
+
started_at: call.timeStart === null ? null : new Date(call.timeStart).toISOString(),
|
|
8829
|
+
ended_at: call.timeEnd === null ? null : new Date(call.timeEnd).toISOString(),
|
|
8830
|
+
usage_provider_id: usage?.usage_provider_id ?? call.model?.providerID ?? null,
|
|
8831
|
+
usage_model_id: usage?.usage_model_id ?? call.model?.modelID ?? null,
|
|
8832
|
+
usage_tokens_input: usage?.usage_tokens_input ?? null,
|
|
8833
|
+
usage_tokens_output: usage?.usage_tokens_output ?? null,
|
|
8834
|
+
usage_tokens_reasoning: usage?.usage_tokens_reasoning ?? null,
|
|
8835
|
+
usage_tokens_cache_read: usage?.usage_tokens_cache_read ?? null,
|
|
8836
|
+
usage_tokens_cache_write: usage?.usage_tokens_cache_write ?? null,
|
|
8837
|
+
usage_cost_usd: usage?.usage_cost_usd ?? null
|
|
8838
|
+
});
|
|
8839
|
+
for (const assigned of messagesByCall.get(call.callID) ?? []) {
|
|
8840
|
+
const parentId = assigned.info?.parentID ?? assigned.parentID;
|
|
8841
|
+
if (!parentId) continue;
|
|
8842
|
+
for (const nested of collectTaskCalls([assigned], parentId)) {
|
|
8843
|
+
if (seenCallIds.has(nested.callID)) continue;
|
|
8844
|
+
seenCallIds.add(nested.callID);
|
|
8845
|
+
work.push({ call: nested, depth: depth + 1 });
|
|
8846
|
+
}
|
|
8847
|
+
}
|
|
8848
|
+
}
|
|
8849
|
+
}
|
|
8850
|
+
}
|
|
8851
|
+
return payload.length > 0 ? payload : void 0;
|
|
8852
|
+
}
|
|
8297
8853
|
/**
|
|
8298
8854
|
* OpenCode's synchronous default session title (e.g.
|
|
8299
8855
|
* `"New session - 1737800000000"`), assigned immediately when a session is
|
|
@@ -8777,7 +9333,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8777
9333
|
* watcher retries next tick within the
|
|
8778
9334
|
* deadline, Finding 4).
|
|
8779
9335
|
*/
|
|
8780
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
|
|
9336
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
|
|
8781
9337
|
const res = await this.fetchImpl(
|
|
8782
9338
|
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
8783
9339
|
{
|
|
@@ -8793,15 +9349,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8793
9349
|
opencode_session_id: sessionId,
|
|
8794
9350
|
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
8795
9351
|
...title ? { title } : {},
|
|
8796
|
-
...usage ? usage : {}
|
|
9352
|
+
...usage ? usage : {},
|
|
9353
|
+
...usageAgentName ? { usage_agent_name: usageAgentName } : {},
|
|
9354
|
+
...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
|
|
8797
9355
|
})
|
|
8798
9356
|
}
|
|
8799
9357
|
);
|
|
8800
9358
|
this.assertAuth(res, "marking message as done");
|
|
8801
|
-
if (res.ok)
|
|
9359
|
+
if (res.ok) {
|
|
9360
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
9361
|
+
return;
|
|
9362
|
+
}
|
|
8802
9363
|
if (isRetryableStatus(res.status)) {
|
|
8803
9364
|
throw new Error(`marking message as done: HTTP ${res.status}`);
|
|
8804
9365
|
}
|
|
9366
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8805
9367
|
throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
|
|
8806
9368
|
}
|
|
8807
9369
|
/**
|
|
@@ -8816,7 +9378,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8816
9378
|
* exists but is wedged, so the next attempt must get a fresh one
|
|
8817
9379
|
* instead of reusing it — see WI-1's server-side null-clearing PATCH).
|
|
8818
9380
|
*/
|
|
8819
|
-
async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
|
|
9381
|
+
async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
|
|
8820
9382
|
const body = { status: "failed" };
|
|
8821
9383
|
if (sessionId === null) {
|
|
8822
9384
|
body.opencode_session_id = null;
|
|
@@ -8825,23 +9387,33 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8825
9387
|
}
|
|
8826
9388
|
if (error2 !== void 0) body.error = error2;
|
|
8827
9389
|
if (usage) Object.assign(body, usage);
|
|
9390
|
+
if (usageAgentName) body.usage_agent_name = usageAgentName;
|
|
9391
|
+
if (subagentInvocations && subagentInvocations.length > 0) {
|
|
9392
|
+
body.subagent_invocations = subagentInvocations;
|
|
9393
|
+
}
|
|
8828
9394
|
if (failure) {
|
|
8829
9395
|
body.failure_kind = failure.kind;
|
|
8830
9396
|
body.failure_provider_id = failure.providerId;
|
|
8831
9397
|
body.failure_model_id = failure.modelId;
|
|
8832
9398
|
body.failure_reason = failure.reason;
|
|
8833
9399
|
}
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
9400
|
+
try {
|
|
9401
|
+
await this.callWithRetry(
|
|
9402
|
+
"marking message as failed",
|
|
9403
|
+
() => this.fetchImpl(
|
|
9404
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
9405
|
+
{
|
|
9406
|
+
method: "PATCH",
|
|
9407
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
9408
|
+
body: JSON.stringify(body)
|
|
9409
|
+
}
|
|
9410
|
+
)
|
|
9411
|
+
);
|
|
9412
|
+
} catch (err) {
|
|
9413
|
+
if (err instanceof ChannelTerminalError) this.clearSubagentInvocationCaches(messageId);
|
|
9414
|
+
throw err;
|
|
9415
|
+
}
|
|
9416
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8845
9417
|
}
|
|
8846
9418
|
/**
|
|
8847
9419
|
* Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
|
|
@@ -10637,14 +11209,11 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
10637
11209
|
report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
|
|
10638
11210
|
isLocalCredentialProblem,
|
|
10639
11211
|
forcedOnHint: "run `claude` to sign in",
|
|
10640
|
-
firstDelayMs:
|
|
10641
|
-
nextDelayMs:
|
|
10642
|
-
failureLogLevel:
|
|
11212
|
+
firstDelayMs: firstReportDelayMs,
|
|
11213
|
+
nextDelayMs: usageReportDelayMs,
|
|
11214
|
+
failureLogLevel: usageReportFailureLogLevel
|
|
10643
11215
|
});
|
|
10644
11216
|
}
|
|
10645
|
-
var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
|
|
10646
|
-
var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
|
|
10647
|
-
var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
10648
11217
|
function scheduleResourceUsageReporting(state, options) {
|
|
10649
11218
|
const { enabled, warnings } = resolveResourceUsageReportingEnabled(
|
|
10650
11219
|
options.resourceUsageReporting,
|
|
@@ -10697,10 +11266,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10697
11266
|
consecutiveFailures++;
|
|
10698
11267
|
logActivity(state, {
|
|
10699
11268
|
type: "info",
|
|
10700
|
-
level:
|
|
10701
|
-
consecutiveFailures,
|
|
10702
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
10703
|
-
),
|
|
11269
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
10704
11270
|
message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
|
|
10705
11271
|
});
|
|
10706
11272
|
}
|
|
@@ -10709,20 +11275,11 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
10709
11275
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
10710
11276
|
logActivity(state, {
|
|
10711
11277
|
type: "info",
|
|
10712
|
-
level:
|
|
10713
|
-
consecutiveFailures,
|
|
10714
|
-
RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
|
|
10715
|
-
),
|
|
11278
|
+
level: usageReportFailureLogLevel(consecutiveFailures),
|
|
10716
11279
|
message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
|
|
10717
11280
|
});
|
|
10718
11281
|
} finally {
|
|
10719
|
-
state.resourceUsageTimer = setTimeout(
|
|
10720
|
-
() => void tick(),
|
|
10721
|
-
jitteredDelayMs(
|
|
10722
|
-
RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
|
|
10723
|
-
RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
|
|
10724
|
-
)
|
|
10725
|
-
);
|
|
11282
|
+
state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
|
|
10726
11283
|
}
|
|
10727
11284
|
};
|
|
10728
11285
|
state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
|
|
@@ -11269,6 +11826,7 @@ async function run(options) {
|
|
|
11269
11826
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
11270
11827
|
}
|
|
11271
11828
|
}
|
|
11829
|
+
await reloadProviderCache(state.port);
|
|
11272
11830
|
const noProviderWarning = buildNoProviderWarning(
|
|
11273
11831
|
await hasAnyConfiguredProvider(state.port)
|
|
11274
11832
|
);
|