@evident-ai/cli 3.4.1-dev.0b03f1c → 3.4.1-dev.10920a8

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(() => "");
@@ -722,14 +735,6 @@ function toReportedWindow(window) {
722
735
  if (!window) return null;
723
736
  return { utilization: window.utilization, resets_at: window.resetsAt };
724
737
  }
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
738
  async function reportClaudeUsage(agentId, authHeader, snapshot) {
734
739
  try {
735
740
  const apiUrl = getApiUrlConfig();
@@ -739,7 +744,7 @@ async function reportClaudeUsage(agentId, authHeader, snapshot) {
739
744
  body: JSON.stringify({
740
745
  five_hour: toReportedWindow(snapshot.fiveHour),
741
746
  seven_day: toReportedWindow(snapshot.sevenDay),
742
- owner: toReportedOwner(snapshot)
747
+ subscription: toReportedSubscription(snapshot.subscription)
743
748
  }),
744
749
  signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
745
750
  });
@@ -763,13 +768,6 @@ function toReportedOpenAiWindow(window) {
763
768
  resets_at: window.resetsAt
764
769
  };
765
770
  }
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
771
  async function reportOpenAiUsage(agentId, authHeader, snapshot) {
774
772
  try {
775
773
  const apiUrl = getApiUrlConfig();
@@ -781,7 +779,7 @@ async function reportOpenAiUsage(agentId, authHeader, snapshot) {
781
779
  secondary: toReportedOpenAiWindow(snapshot.secondary),
782
780
  has_credits: snapshot.hasCredits,
783
781
  credits_unlimited: snapshot.creditsUnlimited,
784
- subscription: toReportedOpenAiSubscription(snapshot)
782
+ subscription: toReportedSubscription(snapshot.subscription)
785
783
  }),
786
784
  signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
787
785
  });
@@ -1097,7 +1095,7 @@ function ownerLookupFailure(error2) {
1097
1095
  }
1098
1096
  async function getClaudeUsageOwner(accessToken) {
1099
1097
  if (cachedOwner?.accessToken === accessToken) {
1100
- return { owner: cachedOwner.owner, ownerLookupError: null };
1098
+ return { subscription: cachedOwner.owner, ownerLookupError: null };
1101
1099
  }
1102
1100
  try {
1103
1101
  const response = await fetch(CLAUDE_PROFILE_URL, {
@@ -1109,27 +1107,27 @@ async function getClaudeUsageOwner(accessToken) {
1109
1107
  signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
1110
1108
  });
1111
1109
  if (!response.ok) {
1112
- return { owner: null, ownerLookupError: `HTTP ${response.status}` };
1110
+ return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
1113
1111
  }
1114
1112
  let body;
1115
1113
  try {
1116
1114
  body = await response.json();
1117
1115
  } catch (error2) {
1118
- return { owner: null, ownerLookupError: "malformed response" };
1116
+ return { subscription: null, ownerLookupError: "malformed response" };
1119
1117
  }
1120
1118
  const profile = body;
1121
1119
  if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
1122
- return { owner: null, ownerLookupError: "malformed response" };
1120
+ return { subscription: null, ownerLookupError: "malformed response" };
1123
1121
  }
1124
- const owner = {
1125
- email: profile.account.email,
1122
+ const subscription = {
1123
+ ownerEmail: profile.account.email,
1126
1124
  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
1125
+ planType: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
1128
1126
  };
1129
- cachedOwner = { accessToken, owner };
1130
- return { owner, ownerLookupError: null };
1127
+ cachedOwner = { accessToken, owner: subscription };
1128
+ return { subscription, ownerLookupError: null };
1131
1129
  } catch (error2) {
1132
- return { owner: null, ownerLookupError: ownerLookupFailure(error2) };
1130
+ return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
1133
1131
  }
1134
1132
  }
1135
1133
  async function getClaudeUsage() {
@@ -1158,11 +1156,11 @@ async function getClaudeUsage() {
1158
1156
  throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
1159
1157
  }
1160
1158
  const body = await res.json();
1161
- const { owner, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
1159
+ const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
1162
1160
  return {
1163
1161
  fiveHour: toWindow(body.five_hour),
1164
1162
  sevenDay: toWindow(body.seven_day),
1165
- owner,
1163
+ subscription,
1166
1164
  ownerLookupError
1167
1165
  };
1168
1166
  }
@@ -3263,21 +3261,74 @@ function collectSubagentSessions(messages, userMessageId) {
3263
3261
  }
3264
3262
  return refs;
3265
3263
  }
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
3264
+ function finiteNumber(value) {
3265
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
3266
+ }
3267
+ function taskCallModel(value) {
3268
+ if (!value || typeof value !== "object") return null;
3269
+ const model = value;
3270
+ const modelID = typeof model.modelID === "string" ? model.modelID : void 0;
3271
+ const providerID = typeof model.providerID === "string" ? model.providerID : void 0;
3272
+ return modelID || providerID ? { modelID, providerID } : null;
3273
+ }
3274
+ function collectTaskCalls(messages, userMessageId) {
3275
+ if (!messages || messages.length === 0) return [];
3276
+ const calls = [];
3277
+ for (const message of messages) {
3278
+ if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
3279
+ for (const part of message.parts ?? []) {
3280
+ if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
3281
+ continue;
3282
+ }
3283
+ const rawName = part.state.input?.subagent_type;
3284
+ const subagentName = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : rawName === void 0 ? "general" : "unknown";
3285
+ const metadata = part.state.metadata;
3286
+ calls.push({
3287
+ callID: part.callID,
3288
+ subagentName,
3289
+ childSessionId: typeof metadata?.sessionId === "string" ? metadata.sessionId : null,
3290
+ parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
3291
+ model: taskCallModel(metadata?.model),
3292
+ status: part.state.status ?? "unknown",
3293
+ timeStart: finiteNumber(part.state.time?.start),
3294
+ timeEnd: finiteNumber(part.state.time?.end)
3295
+ });
3296
+ }
3297
+ }
3298
+ return calls;
3299
+ }
3300
+ function attributeTaskCallUsage(messages, windows) {
3301
+ const eligibleWindows = windows.filter(
3302
+ (window) => window.timeStart !== null && Number.isFinite(window.timeStart)
3270
3303
  );
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] : [];
3304
+ const assignments = /* @__PURE__ */ new Map();
3305
+ for (const window of eligibleWindows) assignments.set(window.callID, []);
3306
+ const unattributed = [];
3307
+ for (const message of messages ?? []) {
3308
+ if (roleOf(message) !== "assistant") continue;
3309
+ const created = finiteNumber(createdOf(message));
3310
+ const matching = created === null ? [] : eligibleWindows.filter(
3311
+ (window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
3312
+ );
3313
+ if (matching.length === 0) {
3314
+ unattributed.push(message);
3315
+ continue;
3316
+ }
3317
+ matching.sort((a, b) => a.timeStart - b.timeStart);
3318
+ assignments.get(matching[0].callID)?.push(message);
3279
3319
  }
3280
- if (correlated.length === 0) return null;
3320
+ return {
3321
+ invocations: eligibleWindows.map((window) => {
3322
+ const assigned = assignments.get(window.callID) ?? [];
3323
+ return { callID: window.callID, messages: assigned, usage: sumAssistantUsage(assigned) };
3324
+ }),
3325
+ unattributed
3326
+ };
3327
+ }
3328
+ function sumAssistantUsage(messages) {
3329
+ if (!messages || messages.length === 0) return null;
3330
+ const nonErrored = messages.filter((message) => errorOf(message) == null);
3331
+ const selected = nonErrored.length > 0 ? nonErrored : messages;
3281
3332
  let sawAnyUsage = false;
3282
3333
  let inputSum = 0;
3283
3334
  let outputSum = 0;
@@ -3288,7 +3339,7 @@ function messageUsage(messages, userMessageId) {
3288
3339
  let sawCost = false;
3289
3340
  let modelId = null;
3290
3341
  let providerId = null;
3291
- for (const m of correlated) {
3342
+ for (const m of selected) {
3292
3343
  const info = m.info;
3293
3344
  if (!info) continue;
3294
3345
  const tokens = info.tokens;
@@ -3323,12 +3374,28 @@ function messageUsage(messages, userMessageId) {
3323
3374
  usage_tokens_reasoning: reasoningSum,
3324
3375
  usage_tokens_cache_read: cacheReadSum,
3325
3376
  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`.
3377
+ // NULL means OpenCode never reported a cost; it is distinct from a genuine
3378
+ // zero-cost message, which sets `sawCost` with `costSum === 0`.
3329
3379
  usage_cost_usd: sawCost ? costSum : null
3330
3380
  };
3331
3381
  }
3382
+ function messageUsage(messages, userMessageId) {
3383
+ if (!messages || messages.length === 0) return null;
3384
+ const byParentAll = messages.filter(
3385
+ (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
3386
+ );
3387
+ const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
3388
+ const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
3389
+ let correlated;
3390
+ if (byParent.length > 0) {
3391
+ correlated = byParent;
3392
+ } else {
3393
+ const reply = findAssistantReplyAfter(messages, userMessageId);
3394
+ correlated = reply ? [reply] : [];
3395
+ }
3396
+ if (correlated.length === 0) return null;
3397
+ return sumAssistantUsage(correlated);
3398
+ }
3332
3399
  function messageRunState(messages, userMessageId) {
3333
3400
  if (!messages || messages.length === 0) return "unknown";
3334
3401
  const hasUser = messages.some((m) => idOf(m) === userMessageId);
@@ -3458,6 +3525,28 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
3458
3525
  (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
3459
3526
  );
3460
3527
  }
3528
+ function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
3529
+ if (!messages || messages.length === 0) return false;
3530
+ const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
3531
+ if (userIndex === -1) return false;
3532
+ let hasLaterUser = false;
3533
+ let hasStartedLaterUser = false;
3534
+ for (let i = userIndex + 1; i < messages.length; i++) {
3535
+ const message = messages[i];
3536
+ if (roleOf(message) !== "user") continue;
3537
+ hasLaterUser = true;
3538
+ const laterUserMessageId = idOf(message);
3539
+ if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
3540
+ return false;
3541
+ }
3542
+ if (messages.some(
3543
+ (candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
3544
+ )) {
3545
+ hasStartedLaterUser = true;
3546
+ }
3547
+ }
3548
+ return hasLaterUser && hasStartedLaterUser;
3549
+ }
3461
3550
  async function hasAnyConfiguredProvider(port) {
3462
3551
  try {
3463
3552
  const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
@@ -3489,6 +3578,76 @@ async function hasAnyConfiguredProvider(port) {
3489
3578
  return null;
3490
3579
  }
3491
3580
  }
3581
+ function sessionErrorReason(error2) {
3582
+ const record = typeof error2 === "object" && error2 !== null ? error2 : null;
3583
+ const data = record?.data;
3584
+ const dataRecord = typeof data === "object" && data !== null ? data : null;
3585
+ 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";
3586
+ const reason = rawReason.replace(/\s+/g, " ").trim().slice(0, 500);
3587
+ return reason || "OpenCode reported a session error with no details";
3588
+ }
3589
+ function parseSessionErrorFrame(data) {
3590
+ let parsed;
3591
+ try {
3592
+ parsed = JSON.parse(data);
3593
+ } catch (error2) {
3594
+ void error2;
3595
+ return null;
3596
+ }
3597
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3598
+ const parsedRecord = parsed;
3599
+ const payload = parsedRecord.payload;
3600
+ const event = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : parsedRecord;
3601
+ if (event.type !== "session.error") return null;
3602
+ const properties = event.properties;
3603
+ if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
3604
+ return null;
3605
+ }
3606
+ const propertiesRecord = properties;
3607
+ const sessionId = propertiesRecord.sessionID;
3608
+ if (typeof sessionId !== "string" || sessionId.length === 0) return null;
3609
+ return {
3610
+ sessionId,
3611
+ reason: sessionErrorReason(propertiesRecord.error)
3612
+ };
3613
+ }
3614
+ async function readSessionErrorStream(port, options) {
3615
+ let reader = null;
3616
+ try {
3617
+ const response = await fetch(`${opencodeBase(port)}/event`, {
3618
+ headers: { accept: "text/event-stream" },
3619
+ signal: options.signal
3620
+ });
3621
+ if (!response.ok || !response.body) {
3622
+ return { reason: "unavailable", detail: `HTTP ${response.status}` };
3623
+ }
3624
+ reader = response.body.getReader();
3625
+ const decoder = new TextDecoder();
3626
+ let buffer = "";
3627
+ const processLine = (line) => {
3628
+ const trimmed = line.trimEnd();
3629
+ if (!trimmed.startsWith("data:")) return;
3630
+ const event = parseSessionErrorFrame(trimmed.slice("data:".length).replace(/^ /, ""));
3631
+ if (event) options.onSessionError(event);
3632
+ };
3633
+ while (true) {
3634
+ const { done, value } = await reader.read();
3635
+ if (done) return { reason: "ended" };
3636
+ buffer += decoder.decode(value, { stream: true });
3637
+ const lines = buffer.split("\n");
3638
+ buffer = lines.pop() ?? "";
3639
+ for (const line of lines) processLine(line);
3640
+ }
3641
+ } catch (err) {
3642
+ if (options.signal.aborted) return { reason: "aborted" };
3643
+ return {
3644
+ reason: "unavailable",
3645
+ detail: err instanceof Error ? err.message : String(err)
3646
+ };
3647
+ } finally {
3648
+ if (reader) void reader.cancel().catch(() => void 0);
3649
+ }
3650
+ }
3492
3651
  async function reloadProviderCache(port) {
3493
3652
  try {
3494
3653
  const res = await timedFetch(`${opencodeBase(port)}/config`, {
@@ -4384,7 +4543,7 @@ function parseChatGptIdentity(accessToken) {
4384
4543
  const auth = payload["https://api.openai.com/auth"];
4385
4544
  const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
4386
4545
  const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
4387
- return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
4546
+ return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
4388
4547
  }
4389
4548
  function toWindow2(headers, name) {
4390
4549
  const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
@@ -5228,6 +5387,10 @@ var DEFAULT_RETRY_POLICY = {
5228
5387
  baseDelayMs: 500,
5229
5388
  maxDelayMs: 3e4
5230
5389
  };
5390
+ var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
5391
+ var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
5392
+ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
5393
+ var MAX_BUFFERED_SESSION_ERRORS = 256;
5231
5394
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
5232
5395
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
5233
5396
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
@@ -5368,6 +5531,17 @@ var ChannelDriver = class _ChannelDriver {
5368
5531
  * message; it is removed once its in-flight set empties.
5369
5532
  */
5370
5533
  watchers = /* @__PURE__ */ new Map();
5534
+ sessionErrorStream = null;
5535
+ /**
5536
+ * Session-error failures currently being reported; entries are empty at rest
5537
+ * because each handoff deletes its id in `finally`.
5538
+ */
5539
+ sessionErrorHandled = /* @__PURE__ */ new Set();
5540
+ /**
5541
+ * Session errors that arrived before their dispatch was registered. Bounded FIFO
5542
+ * with a short TTL so an unmatched session cannot retain an event indefinitely.
5543
+ */
5544
+ bufferedSessionErrors = /* @__PURE__ */ new Map();
5371
5545
  /**
5372
5546
  * AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
5373
5547
  * dispatched and are still in-flight. A message in this set is never
@@ -5551,6 +5725,13 @@ var ChannelDriver = class _ChannelDriver {
5551
5725
  * no watcher) can resolve the title.
5552
5726
  */
5553
5727
  sessionTitles = /* @__PURE__ */ new Map();
5728
+ /** One best-effort terminal subagent collection per Evident message id. */
5729
+ subagentInvocationCollections = /* @__PURE__ */ new Map();
5730
+ /**
5731
+ * Early snapshots are only liveness hints; they must not become the terminal
5732
+ * collection when the task parts or child transcript have advanced.
5733
+ */
5734
+ subagentInvocationPrefetches = /* @__PURE__ */ new Map();
5554
5735
  /** Serialises drains so a reconnect during a drain doesn't double-process. */
5555
5736
  draining = false;
5556
5737
  /**
@@ -5765,6 +5946,21 @@ var ChannelDriver = class _ChannelDriver {
5765
5946
  }
5766
5947
  return ids;
5767
5948
  }
5949
+ /**
5950
+ * OpenCode user-message ids tracked for other Evident messages in a session.
5951
+ * Excluding this message makes an unattributed later row fail safe; a missing
5952
+ * watcher yields no attributions, per `hasLaterSiblingTurnStarted`'s docblock.
5953
+ */
5954
+ siblingOpencodeMessageIds(watcher, ownEvidentMessageId) {
5955
+ const ids = /* @__PURE__ */ new Set();
5956
+ if (!watcher) return ids;
5957
+ for (const inFlight of watcher.inFlight.values()) {
5958
+ if (inFlight.evidentMessageId !== ownEvidentMessageId) {
5959
+ ids.add(inFlight.opencodeMessageId);
5960
+ }
5961
+ }
5962
+ return ids;
5963
+ }
5768
5964
  /**
5769
5965
  * File-pull work, for `run.ts`'s idle accounting (#559).
5770
5966
  *
@@ -5831,6 +6027,8 @@ var ChannelDriver = class _ChannelDriver {
5831
6027
  */
5832
6028
  stop() {
5833
6029
  this.stopped = true;
6030
+ this.sessionErrorStream?.abort.abort();
6031
+ this.sessionErrorStream = null;
5834
6032
  }
5835
6033
  /**
5836
6034
  * The server clears this request when a new MicroVM identity is recorded, so a
@@ -5905,6 +6103,7 @@ var ChannelDriver = class _ChannelDriver {
5905
6103
  */
5906
6104
  async processConversation(conv) {
5907
6105
  const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
6106
+ this.ensureSessionErrorStream();
5908
6107
  const messages = await this.getPendingMessages(conv.id);
5909
6108
  let dispatched = 0;
5910
6109
  let skippedAlreadyDispatched = 0;
@@ -6251,6 +6450,23 @@ var ChannelDriver = class _ChannelDriver {
6251
6450
  if (state === "running" || state === "queued") {
6252
6451
  const ongoing = await isSessionOngoing(this.port, sessionId);
6253
6452
  if (ongoing === true) {
6453
+ if (state === "queued") {
6454
+ const siblingOcIds = this.siblingOpencodeMessageIds(
6455
+ this.watchers.get(sessionId),
6456
+ message.id
6457
+ );
6458
+ if (hasLaterSiblingTurnStarted(messages, ocId ?? "", siblingOcIds)) {
6459
+ this.log({
6460
+ level: "warn",
6461
+ 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`,
6462
+ conversation_id: conv.id,
6463
+ message_id: message.id
6464
+ });
6465
+ this.clearRedriveUnresolved(message.id);
6466
+ void this.postSignal(conv.id, message.id, "redrive_redispatched");
6467
+ return "dispatch";
6468
+ }
6469
+ }
6254
6470
  return this.reattachRedrive(conv, sessionId, message, ocId);
6255
6471
  }
6256
6472
  if (ongoing === false) {
@@ -6340,16 +6556,37 @@ var ChannelDriver = class _ChannelDriver {
6340
6556
  if (state === "done") {
6341
6557
  const title = await this.resolveSessionTitle(sessionId, conv.id);
6342
6558
  const usage = messageUsage(messages, ocId ?? "");
6559
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
6560
+ const subagentInvocations = await this.resolveSubagentInvocations(
6561
+ messages,
6562
+ ocId ?? "",
6563
+ message.id
6564
+ );
6343
6565
  this.log({
6344
6566
  level: "info",
6345
6567
  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
6568
  conversation_id: conv.id,
6347
6569
  message_id: message.id
6348
6570
  });
6349
- await this.markDone(conv.id, message.id, sessionId, ocId, title, usage);
6571
+ await this.markDone(
6572
+ conv.id,
6573
+ message.id,
6574
+ sessionId,
6575
+ ocId,
6576
+ title,
6577
+ usage,
6578
+ usageAgentName,
6579
+ subagentInvocations
6580
+ );
6350
6581
  } else {
6351
6582
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
6352
6583
  const usage = messageUsage(messages, ocId ?? "");
6584
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
6585
+ const subagentInvocations = await this.resolveSubagentInvocations(
6586
+ messages,
6587
+ ocId ?? "",
6588
+ message.id
6589
+ );
6353
6590
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
6354
6591
  this.log({
6355
6592
  level: "error",
@@ -6357,7 +6594,16 @@ var ChannelDriver = class _ChannelDriver {
6357
6594
  conversation_id: conv.id,
6358
6595
  message_id: message.id
6359
6596
  });
6360
- await this.markFailed(conv.id, message.id, sessionId, error2, usage, failure);
6597
+ await this.markFailed(
6598
+ conv.id,
6599
+ message.id,
6600
+ sessionId,
6601
+ error2,
6602
+ usage,
6603
+ failure,
6604
+ usageAgentName,
6605
+ subagentInvocations
6606
+ );
6361
6607
  }
6362
6608
  if (ocId !== null) {
6363
6609
  await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
@@ -6901,6 +7147,24 @@ var ChannelDriver = class _ChannelDriver {
6901
7147
  ambiguousPinnedSinceMs: 0,
6902
7148
  ambiguousResolved: false
6903
7149
  });
7150
+ const buffered = this.bufferedSessionErrors.get(sessionId);
7151
+ if (!buffered) return;
7152
+ this.bufferedSessionErrors.delete(sessionId);
7153
+ if (this.now() - buffered.receivedAt < SESSION_ERROR_BUFFER_TTL_MS) {
7154
+ this.handleSessionError(buffered.event);
7155
+ }
7156
+ }
7157
+ bufferSessionError(event) {
7158
+ this.bufferedSessionErrors.delete(event.sessionId);
7159
+ this.bufferedSessionErrors.set(event.sessionId, {
7160
+ event,
7161
+ receivedAt: this.now()
7162
+ });
7163
+ while (this.bufferedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
7164
+ const oldest = this.bufferedSessionErrors.keys().next().value;
7165
+ if (typeof oldest !== "string") break;
7166
+ this.bufferedSessionErrors.delete(oldest);
7167
+ }
6904
7168
  }
6905
7169
  /**
6906
7170
  * Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
@@ -7105,6 +7369,7 @@ var ChannelDriver = class _ChannelDriver {
7105
7369
  ensureWatcherRunning(sessionId) {
7106
7370
  const watcher = this.watchers.get(sessionId);
7107
7371
  if (!watcher) return;
7372
+ this.ensureSessionErrorStream();
7108
7373
  if (watcher.loop) return;
7109
7374
  if (watcher.inFlight.size === 0) {
7110
7375
  this.watchers.delete(sessionId);
@@ -7120,6 +7385,154 @@ var ChannelDriver = class _ChannelDriver {
7120
7385
  });
7121
7386
  watcher.loop = loop;
7122
7387
  }
7388
+ ensureSessionErrorStream() {
7389
+ if (this.sessionErrorStream || this.stopped) return;
7390
+ const abort = new AbortController();
7391
+ const loop = this.runSessionErrorStream(abort.signal);
7392
+ this.sessionErrorStream = { abort, loop };
7393
+ }
7394
+ async runSessionErrorStream(signal) {
7395
+ let attempt = 0;
7396
+ let warned = false;
7397
+ while (!this.stopped && !signal.aborted) {
7398
+ const openedAt = this.now();
7399
+ try {
7400
+ const outcome = await readSessionErrorStream(this.port, {
7401
+ signal,
7402
+ onSessionError: (event) => this.handleSessionError(event)
7403
+ });
7404
+ if (outcome.reason === "aborted" || signal.aborted) return;
7405
+ const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
7406
+ if (outcome.reason === "unavailable" || outcome.reason === "ended") {
7407
+ if (!healthy) {
7408
+ const detail = outcome.reason === "unavailable" ? outcome.detail : "stream ended";
7409
+ this.log({
7410
+ level: warned ? "debug" : "warn",
7411
+ message: `OpenCode session-error stream ${warned ? "still unavailable" : "unavailable"} (${detail}); transcript polling remains the evidence path`
7412
+ });
7413
+ warned = true;
7414
+ }
7415
+ }
7416
+ if (healthy) {
7417
+ if (warned) {
7418
+ this.log({
7419
+ level: "info",
7420
+ message: "OpenCode session-error stream reconnected; transcript polling remains the evidence path"
7421
+ });
7422
+ warned = false;
7423
+ }
7424
+ attempt = 0;
7425
+ } else {
7426
+ attempt += 1;
7427
+ }
7428
+ if (this.stopped || signal.aborted) return;
7429
+ await this.sleep(backoffDelay(healthy ? 0 : attempt - 1, this.retry));
7430
+ } catch (err) {
7431
+ if (this.stopped || signal.aborted) return;
7432
+ this.log({
7433
+ level: "error",
7434
+ message: `OpenCode session-error stream failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
7435
+ });
7436
+ const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
7437
+ const delayAttempt = healthy ? 0 : attempt;
7438
+ attempt = healthy ? 0 : attempt + 1;
7439
+ try {
7440
+ await this.sleep(backoffDelay(delayAttempt, this.retry));
7441
+ } catch (sleepErr) {
7442
+ this.log({
7443
+ level: "error",
7444
+ message: `OpenCode session-error stream backoff failed unexpectedly: ${sleepErr instanceof Error ? sleepErr.message : String(sleepErr)}`
7445
+ });
7446
+ }
7447
+ }
7448
+ }
7449
+ }
7450
+ handleSessionError(event) {
7451
+ try {
7452
+ const watcher = this.watchers.get(event.sessionId);
7453
+ if (!watcher) {
7454
+ this.bufferSessionError(event);
7455
+ this.log({
7456
+ level: "debug",
7457
+ message: `Ignoring session error for unknown session ${event.sessionId.slice(0, 8)}`
7458
+ });
7459
+ return;
7460
+ }
7461
+ if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
7462
+ this.log({
7463
+ level: "debug",
7464
+ message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
7465
+ conversation_id: watcher.conv.id
7466
+ });
7467
+ return;
7468
+ }
7469
+ const inFlight = [...watcher.inFlight.values()].filter((message) => !message.started && !message.done).sort((a, b) => a.dispatchedAt - b.dispatchedAt)[0];
7470
+ if (!inFlight) {
7471
+ this.bufferSessionError(event);
7472
+ this.log({
7473
+ level: "debug",
7474
+ message: `No queued in-flight turn to correlate with session error in ${event.sessionId.slice(0, 8)}`,
7475
+ conversation_id: watcher.conv.id
7476
+ });
7477
+ return;
7478
+ }
7479
+ if (this.sessionErrorHandled.has(inFlight.evidentMessageId)) return;
7480
+ this.sessionErrorHandled.add(inFlight.evidentMessageId);
7481
+ void this.failFromSessionError(watcher, event, inFlight);
7482
+ } catch (err) {
7483
+ this.log({
7484
+ level: "error",
7485
+ message: `Failed to handle OpenCode session error for ${event.sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`
7486
+ });
7487
+ }
7488
+ }
7489
+ async failFromSessionError(watcher, event, inFlight) {
7490
+ try {
7491
+ const messages = await getSessionMessages(this.port, event.sessionId);
7492
+ const state = messageRunState(messages, inFlight.opencodeMessageId);
7493
+ if (state !== "queued") {
7494
+ this.log({
7495
+ level: "debug",
7496
+ message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
7497
+ conversation_id: watcher.conv.id,
7498
+ message_id: inFlight.evidentMessageId
7499
+ });
7500
+ return;
7501
+ }
7502
+ this.log({
7503
+ level: "error",
7504
+ message: `OpenCode could not run message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)}: ${event.reason}`,
7505
+ conversation_id: watcher.conv.id,
7506
+ message_id: inFlight.evidentMessageId
7507
+ });
7508
+ await this.markFailed(
7509
+ watcher.conv.id,
7510
+ inFlight.evidentMessageId,
7511
+ event.sessionId,
7512
+ `OpenCode could not run this turn: ${event.reason}`
7513
+ );
7514
+ inFlight.done = true;
7515
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
7516
+ } catch (err) {
7517
+ if (err instanceof ChannelAuthError) {
7518
+ this.log({
7519
+ level: "warn",
7520
+ 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`,
7521
+ conversation_id: watcher.conv.id,
7522
+ message_id: inFlight.evidentMessageId
7523
+ });
7524
+ } else {
7525
+ this.log({
7526
+ level: "warn",
7527
+ 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`,
7528
+ conversation_id: watcher.conv.id,
7529
+ message_id: inFlight.evidentMessageId
7530
+ });
7531
+ }
7532
+ } finally {
7533
+ this.sessionErrorHandled.delete(inFlight.evidentMessageId);
7534
+ }
7535
+ }
7123
7536
  /**
7124
7537
  * The per-session polling loop (WI-3). Once per tick it:
7125
7538
  * 1. polls `GET /session/:id/message` once and, per in-flight message,
@@ -7232,6 +7645,21 @@ var ChannelDriver = class _ChannelDriver {
7232
7645
  const conv = watcher.conv;
7233
7646
  const state = messageRunState(messages, inFlight.opencodeMessageId);
7234
7647
  const id = inFlight.evidentMessageId;
7648
+ if (messages && collectTaskCalls(messages, inFlight.opencodeMessageId).length > 0 && !this.subagentInvocationPrefetches.has(id)) {
7649
+ void this.resolveSubagentInvocations(
7650
+ messages,
7651
+ inFlight.opencodeMessageId,
7652
+ id,
7653
+ "prefetch"
7654
+ ).catch((err) => {
7655
+ this.log({
7656
+ level: "warn",
7657
+ message: `Best-effort subagent usage prefetch failed for message ${id.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
7658
+ conversation_id: conv.id,
7659
+ message_id: id
7660
+ });
7661
+ });
7662
+ }
7235
7663
  if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
7236
7664
  else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
7237
7665
  if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
@@ -7285,6 +7713,12 @@ var ChannelDriver = class _ChannelDriver {
7285
7713
  message_id: inFlight.evidentMessageId
7286
7714
  });
7287
7715
  const usage = messageUsage(messages, inFlight.opencodeMessageId);
7716
+ const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
7717
+ const subagentInvocations = await this.resolveSubagentInvocations(
7718
+ messages,
7719
+ inFlight.opencodeMessageId,
7720
+ inFlight.evidentMessageId
7721
+ );
7288
7722
  const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
7289
7723
  try {
7290
7724
  await this.markFailed(
@@ -7293,7 +7727,9 @@ var ChannelDriver = class _ChannelDriver {
7293
7727
  sessionId,
7294
7728
  error2,
7295
7729
  usage,
7296
- failure
7730
+ failure,
7731
+ usageAgentName,
7732
+ subagentInvocations
7297
7733
  );
7298
7734
  } catch (err) {
7299
7735
  if (err instanceof ChannelAuthError) throw err;
@@ -7336,9 +7772,11 @@ var ChannelDriver = class _ChannelDriver {
7336
7772
  this.removeInFlight(watcher, inFlight.evidentMessageId);
7337
7773
  return;
7338
7774
  }
7775
+ const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
7776
+ const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
7339
7777
  const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
7340
7778
  const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
7341
- if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
7779
+ if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
7342
7780
  inFlight.stuckReported = true;
7343
7781
  void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
7344
7782
  stuck_for_ms: this.now() - inFlight.dispatchedAt
@@ -7499,7 +7937,7 @@ var ChannelDriver = class _ChannelDriver {
7499
7937
  const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
7500
7938
  (sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
7501
7939
  );
7502
- const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
7940
+ const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
7503
7941
  if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
7504
7942
  this.log({
7505
7943
  level: "debug",
@@ -7534,6 +7972,12 @@ var ChannelDriver = class _ChannelDriver {
7534
7972
  });
7535
7973
  const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
7536
7974
  const usage = messageUsage(messages, inFlight.opencodeMessageId);
7975
+ const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
7976
+ const subagentInvocations = await this.resolveSubagentInvocations(
7977
+ messages,
7978
+ inFlight.opencodeMessageId,
7979
+ inFlight.evidentMessageId
7980
+ );
7537
7981
  try {
7538
7982
  await this.markDone(
7539
7983
  conv.id,
@@ -7541,7 +7985,9 @@ var ChannelDriver = class _ChannelDriver {
7541
7985
  sessionId,
7542
7986
  inFlight.opencodeMessageId,
7543
7987
  title,
7544
- usage
7988
+ usage,
7989
+ usageAgentName,
7990
+ subagentInvocations
7545
7991
  );
7546
7992
  } catch (err) {
7547
7993
  if (err instanceof ChannelAuthError) throw err;
@@ -7720,6 +8166,12 @@ var ChannelDriver = class _ChannelDriver {
7720
8166
  if (state === "failed" && !restartAborted) {
7721
8167
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
7722
8168
  const usage = messageUsage(messages, ocId ?? "");
8169
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
8170
+ const subagentInvocations = await this.resolveSubagentInvocations(
8171
+ messages,
8172
+ ocId ?? "",
8173
+ row.id
8174
+ );
7723
8175
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
7724
8176
  this.log({
7725
8177
  level: "error",
@@ -7728,7 +8180,16 @@ var ChannelDriver = class _ChannelDriver {
7728
8180
  message_id: row.id
7729
8181
  });
7730
8182
  try {
7731
- await this.markFailed(row.conversation_id, row.id, sessionId, error2, usage, failure);
8183
+ await this.markFailed(
8184
+ row.conversation_id,
8185
+ row.id,
8186
+ sessionId,
8187
+ error2,
8188
+ usage,
8189
+ failure,
8190
+ usageAgentName,
8191
+ subagentInvocations
8192
+ );
7732
8193
  } catch (err) {
7733
8194
  if (err instanceof ChannelAuthError) throw err;
7734
8195
  if (err instanceof ChannelTerminalError) {
@@ -7890,7 +8351,22 @@ var ChannelDriver = class _ChannelDriver {
7890
8351
  try {
7891
8352
  const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
7892
8353
  const usage = messageUsage(messages, ocId ?? "");
7893
- await this.markDone(row.conversation_id, row.id, sessionId, ocId, title, usage);
8354
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
8355
+ const subagentInvocations = await this.resolveSubagentInvocations(
8356
+ messages,
8357
+ ocId ?? "",
8358
+ row.id
8359
+ );
8360
+ await this.markDone(
8361
+ row.conversation_id,
8362
+ row.id,
8363
+ sessionId,
8364
+ ocId,
8365
+ title,
8366
+ usage,
8367
+ usageAgentName,
8368
+ subagentInvocations
8369
+ );
7894
8370
  } catch (err) {
7895
8371
  if (err instanceof ChannelAuthError) throw err;
7896
8372
  if (err instanceof ChannelTerminalError) {
@@ -8294,6 +8770,166 @@ var ChannelDriver = class _ChannelDriver {
8294
8770
  if (parent !== void 0) this.sessionParents.set(sessionId, parent);
8295
8771
  return parent;
8296
8772
  }
8773
+ usageAgentName(messages, userMessageId) {
8774
+ const reply = findLastAssistantReplyFor(messages, userMessageId);
8775
+ const mode = reply?.info?.mode;
8776
+ if (typeof mode === "string" && mode.length > 0) return mode;
8777
+ const agent = reply?.info?.agent;
8778
+ return typeof agent === "string" && agent.length > 0 ? agent : null;
8779
+ }
8780
+ async resolveSubagentInvocations(messages, userMessageId, messageId, phase = "terminal") {
8781
+ if (!messages) return void 0;
8782
+ const cache = phase === "prefetch" ? this.subagentInvocationPrefetches : this.subagentInvocationCollections;
8783
+ const cached = cache.get(messageId);
8784
+ if (cached) return cached;
8785
+ const collection = this.buildSubagentInvocations(messages, userMessageId, messageId).catch(
8786
+ (err) => {
8787
+ this.log({
8788
+ level: "warn",
8789
+ message: `Best-effort subagent usage collection failed for message ${messageId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
8790
+ message_id: messageId
8791
+ });
8792
+ return void 0;
8793
+ }
8794
+ );
8795
+ cache.set(messageId, collection);
8796
+ const result = await collection;
8797
+ if (result === void 0 && cache.get(messageId) === collection) cache.delete(messageId);
8798
+ return result;
8799
+ }
8800
+ clearSubagentInvocationCaches(messageId) {
8801
+ this.subagentInvocationCollections.delete(messageId);
8802
+ this.subagentInvocationPrefetches.delete(messageId);
8803
+ }
8804
+ async buildSubagentInvocations(messages, userMessageId, messageId) {
8805
+ const rootCalls = collectTaskCalls(messages, userMessageId);
8806
+ if (rootCalls.length === 0) return void 0;
8807
+ const childMessages = /* @__PURE__ */ new Map();
8808
+ const seenCallIds = new Set(rootCalls.map((call) => call.callID));
8809
+ const work = rootCalls.map((call) => ({
8810
+ call,
8811
+ depth: 1
8812
+ }));
8813
+ const payload = [];
8814
+ const fetchChildMessages = (sessionId) => {
8815
+ const cached = childMessages.get(sessionId);
8816
+ if (cached) return cached;
8817
+ const pending = (async () => {
8818
+ try {
8819
+ const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
8820
+ if (!res.ok) {
8821
+ this.log({
8822
+ level: "warn",
8823
+ 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`,
8824
+ message_id: messageId
8825
+ });
8826
+ return null;
8827
+ }
8828
+ const body = await res.json();
8829
+ if (!Array.isArray(body)) throw new Error("response body was not a message array");
8830
+ return body;
8831
+ } catch (err) {
8832
+ this.log({
8833
+ level: "warn",
8834
+ 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)}`,
8835
+ message_id: messageId
8836
+ });
8837
+ return null;
8838
+ }
8839
+ })();
8840
+ childMessages.set(sessionId, pending);
8841
+ return pending;
8842
+ };
8843
+ const fetchChildWithoutBlocking = async (sessionId) => {
8844
+ const pending = fetchChildMessages(sessionId);
8845
+ let timer;
8846
+ const timeout = new Promise((resolve4) => {
8847
+ timer = setTimeout(() => {
8848
+ this.log({
8849
+ level: "warn",
8850
+ 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`,
8851
+ message_id: messageId
8852
+ });
8853
+ resolve4(null);
8854
+ }, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
8855
+ });
8856
+ try {
8857
+ return await Promise.race([pending, timeout]);
8858
+ } finally {
8859
+ if (timer !== void 0) clearTimeout(timer);
8860
+ }
8861
+ };
8862
+ while (work.length > 0) {
8863
+ const groups = /* @__PURE__ */ new Map();
8864
+ for (const item of work.splice(0)) {
8865
+ const group = groups.get(item.call.childSessionId) ?? [];
8866
+ group.push(item);
8867
+ groups.set(item.call.childSessionId, group);
8868
+ }
8869
+ const groupResults = await Promise.all(
8870
+ [...groups].map(async ([sessionId, items]) => ({
8871
+ sessionId,
8872
+ items,
8873
+ messages: sessionId === null ? [] : await fetchChildWithoutBlocking(sessionId)
8874
+ }))
8875
+ );
8876
+ for (const { sessionId, items, messages: child } of groupResults) {
8877
+ if (sessionId !== null && child === null) continue;
8878
+ const attribution = sessionId === null ? { invocations: [], unattributed: [] } : attributeTaskCallUsage(
8879
+ child,
8880
+ items.map(({ call }) => ({
8881
+ callID: call.callID,
8882
+ timeStart: call.timeStart,
8883
+ timeEnd: call.timeEnd
8884
+ }))
8885
+ );
8886
+ if (sessionId !== null && attribution.unattributed.length > 0) {
8887
+ this.log({
8888
+ level: "warn",
8889
+ 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`,
8890
+ message_id: messageId
8891
+ });
8892
+ }
8893
+ const usageByCall = new Map(
8894
+ attribution.invocations.map((invocation) => [invocation.callID, invocation.usage])
8895
+ );
8896
+ const messagesByCall = new Map(
8897
+ attribution.invocations.map((invocation) => [invocation.callID, invocation.messages])
8898
+ );
8899
+ for (const { call, depth } of items) {
8900
+ const usage = usageByCall.get(call.callID) ?? null;
8901
+ payload.push({
8902
+ tool_call_id: call.callID,
8903
+ agent_name: call.subagentName,
8904
+ opencode_session_id: call.childSessionId,
8905
+ parent_opencode_session_id: call.parentSessionId,
8906
+ depth,
8907
+ status: call.status,
8908
+ started_at: call.timeStart === null ? null : new Date(call.timeStart).toISOString(),
8909
+ ended_at: call.timeEnd === null ? null : new Date(call.timeEnd).toISOString(),
8910
+ usage_provider_id: usage?.usage_provider_id ?? call.model?.providerID ?? null,
8911
+ usage_model_id: usage?.usage_model_id ?? call.model?.modelID ?? null,
8912
+ usage_tokens_input: usage?.usage_tokens_input ?? null,
8913
+ usage_tokens_output: usage?.usage_tokens_output ?? null,
8914
+ usage_tokens_reasoning: usage?.usage_tokens_reasoning ?? null,
8915
+ usage_tokens_cache_read: usage?.usage_tokens_cache_read ?? null,
8916
+ usage_tokens_cache_write: usage?.usage_tokens_cache_write ?? null,
8917
+ usage_cost_usd: usage?.usage_cost_usd ?? null
8918
+ });
8919
+ for (const assigned of messagesByCall.get(call.callID) ?? []) {
8920
+ const parentId = assigned.info?.parentID ?? assigned.parentID;
8921
+ if (!parentId) continue;
8922
+ for (const nested of collectTaskCalls([assigned], parentId)) {
8923
+ if (seenCallIds.has(nested.callID)) continue;
8924
+ seenCallIds.add(nested.callID);
8925
+ work.push({ call: nested, depth: depth + 1 });
8926
+ }
8927
+ }
8928
+ }
8929
+ }
8930
+ }
8931
+ return payload.length > 0 ? payload : void 0;
8932
+ }
8297
8933
  /**
8298
8934
  * OpenCode's synchronous default session title (e.g.
8299
8935
  * `"New session - 1737800000000"`), assigned immediately when a session is
@@ -8777,7 +9413,7 @@ var ChannelDriver = class _ChannelDriver {
8777
9413
  * watcher retries next tick within the
8778
9414
  * deadline, Finding 4).
8779
9415
  */
8780
- async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
9416
+ async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
8781
9417
  const res = await this.fetchImpl(
8782
9418
  `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8783
9419
  {
@@ -8793,15 +9429,21 @@ var ChannelDriver = class _ChannelDriver {
8793
9429
  opencode_session_id: sessionId,
8794
9430
  ...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
8795
9431
  ...title ? { title } : {},
8796
- ...usage ? usage : {}
9432
+ ...usage ? usage : {},
9433
+ ...usageAgentName ? { usage_agent_name: usageAgentName } : {},
9434
+ ...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
8797
9435
  })
8798
9436
  }
8799
9437
  );
8800
9438
  this.assertAuth(res, "marking message as done");
8801
- if (res.ok) return;
9439
+ if (res.ok) {
9440
+ this.clearSubagentInvocationCaches(messageId);
9441
+ return;
9442
+ }
8802
9443
  if (isRetryableStatus(res.status)) {
8803
9444
  throw new Error(`marking message as done: HTTP ${res.status}`);
8804
9445
  }
9446
+ this.clearSubagentInvocationCaches(messageId);
8805
9447
  throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
8806
9448
  }
8807
9449
  /**
@@ -8816,7 +9458,7 @@ var ChannelDriver = class _ChannelDriver {
8816
9458
  * exists but is wedged, so the next attempt must get a fresh one
8817
9459
  * instead of reusing it — see WI-1's server-side null-clearing PATCH).
8818
9460
  */
8819
- async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
9461
+ async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
8820
9462
  const body = { status: "failed" };
8821
9463
  if (sessionId === null) {
8822
9464
  body.opencode_session_id = null;
@@ -8825,23 +9467,33 @@ var ChannelDriver = class _ChannelDriver {
8825
9467
  }
8826
9468
  if (error2 !== void 0) body.error = error2;
8827
9469
  if (usage) Object.assign(body, usage);
9470
+ if (usageAgentName) body.usage_agent_name = usageAgentName;
9471
+ if (subagentInvocations && subagentInvocations.length > 0) {
9472
+ body.subagent_invocations = subagentInvocations;
9473
+ }
8828
9474
  if (failure) {
8829
9475
  body.failure_kind = failure.kind;
8830
9476
  body.failure_provider_id = failure.providerId;
8831
9477
  body.failure_model_id = failure.modelId;
8832
9478
  body.failure_reason = failure.reason;
8833
9479
  }
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
- );
9480
+ try {
9481
+ await this.callWithRetry(
9482
+ "marking message as failed",
9483
+ () => this.fetchImpl(
9484
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
9485
+ {
9486
+ method: "PATCH",
9487
+ headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
9488
+ body: JSON.stringify(body)
9489
+ }
9490
+ )
9491
+ );
9492
+ } catch (err) {
9493
+ if (err instanceof ChannelTerminalError) this.clearSubagentInvocationCaches(messageId);
9494
+ throw err;
9495
+ }
9496
+ this.clearSubagentInvocationCaches(messageId);
8845
9497
  }
8846
9498
  /**
8847
9499
  * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
@@ -11269,6 +11921,7 @@ async function run(options) {
11269
11921
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
11270
11922
  }
11271
11923
  }
11924
+ await reloadProviderCache(state.port);
11272
11925
  const noProviderWarning = buildNoProviderWarning(
11273
11926
  await hasAnyConfiguredProvider(state.port)
11274
11927
  );