@evident-ai/cli 3.5.2-dev.ffa823b → 3.6.1-dev.10d71b3

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) {
@@ -703,6 +723,9 @@ async function reportResourceUsage(agentId, authHeader, usage) {
703
723
  opencode_db_bytes: usage.opencodeDbBytes
704
724
  });
705
725
  }
726
+ async function reportSessionDeletions(agentId, authHeader, deletions) {
727
+ return postBestEffort(`/runners/${agentId}/session-deletions`, authHeader, { deletions });
728
+ }
706
729
  async function getRunnerCredentialVault(agentId, authHeader) {
707
730
  const empty = { prefix: null, vaultId: null, source: null };
708
731
  try {
@@ -1156,9 +1179,9 @@ async function claudeUsage() {
1156
1179
  }
1157
1180
 
1158
1181
  // 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";
1182
+ import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "node:fs";
1183
+ import { homedir as homedir8 } from "node:os";
1184
+ import { isAbsolute as isAbsolute4, join as join14, parse, resolve as resolvePath2 } from "node:path";
1162
1185
  import chalk7 from "chalk";
1163
1186
 
1164
1187
  // ../../packages/types/src/agents/index.ts
@@ -1287,13 +1310,113 @@ var TUNNEL_USAGE_REARM_PING_PATH = "/__evident/usage-rearm";
1287
1310
  var MAX_FILE_PUSH_BYTES = 64 * 1024;
1288
1311
  var MAX_FILE_SYNC_DIRECTORIES = 16;
1289
1312
 
1313
+ // ../../packages/types/src/credential-proxy-native-config.ts
1314
+ var EVIDENT_PROXY_TOKEN_REFERENCE = "{env:EVIDENT_PROXY_TOKEN}";
1315
+ var CLAUDE_PROVIDER = {
1316
+ namespace: "evident-claude",
1317
+ name: "Evident Claude credential proxy",
1318
+ npm: "@ai-sdk/anthropic"
1319
+ };
1320
+ var NATIVE_PROVIDERS = {
1321
+ claude: CLAUDE_PROVIDER,
1322
+ anthropic: CLAUDE_PROVIDER,
1323
+ codex: {
1324
+ namespace: "evident-codex",
1325
+ name: "Evident Codex credential proxy",
1326
+ npm: "@ai-sdk/openai"
1327
+ },
1328
+ openai: {
1329
+ namespace: "evident-openai",
1330
+ name: "Evident OpenAI credential proxy",
1331
+ npm: "@ai-sdk/openai"
1332
+ },
1333
+ kimi: {
1334
+ namespace: "evident-kimi",
1335
+ name: "Evident Kimi credential proxy",
1336
+ npm: "@ai-sdk/openai-compatible",
1337
+ sdkName: "evident-kimi"
1338
+ },
1339
+ antigravity: {
1340
+ namespace: "evident-antigravity",
1341
+ name: "Evident Antigravity credential proxy",
1342
+ npm: "@ai-sdk/openai-compatible",
1343
+ sdkName: "evident-antigravity"
1344
+ },
1345
+ xai: {
1346
+ namespace: "evident-xai",
1347
+ name: "Evident xAI credential proxy",
1348
+ npm: "@ai-sdk/openai-compatible",
1349
+ sdkName: "evident-xai"
1350
+ },
1351
+ mistral: {
1352
+ namespace: "evident-mistral",
1353
+ name: "Evident Mistral credential proxy",
1354
+ npm: "@ai-sdk/openai-compatible",
1355
+ sdkName: "evident-mistral"
1356
+ }
1357
+ };
1358
+ var CREDENTIAL_PROXY_NATIVE_NAMESPACES = [
1359
+ ...new Set(Object.values(NATIVE_PROVIDERS).map(({ namespace }) => namespace))
1360
+ ];
1361
+ function credentialProxyInferenceUrl(baseUrl) {
1362
+ const trimmed = baseUrl.trim().replace(/\/+$/, "");
1363
+ return /\/v1$/i.test(trimmed) ? trimmed : `${trimmed}/v1`;
1364
+ }
1365
+ function buildCredentialProxyNativeConfig({
1366
+ inferenceBaseUrl,
1367
+ modelDescriptors
1368
+ }) {
1369
+ const baseURL = credentialProxyInferenceUrl(inferenceBaseUrl);
1370
+ const providers = {};
1371
+ const unsupportedProviders = /* @__PURE__ */ new Set();
1372
+ for (const descriptor of modelDescriptors) {
1373
+ const definition = NATIVE_PROVIDERS[descriptor.provider.trim().toLowerCase()];
1374
+ if (!definition) {
1375
+ unsupportedProviders.add(descriptor.provider.trim() || descriptor.provider);
1376
+ continue;
1377
+ }
1378
+ providers[definition.namespace] ??= definition.npm === "@ai-sdk/anthropic" ? {
1379
+ name: definition.name,
1380
+ npm: definition.npm,
1381
+ options: { baseURL, authToken: EVIDENT_PROXY_TOKEN_REFERENCE },
1382
+ models: {}
1383
+ } : definition.npm === "@ai-sdk/openai" ? {
1384
+ name: definition.name,
1385
+ npm: definition.npm,
1386
+ options: { baseURL, apiKey: EVIDENT_PROXY_TOKEN_REFERENCE },
1387
+ models: {}
1388
+ } : {
1389
+ name: definition.name,
1390
+ npm: definition.npm,
1391
+ options: {
1392
+ name: definition.sdkName,
1393
+ baseURL,
1394
+ apiKey: EVIDENT_PROXY_TOKEN_REFERENCE
1395
+ },
1396
+ models: {}
1397
+ };
1398
+ providers[definition.namespace].models[descriptor.id] = { name: descriptor.id };
1399
+ }
1400
+ return {
1401
+ unsupportedProviders: [...unsupportedProviders],
1402
+ config: {
1403
+ $schema: "https://opencode.ai/config.json",
1404
+ provider: providers
1405
+ }
1406
+ };
1407
+ }
1408
+
1290
1409
  // ../../packages/types/src/logging/index.ts
1291
1410
  var CORRELATION_ID_HEADER = "x-evident-correlation-id";
1292
1411
  var FORWARD_FAILURE_REASON_HEADER = "X-Evident-Failure-Reason";
1293
1412
  function log(level, event, fields) {
1294
1413
  const method = level === "debug" ? "log" : level;
1295
1414
  try {
1296
- console[method]("[evident]", JSON.stringify({ level, event, ...fields }));
1415
+ const payload = Object.fromEntries(
1416
+ Object.entries({ level, event, ...fields }).filter(([, value]) => value !== void 0)
1417
+ );
1418
+ const secondArgument = globalThis.navigator?.userAgent === "Cloudflare-Workers" ? payload : JSON.stringify(payload);
1419
+ console[method]("[evident]", secondArgument);
1297
1420
  } catch (err) {
1298
1421
  console.error(
1299
1422
  "[evident] log_serialize_failed",
@@ -1322,7 +1445,7 @@ import ora4 from "ora";
1322
1445
  import { select as select4 } from "@inquirer/prompts";
1323
1446
 
1324
1447
  // src/lib/telemetry.ts
1325
- var CLI_VERSION = (true ? "3.5.2-dev.ffa823b" : void 0) ?? process.env.npm_package_version ?? "unknown";
1448
+ var CLI_VERSION = (true ? "3.6.1-dev.10d71b3" : void 0) ?? process.env.npm_package_version ?? "unknown";
1326
1449
  function getCliVersion() {
1327
1450
  return CLI_VERSION;
1328
1451
  }
@@ -1403,7 +1526,7 @@ async function flushEvents() {
1403
1526
  const controller = new AbortController();
1404
1527
  const timeout = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
1405
1528
  try {
1406
- const request2 = {
1529
+ const request3 = {
1407
1530
  events,
1408
1531
  client_type: "cli",
1409
1532
  client_version: CLI_VERSION
@@ -1414,7 +1537,7 @@ async function flushEvents() {
1414
1537
  "Content-Type": "application/json",
1415
1538
  Authorization: authHeader
1416
1539
  },
1417
- body: JSON.stringify(request2),
1540
+ body: JSON.stringify(request3),
1418
1541
  signal: controller.signal
1419
1542
  });
1420
1543
  if (!response.ok) {
@@ -2071,7 +2194,8 @@ function createDependencyInstallDiagnosticReporter({
2071
2194
  var FORWARDED_LEVELS_BY_SOURCE = {
2072
2195
  "cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
2073
2196
  opencode: /* @__PURE__ */ new Set(["warn", "error"]),
2074
- "cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"])
2197
+ "cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"]),
2198
+ "cli.runtime-provenance": /* @__PURE__ */ new Set(["info", "warn", "error"])
2075
2199
  };
2076
2200
  var SEVERITY_BY_LEVEL = {
2077
2201
  info: "info",
@@ -2517,6 +2641,122 @@ function isSessionDbRecoveryRecord(value) {
2517
2641
  );
2518
2642
  }
2519
2643
 
2644
+ // src/lib/opencode/session-deletion-report.ts
2645
+ import { appendFileSync, mkdirSync, readFileSync as readFileSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
2646
+ import { dirname as dirname4, join as join4 } from "node:path";
2647
+ var SESSION_DELETION_SPOOL_MAX_RECORDS = 1e3;
2648
+ var MAX_DIAGNOSTIC_LENGTH = 200;
2649
+ function sessionDeletionReportPath(homeDir, env) {
2650
+ const override = env.EVIDENT_SESSION_DELETION_REPORT?.trim();
2651
+ return override || join4(homeDir, ".local", "state", "evident", "session-deletions.jsonl");
2652
+ }
2653
+ function appendSessionDeletionRecords(path, records) {
2654
+ if (records.length === 0) return 0;
2655
+ mkdirSync(dirname4(path), { recursive: true });
2656
+ const current = readSessionDeletionRecords(path);
2657
+ if (current.readError !== null) {
2658
+ throw new Error(`cannot enforce the spool limit after reading ${path}: ${current.readError}`);
2659
+ }
2660
+ const combined = [...current.records, ...records];
2661
+ const kept = combined.slice(-SESSION_DELETION_SPOOL_MAX_RECORDS);
2662
+ const dropped = combined.length - kept.length;
2663
+ if (dropped > 0) {
2664
+ writeFileSync(path, `${kept.map((record) => JSON.stringify(record)).join("\n")}
2665
+ `, "utf8");
2666
+ } else {
2667
+ appendFileSync(path, `${records.map((record) => JSON.stringify(record)).join("\n")}
2668
+ `, "utf8");
2669
+ }
2670
+ return dropped;
2671
+ }
2672
+ function readSessionDeletionRecords(path) {
2673
+ let content;
2674
+ try {
2675
+ content = readFileSync3(path, "utf8");
2676
+ } catch (error2) {
2677
+ if (isMissingFile(error2)) return { records: [], skippedLines: 0, readError: null };
2678
+ const readError = boundedError(error2);
2679
+ console.error(`[session-deletion-report] could not read ${path}: ${readError}`);
2680
+ return { records: [], skippedLines: 0, readError };
2681
+ }
2682
+ let skippedLines = 0;
2683
+ const records = content.split("\n").flatMap((line) => {
2684
+ if (!line.trim()) return [];
2685
+ const record = parseRecord(line, path);
2686
+ if (record) return [record];
2687
+ skippedLines++;
2688
+ return [];
2689
+ });
2690
+ return { records, skippedLines, readError: null };
2691
+ }
2692
+ function removeDeliveredSessionDeletionRecords(path, delivered) {
2693
+ let content;
2694
+ try {
2695
+ content = readFileSync3(path, "utf8");
2696
+ } catch (error2) {
2697
+ if (isMissingFile(error2)) return;
2698
+ console.error(
2699
+ `[session-deletion-report] could not read ${path} before removing delivered records: ${boundedError(error2)}`
2700
+ );
2701
+ throw error2;
2702
+ }
2703
+ const toRemove = /* @__PURE__ */ new Map();
2704
+ for (const record of delivered) {
2705
+ toRemove.set(record.opencode_session_id, (toRemove.get(record.opencode_session_id) ?? 0) + 1);
2706
+ }
2707
+ const remaining = [];
2708
+ for (const line of content.split("\n")) {
2709
+ if (!line.trim()) continue;
2710
+ const record = parseRecord(line, path);
2711
+ const count = record ? toRemove.get(record.opencode_session_id) ?? 0 : 0;
2712
+ if (record && count > 0) {
2713
+ if (count === 1) toRemove.delete(record.opencode_session_id);
2714
+ else toRemove.set(record.opencode_session_id, count - 1);
2715
+ } else {
2716
+ remaining.push(line);
2717
+ }
2718
+ }
2719
+ try {
2720
+ if (remaining.length === 0) unlinkSync2(path);
2721
+ else writeFileSync(path, `${remaining.join("\n")}
2722
+ `, "utf8");
2723
+ } catch (error2) {
2724
+ if (isMissingFile(error2)) return;
2725
+ console.error(
2726
+ `[session-deletion-report] could not write ${path} after removing delivered records: ${boundedError(error2)}`
2727
+ );
2728
+ throw error2;
2729
+ }
2730
+ }
2731
+ function parseRecord(line, path) {
2732
+ try {
2733
+ const value = JSON.parse(line);
2734
+ if (isSessionDeletionRecord(value)) return value;
2735
+ console.error(`[session-deletion-report] skipped malformed record in ${path}: invalid shape`);
2736
+ return null;
2737
+ } catch (error2) {
2738
+ console.error(
2739
+ `[session-deletion-report] skipped malformed record in ${path}: ${boundedError(error2)}`
2740
+ );
2741
+ return null;
2742
+ }
2743
+ }
2744
+ function isSessionDeletionRecord(value) {
2745
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2746
+ const record = value;
2747
+ const policy = record.policy;
2748
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) return false;
2749
+ const fields = policy;
2750
+ return typeof record.opencode_session_id === "string" && record.opencode_session_id.length > 0 && typeof record.deleted_at === "string" && record.reason === "auto_cleanup" && (fields.max_age_ms === null || typeof fields.max_age_ms === "number") && (fields.max_count === null || typeof fields.max_count === "number") && (fields.last_activity_at === null || typeof fields.last_activity_at === "string");
2751
+ }
2752
+ function isMissingFile(error2) {
2753
+ return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
2754
+ }
2755
+ function boundedError(error2) {
2756
+ const message = error2 instanceof Error ? error2.message : String(error2);
2757
+ return message.slice(0, MAX_DIAGNOSTIC_LENGTH);
2758
+ }
2759
+
2520
2760
  // src/lib/opencode/auth.ts
2521
2761
  function buildOpenCodeBasicAuthHeader(password) {
2522
2762
  return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
@@ -2867,6 +3107,58 @@ async function listAllSessions(port, client) {
2867
3107
  }
2868
3108
  return { sessions };
2869
3109
  }
3110
+ var INTERRUPT_NETWORK_CODES = /* @__PURE__ */ new Set([
3111
+ "ABORT_ERR",
3112
+ "EAI_AGAIN",
3113
+ "ECONNREFUSED",
3114
+ "ECONNRESET",
3115
+ "ENETUNREACH",
3116
+ "ENOTFOUND",
3117
+ "ETIMEDOUT",
3118
+ "UND_ERR_CONNECT_TIMEOUT",
3119
+ "UND_ERR_HEADERS_TIMEOUT",
3120
+ "UND_ERR_SOCKET"
3121
+ ]);
3122
+ function interruptFailureDiagnostic(error2) {
3123
+ const message = error2 instanceof Error ? error2.message : "";
3124
+ const httpStatus = /^OpenCode request failed: HTTP ([1-5]\d{2})(?::|$)/.exec(message)?.[1];
3125
+ if (httpStatus) return `HTTP ${httpStatus}`;
3126
+ const candidates = [error2, error2 instanceof Error ? error2.cause : void 0];
3127
+ for (const candidate of candidates) {
3128
+ if (typeof candidate !== "object" || candidate === null || !("code" in candidate)) continue;
3129
+ const code = candidate.code;
3130
+ if (typeof code === "string" && INTERRUPT_NETWORK_CODES.has(code)) {
3131
+ return redactDiagnosticText(`network ${code}`).slice(0, 120);
3132
+ }
3133
+ }
3134
+ const name = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
3135
+ return redactDiagnosticText(`request threw ${name}`).slice(0, 120);
3136
+ }
3137
+ async function interruptSession(port, id, client) {
3138
+ if (client?.version === "v2") {
3139
+ try {
3140
+ await interruptV2Session(client, id);
3141
+ return { kind: "confirmed_stopped" };
3142
+ } catch (error2) {
3143
+ return { kind: "not_confirmed", diagnostic: interruptFailureDiagnostic(error2) };
3144
+ }
3145
+ }
3146
+ try {
3147
+ const res = await requestWithClient(
3148
+ port,
3149
+ client,
3150
+ `/session/${id}/abort`,
3151
+ { method: "POST" },
3152
+ { allowStatuses: ALL_HTTP_STATUSES }
3153
+ );
3154
+ if (res.status >= 200 && res.status < 300) {
3155
+ return { kind: "confirmed_stopped", status: res.status };
3156
+ }
3157
+ return { kind: "not_confirmed", status: res.status };
3158
+ } catch (error2) {
3159
+ return { kind: "threw", diagnostic: interruptFailureDiagnostic(error2) };
3160
+ }
3161
+ }
2870
3162
  async function deleteSession(port, id, client) {
2871
3163
  if (client?.version === "v2") return deleteV2Session(client, id);
2872
3164
  try {
@@ -3036,6 +3328,52 @@ async function getModelAttachmentCapability(port, model, client) {
3036
3328
  return null;
3037
3329
  }
3038
3330
  }
3331
+ async function isPinnedProviderLoaded(port, model, client) {
3332
+ if (client?.version === "v2") return null;
3333
+ const { model: baseModel } = splitModelVariant(model);
3334
+ if (!baseModel) return null;
3335
+ const slash = baseModel.indexOf("/");
3336
+ if (slash <= 0) return null;
3337
+ const providerId = baseModel.slice(0, slash);
3338
+ try {
3339
+ const res = await requestWithClient(port, client, "/config/providers");
3340
+ if (!res.ok) {
3341
+ console.error(
3342
+ `[isPinnedProviderLoaded] GET /config/providers returned HTTP ${res.status} (port ${port})`
3343
+ );
3344
+ return null;
3345
+ }
3346
+ const body = await res.json();
3347
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
3348
+ console.error(
3349
+ `[isPinnedProviderLoaded] GET /config/providers body was not a plain object (port ${port})`
3350
+ );
3351
+ return null;
3352
+ }
3353
+ const providers = body.providers;
3354
+ if (!Array.isArray(providers)) {
3355
+ console.error(
3356
+ `[isPinnedProviderLoaded] GET /config/providers body had no providers array (port ${port})`
3357
+ );
3358
+ return null;
3359
+ }
3360
+ const providerIds = providers.filter(
3361
+ (provider) => provider !== null && typeof provider === "object" && typeof provider.id === "string"
3362
+ ).map((provider) => provider.id);
3363
+ if (providerIds.length === 0) {
3364
+ console.error(
3365
+ `[isPinnedProviderLoaded] GET /config/providers had no provider entries with string ids (port ${port})`
3366
+ );
3367
+ return null;
3368
+ }
3369
+ return providerIds.includes(providerId);
3370
+ } catch (err) {
3371
+ console.error(
3372
+ `[isPinnedProviderLoaded] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3373
+ );
3374
+ return null;
3375
+ }
3376
+ }
3039
3377
  async function buildFileParts(attachments, capable) {
3040
3378
  const outcomes = [];
3041
3379
  const parts = [];
@@ -3450,9 +3788,17 @@ function isPreamblePinnedRunning(messages, userMessageId) {
3450
3788
  const reply = findLastAssistantReplyFor(messages, userMessageId);
3451
3789
  return completedOf(reply) != null && finishOf(reply) === "tool-calls";
3452
3790
  }
3791
+ function isMidFlightPinnedRunning(messages, userMessageId) {
3792
+ if (messageRunState(messages, userMessageId) !== "running") return false;
3793
+ const reply = findLastAssistantReplyFor(messages, userMessageId);
3794
+ return reply !== null && isAssistantInFlight(reply) && completedOf(reply) == null;
3795
+ }
3453
3796
  function isB2AbandonmentConfirmed(params) {
3454
3797
  return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
3455
3798
  }
3799
+ function isMidFlightOrphanConfirmed(params) {
3800
+ return params.pinnedForMs >= params.minPinnedMs && params.notOngoingStreak >= params.minNotOngoingStreak;
3801
+ }
3456
3802
  function isAmbiguousTerminalFinish(m) {
3457
3803
  if (completedOf(m) == null) return false;
3458
3804
  if (errorOf(m) != null) return false;
@@ -3518,19 +3864,32 @@ function isAbortedTerminalReply(messages, userMessageId) {
3518
3864
  }
3519
3865
  return false;
3520
3866
  }
3867
+ var CLAUDE_AUTH_EXPIRY_MESSAGES = Object.freeze([
3868
+ "Claude Code credentials are unavailable or expired. Run `claude` to refresh them."
3869
+ ]);
3870
+ function extractReplyErrorShape(reply) {
3871
+ const error2 = errorOf(reply);
3872
+ if (error2 == null || typeof error2 !== "object") {
3873
+ return { name: null, statusCode: null };
3874
+ }
3875
+ const e = error2;
3876
+ const name = typeof e.name === "string" ? e.name.slice(0, 100) : null;
3877
+ const data = e.data;
3878
+ const statusCode = typeof data?.statusCode === "number" && Number.isFinite(data.statusCode) ? data.statusCode : null;
3879
+ return { name, statusCode };
3880
+ }
3521
3881
  function classifyReplyAuthError(reply) {
3522
3882
  const error2 = errorOf(reply);
3523
3883
  if (error2 == null || typeof error2 !== "object") return null;
3524
3884
  const e = error2;
3525
3885
  const replyProviderId = reply?.info?.providerID ?? null;
3526
3886
  const replyModelId = reply?.info?.modelID ?? null;
3887
+ const data = e.data;
3527
3888
  if (e.name === "ProviderAuthError") {
3528
- const data = e.data;
3529
3889
  const providerId = typeof data?.providerID === "string" && data.providerID || replyProviderId;
3530
3890
  return { kind: "model_auth", providerId, modelId: replyModelId, reason: "missing" };
3531
3891
  }
3532
3892
  if (e.name === "APIError") {
3533
- const data = e.data;
3534
3893
  const statusCode = data?.statusCode;
3535
3894
  if (statusCode === 401 || statusCode === 403) {
3536
3895
  return {
@@ -3541,11 +3900,23 @@ function classifyReplyAuthError(reply) {
3541
3900
  };
3542
3901
  }
3543
3902
  }
3903
+ const errorMessage3 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
3904
+ if (errorMessage3 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage3.includes(knownMessage))) {
3905
+ return {
3906
+ kind: "model_auth",
3907
+ providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
3908
+ modelId: replyModelId,
3909
+ reason: "rejected"
3910
+ };
3911
+ }
3544
3912
  return null;
3545
3913
  }
3546
3914
  function messageFailure(messages, userMessageId) {
3547
3915
  return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
3548
3916
  }
3917
+ function isProviderModelNotFoundError(reason) {
3918
+ return reason.includes("Model not found:");
3919
+ }
3549
3920
  function findLatestSubagentAuthOutcome(messages, sinceMs) {
3550
3921
  if (!messages || messages.length === 0) return null;
3551
3922
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -3754,23 +4125,49 @@ async function readSessionErrorStream(port, options, client) {
3754
4125
  if (reader) void reader.cancel().catch(() => void 0);
3755
4126
  }
3756
4127
  }
3757
- async function reloadProviderCache(port, client) {
3758
- if (client?.version === "v2") return;
4128
+ async function disposeAndReloadProviderConfig(port, client) {
4129
+ if (client?.version === "v2") return { outcome: "skipped_v2" };
3759
4130
  try {
3760
- const res = await requestWithClient(port, client, "/config", {
3761
- method: "PATCH",
3762
- headers: { "Content-Type": "application/json" },
3763
- body: JSON.stringify({})
3764
- });
4131
+ const res = await requestWithClient(
4132
+ port,
4133
+ client,
4134
+ "/config",
4135
+ {
4136
+ method: "PATCH",
4137
+ headers: { "Content-Type": "application/json" },
4138
+ body: JSON.stringify({})
4139
+ },
4140
+ { allowStatuses: ALL_HTTP_STATUSES }
4141
+ );
3765
4142
  if (!res.ok) {
3766
4143
  console.error(
3767
- `[reloadProviderCache] PATCH /config returned HTTP ${res.status} (port ${port})`
4144
+ `[disposeAndReloadProviderConfig] PATCH /config returned HTTP ${res.status} (port ${port})`
3768
4145
  );
4146
+ return { outcome: "failed", status: res.status, detail: null };
3769
4147
  }
4148
+ return { outcome: "reloaded" };
3770
4149
  } catch (err) {
3771
4150
  console.error(
3772
- `[reloadProviderCache] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
4151
+ `[disposeAndReloadProviderConfig] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3773
4152
  );
4153
+ return {
4154
+ outcome: "failed",
4155
+ status: null,
4156
+ detail: err instanceof Error ? err.message : String(err)
4157
+ };
4158
+ }
4159
+ }
4160
+ async function probeOpenCodeRequestReadiness(port, client) {
4161
+ try {
4162
+ const res = await requestWithClient(port, client, "/config", void 0, {
4163
+ allowStatuses: ALL_HTTP_STATUSES
4164
+ });
4165
+ const body = await res.text();
4166
+ return { ok: res.ok, status: res.status, bodyEmpty: body.trim().length === 0, detail: null };
4167
+ } catch (err) {
4168
+ const detail = err instanceof Error ? err.message : String(err);
4169
+ console.error(`[probeOpenCodeRequestReadiness] GET /config failed (port ${port}): ${detail}`);
4170
+ return { ok: false, status: null, bodyEmpty: null, detail };
3774
4171
  }
3775
4172
  }
3776
4173
 
@@ -4166,6 +4563,11 @@ async function listV2Permissions(client, sessionId) {
4166
4563
  return null;
4167
4564
  }
4168
4565
  }
4566
+ async function interruptV2Session(client, sessionId) {
4567
+ await client.request(`/api/session/${encodeURIComponent(sessionId)}/interrupt`, {
4568
+ method: "POST"
4569
+ });
4570
+ }
4169
4571
  async function getV2ActiveSessions(client) {
4170
4572
  try {
4171
4573
  const body = await readJson(await client.request("/api/session/active"));
@@ -4262,13 +4664,13 @@ async function readV2SessionErrorStream(client, options) {
4262
4664
  }
4263
4665
 
4264
4666
  // src/lib/opencode/credential-import-v2.ts
4265
- import { readFileSync as readFileSync4 } from "node:fs";
4266
- import { join as join5 } from "node:path";
4667
+ import { readFileSync as readFileSync5 } from "node:fs";
4668
+ import { join as join6 } from "node:path";
4267
4669
 
4268
4670
  // src/lib/openai-usage.ts
4269
- import { readFileSync as readFileSync3 } from "node:fs";
4671
+ import { readFileSync as readFileSync4 } from "node:fs";
4270
4672
  import { homedir as homedir3 } from "node:os";
4271
- import { join as join4 } from "node:path";
4673
+ import { join as join5 } from "node:path";
4272
4674
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
4273
4675
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
4274
4676
  var OpenAiUsageError = class extends Error {
@@ -4282,7 +4684,7 @@ function isLocalCredentialProblem2(err) {
4282
4684
  }
4283
4685
  function readOpenCodeChatGptCredentials() {
4284
4686
  try {
4285
- const raw = readFileSync3(join4(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4687
+ const raw = readFileSync4(join5(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4286
4688
  let parsed;
4287
4689
  try {
4288
4690
  parsed = JSON.parse(raw);
@@ -4514,7 +4916,7 @@ function emptyResult(unsupported = [], outcome = "completed") {
4514
4916
  }
4515
4917
  function readOpenCodeCredentials(homeDir, log3) {
4516
4918
  try {
4517
- const raw = readFileSync4(join5(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
4919
+ const raw = readFileSync5(join6(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
4518
4920
  let parsed;
4519
4921
  try {
4520
4922
  parsed = JSON.parse(raw);
@@ -4568,11 +4970,11 @@ function failureMessage(status2) {
4568
4970
  function isRetryableStatus(status2) {
4569
4971
  return CONNECT_KEY_RETRY_STATUSES.includes(status2);
4570
4972
  }
4571
- async function importOneCredential(request2, credential, sleepForRetry, log3) {
4973
+ async function importOneCredential(request3, credential, sleepForRetry, log3) {
4572
4974
  const path = `/api/integration/${encodeURIComponent(credential.integrationId)}/connect/key`;
4573
4975
  for (let attempt = 1; attempt <= CONNECT_KEY_MAX_ATTEMPTS; attempt += 1) {
4574
4976
  try {
4575
- const response = await request2(
4977
+ const response = await request3(
4576
4978
  path,
4577
4979
  {
4578
4980
  method: "POST",
@@ -4607,7 +5009,7 @@ async function importOneCredential(request2, credential, sleepForRetry, log3) {
4607
5009
  throw new Error("OpenCode credential import retry loop did not return");
4608
5010
  }
4609
5011
  async function importOpenCodeCredentials({
4610
- request: request2,
5012
+ request: request3,
4611
5013
  classified,
4612
5014
  connectedIntegrationIds,
4613
5015
  sleepForRetry = sleep2,
@@ -4619,7 +5021,7 @@ async function importOpenCodeCredentials({
4619
5021
  result.alreadyConnected.push(credential.integrationId);
4620
5022
  continue;
4621
5023
  }
4622
- const outcome = await importOneCredential(request2, credential, sleepForRetry, log3);
5024
+ const outcome = await importOneCredential(request3, credential, sleepForRetry, log3);
4623
5025
  if (outcome.ok) result.imported.push(credential.integrationId);
4624
5026
  else result.failed.push(outcome.failure);
4625
5027
  }
@@ -4637,7 +5039,7 @@ function parseIntegrationList(value) {
4637
5039
  return { integrationIds, hasData: value.data.length > 0 };
4638
5040
  }
4639
5041
  async function getConnectedOpenCodeIntegrationIds({
4640
- request: request2,
5042
+ request: request3,
4641
5043
  directory,
4642
5044
  sleepForRetry = sleep2,
4643
5045
  log: log3
@@ -4645,7 +5047,7 @@ async function getConnectedOpenCodeIntegrationIds({
4645
5047
  const path = `/api/integration?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
4646
5048
  for (let attempt = 1; attempt <= INTEGRATION_LIST_MAX_READS; attempt += 1) {
4647
5049
  try {
4648
- const response = await request2(path);
5050
+ const response = await request3(path);
4649
5051
  const parsed = parseIntegrationList(await response.json());
4650
5052
  if (parsed === null) {
4651
5053
  log3(
@@ -4674,7 +5076,7 @@ async function getConnectedOpenCodeIntegrationIds({
4674
5076
  return null;
4675
5077
  }
4676
5078
  async function importOpenCodeV2Credentials({
4677
- request: request2,
5079
+ request: request3,
4678
5080
  homeDir,
4679
5081
  directory,
4680
5082
  sleepForRetry = sleep2,
@@ -4692,7 +5094,7 @@ async function importOpenCodeV2Credentials({
4692
5094
  const classified = classifyOpenCodeCredentials(credentials2);
4693
5095
  if (classified.importable.length === 0) return emptyResult(classified.unsupported);
4694
5096
  const connectedIntegrationIds = await getConnectedOpenCodeIntegrationIds({
4695
- request: request2,
5097
+ request: request3,
4696
5098
  directory,
4697
5099
  sleepForRetry,
4698
5100
  log: log3
@@ -4701,7 +5103,7 @@ async function importOpenCodeV2Credentials({
4701
5103
  return emptyResult(classified.unsupported, "connection-list-unavailable");
4702
5104
  }
4703
5105
  return importOpenCodeCredentials({
4704
- request: request2,
5106
+ request: request3,
4705
5107
  classified,
4706
5108
  connectedIntegrationIds,
4707
5109
  sleepForRetry,
@@ -4714,9 +5116,9 @@ async function importOpenCodeV2Credentials({
4714
5116
 
4715
5117
  // src/lib/opencode/session-db-boot.ts
4716
5118
  import { spawn as spawn2 } from "node:child_process";
4717
- import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
5119
+ import { mkdirSync as mkdirSync2, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
4718
5120
  import { homedir as homedir4 } from "node:os";
4719
- import { dirname as dirname4, resolve as resolvePath } from "node:path";
5121
+ import { dirname as dirname5, resolve as resolvePath } from "node:path";
4720
5122
 
4721
5123
  // src/lib/runner-synchroniser.ts
4722
5124
  import { spawn } from "node:child_process";
@@ -4832,7 +5234,7 @@ function reportRecord(stage, outcome, reason, litestreamExitCode, options) {
4832
5234
  function clearMarker(options) {
4833
5235
  if (!options.noReplicateMarker) return;
4834
5236
  try {
4835
- unlinkSync2(options.noReplicateMarker);
5237
+ unlinkSync3(options.noReplicateMarker);
4836
5238
  } catch (error2) {
4837
5239
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return;
4838
5240
  options.log(
@@ -4844,8 +5246,8 @@ function clearMarker(options) {
4844
5246
  function markNoReplicate(options, message) {
4845
5247
  if (options.noReplicateMarker) {
4846
5248
  try {
4847
- mkdirSync(dirname4(options.noReplicateMarker), { recursive: true });
4848
- writeFileSync(options.noReplicateMarker, "");
5249
+ mkdirSync2(dirname5(options.noReplicateMarker), { recursive: true });
5250
+ writeFileSync2(options.noReplicateMarker, "");
4849
5251
  } catch (error2) {
4850
5252
  options.log(
4851
5253
  `Could not write session-DB no-replicate marker ${options.noReplicateMarker}: ${error2 instanceof Error ? error2.message : String(error2)}`,
@@ -4858,7 +5260,7 @@ function markNoReplicate(options, message) {
4858
5260
  function discardSessionDbDebris(options) {
4859
5261
  for (const path of [options.dbPath, `${options.dbPath}-wal`, `${options.dbPath}-shm`]) {
4860
5262
  try {
4861
- unlinkSync2(path);
5263
+ unlinkSync3(path);
4862
5264
  } catch (error2) {
4863
5265
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") continue;
4864
5266
  options.log(
@@ -4999,8 +5401,8 @@ async function ensureLitestreamConfig(options, env) {
4999
5401
  return null;
5000
5402
  }
5001
5403
  try {
5002
- mkdirSync(dirname4(configPath), { recursive: true });
5003
- writeFileSync(configPath, rendered.stdout);
5404
+ mkdirSync2(dirname5(configPath), { recursive: true });
5405
+ writeFileSync2(configPath, rendered.stdout);
5004
5406
  } catch (error2) {
5005
5407
  options.log(
5006
5408
  `Could not write ${configPath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
@@ -5265,8 +5667,8 @@ async function restoreAndVerifySessionDb(options) {
5265
5667
 
5266
5668
  // src/lib/opencode/session-db-provenance.ts
5267
5669
  import { createRequire } from "node:module";
5268
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
5269
- import { dirname as dirname5, join as join6 } from "node:path";
5670
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
5671
+ import { dirname as dirname6, join as join7 } from "node:path";
5270
5672
  var require2 = createRequire(import.meta.url);
5271
5673
  function readSessionDbMigrationIds(dbPath) {
5272
5674
  let db;
@@ -5303,12 +5705,12 @@ function readSessionDbMigrationIds(dbPath) {
5303
5705
  }
5304
5706
  function sessionDbProvenanceStatePath(homeDir, env) {
5305
5707
  const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
5306
- return override || join6(homeDir, ".local", "state", "evident", "session-db-provenance.json");
5708
+ return override || join7(homeDir, ".local", "state", "evident", "session-db-provenance.json");
5307
5709
  }
5308
5710
  function loadSessionDbProvenanceState(path) {
5309
5711
  let value;
5310
5712
  try {
5311
- value = JSON.parse(readFileSync5(path, "utf8"));
5713
+ value = JSON.parse(readFileSync6(path, "utf8"));
5312
5714
  } catch (error2) {
5313
5715
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
5314
5716
  console.error(
@@ -5332,8 +5734,8 @@ function loadSessionDbProvenanceState(path) {
5332
5734
  }
5333
5735
  function saveSessionDbProvenanceState(path, state) {
5334
5736
  try {
5335
- mkdirSync2(dirname5(path), { recursive: true });
5336
- writeFileSync2(path, `${JSON.stringify(state, null, 2)}
5737
+ mkdirSync3(dirname6(path), { recursive: true });
5738
+ writeFileSync3(path, `${JSON.stringify(state, null, 2)}
5337
5739
  `, "utf8");
5338
5740
  } catch (error2) {
5339
5741
  console.error(
@@ -5392,7 +5794,11 @@ function isSessionDbProvenanceRecord(value) {
5392
5794
  var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
5393
5795
 
5394
5796
  // src/lib/opencode/opencode-version-gate.ts
5395
- var QUEUE_VALIDATED_OPENCODE_VERSIONS = ["1.17.11", "1.18.3"];
5797
+ var QUEUE_VALIDATED_OPENCODE_VERSIONS = [
5798
+ "1.17.11",
5799
+ "1.18.3",
5800
+ "1.18.35"
5801
+ ];
5396
5802
  function isQueueValidatedVersion(version2) {
5397
5803
  if (!version2) return false;
5398
5804
  return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version2);
@@ -5977,15 +6383,15 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
5977
6383
 
5978
6384
  // src/lib/opencode/session-db-size.ts
5979
6385
  import { statSync as statSync4 } from "node:fs";
5980
- import { join as join7 } from "node:path";
6386
+ import { join as join8 } from "node:path";
5981
6387
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
5982
6388
  function statSessionDbBytes(homeDir) {
5983
- const dbPath = join7(homeDir, ".local", "share", "opencode", "opencode.db");
6389
+ const dbPath = join8(homeDir, ".local", "share", "opencode", "opencode.db");
5984
6390
  try {
5985
6391
  return statSync4(dbPath).size;
5986
6392
  } catch (err) {
5987
- const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
5988
- if (!isMissingFile) {
6393
+ const isMissingFile2 = err instanceof Error && "code" in err && err.code === "ENOENT";
6394
+ if (!isMissingFile2) {
5989
6395
  console.error(
5990
6396
  `[statSessionDbBytes] could not stat ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
5991
6397
  );
@@ -6010,12 +6416,12 @@ function buildSessionStoreSizeWarning(input) {
6010
6416
  // src/lib/opencode/log-tail.ts
6011
6417
  import { statSync as statSync5 } from "node:fs";
6012
6418
  import { homedir as homedir5 } from "node:os";
6013
- import { join as join8 } from "node:path";
6419
+ import { join as join9 } from "node:path";
6014
6420
  import { open as open2, stat } from "node:fs/promises";
6015
6421
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
6016
6422
  function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
6017
- const dataDir = env.XDG_DATA_HOME || join8(homeDir, ".local", "share");
6018
- return join8(dataDir, "opencode", "log", "opencode.log");
6423
+ const dataDir = env.XDG_DATA_HOME || join9(homeDir, ".local", "share");
6424
+ return join9(dataDir, "opencode", "log", "opencode.log");
6019
6425
  }
6020
6426
  function isEnoent(error2) {
6021
6427
  return error2?.code === "ENOENT";
@@ -6437,6 +6843,7 @@ function connectTunnel(options) {
6437
6843
  }, 3e4);
6438
6844
  let relaySilenceTimeout;
6439
6845
  let relaySilenceTimedOut = false;
6846
+ let relayConnectionId;
6440
6847
  const clearRelaySilenceTimeout = () => {
6441
6848
  if (relaySilenceTimeout === void 0) return;
6442
6849
  clearTimeout(relaySilenceTimeout);
@@ -6522,7 +6929,8 @@ function connectTunnel(options) {
6522
6929
  armRelaySilenceTimeout();
6523
6930
  forwarder.setBinaryFramesSupported(message.binary_frames === true);
6524
6931
  const connectedAgentId = message.agent_id ?? agentId;
6525
- onConnected?.(connectedAgentId);
6932
+ relayConnectionId = message.connection_id;
6933
+ onConnected?.(connectedAgentId, relayConnectionId);
6526
6934
  resolve5({
6527
6935
  ws,
6528
6936
  close: () => ws.close(1e3, "CLI shutdown")
@@ -6559,7 +6967,7 @@ function connectTunnel(options) {
6559
6967
  clearRelaySilenceTimeout();
6560
6968
  const reasonStr = (relaySilenceTimedOut ? "no relay heartbeat for 90s \u2014 reconnecting" : void 0) || reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
6561
6969
  forwarder.abortAll();
6562
- onDisconnected?.(code, reasonStr);
6970
+ onDisconnected?.(code, reasonStr, relayConnectionId);
6563
6971
  });
6564
6972
  });
6565
6973
  }
@@ -6622,7 +7030,7 @@ var RunnerConnection = class _RunnerConnection {
6622
7030
  authHeader: this.opts.getAuthHeader(),
6623
7031
  port: this.opts.port,
6624
7032
  openCodePassword: this.opts.openCodePassword,
6625
- onConnected: (agentId) => {
7033
+ onConnected: (agentId, relayConnectionId) => {
6626
7034
  const attempt = this.reconnectAttempt + 1;
6627
7035
  this.reconnectAttempt = 0;
6628
7036
  this.reconnecting = false;
@@ -6632,13 +7040,17 @@ var RunnerConnection = class _RunnerConnection {
6632
7040
  this.disconnectedAtMs = null;
6633
7041
  events.onConnected(agentId, isReconnect, {
6634
7042
  generation: this.generation,
7043
+ relayConnectionId: relayConnectionId ?? null,
6635
7044
  attempt,
6636
7045
  downtimeMs
6637
7046
  });
6638
7047
  },
6639
- onDisconnected: (code, reason) => {
7048
+ onDisconnected: (code, reason, relayConnectionId) => {
6640
7049
  this.disconnectedAtMs = this.now();
6641
- events.onDisconnected(code, reason, { generation: this.generation });
7050
+ events.onDisconnected(code, reason, {
7051
+ generation: this.generation,
7052
+ relayConnectionId: relayConnectionId ?? null
7053
+ });
6642
7054
  const relayRequestedReconnect = code === 1e3 && _RunnerConnection.RELAY_RECONNECT_CLOSE_REASONS.has(reason);
6643
7055
  if (this.opts.isRunning() && (code !== 1e3 || relayRequestedReconnect) && !this.reconnecting) {
6644
7056
  this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
@@ -6676,10 +7088,10 @@ var RunnerConnection = class _RunnerConnection {
6676
7088
  };
6677
7089
 
6678
7090
  // src/lib/tunnel/ready-marker.ts
6679
- import { writeFileSync as writeFileSync3 } from "node:fs";
7091
+ import { writeFileSync as writeFileSync4 } from "node:fs";
6680
7092
  function writeTunnelReadyMarker(path, agentId) {
6681
7093
  try {
6682
- writeFileSync3(path, `${agentId}
7094
+ writeFileSync4(path, `${agentId}
6683
7095
  `);
6684
7096
  return { ok: true };
6685
7097
  } catch (error2) {
@@ -6704,7 +7116,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
6704
7116
  }
6705
7117
 
6706
7118
  // src/lib/process-liveness.ts
6707
- import { readFileSync as readFileSync6 } from "node:fs";
7119
+ import { readFileSync as readFileSync7 } from "node:fs";
6708
7120
  function isProcessAlive(pid) {
6709
7121
  try {
6710
7122
  process.kill(pid, 0);
@@ -6719,7 +7131,7 @@ function isProcessAlive(pid) {
6719
7131
  }
6720
7132
  if (process.platform !== "linux") return true;
6721
7133
  try {
6722
- const status2 = readFileSync6(`/proc/${pid}/status`, "utf8");
7134
+ const status2 = readFileSync7(`/proc/${pid}/status`, "utf8");
6723
7135
  return !/^State:\s+Z(?:\s|$)/m.test(status2);
6724
7136
  } catch (error2) {
6725
7137
  console.error(
@@ -7078,18 +7490,18 @@ function createSessionStoreHealth(options = {}) {
7078
7490
  import { homedir as homedir6 } from "node:os";
7079
7491
 
7080
7492
  // src/lib/runner-file-sync.ts
7081
- import { join as join10 } from "node:path";
7493
+ import { join as join11 } from "node:path";
7082
7494
 
7083
7495
  // src/lib/file-push.ts
7084
7496
  import { randomUUID } from "node:crypto";
7085
7497
  import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
7086
- 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";
7498
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
7087
7499
  var FILE_MODE = 384;
7088
7500
  var DIRECTORY_MODE = 448;
7089
- async function writePushedFile(request2) {
7090
- const { requestedPath, content, allowedDirectories, homeDir } = request2;
7501
+ async function writePushedFile(request3) {
7502
+ const { requestedPath, content, allowedDirectories, allowedFiles, homeDir } = request3;
7091
7503
  const bytes = content.byteLength;
7092
- if (allowedDirectories.length === 0) {
7504
+ if (allowedDirectories.length === 0 && allowedFiles.length === 0) {
7093
7505
  return refuse("file_sync_disabled", "File sync is not enabled on this runner.", {
7094
7506
  path: requestedPath,
7095
7507
  bytes
@@ -7114,10 +7526,10 @@ async function writePushedFile(request2) {
7114
7526
  }
7115
7527
  try {
7116
7528
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
7117
- dirname6(candidate)
7529
+ dirname7(candidate)
7118
7530
  );
7119
- const realTarget = join9(existingAncestor, ...missingSegments, basename2(candidate));
7120
- const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
7531
+ const realTarget = join10(existingAncestor, ...missingSegments, basename2(candidate));
7532
+ const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget) ?? await findMatchingAllowedFile(allowedFiles, realTarget);
7121
7533
  if (allowedDirectory === null) {
7122
7534
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
7123
7535
  path: realTarget,
@@ -7126,8 +7538,8 @@ async function writePushedFile(request2) {
7126
7538
  }
7127
7539
  if (missingSegments.length > 0) {
7128
7540
  await createMissingDirectories(existingAncestor, missingSegments);
7129
- const realParent = await realpath(dirname6(realTarget));
7130
- if (realParent !== dirname6(realTarget) || !contains(allowedDirectory, realTarget)) {
7541
+ const realParent = await realpath(dirname7(realTarget));
7542
+ if (realParent !== dirname7(realTarget) || !contains(allowedDirectory, realTarget)) {
7131
7543
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
7132
7544
  path: realTarget,
7133
7545
  bytes,
@@ -7152,7 +7564,7 @@ function expandAndValidate(requestedPath, homeDir) {
7152
7564
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
7153
7565
  return null;
7154
7566
  }
7155
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join9(homeDir, requestedPath.slice(2)) : requestedPath;
7567
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
7156
7568
  if (expanded.split(/[/\\]/).includes("..")) {
7157
7569
  return null;
7158
7570
  }
@@ -7170,7 +7582,7 @@ async function resolveNearestExistingAncestor(directory) {
7170
7582
  try {
7171
7583
  return { existingAncestor: await realpath(current), missingSegments };
7172
7584
  } catch (err) {
7173
- const parent = dirname6(current);
7585
+ const parent = dirname7(current);
7174
7586
  if (err.code !== "ENOENT" || parent === current) {
7175
7587
  throw err;
7176
7588
  }
@@ -7192,6 +7604,22 @@ async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
7192
7604
  }
7193
7605
  return null;
7194
7606
  }
7607
+ async function findMatchingAllowedFile(allowedFiles, realTarget) {
7608
+ for (const entry of allowedFiles) {
7609
+ if (!isAbsolute2(entry)) {
7610
+ log("warn", "file_push_allowed_directory_skipped", {
7611
+ directory: entry,
7612
+ reason: "not_absolute_file"
7613
+ });
7614
+ continue;
7615
+ }
7616
+ const realParent = await realpathCreatingIfMissing(dirname7(entry));
7617
+ if (realParent !== null && join10(realParent, basename2(entry)) === realTarget) {
7618
+ return realParent;
7619
+ }
7620
+ }
7621
+ return null;
7622
+ }
7195
7623
  async function realpathCreatingIfMissing(directory) {
7196
7624
  try {
7197
7625
  return await realpath(directory);
@@ -7225,13 +7653,13 @@ function contains(realDirectory, realTarget) {
7225
7653
  async function createMissingDirectories(existingAncestor, missingSegments) {
7226
7654
  let current = existingAncestor;
7227
7655
  for (const segment of missingSegments) {
7228
- current = join9(current, segment);
7656
+ current = join10(current, segment);
7229
7657
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
7230
7658
  await chmod(current, DIRECTORY_MODE);
7231
7659
  }
7232
7660
  }
7233
7661
  async function writeAtomically(realTarget, content) {
7234
- const temporaryPath = join9(dirname6(realTarget), `.evident-push-${randomUUID()}.tmp`);
7662
+ const temporaryPath = join10(dirname7(realTarget), `.evident-push-${randomUUID()}.tmp`);
7235
7663
  let handle;
7236
7664
  try {
7237
7665
  handle = await open3(temporaryPath, "wx", FILE_MODE);
@@ -7361,16 +7789,16 @@ var NOT_APPLIED = {
7361
7789
  opencodeAuthApplied: false
7362
7790
  };
7363
7791
  function isClaudeCredentialPath(requestedPath, homeDir) {
7364
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
7365
- return expanded === join10(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
7792
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join11(homeDir, requestedPath.slice(2)) : requestedPath;
7793
+ return expanded === join11(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
7366
7794
  }
7367
7795
  function isOpenCodeAuthPath(requestedPath, homeDir) {
7368
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
7369
- return expanded === join10(homeDir, ...OPENCODE_AUTH_SEGMENTS);
7796
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join11(homeDir, requestedPath.slice(2)) : requestedPath;
7797
+ return expanded === join11(homeDir, ...OPENCODE_AUTH_SEGMENTS);
7370
7798
  }
7371
7799
  async function applyOne(options, file) {
7372
7800
  const label = `${file.id.slice(0, 8)} (${file.path})`;
7373
- if (options.allowedDirectories.length === 0) {
7801
+ if (options.allowedDirectories.length === 0 && options.allowedFiles.length === 0) {
7374
7802
  options.log({
7375
7803
  level: "warn",
7376
7804
  message: `Runner file ${label} rejected: file sync is not enabled on this runner (start it with --enable-file-sync-to)`
@@ -7397,6 +7825,7 @@ async function applyOne(options, file) {
7397
7825
  requestedPath: file.path,
7398
7826
  content: download.content,
7399
7827
  allowedDirectories: options.allowedDirectories,
7828
+ allowedFiles: options.allowedFiles,
7400
7829
  homeDir: options.homeDir
7401
7830
  });
7402
7831
  } catch (err) {
@@ -7503,32 +7932,238 @@ function describe(err) {
7503
7932
  return err instanceof Error ? err.message : String(err);
7504
7933
  }
7505
7934
 
7506
- // src/lib/channels/driver.ts
7507
- function messageIdOf(m) {
7508
- if (!m || typeof m !== "object") return void 0;
7509
- if (typeof m.id === "string") return m.id;
7510
- const infoId = m.info?.id;
7511
- return typeof infoId === "string" ? infoId : void 0;
7935
+ // src/lib/vault-file-sync.ts
7936
+ import { createHash } from "node:crypto";
7937
+ async function syncVaultFiles(options) {
7938
+ const files = await listVaultFiles(options);
7939
+ if (files === null) return { applied: 0 };
7940
+ const listedIds = new Set(files.map((file) => file.id));
7941
+ for (const fileId of options.outcomes.keys()) {
7942
+ if (!listedIds.has(fileId)) options.outcomes.delete(fileId);
7943
+ }
7944
+ let applied = 0;
7945
+ for (const file of files) {
7946
+ try {
7947
+ if (await reconcileVaultFile(options, file)) applied += 1;
7948
+ } catch (err) {
7949
+ options.log({
7950
+ level: "error",
7951
+ message: `Vault file ${file.id} reconciliation failed unexpectedly (message processing is unaffected): ${describe2(err)}`
7952
+ });
7953
+ }
7954
+ }
7955
+ return { applied };
7512
7956
  }
7513
- function composeAbandonmentNote(finish, replyShape, outcome) {
7514
- const base = `OpenCode ended this turn after a tool call (finish="${finish ?? "(unknown)"}") ` + (outcome === "without-answer" ? "without writing an answer, and confirmed it is no longer working on this conversation. Evident is reporting the run as failed so the failure and its reason are visible in failed runs." : "after writing only part of an answer, and confirmed it is no longer working on this conversation. Evident delivered what was written and is reporting the run as finished.");
7515
- if (replyShape === null) return base;
7516
- return `${base} ${replyShape.lastToolName === null ? "No tool call was recorded" : `The last tool call was "${replyShape.lastToolName}" with status "${replyShape.lastToolStatus ?? "(unknown)"}"`}; the reply's final part types were [${replyShape.partTypes.join(", ")}]${replyShape.droppedPartCount > 0 ? ` (${replyShape.droppedPartCount} earlier part types omitted)` : ""}.`;
7957
+ async function listVaultFiles(options) {
7958
+ let response;
7959
+ try {
7960
+ response = await options.fetchImpl(`${options.apiUrl}/runners/${options.agentId}/vault-files`, {
7961
+ headers: { Authorization: options.getAuthHeader() }
7962
+ });
7963
+ } catch (err) {
7964
+ options.log({
7965
+ level: "warn",
7966
+ message: `Could not list vault files \u2014 retrying on the next drain: ${describe2(err)}`
7967
+ });
7968
+ return null;
7969
+ }
7970
+ if (!response.ok) {
7971
+ options.log({
7972
+ level: response.status === 404 ? "debug" : "warn",
7973
+ message: `Listing vault files returned HTTP ${response.status} \u2014 retrying on the next drain`
7974
+ });
7975
+ return null;
7976
+ }
7977
+ let body;
7978
+ try {
7979
+ body = await response.json();
7980
+ } catch (err) {
7981
+ options.log({
7982
+ level: "warn",
7983
+ message: `Vault file list was not readable JSON \u2014 retrying on the next drain: ${describe2(err)}`
7984
+ });
7985
+ return null;
7986
+ }
7987
+ if (body === null || typeof body !== "object" || !Array.isArray(body.files)) {
7988
+ options.log({
7989
+ level: "warn",
7990
+ message: "Vault file list was not an object with a files array \u2014 ignoring it for this drain"
7991
+ });
7992
+ return null;
7993
+ }
7994
+ const files = [];
7995
+ for (const entry of body.files) {
7996
+ const file = asVaultFile(entry);
7997
+ if (file === null) {
7998
+ options.log({
7999
+ level: "warn",
8000
+ message: "Ignoring a malformed vault file entry"
8001
+ });
8002
+ continue;
8003
+ }
8004
+ files.push(file);
8005
+ }
8006
+ return files;
7517
8007
  }
7518
- function cleanImageMime(contentType) {
7519
- if (!contentType) return null;
7520
- const media = contentType.split(";")[0].trim().toLowerCase();
7521
- return /^image\/[a-z0-9.+-]+$/.test(media) ? media : null;
8008
+ function asVaultFile(entry) {
8009
+ if (entry === null || typeof entry !== "object") return null;
8010
+ const value = entry;
8011
+ if (typeof value.id !== "string" || value.id === "" || typeof value.destination_path !== "string" || value.destination_path === "" || typeof value.size_bytes !== "number" || !Number.isFinite(value.size_bytes) || value.size_bytes < 0 || typeof value.content_sha256 !== "string" || value.content_sha256 === "" || typeof value.created_at !== "string" || typeof value.updated_at !== "string") {
8012
+ return null;
8013
+ }
8014
+ return {
8015
+ id: value.id,
8016
+ destination_path: value.destination_path,
8017
+ size_bytes: value.size_bytes,
8018
+ content_sha256: value.content_sha256,
8019
+ created_at: value.created_at,
8020
+ updated_at: value.updated_at
8021
+ };
7522
8022
  }
7523
- var LOG_LEVELS = {
7524
- debug: 0,
7525
- info: 1,
7526
- warn: 2,
7527
- error: 3
7528
- };
7529
- var DEFAULT_RETRY_POLICY = {
7530
- maxAttempts: 6,
7531
- baseDelayMs: 500,
8023
+ async function reconcileVaultFile(options, file) {
8024
+ const existing = options.outcomes.get(file.id);
8025
+ if (existing?.sha256 === file.content_sha256) {
8026
+ if (existing.reported || existing.reportAttempts >= MAX_ACK_ATTEMPTS) return false;
8027
+ await reportOutcome(options, file, existing);
8028
+ return false;
8029
+ }
8030
+ const content = await downloadVaultFile(options, file);
8031
+ if (content === null) return false;
8032
+ const actualSha256 = createHash("sha256").update(content).digest("hex");
8033
+ if (actualSha256 !== file.content_sha256) {
8034
+ options.log({
8035
+ level: "warn",
8036
+ message: `Vault file ${file.id} content hash mismatch: expected ${file.content_sha256}, received ${actualSha256} \u2014 retrying on the next drain`
8037
+ });
8038
+ return false;
8039
+ }
8040
+ const outcome = await applyVaultFile(options, file, content);
8041
+ const entry = {
8042
+ sha256: file.content_sha256,
8043
+ outcome: outcome.ok ? "applied" : "refused",
8044
+ reason: outcome.ok ? null : outcome.code,
8045
+ reported: false,
8046
+ reportAttempts: 0
8047
+ };
8048
+ options.outcomes.set(file.id, entry);
8049
+ await reportOutcome(options, file, entry);
8050
+ return outcome.ok;
8051
+ }
8052
+ async function downloadVaultFile(options, file) {
8053
+ try {
8054
+ const response = await options.fetchImpl(
8055
+ `${options.apiUrl}/runners/${options.agentId}/vault-files/${file.id}/content`,
8056
+ { headers: { Authorization: options.getAuthHeader() } }
8057
+ );
8058
+ if (!response.ok) {
8059
+ options.log({
8060
+ level: "warn",
8061
+ message: `Downloading vault file ${file.id} returned HTTP ${response.status} \u2014 retrying on the next drain`
8062
+ });
8063
+ return null;
8064
+ }
8065
+ return Buffer.from(await response.arrayBuffer());
8066
+ } catch (err) {
8067
+ options.log({
8068
+ level: "warn",
8069
+ message: `Downloading vault file ${file.id} failed \u2014 retrying on the next drain: ${describe2(err)}`
8070
+ });
8071
+ return null;
8072
+ }
8073
+ }
8074
+ async function applyVaultFile(options, file, content) {
8075
+ try {
8076
+ const outcome = await writePushedFile({
8077
+ requestedPath: file.destination_path,
8078
+ content,
8079
+ allowedDirectories: options.allowedDirectories,
8080
+ allowedFiles: options.allowedFiles,
8081
+ homeDir: options.homeDir
8082
+ });
8083
+ if (!outcome.ok) {
8084
+ options.log({
8085
+ level: "warn",
8086
+ message: `Vault file ${file.id} refused (${outcome.code}): ${outcome.message}`
8087
+ });
8088
+ } else {
8089
+ options.log({
8090
+ level: "info",
8091
+ message: `Vault file ${file.id} applied (${content.byteLength} bytes)`
8092
+ });
8093
+ }
8094
+ return outcome;
8095
+ } catch (err) {
8096
+ options.log({
8097
+ level: "error",
8098
+ message: `Vault file ${file.id} could not be written: ${describe2(err)}`
8099
+ });
8100
+ return { ok: false, code: "write_failed", message: "The runner could not write the file." };
8101
+ }
8102
+ }
8103
+ async function reportOutcome(options, file, entry) {
8104
+ const body = {
8105
+ status: entry.outcome === "applied" ? "applied" : "rejected",
8106
+ reported_sha256: entry.sha256
8107
+ };
8108
+ if (entry.outcome === "refused" && entry.reason !== null) body.reason = entry.reason;
8109
+ try {
8110
+ const response = await options.fetchImpl(
8111
+ `${options.apiUrl}/runners/${options.agentId}/vault-files/${file.id}/status`,
8112
+ {
8113
+ method: "POST",
8114
+ headers: {
8115
+ Authorization: options.getAuthHeader(),
8116
+ "Content-Type": "application/json"
8117
+ },
8118
+ body: JSON.stringify(body)
8119
+ }
8120
+ );
8121
+ if (response.ok) {
8122
+ entry.reported = true;
8123
+ return;
8124
+ }
8125
+ recordReportFailure(options, file, entry, `HTTP ${response.status}`);
8126
+ } catch (err) {
8127
+ recordReportFailure(options, file, entry, describe2(err));
8128
+ }
8129
+ }
8130
+ function recordReportFailure(options, file, entry, reason) {
8131
+ entry.reportAttempts += 1;
8132
+ options.log({
8133
+ level: "error",
8134
+ message: entry.reportAttempts >= MAX_ACK_ATTEMPTS ? `Reporting vault file ${file.id} as ${entry.outcome} failed (${reason}) \u2014 giving up after ${entry.reportAttempts} attempts` : `Reporting vault file ${file.id} as ${entry.outcome} failed (${reason}) \u2014 retrying on the next drain (attempt ${entry.reportAttempts} of ${MAX_ACK_ATTEMPTS})`
8135
+ });
8136
+ }
8137
+ function describe2(err) {
8138
+ return err instanceof Error ? err.message : String(err);
8139
+ }
8140
+
8141
+ // src/lib/channels/driver.ts
8142
+ function messageIdOf(m) {
8143
+ if (!m || typeof m !== "object") return void 0;
8144
+ if (typeof m.id === "string") return m.id;
8145
+ const infoId = m.info?.id;
8146
+ return typeof infoId === "string" ? infoId : void 0;
8147
+ }
8148
+ function composeAbandonmentNote(finish, replyShape, outcome) {
8149
+ const base = outcome === "midflight-without-answer" || outcome === "midflight-with-answer" ? "OpenCode stopped this turn without ever finishing, and confirmed the session is no longer running. " + (outcome === "midflight-without-answer" ? "Evident is reporting the run as failed so the failure and its reason are visible in failed runs." : "Evident delivered what was written and is reporting the run as finished.") : `OpenCode ended this turn after a tool call (finish="${finish ?? "(unknown)"}") ` + (outcome === "without-answer" ? "without writing an answer, and confirmed it is no longer working on this conversation. Evident is reporting the run as failed so the failure and its reason are visible in failed runs." : "after writing only part of an answer, and confirmed it is no longer working on this conversation. Evident delivered what was written and is reporting the run as finished.");
8150
+ if (replyShape === null) return base;
8151
+ return `${base} ${replyShape.lastToolName === null ? "No tool call was recorded" : `The last tool call was "${replyShape.lastToolName}" with status "${replyShape.lastToolStatus ?? "(unknown)"}"`}; the reply's final part types were [${replyShape.partTypes.join(", ")}]${replyShape.droppedPartCount > 0 ? ` (${replyShape.droppedPartCount} earlier part types omitted)` : ""}.`;
8152
+ }
8153
+ function cleanImageMime(contentType) {
8154
+ if (!contentType) return null;
8155
+ const media = contentType.split(";")[0].trim().toLowerCase();
8156
+ return /^image\/[a-z0-9.+-]+$/.test(media) ? media : null;
8157
+ }
8158
+ var LOG_LEVELS = {
8159
+ debug: 0,
8160
+ info: 1,
8161
+ warn: 2,
8162
+ error: 3
8163
+ };
8164
+ var DEFAULT_RETRY_POLICY = {
8165
+ maxAttempts: 6,
8166
+ baseDelayMs: 500,
7532
8167
  maxDelayMs: 3e4
7533
8168
  };
7534
8169
  var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
@@ -7537,7 +8172,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
7537
8172
  var MAX_BUFFERED_SESSION_ERRORS = 256;
7538
8173
  var SESSION_ERROR_RETENTION_TTL_MS = 6e4;
7539
8174
  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: ";
8175
+ var SESSION_ERROR_RETAINED_DETAIL = "Runner-observed OpenCode session error: ";
7541
8176
  var SESSION_ERROR_OBSERVED_CAUSE = "Runner-observed OpenCode session-store cause: ";
7542
8177
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
7543
8178
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
@@ -7548,6 +8183,9 @@ var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
7548
8183
  var ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1e3;
7549
8184
  var B2_ABANDONMENT_MIN_PINNED_MS = 3 * 6e4;
7550
8185
  var AMBIGUOUS_FINISH_MAX_PINNED_MS = 3 * 6e4;
8186
+ var MIDFLIGHT_ORPHAN_MIN_PINNED_MS = 3 * 6e4;
8187
+ var MIDFLIGHT_ORPHAN_RECHECK_MS = 6e4;
8188
+ var MIDFLIGHT_ORPHAN_MIN_NOT_ONGOING_STREAK = 2;
7551
8189
  var B2_ABANDONMENT_RECHECK_MS = HEARTBEAT_MS;
7552
8190
  var POLL_MISS_GRACE_MS = HEARTBEAT_MS;
7553
8191
  var WATCHER_STALL_MS = 3 * POLL_MISS_GRACE_MS;
@@ -7599,6 +8237,7 @@ var ChannelDriver = class _ChannelDriver {
7599
8237
  stuckQueuedMs;
7600
8238
  now;
7601
8239
  fileSyncDirectories;
8240
+ fileSyncExactFiles;
7602
8241
  homeDir;
7603
8242
  maxActiveSessions;
7604
8243
  watcherStallMs;
@@ -7661,12 +8300,15 @@ var ChannelDriver = class _ChannelDriver {
7661
8300
  /**
7662
8301
  * Per-conversation throttle state for the #183 recurrence warning (#1618
7663
8302
  * 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`
8303
+ * `firstWedgedAt` anchors `stuck_for_ms`; `warnAnchorAt` is the throttle
8304
+ * anchor from first detection through the first warning, then from each
8305
+ * warning onward; `consecutiveTicks` is reported in the log text so the
8306
+ * operator sees magnitude, not repetition. Cleared the moment the
8307
+ * conversation dispatches anything, or when it drops off the pending list
8308
+ * (`runDrain`'s prune, the dominant real-world path — a wedge normally ends
8309
+ * with its message completing, not a fresh dispatch on the same
8310
+ * conversation) — either way a fresh wedge, if it recurs, is a new
8311
+ * incident. Bounded FIFO, mirroring `supersededSessions`
7670
8312
  * (`MAX_WEDGED_CONVERSATIONS`).
7671
8313
  */
7672
8314
  wedgeWarnings = /* @__PURE__ */ new Map();
@@ -7896,6 +8538,7 @@ var ChannelDriver = class _ChannelDriver {
7896
8538
  subagentInvocationPrefetches = /* @__PURE__ */ new Map();
7897
8539
  /** Serialises drains so a reconnect during a drain doesn't double-process. */
7898
8540
  draining = false;
8541
+ pendingProviderRegistryRepair = null;
7899
8542
  /**
7900
8543
  * Serialises runner-file syncs (#559) so the ~2s poll tick and a concurrent
7901
8544
  * drain ping don't download, write and ack the same file twice.
@@ -7907,6 +8550,7 @@ var ChannelDriver = class _ChannelDriver {
7907
8550
  * re-downloaded and re-written every ~2s until the server expires it.
7908
8551
  */
7909
8552
  fileAckFailures = /* @__PURE__ */ new Map();
8553
+ vaultFileOutcomes = /* @__PURE__ */ new Map();
7910
8554
  /**
7911
8555
  * Monotonic count of files this runner has pulled and written (#559). Only
7912
8556
  * ever increases, so `run.ts` detects work by comparing it against the value
@@ -7963,6 +8607,7 @@ var ChannelDriver = class _ChannelDriver {
7963
8607
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
7964
8608
  this.now = config.now ?? (() => Date.now());
7965
8609
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
8610
+ this.fileSyncExactFiles = config.fileSyncExactFiles ?? [];
7966
8611
  this.homeDir = config.homeDir ?? homedir6();
7967
8612
  this.maxActiveSessions = config.maxActiveSessions;
7968
8613
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
@@ -8060,14 +8705,27 @@ var ChannelDriver = class _ChannelDriver {
8060
8705
  getAuthHeader: this.getAuthHeader,
8061
8706
  fetchImpl: this.fetchImpl,
8062
8707
  allowedDirectories: this.fileSyncDirectories,
8708
+ allowedFiles: this.fileSyncExactFiles,
8063
8709
  homeDir: this.homeDir,
8064
8710
  ackFailures: this.fileAckFailures,
8065
8711
  log: this.log
8066
8712
  });
8067
- this.appliedFileCount += result.applied;
8713
+ const vaultResult = await syncVaultFiles({
8714
+ agentId: this.agentId,
8715
+ apiUrl: this.apiUrl,
8716
+ getAuthHeader: this.getAuthHeader,
8717
+ fetchImpl: this.fetchImpl,
8718
+ allowedDirectories: this.fileSyncDirectories,
8719
+ allowedFiles: this.fileSyncExactFiles,
8720
+ homeDir: this.homeDir,
8721
+ outcomes: this.vaultFileOutcomes,
8722
+ log: this.log
8723
+ });
8724
+ const applied = result.applied + vaultResult.applied;
8725
+ this.appliedFileCount += applied;
8068
8726
  if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;
8069
8727
  if (result.opencodeAuthApplied) this.opencodeAuthApplyCount += 1;
8070
- return result.applied;
8728
+ return applied;
8071
8729
  } catch (err) {
8072
8730
  this.log({
8073
8731
  level: "error",
@@ -8081,8 +8739,33 @@ var ChannelDriver = class _ChannelDriver {
8081
8739
  async runDrain() {
8082
8740
  let dispatched = 0;
8083
8741
  try {
8742
+ const pendingRepair = this.pendingProviderRegistryRepair;
8743
+ if (pendingRepair && this.protectedSessionIds().size === 0) {
8744
+ try {
8745
+ await this.issueProviderRegistryReload(
8746
+ pendingRepair.conversationId,
8747
+ pendingRepair.messageId
8748
+ );
8749
+ } catch (err) {
8750
+ this.log({
8751
+ level: "error",
8752
+ message: `Provider registry repair adoption failed for message ${pendingRepair.messageId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`,
8753
+ conversation_id: pendingRepair.conversationId,
8754
+ message_id: pendingRepair.messageId
8755
+ });
8756
+ }
8757
+ return 0;
8758
+ }
8084
8759
  this.pendingMessageIds.clear();
8085
8760
  const conversations = await this.getPendingConversations();
8761
+ if (this.wedgeWarnings.size > 0) {
8762
+ const pendingConversationIds = new Set(conversations.map(({ id }) => id));
8763
+ for (const conversationId of this.wedgeWarnings.keys()) {
8764
+ if (!pendingConversationIds.has(conversationId)) {
8765
+ this.wedgeWarnings.delete(conversationId);
8766
+ }
8767
+ }
8768
+ }
8086
8769
  if (this.recycleRequestedFlag) {
8087
8770
  this.stop();
8088
8771
  }
@@ -8454,6 +9137,47 @@ var ChannelDriver = class _ChannelDriver {
8454
9137
  agent: message.opencode_agent ?? void 0,
8455
9138
  model: message.opencode_model ?? void 0
8456
9139
  };
9140
+ const pinnedProviderLoaded = await isPinnedProviderLoaded(
9141
+ this.port,
9142
+ options.model,
9143
+ this.openCodeClient
9144
+ );
9145
+ if (pinnedProviderLoaded === false) {
9146
+ const dispatchedModel = message.opencode_model;
9147
+ const separator = dispatchedModel?.indexOf("/") ?? -1;
9148
+ const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
9149
+ const providerId = hasProviderAndModel ? dispatchedModel.slice(0, separator) : null;
9150
+ const modelId = hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null;
9151
+ const errorMessage3 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
9152
+ this.log({
9153
+ level: "error",
9154
+ message: `Refusing message ${message.id.slice(0, 8)} in conversation ${conv.id.slice(0, 8)}: provider namespace ${providerId ?? "(unknown)"} is not loaded; no turn was started`,
9155
+ conversation_id: conv.id,
9156
+ message_id: message.id
9157
+ });
9158
+ try {
9159
+ await this.markFailed(conv.id, message.id, sessionId, errorMessage3, void 0, {
9160
+ classification: {
9161
+ kind: "model_auth",
9162
+ providerId,
9163
+ modelId,
9164
+ reason: "missing"
9165
+ },
9166
+ errorShape: { name: null, statusCode: null }
9167
+ });
9168
+ this.signalDispatchNotStarted(conv, message, "model_provider_absent");
9169
+ } catch (markErr) {
9170
+ this.log({
9171
+ level: "warn",
9172
+ message: `markFailed PATCH for refused message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
9173
+ conversation_id: conv.id,
9174
+ message_id: message.id
9175
+ });
9176
+ this.signalDispatchNotStarted(conv, message, "failure_unreported");
9177
+ }
9178
+ await this.repairProviderRegistry(conv.id, message.id);
9179
+ break;
9180
+ }
8457
9181
  let opencodeMessageId;
8458
9182
  let preDispatchUserIds = null;
8459
9183
  try {
@@ -8619,14 +9343,13 @@ var ChannelDriver = class _ChannelDriver {
8619
9343
  * skip-because-already-`dispatched`, the exact signature of a message stuck
8620
9344
  * acknowledged-but-never-worked.
8621
9345
  *
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.
9346
+ * The first detection starts the clock and emits a local `debug` entry. The
9347
+ * `warn` log and paired `dispatch_wedged` signal fire once the wedge has
9348
+ * lasted `wedgeWarningIntervalMs`, then at most once per interval, naming the
9349
+ * consecutive-tick count so the operator sees magnitude rather than repetition.
9350
+ * A watcher stall that the loop-liveness watchdog recovers is expected and is
9351
+ * already reported by its `watcher_recovered` signal, so it does not reach an
9352
+ * operator through this warning.
8630
9353
  *
8631
9354
  * Deliberately does NOT trigger a release: WI-1's `reconcileWatchers` runs
8632
9355
  * unconditionally on this same tick and is already recovering anything it
@@ -8638,36 +9361,41 @@ var ChannelDriver = class _ChannelDriver {
8638
9361
  const existing = this.wedgeWarnings.get(conv.id);
8639
9362
  const firstWedgedAt = existing?.firstWedgedAt ?? now;
8640
9363
  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
9364
+ const warnAnchorAt = existing?.warnAnchorAt ?? now;
9365
+ const dueForWarn = now - warnAnchorAt >= this.wedgeWarningIntervalMs;
9366
+ if (!existing) {
9367
+ this.log({
9368
+ level: "debug",
9369
+ 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.`,
9370
+ conversation_id: conv.id
9371
+ });
9372
+ }
9373
+ let untracked = 0;
9374
+ if (dueForWarn) {
9375
+ const stuckForMs = now - firstWedgedAt;
9376
+ untracked = this.countUntrackedIds(messages);
9377
+ this.log({
9378
+ level: "warn",
9379
+ 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.`),
9380
+ conversation_id: conv.id
8648
9381
  });
8649
- return;
8650
9382
  }
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
9383
  this.wedgeWarnings.delete(conv.id);
8659
- this.wedgeWarnings.set(conv.id, { firstWedgedAt, lastWarnedAt: now, consecutiveTicks });
9384
+ this.wedgeWarnings.set(conv.id, {
9385
+ firstWedgedAt,
9386
+ warnAnchorAt: dueForWarn ? now : warnAnchorAt,
9387
+ consecutiveTicks
9388
+ });
8660
9389
  while (this.wedgeWarnings.size > MAX_WEDGED_CONVERSATIONS) {
8661
9390
  const oldest = this.wedgeWarnings.keys().next().value;
8662
9391
  if (oldest === void 0) break;
8663
9392
  this.wedgeWarnings.delete(oldest);
8664
9393
  }
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
- }
9394
+ if (!dueForWarn) return;
9395
+ void this.postSignal(conv.id, messages[0].id, "dispatch_wedged", {
9396
+ stuck_for_ms: now - firstWedgedAt,
9397
+ untracked
9398
+ });
8671
9399
  }
8672
9400
  /**
8673
9401
  * How many of `messages`' ids are tracked by NO watcher's `inFlight` (#1618
@@ -8803,6 +9531,34 @@ var ChannelDriver = class _ChannelDriver {
8803
9531
  if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
8804
9532
  const ongoing = await this.isSessionOngoing(sessionId);
8805
9533
  if (ongoing === false) {
9534
+ const age = this.absoluteAgeForRow(message.processing_started_at);
9535
+ if (age.tripped && age.anchorMs !== null) {
9536
+ try {
9537
+ await this.markFailed(
9538
+ conv.id,
9539
+ message.id,
9540
+ sessionId,
9541
+ this.absoluteCeilingError(message.id, sessionId, age.anchorMs),
9542
+ void 0,
9543
+ {
9544
+ classification: null,
9545
+ errorShape: extractReplyErrorShape(findLastAssistantReplyFor(messages, ocId ?? ""))
9546
+ }
9547
+ );
9548
+ } catch (err) {
9549
+ if (err instanceof ChannelAuthError) throw err;
9550
+ this.log({
9551
+ level: "warn",
9552
+ 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)}`,
9553
+ conversation_id: conv.id,
9554
+ message_id: message.id
9555
+ });
9556
+ return "unresolved";
9557
+ }
9558
+ this.clearRedriveUnresolved(message.id);
9559
+ void this.postSignal(conv.id, message.id, "redrive_ceiling_refused");
9560
+ return "settled";
9561
+ }
8806
9562
  this.log({
8807
9563
  level: "info",
8808
9564
  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 +9629,29 @@ var ChannelDriver = class _ChannelDriver {
8873
9629
  message_id: message.id
8874
9630
  });
8875
9631
  }
9632
+ if (this.absoluteAgeForRow(message.processing_started_at).tripped) {
9633
+ try {
9634
+ await this.markFailed(
9635
+ conv.id,
9636
+ message.id,
9637
+ sessionId,
9638
+ this.absoluteCeilingError(message.id, sessionId, anchorMs)
9639
+ );
9640
+ } catch (err) {
9641
+ if (err instanceof ChannelAuthError) throw err;
9642
+ this.log({
9643
+ level: "warn",
9644
+ 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)}`,
9645
+ conversation_id: conv.id,
9646
+ message_id: message.id
9647
+ });
9648
+ const bound = await this.boundRedriveOutcome(conv, message, "reattach");
9649
+ return bound === "abandoned" ? "abandoned" : "unresolved";
9650
+ }
9651
+ this.clearRedriveUnresolved(message.id);
9652
+ void this.postSignal(conv.id, message.id, "redrive_ceiling_refused");
9653
+ return "abandoned";
9654
+ }
8876
9655
  const title = await this.resolveSessionTitle(sessionId, conv.id);
8877
9656
  try {
8878
9657
  await this.markProcessing(conv.id, message.id, sessionId, ocId, title);
@@ -9139,8 +9918,7 @@ var ChannelDriver = class _ChannelDriver {
9139
9918
  const since = this.redriveOutcomeFailingSince.get(message.id);
9140
9919
  if (since === void 0) this.redriveOutcomeFailingSince.set(message.id, now);
9141
9920
  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;
9921
+ const absoluteAgeTripped = this.absoluteAgeForRow(message.processing_started_at).tripped;
9144
9922
  if (!durationTripped && !absoluteAgeTripped) {
9145
9923
  this.signalRedriveOutcomeUnreported(conv, message, outcome);
9146
9924
  return "retry";
@@ -9179,6 +9957,15 @@ var ChannelDriver = class _ChannelDriver {
9179
9957
  });
9180
9958
  return "abandoned";
9181
9959
  }
9960
+ /** Row-level guards use the durable server stamp; in-flight guards use processingAnchorMs. */
9961
+ absoluteAgeForRow(stamp) {
9962
+ const anchorMs = stamp ? Date.parse(stamp) : NaN;
9963
+ return Number.isNaN(anchorMs) ? { tripped: false, anchorMs: null } : { tripped: this.now() - anchorMs >= ABSOLUTE_MAX_PROCESSING_MS, anchorMs };
9964
+ }
9965
+ absoluteCeilingError(messageId, sessionId, anchorMs, retainedReason = this.readRetainedSessionError(sessionId)) {
9966
+ 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.`;
9967
+ return retainedReason ? `${baseError} ${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}` : baseError;
9968
+ }
9182
9969
  /**
9183
9970
  * Record one poll outcome toward the re-drive fence's consecutive-identical-
9184
9971
  * failure streak (#1348) and return the resulting count. `signature === null`
@@ -9515,6 +10302,12 @@ var ChannelDriver = class _ChannelDriver {
9515
10302
  b2AbandonedWithoutAnswer: false,
9516
10303
  ambiguousPinnedSinceMs: 0,
9517
10304
  ambiguousResolved: false,
10305
+ midFlightPinnedSinceMs: 0,
10306
+ midFlightNotOngoingStreak: 0,
10307
+ midFlightLastStatusCheckMs: 0,
10308
+ midFlightOrphanSignalled: false,
10309
+ midFlightOrphanWithoutAnswer: false,
10310
+ midFlightHeldLogged: false,
9518
10311
  sessionStoreFailurePending: false,
9519
10312
  neverStartedResolved: false
9520
10313
  });
@@ -9620,10 +10413,7 @@ var ChannelDriver = class _ChannelDriver {
9620
10413
  opencodeMessageId,
9621
10414
  message,
9622
10415
  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,
10416
+ processingAnchorMs: this.absoluteAgeForRow(message.processing_started_at).anchorMs ?? processedAtMs,
9627
10417
  deadline: watchDeadlineAnchorMs + this.pausedMaxWaitMs,
9628
10418
  // The server row is ALREADY `processing`; do not re-fire markProcessing.
9629
10419
  started: true,
@@ -9657,6 +10447,12 @@ var ChannelDriver = class _ChannelDriver {
9657
10447
  b2AbandonedWithoutAnswer: false,
9658
10448
  ambiguousPinnedSinceMs: 0,
9659
10449
  ambiguousResolved: false,
10450
+ midFlightPinnedSinceMs: 0,
10451
+ midFlightNotOngoingStreak: 0,
10452
+ midFlightLastStatusCheckMs: 0,
10453
+ midFlightOrphanSignalled: false,
10454
+ midFlightOrphanWithoutAnswer: false,
10455
+ midFlightHeldLogged: false,
9660
10456
  sessionStoreFailurePending: false,
9661
10457
  neverStartedResolved: false
9662
10458
  });
@@ -9933,14 +10729,40 @@ var ChannelDriver = class _ChannelDriver {
9933
10729
  conversation_id: watcher.conv.id,
9934
10730
  message_id: inFlight.evidentMessageId
9935
10731
  });
9936
- await this.markFailed(
9937
- watcher.conv.id,
9938
- inFlight.evidentMessageId,
9939
- event.sessionId,
9940
- `OpenCode could not run this turn: ${event.reason}`
9941
- );
10732
+ const isMissingProvider = isProviderModelNotFoundError(event.reason);
10733
+ if (isMissingProvider) {
10734
+ const dispatchedModel = inFlight.message.opencode_model;
10735
+ const separator = dispatchedModel?.indexOf("/") ?? -1;
10736
+ const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
10737
+ await this.markFailed(
10738
+ watcher.conv.id,
10739
+ inFlight.evidentMessageId,
10740
+ event.sessionId,
10741
+ `OpenCode could not run this turn: ${event.reason}`,
10742
+ void 0,
10743
+ {
10744
+ classification: {
10745
+ kind: "model_auth",
10746
+ providerId: hasProviderAndModel ? dispatchedModel.slice(0, separator) : null,
10747
+ modelId: hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null,
10748
+ reason: "missing"
10749
+ },
10750
+ errorShape: { name: null, statusCode: null }
10751
+ }
10752
+ );
10753
+ } else {
10754
+ await this.markFailed(
10755
+ watcher.conv.id,
10756
+ inFlight.evidentMessageId,
10757
+ event.sessionId,
10758
+ `OpenCode could not run this turn: ${event.reason}`
10759
+ );
10760
+ }
9942
10761
  inFlight.done = true;
9943
10762
  this.removeInFlight(watcher, inFlight.evidentMessageId);
10763
+ if (isMissingProvider) {
10764
+ await this.repairProviderRegistry(watcher.conv.id, inFlight.evidentMessageId);
10765
+ }
9944
10766
  } catch (err) {
9945
10767
  if (err instanceof ChannelAuthError) {
9946
10768
  this.log({
@@ -9961,6 +10783,44 @@ var ChannelDriver = class _ChannelDriver {
9961
10783
  this.sessionErrorHandled.delete(inFlight.evidentMessageId);
9962
10784
  }
9963
10785
  }
10786
+ async repairProviderRegistry(conversationId, messageId) {
10787
+ if (this.isV2) {
10788
+ void this.postSignal(conversationId, messageId, "provider_registry_reload_unsupported");
10789
+ return;
10790
+ }
10791
+ const activeSessions = this.protectedSessionIds().size;
10792
+ if (activeSessions > 0 || this.draining) {
10793
+ this.pendingProviderRegistryRepair = { conversationId, messageId };
10794
+ void this.postSignal(conversationId, messageId, "provider_registry_repair_deferred", {
10795
+ active_sessions: activeSessions
10796
+ });
10797
+ this.log({
10798
+ level: "warn",
10799
+ message: `Provider registry repair deferred until a lifecycle-safe point; ${activeSessions} active session(s) protected`,
10800
+ conversation_id: conversationId,
10801
+ message_id: messageId
10802
+ });
10803
+ return;
10804
+ }
10805
+ await this.issueProviderRegistryReload(conversationId, messageId);
10806
+ }
10807
+ async issueProviderRegistryReload(conversationId, messageId) {
10808
+ this.pendingProviderRegistryRepair = null;
10809
+ const result = await disposeAndReloadProviderConfig(this.port, this.openCodeClient);
10810
+ if (result.outcome === "failed") {
10811
+ const failure = result.status === null ? `transport failure${result.detail ? `: ${result.detail}` : ""}` : `HTTP ${result.status}`;
10812
+ this.log({
10813
+ level: "warn",
10814
+ message: `Provider registry reload failed (${failure})`,
10815
+ conversation_id: conversationId,
10816
+ message_id: messageId
10817
+ });
10818
+ }
10819
+ const repairOutcome = result.outcome === "reloaded" ? "reloaded" : "reload_failed";
10820
+ void this.postSignal(conversationId, messageId, "provider_registry_reload_issued", {
10821
+ repair_outcome: repairOutcome
10822
+ });
10823
+ }
9964
10824
  /**
9965
10825
  * The per-session polling loop (WI-3). Once per tick it:
9966
10826
  * 1. polls `GET /session/:id/message` once and, per in-flight message,
@@ -10010,7 +10870,9 @@ var ChannelDriver = class _ChannelDriver {
10010
10870
  }
10011
10871
  const { openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk } = await this.pollInteractions(sessionId, watcher, messages);
10012
10872
  if (watcher.generation !== generation) return;
10873
+ const ceilingAttemptsThisTick = /* @__PURE__ */ new Set();
10013
10874
  for (const inFlight of [...watcher.inFlight.values()]) {
10875
+ if (ceilingAttemptsThisTick.has(inFlight.evidentMessageId)) continue;
10014
10876
  await this.serviceInFlightMessage(
10015
10877
  sessionId,
10016
10878
  watcher,
@@ -10019,7 +10881,8 @@ var ChannelDriver = class _ChannelDriver {
10019
10881
  openQuestions,
10020
10882
  openPermissions,
10021
10883
  questionsPolledOk,
10022
- permissionsPolledOk
10884
+ permissionsPolledOk,
10885
+ ceilingAttemptsThisTick
10023
10886
  );
10024
10887
  }
10025
10888
  }
@@ -10060,35 +10923,139 @@ var ChannelDriver = class _ChannelDriver {
10060
10923
  inFlight.deadline = this.now() + this.pausedMaxWaitMs;
10061
10924
  }
10062
10925
  }
10063
- /**
10064
- * Drive ONE in-flight message's lifecycle from the tick's message snapshot.
10065
- * Fires markProcessing on queued→running and markDone on done (each once),
10066
- * applies the idle-path re-dispatch guard, and removes the message from the
10067
- * in-flight set on completion or timeout.
10068
- */
10069
- async serviceInFlightMessage(sessionId, watcher, inFlight, messages, openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk) {
10070
- const conv = watcher.conv;
10071
- const state = messageRunState(messages, inFlight.opencodeMessageId);
10072
- const id = inFlight.evidentMessageId;
10073
- if (state === "running" || state === "done" || state === "failed") {
10074
- this.neverStartedAttempts.delete(id);
10075
- }
10076
- if (inFlight.sessionStoreFailurePending) {
10077
- await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
10078
- return;
10079
- }
10080
- if (inFlight.neverStartedResolved) {
10081
- if (state === "running" || state === "done" || state === "failed") {
10082
- inFlight.neverStartedResolved = false;
10083
- } else {
10084
- await this.settleMessageNeverStarted(
10926
+ async settleAbsoluteCeiling(sessionId, watcher, messages, ceilingAttemptsThisTick) {
10927
+ const entries = [...watcher.inFlight.values()];
10928
+ const pastCeiling = entries.filter(
10929
+ (entry) => this.now() - entry.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS
10930
+ );
10931
+ const healthySiblings = entries.length - pastCeiling.length;
10932
+ const landed = [];
10933
+ let allRecordsLanded = true;
10934
+ const retainedReason = this.readRetainedSessionError(sessionId);
10935
+ for (const entry of pastCeiling) {
10936
+ if (!watcher.inFlight.has(entry.evidentMessageId)) continue;
10937
+ ceilingAttemptsThisTick.add(entry.evidentMessageId);
10938
+ const error2 = this.absoluteCeilingError(
10939
+ entry.evidentMessageId,
10940
+ sessionId,
10941
+ entry.processingAnchorMs,
10942
+ retainedReason
10943
+ );
10944
+ this.log({
10945
+ level: "warn",
10946
+ message: error2,
10947
+ conversation_id: watcher.conv.id,
10948
+ message_id: entry.evidentMessageId
10949
+ });
10950
+ try {
10951
+ await this.markFailed(
10952
+ watcher.conv.id,
10953
+ entry.evidentMessageId,
10085
10954
  sessionId,
10086
- watcher,
10087
- inFlight,
10088
- this.neverStartedAttempts.get(id)?.count ?? MAX_NEVER_STARTED_ATTEMPTS
10955
+ error2,
10956
+ void 0,
10957
+ {
10958
+ classification: null,
10959
+ errorShape: extractReplyErrorShape(
10960
+ findLastAssistantReplyFor(messages, entry.opencodeMessageId)
10961
+ )
10962
+ }
10089
10963
  );
10090
- return;
10091
- }
10964
+ } catch (err) {
10965
+ if (err instanceof ChannelAuthError) throw err;
10966
+ allRecordsLanded = false;
10967
+ const watched_for_ms = this.now() - entry.processingAnchorMs;
10968
+ if (err instanceof ChannelTerminalError) {
10969
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10970
+ watched_for_ms,
10971
+ interrupt_outcome: "record_terminal"
10972
+ });
10973
+ this.removeInFlight(watcher, entry.evidentMessageId);
10974
+ continue;
10975
+ }
10976
+ if (this.now() >= entry.deadline) {
10977
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10978
+ watched_for_ms,
10979
+ interrupt_outcome: "record_deadline"
10980
+ });
10981
+ this.removeInFlight(watcher, entry.evidentMessageId);
10982
+ continue;
10983
+ }
10984
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
10985
+ watched_for_ms,
10986
+ interrupt_outcome: "record_retry"
10987
+ });
10988
+ this.log({
10989
+ level: "warn",
10990
+ 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)}`,
10991
+ conversation_id: watcher.conv.id,
10992
+ message_id: entry.evidentMessageId
10993
+ });
10994
+ continue;
10995
+ }
10996
+ entry.done = true;
10997
+ landed.push(entry);
10998
+ }
10999
+ if (!allRecordsLanded) {
11000
+ for (const entry of landed) this.removeInFlight(watcher, entry.evidentMessageId);
11001
+ return;
11002
+ }
11003
+ if (healthySiblings > 0) {
11004
+ for (const entry of landed) {
11005
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
11006
+ watched_for_ms: this.now() - entry.processingAnchorMs,
11007
+ interrupt_outcome: "deferred_for_healthy_sibling",
11008
+ healthy_siblings: healthySiblings
11009
+ });
11010
+ this.removeInFlight(watcher, entry.evidentMessageId);
11011
+ }
11012
+ return;
11013
+ }
11014
+ const interrupt = await interruptSession(this.port, sessionId, this.openCodeClient);
11015
+ for (const entry of landed) {
11016
+ void this.postSignal(watcher.conv.id, entry.evidentMessageId, "gave_up", {
11017
+ watched_for_ms: this.now() - entry.processingAnchorMs,
11018
+ interrupt_outcome: interrupt.kind
11019
+ });
11020
+ this.removeInFlight(watcher, entry.evidentMessageId);
11021
+ }
11022
+ if (interrupt.kind !== "confirmed_stopped") {
11023
+ this.log({
11024
+ level: "warn",
11025
+ 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`,
11026
+ conversation_id: watcher.conv.id
11027
+ });
11028
+ }
11029
+ }
11030
+ /**
11031
+ * Drive ONE in-flight message's lifecycle from the tick's message snapshot.
11032
+ * Fires markProcessing on queued→running and markDone on done (each once),
11033
+ * applies the idle-path re-dispatch guard, and removes the message from the
11034
+ * in-flight set on completion or timeout.
11035
+ */
11036
+ async serviceInFlightMessage(sessionId, watcher, inFlight, messages, openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk, ceilingAttemptsThisTick) {
11037
+ const conv = watcher.conv;
11038
+ const state = messageRunState(messages, inFlight.opencodeMessageId);
11039
+ const id = inFlight.evidentMessageId;
11040
+ if (state === "running" || state === "done" || state === "failed") {
11041
+ this.neverStartedAttempts.delete(id);
11042
+ }
11043
+ if (inFlight.sessionStoreFailurePending) {
11044
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
11045
+ return;
11046
+ }
11047
+ if (inFlight.neverStartedResolved) {
11048
+ if (state === "running" || state === "done" || state === "failed") {
11049
+ inFlight.neverStartedResolved = false;
11050
+ } else {
11051
+ await this.settleMessageNeverStarted(
11052
+ sessionId,
11053
+ watcher,
11054
+ inFlight,
11055
+ this.neverStartedAttempts.get(id)?.count ?? MAX_NEVER_STARTED_ATTEMPTS
11056
+ );
11057
+ return;
11058
+ }
10092
11059
  }
10093
11060
  if (messages && collectTaskCalls(messages, inFlight.opencodeMessageId).length > 0 && !this.subagentInvocationPrefetches.has(id)) {
10094
11061
  void this.resolveSubagentInvocations(
@@ -10152,7 +11119,7 @@ var ChannelDriver = class _ChannelDriver {
10152
11119
  if (!inFlight.done) {
10153
11120
  const messageErrorText = messageError(messages, inFlight.opencodeMessageId);
10154
11121
  const retainedReason = this.readRetainedSessionError(sessionId);
10155
- const retainedError = retainedReason === null ? null : `${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}`;
11122
+ const retainedError = retainedReason === null || messageErrorText !== null && messageErrorText.includes(retainedReason) ? null : `${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}`;
10156
11123
  const observedCause = this.sessionStoreHealth?.readCause(sessionId);
10157
11124
  const causeError = observedCause === null || observedCause === void 0 ? null : `${SESSION_ERROR_OBSERVED_CAUSE}${observedCause}`;
10158
11125
  const storeError = this.sessionStoreHealth?.isUnwritable() ? SESSION_STORE_UNWRITABLE_ERROR : null;
@@ -10257,7 +11224,11 @@ var ChannelDriver = class _ChannelDriver {
10257
11224
  watcher,
10258
11225
  inFlight,
10259
11226
  messages,
10260
- finish
11227
+ composeAbandonmentNote(
11228
+ finish,
11229
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
11230
+ "without-answer"
11231
+ )
10261
11232
  );
10262
11233
  } else {
10263
11234
  await this.settleMessageDone(
@@ -10316,7 +11287,11 @@ var ChannelDriver = class _ChannelDriver {
10316
11287
  watcher,
10317
11288
  inFlight,
10318
11289
  messages,
10319
- finish
11290
+ composeAbandonmentNote(
11291
+ finish,
11292
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
11293
+ "without-answer"
11294
+ )
10320
11295
  );
10321
11296
  return;
10322
11297
  }
@@ -10410,17 +11385,108 @@ var ChannelDriver = class _ChannelDriver {
10410
11385
  return;
10411
11386
  }
10412
11387
  }
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);
11388
+ const midFlightPinnedNow = activelyRunning && isMidFlightPinnedRunning(messages, inFlight.opencodeMessageId);
11389
+ if (!midFlightPinnedNow) {
11390
+ if (snapshotReadable) {
11391
+ inFlight.midFlightPinnedSinceMs = 0;
11392
+ inFlight.midFlightNotOngoingStreak = 0;
11393
+ inFlight.midFlightLastStatusCheckMs = 0;
11394
+ inFlight.midFlightOrphanSignalled = false;
11395
+ inFlight.midFlightOrphanWithoutAnswer = false;
11396
+ inFlight.midFlightHeldLogged = false;
11397
+ }
11398
+ } else {
11399
+ if (inFlight.midFlightOrphanSignalled) {
11400
+ const note = composeAbandonmentNote(
11401
+ void 0,
11402
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
11403
+ inFlight.midFlightOrphanWithoutAnswer ? "midflight-without-answer" : "midflight-with-answer"
11404
+ );
11405
+ if (inFlight.midFlightOrphanWithoutAnswer) {
11406
+ await this.settleMessageAbandonedWithoutAnswer(
11407
+ sessionId,
11408
+ watcher,
11409
+ inFlight,
11410
+ messages,
11411
+ note
11412
+ );
11413
+ } else {
11414
+ await this.settleMessageDone(sessionId, watcher, inFlight, messages, note);
11415
+ }
11416
+ return;
11417
+ }
11418
+ if (inFlight.midFlightPinnedSinceMs === 0) {
11419
+ inFlight.midFlightPinnedSinceMs = this.now();
11420
+ }
11421
+ const pinnedForMs = this.now() - inFlight.midFlightPinnedSinceMs;
11422
+ if (pinnedForMs >= MIDFLIGHT_ORPHAN_MIN_PINNED_MS && this.now() - inFlight.midFlightLastStatusCheckMs >= MIDFLIGHT_ORPHAN_RECHECK_MS) {
11423
+ inFlight.midFlightLastStatusCheckMs = this.now();
11424
+ const rootOngoing = await this.isSessionOngoing(sessionId);
11425
+ inFlight.midFlightNotOngoingStreak = rootOngoing === false ? inFlight.midFlightNotOngoingStreak + 1 : 0;
11426
+ if (isMidFlightOrphanConfirmed({
11427
+ pinnedForMs,
11428
+ minPinnedMs: MIDFLIGHT_ORPHAN_MIN_PINNED_MS,
11429
+ notOngoingStreak: inFlight.midFlightNotOngoingStreak,
11430
+ minNotOngoingStreak: MIDFLIGHT_ORPHAN_MIN_NOT_ONGOING_STREAK
11431
+ })) {
11432
+ if (this.sessionStoreHealth?.isUnwritable()) {
11433
+ inFlight.sessionStoreFailurePending = true;
11434
+ void this.postSignal(conv.id, id, "midflight_orphan_resolved", {
11435
+ watched_for_ms: pinnedForMs,
11436
+ ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
11437
+ opencode_message_id: inFlight.opencodeMessageId,
11438
+ run_state: "failed"
11439
+ });
11440
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
11441
+ return;
11442
+ }
11443
+ inFlight.midFlightOrphanSignalled = true;
11444
+ const noteOutcome = extractReplyTextForMessage(messages, inFlight.opencodeMessageId) === null ? "midflight-without-answer" : "midflight-with-answer";
11445
+ inFlight.midFlightOrphanWithoutAnswer = noteOutcome === "midflight-without-answer";
11446
+ void this.postSignal(conv.id, id, "midflight_orphan_resolved", {
11447
+ watched_for_ms: pinnedForMs,
11448
+ ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
11449
+ opencode_message_id: inFlight.opencodeMessageId,
11450
+ ...inFlight.midFlightOrphanWithoutAnswer ? { run_state: "failed" } : {}
11451
+ });
11452
+ const note = composeAbandonmentNote(
11453
+ void 0,
11454
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
11455
+ noteOutcome
11456
+ );
11457
+ this.log({
11458
+ level: "warn",
11459
+ message: `Message ${id.slice(0, 8)} was pinned mid-flight for ${Math.round(pinnedForMs / 1e3)}s with root_ongoing=${rootOngoing} \u2014 resolving the orphaned turn`,
11460
+ conversation_id: conv.id,
11461
+ message_id: id
11462
+ });
11463
+ if (inFlight.midFlightOrphanWithoutAnswer) {
11464
+ await this.settleMessageAbandonedWithoutAnswer(
11465
+ sessionId,
11466
+ watcher,
11467
+ inFlight,
11468
+ messages,
11469
+ note
11470
+ );
11471
+ } else {
11472
+ await this.settleMessageDone(sessionId, watcher, inFlight, messages, note);
11473
+ }
11474
+ return;
11475
+ }
11476
+ if (rootOngoing === true && !inFlight.midFlightHeldLogged) {
11477
+ inFlight.midFlightHeldLogged = true;
11478
+ const replyShape = describeAbandonedReplyShape(messages, inFlight.opencodeMessageId);
11479
+ this.log({
11480
+ level: "warn",
11481
+ message: `Message ${id.slice(0, 8)} was pinned mid-flight for ${Math.round(pinnedForMs / 1e3)}s with root_ongoing=true \u2014 holding; last tool ${replyShape?.lastToolName ?? "(unknown)"} status ${replyShape?.lastToolStatus ?? "(unknown)"}`,
11482
+ conversation_id: conv.id,
11483
+ message_id: id
11484
+ });
11485
+ }
11486
+ }
11487
+ }
11488
+ if (watcher.inFlight.has(inFlight.evidentMessageId) && activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
11489
+ await this.settleAbsoluteCeiling(sessionId, watcher, messages, ceilingAttemptsThisTick);
10424
11490
  return;
10425
11491
  }
10426
11492
  const heartbeatDue = activelyRunning && !inFlight.awaitingHumanLatched && !inFlight.aliveInFlight && this.now() - inFlight.lastAliveAt >= HEARTBEAT_MS;
@@ -10618,13 +11684,8 @@ var ChannelDriver = class _ChannelDriver {
10618
11684
  this.removeInFlight(watcher, inFlight.evidentMessageId);
10619
11685
  }
10620
11686
  /** Report a confirmed b2 abandonment that produced no answer text. */
10621
- async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, finish) {
11687
+ async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, error2) {
10622
11688
  const conv = watcher.conv;
10623
- const error2 = composeAbandonmentNote(
10624
- finish,
10625
- describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
10626
- "without-answer"
10627
- );
10628
11689
  this.anchorDeliveryDeadline(inFlight);
10629
11690
  this.log({
10630
11691
  level: "warn",
@@ -10648,7 +11709,8 @@ var ChannelDriver = class _ChannelDriver {
10648
11709
  usage,
10649
11710
  void 0,
10650
11711
  usageAgentName,
10651
- subagentInvocations
11712
+ subagentInvocations,
11713
+ true
10652
11714
  );
10653
11715
  } catch (err) {
10654
11716
  if (err instanceof ChannelAuthError) throw err;
@@ -11042,6 +12104,33 @@ var ChannelDriver = class _ChannelDriver {
11042
12104
  }
11043
12105
  const restartAborted = state === "failed" && sessionOngoing === false && isAbortedTerminalReply(messages, ocId ?? "");
11044
12106
  if (restartAborted) {
12107
+ const age = this.absoluteAgeForRow(row.processing_started_at);
12108
+ if (age.tripped && age.anchorMs !== null) {
12109
+ try {
12110
+ await this.markFailed(
12111
+ row.conversation_id,
12112
+ row.id,
12113
+ sessionId,
12114
+ this.absoluteCeilingError(row.id, sessionId, age.anchorMs),
12115
+ void 0,
12116
+ {
12117
+ classification: null,
12118
+ errorShape: extractReplyErrorShape(findLastAssistantReplyFor(messages, ocId ?? ""))
12119
+ }
12120
+ );
12121
+ } catch (err) {
12122
+ if (err instanceof ChannelAuthError) throw err;
12123
+ this.log({
12124
+ level: "warn",
12125
+ 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)}`,
12126
+ conversation_id: row.conversation_id,
12127
+ message_id: row.id
12128
+ });
12129
+ return;
12130
+ }
12131
+ void this.postSignal(row.conversation_id, row.id, "redrive_ceiling_refused");
12132
+ return;
12133
+ }
11045
12134
  this.log({
11046
12135
  level: "info",
11047
12136
  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 +12676,8 @@ var ChannelDriver = class _ChannelDriver {
11587
12676
  source_message_id: row.source_message_id,
11588
12677
  slack_user_id: row.slack_user_id,
11589
12678
  attachments: row.attachments ?? null,
11590
- opencode_message_id: row.opencode_message_id
12679
+ opencode_message_id: row.opencode_message_id,
12680
+ processing_started_at: row.processing_started_at
11591
12681
  };
11592
12682
  }
11593
12683
  /**
@@ -12494,7 +13584,7 @@ var ChannelDriver = class _ChannelDriver {
12494
13584
  * exists but is wedged, so the next attempt must get a fresh one
12495
13585
  * instead of reusing it — see WI-1's server-side null-clearing PATCH).
12496
13586
  */
12497
- async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
13587
+ async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations, abandonedWithoutAnswer) {
12498
13588
  const body = { status: "failed" };
12499
13589
  if (sessionId === null) {
12500
13590
  body.opencode_session_id = null;
@@ -12507,12 +13597,19 @@ var ChannelDriver = class _ChannelDriver {
12507
13597
  if (subagentInvocations && subagentInvocations.length > 0) {
12508
13598
  body.subagent_invocations = subagentInvocations;
12509
13599
  }
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;
13600
+ if (failure && failure.errorShape.name !== null) {
13601
+ body.failure_error_name = failure.errorShape.name;
13602
+ }
13603
+ if (failure && failure.errorShape.statusCode !== null) {
13604
+ body.failure_status_code = failure.errorShape.statusCode;
12515
13605
  }
13606
+ if (failure?.classification) {
13607
+ body.failure_kind = failure.classification.kind;
13608
+ body.failure_provider_id = failure.classification.providerId;
13609
+ body.failure_model_id = failure.classification.modelId;
13610
+ body.failure_reason = failure.classification.reason;
13611
+ }
13612
+ if (abandonedWithoutAnswer) body.abandoned_without_answer = true;
12516
13613
  try {
12517
13614
  await this.callWithRetry(
12518
13615
  "marking message as failed",
@@ -12532,27 +13629,28 @@ var ChannelDriver = class _ChannelDriver {
12532
13629
  this.clearSubagentInvocationCaches(messageId);
12533
13630
  }
12534
13631
  /**
12535
- * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
13632
+ * Classify an errored turn and extract its bounded error shape for `markFailed`.
12536
13633
  *
12537
13634
  * `messageFailure` alone (structured OpenCode error → `model_auth`) covers
12538
13635
  * most cases; when it returns `null` on this ALREADY-FAILED turn, fall back
12539
13636
  * to the P1-2b zero-provider check — one extra loopback call to
12540
13637
  * `hasAnyConfiguredProvider`, only reached when the structured classifier
12541
13638
  * 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.
13639
+ * (`null`/indeterminate) leaves the classification `null`; the raw error shape
13640
+ * is still retained for diagnostics.
12544
13641
  */
12545
13642
  async classifyModelAuthFailure(messages, userMessageId) {
12546
- const classified = messageFailure(messages, userMessageId);
12547
- if (classified != null) return classified;
12548
13643
  const reply = findLastAssistantReplyFor(messages, userMessageId);
12549
- const hasProvider = await this.hasAnyConfiguredProvider();
12550
- return applyZeroProviderFallback(
13644
+ const errorShape = extractReplyErrorShape(reply);
13645
+ const classified = messageFailure(messages, userMessageId);
13646
+ if (classified != null) return { classification: classified, errorShape };
13647
+ const classification = applyZeroProviderFallback(
12551
13648
  classified,
12552
- hasProvider,
13649
+ await this.hasAnyConfiguredProvider(),
12553
13650
  reply?.info?.providerID ?? null,
12554
13651
  reply?.info?.modelID ?? null
12555
13652
  );
13653
+ return { classification, errorShape };
12556
13654
  }
12557
13655
  async recordSubagentModelAuthFailure(failure, conversationId, messageId) {
12558
13656
  const providerId = failure.providerId ?? "(unknown)";
@@ -13099,7 +14197,7 @@ Port ${port} is already in use.`));
13099
14197
  }
13100
14198
 
13101
14199
  // src/lib/runner-credentials.ts
13102
- import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "node:fs";
14200
+ import { chmodSync as chmodSync2, writeFileSync as writeFileSync5 } from "node:fs";
13103
14201
  import { spawn as spawn5 } from "node:child_process";
13104
14202
  var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
13105
14203
  var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
@@ -13342,8 +14440,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
13342
14440
  }
13343
14441
  try {
13344
14442
  env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
13345
- writeFileSync4(GIT_CONFIG_GLOBAL, "");
13346
- writeFileSync4(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
14443
+ writeFileSync5(GIT_CONFIG_GLOBAL, "");
14444
+ writeFileSync5(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
13347
14445
  chmodSync2(GIT_CREDENTIAL_HELPER, 448);
13348
14446
  const config = [
13349
14447
  ["user.name", env.GIT_USER_NAME ?? "evident-bot"],
@@ -13376,7 +14474,23 @@ async function configureGitHubAccess({ env, log: log3 }) {
13376
14474
  // src/lib/opencode/config-overlay.ts
13377
14475
  import { execFileSync as execFileSync2 } from "node:child_process";
13378
14476
  import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
13379
- import { isAbsolute as isAbsolute3, join as join11, resolve as resolve4 } from "node:path";
14477
+ import { isAbsolute as isAbsolute3, join as join12, resolve as resolve4 } from "node:path";
14478
+ function parseConfigContent(content) {
14479
+ if (!content?.trim()) return {};
14480
+ const parsed = JSON.parse(content);
14481
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
14482
+ throw new Error("OPENCODE_CONFIG_CONTENT must contain a JSON object");
14483
+ }
14484
+ return parsed;
14485
+ }
14486
+ function mergeManagedOpenCodeConfigContent(content, managedProviders) {
14487
+ const config = parseConfigContent(content);
14488
+ const provider = typeof config.provider === "object" && config.provider !== null && !Array.isArray(config.provider) ? { ...config.provider } : {};
14489
+ for (const namespace of CREDENTIAL_PROXY_NATIVE_NAMESPACES) {
14490
+ delete provider[namespace];
14491
+ }
14492
+ return JSON.stringify({ ...config, provider: { ...provider, ...managedProviders } });
14493
+ }
13380
14494
  function isFile(filePath) {
13381
14495
  return existsSync2(filePath) && statSync6(filePath).isFile();
13382
14496
  }
@@ -13390,7 +14504,7 @@ function applyRunnerOpenCodeConfig({
13390
14504
  return;
13391
14505
  }
13392
14506
  const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
13393
- const target = isFile(join11(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
14507
+ const target = isFile(join12(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
13394
14508
  if (!isFile(source)) {
13395
14509
  log3(
13396
14510
  `RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
@@ -13398,7 +14512,7 @@ function applyRunnerOpenCodeConfig({
13398
14512
  );
13399
14513
  return;
13400
14514
  }
13401
- copyFileSync(source, join11(cwd, target));
14515
+ copyFileSync(source, join12(cwd, target));
13402
14516
  try {
13403
14517
  execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
13404
14518
  stdio: "ignore"
@@ -13407,11 +14521,164 @@ function applyRunnerOpenCodeConfig({
13407
14521
  const detail = error2 instanceof Error ? error2.message : String(error2);
13408
14522
  log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
13409
14523
  }
13410
- log3(`Applied runner OpenCode config ${source} to ${join11(cwd, target)}`);
14524
+ log3(`Applied runner OpenCode config ${source} to ${join12(cwd, target)}`);
14525
+ }
14526
+
14527
+ // src/lib/opencode/runtime-provenance.ts
14528
+ import { readFileSync as readFileSync8, readdirSync } from "node:fs";
14529
+ import { homedir as homedir7 } from "node:os";
14530
+ import { join as join13 } from "node:path";
14531
+ var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
14532
+ var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
14533
+ function errorKind(error2) {
14534
+ if (!(error2 instanceof Error)) return "unknown error";
14535
+ const code = "code" in error2 && typeof error2.code === "string" ? ` (${error2.code})` : "";
14536
+ return `${error2.name}${code}`;
14537
+ }
14538
+ function parseRegistryPluginSpec(value) {
14539
+ const separator = value.lastIndexOf("@");
14540
+ if (separator <= 0) return null;
14541
+ const name = value.slice(0, separator);
14542
+ const spec = value.slice(separator + 1);
14543
+ if (!PACKAGE_NAME.test(name) || !REGISTRY_SPEC.test(spec)) return null;
14544
+ return { name, spec };
14545
+ }
14546
+ function parseJsonc(value) {
14547
+ let output = "";
14548
+ let inString = false;
14549
+ let escaped = false;
14550
+ let lineComment = false;
14551
+ let blockComment = false;
14552
+ for (let index = 0; index < value.length; index++) {
14553
+ const character = value[index];
14554
+ const next = value[index + 1];
14555
+ if (lineComment) {
14556
+ if (character === "\n") {
14557
+ lineComment = false;
14558
+ output += character;
14559
+ }
14560
+ continue;
14561
+ }
14562
+ if (blockComment) {
14563
+ if (character === "*" && next === "/") {
14564
+ blockComment = false;
14565
+ index++;
14566
+ } else if (character === "\n") {
14567
+ output += character;
14568
+ }
14569
+ continue;
14570
+ }
14571
+ if (inString) {
14572
+ output += character;
14573
+ if (escaped) escaped = false;
14574
+ else if (character === "\\") escaped = true;
14575
+ else if (character === '"') inString = false;
14576
+ continue;
14577
+ }
14578
+ if (character === '"') {
14579
+ inString = true;
14580
+ output += character;
14581
+ } else if (character === "/" && next === "/") {
14582
+ lineComment = true;
14583
+ index++;
14584
+ } else if (character === "/" && next === "*") {
14585
+ blockComment = true;
14586
+ index++;
14587
+ } else if (character === ",") {
14588
+ let lookahead = index + 1;
14589
+ while (/\s/.test(value[lookahead] ?? "")) lookahead++;
14590
+ if (value[lookahead] !== "]" && value[lookahead] !== "}") output += character;
14591
+ } else {
14592
+ output += character;
14593
+ }
14594
+ }
14595
+ return JSON.parse(output);
14596
+ }
14597
+ function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
14598
+ return join13(env.XDG_CACHE_HOME ?? join13(homeDir, ".cache"), "opencode");
14599
+ }
14600
+ function readConfiguredPluginSpecs({
14601
+ cwd = process.cwd(),
14602
+ log: log3
14603
+ }) {
14604
+ let config;
14605
+ try {
14606
+ try {
14607
+ config = parseJsonc(readFileSync8(join13(cwd, "opencode.jsonc"), "utf8"));
14608
+ } catch (error2) {
14609
+ if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
14610
+ config = parseJsonc(readFileSync8(join13(cwd, "opencode.json"), "utf8"));
14611
+ }
14612
+ } catch (error2) {
14613
+ log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
14614
+ return [];
14615
+ }
14616
+ if (typeof config !== "object" || config === null || !("plugin" in config)) return [];
14617
+ const plugins = config.plugin;
14618
+ if (!Array.isArray(plugins)) {
14619
+ log3("Could not read OpenCode plugin configuration: plugin is not an array");
14620
+ return [];
14621
+ }
14622
+ const configured = plugins.filter(
14623
+ (plugin) => typeof plugin === "string" && parseRegistryPluginSpec(plugin) !== null
14624
+ );
14625
+ if (configured.length !== plugins.length) {
14626
+ log3(
14627
+ `Skipped ${plugins.length - configured.length} non-registry OpenCode plugin spec(s); their values were not recorded`
14628
+ );
14629
+ }
14630
+ return configured;
14631
+ }
14632
+ function resolveInstalledPluginVersions({
14633
+ cacheRoot,
14634
+ env = process.env,
14635
+ homeDir = homedir7(),
14636
+ log: log3
14637
+ }) {
14638
+ const packagesRoot = join13(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
14639
+ let directories;
14640
+ try {
14641
+ directories = readdirSync(packagesRoot, { withFileTypes: true });
14642
+ } catch (error2) {
14643
+ if ("code" in Object(error2) && Object(error2).code === "ENOENT") return [];
14644
+ log3(`Could not inspect the OpenCode plugin cache: ${errorKind(error2)}`);
14645
+ return [];
14646
+ }
14647
+ const plugins = [];
14648
+ let skipped = 0;
14649
+ for (const directory of directories) {
14650
+ if (!directory.isDirectory()) continue;
14651
+ const parsed = parseRegistryPluginSpec(directory.name);
14652
+ if (parsed === null) {
14653
+ skipped++;
14654
+ continue;
14655
+ }
14656
+ let resolvedVersion = null;
14657
+ try {
14658
+ const packageJson = JSON.parse(
14659
+ readFileSync8(
14660
+ join13(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
14661
+ "utf8"
14662
+ )
14663
+ );
14664
+ if (typeof packageJson.version === "string" && REGISTRY_SPEC.test(packageJson.version)) {
14665
+ resolvedVersion = packageJson.version;
14666
+ } else {
14667
+ log3(`Could not resolve OpenCode plugin ${parsed.name}@${parsed.spec}: invalid version`);
14668
+ }
14669
+ } catch (error2) {
14670
+ log3(`Could not resolve OpenCode plugin ${parsed.name}@${parsed.spec}: ${errorKind(error2)}`);
14671
+ }
14672
+ plugins.push({ ...parsed, resolvedVersion });
14673
+ }
14674
+ if (skipped > 0) {
14675
+ log3(`Skipped ${skipped} non-registry OpenCode package cache entry; its value was not recorded`);
14676
+ }
14677
+ return plugins;
13411
14678
  }
13412
14679
 
13413
14680
  // src/lib/credential-sync.ts
13414
- import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
14681
+ import { renameSync, writeFileSync as writeFileSync6 } from "node:fs";
13415
14682
  var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
13416
14683
  var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
13417
14684
  var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
@@ -13445,7 +14712,7 @@ function writeMarker(markerPath, outcomes, log3) {
13445
14712
  `;
13446
14713
  const temporaryPath = `${markerPath}.tmp`;
13447
14714
  try {
13448
- writeFileSync5(temporaryPath, body, { mode: 384 });
14715
+ writeFileSync6(temporaryPath, body, { mode: 384 });
13449
14716
  renameSync(temporaryPath, markerPath);
13450
14717
  } catch (error2) {
13451
14718
  log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
@@ -13645,10 +14912,261 @@ function createCredentialSync({
13645
14912
  };
13646
14913
  }
13647
14914
 
14915
+ // src/lib/managed-inference-setup.ts
14916
+ var defaultStore = {
14917
+ get: getManagedInferenceCredential,
14918
+ set: setManagedInferenceCredential,
14919
+ clear: clearManagedInferenceCredential
14920
+ };
14921
+ function endpoint(options, operation) {
14922
+ return `${options.apiUrl}/runners/${options.agentId}/managed-credentials/${operation}`;
14923
+ }
14924
+ function requestHeaders(options) {
14925
+ return { Authorization: options.getAuthHeader(), "Content-Type": "application/json" };
14926
+ }
14927
+ function request2(options) {
14928
+ return withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
14929
+ }
14930
+ function safeFailure(operation, status2) {
14931
+ return status2 === void 0 ? `${operation} could not reach the managed credential service` : `${operation} returned HTTP ${status2}`;
14932
+ }
14933
+ function logTransportFailure(options, operation, error2, authorization) {
14934
+ let detail = error2 instanceof Error ? error2.message : String(error2);
14935
+ const credential = authorization.split(/\s+/, 2)[1];
14936
+ for (const secret of [authorization, credential]) {
14937
+ if (secret) detail = detail.replaceAll(secret, "[redacted]");
14938
+ }
14939
+ options.log(
14940
+ `MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${redactDiagnosticText(detail)}`,
14941
+ "debug"
14942
+ );
14943
+ }
14944
+ async function responseJson(response) {
14945
+ try {
14946
+ const value = await response.json();
14947
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
14948
+ } catch (error2) {
14949
+ return null;
14950
+ }
14951
+ }
14952
+ function readSetup(value) {
14953
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
14954
+ const setup = value;
14955
+ if (typeof setup.runner_id !== "string" || typeof setup.generation !== "string" || typeof setup.status !== "string" || setup.token_id !== null && typeof setup.token_id !== "string" || typeof setup.acknowledged_at !== "string" && setup.acknowledged_at !== null) {
14956
+ return null;
14957
+ }
14958
+ return setup;
14959
+ }
14960
+ function clearLocalSetup(options) {
14961
+ const env = options.env ?? process.env;
14962
+ const store = options.store ?? defaultStore;
14963
+ const stored = store.get(options.agentId);
14964
+ store.clear(options.agentId);
14965
+ const token = env.EVIDENT_PROXY_TOKEN;
14966
+ if (token !== void 0 && token === stored?.token) {
14967
+ delete env.EVIDENT_PROXY_TOKEN;
14968
+ } else if (token !== void 0) {
14969
+ options.log(
14970
+ `MANAGED-INFERENCE-CLEANUP-PRESERVED-EXTERNAL-TOKEN: runner=${options.agentId}`,
14971
+ "debug"
14972
+ );
14973
+ }
14974
+ }
14975
+ async function discoverManagedInferenceSetup(options) {
14976
+ const headers = requestHeaders(options);
14977
+ let response;
14978
+ try {
14979
+ response = await request2(options)(endpoint(options, "current"), {
14980
+ headers
14981
+ });
14982
+ } catch (error2) {
14983
+ logTransportFailure(options, "discovery", error2, headers.Authorization);
14984
+ return { kind: "failed", reason: safeFailure("Managed setup discovery") };
14985
+ }
14986
+ if (response.status === 404) {
14987
+ clearLocalSetup(options);
14988
+ return { kind: "none" };
14989
+ }
14990
+ if (!response.ok) {
14991
+ return { kind: "failed", reason: safeFailure("Managed setup discovery", response.status) };
14992
+ }
14993
+ const body = await responseJson(response);
14994
+ const setup = readSetup(body?.setup);
14995
+ return setup ? { kind: "setup", setup } : { kind: "failed", reason: "Managed setup discovery returned an invalid response" };
14996
+ }
14997
+ async function enrollManagedInferenceSetup(options) {
14998
+ const response = await post(options, "enroll", {});
14999
+ if (!response) {
15000
+ return { kind: "failed", reason: safeFailure("Managed credential enrollment") };
15001
+ }
15002
+ if (response.status === 404) {
15003
+ return { kind: "none" };
15004
+ }
15005
+ const body = await responseJson(response);
15006
+ if (!response.ok) {
15007
+ const serverError = body?.error;
15008
+ return {
15009
+ kind: "failed",
15010
+ reason: typeof serverError === "string" && serverError.trim() ? redactDiagnosticText(serverError).slice(0, 500) : safeFailure("Managed credential enrollment", response.status)
15011
+ };
15012
+ }
15013
+ const setup = readSetup(body?.setup);
15014
+ return setup ? { kind: "setup", setup } : { kind: "failed", reason: "Managed credential enrollment returned an invalid response" };
15015
+ }
15016
+ async function post(options, operation, body) {
15017
+ const headers = requestHeaders(options);
15018
+ try {
15019
+ return await request2(options)(endpoint(options, operation), {
15020
+ method: "POST",
15021
+ headers,
15022
+ body: JSON.stringify(body)
15023
+ });
15024
+ } catch (error2) {
15025
+ logTransportFailure(options, operation, error2, headers.Authorization);
15026
+ return null;
15027
+ }
15028
+ }
15029
+ function applyCredential(options, credential) {
15030
+ const env = options.env ?? process.env;
15031
+ let merged;
15032
+ if (options.autoConfigureCredentialsProxy) {
15033
+ const { config, unsupportedProviders } = buildCredentialProxyNativeConfig({
15034
+ inferenceBaseUrl: credential.inferenceBaseUrl,
15035
+ modelDescriptors: credential.modelDescriptors
15036
+ });
15037
+ if (unsupportedProviders.length > 0 || Object.keys(config.provider).length === 0) {
15038
+ return "Managed setup contains unsupported provider descriptors";
15039
+ }
15040
+ try {
15041
+ merged = mergeManagedOpenCodeConfigContent(env.OPENCODE_CONFIG_CONTENT, config.provider);
15042
+ } catch (error2) {
15043
+ return "OPENCODE_CONFIG_CONTENT is malformed; managed providers were not installed";
15044
+ }
15045
+ }
15046
+ try {
15047
+ (options.store ?? defaultStore).set(options.agentId, credential);
15048
+ } catch (error2) {
15049
+ const detail = (error2 instanceof Error ? error2.message : String(error2)).replaceAll(
15050
+ credential.token,
15051
+ "[redacted]"
15052
+ );
15053
+ options.log(
15054
+ `MANAGED-INFERENCE-STORE-FAILED: runner=${options.agentId} generation=${credential.generation} detail=${redactDiagnosticText(detail)}`,
15055
+ "warn"
15056
+ );
15057
+ return "Managed credentials could not be written to the protected store";
15058
+ }
15059
+ env.EVIDENT_PROXY_TOKEN = credential.token;
15060
+ if (merged !== void 0) env.OPENCODE_CONFIG_CONTENT = merged;
15061
+ return null;
15062
+ }
15063
+ async function reportManagedInferenceApplyFailure(options, failure) {
15064
+ const response = await post(options, "report-failure", {
15065
+ generation: failure.generation,
15066
+ token_id: failure.tokenId
15067
+ });
15068
+ if (!response?.ok) {
15069
+ options.log(
15070
+ `MANAGED-INFERENCE-REPORT-FAILED: runner=${options.agentId} generation=${failure.generation}${response ? ` status=${response.status}` : ""}`,
15071
+ "warn"
15072
+ );
15073
+ }
15074
+ }
15075
+ async function reaffirmStored(options, credential) {
15076
+ const response = await post(options, "reaffirm", {
15077
+ generation: credential.generation,
15078
+ token_id: credential.tokenId
15079
+ });
15080
+ if (!response?.ok) {
15081
+ return {
15082
+ kind: "failed",
15083
+ 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")
15084
+ };
15085
+ }
15086
+ const body = await responseJson(response);
15087
+ const setup = readSetup(body?.setup);
15088
+ if (!setup || setup.generation !== credential.generation || setup.token_id !== credential.tokenId) {
15089
+ return { kind: "failed", reason: "Managed credential reaffirmation returned a mismatch" };
15090
+ }
15091
+ const failure = applyCredential(options, credential);
15092
+ return failure ? { kind: "failed", reason: failure } : { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
15093
+ }
15094
+ async function prepareManagedInferenceSetup(options, discovery, health) {
15095
+ if (health.authFailed) {
15096
+ return {
15097
+ kind: "failed",
15098
+ reason: "OpenCode authentication failed; managed credentials were not rotated"
15099
+ };
15100
+ }
15101
+ const stored = (options.store ?? defaultStore).get(options.agentId);
15102
+ if (health.healthy) {
15103
+ if (!stored) {
15104
+ return {
15105
+ kind: "failed",
15106
+ reason: "Managed credentials are missing for the serving OpenCode process; restart required"
15107
+ };
15108
+ }
15109
+ return reaffirmStored(options, stored);
15110
+ }
15111
+ if (discovery.setup.acknowledged_at !== null && stored?.generation === discovery.setup.generation && stored.tokenId === discovery.setup.token_id) {
15112
+ return reaffirmStored(options, stored);
15113
+ }
15114
+ const response = await post(options, "bootstrap", { generation: discovery.setup.generation });
15115
+ if (!response?.ok) {
15116
+ return {
15117
+ kind: "failed",
15118
+ reason: response ? safeFailure("Managed credential bootstrap", response.status) : safeFailure("Managed credential bootstrap")
15119
+ };
15120
+ }
15121
+ const body = await responseJson(response);
15122
+ const setup = readSetup(body?.setup);
15123
+ const token = body?.inference_token;
15124
+ const inferenceBaseUrl = body?.inference_base_url;
15125
+ const modelDescriptors = body?.model_descriptors;
15126
+ if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || !Array.isArray(modelDescriptors) || !modelDescriptors.every(
15127
+ (descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
15128
+ )) {
15129
+ return { kind: "failed", reason: "Managed credential bootstrap returned an invalid response" };
15130
+ }
15131
+ const credential = {
15132
+ generation: setup.generation,
15133
+ tokenId: setup.token_id,
15134
+ token,
15135
+ inferenceBaseUrl,
15136
+ modelDescriptors
15137
+ };
15138
+ const failure = applyCredential(options, credential);
15139
+ if (failure) {
15140
+ await reportManagedInferenceApplyFailure(options, credential);
15141
+ return { kind: "failed", reason: failure };
15142
+ }
15143
+ return { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
15144
+ }
15145
+ async function acknowledgeManagedInferenceSetup(options, preparation) {
15146
+ const response = await post(options, "acknowledge", {
15147
+ generation: preparation.generation,
15148
+ token_id: preparation.tokenId,
15149
+ config_validated: options.autoConfigureCredentialsProxy === true
15150
+ });
15151
+ if (!response?.ok) {
15152
+ throw new Error(
15153
+ response ? safeFailure("Managed credential acknowledgment", response.status) : safeFailure("Managed credential acknowledgment")
15154
+ );
15155
+ }
15156
+ const body = await responseJson(response);
15157
+ const setup = readSetup(body?.setup);
15158
+ if (!setup || setup.generation !== preparation.generation || setup.token_id !== preparation.tokenId) {
15159
+ throw new Error("Managed credential acknowledgment returned a mismatch");
15160
+ }
15161
+ return response.status === 200 && setup.status === "acknowledged" ? "acknowledged" : "pending";
15162
+ }
15163
+
13648
15164
  // src/commands/run.ts
13649
15165
  var MAX_ACTIVITY_LOG_ENTRIES = 10;
13650
15166
  var MAX_DISCONNECT_REASON_LENGTH = 200;
13651
15167
  var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
15168
+ var CREDENTIAL_ADOPTION_PROBE_ATTEMPTS = 3;
15169
+ var CREDENTIAL_ADOPTION_PROBE_INTERVAL_MS = 1e3;
13652
15170
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
13653
15171
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
13654
15172
  var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
@@ -13676,33 +15194,49 @@ function resolveLogLevel(options) {
13676
15194
  }
13677
15195
  return "info";
13678
15196
  }
13679
- function resolveFileSyncDirectories(raw, homeDir) {
13680
- const directories = [];
15197
+ function resolveFileSyncEntry(entry, homeDir, flagName, labels) {
15198
+ const trimmed = entry.trim();
15199
+ if (trimmed === "") {
15200
+ throw new Error(`${flagName} requires a ${labels.noun} path (got an empty value)`);
15201
+ }
15202
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join14(homeDir, trimmed.slice(2)) : trimmed;
15203
+ if (!isAbsolute4(expanded)) {
15204
+ throw new Error(`${flagName} requires an absolute ${labels.noun} path; got "${entry}"`);
15205
+ }
15206
+ const normalized = resolvePath2(expanded);
15207
+ if (parse(normalized).root === normalized) {
15208
+ throw new Error(
15209
+ `${flagName} will not allow-list the filesystem root ("${entry}"); name the specific ${labels.noun} the credentials belong in (for example ${labels.rootExample})`
15210
+ );
15211
+ }
15212
+ return normalized;
15213
+ }
15214
+ function resolveFileSyncAllowList(raw, homeDir, flagName, labels) {
15215
+ const paths = [];
13681
15216
  for (const entry of raw ?? []) {
13682
- const trimmed = entry.trim();
13683
- if (trimmed === "") {
13684
- throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
13685
- }
13686
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join12(homeDir, trimmed.slice(2)) : trimmed;
13687
- if (!isAbsolute4(expanded)) {
13688
- throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
13689
- }
13690
- const normalized = resolvePath2(expanded);
13691
- if (parse(normalized).root === normalized) {
13692
- throw new Error(
13693
- `--enable-file-sync-to will not allow-list the filesystem root ("${entry}"); name the specific directory the credentials belong in (for example ~/.claude)`
13694
- );
13695
- }
13696
- if (!directories.includes(normalized)) {
13697
- directories.push(normalized);
13698
- }
15217
+ const normalized = resolveFileSyncEntry(entry, homeDir, flagName, labels);
15218
+ if (!paths.includes(normalized)) paths.push(normalized);
13699
15219
  }
13700
- if (directories.length > MAX_FILE_SYNC_DIRECTORIES) {
15220
+ if (paths.length > MAX_FILE_SYNC_DIRECTORIES) {
13701
15221
  throw new Error(
13702
- `--enable-file-sync-to accepts at most ${MAX_FILE_SYNC_DIRECTORIES} directories; got ${directories.length}`
15222
+ `${flagName} accepts at most ${MAX_FILE_SYNC_DIRECTORIES} ${labels.plural}; got ${paths.length}`
13703
15223
  );
13704
15224
  }
13705
- return directories;
15225
+ return paths;
15226
+ }
15227
+ function resolveFileSyncDirectories(raw, homeDir) {
15228
+ return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to", {
15229
+ noun: "directory",
15230
+ plural: "directories",
15231
+ rootExample: "~/.claude"
15232
+ });
15233
+ }
15234
+ function resolveFileSyncExactFiles(raw, homeDir) {
15235
+ return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to-exact-file", {
15236
+ noun: "file",
15237
+ plural: "files",
15238
+ rootExample: "~/.local/share/opencode/mcp-auth.json"
15239
+ });
13706
15240
  }
13707
15241
  var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
13708
15242
  var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
@@ -13838,7 +15372,7 @@ function logActivity(state, entry) {
13838
15372
  }
13839
15373
  function reportSessionDbRecovery(state) {
13840
15374
  try {
13841
- const report = drainSessionDbRecoveryReport({ homeDir: homedir7(), env: process.env });
15375
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir8(), env: process.env });
13842
15376
  for (const record of report.records) {
13843
15377
  const activity = buildSessionDbRecoveryActivity(record);
13844
15378
  if (!activity) throw new Error("could not map session-DB recovery record");
@@ -13919,7 +15453,7 @@ async function importOpenCodeV2CredentialsForState(state) {
13919
15453
  }
13920
15454
  const result = await importOpenCodeV2Credentials({
13921
15455
  request: client.request,
13922
- homeDir: homedir7(),
15456
+ homeDir: homedir8(),
13923
15457
  directory,
13924
15458
  guard: state.opencodeV2CredentialImportGuard,
13925
15459
  log: (message, level = "info") => logActivity(state, { type: "info", level, message })
@@ -13996,6 +15530,125 @@ async function handleAuthError(state, error2) {
13996
15530
  return { success: false };
13997
15531
  }
13998
15532
  }
15533
+ function logCredentialAdoptionActivity(state, activity, level) {
15534
+ const message = (() => {
15535
+ switch (activity.outcome) {
15536
+ case "deferred":
15537
+ return `CREDENTIAL-ADOPTION: deferred generation ${activity.generation} with ${activity.active_sessions} active session(s)`;
15538
+ case "reloaded_ready":
15539
+ return `CREDENTIAL-ADOPTION: adopted generation ${activity.generation} with ${activity.active_sessions} active session(s); readiness probe succeeded after ${activity.probe_attempts} attempt(s)`;
15540
+ case "reloaded_interrupted": {
15541
+ 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`;
15542
+ return `CREDENTIAL-ADOPTION: generation ${activity.generation} with ${activity.active_sessions} active session(s); replacement instance answered ${response} after ${activity.probe_attempts} probe attempt(s)`;
15543
+ }
15544
+ case "reload_failed":
15545
+ return `CREDENTIAL-ADOPTION: failed to adopt generation ${activity.generation} with ${activity.active_sessions} active session(s)`;
15546
+ case "skipped_v2":
15547
+ return `CREDENTIAL-ADOPTION: skipped generation ${activity.generation} with ${activity.active_sessions} active session(s) on OpenCode V2`;
15548
+ }
15549
+ })();
15550
+ logActivity(state, {
15551
+ type: level === "error" ? "error" : "info",
15552
+ level,
15553
+ ...level === "error" ? { error: message } : { message },
15554
+ metadata: {
15555
+ ...activity,
15556
+ opencode_version: state.opencodeVersion,
15557
+ port: state.port
15558
+ }
15559
+ });
15560
+ }
15561
+ async function adoptCredentialGeneration(state, generation, adoption, activeSessions, deferredForMs) {
15562
+ const reload = await disposeAndReloadProviderConfig(
15563
+ state.port,
15564
+ state.opencodeClient ?? void 0
15565
+ );
15566
+ if (reload.outcome === "failed") {
15567
+ logCredentialAdoptionActivity(
15568
+ state,
15569
+ {
15570
+ outcome: "reload_failed",
15571
+ generation,
15572
+ active_sessions: activeSessions,
15573
+ adoption,
15574
+ deferred_for_ms: deferredForMs,
15575
+ reload_status: reload.status,
15576
+ reload_detail: reload.detail,
15577
+ probe_attempts: null,
15578
+ probe_status: null,
15579
+ probe_body_empty: null,
15580
+ probe_detail: null
15581
+ },
15582
+ "error"
15583
+ );
15584
+ return;
15585
+ }
15586
+ if (reload.outcome === "skipped_v2") {
15587
+ logCredentialAdoptionActivity(
15588
+ state,
15589
+ {
15590
+ outcome: "skipped_v2",
15591
+ generation,
15592
+ active_sessions: activeSessions,
15593
+ adoption,
15594
+ deferred_for_ms: deferredForMs,
15595
+ reload_status: null,
15596
+ reload_detail: null,
15597
+ probe_attempts: null,
15598
+ probe_status: null,
15599
+ probe_body_empty: null,
15600
+ probe_detail: null
15601
+ },
15602
+ "debug"
15603
+ );
15604
+ return;
15605
+ }
15606
+ let lastProbe = null;
15607
+ let attempts = 0;
15608
+ for (attempts = 1; attempts <= CREDENTIAL_ADOPTION_PROBE_ATTEMPTS; attempts++) {
15609
+ lastProbe = await probeOpenCodeRequestReadiness(state.port, state.opencodeClient ?? void 0);
15610
+ if (lastProbe.ok) {
15611
+ logCredentialAdoptionActivity(
15612
+ state,
15613
+ {
15614
+ outcome: "reloaded_ready",
15615
+ generation,
15616
+ active_sessions: activeSessions,
15617
+ adoption,
15618
+ deferred_for_ms: deferredForMs,
15619
+ reload_status: null,
15620
+ reload_detail: null,
15621
+ probe_attempts: attempts,
15622
+ probe_status: lastProbe.status,
15623
+ probe_body_empty: lastProbe.bodyEmpty,
15624
+ probe_detail: lastProbe.detail
15625
+ },
15626
+ "warn"
15627
+ );
15628
+ return;
15629
+ }
15630
+ if (attempts < CREDENTIAL_ADOPTION_PROBE_ATTEMPTS) {
15631
+ await new Promise((resolve5) => setTimeout(resolve5, CREDENTIAL_ADOPTION_PROBE_INTERVAL_MS));
15632
+ }
15633
+ }
15634
+ logCredentialAdoptionActivity(
15635
+ state,
15636
+ {
15637
+ outcome: "reloaded_interrupted",
15638
+ generation,
15639
+ active_sessions: activeSessions,
15640
+ adoption,
15641
+ deferred_for_ms: deferredForMs,
15642
+ reload_status: null,
15643
+ reload_detail: null,
15644
+ probe_attempts: CREDENTIAL_ADOPTION_PROBE_ATTEMPTS,
15645
+ probe_status: lastProbe?.status ?? null,
15646
+ probe_body_empty: lastProbe?.bodyEmpty ?? null,
15647
+ probe_detail: lastProbe?.detail ?? null
15648
+ },
15649
+ "error"
15650
+ );
15651
+ }
13999
15652
  async function driveChannels(state, driver) {
14000
15653
  let idlePolls = 0;
14001
15654
  let idleMs = 0;
@@ -14005,6 +15658,15 @@ async function driveChannels(state, driver) {
14005
15658
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
14006
15659
  let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
14007
15660
  let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
15661
+ const credentialAdoption = {
15662
+ generation: 0,
15663
+ pendingGeneration: null,
15664
+ pendingSinceMs: null,
15665
+ deferredGeneration: null,
15666
+ adoption: 0
15667
+ };
15668
+ const stayAwakeHolds = () => state.stayAwakeUntilMs !== null && Date.now() < state.stayAwakeUntilMs;
15669
+ let stayAwakeSuppressionLogged = false;
14008
15670
  while (state.running) {
14009
15671
  const cycleStartedAtMs = performance.now();
14010
15672
  let idleThisCycle = false;
@@ -14046,12 +15708,30 @@ async function driveChannels(state, driver) {
14046
15708
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
14047
15709
  if (opencodeAuthApplied) state.openaiUsageRearm?.();
14048
15710
  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
- );
15711
+ credentialAdoption.generation++;
15712
+ if (state.opencodeApiVersion === "v2") {
15713
+ logCredentialAdoptionActivity(
15714
+ state,
15715
+ {
15716
+ outcome: "skipped_v2",
15717
+ generation: credentialAdoption.generation,
15718
+ active_sessions: driver.protectedSessionIds().size,
15719
+ adoption: null,
15720
+ deferred_for_ms: null,
15721
+ reload_status: null,
15722
+ reload_detail: null,
15723
+ probe_attempts: null,
15724
+ probe_status: null,
15725
+ probe_body_empty: null,
15726
+ probe_detail: null
15727
+ },
15728
+ "debug"
15729
+ );
15730
+ } else {
15731
+ credentialAdoption.pendingGeneration = credentialAdoption.generation;
15732
+ credentialAdoption.pendingSinceMs = Date.now();
15733
+ credentialAdoption.deferredGeneration = null;
15734
+ }
14055
15735
  }
14056
15736
  if (opencodeAuthApplied && state.opencodeApiVersion === "v2") {
14057
15737
  void importOpenCodeV2CredentialsForState(state).catch(
@@ -14106,6 +15786,49 @@ async function driveChannels(state, driver) {
14106
15786
  }
14107
15787
  }
14108
15788
  }
15789
+ if (credentialAdoption.pendingGeneration !== null) {
15790
+ const activeSessions = driver.protectedSessionIds();
15791
+ const generation = credentialAdoption.pendingGeneration;
15792
+ if (activeSessions.size > 0) {
15793
+ if (credentialAdoption.deferredGeneration !== generation) {
15794
+ credentialAdoption.deferredGeneration = generation;
15795
+ logCredentialAdoptionActivity(
15796
+ state,
15797
+ {
15798
+ outcome: "deferred",
15799
+ generation,
15800
+ active_sessions: activeSessions.size,
15801
+ adoption: null,
15802
+ deferred_for_ms: null,
15803
+ reload_status: null,
15804
+ reload_detail: null,
15805
+ probe_attempts: null,
15806
+ probe_status: null,
15807
+ probe_body_empty: null,
15808
+ probe_detail: null
15809
+ },
15810
+ "warn"
15811
+ );
15812
+ }
15813
+ } else {
15814
+ const deferredForMs = credentialAdoption.deferredGeneration === generation && credentialAdoption.pendingSinceMs !== null ? Math.max(0, Date.now() - credentialAdoption.pendingSinceMs) : null;
15815
+ credentialAdoption.pendingGeneration = null;
15816
+ credentialAdoption.pendingSinceMs = null;
15817
+ credentialAdoption.adoption++;
15818
+ void adoptCredentialGeneration(
15819
+ state,
15820
+ generation,
15821
+ credentialAdoption.adoption,
15822
+ activeSessions.size,
15823
+ deferredForMs
15824
+ ).catch(
15825
+ (error2) => logActivity(state, {
15826
+ type: "error",
15827
+ 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)}`
15828
+ })
15829
+ );
15830
+ }
15831
+ }
14109
15832
  await new Promise((resolve5) => setTimeout(resolve5, CHANNEL_POLL_INTERVAL_MS));
14110
15833
  const cycleMs = performance.now() - cycleStartedAtMs;
14111
15834
  if (idleThisCycle) idleMs += cycleMs;
@@ -14119,11 +15842,20 @@ async function driveChannels(state, driver) {
14119
15842
  if (state.interactive) displayStatus(state);
14120
15843
  break;
14121
15844
  }
14122
- if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3) {
15845
+ if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3 && !stayAwakeHolds()) {
14123
15846
  logActivity(state, { type: "info", message: "Idle timeout reached" });
14124
15847
  if (state.interactive) displayStatus(state);
14125
15848
  break;
14126
15849
  }
15850
+ if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3 && stayAwakeHolds()) {
15851
+ if (!stayAwakeSuppressionLogged) {
15852
+ stayAwakeSuppressionLogged = true;
15853
+ logActivity(state, {
15854
+ type: "info",
15855
+ message: "Idle, but staying awake until the requested minimum passes"
15856
+ });
15857
+ }
15858
+ }
14127
15859
  }
14128
15860
  }
14129
15861
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
@@ -14141,7 +15873,7 @@ function shouldWarnForReclaimSkip(reason) {
14141
15873
  return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
14142
15874
  }
14143
15875
  function sessionDbPath() {
14144
- return join12(homedir7(), ".local", "share", "opencode", "opencode.db");
15876
+ return join14(homedir8(), ".local", "share", "opencode", "opencode.db");
14145
15877
  }
14146
15878
  function logSessionCleanupActivity(state, phase, entry) {
14147
15879
  logActivity(state, {
@@ -14150,6 +15882,47 @@ function logSessionCleanupActivity(state, phase, entry) {
14150
15882
  metadata: { ...entry.metadata, phase }
14151
15883
  });
14152
15884
  }
15885
+ async function drainSessionDeletionReports(state) {
15886
+ const path = sessionDeletionReportPath(homedir8(), process.env);
15887
+ let delivered = 0;
15888
+ let failure = null;
15889
+ try {
15890
+ const spool = readSessionDeletionRecords(path);
15891
+ if (spool.readError !== null) {
15892
+ failure = `could not read spool: ${spool.readError}`;
15893
+ } else if (spool.records.length === 0) {
15894
+ return;
15895
+ } else if (!state.agentId || !state.authHeader) {
15896
+ failure = "runner authentication is not available";
15897
+ } else {
15898
+ for (let offset = 0; offset < spool.records.length; offset += 200) {
15899
+ const batch = spool.records.slice(offset, offset + 200);
15900
+ const result = await reportSessionDeletions(state.agentId, state.authHeader, batch);
15901
+ if (!result.ok) {
15902
+ failure = result.error ?? "unknown API error";
15903
+ break;
15904
+ }
15905
+ removeDeliveredSessionDeletionRecords(path, batch);
15906
+ delivered += batch.length;
15907
+ }
15908
+ }
15909
+ } catch (error2) {
15910
+ failure = error2 instanceof Error ? error2.message : String(error2);
15911
+ }
15912
+ const pending = readSessionDeletionRecords(path);
15913
+ const pendingCount = pending.readError === null ? pending.records.length : null;
15914
+ const message = `Session deletion reports: delivered ${delivered}, ${pendingCount === null ? "pending count unavailable" : `${pendingCount} pending`}` + (failure === null ? "." : `; delivery failed: ${failure}.`);
15915
+ logActivity(state, {
15916
+ type: "info",
15917
+ level: failure === null ? "info" : "warn",
15918
+ message,
15919
+ metadata: {
15920
+ outcome: failure === null ? "session-deletion-reports-drained" : "session-deletion-reports-pending",
15921
+ delivered,
15922
+ pending: pendingCount
15923
+ }
15924
+ });
15925
+ }
14153
15926
  function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
14154
15927
  const record = {
14155
15928
  v: 1,
@@ -14207,17 +15980,22 @@ async function runSweep(state, driver, config) {
14207
15980
  let failedThrew = 0;
14208
15981
  let skippedNewlyActive = 0;
14209
15982
  let remaining = 0;
15983
+ const deletionRecords = [];
14210
15984
  let protectedNow = driver.protectedSessionIds();
14211
15985
  if (sessions !== null) {
14212
- const toDelete = selectSessionsToDelete(
14213
- sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
14214
- {
14215
- maxAgeMs: config.maxAgeMs,
14216
- maxCount: config.maxCount,
14217
- nowMs: Date.now(),
14218
- protectedIds: protectedNow
14219
- }
15986
+ const snapshots = sessions.map((session) => ({
15987
+ id: session.id,
15988
+ lastActivityMs: sessionLastActivityMs(session)
15989
+ }));
15990
+ const lastActivityById = new Map(
15991
+ snapshots.map(({ id, lastActivityMs }) => [id, lastActivityMs])
14220
15992
  );
15993
+ const toDelete = selectSessionsToDelete(snapshots, {
15994
+ maxAgeMs: config.maxAgeMs,
15995
+ maxCount: config.maxCount,
15996
+ nowMs: Date.now(),
15997
+ protectedIds: protectedNow
15998
+ });
14221
15999
  const eligibleToDelete = toDelete.filter(
14222
16000
  (id) => (state.sessionDeleteFailures.get(id)?.isolatedFailures ?? 0) < SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES
14223
16001
  );
@@ -14237,6 +16015,17 @@ async function runSweep(state, driver, config) {
14237
16015
  const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
14238
16016
  if (outcome.ok) {
14239
16017
  deleted++;
16018
+ const lastActivityMs = lastActivityById.get(id) ?? null;
16019
+ deletionRecords.push({
16020
+ opencode_session_id: id,
16021
+ deleted_at: (/* @__PURE__ */ new Date()).toISOString(),
16022
+ reason: "auto_cleanup",
16023
+ policy: {
16024
+ max_age_ms: config.maxAgeMs ?? null,
16025
+ max_count: config.maxCount ?? null,
16026
+ last_activity_at: lastActivityMs === null ? null : new Date(lastActivityMs).toISOString()
16027
+ }
16028
+ });
14240
16029
  } else if (outcome.status === "threw") {
14241
16030
  failed++;
14242
16031
  failedThrew++;
@@ -14292,6 +16081,32 @@ async function runSweep(state, driver, config) {
14292
16081
  message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
14293
16082
  });
14294
16083
  }
16084
+ if (deletionRecords.length > 0) {
16085
+ try {
16086
+ const dropped = appendSessionDeletionRecords(
16087
+ sessionDeletionReportPath(homedir8(), process.env),
16088
+ deletionRecords
16089
+ );
16090
+ if (dropped > 0) {
16091
+ logSessionCleanupActivity(state, "sweep", {
16092
+ type: "info",
16093
+ level: "warn",
16094
+ message: `Session cleanup: dropped ${dropped} oldest pending deletion report(s) after reaching the spool limit`,
16095
+ metadata: { outcome: "deletion-reports-dropped", dropped_count: dropped }
16096
+ });
16097
+ }
16098
+ } catch (error2) {
16099
+ logSessionCleanupActivity(state, "sweep", {
16100
+ type: "info",
16101
+ level: "warn",
16102
+ message: `Session cleanup: could not persist ${deletionRecords.length} confirmed deletion report(s): ${error2 instanceof Error ? error2.message : String(error2)}`,
16103
+ metadata: {
16104
+ outcome: "deletion-report-spool-write-failed",
16105
+ records: deletionRecords.length
16106
+ }
16107
+ });
16108
+ }
16109
+ }
14295
16110
  const sessionMetadata = {
14296
16111
  sessions_inspected: sessions?.length ?? null,
14297
16112
  sessions_deleted: deleted,
@@ -14361,6 +16176,7 @@ async function runSweep(state, driver, config) {
14361
16176
  }
14362
16177
  });
14363
16178
  }
16179
+ await drainSessionDeletionReports(state);
14364
16180
  } catch (error2) {
14365
16181
  const message = error2 instanceof Error ? error2.message : String(error2);
14366
16182
  logSessionCleanupActivity(state, "sweep", {
@@ -14385,7 +16201,7 @@ function scheduleSessionCleanup(state, driver, options) {
14385
16201
  for (const warning2 of config.warnings) {
14386
16202
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
14387
16203
  }
14388
- const dbBytes = statSessionDbBytes(homedir7());
16204
+ const dbBytes = statSessionDbBytes(homedir8());
14389
16205
  void (async () => {
14390
16206
  const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
14391
16207
  if (reclaimAvailability !== null) {
@@ -14452,10 +16268,12 @@ function scheduleUsageReporting(state, params) {
14452
16268
  let phase = "dormant";
14453
16269
  let rearmRequested = false;
14454
16270
  const armProbe = () => {
16271
+ if (!state.running) return;
14455
16272
  phase = "probe-pending";
14456
16273
  params.setTimer(setTimeout(() => void tick(true), params.firstDelayMs()));
14457
16274
  };
14458
16275
  const scheduleNextTick = () => {
16276
+ if (!state.running) return;
14459
16277
  if (rearmRequested) {
14460
16278
  rearmRequested = false;
14461
16279
  armProbe();
@@ -14489,6 +16307,7 @@ function scheduleUsageReporting(state, params) {
14489
16307
  phase = "tick-in-flight";
14490
16308
  try {
14491
16309
  const usage = await params.fetchUsage();
16310
+ if (!state.running) return;
14492
16311
  const result = await params.report(usage);
14493
16312
  if (result.ok) {
14494
16313
  if (consecutiveFailures > 0) {
@@ -14601,7 +16420,7 @@ function scheduleResourceUsageReporting(state, options) {
14601
16420
  });
14602
16421
  return;
14603
16422
  }
14604
- const { collect, stop } = createResourceUsageCollector(homedir7());
16423
+ const { collect, stop } = createResourceUsageCollector(homedir8());
14605
16424
  state.stopResourceUsageSampling = stop;
14606
16425
  let consecutiveFailures = 0;
14607
16426
  const tick = async () => {
@@ -14627,6 +16446,7 @@ function scheduleResourceUsageReporting(state, options) {
14627
16446
  message: lowDiskDecision.warning
14628
16447
  });
14629
16448
  }
16449
+ if (!state.running) return;
14630
16450
  const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
14631
16451
  if (result.ok) {
14632
16452
  if (consecutiveFailures > 0) {
@@ -14659,7 +16479,9 @@ function scheduleResourceUsageReporting(state, options) {
14659
16479
  message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
14660
16480
  });
14661
16481
  } finally {
14662
- state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
16482
+ if (state.running) {
16483
+ state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
16484
+ }
14663
16485
  }
14664
16486
  };
14665
16487
  state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
@@ -14837,9 +16659,11 @@ async function run(options) {
14837
16659
  const interactive = isInteractive(options.json);
14838
16660
  let logLevel;
14839
16661
  let fileSyncDirectories;
16662
+ let fileSyncExactFiles;
14840
16663
  try {
14841
16664
  logLevel = resolveLogLevel(options);
14842
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir7());
16665
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir8());
16666
+ fileSyncExactFiles = resolveFileSyncExactFiles(options.enableFileSyncToExactFile, homedir8());
14843
16667
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
14844
16668
  throw new Error(
14845
16669
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -14862,6 +16686,8 @@ async function run(options) {
14862
16686
  port: options.port ?? 4096,
14863
16687
  conversationFilter: options.conversation ?? null,
14864
16688
  idleTimeout: options.idleTimeout ?? null,
16689
+ stayAwakeSeconds: null,
16690
+ stayAwakeUntilMs: null,
14865
16691
  json: options.json ?? false,
14866
16692
  interactive,
14867
16693
  logLevel,
@@ -14913,7 +16739,11 @@ async function run(options) {
14913
16739
  }
14914
16740
  if (fileSyncDirectories.length > 0) {
14915
16741
  log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
14916
- } else {
16742
+ }
16743
+ if (fileSyncExactFiles.length > 0) {
16744
+ log2(state, `File sync exact files enabled for: ${fileSyncExactFiles.join(", ")}`);
16745
+ }
16746
+ if (fileSyncDirectories.length === 0 && fileSyncExactFiles.length === 0) {
14917
16747
  log2(state, "File sync is disabled (no --enable-file-sync-to given)", "debug");
14918
16748
  }
14919
16749
  if (!options.runner && options.agent) {
@@ -15051,6 +16881,17 @@ async function run(options) {
15051
16881
  }
15052
16882
  spinner?.succeed(`Runner: ${validation.agent.name || state.agentId}`);
15053
16883
  state.agentName = validation.agent.name;
16884
+ const requestedStayAwakeSeconds = validation.agent?.wake_stay_awake_seconds;
16885
+ if (typeof requestedStayAwakeSeconds === "number" && Number.isFinite(requestedStayAwakeSeconds) && requestedStayAwakeSeconds > 0) {
16886
+ state.stayAwakeSeconds = requestedStayAwakeSeconds;
16887
+ } else if (requestedStayAwakeSeconds !== void 0 && requestedStayAwakeSeconds !== null) {
16888
+ state.stayAwakeSeconds = null;
16889
+ log2(
16890
+ state,
16891
+ `Ignoring invalid wake_stay_awake_seconds (type: ${typeof requestedStayAwakeSeconds})`,
16892
+ "warn"
16893
+ );
16894
+ }
15054
16895
  const microvmId = process.env.MICROVM_ID?.trim();
15055
16896
  if (microvmId) {
15056
16897
  const reported = await reportMicrovmId(state.agentId, state.authHeader, microvmId);
@@ -15112,7 +16953,7 @@ async function run(options) {
15112
16953
  state.credentialSync?.arm();
15113
16954
  const dependencyInstallDiagnosticReporter = createDependencyInstallDiagnosticReporter({
15114
16955
  context: {
15115
- homeDir: homedir7(),
16956
+ homeDir: homedir8(),
15116
16957
  cwd: process.cwd(),
15117
16958
  microvmId: process.env.MICROVM_ID?.trim(),
15118
16959
  euid: process.getuid?.(),
@@ -15135,7 +16976,7 @@ async function run(options) {
15135
16976
  dependencyInstallDiagnosticReporter
15136
16977
  );
15137
16978
  const openCodeLogTail = tailOpenCodeLogFile(
15138
- resolveOpenCodeLogPath(homedir7(), process.env),
16979
+ resolveOpenCodeLogPath(homedir8(), process.env),
15139
16980
  openCodeActivityForwarder
15140
16981
  );
15141
16982
  state.stopOpenCodeLogTail = () => {
@@ -15184,6 +17025,7 @@ async function run(options) {
15184
17025
  }
15185
17026
  }
15186
17027
  reportSessionDbRecovery(state);
17028
+ await drainSessionDeletionReports(state);
15187
17029
  if (sessionDbVerifyFatal) {
15188
17030
  throw new Error(
15189
17031
  sessionDbVerifyFatalReason === void 0 ? "SESSION-DB-INTEGRITY-EXHAUSTED: the corrupt session DB could not be proven separated from the active backup prefix or removed from disk" : `SESSION-DB-VERIFY-TIMEOUT: session DB verification timed out (${sessionDbVerifyFatalReason}); refusing to start`
@@ -15210,6 +17052,44 @@ async function run(options) {
15210
17052
  for (const warning2 of opencodeVersionWarnings) {
15211
17053
  logActivity(state, { type: "info", level: "warn", message: warning2 });
15212
17054
  }
17055
+ const managedSetupOptions = {
17056
+ agentId: state.agentId,
17057
+ apiUrl: getApiUrlConfig(),
17058
+ getAuthHeader: () => state.authHeader,
17059
+ autoConfigureCredentialsProxy: options.autoConfigureCredentialsProxy,
17060
+ log: (message, level = "info") => logActivity(state, {
17061
+ type: level === "error" ? "error" : "info",
17062
+ level,
17063
+ ...level === "error" ? { error: message } : { message }
17064
+ })
17065
+ };
17066
+ let managedDiscovery = await discoverManagedInferenceSetup(managedSetupOptions);
17067
+ if (managedDiscovery.kind === "none") {
17068
+ managedDiscovery = await enrollManagedInferenceSetup(managedSetupOptions);
17069
+ }
17070
+ let managedPreparation = { kind: "none" };
17071
+ if (managedDiscovery.kind === "failed") {
17072
+ managedPreparation = managedDiscovery;
17073
+ managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
17074
+ } else if (managedDiscovery.kind === "setup") {
17075
+ const managedHealth = opencodeVersion === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
17076
+ managedPreparation = await prepareManagedInferenceSetup(
17077
+ managedSetupOptions,
17078
+ managedDiscovery,
17079
+ managedHealth
17080
+ );
17081
+ if (managedPreparation.kind === "failed") {
17082
+ managedSetupOptions.log(
17083
+ `MANAGED-INFERENCE-NOT-READY: ${managedPreparation.reason}`,
17084
+ "warn"
17085
+ );
17086
+ } else if (managedPreparation.kind === "ready") {
17087
+ managedSetupOptions.log(
17088
+ `MANAGED-INFERENCE-PREPARED: runner=${state.agentId} generation=${managedPreparation.generation}`,
17089
+ "info"
17090
+ );
17091
+ }
17092
+ }
15213
17093
  const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
15214
17094
  for (const warning2 of maxActiveSessionsWarnings) {
15215
17095
  logActivity(state, { type: "info", level: "warn", message: warning2 });
@@ -15236,6 +17116,32 @@ async function run(options) {
15236
17116
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
15237
17117
  state.opencodeVersion = oc.version;
15238
17118
  state.opencodeApiVersion = opencodeVersion;
17119
+ let configuredPlugins = [];
17120
+ let resolvedPlugins = [];
17121
+ try {
17122
+ const provenanceLog = (message) => logActivity(state, { type: "info", level: "warn", message });
17123
+ configuredPlugins = readConfiguredPluginSpecs({ log: provenanceLog });
17124
+ resolvedPlugins = resolveInstalledPluginVersions({ log: provenanceLog });
17125
+ } catch (error2) {
17126
+ const kind = error2 instanceof Error ? error2.name : "unknown error";
17127
+ logActivity(state, {
17128
+ type: "info",
17129
+ level: "warn",
17130
+ message: `Could not collect OpenCode runtime provenance: ${kind}`
17131
+ });
17132
+ }
17133
+ logActivity(state, {
17134
+ type: "info",
17135
+ level: "info",
17136
+ message: "OpenCode runtime provenance",
17137
+ metadata: {
17138
+ opencode_version: state.opencodeVersion,
17139
+ opencode_api_version: state.opencodeApiVersion,
17140
+ configured_plugins: JSON.stringify(configuredPlugins),
17141
+ resolved_plugins: JSON.stringify(resolvedPlugins)
17142
+ },
17143
+ source: "cli.runtime-provenance"
17144
+ });
15239
17145
  let opencodePassword = null;
15240
17146
  if (opencodeVersion === "v2" && "password" in oc) {
15241
17147
  const value = oc.password;
@@ -15250,7 +17156,7 @@ async function run(options) {
15250
17156
  state.opencodeClient = openCodeClient;
15251
17157
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
15252
17158
  try {
15253
- writeFileSync6(options.opencodePidFile, `${oc.process.pid}
17159
+ writeFileSync7(options.opencodePidFile, `${oc.process.pid}
15254
17160
  `, { mode: 384 });
15255
17161
  chmodSync3(options.opencodePidFile, 384);
15256
17162
  } catch (error2) {
@@ -15264,7 +17170,7 @@ async function run(options) {
15264
17170
  const provenance = checkSessionDbProvenance({
15265
17171
  dbPath: sessionDbPath(),
15266
17172
  currentVersion: state.opencodeVersion,
15267
- homeDir: homedir7(),
17173
+ homeDir: homedir8(),
15268
17174
  env: process.env
15269
17175
  });
15270
17176
  if (provenance.anomaly) {
@@ -15278,6 +17184,30 @@ async function run(options) {
15278
17184
  }
15279
17185
  }
15280
17186
  state.opencodeConnected = oc.notReadyReason === null;
17187
+ if (managedPreparation.kind === "ready" && oc.notReadyReason === null) {
17188
+ try {
17189
+ const acknowledgement = await acknowledgeManagedInferenceSetup(
17190
+ managedSetupOptions,
17191
+ managedPreparation
17192
+ );
17193
+ if (acknowledgement === "acknowledged") {
17194
+ managedSetupOptions.log(
17195
+ `MANAGED-INFERENCE-ACKNOWLEDGED: runner=${state.agentId} generation=${managedPreparation.generation}`,
17196
+ "info"
17197
+ );
17198
+ } else {
17199
+ managedSetupOptions.log(
17200
+ `MANAGED-INFERENCE-NOT-READY: policy refresh is pending for runner=${state.agentId} generation=${managedPreparation.generation}`,
17201
+ "warn"
17202
+ );
17203
+ }
17204
+ } catch (error2) {
17205
+ managedSetupOptions.log(
17206
+ `MANAGED-INFERENCE-NOT-READY: ${error2 instanceof Error ? error2.message : String(error2)}`,
17207
+ "warn"
17208
+ );
17209
+ }
17210
+ }
15281
17211
  const version2 = state.opencodeVersion ? ` (v${state.opencodeVersion})` : "";
15282
17212
  ocSpinner?.succeed(`OpenCode running on port ${state.port}${version2}`);
15283
17213
  if (!state.interactive && oc.notReadyReason !== null) {
@@ -15294,7 +17224,7 @@ async function run(options) {
15294
17224
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
15295
17225
  }
15296
17226
  }
15297
- await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
17227
+ await disposeAndReloadProviderConfig(state.port, state.opencodeClient ?? void 0);
15298
17228
  if (state.opencodeApiVersion === "v2" && state.opencodeClient !== null) {
15299
17229
  await importOpenCodeV2CredentialsForState(state);
15300
17230
  }
@@ -15336,7 +17266,7 @@ async function run(options) {
15336
17266
  let existingPid;
15337
17267
  if (existsSync3(options.litestreamPidFile)) {
15338
17268
  try {
15339
- const rawPid = readFileSync7(options.litestreamPidFile, "utf8").trim();
17269
+ const rawPid = readFileSync9(options.litestreamPidFile, "utf8").trim();
15340
17270
  const parsedPid = Number(rawPid);
15341
17271
  if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
15342
17272
  existingPid = parsedPid;
@@ -15373,7 +17303,7 @@ async function run(options) {
15373
17303
  });
15374
17304
  try {
15375
17305
  if (litestreamProcess.pid !== void 0) {
15376
- writeFileSync6(options.litestreamPidFile, `${litestreamProcess.pid}
17306
+ writeFileSync7(options.litestreamPidFile, `${litestreamProcess.pid}
15377
17307
  `, {
15378
17308
  mode: 384
15379
17309
  });
@@ -15432,10 +17362,11 @@ async function run(options) {
15432
17362
  conversationFilter: state.conversationFilter,
15433
17363
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
15434
17364
  sessionStoreHealth: state.sessionStoreHealth,
15435
- // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
15436
- // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
17365
+ // #559: the file-sync allow-lists. Empty ⇒ pending files are REJECTED with
17366
+ // `file_sync_disabled` on the ack, not silently ignored.
15437
17367
  fileSyncDirectories,
15438
- homeDir: homedir7(),
17368
+ fileSyncExactFiles,
17369
+ homeDir: homedir8(),
15439
17370
  maxActiveSessions,
15440
17371
  log: (entry) => (
15441
17372
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -15460,11 +17391,18 @@ async function run(options) {
15460
17391
  onConnected: (agentId, isReconnect, transition) => {
15461
17392
  state.connected = true;
15462
17393
  state.agentId = agentId;
15463
- const connectionMessage = isReconnect ? `Tunnel reconnected (runner: ${agentId}, generation: ${transition.generation}, attempt: ${transition.attempt}, offline for ${transition.downtimeMs}ms)` : `Tunnel connected (runner: ${agentId}, generation: ${transition.generation})`;
17394
+ const connectionMessage = isReconnect ? `Tunnel reconnected (runner: ${agentId}, generation: ${transition.generation}, relay connection: ${transition.relayConnectionId ?? "unknown"}, attempt: ${transition.attempt}, offline for ${transition.downtimeMs}ms)` : `Tunnel connected (runner: ${agentId}, generation: ${transition.generation}, relay connection: ${transition.relayConnectionId ?? "unknown"})`;
15464
17395
  logActivity(state, {
15465
17396
  type: "info",
15466
17397
  message: connectionMessage
15467
17398
  });
17399
+ if (!isReconnect && state.stayAwakeSeconds !== null && state.stayAwakeUntilMs === null) {
17400
+ state.stayAwakeUntilMs = Date.now() + state.stayAwakeSeconds * 1e3;
17401
+ logActivity(state, {
17402
+ type: "info",
17403
+ message: `Staying awake for at least ${state.stayAwakeSeconds}s as requested by the wake`
17404
+ });
17405
+ }
15468
17406
  if (options.tunnelReadyFile) {
15469
17407
  const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
15470
17408
  if (marker.ok) {
@@ -15486,6 +17424,7 @@ async function run(options) {
15486
17424
  connected: state.opencodeConnected
15487
17425
  }),
15488
17426
  connection_generation: transition.generation,
17427
+ ...transition.relayConnectionId !== null ? { relay_connection_id: transition.relayConnectionId } : {},
15489
17428
  ...isReconnect && transition.downtimeMs !== null ? {
15490
17429
  reconnect_attempt: transition.attempt,
15491
17430
  downtime_ms: transition.downtimeMs
@@ -15516,12 +17455,13 @@ async function run(options) {
15516
17455
  const safeReason2 = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
15517
17456
  logActivity(state, {
15518
17457
  type: "info",
15519
- message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason2})`
17458
+ message: `Tunnel disconnected (generation: ${transition.generation}, relay connection: ${transition.relayConnectionId ?? "unknown"}, code: ${code}, reason: ${safeReason2})`
15520
17459
  });
15521
17460
  emitAgentDisconnected(state.agentId, {
15522
17461
  code,
15523
17462
  reason: safeReason2,
15524
- connection_generation: transition.generation
17463
+ connection_generation: transition.generation,
17464
+ ...transition.relayConnectionId !== null ? { relay_connection_id: transition.relayConnectionId } : {}
15525
17465
  });
15526
17466
  if (state.interactive) displayStatus(state);
15527
17467
  },
@@ -15599,6 +17539,7 @@ async function run(options) {
15599
17539
  if (error2.message === "Unauthorized") tunnelSpinner?.fail("Unauthorized");
15600
17540
  throw error2;
15601
17541
  }
17542
+ if (!state.running) return;
15602
17543
  scheduleSessionCleanup(state, channelDriver, options);
15603
17544
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
15604
17545
  state.openaiUsageRearm = scheduleUsageReporting(state, {
@@ -15671,9 +17612,9 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
15671
17612
  "--endpoint <url>",
15672
17613
  "Evident API base URL (default: production; e.g. http://localhost:3001)"
15673
17614
  ).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);
17615
+ const { endpoint: endpoint2, tunnel } = thisCommand.opts();
17616
+ if (endpoint2) {
17617
+ setEndpoint(endpoint2);
15677
17618
  }
15678
17619
  if (tunnel) {
15679
17620
  setTunnelUrl(tunnel);
@@ -15723,6 +17664,11 @@ program.command("run").description("Connect to Evident and process messages").op
15723
17664
  "Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
15724
17665
  (value, previous) => previous.concat([value]),
15725
17666
  []
17667
+ ).option(
17668
+ "--enable-file-sync-to-exact-file <path>",
17669
+ "Allow Evident to write files to exactly this path (repeatable).",
17670
+ (value, previous) => previous.concat([value]),
17671
+ []
15726
17672
  ).option(
15727
17673
  "--tunnel-ready-file <path>",
15728
17674
  "Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
@@ -15747,6 +17693,9 @@ program.command("run").description("Connect to Evident and process messages").op
15747
17693
  ).option(
15748
17694
  "--opencode-config-overlay <path>",
15749
17695
  "Apply this runner-provided OpenCode config before starting OpenCode."
17696
+ ).option(
17697
+ "--auto-configure-credentials-proxy",
17698
+ "Install Evident's generated OpenCode providers for the runner's credential pool (off by default)."
15750
17699
  ).option(
15751
17700
  "--credential-sync-marker <path>",
15752
17701
  "Own the interval credential sync and write this marker once the shutdown flush has finished, so the runner image's lifecycle hooks can wait on it."
@@ -15782,6 +17731,9 @@ program.command("run").description("Connect to Evident and process messages").op
15782
17731
  // Raw values — expansion/validation is single-sourced in run.ts's
15783
17732
  // resolveFileSyncDirectories.
15784
17733
  enableFileSyncTo: options.enableFileSyncTo,
17734
+ // Raw values — expansion/validation is single-sourced in run.ts's
17735
+ // resolveFileSyncExactFiles.
17736
+ enableFileSyncToExactFile: options.enableFileSyncToExactFile,
15785
17737
  tunnelReadyFile: options.tunnelReadyFile,
15786
17738
  litestreamConfig: options.litestreamConfig,
15787
17739
  opencodePidFile: options.opencodePidFile,
@@ -15790,6 +17742,7 @@ program.command("run").description("Connect to Evident and process messages").op
15790
17742
  restoreSessionDb: options.restoreSessionDb,
15791
17743
  restoreRunnerCredentials: options.restoreRunnerCredentials,
15792
17744
  opencodeConfigOverlay: options.opencodeConfigOverlay,
17745
+ autoConfigureCredentialsProxy: options.autoConfigureCredentialsProxy,
15793
17746
  credentialSyncMarker: options.credentialSyncMarker
15794
17747
  });
15795
17748
  }