@evident-ai/cli 3.5.2-dev.ffa823b → 3.6.1-dev.1a9c5e7

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
@@ -100,6 +100,26 @@ function clearCredentials() {
100
100
  credentials.set("byEndpoint", byEndpoint);
101
101
  hardenCredentialsPermissions();
102
102
  }
103
+ function getManagedInferenceCredential(runnerId) {
104
+ hardenCredentialsPermissions();
105
+ return credentials.get("managedInferenceByEndpoint")?.[credentialsKey()]?.[runnerId];
106
+ }
107
+ function setManagedInferenceCredential(runnerId, credential) {
108
+ const byEndpoint = credentials.get("managedInferenceByEndpoint") ?? {};
109
+ byEndpoint[credentialsKey()] ??= {};
110
+ byEndpoint[credentialsKey()][runnerId] = credential;
111
+ credentials.set("managedInferenceByEndpoint", byEndpoint);
112
+ hardenCredentialsPermissions();
113
+ }
114
+ function clearManagedInferenceCredential(runnerId) {
115
+ const byEndpoint = credentials.get("managedInferenceByEndpoint") ?? {};
116
+ const runners = byEndpoint[credentialsKey()];
117
+ if (!runners || !(runnerId in runners)) return;
118
+ delete runners[runnerId];
119
+ if (Object.keys(runners).length === 0) delete byEndpoint[credentialsKey()];
120
+ credentials.set("managedInferenceByEndpoint", byEndpoint);
121
+ hardenCredentialsPermissions();
122
+ }
103
123
  function clearAllCredentials() {
104
124
  credentials.clear();
105
125
  hardenCredentialsPermissions();
@@ -119,13 +139,13 @@ function getCliName() {
119
139
  // src/lib/api.ts
120
140
  async function request(path, method, body, headers = {}) {
121
141
  const url = `${getApiUrlConfig()}${path}`;
122
- const requestHeaders = {
142
+ const requestHeaders2 = {
123
143
  "Content-Type": "application/json",
124
144
  ...headers
125
145
  };
126
146
  const response = await fetch(url, {
127
147
  method,
128
- headers: requestHeaders,
148
+ headers: requestHeaders2,
129
149
  body: body ? JSON.stringify(body) : void 0
130
150
  });
131
151
  if (!response.ok) {
@@ -1156,9 +1176,9 @@ async function claudeUsage() {
1156
1176
  }
1157
1177
 
1158
1178
  // 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";
1179
+ import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
1180
+ import { homedir as homedir8 } from "node:os";
1181
+ import { isAbsolute as isAbsolute4, join as join13, parse, resolve as resolvePath2 } from "node:path";
1162
1182
  import chalk7 from "chalk";
1163
1183
 
1164
1184
  // ../../packages/types/src/agents/index.ts
@@ -1287,13 +1307,113 @@ var TUNNEL_USAGE_REARM_PING_PATH = "/__evident/usage-rearm";
1287
1307
  var MAX_FILE_PUSH_BYTES = 64 * 1024;
1288
1308
  var MAX_FILE_SYNC_DIRECTORIES = 16;
1289
1309
 
1310
+ // ../../packages/types/src/credential-proxy-native-config.ts
1311
+ var EVIDENT_PROXY_TOKEN_REFERENCE = "{env:EVIDENT_PROXY_TOKEN}";
1312
+ var CLAUDE_PROVIDER = {
1313
+ namespace: "evident-claude",
1314
+ name: "Evident Claude credential proxy",
1315
+ npm: "@ai-sdk/anthropic"
1316
+ };
1317
+ var NATIVE_PROVIDERS = {
1318
+ claude: CLAUDE_PROVIDER,
1319
+ anthropic: CLAUDE_PROVIDER,
1320
+ codex: {
1321
+ namespace: "evident-codex",
1322
+ name: "Evident Codex credential proxy",
1323
+ npm: "@ai-sdk/openai"
1324
+ },
1325
+ openai: {
1326
+ namespace: "evident-openai",
1327
+ name: "Evident OpenAI credential proxy",
1328
+ npm: "@ai-sdk/openai"
1329
+ },
1330
+ kimi: {
1331
+ namespace: "evident-kimi",
1332
+ name: "Evident Kimi credential proxy",
1333
+ npm: "@ai-sdk/openai-compatible",
1334
+ sdkName: "evident-kimi"
1335
+ },
1336
+ antigravity: {
1337
+ namespace: "evident-antigravity",
1338
+ name: "Evident Antigravity credential proxy",
1339
+ npm: "@ai-sdk/openai-compatible",
1340
+ sdkName: "evident-antigravity"
1341
+ },
1342
+ xai: {
1343
+ namespace: "evident-xai",
1344
+ name: "Evident xAI credential proxy",
1345
+ npm: "@ai-sdk/openai-compatible",
1346
+ sdkName: "evident-xai"
1347
+ },
1348
+ mistral: {
1349
+ namespace: "evident-mistral",
1350
+ name: "Evident Mistral credential proxy",
1351
+ npm: "@ai-sdk/openai-compatible",
1352
+ sdkName: "evident-mistral"
1353
+ }
1354
+ };
1355
+ var CREDENTIAL_PROXY_NATIVE_NAMESPACES = [
1356
+ ...new Set(Object.values(NATIVE_PROVIDERS).map(({ namespace }) => namespace))
1357
+ ];
1358
+ function credentialProxyInferenceUrl(baseUrl) {
1359
+ const trimmed = baseUrl.trim().replace(/\/+$/, "");
1360
+ return /\/v1$/i.test(trimmed) ? trimmed : `${trimmed}/v1`;
1361
+ }
1362
+ function buildCredentialProxyNativeConfig({
1363
+ inferenceBaseUrl,
1364
+ modelDescriptors
1365
+ }) {
1366
+ const baseURL = credentialProxyInferenceUrl(inferenceBaseUrl);
1367
+ const providers = {};
1368
+ const unsupportedProviders = /* @__PURE__ */ new Set();
1369
+ for (const descriptor of modelDescriptors) {
1370
+ const definition = NATIVE_PROVIDERS[descriptor.provider.trim().toLowerCase()];
1371
+ if (!definition) {
1372
+ unsupportedProviders.add(descriptor.provider.trim() || descriptor.provider);
1373
+ continue;
1374
+ }
1375
+ providers[definition.namespace] ??= definition.npm === "@ai-sdk/anthropic" ? {
1376
+ name: definition.name,
1377
+ npm: definition.npm,
1378
+ options: { baseURL, authToken: EVIDENT_PROXY_TOKEN_REFERENCE },
1379
+ models: {}
1380
+ } : definition.npm === "@ai-sdk/openai" ? {
1381
+ name: definition.name,
1382
+ npm: definition.npm,
1383
+ options: { baseURL, apiKey: EVIDENT_PROXY_TOKEN_REFERENCE },
1384
+ models: {}
1385
+ } : {
1386
+ name: definition.name,
1387
+ npm: definition.npm,
1388
+ options: {
1389
+ name: definition.sdkName,
1390
+ baseURL,
1391
+ apiKey: EVIDENT_PROXY_TOKEN_REFERENCE
1392
+ },
1393
+ models: {}
1394
+ };
1395
+ providers[definition.namespace].models[descriptor.id] = { name: descriptor.id };
1396
+ }
1397
+ return {
1398
+ unsupportedProviders: [...unsupportedProviders],
1399
+ config: {
1400
+ $schema: "https://opencode.ai/config.json",
1401
+ provider: providers
1402
+ }
1403
+ };
1404
+ }
1405
+
1290
1406
  // ../../packages/types/src/logging/index.ts
1291
1407
  var CORRELATION_ID_HEADER = "x-evident-correlation-id";
1292
1408
  var FORWARD_FAILURE_REASON_HEADER = "X-Evident-Failure-Reason";
1293
1409
  function log(level, event, fields) {
1294
1410
  const method = level === "debug" ? "log" : level;
1295
1411
  try {
1296
- console[method]("[evident]", JSON.stringify({ level, event, ...fields }));
1412
+ const payload = Object.fromEntries(
1413
+ Object.entries({ level, event, ...fields }).filter(([, value]) => value !== void 0)
1414
+ );
1415
+ const secondArgument = globalThis.navigator?.userAgent === "Cloudflare-Workers" ? payload : JSON.stringify(payload);
1416
+ console[method]("[evident]", secondArgument);
1297
1417
  } catch (err) {
1298
1418
  console.error(
1299
1419
  "[evident] log_serialize_failed",
@@ -1322,7 +1442,7 @@ import ora4 from "ora";
1322
1442
  import { select as select4 } from "@inquirer/prompts";
1323
1443
 
1324
1444
  // src/lib/telemetry.ts
1325
- var CLI_VERSION = (true ? "3.5.2-dev.ffa823b" : void 0) ?? process.env.npm_package_version ?? "unknown";
1445
+ var CLI_VERSION = (true ? "3.6.1-dev.1a9c5e7" : void 0) ?? process.env.npm_package_version ?? "unknown";
1326
1446
  function getCliVersion() {
1327
1447
  return CLI_VERSION;
1328
1448
  }
@@ -1403,7 +1523,7 @@ async function flushEvents() {
1403
1523
  const controller = new AbortController();
1404
1524
  const timeout = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
1405
1525
  try {
1406
- const request2 = {
1526
+ const request3 = {
1407
1527
  events,
1408
1528
  client_type: "cli",
1409
1529
  client_version: CLI_VERSION
@@ -1414,7 +1534,7 @@ async function flushEvents() {
1414
1534
  "Content-Type": "application/json",
1415
1535
  Authorization: authHeader
1416
1536
  },
1417
- body: JSON.stringify(request2),
1537
+ body: JSON.stringify(request3),
1418
1538
  signal: controller.signal
1419
1539
  });
1420
1540
  if (!response.ok) {
@@ -2071,7 +2191,8 @@ function createDependencyInstallDiagnosticReporter({
2071
2191
  var FORWARDED_LEVELS_BY_SOURCE = {
2072
2192
  "cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
2073
2193
  opencode: /* @__PURE__ */ new Set(["warn", "error"]),
2074
- "cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"])
2194
+ "cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"]),
2195
+ "cli.runtime-provenance": /* @__PURE__ */ new Set(["info", "warn", "error"])
2075
2196
  };
2076
2197
  var SEVERITY_BY_LEVEL = {
2077
2198
  info: "info",
@@ -2867,6 +2988,58 @@ async function listAllSessions(port, client) {
2867
2988
  }
2868
2989
  return { sessions };
2869
2990
  }
2991
+ var INTERRUPT_NETWORK_CODES = /* @__PURE__ */ new Set([
2992
+ "ABORT_ERR",
2993
+ "EAI_AGAIN",
2994
+ "ECONNREFUSED",
2995
+ "ECONNRESET",
2996
+ "ENETUNREACH",
2997
+ "ENOTFOUND",
2998
+ "ETIMEDOUT",
2999
+ "UND_ERR_CONNECT_TIMEOUT",
3000
+ "UND_ERR_HEADERS_TIMEOUT",
3001
+ "UND_ERR_SOCKET"
3002
+ ]);
3003
+ function interruptFailureDiagnostic(error2) {
3004
+ const message = error2 instanceof Error ? error2.message : "";
3005
+ const httpStatus = /^OpenCode request failed: HTTP ([1-5]\d{2})(?::|$)/.exec(message)?.[1];
3006
+ if (httpStatus) return `HTTP ${httpStatus}`;
3007
+ const candidates = [error2, error2 instanceof Error ? error2.cause : void 0];
3008
+ for (const candidate of candidates) {
3009
+ if (typeof candidate !== "object" || candidate === null || !("code" in candidate)) continue;
3010
+ const code = candidate.code;
3011
+ if (typeof code === "string" && INTERRUPT_NETWORK_CODES.has(code)) {
3012
+ return redactDiagnosticText(`network ${code}`).slice(0, 120);
3013
+ }
3014
+ }
3015
+ const name = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
3016
+ return redactDiagnosticText(`request threw ${name}`).slice(0, 120);
3017
+ }
3018
+ async function interruptSession(port, id, client) {
3019
+ if (client?.version === "v2") {
3020
+ try {
3021
+ await interruptV2Session(client, id);
3022
+ return { kind: "confirmed_stopped" };
3023
+ } catch (error2) {
3024
+ return { kind: "not_confirmed", diagnostic: interruptFailureDiagnostic(error2) };
3025
+ }
3026
+ }
3027
+ try {
3028
+ const res = await requestWithClient(
3029
+ port,
3030
+ client,
3031
+ `/session/${id}/abort`,
3032
+ { method: "POST" },
3033
+ { allowStatuses: ALL_HTTP_STATUSES }
3034
+ );
3035
+ if (res.status >= 200 && res.status < 300) {
3036
+ return { kind: "confirmed_stopped", status: res.status };
3037
+ }
3038
+ return { kind: "not_confirmed", status: res.status };
3039
+ } catch (error2) {
3040
+ return { kind: "threw", diagnostic: interruptFailureDiagnostic(error2) };
3041
+ }
3042
+ }
2870
3043
  async function deleteSession(port, id, client) {
2871
3044
  if (client?.version === "v2") return deleteV2Session(client, id);
2872
3045
  try {
@@ -3036,6 +3209,52 @@ async function getModelAttachmentCapability(port, model, client) {
3036
3209
  return null;
3037
3210
  }
3038
3211
  }
3212
+ async function isPinnedProviderLoaded(port, model, client) {
3213
+ if (client?.version === "v2") return null;
3214
+ const { model: baseModel } = splitModelVariant(model);
3215
+ if (!baseModel) return null;
3216
+ const slash = baseModel.indexOf("/");
3217
+ if (slash <= 0) return null;
3218
+ const providerId = baseModel.slice(0, slash);
3219
+ try {
3220
+ const res = await requestWithClient(port, client, "/config/providers");
3221
+ if (!res.ok) {
3222
+ console.error(
3223
+ `[isPinnedProviderLoaded] GET /config/providers returned HTTP ${res.status} (port ${port})`
3224
+ );
3225
+ return null;
3226
+ }
3227
+ const body = await res.json();
3228
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
3229
+ console.error(
3230
+ `[isPinnedProviderLoaded] GET /config/providers body was not a plain object (port ${port})`
3231
+ );
3232
+ return null;
3233
+ }
3234
+ const providers = body.providers;
3235
+ if (!Array.isArray(providers)) {
3236
+ console.error(
3237
+ `[isPinnedProviderLoaded] GET /config/providers body had no providers array (port ${port})`
3238
+ );
3239
+ return null;
3240
+ }
3241
+ const providerIds = providers.filter(
3242
+ (provider) => provider !== null && typeof provider === "object" && typeof provider.id === "string"
3243
+ ).map((provider) => provider.id);
3244
+ if (providerIds.length === 0) {
3245
+ console.error(
3246
+ `[isPinnedProviderLoaded] GET /config/providers had no provider entries with string ids (port ${port})`
3247
+ );
3248
+ return null;
3249
+ }
3250
+ return providerIds.includes(providerId);
3251
+ } catch (err) {
3252
+ console.error(
3253
+ `[isPinnedProviderLoaded] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3254
+ );
3255
+ return null;
3256
+ }
3257
+ }
3039
3258
  async function buildFileParts(attachments, capable) {
3040
3259
  const outcomes = [];
3041
3260
  const parts = [];
@@ -3518,19 +3737,32 @@ function isAbortedTerminalReply(messages, userMessageId) {
3518
3737
  }
3519
3738
  return false;
3520
3739
  }
3740
+ var CLAUDE_AUTH_EXPIRY_MESSAGES = Object.freeze([
3741
+ "Claude Code credentials are unavailable or expired. Run `claude` to refresh them."
3742
+ ]);
3743
+ function extractReplyErrorShape(reply) {
3744
+ const error2 = errorOf(reply);
3745
+ if (error2 == null || typeof error2 !== "object") {
3746
+ return { name: null, statusCode: null };
3747
+ }
3748
+ const e = error2;
3749
+ const name = typeof e.name === "string" ? e.name.slice(0, 100) : null;
3750
+ const data = e.data;
3751
+ const statusCode = typeof data?.statusCode === "number" && Number.isFinite(data.statusCode) ? data.statusCode : null;
3752
+ return { name, statusCode };
3753
+ }
3521
3754
  function classifyReplyAuthError(reply) {
3522
3755
  const error2 = errorOf(reply);
3523
3756
  if (error2 == null || typeof error2 !== "object") return null;
3524
3757
  const e = error2;
3525
3758
  const replyProviderId = reply?.info?.providerID ?? null;
3526
3759
  const replyModelId = reply?.info?.modelID ?? null;
3760
+ const data = e.data;
3527
3761
  if (e.name === "ProviderAuthError") {
3528
- const data = e.data;
3529
3762
  const providerId = typeof data?.providerID === "string" && data.providerID || replyProviderId;
3530
3763
  return { kind: "model_auth", providerId, modelId: replyModelId, reason: "missing" };
3531
3764
  }
3532
3765
  if (e.name === "APIError") {
3533
- const data = e.data;
3534
3766
  const statusCode = data?.statusCode;
3535
3767
  if (statusCode === 401 || statusCode === 403) {
3536
3768
  return {
@@ -3541,11 +3773,23 @@ function classifyReplyAuthError(reply) {
3541
3773
  };
3542
3774
  }
3543
3775
  }
3776
+ const errorMessage3 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
3777
+ if (errorMessage3 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage3.includes(knownMessage))) {
3778
+ return {
3779
+ kind: "model_auth",
3780
+ providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
3781
+ modelId: replyModelId,
3782
+ reason: "rejected"
3783
+ };
3784
+ }
3544
3785
  return null;
3545
3786
  }
3546
3787
  function messageFailure(messages, userMessageId) {
3547
3788
  return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
3548
3789
  }
3790
+ function isProviderModelNotFoundError(reason) {
3791
+ return reason.includes("Model not found:");
3792
+ }
3549
3793
  function findLatestSubagentAuthOutcome(messages, sinceMs) {
3550
3794
  if (!messages || messages.length === 0) return null;
3551
3795
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -3754,23 +3998,49 @@ async function readSessionErrorStream(port, options, client) {
3754
3998
  if (reader) void reader.cancel().catch(() => void 0);
3755
3999
  }
3756
4000
  }
3757
- async function reloadProviderCache(port, client) {
3758
- if (client?.version === "v2") return;
4001
+ async function disposeAndReloadProviderConfig(port, client) {
4002
+ if (client?.version === "v2") return { outcome: "skipped_v2" };
3759
4003
  try {
3760
- const res = await requestWithClient(port, client, "/config", {
3761
- method: "PATCH",
3762
- headers: { "Content-Type": "application/json" },
3763
- body: JSON.stringify({})
3764
- });
4004
+ const res = await requestWithClient(
4005
+ port,
4006
+ client,
4007
+ "/config",
4008
+ {
4009
+ method: "PATCH",
4010
+ headers: { "Content-Type": "application/json" },
4011
+ body: JSON.stringify({})
4012
+ },
4013
+ { allowStatuses: ALL_HTTP_STATUSES }
4014
+ );
3765
4015
  if (!res.ok) {
3766
4016
  console.error(
3767
- `[reloadProviderCache] PATCH /config returned HTTP ${res.status} (port ${port})`
4017
+ `[disposeAndReloadProviderConfig] PATCH /config returned HTTP ${res.status} (port ${port})`
3768
4018
  );
4019
+ return { outcome: "failed", status: res.status, detail: null };
3769
4020
  }
4021
+ return { outcome: "reloaded" };
3770
4022
  } catch (err) {
3771
4023
  console.error(
3772
- `[reloadProviderCache] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
4024
+ `[disposeAndReloadProviderConfig] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3773
4025
  );
4026
+ return {
4027
+ outcome: "failed",
4028
+ status: null,
4029
+ detail: err instanceof Error ? err.message : String(err)
4030
+ };
4031
+ }
4032
+ }
4033
+ async function probeOpenCodeRequestReadiness(port, client) {
4034
+ try {
4035
+ const res = await requestWithClient(port, client, "/config", void 0, {
4036
+ allowStatuses: ALL_HTTP_STATUSES
4037
+ });
4038
+ const body = await res.text();
4039
+ return { ok: res.ok, status: res.status, bodyEmpty: body.trim().length === 0, detail: null };
4040
+ } catch (err) {
4041
+ const detail = err instanceof Error ? err.message : String(err);
4042
+ console.error(`[probeOpenCodeRequestReadiness] GET /config failed (port ${port}): ${detail}`);
4043
+ return { ok: false, status: null, bodyEmpty: null, detail };
3774
4044
  }
3775
4045
  }
3776
4046
 
@@ -4166,6 +4436,11 @@ async function listV2Permissions(client, sessionId) {
4166
4436
  return null;
4167
4437
  }
4168
4438
  }
4439
+ async function interruptV2Session(client, sessionId) {
4440
+ await client.request(`/api/session/${encodeURIComponent(sessionId)}/interrupt`, {
4441
+ method: "POST"
4442
+ });
4443
+ }
4169
4444
  async function getV2ActiveSessions(client) {
4170
4445
  try {
4171
4446
  const body = await readJson(await client.request("/api/session/active"));
@@ -4568,11 +4843,11 @@ function failureMessage(status2) {
4568
4843
  function isRetryableStatus(status2) {
4569
4844
  return CONNECT_KEY_RETRY_STATUSES.includes(status2);
4570
4845
  }
4571
- async function importOneCredential(request2, credential, sleepForRetry, log3) {
4846
+ async function importOneCredential(request3, credential, sleepForRetry, log3) {
4572
4847
  const path = `/api/integration/${encodeURIComponent(credential.integrationId)}/connect/key`;
4573
4848
  for (let attempt = 1; attempt <= CONNECT_KEY_MAX_ATTEMPTS; attempt += 1) {
4574
4849
  try {
4575
- const response = await request2(
4850
+ const response = await request3(
4576
4851
  path,
4577
4852
  {
4578
4853
  method: "POST",
@@ -4607,7 +4882,7 @@ async function importOneCredential(request2, credential, sleepForRetry, log3) {
4607
4882
  throw new Error("OpenCode credential import retry loop did not return");
4608
4883
  }
4609
4884
  async function importOpenCodeCredentials({
4610
- request: request2,
4885
+ request: request3,
4611
4886
  classified,
4612
4887
  connectedIntegrationIds,
4613
4888
  sleepForRetry = sleep2,
@@ -4619,7 +4894,7 @@ async function importOpenCodeCredentials({
4619
4894
  result.alreadyConnected.push(credential.integrationId);
4620
4895
  continue;
4621
4896
  }
4622
- const outcome = await importOneCredential(request2, credential, sleepForRetry, log3);
4897
+ const outcome = await importOneCredential(request3, credential, sleepForRetry, log3);
4623
4898
  if (outcome.ok) result.imported.push(credential.integrationId);
4624
4899
  else result.failed.push(outcome.failure);
4625
4900
  }
@@ -4637,7 +4912,7 @@ function parseIntegrationList(value) {
4637
4912
  return { integrationIds, hasData: value.data.length > 0 };
4638
4913
  }
4639
4914
  async function getConnectedOpenCodeIntegrationIds({
4640
- request: request2,
4915
+ request: request3,
4641
4916
  directory,
4642
4917
  sleepForRetry = sleep2,
4643
4918
  log: log3
@@ -4645,7 +4920,7 @@ async function getConnectedOpenCodeIntegrationIds({
4645
4920
  const path = `/api/integration?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
4646
4921
  for (let attempt = 1; attempt <= INTEGRATION_LIST_MAX_READS; attempt += 1) {
4647
4922
  try {
4648
- const response = await request2(path);
4923
+ const response = await request3(path);
4649
4924
  const parsed = parseIntegrationList(await response.json());
4650
4925
  if (parsed === null) {
4651
4926
  log3(
@@ -4674,7 +4949,7 @@ async function getConnectedOpenCodeIntegrationIds({
4674
4949
  return null;
4675
4950
  }
4676
4951
  async function importOpenCodeV2Credentials({
4677
- request: request2,
4952
+ request: request3,
4678
4953
  homeDir,
4679
4954
  directory,
4680
4955
  sleepForRetry = sleep2,
@@ -4692,7 +4967,7 @@ async function importOpenCodeV2Credentials({
4692
4967
  const classified = classifyOpenCodeCredentials(credentials2);
4693
4968
  if (classified.importable.length === 0) return emptyResult(classified.unsupported);
4694
4969
  const connectedIntegrationIds = await getConnectedOpenCodeIntegrationIds({
4695
- request: request2,
4970
+ request: request3,
4696
4971
  directory,
4697
4972
  sleepForRetry,
4698
4973
  log: log3
@@ -4701,7 +4976,7 @@ async function importOpenCodeV2Credentials({
4701
4976
  return emptyResult(classified.unsupported, "connection-list-unavailable");
4702
4977
  }
4703
4978
  return importOpenCodeCredentials({
4704
- request: request2,
4979
+ request: request3,
4705
4980
  classified,
4706
4981
  connectedIntegrationIds,
4707
4982
  sleepForRetry,
@@ -5392,7 +5667,11 @@ function isSessionDbProvenanceRecord(value) {
5392
5667
  var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
5393
5668
 
5394
5669
  // src/lib/opencode/opencode-version-gate.ts
5395
- var QUEUE_VALIDATED_OPENCODE_VERSIONS = ["1.17.11", "1.18.3"];
5670
+ var QUEUE_VALIDATED_OPENCODE_VERSIONS = [
5671
+ "1.17.11",
5672
+ "1.18.3",
5673
+ "1.18.35"
5674
+ ];
5396
5675
  function isQueueValidatedVersion(version2) {
5397
5676
  if (!version2) return false;
5398
5677
  return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version2);
@@ -6437,6 +6716,7 @@ function connectTunnel(options) {
6437
6716
  }, 3e4);
6438
6717
  let relaySilenceTimeout;
6439
6718
  let relaySilenceTimedOut = false;
6719
+ let relayConnectionId;
6440
6720
  const clearRelaySilenceTimeout = () => {
6441
6721
  if (relaySilenceTimeout === void 0) return;
6442
6722
  clearTimeout(relaySilenceTimeout);
@@ -6522,7 +6802,8 @@ function connectTunnel(options) {
6522
6802
  armRelaySilenceTimeout();
6523
6803
  forwarder.setBinaryFramesSupported(message.binary_frames === true);
6524
6804
  const connectedAgentId = message.agent_id ?? agentId;
6525
- onConnected?.(connectedAgentId);
6805
+ relayConnectionId = message.connection_id;
6806
+ onConnected?.(connectedAgentId, relayConnectionId);
6526
6807
  resolve5({
6527
6808
  ws,
6528
6809
  close: () => ws.close(1e3, "CLI shutdown")
@@ -6559,7 +6840,7 @@ function connectTunnel(options) {
6559
6840
  clearRelaySilenceTimeout();
6560
6841
  const reasonStr = (relaySilenceTimedOut ? "no relay heartbeat for 90s \u2014 reconnecting" : void 0) || reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
6561
6842
  forwarder.abortAll();
6562
- onDisconnected?.(code, reasonStr);
6843
+ onDisconnected?.(code, reasonStr, relayConnectionId);
6563
6844
  });
6564
6845
  });
6565
6846
  }
@@ -6622,7 +6903,7 @@ var RunnerConnection = class _RunnerConnection {
6622
6903
  authHeader: this.opts.getAuthHeader(),
6623
6904
  port: this.opts.port,
6624
6905
  openCodePassword: this.opts.openCodePassword,
6625
- onConnected: (agentId) => {
6906
+ onConnected: (agentId, relayConnectionId) => {
6626
6907
  const attempt = this.reconnectAttempt + 1;
6627
6908
  this.reconnectAttempt = 0;
6628
6909
  this.reconnecting = false;
@@ -6632,13 +6913,17 @@ var RunnerConnection = class _RunnerConnection {
6632
6913
  this.disconnectedAtMs = null;
6633
6914
  events.onConnected(agentId, isReconnect, {
6634
6915
  generation: this.generation,
6916
+ relayConnectionId: relayConnectionId ?? null,
6635
6917
  attempt,
6636
6918
  downtimeMs
6637
6919
  });
6638
6920
  },
6639
- onDisconnected: (code, reason) => {
6921
+ onDisconnected: (code, reason, relayConnectionId) => {
6640
6922
  this.disconnectedAtMs = this.now();
6641
- events.onDisconnected(code, reason, { generation: this.generation });
6923
+ events.onDisconnected(code, reason, {
6924
+ generation: this.generation,
6925
+ relayConnectionId: relayConnectionId ?? null
6926
+ });
6642
6927
  const relayRequestedReconnect = code === 1e3 && _RunnerConnection.RELAY_RECONNECT_CLOSE_REASONS.has(reason);
6643
6928
  if (this.opts.isRunning() && (code !== 1e3 || relayRequestedReconnect) && !this.reconnecting) {
6644
6929
  this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
@@ -7086,8 +7371,8 @@ import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/p
7086
7371
  import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute2, join as join9, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
7087
7372
  var FILE_MODE = 384;
7088
7373
  var DIRECTORY_MODE = 448;
7089
- async function writePushedFile(request2) {
7090
- const { requestedPath, content, allowedDirectories, homeDir } = request2;
7374
+ async function writePushedFile(request3) {
7375
+ const { requestedPath, content, allowedDirectories, homeDir } = request3;
7091
7376
  const bytes = content.byteLength;
7092
7377
  if (allowedDirectories.length === 0) {
7093
7378
  return refuse("file_sync_disabled", "File sync is not enabled on this runner.", {
@@ -7537,7 +7822,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
7537
7822
  var MAX_BUFFERED_SESSION_ERRORS = 256;
7538
7823
  var SESSION_ERROR_RETENTION_TTL_MS = 6e4;
7539
7824
  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: ";
7825
+ var SESSION_ERROR_RETAINED_DETAIL = "Runner-observed OpenCode session error: ";
7541
7826
  var SESSION_ERROR_OBSERVED_CAUSE = "Runner-observed OpenCode session-store cause: ";
7542
7827
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
7543
7828
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
@@ -7661,12 +7946,15 @@ var ChannelDriver = class _ChannelDriver {
7661
7946
  /**
7662
7947
  * Per-conversation throttle state for the #183 recurrence warning (#1618
7663
7948
  * 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`
7949
+ * `firstWedgedAt` anchors `stuck_for_ms`; `warnAnchorAt` is the throttle
7950
+ * anchor from first detection through the first warning, then from each
7951
+ * warning onward; `consecutiveTicks` is reported in the log text so the
7952
+ * operator sees magnitude, not repetition. Cleared the moment the
7953
+ * conversation dispatches anything, or when it drops off the pending list
7954
+ * (`runDrain`'s prune, the dominant real-world path — a wedge normally ends
7955
+ * with its message completing, not a fresh dispatch on the same
7956
+ * conversation) — either way a fresh wedge, if it recurs, is a new
7957
+ * incident. Bounded FIFO, mirroring `supersededSessions`
7670
7958
  * (`MAX_WEDGED_CONVERSATIONS`).
7671
7959
  */
7672
7960
  wedgeWarnings = /* @__PURE__ */ new Map();
@@ -7896,6 +8184,7 @@ var ChannelDriver = class _ChannelDriver {
7896
8184
  subagentInvocationPrefetches = /* @__PURE__ */ new Map();
7897
8185
  /** Serialises drains so a reconnect during a drain doesn't double-process. */
7898
8186
  draining = false;
8187
+ pendingProviderRegistryRepair = null;
7899
8188
  /**
7900
8189
  * Serialises runner-file syncs (#559) so the ~2s poll tick and a concurrent
7901
8190
  * drain ping don't download, write and ack the same file twice.
@@ -8081,8 +8370,33 @@ var ChannelDriver = class _ChannelDriver {
8081
8370
  async runDrain() {
8082
8371
  let dispatched = 0;
8083
8372
  try {
8373
+ const pendingRepair = this.pendingProviderRegistryRepair;
8374
+ if (pendingRepair && this.protectedSessionIds().size === 0) {
8375
+ try {
8376
+ await this.issueProviderRegistryReload(
8377
+ pendingRepair.conversationId,
8378
+ pendingRepair.messageId
8379
+ );
8380
+ } catch (err) {
8381
+ this.log({
8382
+ level: "error",
8383
+ message: `Provider registry repair adoption failed for message ${pendingRepair.messageId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`,
8384
+ conversation_id: pendingRepair.conversationId,
8385
+ message_id: pendingRepair.messageId
8386
+ });
8387
+ }
8388
+ return 0;
8389
+ }
8084
8390
  this.pendingMessageIds.clear();
8085
8391
  const conversations = await this.getPendingConversations();
8392
+ if (this.wedgeWarnings.size > 0) {
8393
+ const pendingConversationIds = new Set(conversations.map(({ id }) => id));
8394
+ for (const conversationId of this.wedgeWarnings.keys()) {
8395
+ if (!pendingConversationIds.has(conversationId)) {
8396
+ this.wedgeWarnings.delete(conversationId);
8397
+ }
8398
+ }
8399
+ }
8086
8400
  if (this.recycleRequestedFlag) {
8087
8401
  this.stop();
8088
8402
  }
@@ -8454,6 +8768,47 @@ var ChannelDriver = class _ChannelDriver {
8454
8768
  agent: message.opencode_agent ?? void 0,
8455
8769
  model: message.opencode_model ?? void 0
8456
8770
  };
8771
+ const pinnedProviderLoaded = await isPinnedProviderLoaded(
8772
+ this.port,
8773
+ options.model,
8774
+ this.openCodeClient
8775
+ );
8776
+ if (pinnedProviderLoaded === false) {
8777
+ const dispatchedModel = message.opencode_model;
8778
+ const separator = dispatchedModel?.indexOf("/") ?? -1;
8779
+ const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
8780
+ const providerId = hasProviderAndModel ? dispatchedModel.slice(0, separator) : null;
8781
+ const modelId = hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null;
8782
+ const errorMessage3 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
8783
+ this.log({
8784
+ level: "error",
8785
+ message: `Refusing message ${message.id.slice(0, 8)} in conversation ${conv.id.slice(0, 8)}: provider namespace ${providerId ?? "(unknown)"} is not loaded; no turn was started`,
8786
+ conversation_id: conv.id,
8787
+ message_id: message.id
8788
+ });
8789
+ try {
8790
+ await this.markFailed(conv.id, message.id, sessionId, errorMessage3, void 0, {
8791
+ classification: {
8792
+ kind: "model_auth",
8793
+ providerId,
8794
+ modelId,
8795
+ reason: "missing"
8796
+ },
8797
+ errorShape: { name: null, statusCode: null }
8798
+ });
8799
+ this.signalDispatchNotStarted(conv, message, "model_provider_absent");
8800
+ } catch (markErr) {
8801
+ this.log({
8802
+ level: "warn",
8803
+ message: `markFailed PATCH for refused message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
8804
+ conversation_id: conv.id,
8805
+ message_id: message.id
8806
+ });
8807
+ this.signalDispatchNotStarted(conv, message, "failure_unreported");
8808
+ }
8809
+ await this.repairProviderRegistry(conv.id, message.id);
8810
+ break;
8811
+ }
8457
8812
  let opencodeMessageId;
8458
8813
  let preDispatchUserIds = null;
8459
8814
  try {
@@ -8619,14 +8974,13 @@ var ChannelDriver = class _ChannelDriver {
8619
8974
  * skip-because-already-`dispatched`, the exact signature of a message stuck
8620
8975
  * acknowledged-but-never-worked.
8621
8976
  *
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.
8977
+ * The first detection starts the clock and emits a local `debug` entry. The
8978
+ * `warn` log and paired `dispatch_wedged` signal fire once the wedge has
8979
+ * lasted `wedgeWarningIntervalMs`, then at most once per interval, naming the
8980
+ * consecutive-tick count so the operator sees magnitude rather than repetition.
8981
+ * A watcher stall that the loop-liveness watchdog recovers is expected and is
8982
+ * already reported by its `watcher_recovered` signal, so it does not reach an
8983
+ * operator through this warning.
8630
8984
  *
8631
8985
  * Deliberately does NOT trigger a release: WI-1's `reconcileWatchers` runs
8632
8986
  * unconditionally on this same tick and is already recovering anything it
@@ -8638,36 +8992,41 @@ var ChannelDriver = class _ChannelDriver {
8638
8992
  const existing = this.wedgeWarnings.get(conv.id);
8639
8993
  const firstWedgedAt = existing?.firstWedgedAt ?? now;
8640
8994
  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
8995
+ const warnAnchorAt = existing?.warnAnchorAt ?? now;
8996
+ const dueForWarn = now - warnAnchorAt >= this.wedgeWarningIntervalMs;
8997
+ if (!existing) {
8998
+ this.log({
8999
+ level: "debug",
9000
+ 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.`,
9001
+ conversation_id: conv.id
9002
+ });
9003
+ }
9004
+ let untracked = 0;
9005
+ if (dueForWarn) {
9006
+ const stuckForMs = now - firstWedgedAt;
9007
+ untracked = this.countUntrackedIds(messages);
9008
+ this.log({
9009
+ level: "warn",
9010
+ 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.`),
9011
+ conversation_id: conv.id
8648
9012
  });
8649
- return;
8650
9013
  }
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
9014
  this.wedgeWarnings.delete(conv.id);
8659
- this.wedgeWarnings.set(conv.id, { firstWedgedAt, lastWarnedAt: now, consecutiveTicks });
9015
+ this.wedgeWarnings.set(conv.id, {
9016
+ firstWedgedAt,
9017
+ warnAnchorAt: dueForWarn ? now : warnAnchorAt,
9018
+ consecutiveTicks
9019
+ });
8660
9020
  while (this.wedgeWarnings.size > MAX_WEDGED_CONVERSATIONS) {
8661
9021
  const oldest = this.wedgeWarnings.keys().next().value;
8662
9022
  if (oldest === void 0) break;
8663
9023
  this.wedgeWarnings.delete(oldest);
8664
9024
  }
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
- }
9025
+ if (!dueForWarn) return;
9026
+ void this.postSignal(conv.id, messages[0].id, "dispatch_wedged", {
9027
+ stuck_for_ms: now - firstWedgedAt,
9028
+ untracked
9029
+ });
8671
9030
  }
8672
9031
  /**
8673
9032
  * How many of `messages`' ids are tracked by NO watcher's `inFlight` (#1618
@@ -8803,6 +9162,34 @@ var ChannelDriver = class _ChannelDriver {
8803
9162
  if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
8804
9163
  const ongoing = await this.isSessionOngoing(sessionId);
8805
9164
  if (ongoing === false) {
9165
+ const age = this.absoluteAgeForRow(message.processing_started_at);
9166
+ if (age.tripped && age.anchorMs !== null) {
9167
+ try {
9168
+ await this.markFailed(
9169
+ conv.id,
9170
+ message.id,
9171
+ sessionId,
9172
+ this.absoluteCeilingError(message.id, sessionId, age.anchorMs),
9173
+ void 0,
9174
+ {
9175
+ classification: null,
9176
+ errorShape: extractReplyErrorShape(findLastAssistantReplyFor(messages, ocId ?? ""))
9177
+ }
9178
+ );
9179
+ } catch (err) {
9180
+ if (err instanceof ChannelAuthError) throw err;
9181
+ this.log({
9182
+ level: "warn",
9183
+ 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)}`,
9184
+ conversation_id: conv.id,
9185
+ message_id: message.id
9186
+ });
9187
+ return "unresolved";
9188
+ }
9189
+ this.clearRedriveUnresolved(message.id);
9190
+ void this.postSignal(conv.id, message.id, "redrive_ceiling_refused");
9191
+ return "settled";
9192
+ }
8806
9193
  this.log({
8807
9194
  level: "info",
8808
9195
  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 +9260,29 @@ var ChannelDriver = class _ChannelDriver {
8873
9260
  message_id: message.id
8874
9261
  });
8875
9262
  }
9263
+ if (this.absoluteAgeForRow(message.processing_started_at).tripped) {
9264
+ try {
9265
+ await this.markFailed(
9266
+ conv.id,
9267
+ message.id,
9268
+ sessionId,
9269
+ this.absoluteCeilingError(message.id, sessionId, anchorMs)
9270
+ );
9271
+ } catch (err) {
9272
+ if (err instanceof ChannelAuthError) throw err;
9273
+ this.log({
9274
+ level: "warn",
9275
+ 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)}`,
9276
+ conversation_id: conv.id,
9277
+ message_id: message.id
9278
+ });
9279
+ const bound = await this.boundRedriveOutcome(conv, message, "reattach");
9280
+ return bound === "abandoned" ? "abandoned" : "unresolved";
9281
+ }
9282
+ this.clearRedriveUnresolved(message.id);
9283
+ void this.postSignal(conv.id, message.id, "redrive_ceiling_refused");
9284
+ return "abandoned";
9285
+ }
8876
9286
  const title = await this.resolveSessionTitle(sessionId, conv.id);
8877
9287
  try {
8878
9288
  await this.markProcessing(conv.id, message.id, sessionId, ocId, title);
@@ -9139,8 +9549,7 @@ var ChannelDriver = class _ChannelDriver {
9139
9549
  const since = this.redriveOutcomeFailingSince.get(message.id);
9140
9550
  if (since === void 0) this.redriveOutcomeFailingSince.set(message.id, now);
9141
9551
  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;
9552
+ const absoluteAgeTripped = this.absoluteAgeForRow(message.processing_started_at).tripped;
9144
9553
  if (!durationTripped && !absoluteAgeTripped) {
9145
9554
  this.signalRedriveOutcomeUnreported(conv, message, outcome);
9146
9555
  return "retry";
@@ -9179,6 +9588,15 @@ var ChannelDriver = class _ChannelDriver {
9179
9588
  });
9180
9589
  return "abandoned";
9181
9590
  }
9591
+ /** Row-level guards use the durable server stamp; in-flight guards use processingAnchorMs. */
9592
+ absoluteAgeForRow(stamp) {
9593
+ const anchorMs = stamp ? Date.parse(stamp) : NaN;
9594
+ return Number.isNaN(anchorMs) ? { tripped: false, anchorMs: null } : { tripped: this.now() - anchorMs >= ABSOLUTE_MAX_PROCESSING_MS, anchorMs };
9595
+ }
9596
+ absoluteCeilingError(messageId, sessionId, anchorMs, retainedReason = this.readRetainedSessionError(sessionId)) {
9597
+ 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.`;
9598
+ return retainedReason ? `${baseError} ${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}` : baseError;
9599
+ }
9182
9600
  /**
9183
9601
  * Record one poll outcome toward the re-drive fence's consecutive-identical-
9184
9602
  * failure streak (#1348) and return the resulting count. `signature === null`
@@ -9620,10 +10038,7 @@ var ChannelDriver = class _ChannelDriver {
9620
10038
  opencodeMessageId,
9621
10039
  message,
9622
10040
  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,
10041
+ processingAnchorMs: this.absoluteAgeForRow(message.processing_started_at).anchorMs ?? processedAtMs,
9627
10042
  deadline: watchDeadlineAnchorMs + this.pausedMaxWaitMs,
9628
10043
  // The server row is ALREADY `processing`; do not re-fire markProcessing.
9629
10044
  started: true,
@@ -9933,14 +10348,40 @@ var ChannelDriver = class _ChannelDriver {
9933
10348
  conversation_id: watcher.conv.id,
9934
10349
  message_id: inFlight.evidentMessageId
9935
10350
  });
9936
- await this.markFailed(
9937
- watcher.conv.id,
9938
- inFlight.evidentMessageId,
9939
- event.sessionId,
9940
- `OpenCode could not run this turn: ${event.reason}`
9941
- );
10351
+ const isMissingProvider = isProviderModelNotFoundError(event.reason);
10352
+ if (isMissingProvider) {
10353
+ const dispatchedModel = inFlight.message.opencode_model;
10354
+ const separator = dispatchedModel?.indexOf("/") ?? -1;
10355
+ const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
10356
+ await this.markFailed(
10357
+ watcher.conv.id,
10358
+ inFlight.evidentMessageId,
10359
+ event.sessionId,
10360
+ `OpenCode could not run this turn: ${event.reason}`,
10361
+ void 0,
10362
+ {
10363
+ classification: {
10364
+ kind: "model_auth",
10365
+ providerId: hasProviderAndModel ? dispatchedModel.slice(0, separator) : null,
10366
+ modelId: hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null,
10367
+ reason: "missing"
10368
+ },
10369
+ errorShape: { name: null, statusCode: null }
10370
+ }
10371
+ );
10372
+ } else {
10373
+ await this.markFailed(
10374
+ watcher.conv.id,
10375
+ inFlight.evidentMessageId,
10376
+ event.sessionId,
10377
+ `OpenCode could not run this turn: ${event.reason}`
10378
+ );
10379
+ }
9942
10380
  inFlight.done = true;
9943
10381
  this.removeInFlight(watcher, inFlight.evidentMessageId);
10382
+ if (isMissingProvider) {
10383
+ await this.repairProviderRegistry(watcher.conv.id, inFlight.evidentMessageId);
10384
+ }
9944
10385
  } catch (err) {
9945
10386
  if (err instanceof ChannelAuthError) {
9946
10387
  this.log({
@@ -9961,6 +10402,44 @@ var ChannelDriver = class _ChannelDriver {
9961
10402
  this.sessionErrorHandled.delete(inFlight.evidentMessageId);
9962
10403
  }
9963
10404
  }
10405
+ async repairProviderRegistry(conversationId, messageId) {
10406
+ if (this.isV2) {
10407
+ void this.postSignal(conversationId, messageId, "provider_registry_reload_unsupported");
10408
+ return;
10409
+ }
10410
+ const activeSessions = this.protectedSessionIds().size;
10411
+ if (activeSessions > 0 || this.draining) {
10412
+ this.pendingProviderRegistryRepair = { conversationId, messageId };
10413
+ void this.postSignal(conversationId, messageId, "provider_registry_repair_deferred", {
10414
+ active_sessions: activeSessions
10415
+ });
10416
+ this.log({
10417
+ level: "warn",
10418
+ message: `Provider registry repair deferred until a lifecycle-safe point; ${activeSessions} active session(s) protected`,
10419
+ conversation_id: conversationId,
10420
+ message_id: messageId
10421
+ });
10422
+ return;
10423
+ }
10424
+ await this.issueProviderRegistryReload(conversationId, messageId);
10425
+ }
10426
+ async issueProviderRegistryReload(conversationId, messageId) {
10427
+ this.pendingProviderRegistryRepair = null;
10428
+ const result = await disposeAndReloadProviderConfig(this.port, this.openCodeClient);
10429
+ if (result.outcome === "failed") {
10430
+ const failure = result.status === null ? `transport failure${result.detail ? `: ${result.detail}` : ""}` : `HTTP ${result.status}`;
10431
+ this.log({
10432
+ level: "warn",
10433
+ message: `Provider registry reload failed (${failure})`,
10434
+ conversation_id: conversationId,
10435
+ message_id: messageId
10436
+ });
10437
+ }
10438
+ const repairOutcome = result.outcome === "reloaded" ? "reloaded" : "reload_failed";
10439
+ void this.postSignal(conversationId, messageId, "provider_registry_reload_issued", {
10440
+ repair_outcome: repairOutcome
10441
+ });
10442
+ }
9964
10443
  /**
9965
10444
  * The per-session polling loop (WI-3). Once per tick it:
9966
10445
  * 1. polls `GET /session/:id/message` once and, per in-flight message,
@@ -10010,7 +10489,9 @@ var ChannelDriver = class _ChannelDriver {
10010
10489
  }
10011
10490
  const { openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk } = await this.pollInteractions(sessionId, watcher, messages);
10012
10491
  if (watcher.generation !== generation) return;
10492
+ const ceilingAttemptsThisTick = /* @__PURE__ */ new Set();
10013
10493
  for (const inFlight of [...watcher.inFlight.values()]) {
10494
+ if (ceilingAttemptsThisTick.has(inFlight.evidentMessageId)) continue;
10014
10495
  await this.serviceInFlightMessage(
10015
10496
  sessionId,
10016
10497
  watcher,
@@ -10019,7 +10500,8 @@ var ChannelDriver = class _ChannelDriver {
10019
10500
  openQuestions,
10020
10501
  openPermissions,
10021
10502
  questionsPolledOk,
10022
- permissionsPolledOk
10503
+ permissionsPolledOk,
10504
+ ceilingAttemptsThisTick
10023
10505
  );
10024
10506
  }
10025
10507
  }
@@ -10060,35 +10542,139 @@ var ChannelDriver = class _ChannelDriver {
10060
10542
  inFlight.deadline = this.now() + this.pausedMaxWaitMs;
10061
10543
  }
10062
10544
  }
10063
- /**
10064
- * Drive ONE in-flight message's lifecycle from the tick's message snapshot.
10065
- * Fires markProcessing on queued→running and markDone on done (each once),
10066
- * applies the idle-path re-dispatch guard, and removes the message from the
10067
- * in-flight set on completion or timeout.
10068
- */
10069
- async serviceInFlightMessage(sessionId, watcher, inFlight, messages, openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk) {
10070
- const conv = watcher.conv;
10071
- const state = messageRunState(messages, inFlight.opencodeMessageId);
10072
- const id = inFlight.evidentMessageId;
10073
- if (state === "running" || state === "done" || state === "failed") {
10074
- this.neverStartedAttempts.delete(id);
10075
- }
10076
- if (inFlight.sessionStoreFailurePending) {
10077
- await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
10078
- return;
10079
- }
10080
- if (inFlight.neverStartedResolved) {
10081
- if (state === "running" || state === "done" || state === "failed") {
10082
- inFlight.neverStartedResolved = false;
10083
- } else {
10084
- await this.settleMessageNeverStarted(
10085
- sessionId,
10086
- watcher,
10087
- inFlight,
10088
- this.neverStartedAttempts.get(id)?.count ?? MAX_NEVER_STARTED_ATTEMPTS
10089
- );
10090
- return;
10091
- }
10545
+ async settleAbsoluteCeiling(sessionId, watcher, messages, ceilingAttemptsThisTick) {
10546
+ const entries = [...watcher.inFlight.values()];
10547
+ const pastCeiling = entries.filter(
10548
+ (entry) => this.now() - entry.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS
10549
+ );
10550
+ const healthySiblings = entries.length - pastCeiling.length;
10551
+ const landed = [];
10552
+ let allRecordsLanded = true;
10553
+ const retainedReason = this.readRetainedSessionError(sessionId);
10554
+ for (const entry of pastCeiling) {
10555
+ if (!watcher.inFlight.has(entry.evidentMessageId)) continue;
10556
+ ceilingAttemptsThisTick.add(entry.evidentMessageId);
10557
+ const error2 = this.absoluteCeilingError(
10558
+ entry.evidentMessageId,
10559
+ sessionId,
10560
+ entry.processingAnchorMs,
10561
+ retainedReason
10562
+ );
10563
+ this.log({
10564
+ level: "warn",
10565
+ message: error2,
10566
+ conversation_id: watcher.conv.id,
10567
+ message_id: entry.evidentMessageId
10568
+ });
10569
+ try {
10570
+ await this.markFailed(
10571
+ watcher.conv.id,
10572
+ entry.evidentMessageId,
10573
+ sessionId,
10574
+ error2,
10575
+ void 0,
10576
+ {
10577
+ classification: null,
10578
+ errorShape: extractReplyErrorShape(
10579
+ findLastAssistantReplyFor(messages, entry.opencodeMessageId)
10580
+ )
10581
+ }
10582
+ );
10583
+ } catch (err) {
10584
+ if (err instanceof ChannelAuthError) throw err;
10585
+ allRecordsLanded = false;
10586
+ const watched_for_ms = this.now() - entry.processingAnchorMs;
10587
+ if (err instanceof ChannelTerminalError) {
10588
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10589
+ watched_for_ms,
10590
+ interrupt_outcome: "record_terminal"
10591
+ });
10592
+ this.removeInFlight(watcher, entry.evidentMessageId);
10593
+ continue;
10594
+ }
10595
+ if (this.now() >= entry.deadline) {
10596
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10597
+ watched_for_ms,
10598
+ interrupt_outcome: "record_deadline"
10599
+ });
10600
+ this.removeInFlight(watcher, entry.evidentMessageId);
10601
+ continue;
10602
+ }
10603
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10604
+ watched_for_ms,
10605
+ interrupt_outcome: "record_retry"
10606
+ });
10607
+ this.log({
10608
+ level: "warn",
10609
+ 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)}`,
10610
+ conversation_id: watcher.conv.id,
10611
+ message_id: entry.evidentMessageId
10612
+ });
10613
+ continue;
10614
+ }
10615
+ entry.done = true;
10616
+ landed.push(entry);
10617
+ }
10618
+ if (!allRecordsLanded) {
10619
+ for (const entry of landed) this.removeInFlight(watcher, entry.evidentMessageId);
10620
+ return;
10621
+ }
10622
+ if (healthySiblings > 0) {
10623
+ for (const entry of landed) {
10624
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10625
+ watched_for_ms: this.now() - entry.processingAnchorMs,
10626
+ interrupt_outcome: "deferred_for_healthy_sibling",
10627
+ healthy_siblings: healthySiblings
10628
+ });
10629
+ this.removeInFlight(watcher, entry.evidentMessageId);
10630
+ }
10631
+ return;
10632
+ }
10633
+ const interrupt = await interruptSession(this.port, sessionId, this.openCodeClient);
10634
+ for (const entry of landed) {
10635
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10636
+ watched_for_ms: this.now() - entry.processingAnchorMs,
10637
+ interrupt_outcome: interrupt.kind
10638
+ });
10639
+ this.removeInFlight(watcher, entry.evidentMessageId);
10640
+ }
10641
+ if (interrupt.kind !== "confirmed_stopped") {
10642
+ this.log({
10643
+ level: "warn",
10644
+ 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`,
10645
+ conversation_id: watcher.conv.id
10646
+ });
10647
+ }
10648
+ }
10649
+ /**
10650
+ * Drive ONE in-flight message's lifecycle from the tick's message snapshot.
10651
+ * Fires markProcessing on queued→running and markDone on done (each once),
10652
+ * applies the idle-path re-dispatch guard, and removes the message from the
10653
+ * in-flight set on completion or timeout.
10654
+ */
10655
+ async serviceInFlightMessage(sessionId, watcher, inFlight, messages, openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk, ceilingAttemptsThisTick) {
10656
+ const conv = watcher.conv;
10657
+ const state = messageRunState(messages, inFlight.opencodeMessageId);
10658
+ const id = inFlight.evidentMessageId;
10659
+ if (state === "running" || state === "done" || state === "failed") {
10660
+ this.neverStartedAttempts.delete(id);
10661
+ }
10662
+ if (inFlight.sessionStoreFailurePending) {
10663
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
10664
+ return;
10665
+ }
10666
+ if (inFlight.neverStartedResolved) {
10667
+ if (state === "running" || state === "done" || state === "failed") {
10668
+ inFlight.neverStartedResolved = false;
10669
+ } else {
10670
+ await this.settleMessageNeverStarted(
10671
+ sessionId,
10672
+ watcher,
10673
+ inFlight,
10674
+ this.neverStartedAttempts.get(id)?.count ?? MAX_NEVER_STARTED_ATTEMPTS
10675
+ );
10676
+ return;
10677
+ }
10092
10678
  }
10093
10679
  if (messages && collectTaskCalls(messages, inFlight.opencodeMessageId).length > 0 && !this.subagentInvocationPrefetches.has(id)) {
10094
10680
  void this.resolveSubagentInvocations(
@@ -10152,7 +10738,7 @@ var ChannelDriver = class _ChannelDriver {
10152
10738
  if (!inFlight.done) {
10153
10739
  const messageErrorText = messageError(messages, inFlight.opencodeMessageId);
10154
10740
  const retainedReason = this.readRetainedSessionError(sessionId);
10155
- const retainedError = retainedReason === null ? null : `${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}`;
10741
+ const retainedError = retainedReason === null || messageErrorText !== null && messageErrorText.includes(retainedReason) ? null : `${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}`;
10156
10742
  const observedCause = this.sessionStoreHealth?.readCause(sessionId);
10157
10743
  const causeError = observedCause === null || observedCause === void 0 ? null : `${SESSION_ERROR_OBSERVED_CAUSE}${observedCause}`;
10158
10744
  const storeError = this.sessionStoreHealth?.isUnwritable() ? SESSION_STORE_UNWRITABLE_ERROR : null;
@@ -10410,17 +10996,8 @@ var ChannelDriver = class _ChannelDriver {
10410
10996
  return;
10411
10997
  }
10412
10998
  }
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);
10999
+ if (watcher.inFlight.has(inFlight.evidentMessageId) && activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
11000
+ await this.settleAbsoluteCeiling(sessionId, watcher, messages, ceilingAttemptsThisTick);
10424
11001
  return;
10425
11002
  }
10426
11003
  const heartbeatDue = activelyRunning && !inFlight.awaitingHumanLatched && !inFlight.aliveInFlight && this.now() - inFlight.lastAliveAt >= HEARTBEAT_MS;
@@ -10648,7 +11225,8 @@ var ChannelDriver = class _ChannelDriver {
10648
11225
  usage,
10649
11226
  void 0,
10650
11227
  usageAgentName,
10651
- subagentInvocations
11228
+ subagentInvocations,
11229
+ true
10652
11230
  );
10653
11231
  } catch (err) {
10654
11232
  if (err instanceof ChannelAuthError) throw err;
@@ -11042,6 +11620,33 @@ var ChannelDriver = class _ChannelDriver {
11042
11620
  }
11043
11621
  const restartAborted = state === "failed" && sessionOngoing === false && isAbortedTerminalReply(messages, ocId ?? "");
11044
11622
  if (restartAborted) {
11623
+ const age = this.absoluteAgeForRow(row.processing_started_at);
11624
+ if (age.tripped && age.anchorMs !== null) {
11625
+ try {
11626
+ await this.markFailed(
11627
+ row.conversation_id,
11628
+ row.id,
11629
+ sessionId,
11630
+ this.absoluteCeilingError(row.id, sessionId, age.anchorMs),
11631
+ void 0,
11632
+ {
11633
+ classification: null,
11634
+ errorShape: extractReplyErrorShape(findLastAssistantReplyFor(messages, ocId ?? ""))
11635
+ }
11636
+ );
11637
+ } catch (err) {
11638
+ if (err instanceof ChannelAuthError) throw err;
11639
+ this.log({
11640
+ level: "warn",
11641
+ 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)}`,
11642
+ conversation_id: row.conversation_id,
11643
+ message_id: row.id
11644
+ });
11645
+ return;
11646
+ }
11647
+ void this.postSignal(row.conversation_id, row.id, "redrive_ceiling_refused");
11648
+ return;
11649
+ }
11045
11650
  this.log({
11046
11651
  level: "info",
11047
11652
  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 +12192,8 @@ var ChannelDriver = class _ChannelDriver {
11587
12192
  source_message_id: row.source_message_id,
11588
12193
  slack_user_id: row.slack_user_id,
11589
12194
  attachments: row.attachments ?? null,
11590
- opencode_message_id: row.opencode_message_id
12195
+ opencode_message_id: row.opencode_message_id,
12196
+ processing_started_at: row.processing_started_at
11591
12197
  };
11592
12198
  }
11593
12199
  /**
@@ -12494,7 +13100,7 @@ var ChannelDriver = class _ChannelDriver {
12494
13100
  * exists but is wedged, so the next attempt must get a fresh one
12495
13101
  * instead of reusing it — see WI-1's server-side null-clearing PATCH).
12496
13102
  */
12497
- async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
13103
+ async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations, abandonedWithoutAnswer) {
12498
13104
  const body = { status: "failed" };
12499
13105
  if (sessionId === null) {
12500
13106
  body.opencode_session_id = null;
@@ -12507,12 +13113,19 @@ var ChannelDriver = class _ChannelDriver {
12507
13113
  if (subagentInvocations && subagentInvocations.length > 0) {
12508
13114
  body.subagent_invocations = subagentInvocations;
12509
13115
  }
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;
13116
+ if (failure && failure.errorShape.name !== null) {
13117
+ body.failure_error_name = failure.errorShape.name;
12515
13118
  }
13119
+ if (failure && failure.errorShape.statusCode !== null) {
13120
+ body.failure_status_code = failure.errorShape.statusCode;
13121
+ }
13122
+ if (failure?.classification) {
13123
+ body.failure_kind = failure.classification.kind;
13124
+ body.failure_provider_id = failure.classification.providerId;
13125
+ body.failure_model_id = failure.classification.modelId;
13126
+ body.failure_reason = failure.classification.reason;
13127
+ }
13128
+ if (abandonedWithoutAnswer) body.abandoned_without_answer = true;
12516
13129
  try {
12517
13130
  await this.callWithRetry(
12518
13131
  "marking message as failed",
@@ -12532,27 +13145,28 @@ var ChannelDriver = class _ChannelDriver {
12532
13145
  this.clearSubagentInvocationCaches(messageId);
12533
13146
  }
12534
13147
  /**
12535
- * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
13148
+ * Classify an errored turn and extract its bounded error shape for `markFailed`.
12536
13149
  *
12537
13150
  * `messageFailure` alone (structured OpenCode error → `model_auth`) covers
12538
13151
  * most cases; when it returns `null` on this ALREADY-FAILED turn, fall back
12539
13152
  * to the P1-2b zero-provider check — one extra loopback call to
12540
13153
  * `hasAnyConfiguredProvider`, only reached when the structured classifier
12541
13154
  * 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.
13155
+ * (`null`/indeterminate) leaves the classification `null`; the raw error shape
13156
+ * is still retained for diagnostics.
12544
13157
  */
12545
13158
  async classifyModelAuthFailure(messages, userMessageId) {
12546
- const classified = messageFailure(messages, userMessageId);
12547
- if (classified != null) return classified;
12548
13159
  const reply = findLastAssistantReplyFor(messages, userMessageId);
12549
- const hasProvider = await this.hasAnyConfiguredProvider();
12550
- return applyZeroProviderFallback(
13160
+ const errorShape = extractReplyErrorShape(reply);
13161
+ const classified = messageFailure(messages, userMessageId);
13162
+ if (classified != null) return { classification: classified, errorShape };
13163
+ const classification = applyZeroProviderFallback(
12551
13164
  classified,
12552
- hasProvider,
13165
+ await this.hasAnyConfiguredProvider(),
12553
13166
  reply?.info?.providerID ?? null,
12554
13167
  reply?.info?.modelID ?? null
12555
13168
  );
13169
+ return { classification, errorShape };
12556
13170
  }
12557
13171
  async recordSubagentModelAuthFailure(failure, conversationId, messageId) {
12558
13172
  const providerId = failure.providerId ?? "(unknown)";
@@ -13377,6 +13991,22 @@ async function configureGitHubAccess({ env, log: log3 }) {
13377
13991
  import { execFileSync as execFileSync2 } from "node:child_process";
13378
13992
  import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
13379
13993
  import { isAbsolute as isAbsolute3, join as join11, resolve as resolve4 } from "node:path";
13994
+ function parseConfigContent(content) {
13995
+ if (!content?.trim()) return {};
13996
+ const parsed = JSON.parse(content);
13997
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
13998
+ throw new Error("OPENCODE_CONFIG_CONTENT must contain a JSON object");
13999
+ }
14000
+ return parsed;
14001
+ }
14002
+ function mergeManagedOpenCodeConfigContent(content, managedProviders) {
14003
+ const config = parseConfigContent(content);
14004
+ const provider = typeof config.provider === "object" && config.provider !== null && !Array.isArray(config.provider) ? { ...config.provider } : {};
14005
+ for (const namespace of CREDENTIAL_PROXY_NATIVE_NAMESPACES) {
14006
+ delete provider[namespace];
14007
+ }
14008
+ return JSON.stringify({ ...config, provider: { ...provider, ...managedProviders } });
14009
+ }
13380
14010
  function isFile(filePath) {
13381
14011
  return existsSync2(filePath) && statSync6(filePath).isFile();
13382
14012
  }
@@ -13410,6 +14040,159 @@ function applyRunnerOpenCodeConfig({
13410
14040
  log3(`Applied runner OpenCode config ${source} to ${join11(cwd, target)}`);
13411
14041
  }
13412
14042
 
14043
+ // src/lib/opencode/runtime-provenance.ts
14044
+ import { readFileSync as readFileSync7, readdirSync } from "node:fs";
14045
+ import { homedir as homedir7 } from "node:os";
14046
+ import { join as join12 } from "node:path";
14047
+ var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
14048
+ var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
14049
+ function errorKind(error2) {
14050
+ if (!(error2 instanceof Error)) return "unknown error";
14051
+ const code = "code" in error2 && typeof error2.code === "string" ? ` (${error2.code})` : "";
14052
+ return `${error2.name}${code}`;
14053
+ }
14054
+ function parseRegistryPluginSpec(value) {
14055
+ const separator = value.lastIndexOf("@");
14056
+ if (separator <= 0) return null;
14057
+ const name = value.slice(0, separator);
14058
+ const spec = value.slice(separator + 1);
14059
+ if (!PACKAGE_NAME.test(name) || !REGISTRY_SPEC.test(spec)) return null;
14060
+ return { name, spec };
14061
+ }
14062
+ function parseJsonc(value) {
14063
+ let output = "";
14064
+ let inString = false;
14065
+ let escaped = false;
14066
+ let lineComment = false;
14067
+ let blockComment = false;
14068
+ for (let index = 0; index < value.length; index++) {
14069
+ const character = value[index];
14070
+ const next = value[index + 1];
14071
+ if (lineComment) {
14072
+ if (character === "\n") {
14073
+ lineComment = false;
14074
+ output += character;
14075
+ }
14076
+ continue;
14077
+ }
14078
+ if (blockComment) {
14079
+ if (character === "*" && next === "/") {
14080
+ blockComment = false;
14081
+ index++;
14082
+ } else if (character === "\n") {
14083
+ output += character;
14084
+ }
14085
+ continue;
14086
+ }
14087
+ if (inString) {
14088
+ output += character;
14089
+ if (escaped) escaped = false;
14090
+ else if (character === "\\") escaped = true;
14091
+ else if (character === '"') inString = false;
14092
+ continue;
14093
+ }
14094
+ if (character === '"') {
14095
+ inString = true;
14096
+ output += character;
14097
+ } else if (character === "/" && next === "/") {
14098
+ lineComment = true;
14099
+ index++;
14100
+ } else if (character === "/" && next === "*") {
14101
+ blockComment = true;
14102
+ index++;
14103
+ } else if (character === ",") {
14104
+ let lookahead = index + 1;
14105
+ while (/\s/.test(value[lookahead] ?? "")) lookahead++;
14106
+ if (value[lookahead] !== "]" && value[lookahead] !== "}") output += character;
14107
+ } else {
14108
+ output += character;
14109
+ }
14110
+ }
14111
+ return JSON.parse(output);
14112
+ }
14113
+ function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
14114
+ return join12(env.XDG_CACHE_HOME ?? join12(homeDir, ".cache"), "opencode");
14115
+ }
14116
+ function readConfiguredPluginSpecs({
14117
+ cwd = process.cwd(),
14118
+ log: log3
14119
+ }) {
14120
+ let config;
14121
+ try {
14122
+ try {
14123
+ config = parseJsonc(readFileSync7(join12(cwd, "opencode.jsonc"), "utf8"));
14124
+ } catch (error2) {
14125
+ if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
14126
+ config = parseJsonc(readFileSync7(join12(cwd, "opencode.json"), "utf8"));
14127
+ }
14128
+ } catch (error2) {
14129
+ log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
14130
+ return [];
14131
+ }
14132
+ if (typeof config !== "object" || config === null || !("plugin" in config)) return [];
14133
+ const plugins = config.plugin;
14134
+ if (!Array.isArray(plugins)) {
14135
+ log3("Could not read OpenCode plugin configuration: plugin is not an array");
14136
+ return [];
14137
+ }
14138
+ const configured = plugins.filter(
14139
+ (plugin) => typeof plugin === "string" && parseRegistryPluginSpec(plugin) !== null
14140
+ );
14141
+ if (configured.length !== plugins.length) {
14142
+ log3(
14143
+ `Skipped ${plugins.length - configured.length} non-registry OpenCode plugin spec(s); their values were not recorded`
14144
+ );
14145
+ }
14146
+ return configured;
14147
+ }
14148
+ function resolveInstalledPluginVersions({
14149
+ cacheRoot,
14150
+ env = process.env,
14151
+ homeDir = homedir7(),
14152
+ log: log3
14153
+ }) {
14154
+ const packagesRoot = join12(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
14155
+ let directories;
14156
+ try {
14157
+ directories = readdirSync(packagesRoot, { withFileTypes: true });
14158
+ } catch (error2) {
14159
+ if ("code" in Object(error2) && Object(error2).code === "ENOENT") return [];
14160
+ log3(`Could not inspect the OpenCode plugin cache: ${errorKind(error2)}`);
14161
+ return [];
14162
+ }
14163
+ const plugins = [];
14164
+ let skipped = 0;
14165
+ for (const directory of directories) {
14166
+ if (!directory.isDirectory()) continue;
14167
+ const parsed = parseRegistryPluginSpec(directory.name);
14168
+ if (parsed === null) {
14169
+ skipped++;
14170
+ continue;
14171
+ }
14172
+ let resolvedVersion = null;
14173
+ try {
14174
+ const packageJson = JSON.parse(
14175
+ readFileSync7(
14176
+ join12(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
14177
+ "utf8"
14178
+ )
14179
+ );
14180
+ if (typeof packageJson.version === "string" && REGISTRY_SPEC.test(packageJson.version)) {
14181
+ resolvedVersion = packageJson.version;
14182
+ } else {
14183
+ log3(`Could not resolve OpenCode plugin ${parsed.name}@${parsed.spec}: invalid version`);
14184
+ }
14185
+ } catch (error2) {
14186
+ log3(`Could not resolve OpenCode plugin ${parsed.name}@${parsed.spec}: ${errorKind(error2)}`);
14187
+ }
14188
+ plugins.push({ ...parsed, resolvedVersion });
14189
+ }
14190
+ if (skipped > 0) {
14191
+ log3(`Skipped ${skipped} non-registry OpenCode package cache entry; its value was not recorded`);
14192
+ }
14193
+ return plugins;
14194
+ }
14195
+
13413
14196
  // src/lib/credential-sync.ts
13414
14197
  import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
13415
14198
  var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
@@ -13645,10 +14428,261 @@ function createCredentialSync({
13645
14428
  };
13646
14429
  }
13647
14430
 
14431
+ // src/lib/managed-inference-setup.ts
14432
+ var defaultStore = {
14433
+ get: getManagedInferenceCredential,
14434
+ set: setManagedInferenceCredential,
14435
+ clear: clearManagedInferenceCredential
14436
+ };
14437
+ function endpoint(options, operation) {
14438
+ return `${options.apiUrl}/runners/${options.agentId}/managed-credentials/${operation}`;
14439
+ }
14440
+ function requestHeaders(options) {
14441
+ return { Authorization: options.getAuthHeader(), "Content-Type": "application/json" };
14442
+ }
14443
+ function request2(options) {
14444
+ return withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
14445
+ }
14446
+ function safeFailure(operation, status2) {
14447
+ return status2 === void 0 ? `${operation} could not reach the managed credential service` : `${operation} returned HTTP ${status2}`;
14448
+ }
14449
+ function logTransportFailure(options, operation, error2, authorization) {
14450
+ let detail = error2 instanceof Error ? error2.message : String(error2);
14451
+ const credential = authorization.split(/\s+/, 2)[1];
14452
+ for (const secret of [authorization, credential]) {
14453
+ if (secret) detail = detail.replaceAll(secret, "[redacted]");
14454
+ }
14455
+ options.log(
14456
+ `MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${redactDiagnosticText(detail)}`,
14457
+ "debug"
14458
+ );
14459
+ }
14460
+ async function responseJson(response) {
14461
+ try {
14462
+ const value = await response.json();
14463
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
14464
+ } catch (error2) {
14465
+ return null;
14466
+ }
14467
+ }
14468
+ function readSetup(value) {
14469
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
14470
+ const setup = value;
14471
+ if (typeof setup.runner_id !== "string" || typeof setup.generation !== "string" || typeof setup.status !== "string" || setup.token_id !== null && typeof setup.token_id !== "string" || typeof setup.acknowledged_at !== "string" && setup.acknowledged_at !== null) {
14472
+ return null;
14473
+ }
14474
+ return setup;
14475
+ }
14476
+ function clearLocalSetup(options) {
14477
+ const env = options.env ?? process.env;
14478
+ const store = options.store ?? defaultStore;
14479
+ const stored = store.get(options.agentId);
14480
+ store.clear(options.agentId);
14481
+ const token = env.EVIDENT_PROXY_TOKEN;
14482
+ if (token !== void 0 && token === stored?.token) {
14483
+ delete env.EVIDENT_PROXY_TOKEN;
14484
+ } else if (token !== void 0) {
14485
+ options.log(
14486
+ `MANAGED-INFERENCE-CLEANUP-PRESERVED-EXTERNAL-TOKEN: runner=${options.agentId}`,
14487
+ "debug"
14488
+ );
14489
+ }
14490
+ }
14491
+ async function discoverManagedInferenceSetup(options) {
14492
+ const headers = requestHeaders(options);
14493
+ let response;
14494
+ try {
14495
+ response = await request2(options)(endpoint(options, "current"), {
14496
+ headers
14497
+ });
14498
+ } catch (error2) {
14499
+ logTransportFailure(options, "discovery", error2, headers.Authorization);
14500
+ return { kind: "failed", reason: safeFailure("Managed setup discovery") };
14501
+ }
14502
+ if (response.status === 404) {
14503
+ clearLocalSetup(options);
14504
+ return { kind: "none" };
14505
+ }
14506
+ if (!response.ok) {
14507
+ return { kind: "failed", reason: safeFailure("Managed setup discovery", response.status) };
14508
+ }
14509
+ const body = await responseJson(response);
14510
+ const setup = readSetup(body?.setup);
14511
+ return setup ? { kind: "setup", setup } : { kind: "failed", reason: "Managed setup discovery returned an invalid response" };
14512
+ }
14513
+ async function enrollManagedInferenceSetup(options) {
14514
+ const response = await post(options, "enroll", {});
14515
+ if (!response) {
14516
+ return { kind: "failed", reason: safeFailure("Managed credential enrollment") };
14517
+ }
14518
+ if (response.status === 404) {
14519
+ return { kind: "none" };
14520
+ }
14521
+ const body = await responseJson(response);
14522
+ if (!response.ok) {
14523
+ const serverError = body?.error;
14524
+ return {
14525
+ kind: "failed",
14526
+ reason: typeof serverError === "string" && serverError.trim() ? redactDiagnosticText(serverError).slice(0, 500) : safeFailure("Managed credential enrollment", response.status)
14527
+ };
14528
+ }
14529
+ const setup = readSetup(body?.setup);
14530
+ return setup ? { kind: "setup", setup } : { kind: "failed", reason: "Managed credential enrollment returned an invalid response" };
14531
+ }
14532
+ async function post(options, operation, body) {
14533
+ const headers = requestHeaders(options);
14534
+ try {
14535
+ return await request2(options)(endpoint(options, operation), {
14536
+ method: "POST",
14537
+ headers,
14538
+ body: JSON.stringify(body)
14539
+ });
14540
+ } catch (error2) {
14541
+ logTransportFailure(options, operation, error2, headers.Authorization);
14542
+ return null;
14543
+ }
14544
+ }
14545
+ function applyCredential(options, credential) {
14546
+ const env = options.env ?? process.env;
14547
+ let merged;
14548
+ if (options.autoConfigureCredentialsProxy) {
14549
+ const { config, unsupportedProviders } = buildCredentialProxyNativeConfig({
14550
+ inferenceBaseUrl: credential.inferenceBaseUrl,
14551
+ modelDescriptors: credential.modelDescriptors
14552
+ });
14553
+ if (unsupportedProviders.length > 0 || Object.keys(config.provider).length === 0) {
14554
+ return "Managed setup contains unsupported provider descriptors";
14555
+ }
14556
+ try {
14557
+ merged = mergeManagedOpenCodeConfigContent(env.OPENCODE_CONFIG_CONTENT, config.provider);
14558
+ } catch (error2) {
14559
+ return "OPENCODE_CONFIG_CONTENT is malformed; managed providers were not installed";
14560
+ }
14561
+ }
14562
+ try {
14563
+ (options.store ?? defaultStore).set(options.agentId, credential);
14564
+ } catch (error2) {
14565
+ const detail = (error2 instanceof Error ? error2.message : String(error2)).replaceAll(
14566
+ credential.token,
14567
+ "[redacted]"
14568
+ );
14569
+ options.log(
14570
+ `MANAGED-INFERENCE-STORE-FAILED: runner=${options.agentId} generation=${credential.generation} detail=${redactDiagnosticText(detail)}`,
14571
+ "warn"
14572
+ );
14573
+ return "Managed credentials could not be written to the protected store";
14574
+ }
14575
+ env.EVIDENT_PROXY_TOKEN = credential.token;
14576
+ if (merged !== void 0) env.OPENCODE_CONFIG_CONTENT = merged;
14577
+ return null;
14578
+ }
14579
+ async function reportManagedInferenceApplyFailure(options, failure) {
14580
+ const response = await post(options, "report-failure", {
14581
+ generation: failure.generation,
14582
+ token_id: failure.tokenId
14583
+ });
14584
+ if (!response?.ok) {
14585
+ options.log(
14586
+ `MANAGED-INFERENCE-REPORT-FAILED: runner=${options.agentId} generation=${failure.generation}${response ? ` status=${response.status}` : ""}`,
14587
+ "warn"
14588
+ );
14589
+ }
14590
+ }
14591
+ async function reaffirmStored(options, credential) {
14592
+ const response = await post(options, "reaffirm", {
14593
+ generation: credential.generation,
14594
+ token_id: credential.tokenId
14595
+ });
14596
+ if (!response?.ok) {
14597
+ return {
14598
+ kind: "failed",
14599
+ reason: response?.status === 409 ? "Stored managed credential no longer matches this setup; restart required" : response ? safeFailure("Managed credential reaffirmation", response.status) : safeFailure("Managed credential reaffirmation")
14600
+ };
14601
+ }
14602
+ const body = await responseJson(response);
14603
+ const setup = readSetup(body?.setup);
14604
+ if (!setup || setup.generation !== credential.generation || setup.token_id !== credential.tokenId) {
14605
+ return { kind: "failed", reason: "Managed credential reaffirmation returned a mismatch" };
14606
+ }
14607
+ const failure = applyCredential(options, credential);
14608
+ return failure ? { kind: "failed", reason: failure } : { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
14609
+ }
14610
+ async function prepareManagedInferenceSetup(options, discovery, health) {
14611
+ if (health.authFailed) {
14612
+ return {
14613
+ kind: "failed",
14614
+ reason: "OpenCode authentication failed; managed credentials were not rotated"
14615
+ };
14616
+ }
14617
+ const stored = (options.store ?? defaultStore).get(options.agentId);
14618
+ if (health.healthy) {
14619
+ if (!stored) {
14620
+ return {
14621
+ kind: "failed",
14622
+ reason: "Managed credentials are missing for the serving OpenCode process; restart required"
14623
+ };
14624
+ }
14625
+ return reaffirmStored(options, stored);
14626
+ }
14627
+ if (discovery.setup.acknowledged_at !== null && stored?.generation === discovery.setup.generation && stored.tokenId === discovery.setup.token_id) {
14628
+ return reaffirmStored(options, stored);
14629
+ }
14630
+ const response = await post(options, "bootstrap", { generation: discovery.setup.generation });
14631
+ if (!response?.ok) {
14632
+ return {
14633
+ kind: "failed",
14634
+ reason: response ? safeFailure("Managed credential bootstrap", response.status) : safeFailure("Managed credential bootstrap")
14635
+ };
14636
+ }
14637
+ const body = await responseJson(response);
14638
+ const setup = readSetup(body?.setup);
14639
+ const token = body?.inference_token;
14640
+ const inferenceBaseUrl = body?.inference_base_url;
14641
+ const modelDescriptors = body?.model_descriptors;
14642
+ if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || !Array.isArray(modelDescriptors) || !modelDescriptors.every(
14643
+ (descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
14644
+ )) {
14645
+ return { kind: "failed", reason: "Managed credential bootstrap returned an invalid response" };
14646
+ }
14647
+ const credential = {
14648
+ generation: setup.generation,
14649
+ tokenId: setup.token_id,
14650
+ token,
14651
+ inferenceBaseUrl,
14652
+ modelDescriptors
14653
+ };
14654
+ const failure = applyCredential(options, credential);
14655
+ if (failure) {
14656
+ await reportManagedInferenceApplyFailure(options, credential);
14657
+ return { kind: "failed", reason: failure };
14658
+ }
14659
+ return { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
14660
+ }
14661
+ async function acknowledgeManagedInferenceSetup(options, preparation) {
14662
+ const response = await post(options, "acknowledge", {
14663
+ generation: preparation.generation,
14664
+ token_id: preparation.tokenId,
14665
+ config_validated: options.autoConfigureCredentialsProxy === true
14666
+ });
14667
+ if (!response?.ok) {
14668
+ throw new Error(
14669
+ response ? safeFailure("Managed credential acknowledgment", response.status) : safeFailure("Managed credential acknowledgment")
14670
+ );
14671
+ }
14672
+ const body = await responseJson(response);
14673
+ const setup = readSetup(body?.setup);
14674
+ if (!setup || setup.generation !== preparation.generation || setup.token_id !== preparation.tokenId) {
14675
+ throw new Error("Managed credential acknowledgment returned a mismatch");
14676
+ }
14677
+ return response.status === 200 && setup.status === "acknowledged" ? "acknowledged" : "pending";
14678
+ }
14679
+
13648
14680
  // src/commands/run.ts
13649
14681
  var MAX_ACTIVITY_LOG_ENTRIES = 10;
13650
14682
  var MAX_DISCONNECT_REASON_LENGTH = 200;
13651
14683
  var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
14684
+ var CREDENTIAL_ADOPTION_PROBE_ATTEMPTS = 3;
14685
+ var CREDENTIAL_ADOPTION_PROBE_INTERVAL_MS = 1e3;
13652
14686
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
13653
14687
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
13654
14688
  var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
@@ -13683,7 +14717,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
13683
14717
  if (trimmed === "") {
13684
14718
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
13685
14719
  }
13686
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join12(homeDir, trimmed.slice(2)) : trimmed;
14720
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join13(homeDir, trimmed.slice(2)) : trimmed;
13687
14721
  if (!isAbsolute4(expanded)) {
13688
14722
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
13689
14723
  }
@@ -13838,7 +14872,7 @@ function logActivity(state, entry) {
13838
14872
  }
13839
14873
  function reportSessionDbRecovery(state) {
13840
14874
  try {
13841
- const report = drainSessionDbRecoveryReport({ homeDir: homedir7(), env: process.env });
14875
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir8(), env: process.env });
13842
14876
  for (const record of report.records) {
13843
14877
  const activity = buildSessionDbRecoveryActivity(record);
13844
14878
  if (!activity) throw new Error("could not map session-DB recovery record");
@@ -13919,7 +14953,7 @@ async function importOpenCodeV2CredentialsForState(state) {
13919
14953
  }
13920
14954
  const result = await importOpenCodeV2Credentials({
13921
14955
  request: client.request,
13922
- homeDir: homedir7(),
14956
+ homeDir: homedir8(),
13923
14957
  directory,
13924
14958
  guard: state.opencodeV2CredentialImportGuard,
13925
14959
  log: (message, level = "info") => logActivity(state, { type: "info", level, message })
@@ -13996,6 +15030,125 @@ async function handleAuthError(state, error2) {
13996
15030
  return { success: false };
13997
15031
  }
13998
15032
  }
15033
+ function logCredentialAdoptionActivity(state, activity, level) {
15034
+ const message = (() => {
15035
+ switch (activity.outcome) {
15036
+ case "deferred":
15037
+ return `CREDENTIAL-ADOPTION: deferred generation ${activity.generation} with ${activity.active_sessions} active session(s)`;
15038
+ case "reloaded_ready":
15039
+ return `CREDENTIAL-ADOPTION: adopted generation ${activity.generation} with ${activity.active_sessions} active session(s); readiness probe succeeded after ${activity.probe_attempts} attempt(s)`;
15040
+ case "reloaded_interrupted": {
15041
+ 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`;
15042
+ return `CREDENTIAL-ADOPTION: generation ${activity.generation} with ${activity.active_sessions} active session(s); replacement instance answered ${response} after ${activity.probe_attempts} probe attempt(s)`;
15043
+ }
15044
+ case "reload_failed":
15045
+ return `CREDENTIAL-ADOPTION: failed to adopt generation ${activity.generation} with ${activity.active_sessions} active session(s)`;
15046
+ case "skipped_v2":
15047
+ return `CREDENTIAL-ADOPTION: skipped generation ${activity.generation} with ${activity.active_sessions} active session(s) on OpenCode V2`;
15048
+ }
15049
+ })();
15050
+ logActivity(state, {
15051
+ type: level === "error" ? "error" : "info",
15052
+ level,
15053
+ ...level === "error" ? { error: message } : { message },
15054
+ metadata: {
15055
+ ...activity,
15056
+ opencode_version: state.opencodeVersion,
15057
+ port: state.port
15058
+ }
15059
+ });
15060
+ }
15061
+ async function adoptCredentialGeneration(state, generation, adoption, activeSessions, deferredForMs) {
15062
+ const reload = await disposeAndReloadProviderConfig(
15063
+ state.port,
15064
+ state.opencodeClient ?? void 0
15065
+ );
15066
+ if (reload.outcome === "failed") {
15067
+ logCredentialAdoptionActivity(
15068
+ state,
15069
+ {
15070
+ outcome: "reload_failed",
15071
+ generation,
15072
+ active_sessions: activeSessions,
15073
+ adoption,
15074
+ deferred_for_ms: deferredForMs,
15075
+ reload_status: reload.status,
15076
+ reload_detail: reload.detail,
15077
+ probe_attempts: null,
15078
+ probe_status: null,
15079
+ probe_body_empty: null,
15080
+ probe_detail: null
15081
+ },
15082
+ "error"
15083
+ );
15084
+ return;
15085
+ }
15086
+ if (reload.outcome === "skipped_v2") {
15087
+ logCredentialAdoptionActivity(
15088
+ state,
15089
+ {
15090
+ outcome: "skipped_v2",
15091
+ generation,
15092
+ active_sessions: activeSessions,
15093
+ adoption,
15094
+ deferred_for_ms: deferredForMs,
15095
+ reload_status: null,
15096
+ reload_detail: null,
15097
+ probe_attempts: null,
15098
+ probe_status: null,
15099
+ probe_body_empty: null,
15100
+ probe_detail: null
15101
+ },
15102
+ "debug"
15103
+ );
15104
+ return;
15105
+ }
15106
+ let lastProbe = null;
15107
+ let attempts = 0;
15108
+ for (attempts = 1; attempts <= CREDENTIAL_ADOPTION_PROBE_ATTEMPTS; attempts++) {
15109
+ lastProbe = await probeOpenCodeRequestReadiness(state.port, state.opencodeClient ?? void 0);
15110
+ if (lastProbe.ok) {
15111
+ logCredentialAdoptionActivity(
15112
+ state,
15113
+ {
15114
+ outcome: "reloaded_ready",
15115
+ generation,
15116
+ active_sessions: activeSessions,
15117
+ adoption,
15118
+ deferred_for_ms: deferredForMs,
15119
+ reload_status: null,
15120
+ reload_detail: null,
15121
+ probe_attempts: attempts,
15122
+ probe_status: lastProbe.status,
15123
+ probe_body_empty: lastProbe.bodyEmpty,
15124
+ probe_detail: lastProbe.detail
15125
+ },
15126
+ "warn"
15127
+ );
15128
+ return;
15129
+ }
15130
+ if (attempts < CREDENTIAL_ADOPTION_PROBE_ATTEMPTS) {
15131
+ await new Promise((resolve5) => setTimeout(resolve5, CREDENTIAL_ADOPTION_PROBE_INTERVAL_MS));
15132
+ }
15133
+ }
15134
+ logCredentialAdoptionActivity(
15135
+ state,
15136
+ {
15137
+ outcome: "reloaded_interrupted",
15138
+ generation,
15139
+ active_sessions: activeSessions,
15140
+ adoption,
15141
+ deferred_for_ms: deferredForMs,
15142
+ reload_status: null,
15143
+ reload_detail: null,
15144
+ probe_attempts: CREDENTIAL_ADOPTION_PROBE_ATTEMPTS,
15145
+ probe_status: lastProbe?.status ?? null,
15146
+ probe_body_empty: lastProbe?.bodyEmpty ?? null,
15147
+ probe_detail: lastProbe?.detail ?? null
15148
+ },
15149
+ "error"
15150
+ );
15151
+ }
13999
15152
  async function driveChannels(state, driver) {
14000
15153
  let idlePolls = 0;
14001
15154
  let idleMs = 0;
@@ -14005,6 +15158,15 @@ async function driveChannels(state, driver) {
14005
15158
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
14006
15159
  let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
14007
15160
  let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
15161
+ const credentialAdoption = {
15162
+ generation: 0,
15163
+ pendingGeneration: null,
15164
+ pendingSinceMs: null,
15165
+ deferredGeneration: null,
15166
+ adoption: 0
15167
+ };
15168
+ const stayAwakeHolds = () => state.stayAwakeUntilMs !== null && Date.now() < state.stayAwakeUntilMs;
15169
+ let stayAwakeSuppressionLogged = false;
14008
15170
  while (state.running) {
14009
15171
  const cycleStartedAtMs = performance.now();
14010
15172
  let idleThisCycle = false;
@@ -14046,12 +15208,30 @@ async function driveChannels(state, driver) {
14046
15208
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
14047
15209
  if (opencodeAuthApplied) state.openaiUsageRearm?.();
14048
15210
  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
- );
15211
+ credentialAdoption.generation++;
15212
+ if (state.opencodeApiVersion === "v2") {
15213
+ logCredentialAdoptionActivity(
15214
+ state,
15215
+ {
15216
+ outcome: "skipped_v2",
15217
+ generation: credentialAdoption.generation,
15218
+ active_sessions: driver.protectedSessionIds().size,
15219
+ adoption: null,
15220
+ deferred_for_ms: null,
15221
+ reload_status: null,
15222
+ reload_detail: null,
15223
+ probe_attempts: null,
15224
+ probe_status: null,
15225
+ probe_body_empty: null,
15226
+ probe_detail: null
15227
+ },
15228
+ "debug"
15229
+ );
15230
+ } else {
15231
+ credentialAdoption.pendingGeneration = credentialAdoption.generation;
15232
+ credentialAdoption.pendingSinceMs = Date.now();
15233
+ credentialAdoption.deferredGeneration = null;
15234
+ }
14055
15235
  }
14056
15236
  if (opencodeAuthApplied && state.opencodeApiVersion === "v2") {
14057
15237
  void importOpenCodeV2CredentialsForState(state).catch(
@@ -14106,6 +15286,49 @@ async function driveChannels(state, driver) {
14106
15286
  }
14107
15287
  }
14108
15288
  }
15289
+ if (credentialAdoption.pendingGeneration !== null) {
15290
+ const activeSessions = driver.protectedSessionIds();
15291
+ const generation = credentialAdoption.pendingGeneration;
15292
+ if (activeSessions.size > 0) {
15293
+ if (credentialAdoption.deferredGeneration !== generation) {
15294
+ credentialAdoption.deferredGeneration = generation;
15295
+ logCredentialAdoptionActivity(
15296
+ state,
15297
+ {
15298
+ outcome: "deferred",
15299
+ generation,
15300
+ active_sessions: activeSessions.size,
15301
+ adoption: null,
15302
+ deferred_for_ms: null,
15303
+ reload_status: null,
15304
+ reload_detail: null,
15305
+ probe_attempts: null,
15306
+ probe_status: null,
15307
+ probe_body_empty: null,
15308
+ probe_detail: null
15309
+ },
15310
+ "warn"
15311
+ );
15312
+ }
15313
+ } else {
15314
+ const deferredForMs = credentialAdoption.deferredGeneration === generation && credentialAdoption.pendingSinceMs !== null ? Math.max(0, Date.now() - credentialAdoption.pendingSinceMs) : null;
15315
+ credentialAdoption.pendingGeneration = null;
15316
+ credentialAdoption.pendingSinceMs = null;
15317
+ credentialAdoption.adoption++;
15318
+ void adoptCredentialGeneration(
15319
+ state,
15320
+ generation,
15321
+ credentialAdoption.adoption,
15322
+ activeSessions.size,
15323
+ deferredForMs
15324
+ ).catch(
15325
+ (error2) => logActivity(state, {
15326
+ type: "error",
15327
+ 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)}`
15328
+ })
15329
+ );
15330
+ }
15331
+ }
14109
15332
  await new Promise((resolve5) => setTimeout(resolve5, CHANNEL_POLL_INTERVAL_MS));
14110
15333
  const cycleMs = performance.now() - cycleStartedAtMs;
14111
15334
  if (idleThisCycle) idleMs += cycleMs;
@@ -14119,11 +15342,20 @@ async function driveChannels(state, driver) {
14119
15342
  if (state.interactive) displayStatus(state);
14120
15343
  break;
14121
15344
  }
14122
- if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3) {
15345
+ if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3 && !stayAwakeHolds()) {
14123
15346
  logActivity(state, { type: "info", message: "Idle timeout reached" });
14124
15347
  if (state.interactive) displayStatus(state);
14125
15348
  break;
14126
15349
  }
15350
+ if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3 && stayAwakeHolds()) {
15351
+ if (!stayAwakeSuppressionLogged) {
15352
+ stayAwakeSuppressionLogged = true;
15353
+ logActivity(state, {
15354
+ type: "info",
15355
+ message: "Idle, but staying awake until the requested minimum passes"
15356
+ });
15357
+ }
15358
+ }
14127
15359
  }
14128
15360
  }
14129
15361
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
@@ -14141,7 +15373,7 @@ function shouldWarnForReclaimSkip(reason) {
14141
15373
  return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
14142
15374
  }
14143
15375
  function sessionDbPath() {
14144
- return join12(homedir7(), ".local", "share", "opencode", "opencode.db");
15376
+ return join13(homedir8(), ".local", "share", "opencode", "opencode.db");
14145
15377
  }
14146
15378
  function logSessionCleanupActivity(state, phase, entry) {
14147
15379
  logActivity(state, {
@@ -14385,7 +15617,7 @@ function scheduleSessionCleanup(state, driver, options) {
14385
15617
  for (const warning2 of config.warnings) {
14386
15618
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
14387
15619
  }
14388
- const dbBytes = statSessionDbBytes(homedir7());
15620
+ const dbBytes = statSessionDbBytes(homedir8());
14389
15621
  void (async () => {
14390
15622
  const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
14391
15623
  if (reclaimAvailability !== null) {
@@ -14452,10 +15684,12 @@ function scheduleUsageReporting(state, params) {
14452
15684
  let phase = "dormant";
14453
15685
  let rearmRequested = false;
14454
15686
  const armProbe = () => {
15687
+ if (!state.running) return;
14455
15688
  phase = "probe-pending";
14456
15689
  params.setTimer(setTimeout(() => void tick(true), params.firstDelayMs()));
14457
15690
  };
14458
15691
  const scheduleNextTick = () => {
15692
+ if (!state.running) return;
14459
15693
  if (rearmRequested) {
14460
15694
  rearmRequested = false;
14461
15695
  armProbe();
@@ -14489,6 +15723,7 @@ function scheduleUsageReporting(state, params) {
14489
15723
  phase = "tick-in-flight";
14490
15724
  try {
14491
15725
  const usage = await params.fetchUsage();
15726
+ if (!state.running) return;
14492
15727
  const result = await params.report(usage);
14493
15728
  if (result.ok) {
14494
15729
  if (consecutiveFailures > 0) {
@@ -14601,7 +15836,7 @@ function scheduleResourceUsageReporting(state, options) {
14601
15836
  });
14602
15837
  return;
14603
15838
  }
14604
- const { collect, stop } = createResourceUsageCollector(homedir7());
15839
+ const { collect, stop } = createResourceUsageCollector(homedir8());
14605
15840
  state.stopResourceUsageSampling = stop;
14606
15841
  let consecutiveFailures = 0;
14607
15842
  const tick = async () => {
@@ -14627,6 +15862,7 @@ function scheduleResourceUsageReporting(state, options) {
14627
15862
  message: lowDiskDecision.warning
14628
15863
  });
14629
15864
  }
15865
+ if (!state.running) return;
14630
15866
  const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
14631
15867
  if (result.ok) {
14632
15868
  if (consecutiveFailures > 0) {
@@ -14659,7 +15895,9 @@ function scheduleResourceUsageReporting(state, options) {
14659
15895
  message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
14660
15896
  });
14661
15897
  } finally {
14662
- state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
15898
+ if (state.running) {
15899
+ state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
15900
+ }
14663
15901
  }
14664
15902
  };
14665
15903
  state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
@@ -14839,7 +16077,7 @@ async function run(options) {
14839
16077
  let fileSyncDirectories;
14840
16078
  try {
14841
16079
  logLevel = resolveLogLevel(options);
14842
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir7());
16080
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir8());
14843
16081
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
14844
16082
  throw new Error(
14845
16083
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -14862,6 +16100,8 @@ async function run(options) {
14862
16100
  port: options.port ?? 4096,
14863
16101
  conversationFilter: options.conversation ?? null,
14864
16102
  idleTimeout: options.idleTimeout ?? null,
16103
+ stayAwakeSeconds: null,
16104
+ stayAwakeUntilMs: null,
14865
16105
  json: options.json ?? false,
14866
16106
  interactive,
14867
16107
  logLevel,
@@ -15051,6 +16291,17 @@ async function run(options) {
15051
16291
  }
15052
16292
  spinner?.succeed(`Runner: ${validation.agent.name || state.agentId}`);
15053
16293
  state.agentName = validation.agent.name;
16294
+ const requestedStayAwakeSeconds = validation.agent?.wake_stay_awake_seconds;
16295
+ if (typeof requestedStayAwakeSeconds === "number" && Number.isFinite(requestedStayAwakeSeconds) && requestedStayAwakeSeconds > 0) {
16296
+ state.stayAwakeSeconds = requestedStayAwakeSeconds;
16297
+ } else if (requestedStayAwakeSeconds !== void 0 && requestedStayAwakeSeconds !== null) {
16298
+ state.stayAwakeSeconds = null;
16299
+ log2(
16300
+ state,
16301
+ `Ignoring invalid wake_stay_awake_seconds (type: ${typeof requestedStayAwakeSeconds})`,
16302
+ "warn"
16303
+ );
16304
+ }
15054
16305
  const microvmId = process.env.MICROVM_ID?.trim();
15055
16306
  if (microvmId) {
15056
16307
  const reported = await reportMicrovmId(state.agentId, state.authHeader, microvmId);
@@ -15112,7 +16363,7 @@ async function run(options) {
15112
16363
  state.credentialSync?.arm();
15113
16364
  const dependencyInstallDiagnosticReporter = createDependencyInstallDiagnosticReporter({
15114
16365
  context: {
15115
- homeDir: homedir7(),
16366
+ homeDir: homedir8(),
15116
16367
  cwd: process.cwd(),
15117
16368
  microvmId: process.env.MICROVM_ID?.trim(),
15118
16369
  euid: process.getuid?.(),
@@ -15135,7 +16386,7 @@ async function run(options) {
15135
16386
  dependencyInstallDiagnosticReporter
15136
16387
  );
15137
16388
  const openCodeLogTail = tailOpenCodeLogFile(
15138
- resolveOpenCodeLogPath(homedir7(), process.env),
16389
+ resolveOpenCodeLogPath(homedir8(), process.env),
15139
16390
  openCodeActivityForwarder
15140
16391
  );
15141
16392
  state.stopOpenCodeLogTail = () => {
@@ -15210,6 +16461,44 @@ async function run(options) {
15210
16461
  for (const warning2 of opencodeVersionWarnings) {
15211
16462
  logActivity(state, { type: "info", level: "warn", message: warning2 });
15212
16463
  }
16464
+ const managedSetupOptions = {
16465
+ agentId: state.agentId,
16466
+ apiUrl: getApiUrlConfig(),
16467
+ getAuthHeader: () => state.authHeader,
16468
+ autoConfigureCredentialsProxy: options.autoConfigureCredentialsProxy,
16469
+ log: (message, level = "info") => logActivity(state, {
16470
+ type: level === "error" ? "error" : "info",
16471
+ level,
16472
+ ...level === "error" ? { error: message } : { message }
16473
+ })
16474
+ };
16475
+ let managedDiscovery = await discoverManagedInferenceSetup(managedSetupOptions);
16476
+ if (managedDiscovery.kind === "none") {
16477
+ managedDiscovery = await enrollManagedInferenceSetup(managedSetupOptions);
16478
+ }
16479
+ let managedPreparation = { kind: "none" };
16480
+ if (managedDiscovery.kind === "failed") {
16481
+ managedPreparation = managedDiscovery;
16482
+ managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
16483
+ } else if (managedDiscovery.kind === "setup") {
16484
+ const managedHealth = opencodeVersion === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
16485
+ managedPreparation = await prepareManagedInferenceSetup(
16486
+ managedSetupOptions,
16487
+ managedDiscovery,
16488
+ managedHealth
16489
+ );
16490
+ if (managedPreparation.kind === "failed") {
16491
+ managedSetupOptions.log(
16492
+ `MANAGED-INFERENCE-NOT-READY: ${managedPreparation.reason}`,
16493
+ "warn"
16494
+ );
16495
+ } else if (managedPreparation.kind === "ready") {
16496
+ managedSetupOptions.log(
16497
+ `MANAGED-INFERENCE-PREPARED: runner=${state.agentId} generation=${managedPreparation.generation}`,
16498
+ "info"
16499
+ );
16500
+ }
16501
+ }
15213
16502
  const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
15214
16503
  for (const warning2 of maxActiveSessionsWarnings) {
15215
16504
  logActivity(state, { type: "info", level: "warn", message: warning2 });
@@ -15236,6 +16525,32 @@ async function run(options) {
15236
16525
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
15237
16526
  state.opencodeVersion = oc.version;
15238
16527
  state.opencodeApiVersion = opencodeVersion;
16528
+ let configuredPlugins = [];
16529
+ let resolvedPlugins = [];
16530
+ try {
16531
+ const provenanceLog = (message) => logActivity(state, { type: "info", level: "warn", message });
16532
+ configuredPlugins = readConfiguredPluginSpecs({ log: provenanceLog });
16533
+ resolvedPlugins = resolveInstalledPluginVersions({ log: provenanceLog });
16534
+ } catch (error2) {
16535
+ const kind = error2 instanceof Error ? error2.name : "unknown error";
16536
+ logActivity(state, {
16537
+ type: "info",
16538
+ level: "warn",
16539
+ message: `Could not collect OpenCode runtime provenance: ${kind}`
16540
+ });
16541
+ }
16542
+ logActivity(state, {
16543
+ type: "info",
16544
+ level: "info",
16545
+ message: "OpenCode runtime provenance",
16546
+ metadata: {
16547
+ opencode_version: state.opencodeVersion,
16548
+ opencode_api_version: state.opencodeApiVersion,
16549
+ configured_plugins: JSON.stringify(configuredPlugins),
16550
+ resolved_plugins: JSON.stringify(resolvedPlugins)
16551
+ },
16552
+ source: "cli.runtime-provenance"
16553
+ });
15239
16554
  let opencodePassword = null;
15240
16555
  if (opencodeVersion === "v2" && "password" in oc) {
15241
16556
  const value = oc.password;
@@ -15264,7 +16579,7 @@ async function run(options) {
15264
16579
  const provenance = checkSessionDbProvenance({
15265
16580
  dbPath: sessionDbPath(),
15266
16581
  currentVersion: state.opencodeVersion,
15267
- homeDir: homedir7(),
16582
+ homeDir: homedir8(),
15268
16583
  env: process.env
15269
16584
  });
15270
16585
  if (provenance.anomaly) {
@@ -15278,6 +16593,30 @@ async function run(options) {
15278
16593
  }
15279
16594
  }
15280
16595
  state.opencodeConnected = oc.notReadyReason === null;
16596
+ if (managedPreparation.kind === "ready" && oc.notReadyReason === null) {
16597
+ try {
16598
+ const acknowledgement = await acknowledgeManagedInferenceSetup(
16599
+ managedSetupOptions,
16600
+ managedPreparation
16601
+ );
16602
+ if (acknowledgement === "acknowledged") {
16603
+ managedSetupOptions.log(
16604
+ `MANAGED-INFERENCE-ACKNOWLEDGED: runner=${state.agentId} generation=${managedPreparation.generation}`,
16605
+ "info"
16606
+ );
16607
+ } else {
16608
+ managedSetupOptions.log(
16609
+ `MANAGED-INFERENCE-NOT-READY: policy refresh is pending for runner=${state.agentId} generation=${managedPreparation.generation}`,
16610
+ "warn"
16611
+ );
16612
+ }
16613
+ } catch (error2) {
16614
+ managedSetupOptions.log(
16615
+ `MANAGED-INFERENCE-NOT-READY: ${error2 instanceof Error ? error2.message : String(error2)}`,
16616
+ "warn"
16617
+ );
16618
+ }
16619
+ }
15281
16620
  const version2 = state.opencodeVersion ? ` (v${state.opencodeVersion})` : "";
15282
16621
  ocSpinner?.succeed(`OpenCode running on port ${state.port}${version2}`);
15283
16622
  if (!state.interactive && oc.notReadyReason !== null) {
@@ -15294,7 +16633,7 @@ async function run(options) {
15294
16633
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
15295
16634
  }
15296
16635
  }
15297
- await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
16636
+ await disposeAndReloadProviderConfig(state.port, state.opencodeClient ?? void 0);
15298
16637
  if (state.opencodeApiVersion === "v2" && state.opencodeClient !== null) {
15299
16638
  await importOpenCodeV2CredentialsForState(state);
15300
16639
  }
@@ -15336,7 +16675,7 @@ async function run(options) {
15336
16675
  let existingPid;
15337
16676
  if (existsSync3(options.litestreamPidFile)) {
15338
16677
  try {
15339
- const rawPid = readFileSync7(options.litestreamPidFile, "utf8").trim();
16678
+ const rawPid = readFileSync8(options.litestreamPidFile, "utf8").trim();
15340
16679
  const parsedPid = Number(rawPid);
15341
16680
  if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
15342
16681
  existingPid = parsedPid;
@@ -15435,7 +16774,7 @@ async function run(options) {
15435
16774
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
15436
16775
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
15437
16776
  fileSyncDirectories,
15438
- homeDir: homedir7(),
16777
+ homeDir: homedir8(),
15439
16778
  maxActiveSessions,
15440
16779
  log: (entry) => (
15441
16780
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -15460,11 +16799,18 @@ async function run(options) {
15460
16799
  onConnected: (agentId, isReconnect, transition) => {
15461
16800
  state.connected = true;
15462
16801
  state.agentId = agentId;
15463
- const connectionMessage = isReconnect ? `Tunnel reconnected (runner: ${agentId}, generation: ${transition.generation}, attempt: ${transition.attempt}, offline for ${transition.downtimeMs}ms)` : `Tunnel connected (runner: ${agentId}, generation: ${transition.generation})`;
16802
+ const connectionMessage = isReconnect ? `Tunnel reconnected (runner: ${agentId}, generation: ${transition.generation}, relay connection: ${transition.relayConnectionId ?? "unknown"}, attempt: ${transition.attempt}, offline for ${transition.downtimeMs}ms)` : `Tunnel connected (runner: ${agentId}, generation: ${transition.generation}, relay connection: ${transition.relayConnectionId ?? "unknown"})`;
15464
16803
  logActivity(state, {
15465
16804
  type: "info",
15466
16805
  message: connectionMessage
15467
16806
  });
16807
+ if (!isReconnect && state.stayAwakeSeconds !== null && state.stayAwakeUntilMs === null) {
16808
+ state.stayAwakeUntilMs = Date.now() + state.stayAwakeSeconds * 1e3;
16809
+ logActivity(state, {
16810
+ type: "info",
16811
+ message: `Staying awake for at least ${state.stayAwakeSeconds}s as requested by the wake`
16812
+ });
16813
+ }
15468
16814
  if (options.tunnelReadyFile) {
15469
16815
  const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
15470
16816
  if (marker.ok) {
@@ -15486,6 +16832,7 @@ async function run(options) {
15486
16832
  connected: state.opencodeConnected
15487
16833
  }),
15488
16834
  connection_generation: transition.generation,
16835
+ ...transition.relayConnectionId !== null ? { relay_connection_id: transition.relayConnectionId } : {},
15489
16836
  ...isReconnect && transition.downtimeMs !== null ? {
15490
16837
  reconnect_attempt: transition.attempt,
15491
16838
  downtime_ms: transition.downtimeMs
@@ -15516,12 +16863,13 @@ async function run(options) {
15516
16863
  const safeReason2 = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
15517
16864
  logActivity(state, {
15518
16865
  type: "info",
15519
- message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason2})`
16866
+ message: `Tunnel disconnected (generation: ${transition.generation}, relay connection: ${transition.relayConnectionId ?? "unknown"}, code: ${code}, reason: ${safeReason2})`
15520
16867
  });
15521
16868
  emitAgentDisconnected(state.agentId, {
15522
16869
  code,
15523
16870
  reason: safeReason2,
15524
- connection_generation: transition.generation
16871
+ connection_generation: transition.generation,
16872
+ ...transition.relayConnectionId !== null ? { relay_connection_id: transition.relayConnectionId } : {}
15525
16873
  });
15526
16874
  if (state.interactive) displayStatus(state);
15527
16875
  },
@@ -15599,6 +16947,7 @@ async function run(options) {
15599
16947
  if (error2.message === "Unauthorized") tunnelSpinner?.fail("Unauthorized");
15600
16948
  throw error2;
15601
16949
  }
16950
+ if (!state.running) return;
15602
16951
  scheduleSessionCleanup(state, channelDriver, options);
15603
16952
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
15604
16953
  state.openaiUsageRearm = scheduleUsageReporting(state, {
@@ -15671,9 +17020,9 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
15671
17020
  "--endpoint <url>",
15672
17021
  "Evident API base URL (default: production; e.g. http://localhost:3001)"
15673
17022
  ).option("--tunnel <url>", "Tunnel WebSocket URL (default: production; e.g. ws://localhost:8787)").hook("preAction", (thisCommand) => {
15674
- const { endpoint, tunnel } = thisCommand.opts();
15675
- if (endpoint) {
15676
- setEndpoint(endpoint);
17023
+ const { endpoint: endpoint2, tunnel } = thisCommand.opts();
17024
+ if (endpoint2) {
17025
+ setEndpoint(endpoint2);
15677
17026
  }
15678
17027
  if (tunnel) {
15679
17028
  setTunnelUrl(tunnel);
@@ -15747,6 +17096,9 @@ program.command("run").description("Connect to Evident and process messages").op
15747
17096
  ).option(
15748
17097
  "--opencode-config-overlay <path>",
15749
17098
  "Apply this runner-provided OpenCode config before starting OpenCode."
17099
+ ).option(
17100
+ "--auto-configure-credentials-proxy",
17101
+ "Install Evident's generated OpenCode providers for the runner's credential pool (off by default)."
15750
17102
  ).option(
15751
17103
  "--credential-sync-marker <path>",
15752
17104
  "Own the interval credential sync and write this marker once the shutdown flush has finished, so the runner image's lifecycle hooks can wait on it."
@@ -15790,6 +17142,7 @@ program.command("run").description("Connect to Evident and process messages").op
15790
17142
  restoreSessionDb: options.restoreSessionDb,
15791
17143
  restoreRunnerCredentials: options.restoreRunnerCredentials,
15792
17144
  opencodeConfigOverlay: options.opencodeConfigOverlay,
17145
+ autoConfigureCredentialsProxy: options.autoConfigureCredentialsProxy,
15793
17146
  credentialSyncMarker: options.credentialSyncMarker
15794
17147
  });
15795
17148
  }