@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 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 notifyAgentDisconnected(agentId, authHeader) {
674
- const apiUrl = getApiUrlConfig();
686
+ async function postBestEffort(path, authHeader, body) {
675
687
  try {
676
- const response = await fetch(`${apiUrl}/runners/${agentId}/disconnect`, {
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: { Authorization: authHeader },
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: describeBestEffortError(error2) };
706
+ return { ok: false, error: describeTimeoutError(error2, BEST_EFFORT_NOTIFY_TIMEOUT_MS) };
691
707
  }
692
708
  }
693
- function describeBestEffortError(error2) {
709
+ function describeTimeoutError(error2, timeoutMs) {
694
710
  const name = error2?.name;
695
711
  if (name === "TimeoutError" || name === "AbortError") {
696
- return `timed out after ${BEST_EFFORT_NOTIFY_TIMEOUT_MS}ms`;
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
- try {
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
- try {
735
- const apiUrl = getApiUrlConfig();
736
- const response = await fetch(`${apiUrl}/runners/${agentId}/claude-usage`, {
737
- method: "POST",
738
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
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
- try {
775
- const apiUrl = getApiUrlConfig();
776
- const response = await fetch(`${apiUrl}/runners/${agentId}/openai-usage`, {
777
- method: "POST",
778
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
779
- body: JSON.stringify({
780
- primary: toReportedOpenAiWindow(snapshot.primary),
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
- try {
802
- const apiUrl = getApiUrlConfig();
803
- const response = await fetch(`${apiUrl}/runners/${agentId}/resource-usage`, {
804
- method: "POST",
805
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
806
- body: JSON.stringify({
807
- cpu_percent: usage.cpuPercent,
808
- cpu_peak_percent: usage.cpuPeakPercent,
809
- cpu_count: usage.cpuCount,
810
- memory_total_bytes: usage.memoryTotalBytes,
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}: ${describeFetchError(error2)}. The credentials were NOT validated.`,
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 { owner: cachedOwner.owner, ownerLookupError: null };
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 { owner: null, ownerLookupError: `HTTP ${response.status}` };
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 { owner: null, ownerLookupError: "malformed response" };
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 { owner: null, ownerLookupError: "malformed response" };
1047
+ return { subscription: null, ownerLookupError: "malformed response" };
1123
1048
  }
1124
- const owner = {
1125
- email: profile.account.email,
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
- rateLimitTier: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
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 { owner, ownerLookupError: null };
1054
+ cachedOwner = { accessToken, owner: subscription };
1055
+ return { subscription, ownerLookupError: null };
1131
1056
  } catch (error2) {
1132
- return { owner: null, ownerLookupError: ownerLookupFailure(error2) };
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 { owner, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
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
- owner,
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 messageUsage(messages, userMessageId) {
3267
- if (!messages || messages.length === 0) return null;
3268
- const byParentAll = messages.filter(
3269
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
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 byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
3272
- const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
3273
- let correlated;
3274
- if (byParent.length > 0) {
3275
- correlated = byParent;
3276
- } else {
3277
- const reply = findAssistantReplyAfter(messages, userMessageId);
3278
- correlated = reply ? [reply] : [];
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
- if (correlated.length === 0) return null;
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 correlated) {
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 "OpenCode never reported a cost" (never inferred from
3327
- // tokens) distinct from a genuine 0-cost turn, which would set
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(conv.id, message.id, sessionId, ocId, title, usage);
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(conv.id, message.id, sessionId, error2, usage, failure);
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(row.conversation_id, row.id, sessionId, error2, usage, failure);
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
- await this.markDone(row.conversation_id, row.id, sessionId, ocId, title, usage);
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) return;
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
- await this.callWithRetry(
8835
- "marking message as failed",
8836
- () => this.fetchImpl(
8837
- `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8838
- {
8839
- method: "PATCH",
8840
- headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
8841
- body: JSON.stringify(body)
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: () => FIRST_REPORT_DELAY_MS,
10641
- nextDelayMs: nextReportDelayMs,
10642
- failureLogLevel: claudeUsageFailureLogLevel
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: reportFailureLogLevel(
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: reportFailureLogLevel(
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
  );