@evident-ai/cli 3.5.2-dev.ffa823b → 3.6.1-dev.37d1a87

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
@@ -1156,9 +1156,9 @@ async function claudeUsage() {
1156
1156
  }
1157
1157
 
1158
1158
  // src/commands/run.ts
1159
- import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
1160
- import { homedir as homedir7 } from "node:os";
1161
- import { isAbsolute as isAbsolute4, join as join12, parse, resolve as resolvePath2 } from "node:path";
1159
+ import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
1160
+ import { homedir as homedir8 } from "node:os";
1161
+ import { isAbsolute as isAbsolute4, join as join13, parse, resolve as resolvePath2 } from "node:path";
1162
1162
  import chalk7 from "chalk";
1163
1163
 
1164
1164
  // ../../packages/types/src/agents/index.ts
@@ -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.37d1a87" : void 0) ?? process.env.npm_package_version ?? "unknown";
1326
1330
  function getCliVersion() {
1327
1331
  return CLI_VERSION;
1328
1332
  }
@@ -2071,7 +2075,8 @@ function createDependencyInstallDiagnosticReporter({
2071
2075
  var FORWARDED_LEVELS_BY_SOURCE = {
2072
2076
  "cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
2073
2077
  opencode: /* @__PURE__ */ new Set(["warn", "error"]),
2074
- "cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"])
2078
+ "cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"]),
2079
+ "cli.runtime-provenance": /* @__PURE__ */ new Set(["info", "warn", "error"])
2075
2080
  };
2076
2081
  var SEVERITY_BY_LEVEL = {
2077
2082
  info: "info",
@@ -2867,6 +2872,58 @@ async function listAllSessions(port, client) {
2867
2872
  }
2868
2873
  return { sessions };
2869
2874
  }
2875
+ var INTERRUPT_NETWORK_CODES = /* @__PURE__ */ new Set([
2876
+ "ABORT_ERR",
2877
+ "EAI_AGAIN",
2878
+ "ECONNREFUSED",
2879
+ "ECONNRESET",
2880
+ "ENETUNREACH",
2881
+ "ENOTFOUND",
2882
+ "ETIMEDOUT",
2883
+ "UND_ERR_CONNECT_TIMEOUT",
2884
+ "UND_ERR_HEADERS_TIMEOUT",
2885
+ "UND_ERR_SOCKET"
2886
+ ]);
2887
+ function interruptFailureDiagnostic(error2) {
2888
+ const message = error2 instanceof Error ? error2.message : "";
2889
+ const httpStatus = /^OpenCode request failed: HTTP ([1-5]\d{2})(?::|$)/.exec(message)?.[1];
2890
+ if (httpStatus) return `HTTP ${httpStatus}`;
2891
+ const candidates = [error2, error2 instanceof Error ? error2.cause : void 0];
2892
+ for (const candidate of candidates) {
2893
+ if (typeof candidate !== "object" || candidate === null || !("code" in candidate)) continue;
2894
+ const code = candidate.code;
2895
+ if (typeof code === "string" && INTERRUPT_NETWORK_CODES.has(code)) {
2896
+ return redactDiagnosticText(`network ${code}`).slice(0, 120);
2897
+ }
2898
+ }
2899
+ const name = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
2900
+ return redactDiagnosticText(`request threw ${name}`).slice(0, 120);
2901
+ }
2902
+ async function interruptSession(port, id, client) {
2903
+ if (client?.version === "v2") {
2904
+ try {
2905
+ await interruptV2Session(client, id);
2906
+ return { kind: "confirmed_stopped" };
2907
+ } catch (error2) {
2908
+ return { kind: "not_confirmed", diagnostic: interruptFailureDiagnostic(error2) };
2909
+ }
2910
+ }
2911
+ try {
2912
+ const res = await requestWithClient(
2913
+ port,
2914
+ client,
2915
+ `/session/${id}/abort`,
2916
+ { method: "POST" },
2917
+ { allowStatuses: ALL_HTTP_STATUSES }
2918
+ );
2919
+ if (res.status >= 200 && res.status < 300) {
2920
+ return { kind: "confirmed_stopped", status: res.status };
2921
+ }
2922
+ return { kind: "not_confirmed", status: res.status };
2923
+ } catch (error2) {
2924
+ return { kind: "threw", diagnostic: interruptFailureDiagnostic(error2) };
2925
+ }
2926
+ }
2870
2927
  async function deleteSession(port, id, client) {
2871
2928
  if (client?.version === "v2") return deleteV2Session(client, id);
2872
2929
  try {
@@ -3518,19 +3575,32 @@ function isAbortedTerminalReply(messages, userMessageId) {
3518
3575
  }
3519
3576
  return false;
3520
3577
  }
3578
+ var CLAUDE_AUTH_EXPIRY_MESSAGES = Object.freeze([
3579
+ "Claude Code credentials are unavailable or expired. Run `claude` to refresh them."
3580
+ ]);
3581
+ function extractReplyErrorShape(reply) {
3582
+ const error2 = errorOf(reply);
3583
+ if (error2 == null || typeof error2 !== "object") {
3584
+ return { name: null, statusCode: null };
3585
+ }
3586
+ const e = error2;
3587
+ const name = typeof e.name === "string" ? e.name.slice(0, 100) : null;
3588
+ const data = e.data;
3589
+ const statusCode = typeof data?.statusCode === "number" && Number.isFinite(data.statusCode) ? data.statusCode : null;
3590
+ return { name, statusCode };
3591
+ }
3521
3592
  function classifyReplyAuthError(reply) {
3522
3593
  const error2 = errorOf(reply);
3523
3594
  if (error2 == null || typeof error2 !== "object") return null;
3524
3595
  const e = error2;
3525
3596
  const replyProviderId = reply?.info?.providerID ?? null;
3526
3597
  const replyModelId = reply?.info?.modelID ?? null;
3598
+ const data = e.data;
3527
3599
  if (e.name === "ProviderAuthError") {
3528
- const data = e.data;
3529
3600
  const providerId = typeof data?.providerID === "string" && data.providerID || replyProviderId;
3530
3601
  return { kind: "model_auth", providerId, modelId: replyModelId, reason: "missing" };
3531
3602
  }
3532
3603
  if (e.name === "APIError") {
3533
- const data = e.data;
3534
3604
  const statusCode = data?.statusCode;
3535
3605
  if (statusCode === 401 || statusCode === 403) {
3536
3606
  return {
@@ -3541,11 +3611,23 @@ function classifyReplyAuthError(reply) {
3541
3611
  };
3542
3612
  }
3543
3613
  }
3614
+ const errorMessage3 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
3615
+ if (errorMessage3 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage3.includes(knownMessage))) {
3616
+ return {
3617
+ kind: "model_auth",
3618
+ providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
3619
+ modelId: replyModelId,
3620
+ reason: "rejected"
3621
+ };
3622
+ }
3544
3623
  return null;
3545
3624
  }
3546
3625
  function messageFailure(messages, userMessageId) {
3547
3626
  return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
3548
3627
  }
3628
+ function isProviderModelNotFoundError(reason) {
3629
+ return reason.includes("Model not found:");
3630
+ }
3549
3631
  function findLatestSubagentAuthOutcome(messages, sinceMs) {
3550
3632
  if (!messages || messages.length === 0) return null;
3551
3633
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -3754,23 +3836,49 @@ async function readSessionErrorStream(port, options, client) {
3754
3836
  if (reader) void reader.cancel().catch(() => void 0);
3755
3837
  }
3756
3838
  }
3757
- async function reloadProviderCache(port, client) {
3758
- if (client?.version === "v2") return;
3839
+ async function disposeAndReloadProviderConfig(port, client) {
3840
+ if (client?.version === "v2") return { outcome: "skipped_v2" };
3759
3841
  try {
3760
- const res = await requestWithClient(port, client, "/config", {
3761
- method: "PATCH",
3762
- headers: { "Content-Type": "application/json" },
3763
- body: JSON.stringify({})
3764
- });
3842
+ const res = await requestWithClient(
3843
+ port,
3844
+ client,
3845
+ "/config",
3846
+ {
3847
+ method: "PATCH",
3848
+ headers: { "Content-Type": "application/json" },
3849
+ body: JSON.stringify({})
3850
+ },
3851
+ { allowStatuses: ALL_HTTP_STATUSES }
3852
+ );
3765
3853
  if (!res.ok) {
3766
3854
  console.error(
3767
- `[reloadProviderCache] PATCH /config returned HTTP ${res.status} (port ${port})`
3855
+ `[disposeAndReloadProviderConfig] PATCH /config returned HTTP ${res.status} (port ${port})`
3768
3856
  );
3857
+ return { outcome: "failed", status: res.status, detail: null };
3769
3858
  }
3859
+ return { outcome: "reloaded" };
3770
3860
  } catch (err) {
3771
3861
  console.error(
3772
- `[reloadProviderCache] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3862
+ `[disposeAndReloadProviderConfig] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3773
3863
  );
3864
+ return {
3865
+ outcome: "failed",
3866
+ status: null,
3867
+ detail: err instanceof Error ? err.message : String(err)
3868
+ };
3869
+ }
3870
+ }
3871
+ async function probeOpenCodeRequestReadiness(port, client) {
3872
+ try {
3873
+ const res = await requestWithClient(port, client, "/config", void 0, {
3874
+ allowStatuses: ALL_HTTP_STATUSES
3875
+ });
3876
+ const body = await res.text();
3877
+ return { ok: res.ok, status: res.status, bodyEmpty: body.trim().length === 0, detail: null };
3878
+ } catch (err) {
3879
+ const detail = err instanceof Error ? err.message : String(err);
3880
+ console.error(`[probeOpenCodeRequestReadiness] GET /config failed (port ${port}): ${detail}`);
3881
+ return { ok: false, status: null, bodyEmpty: null, detail };
3774
3882
  }
3775
3883
  }
3776
3884
 
@@ -4166,6 +4274,11 @@ async function listV2Permissions(client, sessionId) {
4166
4274
  return null;
4167
4275
  }
4168
4276
  }
4277
+ async function interruptV2Session(client, sessionId) {
4278
+ await client.request(`/api/session/${encodeURIComponent(sessionId)}/interrupt`, {
4279
+ method: "POST"
4280
+ });
4281
+ }
4169
4282
  async function getV2ActiveSessions(client) {
4170
4283
  try {
4171
4284
  const body = await readJson(await client.request("/api/session/active"));
@@ -7537,7 +7650,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
7537
7650
  var MAX_BUFFERED_SESSION_ERRORS = 256;
7538
7651
  var SESSION_ERROR_RETENTION_TTL_MS = 6e4;
7539
7652
  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: ";
7653
+ var SESSION_ERROR_RETAINED_DETAIL = "Runner-observed OpenCode session error: ";
7541
7654
  var SESSION_ERROR_OBSERVED_CAUSE = "Runner-observed OpenCode session-store cause: ";
7542
7655
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
7543
7656
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
@@ -7661,12 +7774,15 @@ var ChannelDriver = class _ChannelDriver {
7661
7774
  /**
7662
7775
  * Per-conversation throttle state for the #183 recurrence warning (#1618
7663
7776
  * 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`
7777
+ * `firstWedgedAt` anchors `stuck_for_ms`; `warnAnchorAt` is the throttle
7778
+ * anchor from first detection through the first warning, then from each
7779
+ * warning onward; `consecutiveTicks` is reported in the log text so the
7780
+ * operator sees magnitude, not repetition. Cleared the moment the
7781
+ * conversation dispatches anything, or when it drops off the pending list
7782
+ * (`runDrain`'s prune, the dominant real-world path — a wedge normally ends
7783
+ * with its message completing, not a fresh dispatch on the same
7784
+ * conversation) — either way a fresh wedge, if it recurs, is a new
7785
+ * incident. Bounded FIFO, mirroring `supersededSessions`
7670
7786
  * (`MAX_WEDGED_CONVERSATIONS`).
7671
7787
  */
7672
7788
  wedgeWarnings = /* @__PURE__ */ new Map();
@@ -7896,6 +8012,7 @@ var ChannelDriver = class _ChannelDriver {
7896
8012
  subagentInvocationPrefetches = /* @__PURE__ */ new Map();
7897
8013
  /** Serialises drains so a reconnect during a drain doesn't double-process. */
7898
8014
  draining = false;
8015
+ pendingProviderRegistryRepair = null;
7899
8016
  /**
7900
8017
  * Serialises runner-file syncs (#559) so the ~2s poll tick and a concurrent
7901
8018
  * drain ping don't download, write and ack the same file twice.
@@ -8081,8 +8198,33 @@ var ChannelDriver = class _ChannelDriver {
8081
8198
  async runDrain() {
8082
8199
  let dispatched = 0;
8083
8200
  try {
8201
+ const pendingRepair = this.pendingProviderRegistryRepair;
8202
+ if (pendingRepair && this.protectedSessionIds().size === 0) {
8203
+ try {
8204
+ await this.issueProviderRegistryReload(
8205
+ pendingRepair.conversationId,
8206
+ pendingRepair.messageId
8207
+ );
8208
+ } catch (err) {
8209
+ this.log({
8210
+ level: "error",
8211
+ message: `Provider registry repair adoption failed for message ${pendingRepair.messageId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`,
8212
+ conversation_id: pendingRepair.conversationId,
8213
+ message_id: pendingRepair.messageId
8214
+ });
8215
+ }
8216
+ return 0;
8217
+ }
8084
8218
  this.pendingMessageIds.clear();
8085
8219
  const conversations = await this.getPendingConversations();
8220
+ if (this.wedgeWarnings.size > 0) {
8221
+ const pendingConversationIds = new Set(conversations.map(({ id }) => id));
8222
+ for (const conversationId of this.wedgeWarnings.keys()) {
8223
+ if (!pendingConversationIds.has(conversationId)) {
8224
+ this.wedgeWarnings.delete(conversationId);
8225
+ }
8226
+ }
8227
+ }
8086
8228
  if (this.recycleRequestedFlag) {
8087
8229
  this.stop();
8088
8230
  }
@@ -8619,14 +8761,13 @@ var ChannelDriver = class _ChannelDriver {
8619
8761
  * skip-because-already-`dispatched`, the exact signature of a message stuck
8620
8762
  * acknowledged-but-never-worked.
8621
8763
  *
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.
8764
+ * The first detection starts the clock and emits a local `debug` entry. The
8765
+ * `warn` log and paired `dispatch_wedged` signal fire once the wedge has
8766
+ * lasted `wedgeWarningIntervalMs`, then at most once per interval, naming the
8767
+ * consecutive-tick count so the operator sees magnitude rather than repetition.
8768
+ * A watcher stall that the loop-liveness watchdog recovers is expected and is
8769
+ * already reported by its `watcher_recovered` signal, so it does not reach an
8770
+ * operator through this warning.
8630
8771
  *
8631
8772
  * Deliberately does NOT trigger a release: WI-1's `reconcileWatchers` runs
8632
8773
  * unconditionally on this same tick and is already recovering anything it
@@ -8638,36 +8779,41 @@ var ChannelDriver = class _ChannelDriver {
8638
8779
  const existing = this.wedgeWarnings.get(conv.id);
8639
8780
  const firstWedgedAt = existing?.firstWedgedAt ?? now;
8640
8781
  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
8782
+ const warnAnchorAt = existing?.warnAnchorAt ?? now;
8783
+ const dueForWarn = now - warnAnchorAt >= this.wedgeWarningIntervalMs;
8784
+ if (!existing) {
8785
+ this.log({
8786
+ level: "debug",
8787
+ 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.`,
8788
+ conversation_id: conv.id
8789
+ });
8790
+ }
8791
+ let untracked = 0;
8792
+ if (dueForWarn) {
8793
+ const stuckForMs = now - firstWedgedAt;
8794
+ untracked = this.countUntrackedIds(messages);
8795
+ this.log({
8796
+ level: "warn",
8797
+ 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.`),
8798
+ conversation_id: conv.id
8648
8799
  });
8649
- return;
8650
8800
  }
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
8801
  this.wedgeWarnings.delete(conv.id);
8659
- this.wedgeWarnings.set(conv.id, { firstWedgedAt, lastWarnedAt: now, consecutiveTicks });
8802
+ this.wedgeWarnings.set(conv.id, {
8803
+ firstWedgedAt,
8804
+ warnAnchorAt: dueForWarn ? now : warnAnchorAt,
8805
+ consecutiveTicks
8806
+ });
8660
8807
  while (this.wedgeWarnings.size > MAX_WEDGED_CONVERSATIONS) {
8661
8808
  const oldest = this.wedgeWarnings.keys().next().value;
8662
8809
  if (oldest === void 0) break;
8663
8810
  this.wedgeWarnings.delete(oldest);
8664
8811
  }
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
- }
8812
+ if (!dueForWarn) return;
8813
+ void this.postSignal(conv.id, messages[0].id, "dispatch_wedged", {
8814
+ stuck_for_ms: now - firstWedgedAt,
8815
+ untracked
8816
+ });
8671
8817
  }
8672
8818
  /**
8673
8819
  * How many of `messages`' ids are tracked by NO watcher's `inFlight` (#1618
@@ -8803,6 +8949,34 @@ var ChannelDriver = class _ChannelDriver {
8803
8949
  if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
8804
8950
  const ongoing = await this.isSessionOngoing(sessionId);
8805
8951
  if (ongoing === false) {
8952
+ const age = this.absoluteAgeForRow(message.processing_started_at);
8953
+ if (age.tripped && age.anchorMs !== null) {
8954
+ try {
8955
+ await this.markFailed(
8956
+ conv.id,
8957
+ message.id,
8958
+ sessionId,
8959
+ this.absoluteCeilingError(message.id, sessionId, age.anchorMs),
8960
+ void 0,
8961
+ {
8962
+ classification: null,
8963
+ errorShape: extractReplyErrorShape(findLastAssistantReplyFor(messages, ocId ?? ""))
8964
+ }
8965
+ );
8966
+ } catch (err) {
8967
+ if (err instanceof ChannelAuthError) throw err;
8968
+ this.log({
8969
+ level: "warn",
8970
+ message: `Re-drive: failed to report past-ceiling restart orphan ${message.id.slice(0, 8)} terminal; refusing to re-dispatch: ${err instanceof Error ? err.message : String(err)}`,
8971
+ conversation_id: conv.id,
8972
+ message_id: message.id
8973
+ });
8974
+ return "unresolved";
8975
+ }
8976
+ this.clearRedriveUnresolved(message.id);
8977
+ void this.postSignal(conv.id, message.id, "redrive_ceiling_refused");
8978
+ return "settled";
8979
+ }
8806
8980
  this.log({
8807
8981
  level: "info",
8808
8982
  message: `Re-drive: message ${message.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check \u2014 restart orphan, re-dispatching instead of marking it permanently failed`,
@@ -8873,6 +9047,29 @@ var ChannelDriver = class _ChannelDriver {
8873
9047
  message_id: message.id
8874
9048
  });
8875
9049
  }
9050
+ if (this.absoluteAgeForRow(message.processing_started_at).tripped) {
9051
+ try {
9052
+ await this.markFailed(
9053
+ conv.id,
9054
+ message.id,
9055
+ sessionId,
9056
+ this.absoluteCeilingError(message.id, sessionId, anchorMs)
9057
+ );
9058
+ } catch (err) {
9059
+ if (err instanceof ChannelAuthError) throw err;
9060
+ this.log({
9061
+ level: "warn",
9062
+ message: `Re-drive: failed to report past-ceiling message ${message.id.slice(0, 8)} terminal; refusing to re-attach: ${err instanceof Error ? err.message : String(err)}`,
9063
+ conversation_id: conv.id,
9064
+ message_id: message.id
9065
+ });
9066
+ const bound = await this.boundRedriveOutcome(conv, message, "reattach");
9067
+ return bound === "abandoned" ? "abandoned" : "unresolved";
9068
+ }
9069
+ this.clearRedriveUnresolved(message.id);
9070
+ void this.postSignal(conv.id, message.id, "redrive_ceiling_refused");
9071
+ return "abandoned";
9072
+ }
8876
9073
  const title = await this.resolveSessionTitle(sessionId, conv.id);
8877
9074
  try {
8878
9075
  await this.markProcessing(conv.id, message.id, sessionId, ocId, title);
@@ -9139,8 +9336,7 @@ var ChannelDriver = class _ChannelDriver {
9139
9336
  const since = this.redriveOutcomeFailingSince.get(message.id);
9140
9337
  if (since === void 0) this.redriveOutcomeFailingSince.set(message.id, now);
9141
9338
  const durationTripped = now - (since ?? now) >= this.pausedMaxWaitMs;
9142
- const parsed = message.processing_started_at ? Date.parse(message.processing_started_at) : NaN;
9143
- const absoluteAgeTripped = !Number.isNaN(parsed) && now - parsed >= ABSOLUTE_MAX_PROCESSING_MS;
9339
+ const absoluteAgeTripped = this.absoluteAgeForRow(message.processing_started_at).tripped;
9144
9340
  if (!durationTripped && !absoluteAgeTripped) {
9145
9341
  this.signalRedriveOutcomeUnreported(conv, message, outcome);
9146
9342
  return "retry";
@@ -9179,6 +9375,15 @@ var ChannelDriver = class _ChannelDriver {
9179
9375
  });
9180
9376
  return "abandoned";
9181
9377
  }
9378
+ /** Row-level guards use the durable server stamp; in-flight guards use processingAnchorMs. */
9379
+ absoluteAgeForRow(stamp) {
9380
+ const anchorMs = stamp ? Date.parse(stamp) : NaN;
9381
+ return Number.isNaN(anchorMs) ? { tripped: false, anchorMs: null } : { tripped: this.now() - anchorMs >= ABSOLUTE_MAX_PROCESSING_MS, anchorMs };
9382
+ }
9383
+ absoluteCeilingError(messageId, sessionId, anchorMs, retainedReason = this.readRetainedSessionError(sessionId)) {
9384
+ const baseError = `Message ${messageId.slice(0, 8)} exceeded the absolute processing ceiling (${Math.round((this.now() - anchorMs) / 6e4)}min, session ${sessionId}) while still actively running.`;
9385
+ return retainedReason ? `${baseError} ${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}` : baseError;
9386
+ }
9182
9387
  /**
9183
9388
  * Record one poll outcome toward the re-drive fence's consecutive-identical-
9184
9389
  * failure streak (#1348) and return the resulting count. `signature === null`
@@ -9620,10 +9825,7 @@ var ChannelDriver = class _ChannelDriver {
9620
9825
  opencodeMessageId,
9621
9826
  message,
9622
9827
  dispatchedAt: this.now(),
9623
- // Anchor the absolute-age ceiling to the SERVER-SIDE `processed_at`, NOT
9624
- // `dispatchedAt` — so a re-adopted zombie's age reflects the real turn duration
9625
- // and the ceiling fires on the ORIGINAL turn.
9626
- processingAnchorMs: processedAtMs,
9828
+ processingAnchorMs: this.absoluteAgeForRow(message.processing_started_at).anchorMs ?? processedAtMs,
9627
9829
  deadline: watchDeadlineAnchorMs + this.pausedMaxWaitMs,
9628
9830
  // The server row is ALREADY `processing`; do not re-fire markProcessing.
9629
9831
  started: true,
@@ -9933,14 +10135,40 @@ var ChannelDriver = class _ChannelDriver {
9933
10135
  conversation_id: watcher.conv.id,
9934
10136
  message_id: inFlight.evidentMessageId
9935
10137
  });
9936
- await this.markFailed(
9937
- watcher.conv.id,
9938
- inFlight.evidentMessageId,
9939
- event.sessionId,
9940
- `OpenCode could not run this turn: ${event.reason}`
9941
- );
10138
+ const isMissingProvider = isProviderModelNotFoundError(event.reason);
10139
+ if (isMissingProvider) {
10140
+ const dispatchedModel = inFlight.message.opencode_model;
10141
+ const separator = dispatchedModel?.indexOf("/") ?? -1;
10142
+ const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
10143
+ await this.markFailed(
10144
+ watcher.conv.id,
10145
+ inFlight.evidentMessageId,
10146
+ event.sessionId,
10147
+ `OpenCode could not run this turn: ${event.reason}`,
10148
+ void 0,
10149
+ {
10150
+ classification: {
10151
+ kind: "model_auth",
10152
+ providerId: hasProviderAndModel ? dispatchedModel.slice(0, separator) : null,
10153
+ modelId: hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null,
10154
+ reason: "missing"
10155
+ },
10156
+ errorShape: { name: null, statusCode: null }
10157
+ }
10158
+ );
10159
+ } else {
10160
+ await this.markFailed(
10161
+ watcher.conv.id,
10162
+ inFlight.evidentMessageId,
10163
+ event.sessionId,
10164
+ `OpenCode could not run this turn: ${event.reason}`
10165
+ );
10166
+ }
9942
10167
  inFlight.done = true;
9943
10168
  this.removeInFlight(watcher, inFlight.evidentMessageId);
10169
+ if (isMissingProvider) {
10170
+ await this.repairProviderRegistry(watcher.conv.id, inFlight.evidentMessageId);
10171
+ }
9944
10172
  } catch (err) {
9945
10173
  if (err instanceof ChannelAuthError) {
9946
10174
  this.log({
@@ -9961,6 +10189,44 @@ var ChannelDriver = class _ChannelDriver {
9961
10189
  this.sessionErrorHandled.delete(inFlight.evidentMessageId);
9962
10190
  }
9963
10191
  }
10192
+ async repairProviderRegistry(conversationId, messageId) {
10193
+ if (this.isV2) {
10194
+ void this.postSignal(conversationId, messageId, "provider_registry_reload_unsupported");
10195
+ return;
10196
+ }
10197
+ const activeSessions = this.protectedSessionIds().size;
10198
+ if (activeSessions > 0 || this.draining) {
10199
+ this.pendingProviderRegistryRepair = { conversationId, messageId };
10200
+ void this.postSignal(conversationId, messageId, "provider_registry_repair_deferred", {
10201
+ active_sessions: activeSessions
10202
+ });
10203
+ this.log({
10204
+ level: "warn",
10205
+ message: `Provider registry repair deferred until a lifecycle-safe point; ${activeSessions} active session(s) protected`,
10206
+ conversation_id: conversationId,
10207
+ message_id: messageId
10208
+ });
10209
+ return;
10210
+ }
10211
+ await this.issueProviderRegistryReload(conversationId, messageId);
10212
+ }
10213
+ async issueProviderRegistryReload(conversationId, messageId) {
10214
+ this.pendingProviderRegistryRepair = null;
10215
+ const result = await disposeAndReloadProviderConfig(this.port, this.openCodeClient);
10216
+ if (result.outcome === "failed") {
10217
+ const failure = result.status === null ? `transport failure${result.detail ? `: ${result.detail}` : ""}` : `HTTP ${result.status}`;
10218
+ this.log({
10219
+ level: "warn",
10220
+ message: `Provider registry reload failed (${failure})`,
10221
+ conversation_id: conversationId,
10222
+ message_id: messageId
10223
+ });
10224
+ }
10225
+ const repairOutcome = result.outcome === "reloaded" ? "reloaded" : "reload_failed";
10226
+ void this.postSignal(conversationId, messageId, "provider_registry_reload_issued", {
10227
+ repair_outcome: repairOutcome
10228
+ });
10229
+ }
9964
10230
  /**
9965
10231
  * The per-session polling loop (WI-3). Once per tick it:
9966
10232
  * 1. polls `GET /session/:id/message` once and, per in-flight message,
@@ -10010,7 +10276,9 @@ var ChannelDriver = class _ChannelDriver {
10010
10276
  }
10011
10277
  const { openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk } = await this.pollInteractions(sessionId, watcher, messages);
10012
10278
  if (watcher.generation !== generation) return;
10279
+ const ceilingAttemptsThisTick = /* @__PURE__ */ new Set();
10013
10280
  for (const inFlight of [...watcher.inFlight.values()]) {
10281
+ if (ceilingAttemptsThisTick.has(inFlight.evidentMessageId)) continue;
10014
10282
  await this.serviceInFlightMessage(
10015
10283
  sessionId,
10016
10284
  watcher,
@@ -10019,7 +10287,8 @@ var ChannelDriver = class _ChannelDriver {
10019
10287
  openQuestions,
10020
10288
  openPermissions,
10021
10289
  questionsPolledOk,
10022
- permissionsPolledOk
10290
+ permissionsPolledOk,
10291
+ ceilingAttemptsThisTick
10023
10292
  );
10024
10293
  }
10025
10294
  }
@@ -10060,13 +10329,117 @@ var ChannelDriver = class _ChannelDriver {
10060
10329
  inFlight.deadline = this.now() + this.pausedMaxWaitMs;
10061
10330
  }
10062
10331
  }
10332
+ async settleAbsoluteCeiling(sessionId, watcher, messages, ceilingAttemptsThisTick) {
10333
+ const entries = [...watcher.inFlight.values()];
10334
+ const pastCeiling = entries.filter(
10335
+ (entry) => this.now() - entry.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS
10336
+ );
10337
+ const healthySiblings = entries.length - pastCeiling.length;
10338
+ const landed = [];
10339
+ let allRecordsLanded = true;
10340
+ const retainedReason = this.readRetainedSessionError(sessionId);
10341
+ for (const entry of pastCeiling) {
10342
+ if (!watcher.inFlight.has(entry.evidentMessageId)) continue;
10343
+ ceilingAttemptsThisTick.add(entry.evidentMessageId);
10344
+ const error2 = this.absoluteCeilingError(
10345
+ entry.evidentMessageId,
10346
+ sessionId,
10347
+ entry.processingAnchorMs,
10348
+ retainedReason
10349
+ );
10350
+ this.log({
10351
+ level: "warn",
10352
+ message: error2,
10353
+ conversation_id: watcher.conv.id,
10354
+ message_id: entry.evidentMessageId
10355
+ });
10356
+ try {
10357
+ await this.markFailed(
10358
+ watcher.conv.id,
10359
+ entry.evidentMessageId,
10360
+ sessionId,
10361
+ error2,
10362
+ void 0,
10363
+ {
10364
+ classification: null,
10365
+ errorShape: extractReplyErrorShape(
10366
+ findLastAssistantReplyFor(messages, entry.opencodeMessageId)
10367
+ )
10368
+ }
10369
+ );
10370
+ } catch (err) {
10371
+ if (err instanceof ChannelAuthError) throw err;
10372
+ allRecordsLanded = false;
10373
+ const watched_for_ms = this.now() - entry.processingAnchorMs;
10374
+ if (err instanceof ChannelTerminalError) {
10375
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10376
+ watched_for_ms,
10377
+ interrupt_outcome: "record_terminal"
10378
+ });
10379
+ this.removeInFlight(watcher, entry.evidentMessageId);
10380
+ continue;
10381
+ }
10382
+ if (this.now() >= entry.deadline) {
10383
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10384
+ watched_for_ms,
10385
+ interrupt_outcome: "record_deadline"
10386
+ });
10387
+ this.removeInFlight(watcher, entry.evidentMessageId);
10388
+ continue;
10389
+ }
10390
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10391
+ watched_for_ms,
10392
+ interrupt_outcome: "record_retry"
10393
+ });
10394
+ this.log({
10395
+ level: "warn",
10396
+ message: `Failed to record the absolute-ceiling failure for message ${entry.evidentMessageId.slice(0, 8)}; leaving its running turn untouched and retrying next tick: ${err instanceof Error ? err.message : String(err)}`,
10397
+ conversation_id: watcher.conv.id,
10398
+ message_id: entry.evidentMessageId
10399
+ });
10400
+ continue;
10401
+ }
10402
+ entry.done = true;
10403
+ landed.push(entry);
10404
+ }
10405
+ if (!allRecordsLanded) {
10406
+ for (const entry of landed) this.removeInFlight(watcher, entry.evidentMessageId);
10407
+ return;
10408
+ }
10409
+ if (healthySiblings > 0) {
10410
+ for (const entry of landed) {
10411
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10412
+ watched_for_ms: this.now() - entry.processingAnchorMs,
10413
+ interrupt_outcome: "deferred_for_healthy_sibling",
10414
+ healthy_siblings: healthySiblings
10415
+ });
10416
+ this.removeInFlight(watcher, entry.evidentMessageId);
10417
+ }
10418
+ return;
10419
+ }
10420
+ const interrupt = await interruptSession(this.port, sessionId, this.openCodeClient);
10421
+ for (const entry of landed) {
10422
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10423
+ watched_for_ms: this.now() - entry.processingAnchorMs,
10424
+ interrupt_outcome: interrupt.kind
10425
+ });
10426
+ this.removeInFlight(watcher, entry.evidentMessageId);
10427
+ }
10428
+ if (interrupt.kind !== "confirmed_stopped") {
10429
+ this.log({
10430
+ level: "warn",
10431
+ message: `Absolute-ceiling records landed for session ${sessionId}, but OpenCode interruption was ${interrupt.kind}${interrupt.diagnostic ? ` (${interrupt.diagnostic})` : "status" in interrupt && typeof interrupt.status === "number" ? ` (HTTP ${interrupt.status})` : ""}; the rows are terminal and released, while the session may still be running`,
10432
+ conversation_id: watcher.conv.id
10433
+ });
10434
+ }
10435
+ }
10063
10436
  /**
10064
10437
  * Drive ONE in-flight message's lifecycle from the tick's message snapshot.
10065
10438
  * Fires markProcessing on queued→running and markDone on done (each once),
10066
10439
  * applies the idle-path re-dispatch guard, and removes the message from the
10067
10440
  * in-flight set on completion or timeout.
10068
10441
  */
10069
- async serviceInFlightMessage(sessionId, watcher, inFlight, messages, openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk) {
10442
+ async serviceInFlightMessage(sessionId, watcher, inFlight, messages, openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk, ceilingAttemptsThisTick) {
10070
10443
  const conv = watcher.conv;
10071
10444
  const state = messageRunState(messages, inFlight.opencodeMessageId);
10072
10445
  const id = inFlight.evidentMessageId;
@@ -10152,7 +10525,7 @@ var ChannelDriver = class _ChannelDriver {
10152
10525
  if (!inFlight.done) {
10153
10526
  const messageErrorText = messageError(messages, inFlight.opencodeMessageId);
10154
10527
  const retainedReason = this.readRetainedSessionError(sessionId);
10155
- const retainedError = retainedReason === null ? null : `${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}`;
10528
+ const retainedError = retainedReason === null || messageErrorText !== null && messageErrorText.includes(retainedReason) ? null : `${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}`;
10156
10529
  const observedCause = this.sessionStoreHealth?.readCause(sessionId);
10157
10530
  const causeError = observedCause === null || observedCause === void 0 ? null : `${SESSION_ERROR_OBSERVED_CAUSE}${observedCause}`;
10158
10531
  const storeError = this.sessionStoreHealth?.isUnwritable() ? SESSION_STORE_UNWRITABLE_ERROR : null;
@@ -10410,17 +10783,8 @@ var ChannelDriver = class _ChannelDriver {
10410
10783
  return;
10411
10784
  }
10412
10785
  }
10413
- if (activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
10414
- this.log({
10415
- level: "warn",
10416
- message: `Message ${inFlight.evidentMessageId.slice(0, 8)} exceeded the absolute processing ceiling (${Math.round((this.now() - inFlight.processingAnchorMs) / 6e4)}min, session ${sessionId}) while still actively running \u2014 releasing so the cron can reclaim it`,
10417
- conversation_id: conv.id,
10418
- message_id: inFlight.evidentMessageId
10419
- });
10420
- void this.postSignal(conv.id, inFlight.evidentMessageId, "gave_up", {
10421
- watched_for_ms: this.now() - inFlight.processingAnchorMs
10422
- });
10423
- this.removeInFlight(watcher, inFlight.evidentMessageId);
10786
+ if (watcher.inFlight.has(inFlight.evidentMessageId) && activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
10787
+ await this.settleAbsoluteCeiling(sessionId, watcher, messages, ceilingAttemptsThisTick);
10424
10788
  return;
10425
10789
  }
10426
10790
  const heartbeatDue = activelyRunning && !inFlight.awaitingHumanLatched && !inFlight.aliveInFlight && this.now() - inFlight.lastAliveAt >= HEARTBEAT_MS;
@@ -10648,7 +11012,8 @@ var ChannelDriver = class _ChannelDriver {
10648
11012
  usage,
10649
11013
  void 0,
10650
11014
  usageAgentName,
10651
- subagentInvocations
11015
+ subagentInvocations,
11016
+ true
10652
11017
  );
10653
11018
  } catch (err) {
10654
11019
  if (err instanceof ChannelAuthError) throw err;
@@ -11042,6 +11407,33 @@ var ChannelDriver = class _ChannelDriver {
11042
11407
  }
11043
11408
  const restartAborted = state === "failed" && sessionOngoing === false && isAbortedTerminalReply(messages, ocId ?? "");
11044
11409
  if (restartAborted) {
11410
+ const age = this.absoluteAgeForRow(row.processing_started_at);
11411
+ if (age.tripped && age.anchorMs !== null) {
11412
+ try {
11413
+ await this.markFailed(
11414
+ row.conversation_id,
11415
+ row.id,
11416
+ sessionId,
11417
+ this.absoluteCeilingError(row.id, sessionId, age.anchorMs),
11418
+ void 0,
11419
+ {
11420
+ classification: null,
11421
+ errorShape: extractReplyErrorShape(findLastAssistantReplyFor(messages, ocId ?? ""))
11422
+ }
11423
+ );
11424
+ } catch (err) {
11425
+ if (err instanceof ChannelAuthError) throw err;
11426
+ this.log({
11427
+ level: "warn",
11428
+ message: `Re-adopt: failed to report past-ceiling restart orphan ${row.id.slice(0, 8)} terminal; refusing to re-dispatch: ${err instanceof Error ? err.message : String(err)}`,
11429
+ conversation_id: row.conversation_id,
11430
+ message_id: row.id
11431
+ });
11432
+ return;
11433
+ }
11434
+ void this.postSignal(row.conversation_id, row.id, "redrive_ceiling_refused");
11435
+ return;
11436
+ }
11045
11437
  this.log({
11046
11438
  level: "info",
11047
11439
  message: `Re-adopt: message ${row.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check \u2014 restart orphan, re-dispatching instead of marking it permanently failed`,
@@ -11587,7 +11979,8 @@ var ChannelDriver = class _ChannelDriver {
11587
11979
  source_message_id: row.source_message_id,
11588
11980
  slack_user_id: row.slack_user_id,
11589
11981
  attachments: row.attachments ?? null,
11590
- opencode_message_id: row.opencode_message_id
11982
+ opencode_message_id: row.opencode_message_id,
11983
+ processing_started_at: row.processing_started_at
11591
11984
  };
11592
11985
  }
11593
11986
  /**
@@ -12494,7 +12887,7 @@ var ChannelDriver = class _ChannelDriver {
12494
12887
  * exists but is wedged, so the next attempt must get a fresh one
12495
12888
  * instead of reusing it — see WI-1's server-side null-clearing PATCH).
12496
12889
  */
12497
- async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
12890
+ async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations, abandonedWithoutAnswer) {
12498
12891
  const body = { status: "failed" };
12499
12892
  if (sessionId === null) {
12500
12893
  body.opencode_session_id = null;
@@ -12507,12 +12900,19 @@ var ChannelDriver = class _ChannelDriver {
12507
12900
  if (subagentInvocations && subagentInvocations.length > 0) {
12508
12901
  body.subagent_invocations = subagentInvocations;
12509
12902
  }
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;
12903
+ if (failure && failure.errorShape.name !== null) {
12904
+ body.failure_error_name = failure.errorShape.name;
12515
12905
  }
12906
+ if (failure && failure.errorShape.statusCode !== null) {
12907
+ body.failure_status_code = failure.errorShape.statusCode;
12908
+ }
12909
+ if (failure?.classification) {
12910
+ body.failure_kind = failure.classification.kind;
12911
+ body.failure_provider_id = failure.classification.providerId;
12912
+ body.failure_model_id = failure.classification.modelId;
12913
+ body.failure_reason = failure.classification.reason;
12914
+ }
12915
+ if (abandonedWithoutAnswer) body.abandoned_without_answer = true;
12516
12916
  try {
12517
12917
  await this.callWithRetry(
12518
12918
  "marking message as failed",
@@ -12532,27 +12932,28 @@ var ChannelDriver = class _ChannelDriver {
12532
12932
  this.clearSubagentInvocationCaches(messageId);
12533
12933
  }
12534
12934
  /**
12535
- * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
12935
+ * Classify an errored turn and extract its bounded error shape for `markFailed`.
12536
12936
  *
12537
12937
  * `messageFailure` alone (structured OpenCode error → `model_auth`) covers
12538
12938
  * most cases; when it returns `null` on this ALREADY-FAILED turn, fall back
12539
12939
  * to the P1-2b zero-provider check — one extra loopback call to
12540
12940
  * `hasAnyConfiguredProvider`, only reached when the structured classifier
12541
12941
  * 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.
12942
+ * (`null`/indeterminate) leaves the classification `null`; the raw error shape
12943
+ * is still retained for diagnostics.
12544
12944
  */
12545
12945
  async classifyModelAuthFailure(messages, userMessageId) {
12546
- const classified = messageFailure(messages, userMessageId);
12547
- if (classified != null) return classified;
12548
12946
  const reply = findLastAssistantReplyFor(messages, userMessageId);
12549
- const hasProvider = await this.hasAnyConfiguredProvider();
12550
- return applyZeroProviderFallback(
12947
+ const errorShape = extractReplyErrorShape(reply);
12948
+ const classified = messageFailure(messages, userMessageId);
12949
+ if (classified != null) return { classification: classified, errorShape };
12950
+ const classification = applyZeroProviderFallback(
12551
12951
  classified,
12552
- hasProvider,
12952
+ await this.hasAnyConfiguredProvider(),
12553
12953
  reply?.info?.providerID ?? null,
12554
12954
  reply?.info?.modelID ?? null
12555
12955
  );
12956
+ return { classification, errorShape };
12556
12957
  }
12557
12958
  async recordSubagentModelAuthFailure(failure, conversationId, messageId) {
12558
12959
  const providerId = failure.providerId ?? "(unknown)";
@@ -13410,6 +13811,159 @@ function applyRunnerOpenCodeConfig({
13410
13811
  log3(`Applied runner OpenCode config ${source} to ${join11(cwd, target)}`);
13411
13812
  }
13412
13813
 
13814
+ // src/lib/opencode/runtime-provenance.ts
13815
+ import { readFileSync as readFileSync7, readdirSync } from "node:fs";
13816
+ import { homedir as homedir7 } from "node:os";
13817
+ import { join as join12 } from "node:path";
13818
+ var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
13819
+ var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
13820
+ function errorKind(error2) {
13821
+ if (!(error2 instanceof Error)) return "unknown error";
13822
+ const code = "code" in error2 && typeof error2.code === "string" ? ` (${error2.code})` : "";
13823
+ return `${error2.name}${code}`;
13824
+ }
13825
+ function parseRegistryPluginSpec(value) {
13826
+ const separator = value.lastIndexOf("@");
13827
+ if (separator <= 0) return null;
13828
+ const name = value.slice(0, separator);
13829
+ const spec = value.slice(separator + 1);
13830
+ if (!PACKAGE_NAME.test(name) || !REGISTRY_SPEC.test(spec)) return null;
13831
+ return { name, spec };
13832
+ }
13833
+ function parseJsonc(value) {
13834
+ let output = "";
13835
+ let inString = false;
13836
+ let escaped = false;
13837
+ let lineComment = false;
13838
+ let blockComment = false;
13839
+ for (let index = 0; index < value.length; index++) {
13840
+ const character = value[index];
13841
+ const next = value[index + 1];
13842
+ if (lineComment) {
13843
+ if (character === "\n") {
13844
+ lineComment = false;
13845
+ output += character;
13846
+ }
13847
+ continue;
13848
+ }
13849
+ if (blockComment) {
13850
+ if (character === "*" && next === "/") {
13851
+ blockComment = false;
13852
+ index++;
13853
+ } else if (character === "\n") {
13854
+ output += character;
13855
+ }
13856
+ continue;
13857
+ }
13858
+ if (inString) {
13859
+ output += character;
13860
+ if (escaped) escaped = false;
13861
+ else if (character === "\\") escaped = true;
13862
+ else if (character === '"') inString = false;
13863
+ continue;
13864
+ }
13865
+ if (character === '"') {
13866
+ inString = true;
13867
+ output += character;
13868
+ } else if (character === "/" && next === "/") {
13869
+ lineComment = true;
13870
+ index++;
13871
+ } else if (character === "/" && next === "*") {
13872
+ blockComment = true;
13873
+ index++;
13874
+ } else if (character === ",") {
13875
+ let lookahead = index + 1;
13876
+ while (/\s/.test(value[lookahead] ?? "")) lookahead++;
13877
+ if (value[lookahead] !== "]" && value[lookahead] !== "}") output += character;
13878
+ } else {
13879
+ output += character;
13880
+ }
13881
+ }
13882
+ return JSON.parse(output);
13883
+ }
13884
+ function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
13885
+ return join12(env.XDG_CACHE_HOME ?? join12(homeDir, ".cache"), "opencode");
13886
+ }
13887
+ function readConfiguredPluginSpecs({
13888
+ cwd = process.cwd(),
13889
+ log: log3
13890
+ }) {
13891
+ let config;
13892
+ try {
13893
+ try {
13894
+ config = parseJsonc(readFileSync7(join12(cwd, "opencode.jsonc"), "utf8"));
13895
+ } catch (error2) {
13896
+ if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
13897
+ config = parseJsonc(readFileSync7(join12(cwd, "opencode.json"), "utf8"));
13898
+ }
13899
+ } catch (error2) {
13900
+ log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
13901
+ return [];
13902
+ }
13903
+ if (typeof config !== "object" || config === null || !("plugin" in config)) return [];
13904
+ const plugins = config.plugin;
13905
+ if (!Array.isArray(plugins)) {
13906
+ log3("Could not read OpenCode plugin configuration: plugin is not an array");
13907
+ return [];
13908
+ }
13909
+ const configured = plugins.filter(
13910
+ (plugin) => typeof plugin === "string" && parseRegistryPluginSpec(plugin) !== null
13911
+ );
13912
+ if (configured.length !== plugins.length) {
13913
+ log3(
13914
+ `Skipped ${plugins.length - configured.length} non-registry OpenCode plugin spec(s); their values were not recorded`
13915
+ );
13916
+ }
13917
+ return configured;
13918
+ }
13919
+ function resolveInstalledPluginVersions({
13920
+ cacheRoot,
13921
+ env = process.env,
13922
+ homeDir = homedir7(),
13923
+ log: log3
13924
+ }) {
13925
+ const packagesRoot = join12(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
13926
+ let directories;
13927
+ try {
13928
+ directories = readdirSync(packagesRoot, { withFileTypes: true });
13929
+ } catch (error2) {
13930
+ if ("code" in Object(error2) && Object(error2).code === "ENOENT") return [];
13931
+ log3(`Could not inspect the OpenCode plugin cache: ${errorKind(error2)}`);
13932
+ return [];
13933
+ }
13934
+ const plugins = [];
13935
+ let skipped = 0;
13936
+ for (const directory of directories) {
13937
+ if (!directory.isDirectory()) continue;
13938
+ const parsed = parseRegistryPluginSpec(directory.name);
13939
+ if (parsed === null) {
13940
+ skipped++;
13941
+ continue;
13942
+ }
13943
+ let resolvedVersion = null;
13944
+ try {
13945
+ const packageJson = JSON.parse(
13946
+ readFileSync7(
13947
+ join12(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
13948
+ "utf8"
13949
+ )
13950
+ );
13951
+ if (typeof packageJson.version === "string" && REGISTRY_SPEC.test(packageJson.version)) {
13952
+ resolvedVersion = packageJson.version;
13953
+ } else {
13954
+ log3(`Could not resolve OpenCode plugin ${parsed.name}@${parsed.spec}: invalid version`);
13955
+ }
13956
+ } catch (error2) {
13957
+ log3(`Could not resolve OpenCode plugin ${parsed.name}@${parsed.spec}: ${errorKind(error2)}`);
13958
+ }
13959
+ plugins.push({ ...parsed, resolvedVersion });
13960
+ }
13961
+ if (skipped > 0) {
13962
+ log3(`Skipped ${skipped} non-registry OpenCode package cache entry; its value was not recorded`);
13963
+ }
13964
+ return plugins;
13965
+ }
13966
+
13413
13967
  // src/lib/credential-sync.ts
13414
13968
  import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
13415
13969
  var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
@@ -13649,6 +14203,8 @@ function createCredentialSync({
13649
14203
  var MAX_ACTIVITY_LOG_ENTRIES = 10;
13650
14204
  var MAX_DISCONNECT_REASON_LENGTH = 200;
13651
14205
  var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
14206
+ var CREDENTIAL_ADOPTION_PROBE_ATTEMPTS = 3;
14207
+ var CREDENTIAL_ADOPTION_PROBE_INTERVAL_MS = 1e3;
13652
14208
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
13653
14209
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
13654
14210
  var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
@@ -13683,7 +14239,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
13683
14239
  if (trimmed === "") {
13684
14240
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
13685
14241
  }
13686
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join12(homeDir, trimmed.slice(2)) : trimmed;
14242
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join13(homeDir, trimmed.slice(2)) : trimmed;
13687
14243
  if (!isAbsolute4(expanded)) {
13688
14244
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
13689
14245
  }
@@ -13838,7 +14394,7 @@ function logActivity(state, entry) {
13838
14394
  }
13839
14395
  function reportSessionDbRecovery(state) {
13840
14396
  try {
13841
- const report = drainSessionDbRecoveryReport({ homeDir: homedir7(), env: process.env });
14397
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir8(), env: process.env });
13842
14398
  for (const record of report.records) {
13843
14399
  const activity = buildSessionDbRecoveryActivity(record);
13844
14400
  if (!activity) throw new Error("could not map session-DB recovery record");
@@ -13919,7 +14475,7 @@ async function importOpenCodeV2CredentialsForState(state) {
13919
14475
  }
13920
14476
  const result = await importOpenCodeV2Credentials({
13921
14477
  request: client.request,
13922
- homeDir: homedir7(),
14478
+ homeDir: homedir8(),
13923
14479
  directory,
13924
14480
  guard: state.opencodeV2CredentialImportGuard,
13925
14481
  log: (message, level = "info") => logActivity(state, { type: "info", level, message })
@@ -13996,6 +14552,125 @@ async function handleAuthError(state, error2) {
13996
14552
  return { success: false };
13997
14553
  }
13998
14554
  }
14555
+ function logCredentialAdoptionActivity(state, activity, level) {
14556
+ const message = (() => {
14557
+ switch (activity.outcome) {
14558
+ case "deferred":
14559
+ return `CREDENTIAL-ADOPTION: deferred generation ${activity.generation} with ${activity.active_sessions} active session(s)`;
14560
+ case "reloaded_ready":
14561
+ return `CREDENTIAL-ADOPTION: adopted generation ${activity.generation} with ${activity.active_sessions} active session(s); readiness probe succeeded after ${activity.probe_attempts} attempt(s)`;
14562
+ case "reloaded_interrupted": {
14563
+ 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`;
14564
+ return `CREDENTIAL-ADOPTION: generation ${activity.generation} with ${activity.active_sessions} active session(s); replacement instance answered ${response} after ${activity.probe_attempts} probe attempt(s)`;
14565
+ }
14566
+ case "reload_failed":
14567
+ return `CREDENTIAL-ADOPTION: failed to adopt generation ${activity.generation} with ${activity.active_sessions} active session(s)`;
14568
+ case "skipped_v2":
14569
+ return `CREDENTIAL-ADOPTION: skipped generation ${activity.generation} with ${activity.active_sessions} active session(s) on OpenCode V2`;
14570
+ }
14571
+ })();
14572
+ logActivity(state, {
14573
+ type: level === "error" ? "error" : "info",
14574
+ level,
14575
+ ...level === "error" ? { error: message } : { message },
14576
+ metadata: {
14577
+ ...activity,
14578
+ opencode_version: state.opencodeVersion,
14579
+ port: state.port
14580
+ }
14581
+ });
14582
+ }
14583
+ async function adoptCredentialGeneration(state, generation, adoption, activeSessions, deferredForMs) {
14584
+ const reload = await disposeAndReloadProviderConfig(
14585
+ state.port,
14586
+ state.opencodeClient ?? void 0
14587
+ );
14588
+ if (reload.outcome === "failed") {
14589
+ logCredentialAdoptionActivity(
14590
+ state,
14591
+ {
14592
+ outcome: "reload_failed",
14593
+ generation,
14594
+ active_sessions: activeSessions,
14595
+ adoption,
14596
+ deferred_for_ms: deferredForMs,
14597
+ reload_status: reload.status,
14598
+ reload_detail: reload.detail,
14599
+ probe_attempts: null,
14600
+ probe_status: null,
14601
+ probe_body_empty: null,
14602
+ probe_detail: null
14603
+ },
14604
+ "error"
14605
+ );
14606
+ return;
14607
+ }
14608
+ if (reload.outcome === "skipped_v2") {
14609
+ logCredentialAdoptionActivity(
14610
+ state,
14611
+ {
14612
+ outcome: "skipped_v2",
14613
+ generation,
14614
+ active_sessions: activeSessions,
14615
+ adoption,
14616
+ deferred_for_ms: deferredForMs,
14617
+ reload_status: null,
14618
+ reload_detail: null,
14619
+ probe_attempts: null,
14620
+ probe_status: null,
14621
+ probe_body_empty: null,
14622
+ probe_detail: null
14623
+ },
14624
+ "debug"
14625
+ );
14626
+ return;
14627
+ }
14628
+ let lastProbe = null;
14629
+ let attempts = 0;
14630
+ for (attempts = 1; attempts <= CREDENTIAL_ADOPTION_PROBE_ATTEMPTS; attempts++) {
14631
+ lastProbe = await probeOpenCodeRequestReadiness(state.port, state.opencodeClient ?? void 0);
14632
+ if (lastProbe.ok) {
14633
+ logCredentialAdoptionActivity(
14634
+ state,
14635
+ {
14636
+ outcome: "reloaded_ready",
14637
+ generation,
14638
+ active_sessions: activeSessions,
14639
+ adoption,
14640
+ deferred_for_ms: deferredForMs,
14641
+ reload_status: null,
14642
+ reload_detail: null,
14643
+ probe_attempts: attempts,
14644
+ probe_status: lastProbe.status,
14645
+ probe_body_empty: lastProbe.bodyEmpty,
14646
+ probe_detail: lastProbe.detail
14647
+ },
14648
+ "warn"
14649
+ );
14650
+ return;
14651
+ }
14652
+ if (attempts < CREDENTIAL_ADOPTION_PROBE_ATTEMPTS) {
14653
+ await new Promise((resolve5) => setTimeout(resolve5, CREDENTIAL_ADOPTION_PROBE_INTERVAL_MS));
14654
+ }
14655
+ }
14656
+ logCredentialAdoptionActivity(
14657
+ state,
14658
+ {
14659
+ outcome: "reloaded_interrupted",
14660
+ generation,
14661
+ active_sessions: activeSessions,
14662
+ adoption,
14663
+ deferred_for_ms: deferredForMs,
14664
+ reload_status: null,
14665
+ reload_detail: null,
14666
+ probe_attempts: CREDENTIAL_ADOPTION_PROBE_ATTEMPTS,
14667
+ probe_status: lastProbe?.status ?? null,
14668
+ probe_body_empty: lastProbe?.bodyEmpty ?? null,
14669
+ probe_detail: lastProbe?.detail ?? null
14670
+ },
14671
+ "error"
14672
+ );
14673
+ }
13999
14674
  async function driveChannels(state, driver) {
14000
14675
  let idlePolls = 0;
14001
14676
  let idleMs = 0;
@@ -14005,6 +14680,15 @@ async function driveChannels(state, driver) {
14005
14680
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
14006
14681
  let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
14007
14682
  let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
14683
+ const credentialAdoption = {
14684
+ generation: 0,
14685
+ pendingGeneration: null,
14686
+ pendingSinceMs: null,
14687
+ deferredGeneration: null,
14688
+ adoption: 0
14689
+ };
14690
+ const stayAwakeHolds = () => state.stayAwakeUntilMs !== null && Date.now() < state.stayAwakeUntilMs;
14691
+ let stayAwakeSuppressionLogged = false;
14008
14692
  while (state.running) {
14009
14693
  const cycleStartedAtMs = performance.now();
14010
14694
  let idleThisCycle = false;
@@ -14046,12 +14730,30 @@ async function driveChannels(state, driver) {
14046
14730
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
14047
14731
  if (opencodeAuthApplied) state.openaiUsageRearm?.();
14048
14732
  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
- );
14733
+ credentialAdoption.generation++;
14734
+ if (state.opencodeApiVersion === "v2") {
14735
+ logCredentialAdoptionActivity(
14736
+ state,
14737
+ {
14738
+ outcome: "skipped_v2",
14739
+ generation: credentialAdoption.generation,
14740
+ active_sessions: driver.protectedSessionIds().size,
14741
+ adoption: null,
14742
+ deferred_for_ms: null,
14743
+ reload_status: null,
14744
+ reload_detail: null,
14745
+ probe_attempts: null,
14746
+ probe_status: null,
14747
+ probe_body_empty: null,
14748
+ probe_detail: null
14749
+ },
14750
+ "debug"
14751
+ );
14752
+ } else {
14753
+ credentialAdoption.pendingGeneration = credentialAdoption.generation;
14754
+ credentialAdoption.pendingSinceMs = Date.now();
14755
+ credentialAdoption.deferredGeneration = null;
14756
+ }
14055
14757
  }
14056
14758
  if (opencodeAuthApplied && state.opencodeApiVersion === "v2") {
14057
14759
  void importOpenCodeV2CredentialsForState(state).catch(
@@ -14106,6 +14808,49 @@ async function driveChannels(state, driver) {
14106
14808
  }
14107
14809
  }
14108
14810
  }
14811
+ if (credentialAdoption.pendingGeneration !== null) {
14812
+ const activeSessions = driver.protectedSessionIds();
14813
+ const generation = credentialAdoption.pendingGeneration;
14814
+ if (activeSessions.size > 0) {
14815
+ if (credentialAdoption.deferredGeneration !== generation) {
14816
+ credentialAdoption.deferredGeneration = generation;
14817
+ logCredentialAdoptionActivity(
14818
+ state,
14819
+ {
14820
+ outcome: "deferred",
14821
+ generation,
14822
+ active_sessions: activeSessions.size,
14823
+ adoption: null,
14824
+ deferred_for_ms: null,
14825
+ reload_status: null,
14826
+ reload_detail: null,
14827
+ probe_attempts: null,
14828
+ probe_status: null,
14829
+ probe_body_empty: null,
14830
+ probe_detail: null
14831
+ },
14832
+ "warn"
14833
+ );
14834
+ }
14835
+ } else {
14836
+ const deferredForMs = credentialAdoption.deferredGeneration === generation && credentialAdoption.pendingSinceMs !== null ? Math.max(0, Date.now() - credentialAdoption.pendingSinceMs) : null;
14837
+ credentialAdoption.pendingGeneration = null;
14838
+ credentialAdoption.pendingSinceMs = null;
14839
+ credentialAdoption.adoption++;
14840
+ void adoptCredentialGeneration(
14841
+ state,
14842
+ generation,
14843
+ credentialAdoption.adoption,
14844
+ activeSessions.size,
14845
+ deferredForMs
14846
+ ).catch(
14847
+ (error2) => logActivity(state, {
14848
+ type: "error",
14849
+ 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)}`
14850
+ })
14851
+ );
14852
+ }
14853
+ }
14109
14854
  await new Promise((resolve5) => setTimeout(resolve5, CHANNEL_POLL_INTERVAL_MS));
14110
14855
  const cycleMs = performance.now() - cycleStartedAtMs;
14111
14856
  if (idleThisCycle) idleMs += cycleMs;
@@ -14119,11 +14864,20 @@ async function driveChannels(state, driver) {
14119
14864
  if (state.interactive) displayStatus(state);
14120
14865
  break;
14121
14866
  }
14122
- if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3) {
14867
+ if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3 && !stayAwakeHolds()) {
14123
14868
  logActivity(state, { type: "info", message: "Idle timeout reached" });
14124
14869
  if (state.interactive) displayStatus(state);
14125
14870
  break;
14126
14871
  }
14872
+ if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3 && stayAwakeHolds()) {
14873
+ if (!stayAwakeSuppressionLogged) {
14874
+ stayAwakeSuppressionLogged = true;
14875
+ logActivity(state, {
14876
+ type: "info",
14877
+ message: "Idle, but staying awake until the requested minimum passes"
14878
+ });
14879
+ }
14880
+ }
14127
14881
  }
14128
14882
  }
14129
14883
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
@@ -14141,7 +14895,7 @@ function shouldWarnForReclaimSkip(reason) {
14141
14895
  return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
14142
14896
  }
14143
14897
  function sessionDbPath() {
14144
- return join12(homedir7(), ".local", "share", "opencode", "opencode.db");
14898
+ return join13(homedir8(), ".local", "share", "opencode", "opencode.db");
14145
14899
  }
14146
14900
  function logSessionCleanupActivity(state, phase, entry) {
14147
14901
  logActivity(state, {
@@ -14385,7 +15139,7 @@ function scheduleSessionCleanup(state, driver, options) {
14385
15139
  for (const warning2 of config.warnings) {
14386
15140
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
14387
15141
  }
14388
- const dbBytes = statSessionDbBytes(homedir7());
15142
+ const dbBytes = statSessionDbBytes(homedir8());
14389
15143
  void (async () => {
14390
15144
  const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
14391
15145
  if (reclaimAvailability !== null) {
@@ -14452,10 +15206,12 @@ function scheduleUsageReporting(state, params) {
14452
15206
  let phase = "dormant";
14453
15207
  let rearmRequested = false;
14454
15208
  const armProbe = () => {
15209
+ if (!state.running) return;
14455
15210
  phase = "probe-pending";
14456
15211
  params.setTimer(setTimeout(() => void tick(true), params.firstDelayMs()));
14457
15212
  };
14458
15213
  const scheduleNextTick = () => {
15214
+ if (!state.running) return;
14459
15215
  if (rearmRequested) {
14460
15216
  rearmRequested = false;
14461
15217
  armProbe();
@@ -14489,6 +15245,7 @@ function scheduleUsageReporting(state, params) {
14489
15245
  phase = "tick-in-flight";
14490
15246
  try {
14491
15247
  const usage = await params.fetchUsage();
15248
+ if (!state.running) return;
14492
15249
  const result = await params.report(usage);
14493
15250
  if (result.ok) {
14494
15251
  if (consecutiveFailures > 0) {
@@ -14601,7 +15358,7 @@ function scheduleResourceUsageReporting(state, options) {
14601
15358
  });
14602
15359
  return;
14603
15360
  }
14604
- const { collect, stop } = createResourceUsageCollector(homedir7());
15361
+ const { collect, stop } = createResourceUsageCollector(homedir8());
14605
15362
  state.stopResourceUsageSampling = stop;
14606
15363
  let consecutiveFailures = 0;
14607
15364
  const tick = async () => {
@@ -14627,6 +15384,7 @@ function scheduleResourceUsageReporting(state, options) {
14627
15384
  message: lowDiskDecision.warning
14628
15385
  });
14629
15386
  }
15387
+ if (!state.running) return;
14630
15388
  const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
14631
15389
  if (result.ok) {
14632
15390
  if (consecutiveFailures > 0) {
@@ -14659,7 +15417,9 @@ function scheduleResourceUsageReporting(state, options) {
14659
15417
  message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
14660
15418
  });
14661
15419
  } finally {
14662
- state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
15420
+ if (state.running) {
15421
+ state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
15422
+ }
14663
15423
  }
14664
15424
  };
14665
15425
  state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
@@ -14839,7 +15599,7 @@ async function run(options) {
14839
15599
  let fileSyncDirectories;
14840
15600
  try {
14841
15601
  logLevel = resolveLogLevel(options);
14842
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir7());
15602
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir8());
14843
15603
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
14844
15604
  throw new Error(
14845
15605
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -14862,6 +15622,8 @@ async function run(options) {
14862
15622
  port: options.port ?? 4096,
14863
15623
  conversationFilter: options.conversation ?? null,
14864
15624
  idleTimeout: options.idleTimeout ?? null,
15625
+ stayAwakeSeconds: null,
15626
+ stayAwakeUntilMs: null,
14865
15627
  json: options.json ?? false,
14866
15628
  interactive,
14867
15629
  logLevel,
@@ -15051,6 +15813,17 @@ async function run(options) {
15051
15813
  }
15052
15814
  spinner?.succeed(`Runner: ${validation.agent.name || state.agentId}`);
15053
15815
  state.agentName = validation.agent.name;
15816
+ const requestedStayAwakeSeconds = validation.agent?.wake_stay_awake_seconds;
15817
+ if (typeof requestedStayAwakeSeconds === "number" && Number.isFinite(requestedStayAwakeSeconds) && requestedStayAwakeSeconds > 0) {
15818
+ state.stayAwakeSeconds = requestedStayAwakeSeconds;
15819
+ } else if (requestedStayAwakeSeconds !== void 0 && requestedStayAwakeSeconds !== null) {
15820
+ state.stayAwakeSeconds = null;
15821
+ log2(
15822
+ state,
15823
+ `Ignoring invalid wake_stay_awake_seconds (type: ${typeof requestedStayAwakeSeconds})`,
15824
+ "warn"
15825
+ );
15826
+ }
15054
15827
  const microvmId = process.env.MICROVM_ID?.trim();
15055
15828
  if (microvmId) {
15056
15829
  const reported = await reportMicrovmId(state.agentId, state.authHeader, microvmId);
@@ -15112,7 +15885,7 @@ async function run(options) {
15112
15885
  state.credentialSync?.arm();
15113
15886
  const dependencyInstallDiagnosticReporter = createDependencyInstallDiagnosticReporter({
15114
15887
  context: {
15115
- homeDir: homedir7(),
15888
+ homeDir: homedir8(),
15116
15889
  cwd: process.cwd(),
15117
15890
  microvmId: process.env.MICROVM_ID?.trim(),
15118
15891
  euid: process.getuid?.(),
@@ -15135,7 +15908,7 @@ async function run(options) {
15135
15908
  dependencyInstallDiagnosticReporter
15136
15909
  );
15137
15910
  const openCodeLogTail = tailOpenCodeLogFile(
15138
- resolveOpenCodeLogPath(homedir7(), process.env),
15911
+ resolveOpenCodeLogPath(homedir8(), process.env),
15139
15912
  openCodeActivityForwarder
15140
15913
  );
15141
15914
  state.stopOpenCodeLogTail = () => {
@@ -15236,6 +16009,32 @@ async function run(options) {
15236
16009
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
15237
16010
  state.opencodeVersion = oc.version;
15238
16011
  state.opencodeApiVersion = opencodeVersion;
16012
+ let configuredPlugins = [];
16013
+ let resolvedPlugins = [];
16014
+ try {
16015
+ const provenanceLog = (message) => logActivity(state, { type: "info", level: "warn", message });
16016
+ configuredPlugins = readConfiguredPluginSpecs({ log: provenanceLog });
16017
+ resolvedPlugins = resolveInstalledPluginVersions({ log: provenanceLog });
16018
+ } catch (error2) {
16019
+ const kind = error2 instanceof Error ? error2.name : "unknown error";
16020
+ logActivity(state, {
16021
+ type: "info",
16022
+ level: "warn",
16023
+ message: `Could not collect OpenCode runtime provenance: ${kind}`
16024
+ });
16025
+ }
16026
+ logActivity(state, {
16027
+ type: "info",
16028
+ level: "info",
16029
+ message: "OpenCode runtime provenance",
16030
+ metadata: {
16031
+ opencode_version: state.opencodeVersion,
16032
+ opencode_api_version: state.opencodeApiVersion,
16033
+ configured_plugins: JSON.stringify(configuredPlugins),
16034
+ resolved_plugins: JSON.stringify(resolvedPlugins)
16035
+ },
16036
+ source: "cli.runtime-provenance"
16037
+ });
15239
16038
  let opencodePassword = null;
15240
16039
  if (opencodeVersion === "v2" && "password" in oc) {
15241
16040
  const value = oc.password;
@@ -15264,7 +16063,7 @@ async function run(options) {
15264
16063
  const provenance = checkSessionDbProvenance({
15265
16064
  dbPath: sessionDbPath(),
15266
16065
  currentVersion: state.opencodeVersion,
15267
- homeDir: homedir7(),
16066
+ homeDir: homedir8(),
15268
16067
  env: process.env
15269
16068
  });
15270
16069
  if (provenance.anomaly) {
@@ -15294,7 +16093,7 @@ async function run(options) {
15294
16093
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
15295
16094
  }
15296
16095
  }
15297
- await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
16096
+ await disposeAndReloadProviderConfig(state.port, state.opencodeClient ?? void 0);
15298
16097
  if (state.opencodeApiVersion === "v2" && state.opencodeClient !== null) {
15299
16098
  await importOpenCodeV2CredentialsForState(state);
15300
16099
  }
@@ -15336,7 +16135,7 @@ async function run(options) {
15336
16135
  let existingPid;
15337
16136
  if (existsSync3(options.litestreamPidFile)) {
15338
16137
  try {
15339
- const rawPid = readFileSync7(options.litestreamPidFile, "utf8").trim();
16138
+ const rawPid = readFileSync8(options.litestreamPidFile, "utf8").trim();
15340
16139
  const parsedPid = Number(rawPid);
15341
16140
  if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
15342
16141
  existingPid = parsedPid;
@@ -15435,7 +16234,7 @@ async function run(options) {
15435
16234
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
15436
16235
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
15437
16236
  fileSyncDirectories,
15438
- homeDir: homedir7(),
16237
+ homeDir: homedir8(),
15439
16238
  maxActiveSessions,
15440
16239
  log: (entry) => (
15441
16240
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -15465,6 +16264,13 @@ async function run(options) {
15465
16264
  type: "info",
15466
16265
  message: connectionMessage
15467
16266
  });
16267
+ if (!isReconnect && state.stayAwakeSeconds !== null && state.stayAwakeUntilMs === null) {
16268
+ state.stayAwakeUntilMs = Date.now() + state.stayAwakeSeconds * 1e3;
16269
+ logActivity(state, {
16270
+ type: "info",
16271
+ message: `Staying awake for at least ${state.stayAwakeSeconds}s as requested by the wake`
16272
+ });
16273
+ }
15468
16274
  if (options.tunnelReadyFile) {
15469
16275
  const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
15470
16276
  if (marker.ok) {
@@ -15599,6 +16405,7 @@ async function run(options) {
15599
16405
  if (error2.message === "Unauthorized") tunnelSpinner?.fail("Unauthorized");
15600
16406
  throw error2;
15601
16407
  }
16408
+ if (!state.running) return;
15602
16409
  scheduleSessionCleanup(state, channelDriver, options);
15603
16410
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
15604
16411
  state.openaiUsageRearm = scheduleUsageReporting(state, {