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