@evident-ai/cli 3.5.2-dev.ffa823b → 3.6.1-dev.2fd7890

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.2fd7890" : 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 {
@@ -3518,19 +3691,32 @@ function isAbortedTerminalReply(messages, userMessageId) {
3518
3691
  }
3519
3692
  return false;
3520
3693
  }
3694
+ var CLAUDE_AUTH_EXPIRY_MESSAGES = Object.freeze([
3695
+ "Claude Code credentials are unavailable or expired. Run `claude` to refresh them."
3696
+ ]);
3697
+ function extractReplyErrorShape(reply) {
3698
+ const error2 = errorOf(reply);
3699
+ if (error2 == null || typeof error2 !== "object") {
3700
+ return { name: null, statusCode: null };
3701
+ }
3702
+ const e = error2;
3703
+ const name = typeof e.name === "string" ? e.name.slice(0, 100) : null;
3704
+ const data = e.data;
3705
+ const statusCode = typeof data?.statusCode === "number" && Number.isFinite(data.statusCode) ? data.statusCode : null;
3706
+ return { name, statusCode };
3707
+ }
3521
3708
  function classifyReplyAuthError(reply) {
3522
3709
  const error2 = errorOf(reply);
3523
3710
  if (error2 == null || typeof error2 !== "object") return null;
3524
3711
  const e = error2;
3525
3712
  const replyProviderId = reply?.info?.providerID ?? null;
3526
3713
  const replyModelId = reply?.info?.modelID ?? null;
3714
+ const data = e.data;
3527
3715
  if (e.name === "ProviderAuthError") {
3528
- const data = e.data;
3529
3716
  const providerId = typeof data?.providerID === "string" && data.providerID || replyProviderId;
3530
3717
  return { kind: "model_auth", providerId, modelId: replyModelId, reason: "missing" };
3531
3718
  }
3532
3719
  if (e.name === "APIError") {
3533
- const data = e.data;
3534
3720
  const statusCode = data?.statusCode;
3535
3721
  if (statusCode === 401 || statusCode === 403) {
3536
3722
  return {
@@ -3541,11 +3727,23 @@ function classifyReplyAuthError(reply) {
3541
3727
  };
3542
3728
  }
3543
3729
  }
3730
+ const errorMessage3 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
3731
+ if (errorMessage3 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage3.includes(knownMessage))) {
3732
+ return {
3733
+ kind: "model_auth",
3734
+ providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
3735
+ modelId: replyModelId,
3736
+ reason: "rejected"
3737
+ };
3738
+ }
3544
3739
  return null;
3545
3740
  }
3546
3741
  function messageFailure(messages, userMessageId) {
3547
3742
  return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
3548
3743
  }
3744
+ function isProviderModelNotFoundError(reason) {
3745
+ return reason.includes("Model not found:");
3746
+ }
3549
3747
  function findLatestSubagentAuthOutcome(messages, sinceMs) {
3550
3748
  if (!messages || messages.length === 0) return null;
3551
3749
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -3754,23 +3952,49 @@ async function readSessionErrorStream(port, options, client) {
3754
3952
  if (reader) void reader.cancel().catch(() => void 0);
3755
3953
  }
3756
3954
  }
3757
- async function reloadProviderCache(port, client) {
3758
- if (client?.version === "v2") return;
3955
+ async function disposeAndReloadProviderConfig(port, client) {
3956
+ if (client?.version === "v2") return { outcome: "skipped_v2" };
3759
3957
  try {
3760
- const res = await requestWithClient(port, client, "/config", {
3761
- method: "PATCH",
3762
- headers: { "Content-Type": "application/json" },
3763
- body: JSON.stringify({})
3764
- });
3958
+ const res = await requestWithClient(
3959
+ port,
3960
+ client,
3961
+ "/config",
3962
+ {
3963
+ method: "PATCH",
3964
+ headers: { "Content-Type": "application/json" },
3965
+ body: JSON.stringify({})
3966
+ },
3967
+ { allowStatuses: ALL_HTTP_STATUSES }
3968
+ );
3765
3969
  if (!res.ok) {
3766
3970
  console.error(
3767
- `[reloadProviderCache] PATCH /config returned HTTP ${res.status} (port ${port})`
3971
+ `[disposeAndReloadProviderConfig] PATCH /config returned HTTP ${res.status} (port ${port})`
3768
3972
  );
3973
+ return { outcome: "failed", status: res.status, detail: null };
3769
3974
  }
3975
+ return { outcome: "reloaded" };
3770
3976
  } catch (err) {
3771
3977
  console.error(
3772
- `[reloadProviderCache] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3978
+ `[disposeAndReloadProviderConfig] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3773
3979
  );
3980
+ return {
3981
+ outcome: "failed",
3982
+ status: null,
3983
+ detail: err instanceof Error ? err.message : String(err)
3984
+ };
3985
+ }
3986
+ }
3987
+ async function probeOpenCodeRequestReadiness(port, client) {
3988
+ try {
3989
+ const res = await requestWithClient(port, client, "/config", void 0, {
3990
+ allowStatuses: ALL_HTTP_STATUSES
3991
+ });
3992
+ const body = await res.text();
3993
+ return { ok: res.ok, status: res.status, bodyEmpty: body.trim().length === 0, detail: null };
3994
+ } catch (err) {
3995
+ const detail = err instanceof Error ? err.message : String(err);
3996
+ console.error(`[probeOpenCodeRequestReadiness] GET /config failed (port ${port}): ${detail}`);
3997
+ return { ok: false, status: null, bodyEmpty: null, detail };
3774
3998
  }
3775
3999
  }
3776
4000
 
@@ -4166,6 +4390,11 @@ async function listV2Permissions(client, sessionId) {
4166
4390
  return null;
4167
4391
  }
4168
4392
  }
4393
+ async function interruptV2Session(client, sessionId) {
4394
+ await client.request(`/api/session/${encodeURIComponent(sessionId)}/interrupt`, {
4395
+ method: "POST"
4396
+ });
4397
+ }
4169
4398
  async function getV2ActiveSessions(client) {
4170
4399
  try {
4171
4400
  const body = await readJson(await client.request("/api/session/active"));
@@ -4568,11 +4797,11 @@ function failureMessage(status2) {
4568
4797
  function isRetryableStatus(status2) {
4569
4798
  return CONNECT_KEY_RETRY_STATUSES.includes(status2);
4570
4799
  }
4571
- async function importOneCredential(request2, credential, sleepForRetry, log3) {
4800
+ async function importOneCredential(request3, credential, sleepForRetry, log3) {
4572
4801
  const path = `/api/integration/${encodeURIComponent(credential.integrationId)}/connect/key`;
4573
4802
  for (let attempt = 1; attempt <= CONNECT_KEY_MAX_ATTEMPTS; attempt += 1) {
4574
4803
  try {
4575
- const response = await request2(
4804
+ const response = await request3(
4576
4805
  path,
4577
4806
  {
4578
4807
  method: "POST",
@@ -4607,7 +4836,7 @@ async function importOneCredential(request2, credential, sleepForRetry, log3) {
4607
4836
  throw new Error("OpenCode credential import retry loop did not return");
4608
4837
  }
4609
4838
  async function importOpenCodeCredentials({
4610
- request: request2,
4839
+ request: request3,
4611
4840
  classified,
4612
4841
  connectedIntegrationIds,
4613
4842
  sleepForRetry = sleep2,
@@ -4619,7 +4848,7 @@ async function importOpenCodeCredentials({
4619
4848
  result.alreadyConnected.push(credential.integrationId);
4620
4849
  continue;
4621
4850
  }
4622
- const outcome = await importOneCredential(request2, credential, sleepForRetry, log3);
4851
+ const outcome = await importOneCredential(request3, credential, sleepForRetry, log3);
4623
4852
  if (outcome.ok) result.imported.push(credential.integrationId);
4624
4853
  else result.failed.push(outcome.failure);
4625
4854
  }
@@ -4637,7 +4866,7 @@ function parseIntegrationList(value) {
4637
4866
  return { integrationIds, hasData: value.data.length > 0 };
4638
4867
  }
4639
4868
  async function getConnectedOpenCodeIntegrationIds({
4640
- request: request2,
4869
+ request: request3,
4641
4870
  directory,
4642
4871
  sleepForRetry = sleep2,
4643
4872
  log: log3
@@ -4645,7 +4874,7 @@ async function getConnectedOpenCodeIntegrationIds({
4645
4874
  const path = `/api/integration?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
4646
4875
  for (let attempt = 1; attempt <= INTEGRATION_LIST_MAX_READS; attempt += 1) {
4647
4876
  try {
4648
- const response = await request2(path);
4877
+ const response = await request3(path);
4649
4878
  const parsed = parseIntegrationList(await response.json());
4650
4879
  if (parsed === null) {
4651
4880
  log3(
@@ -4674,7 +4903,7 @@ async function getConnectedOpenCodeIntegrationIds({
4674
4903
  return null;
4675
4904
  }
4676
4905
  async function importOpenCodeV2Credentials({
4677
- request: request2,
4906
+ request: request3,
4678
4907
  homeDir,
4679
4908
  directory,
4680
4909
  sleepForRetry = sleep2,
@@ -4692,7 +4921,7 @@ async function importOpenCodeV2Credentials({
4692
4921
  const classified = classifyOpenCodeCredentials(credentials2);
4693
4922
  if (classified.importable.length === 0) return emptyResult(classified.unsupported);
4694
4923
  const connectedIntegrationIds = await getConnectedOpenCodeIntegrationIds({
4695
- request: request2,
4924
+ request: request3,
4696
4925
  directory,
4697
4926
  sleepForRetry,
4698
4927
  log: log3
@@ -4701,7 +4930,7 @@ async function importOpenCodeV2Credentials({
4701
4930
  return emptyResult(classified.unsupported, "connection-list-unavailable");
4702
4931
  }
4703
4932
  return importOpenCodeCredentials({
4704
- request: request2,
4933
+ request: request3,
4705
4934
  classified,
4706
4935
  connectedIntegrationIds,
4707
4936
  sleepForRetry,
@@ -7086,8 +7315,8 @@ import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/p
7086
7315
  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
7316
  var FILE_MODE = 384;
7088
7317
  var DIRECTORY_MODE = 448;
7089
- async function writePushedFile(request2) {
7090
- const { requestedPath, content, allowedDirectories, homeDir } = request2;
7318
+ async function writePushedFile(request3) {
7319
+ const { requestedPath, content, allowedDirectories, homeDir } = request3;
7091
7320
  const bytes = content.byteLength;
7092
7321
  if (allowedDirectories.length === 0) {
7093
7322
  return refuse("file_sync_disabled", "File sync is not enabled on this runner.", {
@@ -7537,7 +7766,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
7537
7766
  var MAX_BUFFERED_SESSION_ERRORS = 256;
7538
7767
  var SESSION_ERROR_RETENTION_TTL_MS = 6e4;
7539
7768
  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: ";
7769
+ var SESSION_ERROR_RETAINED_DETAIL = "Runner-observed OpenCode session error: ";
7541
7770
  var SESSION_ERROR_OBSERVED_CAUSE = "Runner-observed OpenCode session-store cause: ";
7542
7771
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
7543
7772
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
@@ -7661,12 +7890,15 @@ var ChannelDriver = class _ChannelDriver {
7661
7890
  /**
7662
7891
  * Per-conversation throttle state for the #183 recurrence warning (#1618
7663
7892
  * 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`
7893
+ * `firstWedgedAt` anchors `stuck_for_ms`; `warnAnchorAt` is the throttle
7894
+ * anchor from first detection through the first warning, then from each
7895
+ * warning onward; `consecutiveTicks` is reported in the log text so the
7896
+ * operator sees magnitude, not repetition. Cleared the moment the
7897
+ * conversation dispatches anything, or when it drops off the pending list
7898
+ * (`runDrain`'s prune, the dominant real-world path — a wedge normally ends
7899
+ * with its message completing, not a fresh dispatch on the same
7900
+ * conversation) — either way a fresh wedge, if it recurs, is a new
7901
+ * incident. Bounded FIFO, mirroring `supersededSessions`
7670
7902
  * (`MAX_WEDGED_CONVERSATIONS`).
7671
7903
  */
7672
7904
  wedgeWarnings = /* @__PURE__ */ new Map();
@@ -7896,6 +8128,7 @@ var ChannelDriver = class _ChannelDriver {
7896
8128
  subagentInvocationPrefetches = /* @__PURE__ */ new Map();
7897
8129
  /** Serialises drains so a reconnect during a drain doesn't double-process. */
7898
8130
  draining = false;
8131
+ pendingProviderRegistryRepair = null;
7899
8132
  /**
7900
8133
  * Serialises runner-file syncs (#559) so the ~2s poll tick and a concurrent
7901
8134
  * drain ping don't download, write and ack the same file twice.
@@ -8081,8 +8314,33 @@ var ChannelDriver = class _ChannelDriver {
8081
8314
  async runDrain() {
8082
8315
  let dispatched = 0;
8083
8316
  try {
8317
+ const pendingRepair = this.pendingProviderRegistryRepair;
8318
+ if (pendingRepair && this.protectedSessionIds().size === 0) {
8319
+ try {
8320
+ await this.issueProviderRegistryReload(
8321
+ pendingRepair.conversationId,
8322
+ pendingRepair.messageId
8323
+ );
8324
+ } catch (err) {
8325
+ this.log({
8326
+ level: "error",
8327
+ message: `Provider registry repair adoption failed for message ${pendingRepair.messageId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`,
8328
+ conversation_id: pendingRepair.conversationId,
8329
+ message_id: pendingRepair.messageId
8330
+ });
8331
+ }
8332
+ return 0;
8333
+ }
8084
8334
  this.pendingMessageIds.clear();
8085
8335
  const conversations = await this.getPendingConversations();
8336
+ if (this.wedgeWarnings.size > 0) {
8337
+ const pendingConversationIds = new Set(conversations.map(({ id }) => id));
8338
+ for (const conversationId of this.wedgeWarnings.keys()) {
8339
+ if (!pendingConversationIds.has(conversationId)) {
8340
+ this.wedgeWarnings.delete(conversationId);
8341
+ }
8342
+ }
8343
+ }
8086
8344
  if (this.recycleRequestedFlag) {
8087
8345
  this.stop();
8088
8346
  }
@@ -8619,14 +8877,13 @@ var ChannelDriver = class _ChannelDriver {
8619
8877
  * skip-because-already-`dispatched`, the exact signature of a message stuck
8620
8878
  * acknowledged-but-never-worked.
8621
8879
  *
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.
8880
+ * The first detection starts the clock and emits a local `debug` entry. The
8881
+ * `warn` log and paired `dispatch_wedged` signal fire once the wedge has
8882
+ * lasted `wedgeWarningIntervalMs`, then at most once per interval, naming the
8883
+ * consecutive-tick count so the operator sees magnitude rather than repetition.
8884
+ * A watcher stall that the loop-liveness watchdog recovers is expected and is
8885
+ * already reported by its `watcher_recovered` signal, so it does not reach an
8886
+ * operator through this warning.
8630
8887
  *
8631
8888
  * Deliberately does NOT trigger a release: WI-1's `reconcileWatchers` runs
8632
8889
  * unconditionally on this same tick and is already recovering anything it
@@ -8638,36 +8895,41 @@ var ChannelDriver = class _ChannelDriver {
8638
8895
  const existing = this.wedgeWarnings.get(conv.id);
8639
8896
  const firstWedgedAt = existing?.firstWedgedAt ?? now;
8640
8897
  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
8898
+ const warnAnchorAt = existing?.warnAnchorAt ?? now;
8899
+ const dueForWarn = now - warnAnchorAt >= this.wedgeWarningIntervalMs;
8900
+ if (!existing) {
8901
+ this.log({
8902
+ level: "debug",
8903
+ 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.`,
8904
+ conversation_id: conv.id
8905
+ });
8906
+ }
8907
+ let untracked = 0;
8908
+ if (dueForWarn) {
8909
+ const stuckForMs = now - firstWedgedAt;
8910
+ untracked = this.countUntrackedIds(messages);
8911
+ this.log({
8912
+ level: "warn",
8913
+ 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.`),
8914
+ conversation_id: conv.id
8648
8915
  });
8649
- return;
8650
8916
  }
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
8917
  this.wedgeWarnings.delete(conv.id);
8659
- this.wedgeWarnings.set(conv.id, { firstWedgedAt, lastWarnedAt: now, consecutiveTicks });
8918
+ this.wedgeWarnings.set(conv.id, {
8919
+ firstWedgedAt,
8920
+ warnAnchorAt: dueForWarn ? now : warnAnchorAt,
8921
+ consecutiveTicks
8922
+ });
8660
8923
  while (this.wedgeWarnings.size > MAX_WEDGED_CONVERSATIONS) {
8661
8924
  const oldest = this.wedgeWarnings.keys().next().value;
8662
8925
  if (oldest === void 0) break;
8663
8926
  this.wedgeWarnings.delete(oldest);
8664
8927
  }
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
- }
8928
+ if (!dueForWarn) return;
8929
+ void this.postSignal(conv.id, messages[0].id, "dispatch_wedged", {
8930
+ stuck_for_ms: now - firstWedgedAt,
8931
+ untracked
8932
+ });
8671
8933
  }
8672
8934
  /**
8673
8935
  * How many of `messages`' ids are tracked by NO watcher's `inFlight` (#1618
@@ -8803,6 +9065,34 @@ var ChannelDriver = class _ChannelDriver {
8803
9065
  if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
8804
9066
  const ongoing = await this.isSessionOngoing(sessionId);
8805
9067
  if (ongoing === false) {
9068
+ const age = this.absoluteAgeForRow(message.processing_started_at);
9069
+ if (age.tripped && age.anchorMs !== null) {
9070
+ try {
9071
+ await this.markFailed(
9072
+ conv.id,
9073
+ message.id,
9074
+ sessionId,
9075
+ this.absoluteCeilingError(message.id, sessionId, age.anchorMs),
9076
+ void 0,
9077
+ {
9078
+ classification: null,
9079
+ errorShape: extractReplyErrorShape(findLastAssistantReplyFor(messages, ocId ?? ""))
9080
+ }
9081
+ );
9082
+ } catch (err) {
9083
+ if (err instanceof ChannelAuthError) throw err;
9084
+ this.log({
9085
+ level: "warn",
9086
+ 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)}`,
9087
+ conversation_id: conv.id,
9088
+ message_id: message.id
9089
+ });
9090
+ return "unresolved";
9091
+ }
9092
+ this.clearRedriveUnresolved(message.id);
9093
+ void this.postSignal(conv.id, message.id, "redrive_ceiling_refused");
9094
+ return "settled";
9095
+ }
8806
9096
  this.log({
8807
9097
  level: "info",
8808
9098
  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 +9163,29 @@ var ChannelDriver = class _ChannelDriver {
8873
9163
  message_id: message.id
8874
9164
  });
8875
9165
  }
9166
+ if (this.absoluteAgeForRow(message.processing_started_at).tripped) {
9167
+ try {
9168
+ await this.markFailed(
9169
+ conv.id,
9170
+ message.id,
9171
+ sessionId,
9172
+ this.absoluteCeilingError(message.id, sessionId, anchorMs)
9173
+ );
9174
+ } catch (err) {
9175
+ if (err instanceof ChannelAuthError) throw err;
9176
+ this.log({
9177
+ level: "warn",
9178
+ 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)}`,
9179
+ conversation_id: conv.id,
9180
+ message_id: message.id
9181
+ });
9182
+ const bound = await this.boundRedriveOutcome(conv, message, "reattach");
9183
+ return bound === "abandoned" ? "abandoned" : "unresolved";
9184
+ }
9185
+ this.clearRedriveUnresolved(message.id);
9186
+ void this.postSignal(conv.id, message.id, "redrive_ceiling_refused");
9187
+ return "abandoned";
9188
+ }
8876
9189
  const title = await this.resolveSessionTitle(sessionId, conv.id);
8877
9190
  try {
8878
9191
  await this.markProcessing(conv.id, message.id, sessionId, ocId, title);
@@ -9139,8 +9452,7 @@ var ChannelDriver = class _ChannelDriver {
9139
9452
  const since = this.redriveOutcomeFailingSince.get(message.id);
9140
9453
  if (since === void 0) this.redriveOutcomeFailingSince.set(message.id, now);
9141
9454
  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;
9455
+ const absoluteAgeTripped = this.absoluteAgeForRow(message.processing_started_at).tripped;
9144
9456
  if (!durationTripped && !absoluteAgeTripped) {
9145
9457
  this.signalRedriveOutcomeUnreported(conv, message, outcome);
9146
9458
  return "retry";
@@ -9179,6 +9491,15 @@ var ChannelDriver = class _ChannelDriver {
9179
9491
  });
9180
9492
  return "abandoned";
9181
9493
  }
9494
+ /** Row-level guards use the durable server stamp; in-flight guards use processingAnchorMs. */
9495
+ absoluteAgeForRow(stamp) {
9496
+ const anchorMs = stamp ? Date.parse(stamp) : NaN;
9497
+ return Number.isNaN(anchorMs) ? { tripped: false, anchorMs: null } : { tripped: this.now() - anchorMs >= ABSOLUTE_MAX_PROCESSING_MS, anchorMs };
9498
+ }
9499
+ absoluteCeilingError(messageId, sessionId, anchorMs, retainedReason = this.readRetainedSessionError(sessionId)) {
9500
+ 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.`;
9501
+ return retainedReason ? `${baseError} ${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}` : baseError;
9502
+ }
9182
9503
  /**
9183
9504
  * Record one poll outcome toward the re-drive fence's consecutive-identical-
9184
9505
  * failure streak (#1348) and return the resulting count. `signature === null`
@@ -9620,10 +9941,7 @@ var ChannelDriver = class _ChannelDriver {
9620
9941
  opencodeMessageId,
9621
9942
  message,
9622
9943
  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,
9944
+ processingAnchorMs: this.absoluteAgeForRow(message.processing_started_at).anchorMs ?? processedAtMs,
9627
9945
  deadline: watchDeadlineAnchorMs + this.pausedMaxWaitMs,
9628
9946
  // The server row is ALREADY `processing`; do not re-fire markProcessing.
9629
9947
  started: true,
@@ -9933,14 +10251,40 @@ var ChannelDriver = class _ChannelDriver {
9933
10251
  conversation_id: watcher.conv.id,
9934
10252
  message_id: inFlight.evidentMessageId
9935
10253
  });
9936
- await this.markFailed(
9937
- watcher.conv.id,
9938
- inFlight.evidentMessageId,
9939
- event.sessionId,
9940
- `OpenCode could not run this turn: ${event.reason}`
9941
- );
10254
+ const isMissingProvider = isProviderModelNotFoundError(event.reason);
10255
+ if (isMissingProvider) {
10256
+ const dispatchedModel = inFlight.message.opencode_model;
10257
+ const separator = dispatchedModel?.indexOf("/") ?? -1;
10258
+ const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
10259
+ await this.markFailed(
10260
+ watcher.conv.id,
10261
+ inFlight.evidentMessageId,
10262
+ event.sessionId,
10263
+ `OpenCode could not run this turn: ${event.reason}`,
10264
+ void 0,
10265
+ {
10266
+ classification: {
10267
+ kind: "model_auth",
10268
+ providerId: hasProviderAndModel ? dispatchedModel.slice(0, separator) : null,
10269
+ modelId: hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null,
10270
+ reason: "missing"
10271
+ },
10272
+ errorShape: { name: null, statusCode: null }
10273
+ }
10274
+ );
10275
+ } else {
10276
+ await this.markFailed(
10277
+ watcher.conv.id,
10278
+ inFlight.evidentMessageId,
10279
+ event.sessionId,
10280
+ `OpenCode could not run this turn: ${event.reason}`
10281
+ );
10282
+ }
9942
10283
  inFlight.done = true;
9943
10284
  this.removeInFlight(watcher, inFlight.evidentMessageId);
10285
+ if (isMissingProvider) {
10286
+ await this.repairProviderRegistry(watcher.conv.id, inFlight.evidentMessageId);
10287
+ }
9944
10288
  } catch (err) {
9945
10289
  if (err instanceof ChannelAuthError) {
9946
10290
  this.log({
@@ -9961,6 +10305,44 @@ var ChannelDriver = class _ChannelDriver {
9961
10305
  this.sessionErrorHandled.delete(inFlight.evidentMessageId);
9962
10306
  }
9963
10307
  }
10308
+ async repairProviderRegistry(conversationId, messageId) {
10309
+ if (this.isV2) {
10310
+ void this.postSignal(conversationId, messageId, "provider_registry_reload_unsupported");
10311
+ return;
10312
+ }
10313
+ const activeSessions = this.protectedSessionIds().size;
10314
+ if (activeSessions > 0 || this.draining) {
10315
+ this.pendingProviderRegistryRepair = { conversationId, messageId };
10316
+ void this.postSignal(conversationId, messageId, "provider_registry_repair_deferred", {
10317
+ active_sessions: activeSessions
10318
+ });
10319
+ this.log({
10320
+ level: "warn",
10321
+ message: `Provider registry repair deferred until a lifecycle-safe point; ${activeSessions} active session(s) protected`,
10322
+ conversation_id: conversationId,
10323
+ message_id: messageId
10324
+ });
10325
+ return;
10326
+ }
10327
+ await this.issueProviderRegistryReload(conversationId, messageId);
10328
+ }
10329
+ async issueProviderRegistryReload(conversationId, messageId) {
10330
+ this.pendingProviderRegistryRepair = null;
10331
+ const result = await disposeAndReloadProviderConfig(this.port, this.openCodeClient);
10332
+ if (result.outcome === "failed") {
10333
+ const failure = result.status === null ? `transport failure${result.detail ? `: ${result.detail}` : ""}` : `HTTP ${result.status}`;
10334
+ this.log({
10335
+ level: "warn",
10336
+ message: `Provider registry reload failed (${failure})`,
10337
+ conversation_id: conversationId,
10338
+ message_id: messageId
10339
+ });
10340
+ }
10341
+ const repairOutcome = result.outcome === "reloaded" ? "reloaded" : "reload_failed";
10342
+ void this.postSignal(conversationId, messageId, "provider_registry_reload_issued", {
10343
+ repair_outcome: repairOutcome
10344
+ });
10345
+ }
9964
10346
  /**
9965
10347
  * The per-session polling loop (WI-3). Once per tick it:
9966
10348
  * 1. polls `GET /session/:id/message` once and, per in-flight message,
@@ -10010,7 +10392,9 @@ var ChannelDriver = class _ChannelDriver {
10010
10392
  }
10011
10393
  const { openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk } = await this.pollInteractions(sessionId, watcher, messages);
10012
10394
  if (watcher.generation !== generation) return;
10395
+ const ceilingAttemptsThisTick = /* @__PURE__ */ new Set();
10013
10396
  for (const inFlight of [...watcher.inFlight.values()]) {
10397
+ if (ceilingAttemptsThisTick.has(inFlight.evidentMessageId)) continue;
10014
10398
  await this.serviceInFlightMessage(
10015
10399
  sessionId,
10016
10400
  watcher,
@@ -10019,7 +10403,8 @@ var ChannelDriver = class _ChannelDriver {
10019
10403
  openQuestions,
10020
10404
  openPermissions,
10021
10405
  questionsPolledOk,
10022
- permissionsPolledOk
10406
+ permissionsPolledOk,
10407
+ ceilingAttemptsThisTick
10023
10408
  );
10024
10409
  }
10025
10410
  }
@@ -10060,13 +10445,117 @@ var ChannelDriver = class _ChannelDriver {
10060
10445
  inFlight.deadline = this.now() + this.pausedMaxWaitMs;
10061
10446
  }
10062
10447
  }
10448
+ async settleAbsoluteCeiling(sessionId, watcher, messages, ceilingAttemptsThisTick) {
10449
+ const entries = [...watcher.inFlight.values()];
10450
+ const pastCeiling = entries.filter(
10451
+ (entry) => this.now() - entry.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS
10452
+ );
10453
+ const healthySiblings = entries.length - pastCeiling.length;
10454
+ const landed = [];
10455
+ let allRecordsLanded = true;
10456
+ const retainedReason = this.readRetainedSessionError(sessionId);
10457
+ for (const entry of pastCeiling) {
10458
+ if (!watcher.inFlight.has(entry.evidentMessageId)) continue;
10459
+ ceilingAttemptsThisTick.add(entry.evidentMessageId);
10460
+ const error2 = this.absoluteCeilingError(
10461
+ entry.evidentMessageId,
10462
+ sessionId,
10463
+ entry.processingAnchorMs,
10464
+ retainedReason
10465
+ );
10466
+ this.log({
10467
+ level: "warn",
10468
+ message: error2,
10469
+ conversation_id: watcher.conv.id,
10470
+ message_id: entry.evidentMessageId
10471
+ });
10472
+ try {
10473
+ await this.markFailed(
10474
+ watcher.conv.id,
10475
+ entry.evidentMessageId,
10476
+ sessionId,
10477
+ error2,
10478
+ void 0,
10479
+ {
10480
+ classification: null,
10481
+ errorShape: extractReplyErrorShape(
10482
+ findLastAssistantReplyFor(messages, entry.opencodeMessageId)
10483
+ )
10484
+ }
10485
+ );
10486
+ } catch (err) {
10487
+ if (err instanceof ChannelAuthError) throw err;
10488
+ allRecordsLanded = false;
10489
+ const watched_for_ms = this.now() - entry.processingAnchorMs;
10490
+ if (err instanceof ChannelTerminalError) {
10491
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10492
+ watched_for_ms,
10493
+ interrupt_outcome: "record_terminal"
10494
+ });
10495
+ this.removeInFlight(watcher, entry.evidentMessageId);
10496
+ continue;
10497
+ }
10498
+ if (this.now() >= entry.deadline) {
10499
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10500
+ watched_for_ms,
10501
+ interrupt_outcome: "record_deadline"
10502
+ });
10503
+ this.removeInFlight(watcher, entry.evidentMessageId);
10504
+ continue;
10505
+ }
10506
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10507
+ watched_for_ms,
10508
+ interrupt_outcome: "record_retry"
10509
+ });
10510
+ this.log({
10511
+ level: "warn",
10512
+ 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)}`,
10513
+ conversation_id: watcher.conv.id,
10514
+ message_id: entry.evidentMessageId
10515
+ });
10516
+ continue;
10517
+ }
10518
+ entry.done = true;
10519
+ landed.push(entry);
10520
+ }
10521
+ if (!allRecordsLanded) {
10522
+ for (const entry of landed) this.removeInFlight(watcher, entry.evidentMessageId);
10523
+ return;
10524
+ }
10525
+ if (healthySiblings > 0) {
10526
+ for (const entry of landed) {
10527
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10528
+ watched_for_ms: this.now() - entry.processingAnchorMs,
10529
+ interrupt_outcome: "deferred_for_healthy_sibling",
10530
+ healthy_siblings: healthySiblings
10531
+ });
10532
+ this.removeInFlight(watcher, entry.evidentMessageId);
10533
+ }
10534
+ return;
10535
+ }
10536
+ const interrupt = await interruptSession(this.port, sessionId, this.openCodeClient);
10537
+ for (const entry of landed) {
10538
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10539
+ watched_for_ms: this.now() - entry.processingAnchorMs,
10540
+ interrupt_outcome: interrupt.kind
10541
+ });
10542
+ this.removeInFlight(watcher, entry.evidentMessageId);
10543
+ }
10544
+ if (interrupt.kind !== "confirmed_stopped") {
10545
+ this.log({
10546
+ level: "warn",
10547
+ 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`,
10548
+ conversation_id: watcher.conv.id
10549
+ });
10550
+ }
10551
+ }
10063
10552
  /**
10064
10553
  * Drive ONE in-flight message's lifecycle from the tick's message snapshot.
10065
10554
  * Fires markProcessing on queued→running and markDone on done (each once),
10066
10555
  * applies the idle-path re-dispatch guard, and removes the message from the
10067
10556
  * in-flight set on completion or timeout.
10068
10557
  */
10069
- async serviceInFlightMessage(sessionId, watcher, inFlight, messages, openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk) {
10558
+ async serviceInFlightMessage(sessionId, watcher, inFlight, messages, openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk, ceilingAttemptsThisTick) {
10070
10559
  const conv = watcher.conv;
10071
10560
  const state = messageRunState(messages, inFlight.opencodeMessageId);
10072
10561
  const id = inFlight.evidentMessageId;
@@ -10152,7 +10641,7 @@ var ChannelDriver = class _ChannelDriver {
10152
10641
  if (!inFlight.done) {
10153
10642
  const messageErrorText = messageError(messages, inFlight.opencodeMessageId);
10154
10643
  const retainedReason = this.readRetainedSessionError(sessionId);
10155
- const retainedError = retainedReason === null ? null : `${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}`;
10644
+ const retainedError = retainedReason === null || messageErrorText !== null && messageErrorText.includes(retainedReason) ? null : `${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}`;
10156
10645
  const observedCause = this.sessionStoreHealth?.readCause(sessionId);
10157
10646
  const causeError = observedCause === null || observedCause === void 0 ? null : `${SESSION_ERROR_OBSERVED_CAUSE}${observedCause}`;
10158
10647
  const storeError = this.sessionStoreHealth?.isUnwritable() ? SESSION_STORE_UNWRITABLE_ERROR : null;
@@ -10410,17 +10899,8 @@ var ChannelDriver = class _ChannelDriver {
10410
10899
  return;
10411
10900
  }
10412
10901
  }
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);
10902
+ if (watcher.inFlight.has(inFlight.evidentMessageId) && activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
10903
+ await this.settleAbsoluteCeiling(sessionId, watcher, messages, ceilingAttemptsThisTick);
10424
10904
  return;
10425
10905
  }
10426
10906
  const heartbeatDue = activelyRunning && !inFlight.awaitingHumanLatched && !inFlight.aliveInFlight && this.now() - inFlight.lastAliveAt >= HEARTBEAT_MS;
@@ -10648,7 +11128,8 @@ var ChannelDriver = class _ChannelDriver {
10648
11128
  usage,
10649
11129
  void 0,
10650
11130
  usageAgentName,
10651
- subagentInvocations
11131
+ subagentInvocations,
11132
+ true
10652
11133
  );
10653
11134
  } catch (err) {
10654
11135
  if (err instanceof ChannelAuthError) throw err;
@@ -11042,6 +11523,33 @@ var ChannelDriver = class _ChannelDriver {
11042
11523
  }
11043
11524
  const restartAborted = state === "failed" && sessionOngoing === false && isAbortedTerminalReply(messages, ocId ?? "");
11044
11525
  if (restartAborted) {
11526
+ const age = this.absoluteAgeForRow(row.processing_started_at);
11527
+ if (age.tripped && age.anchorMs !== null) {
11528
+ try {
11529
+ await this.markFailed(
11530
+ row.conversation_id,
11531
+ row.id,
11532
+ sessionId,
11533
+ this.absoluteCeilingError(row.id, sessionId, age.anchorMs),
11534
+ void 0,
11535
+ {
11536
+ classification: null,
11537
+ errorShape: extractReplyErrorShape(findLastAssistantReplyFor(messages, ocId ?? ""))
11538
+ }
11539
+ );
11540
+ } catch (err) {
11541
+ if (err instanceof ChannelAuthError) throw err;
11542
+ this.log({
11543
+ level: "warn",
11544
+ 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)}`,
11545
+ conversation_id: row.conversation_id,
11546
+ message_id: row.id
11547
+ });
11548
+ return;
11549
+ }
11550
+ void this.postSignal(row.conversation_id, row.id, "redrive_ceiling_refused");
11551
+ return;
11552
+ }
11045
11553
  this.log({
11046
11554
  level: "info",
11047
11555
  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 +12095,8 @@ var ChannelDriver = class _ChannelDriver {
11587
12095
  source_message_id: row.source_message_id,
11588
12096
  slack_user_id: row.slack_user_id,
11589
12097
  attachments: row.attachments ?? null,
11590
- opencode_message_id: row.opencode_message_id
12098
+ opencode_message_id: row.opencode_message_id,
12099
+ processing_started_at: row.processing_started_at
11591
12100
  };
11592
12101
  }
11593
12102
  /**
@@ -12494,7 +13003,7 @@ var ChannelDriver = class _ChannelDriver {
12494
13003
  * exists but is wedged, so the next attempt must get a fresh one
12495
13004
  * instead of reusing it — see WI-1's server-side null-clearing PATCH).
12496
13005
  */
12497
- async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
13006
+ async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations, abandonedWithoutAnswer) {
12498
13007
  const body = { status: "failed" };
12499
13008
  if (sessionId === null) {
12500
13009
  body.opencode_session_id = null;
@@ -12507,12 +13016,19 @@ var ChannelDriver = class _ChannelDriver {
12507
13016
  if (subagentInvocations && subagentInvocations.length > 0) {
12508
13017
  body.subagent_invocations = subagentInvocations;
12509
13018
  }
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;
13019
+ if (failure && failure.errorShape.name !== null) {
13020
+ body.failure_error_name = failure.errorShape.name;
13021
+ }
13022
+ if (failure && failure.errorShape.statusCode !== null) {
13023
+ body.failure_status_code = failure.errorShape.statusCode;
12515
13024
  }
13025
+ if (failure?.classification) {
13026
+ body.failure_kind = failure.classification.kind;
13027
+ body.failure_provider_id = failure.classification.providerId;
13028
+ body.failure_model_id = failure.classification.modelId;
13029
+ body.failure_reason = failure.classification.reason;
13030
+ }
13031
+ if (abandonedWithoutAnswer) body.abandoned_without_answer = true;
12516
13032
  try {
12517
13033
  await this.callWithRetry(
12518
13034
  "marking message as failed",
@@ -12532,27 +13048,28 @@ var ChannelDriver = class _ChannelDriver {
12532
13048
  this.clearSubagentInvocationCaches(messageId);
12533
13049
  }
12534
13050
  /**
12535
- * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
13051
+ * Classify an errored turn and extract its bounded error shape for `markFailed`.
12536
13052
  *
12537
13053
  * `messageFailure` alone (structured OpenCode error → `model_auth`) covers
12538
13054
  * most cases; when it returns `null` on this ALREADY-FAILED turn, fall back
12539
13055
  * to the P1-2b zero-provider check — one extra loopback call to
12540
13056
  * `hasAnyConfiguredProvider`, only reached when the structured classifier
12541
13057
  * 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.
13058
+ * (`null`/indeterminate) leaves the classification `null`; the raw error shape
13059
+ * is still retained for diagnostics.
12544
13060
  */
12545
13061
  async classifyModelAuthFailure(messages, userMessageId) {
12546
- const classified = messageFailure(messages, userMessageId);
12547
- if (classified != null) return classified;
12548
13062
  const reply = findLastAssistantReplyFor(messages, userMessageId);
12549
- const hasProvider = await this.hasAnyConfiguredProvider();
12550
- return applyZeroProviderFallback(
13063
+ const errorShape = extractReplyErrorShape(reply);
13064
+ const classified = messageFailure(messages, userMessageId);
13065
+ if (classified != null) return { classification: classified, errorShape };
13066
+ const classification = applyZeroProviderFallback(
12551
13067
  classified,
12552
- hasProvider,
13068
+ await this.hasAnyConfiguredProvider(),
12553
13069
  reply?.info?.providerID ?? null,
12554
13070
  reply?.info?.modelID ?? null
12555
13071
  );
13072
+ return { classification, errorShape };
12556
13073
  }
12557
13074
  async recordSubagentModelAuthFailure(failure, conversationId, messageId) {
12558
13075
  const providerId = failure.providerId ?? "(unknown)";
@@ -13377,6 +13894,38 @@ async function configureGitHubAccess({ env, log: log3 }) {
13377
13894
  import { execFileSync as execFileSync2 } from "node:child_process";
13378
13895
  import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
13379
13896
  import { isAbsolute as isAbsolute3, join as join11, resolve as resolve4 } from "node:path";
13897
+ function parseConfigContent(content) {
13898
+ if (!content?.trim()) return {};
13899
+ const parsed = JSON.parse(content);
13900
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
13901
+ throw new Error("OPENCODE_CONFIG_CONTENT must contain a JSON object");
13902
+ }
13903
+ return parsed;
13904
+ }
13905
+ function mergeManagedOpenCodeConfigContent(content, managedProviders) {
13906
+ const config = parseConfigContent(content);
13907
+ const provider = typeof config.provider === "object" && config.provider !== null && !Array.isArray(config.provider) ? { ...config.provider } : {};
13908
+ for (const namespace of CREDENTIAL_PROXY_NATIVE_NAMESPACES) {
13909
+ delete provider[namespace];
13910
+ }
13911
+ return JSON.stringify({ ...config, provider: { ...provider, ...managedProviders } });
13912
+ }
13913
+ function removeManagedOpenCodeConfigContent(content) {
13914
+ if (!content?.trim()) return content;
13915
+ const config = parseConfigContent(content);
13916
+ if (typeof config.provider !== "object" || config.provider === null || Array.isArray(config.provider)) {
13917
+ return content;
13918
+ }
13919
+ const provider = { ...config.provider };
13920
+ let changed = false;
13921
+ for (const namespace of CREDENTIAL_PROXY_NATIVE_NAMESPACES) {
13922
+ if (namespace in provider) {
13923
+ delete provider[namespace];
13924
+ changed = true;
13925
+ }
13926
+ }
13927
+ return changed ? JSON.stringify({ ...config, provider }) : content;
13928
+ }
13380
13929
  function isFile(filePath) {
13381
13930
  return existsSync2(filePath) && statSync6(filePath).isFile();
13382
13931
  }
@@ -13410,6 +13959,159 @@ function applyRunnerOpenCodeConfig({
13410
13959
  log3(`Applied runner OpenCode config ${source} to ${join11(cwd, target)}`);
13411
13960
  }
13412
13961
 
13962
+ // src/lib/opencode/runtime-provenance.ts
13963
+ import { readFileSync as readFileSync7, readdirSync } from "node:fs";
13964
+ import { homedir as homedir7 } from "node:os";
13965
+ import { join as join12 } from "node:path";
13966
+ var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
13967
+ var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
13968
+ function errorKind(error2) {
13969
+ if (!(error2 instanceof Error)) return "unknown error";
13970
+ const code = "code" in error2 && typeof error2.code === "string" ? ` (${error2.code})` : "";
13971
+ return `${error2.name}${code}`;
13972
+ }
13973
+ function parseRegistryPluginSpec(value) {
13974
+ const separator = value.lastIndexOf("@");
13975
+ if (separator <= 0) return null;
13976
+ const name = value.slice(0, separator);
13977
+ const spec = value.slice(separator + 1);
13978
+ if (!PACKAGE_NAME.test(name) || !REGISTRY_SPEC.test(spec)) return null;
13979
+ return { name, spec };
13980
+ }
13981
+ function parseJsonc(value) {
13982
+ let output = "";
13983
+ let inString = false;
13984
+ let escaped = false;
13985
+ let lineComment = false;
13986
+ let blockComment = false;
13987
+ for (let index = 0; index < value.length; index++) {
13988
+ const character = value[index];
13989
+ const next = value[index + 1];
13990
+ if (lineComment) {
13991
+ if (character === "\n") {
13992
+ lineComment = false;
13993
+ output += character;
13994
+ }
13995
+ continue;
13996
+ }
13997
+ if (blockComment) {
13998
+ if (character === "*" && next === "/") {
13999
+ blockComment = false;
14000
+ index++;
14001
+ } else if (character === "\n") {
14002
+ output += character;
14003
+ }
14004
+ continue;
14005
+ }
14006
+ if (inString) {
14007
+ output += character;
14008
+ if (escaped) escaped = false;
14009
+ else if (character === "\\") escaped = true;
14010
+ else if (character === '"') inString = false;
14011
+ continue;
14012
+ }
14013
+ if (character === '"') {
14014
+ inString = true;
14015
+ output += character;
14016
+ } else if (character === "/" && next === "/") {
14017
+ lineComment = true;
14018
+ index++;
14019
+ } else if (character === "/" && next === "*") {
14020
+ blockComment = true;
14021
+ index++;
14022
+ } else if (character === ",") {
14023
+ let lookahead = index + 1;
14024
+ while (/\s/.test(value[lookahead] ?? "")) lookahead++;
14025
+ if (value[lookahead] !== "]" && value[lookahead] !== "}") output += character;
14026
+ } else {
14027
+ output += character;
14028
+ }
14029
+ }
14030
+ return JSON.parse(output);
14031
+ }
14032
+ function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
14033
+ return join12(env.XDG_CACHE_HOME ?? join12(homeDir, ".cache"), "opencode");
14034
+ }
14035
+ function readConfiguredPluginSpecs({
14036
+ cwd = process.cwd(),
14037
+ log: log3
14038
+ }) {
14039
+ let config;
14040
+ try {
14041
+ try {
14042
+ config = parseJsonc(readFileSync7(join12(cwd, "opencode.jsonc"), "utf8"));
14043
+ } catch (error2) {
14044
+ if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
14045
+ config = parseJsonc(readFileSync7(join12(cwd, "opencode.json"), "utf8"));
14046
+ }
14047
+ } catch (error2) {
14048
+ log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
14049
+ return [];
14050
+ }
14051
+ if (typeof config !== "object" || config === null || !("plugin" in config)) return [];
14052
+ const plugins = config.plugin;
14053
+ if (!Array.isArray(plugins)) {
14054
+ log3("Could not read OpenCode plugin configuration: plugin is not an array");
14055
+ return [];
14056
+ }
14057
+ const configured = plugins.filter(
14058
+ (plugin) => typeof plugin === "string" && parseRegistryPluginSpec(plugin) !== null
14059
+ );
14060
+ if (configured.length !== plugins.length) {
14061
+ log3(
14062
+ `Skipped ${plugins.length - configured.length} non-registry OpenCode plugin spec(s); their values were not recorded`
14063
+ );
14064
+ }
14065
+ return configured;
14066
+ }
14067
+ function resolveInstalledPluginVersions({
14068
+ cacheRoot,
14069
+ env = process.env,
14070
+ homeDir = homedir7(),
14071
+ log: log3
14072
+ }) {
14073
+ const packagesRoot = join12(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
14074
+ let directories;
14075
+ try {
14076
+ directories = readdirSync(packagesRoot, { withFileTypes: true });
14077
+ } catch (error2) {
14078
+ if ("code" in Object(error2) && Object(error2).code === "ENOENT") return [];
14079
+ log3(`Could not inspect the OpenCode plugin cache: ${errorKind(error2)}`);
14080
+ return [];
14081
+ }
14082
+ const plugins = [];
14083
+ let skipped = 0;
14084
+ for (const directory of directories) {
14085
+ if (!directory.isDirectory()) continue;
14086
+ const parsed = parseRegistryPluginSpec(directory.name);
14087
+ if (parsed === null) {
14088
+ skipped++;
14089
+ continue;
14090
+ }
14091
+ let resolvedVersion = null;
14092
+ try {
14093
+ const packageJson = JSON.parse(
14094
+ readFileSync7(
14095
+ join12(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
14096
+ "utf8"
14097
+ )
14098
+ );
14099
+ if (typeof packageJson.version === "string" && REGISTRY_SPEC.test(packageJson.version)) {
14100
+ resolvedVersion = packageJson.version;
14101
+ } else {
14102
+ log3(`Could not resolve OpenCode plugin ${parsed.name}@${parsed.spec}: invalid version`);
14103
+ }
14104
+ } catch (error2) {
14105
+ log3(`Could not resolve OpenCode plugin ${parsed.name}@${parsed.spec}: ${errorKind(error2)}`);
14106
+ }
14107
+ plugins.push({ ...parsed, resolvedVersion });
14108
+ }
14109
+ if (skipped > 0) {
14110
+ log3(`Skipped ${skipped} non-registry OpenCode package cache entry; its value was not recorded`);
14111
+ }
14112
+ return plugins;
14113
+ }
14114
+
13413
14115
  // src/lib/credential-sync.ts
13414
14116
  import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
13415
14117
  var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
@@ -13645,10 +14347,236 @@ function createCredentialSync({
13645
14347
  };
13646
14348
  }
13647
14349
 
14350
+ // src/lib/managed-inference-setup.ts
14351
+ var defaultStore = {
14352
+ get: getManagedInferenceCredential,
14353
+ set: setManagedInferenceCredential,
14354
+ clear: clearManagedInferenceCredential
14355
+ };
14356
+ function endpoint(options, operation) {
14357
+ return `${options.apiUrl}/runners/${options.agentId}/managed-credentials/${operation}`;
14358
+ }
14359
+ function requestHeaders(options) {
14360
+ return { Authorization: options.getAuthHeader(), "Content-Type": "application/json" };
14361
+ }
14362
+ function request2(options) {
14363
+ return withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
14364
+ }
14365
+ function safeFailure(operation, status2) {
14366
+ return status2 === void 0 ? `${operation} could not reach the managed credential service` : `${operation} returned HTTP ${status2}`;
14367
+ }
14368
+ function logTransportFailure(options, operation, error2, authorization) {
14369
+ let detail = error2 instanceof Error ? error2.message : String(error2);
14370
+ const credential = authorization.split(/\s+/, 2)[1];
14371
+ for (const secret of [authorization, credential]) {
14372
+ if (secret) detail = detail.replaceAll(secret, "[redacted]");
14373
+ }
14374
+ options.log(
14375
+ `MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${redactDiagnosticText(detail)}`,
14376
+ "debug"
14377
+ );
14378
+ }
14379
+ async function responseJson(response) {
14380
+ try {
14381
+ const value = await response.json();
14382
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
14383
+ } catch (error2) {
14384
+ return null;
14385
+ }
14386
+ }
14387
+ function readSetup(value) {
14388
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
14389
+ const setup = value;
14390
+ if (typeof setup.runner_id !== "string" || typeof setup.generation !== "string" || typeof setup.status !== "string" || setup.token_id !== null && typeof setup.token_id !== "string") {
14391
+ return null;
14392
+ }
14393
+ return setup;
14394
+ }
14395
+ function clearLocalSetup(options) {
14396
+ const env = options.env ?? process.env;
14397
+ (options.store ?? defaultStore).clear(options.agentId);
14398
+ delete env.EVIDENT_PROXY_TOKEN;
14399
+ try {
14400
+ const content = removeManagedOpenCodeConfigContent(env.OPENCODE_CONFIG_CONTENT);
14401
+ if (content === void 0) delete env.OPENCODE_CONFIG_CONTENT;
14402
+ else env.OPENCODE_CONFIG_CONTENT = content;
14403
+ } catch (error2) {
14404
+ options.log(
14405
+ "MANAGED-INFERENCE-CLEANUP-PARTIAL: stored credentials were removed, but malformed OPENCODE_CONFIG_CONTENT could not be inspected",
14406
+ "warn"
14407
+ );
14408
+ }
14409
+ }
14410
+ async function discoverManagedInferenceSetup(options) {
14411
+ const headers = requestHeaders(options);
14412
+ let response;
14413
+ try {
14414
+ response = await request2(options)(endpoint(options, "current"), {
14415
+ headers
14416
+ });
14417
+ } catch (error2) {
14418
+ logTransportFailure(options, "discovery", error2, headers.Authorization);
14419
+ return { kind: "failed", reason: safeFailure("Managed setup discovery") };
14420
+ }
14421
+ if (response.status === 404) {
14422
+ clearLocalSetup(options);
14423
+ return { kind: "none" };
14424
+ }
14425
+ if (!response.ok) {
14426
+ return { kind: "failed", reason: safeFailure("Managed setup discovery", response.status) };
14427
+ }
14428
+ const body = await responseJson(response);
14429
+ const setup = readSetup(body?.setup);
14430
+ return setup ? { kind: "setup", setup } : { kind: "failed", reason: "Managed setup discovery returned an invalid response" };
14431
+ }
14432
+ async function post(options, operation, body) {
14433
+ const headers = requestHeaders(options);
14434
+ try {
14435
+ return await request2(options)(endpoint(options, operation), {
14436
+ method: "POST",
14437
+ headers,
14438
+ body: JSON.stringify(body)
14439
+ });
14440
+ } catch (error2) {
14441
+ logTransportFailure(options, operation, error2, headers.Authorization);
14442
+ return null;
14443
+ }
14444
+ }
14445
+ function applyCredential(options, credential) {
14446
+ const { config, unsupportedProviders } = buildCredentialProxyNativeConfig({
14447
+ inferenceBaseUrl: credential.inferenceBaseUrl,
14448
+ modelDescriptors: credential.modelDescriptors
14449
+ });
14450
+ if (unsupportedProviders.length > 0 || Object.keys(config.provider).length === 0) {
14451
+ return "Managed setup contains unsupported provider descriptors";
14452
+ }
14453
+ const env = options.env ?? process.env;
14454
+ let merged;
14455
+ try {
14456
+ merged = mergeManagedOpenCodeConfigContent(env.OPENCODE_CONFIG_CONTENT, config.provider);
14457
+ } catch (error2) {
14458
+ return "OPENCODE_CONFIG_CONTENT is malformed; managed providers were not installed";
14459
+ }
14460
+ try {
14461
+ (options.store ?? defaultStore).set(options.agentId, credential);
14462
+ } catch (error2) {
14463
+ options.log(
14464
+ `MANAGED-INFERENCE-STORE-FAILED: runner=${options.agentId} generation=${credential.generation}`,
14465
+ "warn"
14466
+ );
14467
+ return "Managed credentials could not be written to the protected store";
14468
+ }
14469
+ env.EVIDENT_PROXY_TOKEN = credential.token;
14470
+ env.OPENCODE_CONFIG_CONTENT = merged;
14471
+ return null;
14472
+ }
14473
+ async function reportManagedInferenceApplyFailure(options, failure) {
14474
+ const response = await post(options, "report-failure", {
14475
+ generation: failure.generation,
14476
+ token_id: failure.tokenId
14477
+ });
14478
+ if (!response?.ok) {
14479
+ options.log(
14480
+ `MANAGED-INFERENCE-REPORT-FAILED: runner=${options.agentId} generation=${failure.generation}${response ? ` status=${response.status}` : ""}`,
14481
+ "warn"
14482
+ );
14483
+ }
14484
+ }
14485
+ async function reaffirmStored(options, credential) {
14486
+ const response = await post(options, "reaffirm", {
14487
+ generation: credential.generation,
14488
+ token_id: credential.tokenId
14489
+ });
14490
+ if (!response?.ok) {
14491
+ return {
14492
+ kind: "failed",
14493
+ 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")
14494
+ };
14495
+ }
14496
+ const body = await responseJson(response);
14497
+ const setup = readSetup(body?.setup);
14498
+ if (!setup || setup.generation !== credential.generation || setup.token_id !== credential.tokenId) {
14499
+ return { kind: "failed", reason: "Managed credential reaffirmation returned a mismatch" };
14500
+ }
14501
+ const failure = applyCredential(options, credential);
14502
+ return failure ? { kind: "failed", reason: failure } : { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
14503
+ }
14504
+ async function prepareManagedInferenceSetup(options, discovery, health) {
14505
+ if (health.authFailed) {
14506
+ return {
14507
+ kind: "failed",
14508
+ reason: "OpenCode authentication failed; managed credentials were not rotated"
14509
+ };
14510
+ }
14511
+ const stored = (options.store ?? defaultStore).get(options.agentId);
14512
+ if (health.healthy) {
14513
+ if (!stored) {
14514
+ return {
14515
+ kind: "failed",
14516
+ reason: "Managed credentials are missing for the serving OpenCode process; restart required"
14517
+ };
14518
+ }
14519
+ return reaffirmStored(options, stored);
14520
+ }
14521
+ if (discovery.setup.status === "acknowledged" && stored?.generation === discovery.setup.generation && stored.tokenId === discovery.setup.token_id) {
14522
+ return reaffirmStored(options, stored);
14523
+ }
14524
+ const response = await post(options, "bootstrap", { generation: discovery.setup.generation });
14525
+ if (!response?.ok) {
14526
+ return {
14527
+ kind: "failed",
14528
+ reason: response ? safeFailure("Managed credential bootstrap", response.status) : safeFailure("Managed credential bootstrap")
14529
+ };
14530
+ }
14531
+ const body = await responseJson(response);
14532
+ const setup = readSetup(body?.setup);
14533
+ const token = body?.inference_token;
14534
+ const inferenceBaseUrl = body?.inference_base_url;
14535
+ const modelDescriptors = body?.model_descriptors;
14536
+ if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || !Array.isArray(modelDescriptors) || !modelDescriptors.every(
14537
+ (descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
14538
+ )) {
14539
+ return { kind: "failed", reason: "Managed credential bootstrap returned an invalid response" };
14540
+ }
14541
+ const credential = {
14542
+ generation: setup.generation,
14543
+ tokenId: setup.token_id,
14544
+ token,
14545
+ inferenceBaseUrl,
14546
+ modelDescriptors
14547
+ };
14548
+ const failure = applyCredential(options, credential);
14549
+ if (failure) {
14550
+ await reportManagedInferenceApplyFailure(options, credential);
14551
+ return { kind: "failed", reason: failure };
14552
+ }
14553
+ return { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
14554
+ }
14555
+ async function acknowledgeManagedInferenceSetup(options, preparation) {
14556
+ const response = await post(options, "acknowledge", {
14557
+ generation: preparation.generation,
14558
+ token_id: preparation.tokenId,
14559
+ config_validated: true
14560
+ });
14561
+ if (!response?.ok) {
14562
+ throw new Error(
14563
+ response ? safeFailure("Managed credential acknowledgment", response.status) : safeFailure("Managed credential acknowledgment")
14564
+ );
14565
+ }
14566
+ const body = await responseJson(response);
14567
+ const setup = readSetup(body?.setup);
14568
+ if (!setup || setup.generation !== preparation.generation || setup.token_id !== preparation.tokenId) {
14569
+ throw new Error("Managed credential acknowledgment returned a mismatch");
14570
+ }
14571
+ return response.status === 200 && setup.status === "acknowledged" ? "acknowledged" : "pending";
14572
+ }
14573
+
13648
14574
  // src/commands/run.ts
13649
14575
  var MAX_ACTIVITY_LOG_ENTRIES = 10;
13650
14576
  var MAX_DISCONNECT_REASON_LENGTH = 200;
13651
14577
  var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
14578
+ var CREDENTIAL_ADOPTION_PROBE_ATTEMPTS = 3;
14579
+ var CREDENTIAL_ADOPTION_PROBE_INTERVAL_MS = 1e3;
13652
14580
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
13653
14581
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
13654
14582
  var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
@@ -13683,7 +14611,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
13683
14611
  if (trimmed === "") {
13684
14612
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
13685
14613
  }
13686
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join12(homeDir, trimmed.slice(2)) : trimmed;
14614
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join13(homeDir, trimmed.slice(2)) : trimmed;
13687
14615
  if (!isAbsolute4(expanded)) {
13688
14616
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
13689
14617
  }
@@ -13838,7 +14766,7 @@ function logActivity(state, entry) {
13838
14766
  }
13839
14767
  function reportSessionDbRecovery(state) {
13840
14768
  try {
13841
- const report = drainSessionDbRecoveryReport({ homeDir: homedir7(), env: process.env });
14769
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir8(), env: process.env });
13842
14770
  for (const record of report.records) {
13843
14771
  const activity = buildSessionDbRecoveryActivity(record);
13844
14772
  if (!activity) throw new Error("could not map session-DB recovery record");
@@ -13919,7 +14847,7 @@ async function importOpenCodeV2CredentialsForState(state) {
13919
14847
  }
13920
14848
  const result = await importOpenCodeV2Credentials({
13921
14849
  request: client.request,
13922
- homeDir: homedir7(),
14850
+ homeDir: homedir8(),
13923
14851
  directory,
13924
14852
  guard: state.opencodeV2CredentialImportGuard,
13925
14853
  log: (message, level = "info") => logActivity(state, { type: "info", level, message })
@@ -13996,6 +14924,125 @@ async function handleAuthError(state, error2) {
13996
14924
  return { success: false };
13997
14925
  }
13998
14926
  }
14927
+ function logCredentialAdoptionActivity(state, activity, level) {
14928
+ const message = (() => {
14929
+ switch (activity.outcome) {
14930
+ case "deferred":
14931
+ return `CREDENTIAL-ADOPTION: deferred generation ${activity.generation} with ${activity.active_sessions} active session(s)`;
14932
+ case "reloaded_ready":
14933
+ return `CREDENTIAL-ADOPTION: adopted generation ${activity.generation} with ${activity.active_sessions} active session(s); readiness probe succeeded after ${activity.probe_attempts} attempt(s)`;
14934
+ case "reloaded_interrupted": {
14935
+ 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`;
14936
+ return `CREDENTIAL-ADOPTION: generation ${activity.generation} with ${activity.active_sessions} active session(s); replacement instance answered ${response} after ${activity.probe_attempts} probe attempt(s)`;
14937
+ }
14938
+ case "reload_failed":
14939
+ return `CREDENTIAL-ADOPTION: failed to adopt generation ${activity.generation} with ${activity.active_sessions} active session(s)`;
14940
+ case "skipped_v2":
14941
+ return `CREDENTIAL-ADOPTION: skipped generation ${activity.generation} with ${activity.active_sessions} active session(s) on OpenCode V2`;
14942
+ }
14943
+ })();
14944
+ logActivity(state, {
14945
+ type: level === "error" ? "error" : "info",
14946
+ level,
14947
+ ...level === "error" ? { error: message } : { message },
14948
+ metadata: {
14949
+ ...activity,
14950
+ opencode_version: state.opencodeVersion,
14951
+ port: state.port
14952
+ }
14953
+ });
14954
+ }
14955
+ async function adoptCredentialGeneration(state, generation, adoption, activeSessions, deferredForMs) {
14956
+ const reload = await disposeAndReloadProviderConfig(
14957
+ state.port,
14958
+ state.opencodeClient ?? void 0
14959
+ );
14960
+ if (reload.outcome === "failed") {
14961
+ logCredentialAdoptionActivity(
14962
+ state,
14963
+ {
14964
+ outcome: "reload_failed",
14965
+ generation,
14966
+ active_sessions: activeSessions,
14967
+ adoption,
14968
+ deferred_for_ms: deferredForMs,
14969
+ reload_status: reload.status,
14970
+ reload_detail: reload.detail,
14971
+ probe_attempts: null,
14972
+ probe_status: null,
14973
+ probe_body_empty: null,
14974
+ probe_detail: null
14975
+ },
14976
+ "error"
14977
+ );
14978
+ return;
14979
+ }
14980
+ if (reload.outcome === "skipped_v2") {
14981
+ logCredentialAdoptionActivity(
14982
+ state,
14983
+ {
14984
+ outcome: "skipped_v2",
14985
+ generation,
14986
+ active_sessions: activeSessions,
14987
+ adoption,
14988
+ deferred_for_ms: deferredForMs,
14989
+ reload_status: null,
14990
+ reload_detail: null,
14991
+ probe_attempts: null,
14992
+ probe_status: null,
14993
+ probe_body_empty: null,
14994
+ probe_detail: null
14995
+ },
14996
+ "debug"
14997
+ );
14998
+ return;
14999
+ }
15000
+ let lastProbe = null;
15001
+ let attempts = 0;
15002
+ for (attempts = 1; attempts <= CREDENTIAL_ADOPTION_PROBE_ATTEMPTS; attempts++) {
15003
+ lastProbe = await probeOpenCodeRequestReadiness(state.port, state.opencodeClient ?? void 0);
15004
+ if (lastProbe.ok) {
15005
+ logCredentialAdoptionActivity(
15006
+ state,
15007
+ {
15008
+ outcome: "reloaded_ready",
15009
+ generation,
15010
+ active_sessions: activeSessions,
15011
+ adoption,
15012
+ deferred_for_ms: deferredForMs,
15013
+ reload_status: null,
15014
+ reload_detail: null,
15015
+ probe_attempts: attempts,
15016
+ probe_status: lastProbe.status,
15017
+ probe_body_empty: lastProbe.bodyEmpty,
15018
+ probe_detail: lastProbe.detail
15019
+ },
15020
+ "warn"
15021
+ );
15022
+ return;
15023
+ }
15024
+ if (attempts < CREDENTIAL_ADOPTION_PROBE_ATTEMPTS) {
15025
+ await new Promise((resolve5) => setTimeout(resolve5, CREDENTIAL_ADOPTION_PROBE_INTERVAL_MS));
15026
+ }
15027
+ }
15028
+ logCredentialAdoptionActivity(
15029
+ state,
15030
+ {
15031
+ outcome: "reloaded_interrupted",
15032
+ generation,
15033
+ active_sessions: activeSessions,
15034
+ adoption,
15035
+ deferred_for_ms: deferredForMs,
15036
+ reload_status: null,
15037
+ reload_detail: null,
15038
+ probe_attempts: CREDENTIAL_ADOPTION_PROBE_ATTEMPTS,
15039
+ probe_status: lastProbe?.status ?? null,
15040
+ probe_body_empty: lastProbe?.bodyEmpty ?? null,
15041
+ probe_detail: lastProbe?.detail ?? null
15042
+ },
15043
+ "error"
15044
+ );
15045
+ }
13999
15046
  async function driveChannels(state, driver) {
14000
15047
  let idlePolls = 0;
14001
15048
  let idleMs = 0;
@@ -14005,6 +15052,15 @@ async function driveChannels(state, driver) {
14005
15052
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
14006
15053
  let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
14007
15054
  let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
15055
+ const credentialAdoption = {
15056
+ generation: 0,
15057
+ pendingGeneration: null,
15058
+ pendingSinceMs: null,
15059
+ deferredGeneration: null,
15060
+ adoption: 0
15061
+ };
15062
+ const stayAwakeHolds = () => state.stayAwakeUntilMs !== null && Date.now() < state.stayAwakeUntilMs;
15063
+ let stayAwakeSuppressionLogged = false;
14008
15064
  while (state.running) {
14009
15065
  const cycleStartedAtMs = performance.now();
14010
15066
  let idleThisCycle = false;
@@ -14046,12 +15102,30 @@ async function driveChannels(state, driver) {
14046
15102
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
14047
15103
  if (opencodeAuthApplied) state.openaiUsageRearm?.();
14048
15104
  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
- );
15105
+ credentialAdoption.generation++;
15106
+ if (state.opencodeApiVersion === "v2") {
15107
+ logCredentialAdoptionActivity(
15108
+ state,
15109
+ {
15110
+ outcome: "skipped_v2",
15111
+ generation: credentialAdoption.generation,
15112
+ active_sessions: driver.protectedSessionIds().size,
15113
+ adoption: null,
15114
+ deferred_for_ms: null,
15115
+ reload_status: null,
15116
+ reload_detail: null,
15117
+ probe_attempts: null,
15118
+ probe_status: null,
15119
+ probe_body_empty: null,
15120
+ probe_detail: null
15121
+ },
15122
+ "debug"
15123
+ );
15124
+ } else {
15125
+ credentialAdoption.pendingGeneration = credentialAdoption.generation;
15126
+ credentialAdoption.pendingSinceMs = Date.now();
15127
+ credentialAdoption.deferredGeneration = null;
15128
+ }
14055
15129
  }
14056
15130
  if (opencodeAuthApplied && state.opencodeApiVersion === "v2") {
14057
15131
  void importOpenCodeV2CredentialsForState(state).catch(
@@ -14106,6 +15180,49 @@ async function driveChannels(state, driver) {
14106
15180
  }
14107
15181
  }
14108
15182
  }
15183
+ if (credentialAdoption.pendingGeneration !== null) {
15184
+ const activeSessions = driver.protectedSessionIds();
15185
+ const generation = credentialAdoption.pendingGeneration;
15186
+ if (activeSessions.size > 0) {
15187
+ if (credentialAdoption.deferredGeneration !== generation) {
15188
+ credentialAdoption.deferredGeneration = generation;
15189
+ logCredentialAdoptionActivity(
15190
+ state,
15191
+ {
15192
+ outcome: "deferred",
15193
+ generation,
15194
+ active_sessions: activeSessions.size,
15195
+ adoption: null,
15196
+ deferred_for_ms: null,
15197
+ reload_status: null,
15198
+ reload_detail: null,
15199
+ probe_attempts: null,
15200
+ probe_status: null,
15201
+ probe_body_empty: null,
15202
+ probe_detail: null
15203
+ },
15204
+ "warn"
15205
+ );
15206
+ }
15207
+ } else {
15208
+ const deferredForMs = credentialAdoption.deferredGeneration === generation && credentialAdoption.pendingSinceMs !== null ? Math.max(0, Date.now() - credentialAdoption.pendingSinceMs) : null;
15209
+ credentialAdoption.pendingGeneration = null;
15210
+ credentialAdoption.pendingSinceMs = null;
15211
+ credentialAdoption.adoption++;
15212
+ void adoptCredentialGeneration(
15213
+ state,
15214
+ generation,
15215
+ credentialAdoption.adoption,
15216
+ activeSessions.size,
15217
+ deferredForMs
15218
+ ).catch(
15219
+ (error2) => logActivity(state, {
15220
+ type: "error",
15221
+ 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)}`
15222
+ })
15223
+ );
15224
+ }
15225
+ }
14109
15226
  await new Promise((resolve5) => setTimeout(resolve5, CHANNEL_POLL_INTERVAL_MS));
14110
15227
  const cycleMs = performance.now() - cycleStartedAtMs;
14111
15228
  if (idleThisCycle) idleMs += cycleMs;
@@ -14119,11 +15236,20 @@ async function driveChannels(state, driver) {
14119
15236
  if (state.interactive) displayStatus(state);
14120
15237
  break;
14121
15238
  }
14122
- if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3) {
15239
+ if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3 && !stayAwakeHolds()) {
14123
15240
  logActivity(state, { type: "info", message: "Idle timeout reached" });
14124
15241
  if (state.interactive) displayStatus(state);
14125
15242
  break;
14126
15243
  }
15244
+ if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3 && stayAwakeHolds()) {
15245
+ if (!stayAwakeSuppressionLogged) {
15246
+ stayAwakeSuppressionLogged = true;
15247
+ logActivity(state, {
15248
+ type: "info",
15249
+ message: "Idle, but staying awake until the requested minimum passes"
15250
+ });
15251
+ }
15252
+ }
14127
15253
  }
14128
15254
  }
14129
15255
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
@@ -14141,7 +15267,7 @@ function shouldWarnForReclaimSkip(reason) {
14141
15267
  return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
14142
15268
  }
14143
15269
  function sessionDbPath() {
14144
- return join12(homedir7(), ".local", "share", "opencode", "opencode.db");
15270
+ return join13(homedir8(), ".local", "share", "opencode", "opencode.db");
14145
15271
  }
14146
15272
  function logSessionCleanupActivity(state, phase, entry) {
14147
15273
  logActivity(state, {
@@ -14385,7 +15511,7 @@ function scheduleSessionCleanup(state, driver, options) {
14385
15511
  for (const warning2 of config.warnings) {
14386
15512
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
14387
15513
  }
14388
- const dbBytes = statSessionDbBytes(homedir7());
15514
+ const dbBytes = statSessionDbBytes(homedir8());
14389
15515
  void (async () => {
14390
15516
  const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
14391
15517
  if (reclaimAvailability !== null) {
@@ -14452,10 +15578,12 @@ function scheduleUsageReporting(state, params) {
14452
15578
  let phase = "dormant";
14453
15579
  let rearmRequested = false;
14454
15580
  const armProbe = () => {
15581
+ if (!state.running) return;
14455
15582
  phase = "probe-pending";
14456
15583
  params.setTimer(setTimeout(() => void tick(true), params.firstDelayMs()));
14457
15584
  };
14458
15585
  const scheduleNextTick = () => {
15586
+ if (!state.running) return;
14459
15587
  if (rearmRequested) {
14460
15588
  rearmRequested = false;
14461
15589
  armProbe();
@@ -14489,6 +15617,7 @@ function scheduleUsageReporting(state, params) {
14489
15617
  phase = "tick-in-flight";
14490
15618
  try {
14491
15619
  const usage = await params.fetchUsage();
15620
+ if (!state.running) return;
14492
15621
  const result = await params.report(usage);
14493
15622
  if (result.ok) {
14494
15623
  if (consecutiveFailures > 0) {
@@ -14601,7 +15730,7 @@ function scheduleResourceUsageReporting(state, options) {
14601
15730
  });
14602
15731
  return;
14603
15732
  }
14604
- const { collect, stop } = createResourceUsageCollector(homedir7());
15733
+ const { collect, stop } = createResourceUsageCollector(homedir8());
14605
15734
  state.stopResourceUsageSampling = stop;
14606
15735
  let consecutiveFailures = 0;
14607
15736
  const tick = async () => {
@@ -14627,6 +15756,7 @@ function scheduleResourceUsageReporting(state, options) {
14627
15756
  message: lowDiskDecision.warning
14628
15757
  });
14629
15758
  }
15759
+ if (!state.running) return;
14630
15760
  const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
14631
15761
  if (result.ok) {
14632
15762
  if (consecutiveFailures > 0) {
@@ -14659,7 +15789,9 @@ function scheduleResourceUsageReporting(state, options) {
14659
15789
  message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
14660
15790
  });
14661
15791
  } finally {
14662
- state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
15792
+ if (state.running) {
15793
+ state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
15794
+ }
14663
15795
  }
14664
15796
  };
14665
15797
  state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
@@ -14839,7 +15971,7 @@ async function run(options) {
14839
15971
  let fileSyncDirectories;
14840
15972
  try {
14841
15973
  logLevel = resolveLogLevel(options);
14842
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir7());
15974
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir8());
14843
15975
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
14844
15976
  throw new Error(
14845
15977
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -14862,6 +15994,8 @@ async function run(options) {
14862
15994
  port: options.port ?? 4096,
14863
15995
  conversationFilter: options.conversation ?? null,
14864
15996
  idleTimeout: options.idleTimeout ?? null,
15997
+ stayAwakeSeconds: null,
15998
+ stayAwakeUntilMs: null,
14865
15999
  json: options.json ?? false,
14866
16000
  interactive,
14867
16001
  logLevel,
@@ -15051,6 +16185,17 @@ async function run(options) {
15051
16185
  }
15052
16186
  spinner?.succeed(`Runner: ${validation.agent.name || state.agentId}`);
15053
16187
  state.agentName = validation.agent.name;
16188
+ const requestedStayAwakeSeconds = validation.agent?.wake_stay_awake_seconds;
16189
+ if (typeof requestedStayAwakeSeconds === "number" && Number.isFinite(requestedStayAwakeSeconds) && requestedStayAwakeSeconds > 0) {
16190
+ state.stayAwakeSeconds = requestedStayAwakeSeconds;
16191
+ } else if (requestedStayAwakeSeconds !== void 0 && requestedStayAwakeSeconds !== null) {
16192
+ state.stayAwakeSeconds = null;
16193
+ log2(
16194
+ state,
16195
+ `Ignoring invalid wake_stay_awake_seconds (type: ${typeof requestedStayAwakeSeconds})`,
16196
+ "warn"
16197
+ );
16198
+ }
15054
16199
  const microvmId = process.env.MICROVM_ID?.trim();
15055
16200
  if (microvmId) {
15056
16201
  const reported = await reportMicrovmId(state.agentId, state.authHeader, microvmId);
@@ -15112,7 +16257,7 @@ async function run(options) {
15112
16257
  state.credentialSync?.arm();
15113
16258
  const dependencyInstallDiagnosticReporter = createDependencyInstallDiagnosticReporter({
15114
16259
  context: {
15115
- homeDir: homedir7(),
16260
+ homeDir: homedir8(),
15116
16261
  cwd: process.cwd(),
15117
16262
  microvmId: process.env.MICROVM_ID?.trim(),
15118
16263
  euid: process.getuid?.(),
@@ -15135,7 +16280,7 @@ async function run(options) {
15135
16280
  dependencyInstallDiagnosticReporter
15136
16281
  );
15137
16282
  const openCodeLogTail = tailOpenCodeLogFile(
15138
- resolveOpenCodeLogPath(homedir7(), process.env),
16283
+ resolveOpenCodeLogPath(homedir8(), process.env),
15139
16284
  openCodeActivityForwarder
15140
16285
  );
15141
16286
  state.stopOpenCodeLogTail = () => {
@@ -15210,6 +16355,40 @@ async function run(options) {
15210
16355
  for (const warning2 of opencodeVersionWarnings) {
15211
16356
  logActivity(state, { type: "info", level: "warn", message: warning2 });
15212
16357
  }
16358
+ const managedSetupOptions = {
16359
+ agentId: state.agentId,
16360
+ apiUrl: getApiUrlConfig(),
16361
+ getAuthHeader: () => state.authHeader,
16362
+ log: (message, level = "info") => logActivity(state, {
16363
+ type: level === "error" ? "error" : "info",
16364
+ level,
16365
+ ...level === "error" ? { error: message } : { message }
16366
+ })
16367
+ };
16368
+ const managedDiscovery = await discoverManagedInferenceSetup(managedSetupOptions);
16369
+ let managedPreparation = { kind: "none" };
16370
+ if (managedDiscovery.kind === "failed") {
16371
+ managedPreparation = managedDiscovery;
16372
+ managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
16373
+ } else if (managedDiscovery.kind === "setup") {
16374
+ const managedHealth = opencodeVersion === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
16375
+ managedPreparation = await prepareManagedInferenceSetup(
16376
+ managedSetupOptions,
16377
+ managedDiscovery,
16378
+ managedHealth
16379
+ );
16380
+ if (managedPreparation.kind === "failed") {
16381
+ managedSetupOptions.log(
16382
+ `MANAGED-INFERENCE-NOT-READY: ${managedPreparation.reason}`,
16383
+ "warn"
16384
+ );
16385
+ } else if (managedPreparation.kind === "ready") {
16386
+ managedSetupOptions.log(
16387
+ `MANAGED-INFERENCE-PREPARED: runner=${state.agentId} generation=${managedPreparation.generation}`,
16388
+ "info"
16389
+ );
16390
+ }
16391
+ }
15213
16392
  const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
15214
16393
  for (const warning2 of maxActiveSessionsWarnings) {
15215
16394
  logActivity(state, { type: "info", level: "warn", message: warning2 });
@@ -15236,6 +16415,32 @@ async function run(options) {
15236
16415
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
15237
16416
  state.opencodeVersion = oc.version;
15238
16417
  state.opencodeApiVersion = opencodeVersion;
16418
+ let configuredPlugins = [];
16419
+ let resolvedPlugins = [];
16420
+ try {
16421
+ const provenanceLog = (message) => logActivity(state, { type: "info", level: "warn", message });
16422
+ configuredPlugins = readConfiguredPluginSpecs({ log: provenanceLog });
16423
+ resolvedPlugins = resolveInstalledPluginVersions({ log: provenanceLog });
16424
+ } catch (error2) {
16425
+ const kind = error2 instanceof Error ? error2.name : "unknown error";
16426
+ logActivity(state, {
16427
+ type: "info",
16428
+ level: "warn",
16429
+ message: `Could not collect OpenCode runtime provenance: ${kind}`
16430
+ });
16431
+ }
16432
+ logActivity(state, {
16433
+ type: "info",
16434
+ level: "info",
16435
+ message: "OpenCode runtime provenance",
16436
+ metadata: {
16437
+ opencode_version: state.opencodeVersion,
16438
+ opencode_api_version: state.opencodeApiVersion,
16439
+ configured_plugins: JSON.stringify(configuredPlugins),
16440
+ resolved_plugins: JSON.stringify(resolvedPlugins)
16441
+ },
16442
+ source: "cli.runtime-provenance"
16443
+ });
15239
16444
  let opencodePassword = null;
15240
16445
  if (opencodeVersion === "v2" && "password" in oc) {
15241
16446
  const value = oc.password;
@@ -15264,7 +16469,7 @@ async function run(options) {
15264
16469
  const provenance = checkSessionDbProvenance({
15265
16470
  dbPath: sessionDbPath(),
15266
16471
  currentVersion: state.opencodeVersion,
15267
- homeDir: homedir7(),
16472
+ homeDir: homedir8(),
15268
16473
  env: process.env
15269
16474
  });
15270
16475
  if (provenance.anomaly) {
@@ -15278,6 +16483,30 @@ async function run(options) {
15278
16483
  }
15279
16484
  }
15280
16485
  state.opencodeConnected = oc.notReadyReason === null;
16486
+ if (managedPreparation.kind === "ready" && oc.notReadyReason === null) {
16487
+ try {
16488
+ const acknowledgement = await acknowledgeManagedInferenceSetup(
16489
+ managedSetupOptions,
16490
+ managedPreparation
16491
+ );
16492
+ if (acknowledgement === "acknowledged") {
16493
+ managedSetupOptions.log(
16494
+ `MANAGED-INFERENCE-ACKNOWLEDGED: runner=${state.agentId} generation=${managedPreparation.generation}`,
16495
+ "info"
16496
+ );
16497
+ } else {
16498
+ managedSetupOptions.log(
16499
+ `MANAGED-INFERENCE-NOT-READY: policy refresh is pending for runner=${state.agentId} generation=${managedPreparation.generation}`,
16500
+ "warn"
16501
+ );
16502
+ }
16503
+ } catch (error2) {
16504
+ managedSetupOptions.log(
16505
+ `MANAGED-INFERENCE-NOT-READY: ${error2 instanceof Error ? error2.message : String(error2)}`,
16506
+ "warn"
16507
+ );
16508
+ }
16509
+ }
15281
16510
  const version2 = state.opencodeVersion ? ` (v${state.opencodeVersion})` : "";
15282
16511
  ocSpinner?.succeed(`OpenCode running on port ${state.port}${version2}`);
15283
16512
  if (!state.interactive && oc.notReadyReason !== null) {
@@ -15294,7 +16523,7 @@ async function run(options) {
15294
16523
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
15295
16524
  }
15296
16525
  }
15297
- await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
16526
+ await disposeAndReloadProviderConfig(state.port, state.opencodeClient ?? void 0);
15298
16527
  if (state.opencodeApiVersion === "v2" && state.opencodeClient !== null) {
15299
16528
  await importOpenCodeV2CredentialsForState(state);
15300
16529
  }
@@ -15336,7 +16565,7 @@ async function run(options) {
15336
16565
  let existingPid;
15337
16566
  if (existsSync3(options.litestreamPidFile)) {
15338
16567
  try {
15339
- const rawPid = readFileSync7(options.litestreamPidFile, "utf8").trim();
16568
+ const rawPid = readFileSync8(options.litestreamPidFile, "utf8").trim();
15340
16569
  const parsedPid = Number(rawPid);
15341
16570
  if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
15342
16571
  existingPid = parsedPid;
@@ -15435,7 +16664,7 @@ async function run(options) {
15435
16664
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
15436
16665
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
15437
16666
  fileSyncDirectories,
15438
- homeDir: homedir7(),
16667
+ homeDir: homedir8(),
15439
16668
  maxActiveSessions,
15440
16669
  log: (entry) => (
15441
16670
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -15465,6 +16694,13 @@ async function run(options) {
15465
16694
  type: "info",
15466
16695
  message: connectionMessage
15467
16696
  });
16697
+ if (!isReconnect && state.stayAwakeSeconds !== null && state.stayAwakeUntilMs === null) {
16698
+ state.stayAwakeUntilMs = Date.now() + state.stayAwakeSeconds * 1e3;
16699
+ logActivity(state, {
16700
+ type: "info",
16701
+ message: `Staying awake for at least ${state.stayAwakeSeconds}s as requested by the wake`
16702
+ });
16703
+ }
15468
16704
  if (options.tunnelReadyFile) {
15469
16705
  const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
15470
16706
  if (marker.ok) {
@@ -15599,6 +16835,7 @@ async function run(options) {
15599
16835
  if (error2.message === "Unauthorized") tunnelSpinner?.fail("Unauthorized");
15600
16836
  throw error2;
15601
16837
  }
16838
+ if (!state.running) return;
15602
16839
  scheduleSessionCleanup(state, channelDriver, options);
15603
16840
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
15604
16841
  state.openaiUsageRearm = scheduleUsageReporting(state, {
@@ -15671,9 +16908,9 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
15671
16908
  "--endpoint <url>",
15672
16909
  "Evident API base URL (default: production; e.g. http://localhost:3001)"
15673
16910
  ).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);
16911
+ const { endpoint: endpoint2, tunnel } = thisCommand.opts();
16912
+ if (endpoint2) {
16913
+ setEndpoint(endpoint2);
15677
16914
  }
15678
16915
  if (tunnel) {
15679
16916
  setTunnelUrl(tunnel);