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