@evident-ai/cli 3.4.1-dev.1633d8c → 3.4.1-dev.180238a

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
@@ -683,12 +683,15 @@ async function resolveAgentIdFromKey(authHeader) {
683
683
  }
684
684
  }
685
685
  var BEST_EFFORT_NOTIFY_TIMEOUT_MS = 2e3;
686
- async function notifyAgentDisconnected(agentId, authHeader) {
687
- const apiUrl = getApiUrlConfig();
686
+ async function postBestEffort(path, authHeader, body) {
688
687
  try {
689
- 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}`, {
690
692
  method: "POST",
691
- headers: { Authorization: authHeader },
693
+ headers,
694
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
692
695
  signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
693
696
  });
694
697
  if (!response.ok) {
@@ -700,65 +703,32 @@ async function notifyAgentDisconnected(agentId, authHeader) {
700
703
  }
701
704
  return { ok: true };
702
705
  } catch (error2) {
703
- return { ok: false, error: describeBestEffortError(error2) };
706
+ return { ok: false, error: describeTimeoutError(error2, BEST_EFFORT_NOTIFY_TIMEOUT_MS) };
704
707
  }
705
708
  }
706
- function describeBestEffortError(error2) {
709
+ function describeTimeoutError(error2, timeoutMs) {
707
710
  const name = error2?.name;
708
711
  if (name === "TimeoutError" || name === "AbortError") {
709
- return `timed out after ${BEST_EFFORT_NOTIFY_TIMEOUT_MS}ms`;
712
+ return `timed out after ${timeoutMs}ms`;
710
713
  }
711
714
  return error2 instanceof Error ? error2.message : String(error2);
712
715
  }
716
+ async function notifyAgentDisconnected(agentId, authHeader) {
717
+ return postBestEffort(`/runners/${agentId}/disconnect`, authHeader);
718
+ }
713
719
  async function reportMicrovmId(agentId, authHeader, microvmId) {
714
- try {
715
- const apiUrl = getApiUrlConfig();
716
- const response = await fetch(`${apiUrl}/runners/${agentId}/microvm`, {
717
- method: "POST",
718
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
719
- body: JSON.stringify({ microvm_id: microvmId }),
720
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
721
- });
722
- if (!response.ok) {
723
- const serverMessage = await readErrorMessage(response);
724
- return {
725
- ok: false,
726
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
727
- };
728
- }
729
- return { ok: true };
730
- } catch (error2) {
731
- return { ok: false, error: describeBestEffortError(error2) };
732
- }
720
+ return postBestEffort(`/runners/${agentId}/microvm`, authHeader, { microvm_id: microvmId });
733
721
  }
734
722
  function toReportedWindow(window) {
735
723
  if (!window) return null;
736
724
  return { utilization: window.utilization, resets_at: window.resetsAt };
737
725
  }
738
726
  async function reportClaudeUsage(agentId, authHeader, snapshot) {
739
- try {
740
- const apiUrl = getApiUrlConfig();
741
- const response = await fetch(`${apiUrl}/runners/${agentId}/claude-usage`, {
742
- method: "POST",
743
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
744
- body: JSON.stringify({
745
- five_hour: toReportedWindow(snapshot.fiveHour),
746
- seven_day: toReportedWindow(snapshot.sevenDay),
747
- subscription: toReportedSubscription(snapshot.subscription)
748
- }),
749
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
750
- });
751
- if (!response.ok) {
752
- const serverMessage = await readErrorMessage(response);
753
- return {
754
- ok: false,
755
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
756
- };
757
- }
758
- return { ok: true };
759
- } catch (error2) {
760
- return { ok: false, error: describeBestEffortError(error2) };
761
- }
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
+ });
762
732
  }
763
733
  function toReportedOpenAiWindow(window) {
764
734
  if (!window) return null;
@@ -769,61 +739,25 @@ function toReportedOpenAiWindow(window) {
769
739
  };
770
740
  }
771
741
  async function reportOpenAiUsage(agentId, authHeader, snapshot) {
772
- try {
773
- const apiUrl = getApiUrlConfig();
774
- const response = await fetch(`${apiUrl}/runners/${agentId}/openai-usage`, {
775
- method: "POST",
776
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
777
- body: JSON.stringify({
778
- primary: toReportedOpenAiWindow(snapshot.primary),
779
- secondary: toReportedOpenAiWindow(snapshot.secondary),
780
- has_credits: snapshot.hasCredits,
781
- credits_unlimited: snapshot.creditsUnlimited,
782
- subscription: toReportedSubscription(snapshot.subscription)
783
- }),
784
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
785
- });
786
- if (!response.ok) {
787
- const serverMessage = await readErrorMessage(response);
788
- return {
789
- ok: false,
790
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
791
- };
792
- }
793
- return { ok: true };
794
- } catch (error2) {
795
- return { ok: false, error: describeBestEffortError(error2) };
796
- }
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
+ });
797
749
  }
798
750
  async function reportResourceUsage(agentId, authHeader, usage) {
799
- try {
800
- const apiUrl = getApiUrlConfig();
801
- const response = await fetch(`${apiUrl}/runners/${agentId}/resource-usage`, {
802
- method: "POST",
803
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
804
- body: JSON.stringify({
805
- cpu_percent: usage.cpuPercent,
806
- cpu_peak_percent: usage.cpuPeakPercent,
807
- cpu_count: usage.cpuCount,
808
- memory_total_bytes: usage.memoryTotalBytes,
809
- memory_available_bytes: usage.memoryAvailableBytes,
810
- disk_total_bytes: usage.diskTotalBytes,
811
- disk_free_bytes: usage.diskFreeBytes,
812
- opencode_db_bytes: usage.opencodeDbBytes
813
- }),
814
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
815
- });
816
- if (!response.ok) {
817
- const serverMessage = await readErrorMessage(response);
818
- return {
819
- ok: false,
820
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
821
- };
822
- }
823
- return { ok: true };
824
- } catch (error2) {
825
- return { ok: false, error: describeBestEffortError(error2) };
826
- }
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
+ });
827
761
  }
828
762
  async function getAgentInfo(agentId, authHeader) {
829
763
  const apiUrl = getApiUrlConfig();
@@ -875,13 +809,6 @@ function authLabelFor(credentials2) {
875
809
  }
876
810
  return "user token";
877
811
  }
878
- function describeFetchError(error2) {
879
- const name = error2?.name;
880
- if (name === "TimeoutError" || name === "AbortError") {
881
- return `timed out after ${STATUS_TIMEOUT_MS}ms waiting for a response`;
882
- }
883
- return error2 instanceof Error ? error2.message : String(error2);
884
- }
885
812
  async function checkStatus(jsonMode) {
886
813
  const apiUrl = getApiUrlConfig();
887
814
  const credentials2 = await getAuthCredentials();
@@ -909,7 +836,7 @@ async function checkStatus(jsonMode) {
909
836
  endpoint: apiUrl,
910
837
  authLabel: authLabelFor(credentials2),
911
838
  reason: "unreachable",
912
- 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.`,
913
840
  exitCode: 75
914
841
  };
915
842
  }
@@ -3261,21 +3188,74 @@ function collectSubagentSessions(messages, userMessageId) {
3261
3188
  }
3262
3189
  return refs;
3263
3190
  }
3264
- function messageUsage(messages, userMessageId) {
3265
- if (!messages || messages.length === 0) return null;
3266
- const byParentAll = messages.filter(
3267
- (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)
3268
3230
  );
3269
- const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
3270
- const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
3271
- let correlated;
3272
- if (byParent.length > 0) {
3273
- correlated = byParent;
3274
- } else {
3275
- const reply = findAssistantReplyAfter(messages, userMessageId);
3276
- 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);
3277
3246
  }
3278
- 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;
3279
3259
  let sawAnyUsage = false;
3280
3260
  let inputSum = 0;
3281
3261
  let outputSum = 0;
@@ -3286,7 +3266,7 @@ function messageUsage(messages, userMessageId) {
3286
3266
  let sawCost = false;
3287
3267
  let modelId = null;
3288
3268
  let providerId = null;
3289
- for (const m of correlated) {
3269
+ for (const m of selected) {
3290
3270
  const info = m.info;
3291
3271
  if (!info) continue;
3292
3272
  const tokens = info.tokens;
@@ -3321,12 +3301,28 @@ function messageUsage(messages, userMessageId) {
3321
3301
  usage_tokens_reasoning: reasoningSum,
3322
3302
  usage_tokens_cache_read: cacheReadSum,
3323
3303
  usage_tokens_cache_write: cacheWriteSum,
3324
- // NULL means "OpenCode never reported a cost" (never inferred from
3325
- // tokens) distinct from a genuine 0-cost turn, which would set
3326
- // `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`.
3327
3306
  usage_cost_usd: sawCost ? costSum : null
3328
3307
  };
3329
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
+ }
3330
3326
  function messageRunState(messages, userMessageId) {
3331
3327
  if (!messages || messages.length === 0) return "unknown";
3332
3328
  const hasUser = messages.some((m) => idOf(m) === userMessageId);
@@ -3456,6 +3452,28 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
3456
3452
  (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
3457
3453
  );
3458
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
+ }
3459
3477
  async function hasAnyConfiguredProvider(port) {
3460
3478
  try {
3461
3479
  const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
@@ -3487,6 +3505,76 @@ async function hasAnyConfiguredProvider(port) {
3487
3505
  return null;
3488
3506
  }
3489
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
+ }
3490
3578
  async function reloadProviderCache(port) {
3491
3579
  try {
3492
3580
  const res = await timedFetch(`${opencodeBase(port)}/config`, {
@@ -4564,13 +4652,6 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
4564
4652
  envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
4565
4653
  });
4566
4654
  }
4567
- function nextReportDelayMs(random = Math.random) {
4568
- return usageReportDelayMs(random);
4569
- }
4570
- var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
4571
- function claudeUsageFailureLogLevel(consecutiveFailures) {
4572
- return usageReportFailureLogLevel(consecutiveFailures);
4573
- }
4574
4655
 
4575
4656
  // src/lib/openai-usage-reporting.ts
4576
4657
  function resolveOpenAiUsageReportingMode(flagValue, env) {
@@ -5226,6 +5307,10 @@ var DEFAULT_RETRY_POLICY = {
5226
5307
  baseDelayMs: 500,
5227
5308
  maxDelayMs: 3e4
5228
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;
5229
5314
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
5230
5315
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
5231
5316
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
@@ -5366,6 +5451,17 @@ var ChannelDriver = class _ChannelDriver {
5366
5451
  * message; it is removed once its in-flight set empties.
5367
5452
  */
5368
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();
5369
5465
  /**
5370
5466
  * AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
5371
5467
  * dispatched and are still in-flight. A message in this set is never
@@ -5549,6 +5645,13 @@ var ChannelDriver = class _ChannelDriver {
5549
5645
  * no watcher) can resolve the title.
5550
5646
  */
5551
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();
5552
5655
  /** Serialises drains so a reconnect during a drain doesn't double-process. */
5553
5656
  draining = false;
5554
5657
  /**
@@ -5763,6 +5866,21 @@ var ChannelDriver = class _ChannelDriver {
5763
5866
  }
5764
5867
  return ids;
5765
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
+ }
5766
5884
  /**
5767
5885
  * File-pull work, for `run.ts`'s idle accounting (#559).
5768
5886
  *
@@ -5829,6 +5947,8 @@ var ChannelDriver = class _ChannelDriver {
5829
5947
  */
5830
5948
  stop() {
5831
5949
  this.stopped = true;
5950
+ this.sessionErrorStream?.abort.abort();
5951
+ this.sessionErrorStream = null;
5832
5952
  }
5833
5953
  /**
5834
5954
  * The server clears this request when a new MicroVM identity is recorded, so a
@@ -5903,6 +6023,7 @@ var ChannelDriver = class _ChannelDriver {
5903
6023
  */
5904
6024
  async processConversation(conv) {
5905
6025
  const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
6026
+ this.ensureSessionErrorStream();
5906
6027
  const messages = await this.getPendingMessages(conv.id);
5907
6028
  let dispatched = 0;
5908
6029
  let skippedAlreadyDispatched = 0;
@@ -6249,6 +6370,23 @@ var ChannelDriver = class _ChannelDriver {
6249
6370
  if (state === "running" || state === "queued") {
6250
6371
  const ongoing = await isSessionOngoing(this.port, sessionId);
6251
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
+ }
6252
6390
  return this.reattachRedrive(conv, sessionId, message, ocId);
6253
6391
  }
6254
6392
  if (ongoing === false) {
@@ -6338,16 +6476,37 @@ var ChannelDriver = class _ChannelDriver {
6338
6476
  if (state === "done") {
6339
6477
  const title = await this.resolveSessionTitle(sessionId, conv.id);
6340
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
+ );
6341
6485
  this.log({
6342
6486
  level: "info",
6343
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`,
6344
6488
  conversation_id: conv.id,
6345
6489
  message_id: message.id
6346
6490
  });
6347
- 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
+ );
6348
6501
  } else {
6349
6502
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
6350
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
+ );
6351
6510
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
6352
6511
  this.log({
6353
6512
  level: "error",
@@ -6355,7 +6514,16 @@ var ChannelDriver = class _ChannelDriver {
6355
6514
  conversation_id: conv.id,
6356
6515
  message_id: message.id
6357
6516
  });
6358
- 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
+ );
6359
6527
  }
6360
6528
  if (ocId !== null) {
6361
6529
  await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
@@ -6899,6 +7067,24 @@ var ChannelDriver = class _ChannelDriver {
6899
7067
  ambiguousPinnedSinceMs: 0,
6900
7068
  ambiguousResolved: false
6901
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
+ }
6902
7088
  }
6903
7089
  /**
6904
7090
  * Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
@@ -7103,6 +7289,7 @@ var ChannelDriver = class _ChannelDriver {
7103
7289
  ensureWatcherRunning(sessionId) {
7104
7290
  const watcher = this.watchers.get(sessionId);
7105
7291
  if (!watcher) return;
7292
+ this.ensureSessionErrorStream();
7106
7293
  if (watcher.loop) return;
7107
7294
  if (watcher.inFlight.size === 0) {
7108
7295
  this.watchers.delete(sessionId);
@@ -7118,6 +7305,154 @@ var ChannelDriver = class _ChannelDriver {
7118
7305
  });
7119
7306
  watcher.loop = loop;
7120
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
+ }
7121
7456
  /**
7122
7457
  * The per-session polling loop (WI-3). Once per tick it:
7123
7458
  * 1. polls `GET /session/:id/message` once and, per in-flight message,
@@ -7230,6 +7565,21 @@ var ChannelDriver = class _ChannelDriver {
7230
7565
  const conv = watcher.conv;
7231
7566
  const state = messageRunState(messages, inFlight.opencodeMessageId);
7232
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
+ }
7233
7583
  if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
7234
7584
  else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
7235
7585
  if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
@@ -7283,6 +7633,12 @@ var ChannelDriver = class _ChannelDriver {
7283
7633
  message_id: inFlight.evidentMessageId
7284
7634
  });
7285
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
+ );
7286
7642
  const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
7287
7643
  try {
7288
7644
  await this.markFailed(
@@ -7291,7 +7647,9 @@ var ChannelDriver = class _ChannelDriver {
7291
7647
  sessionId,
7292
7648
  error2,
7293
7649
  usage,
7294
- failure
7650
+ failure,
7651
+ usageAgentName,
7652
+ subagentInvocations
7295
7653
  );
7296
7654
  } catch (err) {
7297
7655
  if (err instanceof ChannelAuthError) throw err;
@@ -7334,9 +7692,11 @@ var ChannelDriver = class _ChannelDriver {
7334
7692
  this.removeInFlight(watcher, inFlight.evidentMessageId);
7335
7693
  return;
7336
7694
  }
7695
+ const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
7696
+ const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
7337
7697
  const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
7338
7698
  const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
7339
- if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
7699
+ if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
7340
7700
  inFlight.stuckReported = true;
7341
7701
  void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
7342
7702
  stuck_for_ms: this.now() - inFlight.dispatchedAt
@@ -7497,7 +7857,7 @@ var ChannelDriver = class _ChannelDriver {
7497
7857
  const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
7498
7858
  (sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
7499
7859
  );
7500
- const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
7860
+ const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
7501
7861
  if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
7502
7862
  this.log({
7503
7863
  level: "debug",
@@ -7532,6 +7892,12 @@ var ChannelDriver = class _ChannelDriver {
7532
7892
  });
7533
7893
  const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
7534
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
+ );
7535
7901
  try {
7536
7902
  await this.markDone(
7537
7903
  conv.id,
@@ -7539,7 +7905,9 @@ var ChannelDriver = class _ChannelDriver {
7539
7905
  sessionId,
7540
7906
  inFlight.opencodeMessageId,
7541
7907
  title,
7542
- usage
7908
+ usage,
7909
+ usageAgentName,
7910
+ subagentInvocations
7543
7911
  );
7544
7912
  } catch (err) {
7545
7913
  if (err instanceof ChannelAuthError) throw err;
@@ -7718,6 +8086,12 @@ var ChannelDriver = class _ChannelDriver {
7718
8086
  if (state === "failed" && !restartAborted) {
7719
8087
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
7720
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
+ );
7721
8095
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
7722
8096
  this.log({
7723
8097
  level: "error",
@@ -7726,7 +8100,16 @@ var ChannelDriver = class _ChannelDriver {
7726
8100
  message_id: row.id
7727
8101
  });
7728
8102
  try {
7729
- 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
+ );
7730
8113
  } catch (err) {
7731
8114
  if (err instanceof ChannelAuthError) throw err;
7732
8115
  if (err instanceof ChannelTerminalError) {
@@ -7888,7 +8271,22 @@ var ChannelDriver = class _ChannelDriver {
7888
8271
  try {
7889
8272
  const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
7890
8273
  const usage = messageUsage(messages, ocId ?? "");
7891
- 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
+ );
7892
8290
  } catch (err) {
7893
8291
  if (err instanceof ChannelAuthError) throw err;
7894
8292
  if (err instanceof ChannelTerminalError) {
@@ -8292,6 +8690,166 @@ var ChannelDriver = class _ChannelDriver {
8292
8690
  if (parent !== void 0) this.sessionParents.set(sessionId, parent);
8293
8691
  return parent;
8294
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
+ }
8295
8853
  /**
8296
8854
  * OpenCode's synchronous default session title (e.g.
8297
8855
  * `"New session - 1737800000000"`), assigned immediately when a session is
@@ -8775,7 +9333,7 @@ var ChannelDriver = class _ChannelDriver {
8775
9333
  * watcher retries next tick within the
8776
9334
  * deadline, Finding 4).
8777
9335
  */
8778
- async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
9336
+ async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
8779
9337
  const res = await this.fetchImpl(
8780
9338
  `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8781
9339
  {
@@ -8791,15 +9349,21 @@ var ChannelDriver = class _ChannelDriver {
8791
9349
  opencode_session_id: sessionId,
8792
9350
  ...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
8793
9351
  ...title ? { title } : {},
8794
- ...usage ? usage : {}
9352
+ ...usage ? usage : {},
9353
+ ...usageAgentName ? { usage_agent_name: usageAgentName } : {},
9354
+ ...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
8795
9355
  })
8796
9356
  }
8797
9357
  );
8798
9358
  this.assertAuth(res, "marking message as done");
8799
- if (res.ok) return;
9359
+ if (res.ok) {
9360
+ this.clearSubagentInvocationCaches(messageId);
9361
+ return;
9362
+ }
8800
9363
  if (isRetryableStatus(res.status)) {
8801
9364
  throw new Error(`marking message as done: HTTP ${res.status}`);
8802
9365
  }
9366
+ this.clearSubagentInvocationCaches(messageId);
8803
9367
  throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
8804
9368
  }
8805
9369
  /**
@@ -8814,7 +9378,7 @@ var ChannelDriver = class _ChannelDriver {
8814
9378
  * exists but is wedged, so the next attempt must get a fresh one
8815
9379
  * instead of reusing it — see WI-1's server-side null-clearing PATCH).
8816
9380
  */
8817
- async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
9381
+ async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
8818
9382
  const body = { status: "failed" };
8819
9383
  if (sessionId === null) {
8820
9384
  body.opencode_session_id = null;
@@ -8823,23 +9387,33 @@ var ChannelDriver = class _ChannelDriver {
8823
9387
  }
8824
9388
  if (error2 !== void 0) body.error = error2;
8825
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
+ }
8826
9394
  if (failure) {
8827
9395
  body.failure_kind = failure.kind;
8828
9396
  body.failure_provider_id = failure.providerId;
8829
9397
  body.failure_model_id = failure.modelId;
8830
9398
  body.failure_reason = failure.reason;
8831
9399
  }
8832
- await this.callWithRetry(
8833
- "marking message as failed",
8834
- () => this.fetchImpl(
8835
- `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8836
- {
8837
- method: "PATCH",
8838
- headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
8839
- body: JSON.stringify(body)
8840
- }
8841
- )
8842
- );
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);
8843
9417
  }
8844
9418
  /**
8845
9419
  * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
@@ -10635,14 +11209,11 @@ function scheduleClaudeUsageReporting(state, options) {
10635
11209
  report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
10636
11210
  isLocalCredentialProblem,
10637
11211
  forcedOnHint: "run `claude` to sign in",
10638
- firstDelayMs: () => FIRST_REPORT_DELAY_MS,
10639
- nextDelayMs: nextReportDelayMs,
10640
- failureLogLevel: claudeUsageFailureLogLevel
11212
+ firstDelayMs: firstReportDelayMs,
11213
+ nextDelayMs: usageReportDelayMs,
11214
+ failureLogLevel: usageReportFailureLogLevel
10641
11215
  });
10642
11216
  }
10643
- var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
10644
- var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
10645
- var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
10646
11217
  function scheduleResourceUsageReporting(state, options) {
10647
11218
  const { enabled, warnings } = resolveResourceUsageReportingEnabled(
10648
11219
  options.resourceUsageReporting,
@@ -10695,10 +11266,7 @@ function scheduleResourceUsageReporting(state, options) {
10695
11266
  consecutiveFailures++;
10696
11267
  logActivity(state, {
10697
11268
  type: "info",
10698
- level: reportFailureLogLevel(
10699
- consecutiveFailures,
10700
- RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
10701
- ),
11269
+ level: usageReportFailureLogLevel(consecutiveFailures),
10702
11270
  message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
10703
11271
  });
10704
11272
  }
@@ -10707,20 +11275,11 @@ function scheduleResourceUsageReporting(state, options) {
10707
11275
  const message = error2 instanceof Error ? error2.message : String(error2);
10708
11276
  logActivity(state, {
10709
11277
  type: "info",
10710
- level: reportFailureLogLevel(
10711
- consecutiveFailures,
10712
- RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
10713
- ),
11278
+ level: usageReportFailureLogLevel(consecutiveFailures),
10714
11279
  message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
10715
11280
  });
10716
11281
  } finally {
10717
- state.resourceUsageTimer = setTimeout(
10718
- () => void tick(),
10719
- jitteredDelayMs(
10720
- RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
10721
- RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
10722
- )
10723
- );
11282
+ state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
10724
11283
  }
10725
11284
  };
10726
11285
  state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
@@ -11267,6 +11826,7 @@ async function run(options) {
11267
11826
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
11268
11827
  }
11269
11828
  }
11829
+ await reloadProviderCache(state.port);
11270
11830
  const noProviderWarning = buildNoProviderWarning(
11271
11831
  await hasAnyConfiguredProvider(state.port)
11272
11832
  );