@evident-ai/cli 3.5.2-dev.ffa823b → 3.6.1-dev.8d3e144

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
@@ -1293,7 +1293,11 @@ var FORWARD_FAILURE_REASON_HEADER = "X-Evident-Failure-Reason";
1293
1293
  function log(level, event, fields) {
1294
1294
  const method = level === "debug" ? "log" : level;
1295
1295
  try {
1296
- console[method]("[evident]", JSON.stringify({ level, event, ...fields }));
1296
+ const payload = Object.fromEntries(
1297
+ Object.entries({ level, event, ...fields }).filter(([, value]) => value !== void 0)
1298
+ );
1299
+ const secondArgument = globalThis.navigator?.userAgent === "Cloudflare-Workers" ? payload : JSON.stringify(payload);
1300
+ console[method]("[evident]", secondArgument);
1297
1301
  } catch (err) {
1298
1302
  console.error(
1299
1303
  "[evident] log_serialize_failed",
@@ -1322,7 +1326,7 @@ import ora4 from "ora";
1322
1326
  import { select as select4 } from "@inquirer/prompts";
1323
1327
 
1324
1328
  // src/lib/telemetry.ts
1325
- var CLI_VERSION = (true ? "3.5.2-dev.ffa823b" : void 0) ?? process.env.npm_package_version ?? "unknown";
1329
+ var CLI_VERSION = (true ? "3.6.1-dev.8d3e144" : void 0) ?? process.env.npm_package_version ?? "unknown";
1326
1330
  function getCliVersion() {
1327
1331
  return CLI_VERSION;
1328
1332
  }
@@ -3518,19 +3522,32 @@ function isAbortedTerminalReply(messages, userMessageId) {
3518
3522
  }
3519
3523
  return false;
3520
3524
  }
3525
+ var CLAUDE_AUTH_EXPIRY_MESSAGES = Object.freeze([
3526
+ "Claude Code credentials are unavailable or expired. Run `claude` to refresh them."
3527
+ ]);
3528
+ function extractReplyErrorShape(reply) {
3529
+ const error2 = errorOf(reply);
3530
+ if (error2 == null || typeof error2 !== "object") {
3531
+ return { name: null, statusCode: null };
3532
+ }
3533
+ const e = error2;
3534
+ const name = typeof e.name === "string" ? e.name.slice(0, 100) : null;
3535
+ const data = e.data;
3536
+ const statusCode = typeof data?.statusCode === "number" && Number.isFinite(data.statusCode) ? data.statusCode : null;
3537
+ return { name, statusCode };
3538
+ }
3521
3539
  function classifyReplyAuthError(reply) {
3522
3540
  const error2 = errorOf(reply);
3523
3541
  if (error2 == null || typeof error2 !== "object") return null;
3524
3542
  const e = error2;
3525
3543
  const replyProviderId = reply?.info?.providerID ?? null;
3526
3544
  const replyModelId = reply?.info?.modelID ?? null;
3545
+ const data = e.data;
3527
3546
  if (e.name === "ProviderAuthError") {
3528
- const data = e.data;
3529
3547
  const providerId = typeof data?.providerID === "string" && data.providerID || replyProviderId;
3530
3548
  return { kind: "model_auth", providerId, modelId: replyModelId, reason: "missing" };
3531
3549
  }
3532
3550
  if (e.name === "APIError") {
3533
- const data = e.data;
3534
3551
  const statusCode = data?.statusCode;
3535
3552
  if (statusCode === 401 || statusCode === 403) {
3536
3553
  return {
@@ -3541,11 +3558,23 @@ function classifyReplyAuthError(reply) {
3541
3558
  };
3542
3559
  }
3543
3560
  }
3561
+ const errorMessage3 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
3562
+ if (errorMessage3 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage3.includes(knownMessage))) {
3563
+ return {
3564
+ kind: "model_auth",
3565
+ providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
3566
+ modelId: replyModelId,
3567
+ reason: "rejected"
3568
+ };
3569
+ }
3544
3570
  return null;
3545
3571
  }
3546
3572
  function messageFailure(messages, userMessageId) {
3547
3573
  return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
3548
3574
  }
3575
+ function isProviderModelNotFoundError(reason) {
3576
+ return reason.includes("Model not found:");
3577
+ }
3549
3578
  function findLatestSubagentAuthOutcome(messages, sinceMs) {
3550
3579
  if (!messages || messages.length === 0) return null;
3551
3580
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -3754,23 +3783,49 @@ async function readSessionErrorStream(port, options, client) {
3754
3783
  if (reader) void reader.cancel().catch(() => void 0);
3755
3784
  }
3756
3785
  }
3757
- async function reloadProviderCache(port, client) {
3758
- if (client?.version === "v2") return;
3786
+ async function disposeAndReloadProviderConfig(port, client) {
3787
+ if (client?.version === "v2") return { outcome: "skipped_v2" };
3759
3788
  try {
3760
- const res = await requestWithClient(port, client, "/config", {
3761
- method: "PATCH",
3762
- headers: { "Content-Type": "application/json" },
3763
- body: JSON.stringify({})
3764
- });
3789
+ const res = await requestWithClient(
3790
+ port,
3791
+ client,
3792
+ "/config",
3793
+ {
3794
+ method: "PATCH",
3795
+ headers: { "Content-Type": "application/json" },
3796
+ body: JSON.stringify({})
3797
+ },
3798
+ { allowStatuses: ALL_HTTP_STATUSES }
3799
+ );
3765
3800
  if (!res.ok) {
3766
3801
  console.error(
3767
- `[reloadProviderCache] PATCH /config returned HTTP ${res.status} (port ${port})`
3802
+ `[disposeAndReloadProviderConfig] PATCH /config returned HTTP ${res.status} (port ${port})`
3768
3803
  );
3804
+ return { outcome: "failed", status: res.status, detail: null };
3769
3805
  }
3806
+ return { outcome: "reloaded" };
3770
3807
  } catch (err) {
3771
3808
  console.error(
3772
- `[reloadProviderCache] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3809
+ `[disposeAndReloadProviderConfig] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3773
3810
  );
3811
+ return {
3812
+ outcome: "failed",
3813
+ status: null,
3814
+ detail: err instanceof Error ? err.message : String(err)
3815
+ };
3816
+ }
3817
+ }
3818
+ async function probeOpenCodeRequestReadiness(port, client) {
3819
+ try {
3820
+ const res = await requestWithClient(port, client, "/config", void 0, {
3821
+ allowStatuses: ALL_HTTP_STATUSES
3822
+ });
3823
+ const body = await res.text();
3824
+ return { ok: res.ok, status: res.status, bodyEmpty: body.trim().length === 0, detail: null };
3825
+ } catch (err) {
3826
+ const detail = err instanceof Error ? err.message : String(err);
3827
+ console.error(`[probeOpenCodeRequestReadiness] GET /config failed (port ${port}): ${detail}`);
3828
+ return { ok: false, status: null, bodyEmpty: null, detail };
3774
3829
  }
3775
3830
  }
3776
3831
 
@@ -7537,7 +7592,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
7537
7592
  var MAX_BUFFERED_SESSION_ERRORS = 256;
7538
7593
  var SESSION_ERROR_RETENTION_TTL_MS = 6e4;
7539
7594
  var SESSION_STORE_UNWRITABLE_ERROR = "The runner session database could not be written, so this turn did not finish. Nothing was lost from the queue. Clear disk space and retry.";
7540
- var SESSION_ERROR_RETAINED_DETAIL = "The runner's session database could not be written, so this turn stopped part-way. Runner-observed session error: ";
7595
+ var SESSION_ERROR_RETAINED_DETAIL = "Runner-observed OpenCode session error: ";
7541
7596
  var SESSION_ERROR_OBSERVED_CAUSE = "Runner-observed OpenCode session-store cause: ";
7542
7597
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
7543
7598
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
@@ -7661,12 +7716,15 @@ var ChannelDriver = class _ChannelDriver {
7661
7716
  /**
7662
7717
  * Per-conversation throttle state for the #183 recurrence warning (#1618
7663
7718
  * WI-4) — see `reportWedgedConversation`'s doc comment for why this exists.
7664
- * `firstWedgedAt` anchors `stuck_for_ms`; `lastWarnedAt` throttles both the
7665
- * log line and the `dispatch_wedged` signal to at most once per
7666
- * `wedgeWarningIntervalMs`; `consecutiveTicks` is reported in the log text
7667
- * so the operator sees magnitude, not repetition. Cleared the moment the
7668
- * conversation dispatches anything (a fresh wedge, if it recurs, is a new
7669
- * incident). Bounded FIFO, mirroring `supersededSessions`
7719
+ * `firstWedgedAt` anchors `stuck_for_ms`; `warnAnchorAt` is the throttle
7720
+ * anchor from first detection through the first warning, then from each
7721
+ * warning onward; `consecutiveTicks` is reported in the log text so the
7722
+ * operator sees magnitude, not repetition. Cleared the moment the
7723
+ * conversation dispatches anything, or when it drops off the pending list
7724
+ * (`runDrain`'s prune, the dominant real-world path — a wedge normally ends
7725
+ * with its message completing, not a fresh dispatch on the same
7726
+ * conversation) — either way a fresh wedge, if it recurs, is a new
7727
+ * incident. Bounded FIFO, mirroring `supersededSessions`
7670
7728
  * (`MAX_WEDGED_CONVERSATIONS`).
7671
7729
  */
7672
7730
  wedgeWarnings = /* @__PURE__ */ new Map();
@@ -7896,6 +7954,7 @@ var ChannelDriver = class _ChannelDriver {
7896
7954
  subagentInvocationPrefetches = /* @__PURE__ */ new Map();
7897
7955
  /** Serialises drains so a reconnect during a drain doesn't double-process. */
7898
7956
  draining = false;
7957
+ pendingProviderRegistryRepair = null;
7899
7958
  /**
7900
7959
  * Serialises runner-file syncs (#559) so the ~2s poll tick and a concurrent
7901
7960
  * drain ping don't download, write and ack the same file twice.
@@ -8081,8 +8140,33 @@ var ChannelDriver = class _ChannelDriver {
8081
8140
  async runDrain() {
8082
8141
  let dispatched = 0;
8083
8142
  try {
8143
+ const pendingRepair = this.pendingProviderRegistryRepair;
8144
+ if (pendingRepair && this.protectedSessionIds().size === 0) {
8145
+ try {
8146
+ await this.issueProviderRegistryReload(
8147
+ pendingRepair.conversationId,
8148
+ pendingRepair.messageId
8149
+ );
8150
+ } catch (err) {
8151
+ this.log({
8152
+ level: "error",
8153
+ message: `Provider registry repair adoption failed for message ${pendingRepair.messageId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`,
8154
+ conversation_id: pendingRepair.conversationId,
8155
+ message_id: pendingRepair.messageId
8156
+ });
8157
+ }
8158
+ return 0;
8159
+ }
8084
8160
  this.pendingMessageIds.clear();
8085
8161
  const conversations = await this.getPendingConversations();
8162
+ if (this.wedgeWarnings.size > 0) {
8163
+ const pendingConversationIds = new Set(conversations.map(({ id }) => id));
8164
+ for (const conversationId of this.wedgeWarnings.keys()) {
8165
+ if (!pendingConversationIds.has(conversationId)) {
8166
+ this.wedgeWarnings.delete(conversationId);
8167
+ }
8168
+ }
8169
+ }
8086
8170
  if (this.recycleRequestedFlag) {
8087
8171
  this.stop();
8088
8172
  }
@@ -8619,14 +8703,13 @@ var ChannelDriver = class _ChannelDriver {
8619
8703
  * skip-because-already-`dispatched`, the exact signature of a message stuck
8620
8704
  * acknowledged-but-never-worked.
8621
8705
  *
8622
- * Unthrottled, this fired every ~6s drain tick for as long as a wedge lasted
8623
- * (52,843 occurrences observed in one incident) — burning the GLOBAL
8624
- * 30-events/60s `runner-activity-telemetry.ts` budget that was itself
8625
- * suppressing the diagnostics needed to debug the wedge. The `warn` log (and
8626
- * the `dispatch_wedged` signal once the wedge has persisted past the same
8627
- * interval) fire at most once per `wedgeWarningIntervalMs` per conversation,
8628
- * naming the consecutive-tick count so the operator sees magnitude rather
8629
- * than repetition.
8706
+ * The first detection starts the clock and emits a local `debug` entry. The
8707
+ * `warn` log and paired `dispatch_wedged` signal fire once the wedge has
8708
+ * lasted `wedgeWarningIntervalMs`, then at most once per interval, naming the
8709
+ * consecutive-tick count so the operator sees magnitude rather than repetition.
8710
+ * A watcher stall that the loop-liveness watchdog recovers is expected and is
8711
+ * already reported by its `watcher_recovered` signal, so it does not reach an
8712
+ * operator through this warning.
8630
8713
  *
8631
8714
  * Deliberately does NOT trigger a release: WI-1's `reconcileWatchers` runs
8632
8715
  * unconditionally on this same tick and is already recovering anything it
@@ -8638,36 +8721,41 @@ var ChannelDriver = class _ChannelDriver {
8638
8721
  const existing = this.wedgeWarnings.get(conv.id);
8639
8722
  const firstWedgedAt = existing?.firstWedgedAt ?? now;
8640
8723
  const consecutiveTicks = (existing?.consecutiveTicks ?? 0) + 1;
8641
- const dueForWarn = !existing || now - existing.lastWarnedAt >= this.wedgeWarningIntervalMs;
8642
- if (!dueForWarn) {
8643
- this.wedgeWarnings.delete(conv.id);
8644
- this.wedgeWarnings.set(conv.id, {
8645
- firstWedgedAt,
8646
- lastWarnedAt: existing.lastWarnedAt,
8647
- consecutiveTicks
8724
+ const warnAnchorAt = existing?.warnAnchorAt ?? now;
8725
+ const dueForWarn = now - warnAnchorAt >= this.wedgeWarningIntervalMs;
8726
+ if (!existing) {
8727
+ this.log({
8728
+ level: "debug",
8729
+ message: `Detected a possible wedge for conversation ${conv.id.slice(0, 8)} with ${messages.length} pending message(s); it will be reported only if it outlasts the watchdog's recovery.`,
8730
+ conversation_id: conv.id
8731
+ });
8732
+ }
8733
+ let untracked = 0;
8734
+ if (dueForWarn) {
8735
+ const stuckForMs = now - firstWedgedAt;
8736
+ untracked = this.countUntrackedIds(messages);
8737
+ this.log({
8738
+ level: "warn",
8739
+ message: `Conversation ${conv.id.slice(0, 8)} has ${messages.length} pending message(s) but ALL are already marked dispatched locally (in-flight set: ${this.dispatched.size}) \u2014 none sent to OpenCode for ${consecutiveTicks} consecutive tick(s) now (${Math.round(stuckForMs / 1e3)}s stuck). ` + (untracked > 0 ? `${untracked} of these id(s) are tracked by NO watcher \u2014 the dispatched/in-flight pairing invariant is violated for this conversation, which will NOT self-heal and needs a runner restart.` : `A watcher is tracking this work; the loop-liveness watchdog is already recovering it.`),
8740
+ conversation_id: conv.id
8648
8741
  });
8649
- return;
8650
8742
  }
8651
- const stuckForMs = now - firstWedgedAt;
8652
- const untracked = this.countUntrackedIds(messages);
8653
- this.log({
8654
- level: "warn",
8655
- message: `Conversation ${conv.id.slice(0, 8)} has ${messages.length} pending message(s) but ALL are already marked dispatched locally (in-flight set: ${this.dispatched.size}) \u2014 none sent to OpenCode for ${consecutiveTicks} consecutive tick(s) now (${stuckForMs}ms stuck). ` + (untracked > 0 ? `${untracked} of these id(s) are tracked by NO watcher \u2014 the dispatched/in-flight pairing invariant is violated for this conversation, which will NOT self-heal and needs a runner restart.` : `A watcher is tracking this work; the loop-liveness watchdog is already recovering it.`),
8656
- conversation_id: conv.id
8657
- });
8658
8743
  this.wedgeWarnings.delete(conv.id);
8659
- this.wedgeWarnings.set(conv.id, { firstWedgedAt, lastWarnedAt: now, consecutiveTicks });
8744
+ this.wedgeWarnings.set(conv.id, {
8745
+ firstWedgedAt,
8746
+ warnAnchorAt: dueForWarn ? now : warnAnchorAt,
8747
+ consecutiveTicks
8748
+ });
8660
8749
  while (this.wedgeWarnings.size > MAX_WEDGED_CONVERSATIONS) {
8661
8750
  const oldest = this.wedgeWarnings.keys().next().value;
8662
8751
  if (oldest === void 0) break;
8663
8752
  this.wedgeWarnings.delete(oldest);
8664
8753
  }
8665
- if (stuckForMs >= this.wedgeWarningIntervalMs) {
8666
- void this.postSignal(conv.id, messages[0].id, "dispatch_wedged", {
8667
- stuck_for_ms: stuckForMs,
8668
- untracked
8669
- });
8670
- }
8754
+ if (!dueForWarn) return;
8755
+ void this.postSignal(conv.id, messages[0].id, "dispatch_wedged", {
8756
+ stuck_for_ms: now - firstWedgedAt,
8757
+ untracked
8758
+ });
8671
8759
  }
8672
8760
  /**
8673
8761
  * How many of `messages`' ids are tracked by NO watcher's `inFlight` (#1618
@@ -9933,14 +10021,40 @@ var ChannelDriver = class _ChannelDriver {
9933
10021
  conversation_id: watcher.conv.id,
9934
10022
  message_id: inFlight.evidentMessageId
9935
10023
  });
9936
- await this.markFailed(
9937
- watcher.conv.id,
9938
- inFlight.evidentMessageId,
9939
- event.sessionId,
9940
- `OpenCode could not run this turn: ${event.reason}`
9941
- );
10024
+ const isMissingProvider = isProviderModelNotFoundError(event.reason);
10025
+ if (isMissingProvider) {
10026
+ const dispatchedModel = inFlight.message.opencode_model;
10027
+ const separator = dispatchedModel?.indexOf("/") ?? -1;
10028
+ const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
10029
+ await this.markFailed(
10030
+ watcher.conv.id,
10031
+ inFlight.evidentMessageId,
10032
+ event.sessionId,
10033
+ `OpenCode could not run this turn: ${event.reason}`,
10034
+ void 0,
10035
+ {
10036
+ classification: {
10037
+ kind: "model_auth",
10038
+ providerId: hasProviderAndModel ? dispatchedModel.slice(0, separator) : null,
10039
+ modelId: hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null,
10040
+ reason: "missing"
10041
+ },
10042
+ errorShape: { name: null, statusCode: null }
10043
+ }
10044
+ );
10045
+ } else {
10046
+ await this.markFailed(
10047
+ watcher.conv.id,
10048
+ inFlight.evidentMessageId,
10049
+ event.sessionId,
10050
+ `OpenCode could not run this turn: ${event.reason}`
10051
+ );
10052
+ }
9942
10053
  inFlight.done = true;
9943
10054
  this.removeInFlight(watcher, inFlight.evidentMessageId);
10055
+ if (isMissingProvider) {
10056
+ await this.repairProviderRegistry(watcher.conv.id, inFlight.evidentMessageId);
10057
+ }
9944
10058
  } catch (err) {
9945
10059
  if (err instanceof ChannelAuthError) {
9946
10060
  this.log({
@@ -9961,6 +10075,44 @@ var ChannelDriver = class _ChannelDriver {
9961
10075
  this.sessionErrorHandled.delete(inFlight.evidentMessageId);
9962
10076
  }
9963
10077
  }
10078
+ async repairProviderRegistry(conversationId, messageId) {
10079
+ if (this.isV2) {
10080
+ void this.postSignal(conversationId, messageId, "provider_registry_reload_unsupported");
10081
+ return;
10082
+ }
10083
+ const activeSessions = this.protectedSessionIds().size;
10084
+ if (activeSessions > 0 || this.draining) {
10085
+ this.pendingProviderRegistryRepair = { conversationId, messageId };
10086
+ void this.postSignal(conversationId, messageId, "provider_registry_repair_deferred", {
10087
+ active_sessions: activeSessions
10088
+ });
10089
+ this.log({
10090
+ level: "warn",
10091
+ message: `Provider registry repair deferred until a lifecycle-safe point; ${activeSessions} active session(s) protected`,
10092
+ conversation_id: conversationId,
10093
+ message_id: messageId
10094
+ });
10095
+ return;
10096
+ }
10097
+ await this.issueProviderRegistryReload(conversationId, messageId);
10098
+ }
10099
+ async issueProviderRegistryReload(conversationId, messageId) {
10100
+ this.pendingProviderRegistryRepair = null;
10101
+ const result = await disposeAndReloadProviderConfig(this.port, this.openCodeClient);
10102
+ if (result.outcome === "failed") {
10103
+ const failure = result.status === null ? `transport failure${result.detail ? `: ${result.detail}` : ""}` : `HTTP ${result.status}`;
10104
+ this.log({
10105
+ level: "warn",
10106
+ message: `Provider registry reload failed (${failure})`,
10107
+ conversation_id: conversationId,
10108
+ message_id: messageId
10109
+ });
10110
+ }
10111
+ const repairOutcome = result.outcome === "reloaded" ? "reloaded" : "reload_failed";
10112
+ void this.postSignal(conversationId, messageId, "provider_registry_reload_issued", {
10113
+ repair_outcome: repairOutcome
10114
+ });
10115
+ }
9964
10116
  /**
9965
10117
  * The per-session polling loop (WI-3). Once per tick it:
9966
10118
  * 1. polls `GET /session/:id/message` once and, per in-flight message,
@@ -10152,7 +10304,7 @@ var ChannelDriver = class _ChannelDriver {
10152
10304
  if (!inFlight.done) {
10153
10305
  const messageErrorText = messageError(messages, inFlight.opencodeMessageId);
10154
10306
  const retainedReason = this.readRetainedSessionError(sessionId);
10155
- const retainedError = retainedReason === null ? null : `${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}`;
10307
+ const retainedError = retainedReason === null || messageErrorText !== null && messageErrorText.includes(retainedReason) ? null : `${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}`;
10156
10308
  const observedCause = this.sessionStoreHealth?.readCause(sessionId);
10157
10309
  const causeError = observedCause === null || observedCause === void 0 ? null : `${SESSION_ERROR_OBSERVED_CAUSE}${observedCause}`;
10158
10310
  const storeError = this.sessionStoreHealth?.isUnwritable() ? SESSION_STORE_UNWRITABLE_ERROR : null;
@@ -10648,7 +10800,8 @@ var ChannelDriver = class _ChannelDriver {
10648
10800
  usage,
10649
10801
  void 0,
10650
10802
  usageAgentName,
10651
- subagentInvocations
10803
+ subagentInvocations,
10804
+ true
10652
10805
  );
10653
10806
  } catch (err) {
10654
10807
  if (err instanceof ChannelAuthError) throw err;
@@ -12494,7 +12647,7 @@ var ChannelDriver = class _ChannelDriver {
12494
12647
  * exists but is wedged, so the next attempt must get a fresh one
12495
12648
  * instead of reusing it — see WI-1's server-side null-clearing PATCH).
12496
12649
  */
12497
- async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
12650
+ async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations, abandonedWithoutAnswer) {
12498
12651
  const body = { status: "failed" };
12499
12652
  if (sessionId === null) {
12500
12653
  body.opencode_session_id = null;
@@ -12507,12 +12660,19 @@ var ChannelDriver = class _ChannelDriver {
12507
12660
  if (subagentInvocations && subagentInvocations.length > 0) {
12508
12661
  body.subagent_invocations = subagentInvocations;
12509
12662
  }
12510
- if (failure) {
12511
- body.failure_kind = failure.kind;
12512
- body.failure_provider_id = failure.providerId;
12513
- body.failure_model_id = failure.modelId;
12514
- body.failure_reason = failure.reason;
12663
+ if (failure && failure.errorShape.name !== null) {
12664
+ body.failure_error_name = failure.errorShape.name;
12665
+ }
12666
+ if (failure && failure.errorShape.statusCode !== null) {
12667
+ body.failure_status_code = failure.errorShape.statusCode;
12515
12668
  }
12669
+ if (failure?.classification) {
12670
+ body.failure_kind = failure.classification.kind;
12671
+ body.failure_provider_id = failure.classification.providerId;
12672
+ body.failure_model_id = failure.classification.modelId;
12673
+ body.failure_reason = failure.classification.reason;
12674
+ }
12675
+ if (abandonedWithoutAnswer) body.abandoned_without_answer = true;
12516
12676
  try {
12517
12677
  await this.callWithRetry(
12518
12678
  "marking message as failed",
@@ -12532,27 +12692,28 @@ var ChannelDriver = class _ChannelDriver {
12532
12692
  this.clearSubagentInvocationCaches(messageId);
12533
12693
  }
12534
12694
  /**
12535
- * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
12695
+ * Classify an errored turn and extract its bounded error shape for `markFailed`.
12536
12696
  *
12537
12697
  * `messageFailure` alone (structured OpenCode error → `model_auth`) covers
12538
12698
  * most cases; when it returns `null` on this ALREADY-FAILED turn, fall back
12539
12699
  * to the P1-2b zero-provider check — one extra loopback call to
12540
12700
  * `hasAnyConfiguredProvider`, only reached when the structured classifier
12541
12701
  * couldn't place it. Fails open (never throws): a fallback probe failure
12542
- * (`null`/indeterminate) leaves the classification `null`, which produces
12543
- * today's byte-identical PATCH body via `markFailed`'s `if (failure)` guard.
12702
+ * (`null`/indeterminate) leaves the classification `null`; the raw error shape
12703
+ * is still retained for diagnostics.
12544
12704
  */
12545
12705
  async classifyModelAuthFailure(messages, userMessageId) {
12546
- const classified = messageFailure(messages, userMessageId);
12547
- if (classified != null) return classified;
12548
12706
  const reply = findLastAssistantReplyFor(messages, userMessageId);
12549
- const hasProvider = await this.hasAnyConfiguredProvider();
12550
- return applyZeroProviderFallback(
12707
+ const errorShape = extractReplyErrorShape(reply);
12708
+ const classified = messageFailure(messages, userMessageId);
12709
+ if (classified != null) return { classification: classified, errorShape };
12710
+ const classification = applyZeroProviderFallback(
12551
12711
  classified,
12552
- hasProvider,
12712
+ await this.hasAnyConfiguredProvider(),
12553
12713
  reply?.info?.providerID ?? null,
12554
12714
  reply?.info?.modelID ?? null
12555
12715
  );
12716
+ return { classification, errorShape };
12556
12717
  }
12557
12718
  async recordSubagentModelAuthFailure(failure, conversationId, messageId) {
12558
12719
  const providerId = failure.providerId ?? "(unknown)";
@@ -13649,6 +13810,8 @@ function createCredentialSync({
13649
13810
  var MAX_ACTIVITY_LOG_ENTRIES = 10;
13650
13811
  var MAX_DISCONNECT_REASON_LENGTH = 200;
13651
13812
  var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
13813
+ var CREDENTIAL_ADOPTION_PROBE_ATTEMPTS = 3;
13814
+ var CREDENTIAL_ADOPTION_PROBE_INTERVAL_MS = 1e3;
13652
13815
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
13653
13816
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
13654
13817
  var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
@@ -13996,6 +14159,125 @@ async function handleAuthError(state, error2) {
13996
14159
  return { success: false };
13997
14160
  }
13998
14161
  }
14162
+ function logCredentialAdoptionActivity(state, activity, level) {
14163
+ const message = (() => {
14164
+ switch (activity.outcome) {
14165
+ case "deferred":
14166
+ return `CREDENTIAL-ADOPTION: deferred generation ${activity.generation} with ${activity.active_sessions} active session(s)`;
14167
+ case "reloaded_ready":
14168
+ return `CREDENTIAL-ADOPTION: adopted generation ${activity.generation} with ${activity.active_sessions} active session(s); readiness probe succeeded after ${activity.probe_attempts} attempt(s)`;
14169
+ case "reloaded_interrupted": {
14170
+ const response = activity.probe_status === null ? "without an HTTP response" : `with HTTP ${activity.probe_status} and ${activity.probe_body_empty ? "an empty" : "a non-empty"} body`;
14171
+ return `CREDENTIAL-ADOPTION: generation ${activity.generation} with ${activity.active_sessions} active session(s); replacement instance answered ${response} after ${activity.probe_attempts} probe attempt(s)`;
14172
+ }
14173
+ case "reload_failed":
14174
+ return `CREDENTIAL-ADOPTION: failed to adopt generation ${activity.generation} with ${activity.active_sessions} active session(s)`;
14175
+ case "skipped_v2":
14176
+ return `CREDENTIAL-ADOPTION: skipped generation ${activity.generation} with ${activity.active_sessions} active session(s) on OpenCode V2`;
14177
+ }
14178
+ })();
14179
+ logActivity(state, {
14180
+ type: level === "error" ? "error" : "info",
14181
+ level,
14182
+ ...level === "error" ? { error: message } : { message },
14183
+ metadata: {
14184
+ ...activity,
14185
+ opencode_version: state.opencodeVersion,
14186
+ port: state.port
14187
+ }
14188
+ });
14189
+ }
14190
+ async function adoptCredentialGeneration(state, generation, adoption, activeSessions, deferredForMs) {
14191
+ const reload = await disposeAndReloadProviderConfig(
14192
+ state.port,
14193
+ state.opencodeClient ?? void 0
14194
+ );
14195
+ if (reload.outcome === "failed") {
14196
+ logCredentialAdoptionActivity(
14197
+ state,
14198
+ {
14199
+ outcome: "reload_failed",
14200
+ generation,
14201
+ active_sessions: activeSessions,
14202
+ adoption,
14203
+ deferred_for_ms: deferredForMs,
14204
+ reload_status: reload.status,
14205
+ reload_detail: reload.detail,
14206
+ probe_attempts: null,
14207
+ probe_status: null,
14208
+ probe_body_empty: null,
14209
+ probe_detail: null
14210
+ },
14211
+ "error"
14212
+ );
14213
+ return;
14214
+ }
14215
+ if (reload.outcome === "skipped_v2") {
14216
+ logCredentialAdoptionActivity(
14217
+ state,
14218
+ {
14219
+ outcome: "skipped_v2",
14220
+ generation,
14221
+ active_sessions: activeSessions,
14222
+ adoption,
14223
+ deferred_for_ms: deferredForMs,
14224
+ reload_status: null,
14225
+ reload_detail: null,
14226
+ probe_attempts: null,
14227
+ probe_status: null,
14228
+ probe_body_empty: null,
14229
+ probe_detail: null
14230
+ },
14231
+ "debug"
14232
+ );
14233
+ return;
14234
+ }
14235
+ let lastProbe = null;
14236
+ let attempts = 0;
14237
+ for (attempts = 1; attempts <= CREDENTIAL_ADOPTION_PROBE_ATTEMPTS; attempts++) {
14238
+ lastProbe = await probeOpenCodeRequestReadiness(state.port, state.opencodeClient ?? void 0);
14239
+ if (lastProbe.ok) {
14240
+ logCredentialAdoptionActivity(
14241
+ state,
14242
+ {
14243
+ outcome: "reloaded_ready",
14244
+ generation,
14245
+ active_sessions: activeSessions,
14246
+ adoption,
14247
+ deferred_for_ms: deferredForMs,
14248
+ reload_status: null,
14249
+ reload_detail: null,
14250
+ probe_attempts: attempts,
14251
+ probe_status: lastProbe.status,
14252
+ probe_body_empty: lastProbe.bodyEmpty,
14253
+ probe_detail: lastProbe.detail
14254
+ },
14255
+ "warn"
14256
+ );
14257
+ return;
14258
+ }
14259
+ if (attempts < CREDENTIAL_ADOPTION_PROBE_ATTEMPTS) {
14260
+ await new Promise((resolve5) => setTimeout(resolve5, CREDENTIAL_ADOPTION_PROBE_INTERVAL_MS));
14261
+ }
14262
+ }
14263
+ logCredentialAdoptionActivity(
14264
+ state,
14265
+ {
14266
+ outcome: "reloaded_interrupted",
14267
+ generation,
14268
+ active_sessions: activeSessions,
14269
+ adoption,
14270
+ deferred_for_ms: deferredForMs,
14271
+ reload_status: null,
14272
+ reload_detail: null,
14273
+ probe_attempts: CREDENTIAL_ADOPTION_PROBE_ATTEMPTS,
14274
+ probe_status: lastProbe?.status ?? null,
14275
+ probe_body_empty: lastProbe?.bodyEmpty ?? null,
14276
+ probe_detail: lastProbe?.detail ?? null
14277
+ },
14278
+ "error"
14279
+ );
14280
+ }
13999
14281
  async function driveChannels(state, driver) {
14000
14282
  let idlePolls = 0;
14001
14283
  let idleMs = 0;
@@ -14005,6 +14287,15 @@ async function driveChannels(state, driver) {
14005
14287
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
14006
14288
  let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
14007
14289
  let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
14290
+ const credentialAdoption = {
14291
+ generation: 0,
14292
+ pendingGeneration: null,
14293
+ pendingSinceMs: null,
14294
+ deferredGeneration: null,
14295
+ adoption: 0
14296
+ };
14297
+ const stayAwakeHolds = () => state.stayAwakeUntilMs !== null && Date.now() < state.stayAwakeUntilMs;
14298
+ let stayAwakeSuppressionLogged = false;
14008
14299
  while (state.running) {
14009
14300
  const cycleStartedAtMs = performance.now();
14010
14301
  let idleThisCycle = false;
@@ -14046,12 +14337,30 @@ async function driveChannels(state, driver) {
14046
14337
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
14047
14338
  if (opencodeAuthApplied) state.openaiUsageRearm?.();
14048
14339
  if (claudeCredentialApplied || opencodeAuthApplied) {
14049
- void reloadProviderCache(state.port, state.opencodeClient ?? void 0).catch(
14050
- (error2) => logActivity(state, {
14051
- type: "error",
14052
- error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
14053
- })
14054
- );
14340
+ credentialAdoption.generation++;
14341
+ if (state.opencodeApiVersion === "v2") {
14342
+ logCredentialAdoptionActivity(
14343
+ state,
14344
+ {
14345
+ outcome: "skipped_v2",
14346
+ generation: credentialAdoption.generation,
14347
+ active_sessions: driver.protectedSessionIds().size,
14348
+ adoption: null,
14349
+ deferred_for_ms: null,
14350
+ reload_status: null,
14351
+ reload_detail: null,
14352
+ probe_attempts: null,
14353
+ probe_status: null,
14354
+ probe_body_empty: null,
14355
+ probe_detail: null
14356
+ },
14357
+ "debug"
14358
+ );
14359
+ } else {
14360
+ credentialAdoption.pendingGeneration = credentialAdoption.generation;
14361
+ credentialAdoption.pendingSinceMs = Date.now();
14362
+ credentialAdoption.deferredGeneration = null;
14363
+ }
14055
14364
  }
14056
14365
  if (opencodeAuthApplied && state.opencodeApiVersion === "v2") {
14057
14366
  void importOpenCodeV2CredentialsForState(state).catch(
@@ -14106,6 +14415,49 @@ async function driveChannels(state, driver) {
14106
14415
  }
14107
14416
  }
14108
14417
  }
14418
+ if (credentialAdoption.pendingGeneration !== null) {
14419
+ const activeSessions = driver.protectedSessionIds();
14420
+ const generation = credentialAdoption.pendingGeneration;
14421
+ if (activeSessions.size > 0) {
14422
+ if (credentialAdoption.deferredGeneration !== generation) {
14423
+ credentialAdoption.deferredGeneration = generation;
14424
+ logCredentialAdoptionActivity(
14425
+ state,
14426
+ {
14427
+ outcome: "deferred",
14428
+ generation,
14429
+ active_sessions: activeSessions.size,
14430
+ adoption: null,
14431
+ deferred_for_ms: null,
14432
+ reload_status: null,
14433
+ reload_detail: null,
14434
+ probe_attempts: null,
14435
+ probe_status: null,
14436
+ probe_body_empty: null,
14437
+ probe_detail: null
14438
+ },
14439
+ "warn"
14440
+ );
14441
+ }
14442
+ } else {
14443
+ const deferredForMs = credentialAdoption.deferredGeneration === generation && credentialAdoption.pendingSinceMs !== null ? Math.max(0, Date.now() - credentialAdoption.pendingSinceMs) : null;
14444
+ credentialAdoption.pendingGeneration = null;
14445
+ credentialAdoption.pendingSinceMs = null;
14446
+ credentialAdoption.adoption++;
14447
+ void adoptCredentialGeneration(
14448
+ state,
14449
+ generation,
14450
+ credentialAdoption.adoption,
14451
+ activeSessions.size,
14452
+ deferredForMs
14453
+ ).catch(
14454
+ (error2) => logActivity(state, {
14455
+ type: "error",
14456
+ error: `CREDENTIAL-ADOPTION: adoption failed for generation ${generation} with ${activeSessions.size} active session(s) on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
14457
+ })
14458
+ );
14459
+ }
14460
+ }
14109
14461
  await new Promise((resolve5) => setTimeout(resolve5, CHANNEL_POLL_INTERVAL_MS));
14110
14462
  const cycleMs = performance.now() - cycleStartedAtMs;
14111
14463
  if (idleThisCycle) idleMs += cycleMs;
@@ -14119,11 +14471,20 @@ async function driveChannels(state, driver) {
14119
14471
  if (state.interactive) displayStatus(state);
14120
14472
  break;
14121
14473
  }
14122
- if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3) {
14474
+ if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3 && !stayAwakeHolds()) {
14123
14475
  logActivity(state, { type: "info", message: "Idle timeout reached" });
14124
14476
  if (state.interactive) displayStatus(state);
14125
14477
  break;
14126
14478
  }
14479
+ if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3 && stayAwakeHolds()) {
14480
+ if (!stayAwakeSuppressionLogged) {
14481
+ stayAwakeSuppressionLogged = true;
14482
+ logActivity(state, {
14483
+ type: "info",
14484
+ message: "Idle, but staying awake until the requested minimum passes"
14485
+ });
14486
+ }
14487
+ }
14127
14488
  }
14128
14489
  }
14129
14490
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
@@ -14452,10 +14813,12 @@ function scheduleUsageReporting(state, params) {
14452
14813
  let phase = "dormant";
14453
14814
  let rearmRequested = false;
14454
14815
  const armProbe = () => {
14816
+ if (!state.running) return;
14455
14817
  phase = "probe-pending";
14456
14818
  params.setTimer(setTimeout(() => void tick(true), params.firstDelayMs()));
14457
14819
  };
14458
14820
  const scheduleNextTick = () => {
14821
+ if (!state.running) return;
14459
14822
  if (rearmRequested) {
14460
14823
  rearmRequested = false;
14461
14824
  armProbe();
@@ -14489,6 +14852,7 @@ function scheduleUsageReporting(state, params) {
14489
14852
  phase = "tick-in-flight";
14490
14853
  try {
14491
14854
  const usage = await params.fetchUsage();
14855
+ if (!state.running) return;
14492
14856
  const result = await params.report(usage);
14493
14857
  if (result.ok) {
14494
14858
  if (consecutiveFailures > 0) {
@@ -14627,6 +14991,7 @@ function scheduleResourceUsageReporting(state, options) {
14627
14991
  message: lowDiskDecision.warning
14628
14992
  });
14629
14993
  }
14994
+ if (!state.running) return;
14630
14995
  const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
14631
14996
  if (result.ok) {
14632
14997
  if (consecutiveFailures > 0) {
@@ -14659,7 +15024,9 @@ function scheduleResourceUsageReporting(state, options) {
14659
15024
  message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
14660
15025
  });
14661
15026
  } finally {
14662
- state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
15027
+ if (state.running) {
15028
+ state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
15029
+ }
14663
15030
  }
14664
15031
  };
14665
15032
  state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
@@ -14862,6 +15229,8 @@ async function run(options) {
14862
15229
  port: options.port ?? 4096,
14863
15230
  conversationFilter: options.conversation ?? null,
14864
15231
  idleTimeout: options.idleTimeout ?? null,
15232
+ stayAwakeSeconds: null,
15233
+ stayAwakeUntilMs: null,
14865
15234
  json: options.json ?? false,
14866
15235
  interactive,
14867
15236
  logLevel,
@@ -15051,6 +15420,17 @@ async function run(options) {
15051
15420
  }
15052
15421
  spinner?.succeed(`Runner: ${validation.agent.name || state.agentId}`);
15053
15422
  state.agentName = validation.agent.name;
15423
+ const requestedStayAwakeSeconds = validation.agent?.wake_stay_awake_seconds;
15424
+ if (typeof requestedStayAwakeSeconds === "number" && Number.isFinite(requestedStayAwakeSeconds) && requestedStayAwakeSeconds > 0) {
15425
+ state.stayAwakeSeconds = requestedStayAwakeSeconds;
15426
+ } else if (requestedStayAwakeSeconds !== void 0 && requestedStayAwakeSeconds !== null) {
15427
+ state.stayAwakeSeconds = null;
15428
+ log2(
15429
+ state,
15430
+ `Ignoring invalid wake_stay_awake_seconds (type: ${typeof requestedStayAwakeSeconds})`,
15431
+ "warn"
15432
+ );
15433
+ }
15054
15434
  const microvmId = process.env.MICROVM_ID?.trim();
15055
15435
  if (microvmId) {
15056
15436
  const reported = await reportMicrovmId(state.agentId, state.authHeader, microvmId);
@@ -15294,7 +15674,7 @@ async function run(options) {
15294
15674
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
15295
15675
  }
15296
15676
  }
15297
- await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
15677
+ await disposeAndReloadProviderConfig(state.port, state.opencodeClient ?? void 0);
15298
15678
  if (state.opencodeApiVersion === "v2" && state.opencodeClient !== null) {
15299
15679
  await importOpenCodeV2CredentialsForState(state);
15300
15680
  }
@@ -15465,6 +15845,13 @@ async function run(options) {
15465
15845
  type: "info",
15466
15846
  message: connectionMessage
15467
15847
  });
15848
+ if (!isReconnect && state.stayAwakeSeconds !== null && state.stayAwakeUntilMs === null) {
15849
+ state.stayAwakeUntilMs = Date.now() + state.stayAwakeSeconds * 1e3;
15850
+ logActivity(state, {
15851
+ type: "info",
15852
+ message: `Staying awake for at least ${state.stayAwakeSeconds}s as requested by the wake`
15853
+ });
15854
+ }
15468
15855
  if (options.tunnelReadyFile) {
15469
15856
  const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
15470
15857
  if (marker.ok) {
@@ -15599,6 +15986,7 @@ async function run(options) {
15599
15986
  if (error2.message === "Unauthorized") tunnelSpinner?.fail("Unauthorized");
15600
15987
  throw error2;
15601
15988
  }
15989
+ if (!state.running) return;
15602
15990
  scheduleSessionCleanup(state, channelDriver, options);
15603
15991
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
15604
15992
  state.openaiUsageRearm = scheduleUsageReporting(state, {