@evident-ai/cli 3.5.2-dev.f76a2fb → 3.5.2-dev.fdeb718

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.f76a2fb" : void 0) ?? process.env.npm_package_version ?? "unknown";
1329
+ var CLI_VERSION = (true ? "3.5.2-dev.fdeb718" : void 0) ?? process.env.npm_package_version ?? "unknown";
1326
1330
  function getCliVersion() {
1327
1331
  return CLI_VERSION;
1328
1332
  }
@@ -3568,6 +3572,9 @@ function classifyReplyAuthError(reply) {
3568
3572
  function messageFailure(messages, userMessageId) {
3569
3573
  return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
3570
3574
  }
3575
+ function isProviderModelNotFoundError(reason) {
3576
+ return reason.includes("Model not found:");
3577
+ }
3571
3578
  function findLatestSubagentAuthOutcome(messages, sinceMs) {
3572
3579
  if (!messages || messages.length === 0) return null;
3573
3580
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -3776,23 +3783,49 @@ async function readSessionErrorStream(port, options, client) {
3776
3783
  if (reader) void reader.cancel().catch(() => void 0);
3777
3784
  }
3778
3785
  }
3779
- async function reloadProviderCache(port, client) {
3780
- if (client?.version === "v2") return;
3786
+ async function disposeAndReloadProviderConfig(port, client) {
3787
+ if (client?.version === "v2") return { outcome: "skipped_v2" };
3781
3788
  try {
3782
- const res = await requestWithClient(port, client, "/config", {
3783
- method: "PATCH",
3784
- headers: { "Content-Type": "application/json" },
3785
- body: JSON.stringify({})
3786
- });
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
+ );
3787
3800
  if (!res.ok) {
3788
3801
  console.error(
3789
- `[reloadProviderCache] PATCH /config returned HTTP ${res.status} (port ${port})`
3802
+ `[disposeAndReloadProviderConfig] PATCH /config returned HTTP ${res.status} (port ${port})`
3790
3803
  );
3804
+ return { outcome: "failed", status: res.status, detail: null };
3791
3805
  }
3806
+ return { outcome: "reloaded" };
3792
3807
  } catch (err) {
3793
3808
  console.error(
3794
- `[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)}`
3795
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 };
3796
3829
  }
3797
3830
  }
3798
3831
 
@@ -7683,12 +7716,15 @@ var ChannelDriver = class _ChannelDriver {
7683
7716
  /**
7684
7717
  * Per-conversation throttle state for the #183 recurrence warning (#1618
7685
7718
  * WI-4) — see `reportWedgedConversation`'s doc comment for why this exists.
7686
- * `firstWedgedAt` anchors `stuck_for_ms`; `lastWarnedAt` throttles both the
7687
- * log line and the `dispatch_wedged` signal to at most once per
7688
- * `wedgeWarningIntervalMs`; `consecutiveTicks` is reported in the log text
7689
- * so the operator sees magnitude, not repetition. Cleared the moment the
7690
- * conversation dispatches anything (a fresh wedge, if it recurs, is a new
7691
- * 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`
7692
7728
  * (`MAX_WEDGED_CONVERSATIONS`).
7693
7729
  */
7694
7730
  wedgeWarnings = /* @__PURE__ */ new Map();
@@ -7918,6 +7954,7 @@ var ChannelDriver = class _ChannelDriver {
7918
7954
  subagentInvocationPrefetches = /* @__PURE__ */ new Map();
7919
7955
  /** Serialises drains so a reconnect during a drain doesn't double-process. */
7920
7956
  draining = false;
7957
+ pendingProviderRegistryRepair = null;
7921
7958
  /**
7922
7959
  * Serialises runner-file syncs (#559) so the ~2s poll tick and a concurrent
7923
7960
  * drain ping don't download, write and ack the same file twice.
@@ -8103,8 +8140,33 @@ var ChannelDriver = class _ChannelDriver {
8103
8140
  async runDrain() {
8104
8141
  let dispatched = 0;
8105
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
+ }
8106
8160
  this.pendingMessageIds.clear();
8107
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
+ }
8108
8170
  if (this.recycleRequestedFlag) {
8109
8171
  this.stop();
8110
8172
  }
@@ -8641,14 +8703,13 @@ var ChannelDriver = class _ChannelDriver {
8641
8703
  * skip-because-already-`dispatched`, the exact signature of a message stuck
8642
8704
  * acknowledged-but-never-worked.
8643
8705
  *
8644
- * Unthrottled, this fired every ~6s drain tick for as long as a wedge lasted
8645
- * (52,843 occurrences observed in one incident) — burning the GLOBAL
8646
- * 30-events/60s `runner-activity-telemetry.ts` budget that was itself
8647
- * suppressing the diagnostics needed to debug the wedge. The `warn` log (and
8648
- * the `dispatch_wedged` signal once the wedge has persisted past the same
8649
- * interval) fire at most once per `wedgeWarningIntervalMs` per conversation,
8650
- * naming the consecutive-tick count so the operator sees magnitude rather
8651
- * 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.
8652
8713
  *
8653
8714
  * Deliberately does NOT trigger a release: WI-1's `reconcileWatchers` runs
8654
8715
  * unconditionally on this same tick and is already recovering anything it
@@ -8660,36 +8721,41 @@ var ChannelDriver = class _ChannelDriver {
8660
8721
  const existing = this.wedgeWarnings.get(conv.id);
8661
8722
  const firstWedgedAt = existing?.firstWedgedAt ?? now;
8662
8723
  const consecutiveTicks = (existing?.consecutiveTicks ?? 0) + 1;
8663
- const dueForWarn = !existing || now - existing.lastWarnedAt >= this.wedgeWarningIntervalMs;
8664
- if (!dueForWarn) {
8665
- this.wedgeWarnings.delete(conv.id);
8666
- this.wedgeWarnings.set(conv.id, {
8667
- firstWedgedAt,
8668
- lastWarnedAt: existing.lastWarnedAt,
8669
- 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
8670
8741
  });
8671
- return;
8672
8742
  }
8673
- const stuckForMs = now - firstWedgedAt;
8674
- const untracked = this.countUntrackedIds(messages);
8675
- this.log({
8676
- level: "warn",
8677
- 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.`),
8678
- conversation_id: conv.id
8679
- });
8680
8743
  this.wedgeWarnings.delete(conv.id);
8681
- 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
+ });
8682
8749
  while (this.wedgeWarnings.size > MAX_WEDGED_CONVERSATIONS) {
8683
8750
  const oldest = this.wedgeWarnings.keys().next().value;
8684
8751
  if (oldest === void 0) break;
8685
8752
  this.wedgeWarnings.delete(oldest);
8686
8753
  }
8687
- if (stuckForMs >= this.wedgeWarningIntervalMs) {
8688
- void this.postSignal(conv.id, messages[0].id, "dispatch_wedged", {
8689
- stuck_for_ms: stuckForMs,
8690
- untracked
8691
- });
8692
- }
8754
+ if (!dueForWarn) return;
8755
+ void this.postSignal(conv.id, messages[0].id, "dispatch_wedged", {
8756
+ stuck_for_ms: now - firstWedgedAt,
8757
+ untracked
8758
+ });
8693
8759
  }
8694
8760
  /**
8695
8761
  * How many of `messages`' ids are tracked by NO watcher's `inFlight` (#1618
@@ -9955,14 +10021,40 @@ var ChannelDriver = class _ChannelDriver {
9955
10021
  conversation_id: watcher.conv.id,
9956
10022
  message_id: inFlight.evidentMessageId
9957
10023
  });
9958
- await this.markFailed(
9959
- watcher.conv.id,
9960
- inFlight.evidentMessageId,
9961
- event.sessionId,
9962
- `OpenCode could not run this turn: ${event.reason}`
9963
- );
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
+ }
9964
10053
  inFlight.done = true;
9965
10054
  this.removeInFlight(watcher, inFlight.evidentMessageId);
10055
+ if (isMissingProvider) {
10056
+ await this.repairProviderRegistry(watcher.conv.id, inFlight.evidentMessageId);
10057
+ }
9966
10058
  } catch (err) {
9967
10059
  if (err instanceof ChannelAuthError) {
9968
10060
  this.log({
@@ -9983,6 +10075,44 @@ var ChannelDriver = class _ChannelDriver {
9983
10075
  this.sessionErrorHandled.delete(inFlight.evidentMessageId);
9984
10076
  }
9985
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
+ }
9986
10116
  /**
9987
10117
  * The per-session polling loop (WI-3). Once per tick it:
9988
10118
  * 1. polls `GET /session/:id/message` once and, per in-flight message,
@@ -13678,6 +13808,8 @@ function createCredentialSync({
13678
13808
  var MAX_ACTIVITY_LOG_ENTRIES = 10;
13679
13809
  var MAX_DISCONNECT_REASON_LENGTH = 200;
13680
13810
  var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
13811
+ var CREDENTIAL_ADOPTION_PROBE_ATTEMPTS = 3;
13812
+ var CREDENTIAL_ADOPTION_PROBE_INTERVAL_MS = 1e3;
13681
13813
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
13682
13814
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
13683
13815
  var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
@@ -14025,6 +14157,125 @@ async function handleAuthError(state, error2) {
14025
14157
  return { success: false };
14026
14158
  }
14027
14159
  }
14160
+ function logCredentialAdoptionActivity(state, activity, level) {
14161
+ const message = (() => {
14162
+ switch (activity.outcome) {
14163
+ case "deferred":
14164
+ return `CREDENTIAL-ADOPTION: deferred generation ${activity.generation} with ${activity.active_sessions} active session(s)`;
14165
+ case "reloaded_ready":
14166
+ return `CREDENTIAL-ADOPTION: adopted generation ${activity.generation} with ${activity.active_sessions} active session(s); readiness probe succeeded after ${activity.probe_attempts} attempt(s)`;
14167
+ case "reloaded_interrupted": {
14168
+ 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`;
14169
+ return `CREDENTIAL-ADOPTION: generation ${activity.generation} with ${activity.active_sessions} active session(s); replacement instance answered ${response} after ${activity.probe_attempts} probe attempt(s)`;
14170
+ }
14171
+ case "reload_failed":
14172
+ return `CREDENTIAL-ADOPTION: failed to adopt generation ${activity.generation} with ${activity.active_sessions} active session(s)`;
14173
+ case "skipped_v2":
14174
+ return `CREDENTIAL-ADOPTION: skipped generation ${activity.generation} with ${activity.active_sessions} active session(s) on OpenCode V2`;
14175
+ }
14176
+ })();
14177
+ logActivity(state, {
14178
+ type: level === "error" ? "error" : "info",
14179
+ level,
14180
+ ...level === "error" ? { error: message } : { message },
14181
+ metadata: {
14182
+ ...activity,
14183
+ opencode_version: state.opencodeVersion,
14184
+ port: state.port
14185
+ }
14186
+ });
14187
+ }
14188
+ async function adoptCredentialGeneration(state, generation, adoption, activeSessions, deferredForMs) {
14189
+ const reload = await disposeAndReloadProviderConfig(
14190
+ state.port,
14191
+ state.opencodeClient ?? void 0
14192
+ );
14193
+ if (reload.outcome === "failed") {
14194
+ logCredentialAdoptionActivity(
14195
+ state,
14196
+ {
14197
+ outcome: "reload_failed",
14198
+ generation,
14199
+ active_sessions: activeSessions,
14200
+ adoption,
14201
+ deferred_for_ms: deferredForMs,
14202
+ reload_status: reload.status,
14203
+ reload_detail: reload.detail,
14204
+ probe_attempts: null,
14205
+ probe_status: null,
14206
+ probe_body_empty: null,
14207
+ probe_detail: null
14208
+ },
14209
+ "error"
14210
+ );
14211
+ return;
14212
+ }
14213
+ if (reload.outcome === "skipped_v2") {
14214
+ logCredentialAdoptionActivity(
14215
+ state,
14216
+ {
14217
+ outcome: "skipped_v2",
14218
+ generation,
14219
+ active_sessions: activeSessions,
14220
+ adoption,
14221
+ deferred_for_ms: deferredForMs,
14222
+ reload_status: null,
14223
+ reload_detail: null,
14224
+ probe_attempts: null,
14225
+ probe_status: null,
14226
+ probe_body_empty: null,
14227
+ probe_detail: null
14228
+ },
14229
+ "debug"
14230
+ );
14231
+ return;
14232
+ }
14233
+ let lastProbe = null;
14234
+ let attempts = 0;
14235
+ for (attempts = 1; attempts <= CREDENTIAL_ADOPTION_PROBE_ATTEMPTS; attempts++) {
14236
+ lastProbe = await probeOpenCodeRequestReadiness(state.port, state.opencodeClient ?? void 0);
14237
+ if (lastProbe.ok) {
14238
+ logCredentialAdoptionActivity(
14239
+ state,
14240
+ {
14241
+ outcome: "reloaded_ready",
14242
+ generation,
14243
+ active_sessions: activeSessions,
14244
+ adoption,
14245
+ deferred_for_ms: deferredForMs,
14246
+ reload_status: null,
14247
+ reload_detail: null,
14248
+ probe_attempts: attempts,
14249
+ probe_status: lastProbe.status,
14250
+ probe_body_empty: lastProbe.bodyEmpty,
14251
+ probe_detail: lastProbe.detail
14252
+ },
14253
+ "warn"
14254
+ );
14255
+ return;
14256
+ }
14257
+ if (attempts < CREDENTIAL_ADOPTION_PROBE_ATTEMPTS) {
14258
+ await new Promise((resolve5) => setTimeout(resolve5, CREDENTIAL_ADOPTION_PROBE_INTERVAL_MS));
14259
+ }
14260
+ }
14261
+ logCredentialAdoptionActivity(
14262
+ state,
14263
+ {
14264
+ outcome: "reloaded_interrupted",
14265
+ generation,
14266
+ active_sessions: activeSessions,
14267
+ adoption,
14268
+ deferred_for_ms: deferredForMs,
14269
+ reload_status: null,
14270
+ reload_detail: null,
14271
+ probe_attempts: CREDENTIAL_ADOPTION_PROBE_ATTEMPTS,
14272
+ probe_status: lastProbe?.status ?? null,
14273
+ probe_body_empty: lastProbe?.bodyEmpty ?? null,
14274
+ probe_detail: lastProbe?.detail ?? null
14275
+ },
14276
+ "error"
14277
+ );
14278
+ }
14028
14279
  async function driveChannels(state, driver) {
14029
14280
  let idlePolls = 0;
14030
14281
  let idleMs = 0;
@@ -14034,6 +14285,13 @@ async function driveChannels(state, driver) {
14034
14285
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
14035
14286
  let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
14036
14287
  let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
14288
+ const credentialAdoption = {
14289
+ generation: 0,
14290
+ pendingGeneration: null,
14291
+ pendingSinceMs: null,
14292
+ deferredGeneration: null,
14293
+ adoption: 0
14294
+ };
14037
14295
  const stayAwakeHolds = () => state.stayAwakeUntilMs !== null && Date.now() < state.stayAwakeUntilMs;
14038
14296
  let stayAwakeSuppressionLogged = false;
14039
14297
  while (state.running) {
@@ -14077,12 +14335,30 @@ async function driveChannels(state, driver) {
14077
14335
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
14078
14336
  if (opencodeAuthApplied) state.openaiUsageRearm?.();
14079
14337
  if (claudeCredentialApplied || opencodeAuthApplied) {
14080
- void reloadProviderCache(state.port, state.opencodeClient ?? void 0).catch(
14081
- (error2) => logActivity(state, {
14082
- type: "error",
14083
- error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
14084
- })
14085
- );
14338
+ credentialAdoption.generation++;
14339
+ if (state.opencodeApiVersion === "v2") {
14340
+ logCredentialAdoptionActivity(
14341
+ state,
14342
+ {
14343
+ outcome: "skipped_v2",
14344
+ generation: credentialAdoption.generation,
14345
+ active_sessions: driver.protectedSessionIds().size,
14346
+ adoption: null,
14347
+ deferred_for_ms: null,
14348
+ reload_status: null,
14349
+ reload_detail: null,
14350
+ probe_attempts: null,
14351
+ probe_status: null,
14352
+ probe_body_empty: null,
14353
+ probe_detail: null
14354
+ },
14355
+ "debug"
14356
+ );
14357
+ } else {
14358
+ credentialAdoption.pendingGeneration = credentialAdoption.generation;
14359
+ credentialAdoption.pendingSinceMs = Date.now();
14360
+ credentialAdoption.deferredGeneration = null;
14361
+ }
14086
14362
  }
14087
14363
  if (opencodeAuthApplied && state.opencodeApiVersion === "v2") {
14088
14364
  void importOpenCodeV2CredentialsForState(state).catch(
@@ -14137,6 +14413,49 @@ async function driveChannels(state, driver) {
14137
14413
  }
14138
14414
  }
14139
14415
  }
14416
+ if (credentialAdoption.pendingGeneration !== null) {
14417
+ const activeSessions = driver.protectedSessionIds();
14418
+ const generation = credentialAdoption.pendingGeneration;
14419
+ if (activeSessions.size > 0) {
14420
+ if (credentialAdoption.deferredGeneration !== generation) {
14421
+ credentialAdoption.deferredGeneration = generation;
14422
+ logCredentialAdoptionActivity(
14423
+ state,
14424
+ {
14425
+ outcome: "deferred",
14426
+ generation,
14427
+ active_sessions: activeSessions.size,
14428
+ adoption: null,
14429
+ deferred_for_ms: null,
14430
+ reload_status: null,
14431
+ reload_detail: null,
14432
+ probe_attempts: null,
14433
+ probe_status: null,
14434
+ probe_body_empty: null,
14435
+ probe_detail: null
14436
+ },
14437
+ "warn"
14438
+ );
14439
+ }
14440
+ } else {
14441
+ const deferredForMs = credentialAdoption.deferredGeneration === generation && credentialAdoption.pendingSinceMs !== null ? Math.max(0, Date.now() - credentialAdoption.pendingSinceMs) : null;
14442
+ credentialAdoption.pendingGeneration = null;
14443
+ credentialAdoption.pendingSinceMs = null;
14444
+ credentialAdoption.adoption++;
14445
+ void adoptCredentialGeneration(
14446
+ state,
14447
+ generation,
14448
+ credentialAdoption.adoption,
14449
+ activeSessions.size,
14450
+ deferredForMs
14451
+ ).catch(
14452
+ (error2) => logActivity(state, {
14453
+ type: "error",
14454
+ 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)}`
14455
+ })
14456
+ );
14457
+ }
14458
+ }
14140
14459
  await new Promise((resolve5) => setTimeout(resolve5, CHANNEL_POLL_INTERVAL_MS));
14141
14460
  const cycleMs = performance.now() - cycleStartedAtMs;
14142
14461
  if (idleThisCycle) idleMs += cycleMs;
@@ -15353,7 +15672,7 @@ async function run(options) {
15353
15672
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
15354
15673
  }
15355
15674
  }
15356
- await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
15675
+ await disposeAndReloadProviderConfig(state.port, state.opencodeClient ?? void 0);
15357
15676
  if (state.opencodeApiVersion === "v2" && state.opencodeClient !== null) {
15358
15677
  await importOpenCodeV2CredentialsForState(state);
15359
15678
  }