@evident-ai/cli 3.4.1-dev.69d24ff → 3.4.1-dev.6e623a8
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 +3 -5
- package/dist/index.js +1288 -231
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1178,11 +1178,11 @@ function stripQuery(url) {
|
|
|
1178
1178
|
}
|
|
1179
1179
|
|
|
1180
1180
|
// src/commands/run.ts
|
|
1181
|
-
import
|
|
1181
|
+
import ora4 from "ora";
|
|
1182
1182
|
import { select as select4 } from "@inquirer/prompts";
|
|
1183
1183
|
|
|
1184
1184
|
// src/lib/telemetry.ts
|
|
1185
|
-
var CLI_VERSION = (true ? "3.4.1-dev.
|
|
1185
|
+
var CLI_VERSION = (true ? "3.4.1-dev.6e623a8" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1186
1186
|
function getCliVersion() {
|
|
1187
1187
|
return CLI_VERSION;
|
|
1188
1188
|
}
|
|
@@ -1656,6 +1656,11 @@ function isSessionDbRecoveryRecord(value) {
|
|
|
1656
1656
|
);
|
|
1657
1657
|
}
|
|
1658
1658
|
|
|
1659
|
+
// src/lib/opencode/auth.ts
|
|
1660
|
+
function buildOpenCodeBasicAuthHeader(password) {
|
|
1661
|
+
return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1659
1664
|
// src/lib/opencode/health.ts
|
|
1660
1665
|
async function checkOpenCodeHealth(port) {
|
|
1661
1666
|
try {
|
|
@@ -1673,6 +1678,27 @@ async function checkOpenCodeHealth(port) {
|
|
|
1673
1678
|
return { healthy: false, error: message };
|
|
1674
1679
|
}
|
|
1675
1680
|
}
|
|
1681
|
+
async function checkOpenCode2Health(port, password) {
|
|
1682
|
+
try {
|
|
1683
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
1684
|
+
headers: {
|
|
1685
|
+
Authorization: buildOpenCodeBasicAuthHeader(password)
|
|
1686
|
+
},
|
|
1687
|
+
signal: AbortSignal.timeout(2e3)
|
|
1688
|
+
});
|
|
1689
|
+
if (response.status === 401) {
|
|
1690
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
1691
|
+
}
|
|
1692
|
+
if (!response.ok) {
|
|
1693
|
+
return { healthy: false, error: `HTTP ${response.status}` };
|
|
1694
|
+
}
|
|
1695
|
+
const data = await response.json().catch(() => ({}));
|
|
1696
|
+
return { healthy: true, version: data.version };
|
|
1697
|
+
} catch (error2) {
|
|
1698
|
+
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1699
|
+
return { healthy: false, error: message };
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1676
1702
|
async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
1677
1703
|
const startTime = Date.now();
|
|
1678
1704
|
while (Date.now() - startTime < timeoutMs) {
|
|
@@ -1684,6 +1710,61 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
1684
1710
|
}
|
|
1685
1711
|
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
1686
1712
|
}
|
|
1713
|
+
async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
|
|
1714
|
+
const startTime = Date.now();
|
|
1715
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
1716
|
+
const health = await checkOpenCode2Health(port, password);
|
|
1717
|
+
if (health.healthy || health.authFailed) {
|
|
1718
|
+
return health;
|
|
1719
|
+
}
|
|
1720
|
+
await new Promise((resolve4) => setTimeout(resolve4, 1e3));
|
|
1721
|
+
}
|
|
1722
|
+
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/lib/http-timeout.ts
|
|
1726
|
+
var REQUEST_TIMEOUT_MS = 6e4;
|
|
1727
|
+
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
1728
|
+
return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/lib/opencode/client.ts
|
|
1732
|
+
function redactPassword(message, password) {
|
|
1733
|
+
return message.replaceAll(password, "[redacted]");
|
|
1734
|
+
}
|
|
1735
|
+
function createOpenCodeClient(options) {
|
|
1736
|
+
const password = options.password ?? null;
|
|
1737
|
+
const fetchImpl = withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
|
|
1738
|
+
const baseUrl = `http://127.0.0.1:${options.port}`;
|
|
1739
|
+
return {
|
|
1740
|
+
port: options.port,
|
|
1741
|
+
version: options.version,
|
|
1742
|
+
password,
|
|
1743
|
+
async request(path, init, requestOptions) {
|
|
1744
|
+
const requestInit = options.version === "v2" && password !== null ? (() => {
|
|
1745
|
+
const headers = new Headers(init?.headers);
|
|
1746
|
+
headers.set("Authorization", buildOpenCodeBasicAuthHeader(password));
|
|
1747
|
+
return { ...init, headers };
|
|
1748
|
+
})() : init;
|
|
1749
|
+
try {
|
|
1750
|
+
const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
|
|
1751
|
+
if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
|
|
1752
|
+
const body = await response.text();
|
|
1753
|
+
throw new Error(
|
|
1754
|
+
`OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
return response;
|
|
1758
|
+
} catch (error2) {
|
|
1759
|
+
if (options.version === "v2" && password !== null) {
|
|
1760
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1761
|
+
throw new Error(redactPassword(message, password));
|
|
1762
|
+
}
|
|
1763
|
+
throw error2;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1687
1768
|
|
|
1688
1769
|
// src/lib/opencode/session-db-boot.ts
|
|
1689
1770
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -2363,6 +2444,7 @@ function reportedOpenCodeVersion(input) {
|
|
|
2363
2444
|
|
|
2364
2445
|
// src/lib/opencode/process.ts
|
|
2365
2446
|
import { execSync, spawn as spawn3 } from "child_process";
|
|
2447
|
+
import { randomBytes } from "node:crypto";
|
|
2366
2448
|
|
|
2367
2449
|
// src/lib/process-stop.ts
|
|
2368
2450
|
async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
@@ -2421,6 +2503,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
2421
2503
|
// src/lib/opencode/process.ts
|
|
2422
2504
|
var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
|
|
2423
2505
|
var VALID_OPENCODE_LOG_LEVELS = /* @__PURE__ */ new Set(["DEBUG", "INFO", "WARN", "ERROR"]);
|
|
2506
|
+
var VALID_OPENCODE2_LOG_LEVELS = /* @__PURE__ */ new Set([
|
|
2507
|
+
"all",
|
|
2508
|
+
"trace",
|
|
2509
|
+
"debug",
|
|
2510
|
+
"info",
|
|
2511
|
+
"warn",
|
|
2512
|
+
"warning",
|
|
2513
|
+
"error",
|
|
2514
|
+
"fatal",
|
|
2515
|
+
"none"
|
|
2516
|
+
]);
|
|
2424
2517
|
function resolveOpenCodeLogLevel(env) {
|
|
2425
2518
|
const raw = env.OPENCODE_LOG_LEVEL;
|
|
2426
2519
|
if (!raw) return "INFO";
|
|
@@ -2431,6 +2524,16 @@ function resolveOpenCodeLogLevel(env) {
|
|
|
2431
2524
|
);
|
|
2432
2525
|
return "INFO";
|
|
2433
2526
|
}
|
|
2527
|
+
function resolveOpenCode2LogLevel(env) {
|
|
2528
|
+
const raw = env.OPENCODE_LOG_LEVEL;
|
|
2529
|
+
if (!raw) return "info";
|
|
2530
|
+
const lower = raw.toLowerCase();
|
|
2531
|
+
if (VALID_OPENCODE2_LOG_LEVELS.has(lower)) return lower;
|
|
2532
|
+
console.warn(
|
|
2533
|
+
`startOpenCode2: ignoring invalid OPENCODE_LOG_LEVEL "${raw}" (expected all|trace|debug|info|warn|warning|error|fatal|none) \u2014 using info`
|
|
2534
|
+
);
|
|
2535
|
+
return "info";
|
|
2536
|
+
}
|
|
2434
2537
|
function getProcessCwd(pid) {
|
|
2435
2538
|
const platform = process.platform;
|
|
2436
2539
|
try {
|
|
@@ -2613,6 +2716,37 @@ async function startOpenCode(port, options = {}) {
|
|
|
2613
2716
|
});
|
|
2614
2717
|
return child;
|
|
2615
2718
|
}
|
|
2719
|
+
async function startOpenCode2(port, options = {}) {
|
|
2720
|
+
const password = randomBytes(24).toString("hex");
|
|
2721
|
+
let command = "opencode2";
|
|
2722
|
+
const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
|
|
2723
|
+
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
|
|
2724
|
+
try {
|
|
2725
|
+
execSync("which opencode2", { stdio: "ignore" });
|
|
2726
|
+
} catch {
|
|
2727
|
+
command = "npx";
|
|
2728
|
+
args = [
|
|
2729
|
+
"-y",
|
|
2730
|
+
"-p",
|
|
2731
|
+
"@opencode-ai/cli@beta",
|
|
2732
|
+
"--",
|
|
2733
|
+
"opencode2",
|
|
2734
|
+
"serve",
|
|
2735
|
+
"--port",
|
|
2736
|
+
port.toString(),
|
|
2737
|
+
"--hostname",
|
|
2738
|
+
"127.0.0.1",
|
|
2739
|
+
...logLevel
|
|
2740
|
+
];
|
|
2741
|
+
}
|
|
2742
|
+
const child = spawn3(command, args, {
|
|
2743
|
+
env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
|
|
2744
|
+
detached: true,
|
|
2745
|
+
stdio: options.inheritStdio ? "inherit" : "ignore",
|
|
2746
|
+
cwd: process.cwd()
|
|
2747
|
+
});
|
|
2748
|
+
return { child, password };
|
|
2749
|
+
}
|
|
2616
2750
|
function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
|
|
2617
2751
|
const sendSignal = (signal) => {
|
|
2618
2752
|
if (process.platform === "win32") {
|
|
@@ -2760,27 +2894,500 @@ function buildNoProviderWarning(hasProvider) {
|
|
|
2760
2894
|
return "Warning: opencode has no authenticated model provider configured, so it won't be able to answer prompts. Run `opencode auth login` to set one up (see https://opencode.ai for details).";
|
|
2761
2895
|
}
|
|
2762
2896
|
|
|
2763
|
-
// src/lib/
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2897
|
+
// src/lib/opencode/session-v2.ts
|
|
2898
|
+
function isRecord(value) {
|
|
2899
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2900
|
+
}
|
|
2901
|
+
function finiteNumber(value) {
|
|
2902
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2903
|
+
}
|
|
2904
|
+
function adaptTime(value) {
|
|
2905
|
+
if (!isRecord(value)) return void 0;
|
|
2906
|
+
const created = finiteNumber(value.created);
|
|
2907
|
+
const completed = finiteNumber(value.completed);
|
|
2908
|
+
if (created === void 0 && completed === void 0) return void 0;
|
|
2909
|
+
return {
|
|
2910
|
+
...created !== void 0 ? { created } : {},
|
|
2911
|
+
...completed !== void 0 ? { completed } : {}
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
function adaptTokens(value) {
|
|
2915
|
+
if (!isRecord(value)) return void 0;
|
|
2916
|
+
const input = finiteNumber(value.input);
|
|
2917
|
+
const output = finiteNumber(value.output);
|
|
2918
|
+
const reasoning = finiteNumber(value.reasoning);
|
|
2919
|
+
const cache = isRecord(value.cache) ? {
|
|
2920
|
+
...finiteNumber(value.cache.read) !== void 0 ? { read: finiteNumber(value.cache.read) } : {},
|
|
2921
|
+
...finiteNumber(value.cache.write) !== void 0 ? { write: finiteNumber(value.cache.write) } : {}
|
|
2922
|
+
} : void 0;
|
|
2923
|
+
if (input === void 0 && output === void 0 && reasoning === void 0 && !cache) {
|
|
2924
|
+
return void 0;
|
|
2925
|
+
}
|
|
2926
|
+
return {
|
|
2927
|
+
...input !== void 0 ? { input } : {},
|
|
2928
|
+
...output !== void 0 ? { output } : {},
|
|
2929
|
+
...reasoning !== void 0 ? { reasoning } : {},
|
|
2930
|
+
...cache ? { cache } : {}
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
function adaptMessageInfo(value, role) {
|
|
2934
|
+
const info = {
|
|
2935
|
+
id: value.id,
|
|
2936
|
+
role
|
|
2937
|
+
};
|
|
2938
|
+
const time = adaptTime(value.time);
|
|
2939
|
+
if (time) info.time = time;
|
|
2940
|
+
if (typeof value.finish === "string") info.finish = value.finish;
|
|
2941
|
+
if ("error" in value) info.error = value.error;
|
|
2942
|
+
if (typeof value.agent === "string") info.agent = value.agent;
|
|
2943
|
+
if (isRecord(value.model)) {
|
|
2944
|
+
if (typeof value.model.id === "string") info.modelID = value.model.id;
|
|
2945
|
+
if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
|
|
2946
|
+
}
|
|
2947
|
+
if (typeof value.cost === "number" && Number.isFinite(value.cost)) info.cost = value.cost;
|
|
2948
|
+
const tokens = adaptTokens(value.tokens);
|
|
2949
|
+
if (tokens) info.tokens = tokens;
|
|
2950
|
+
return info;
|
|
2951
|
+
}
|
|
2952
|
+
function adaptV2Message(value) {
|
|
2953
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.type !== "string") {
|
|
2954
|
+
return null;
|
|
2955
|
+
}
|
|
2956
|
+
if (value.type === "user") {
|
|
2957
|
+
if (typeof value.text !== "string") return null;
|
|
2958
|
+
return {
|
|
2959
|
+
info: adaptMessageInfo(value, "user"),
|
|
2960
|
+
parts: [{ type: "text", text: value.text }]
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
|
|
2964
|
+
const parts = [];
|
|
2965
|
+
for (const content of value.content) {
|
|
2966
|
+
if (!isRecord(content) || typeof content.type !== "string") return null;
|
|
2967
|
+
if (content.type === "text") {
|
|
2968
|
+
if (typeof content.text !== "string") return null;
|
|
2969
|
+
parts.push({ type: "text", text: content.text });
|
|
2970
|
+
} else {
|
|
2971
|
+
parts.push({ type: content.type });
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
return {
|
|
2975
|
+
info: adaptMessageInfo(value, "assistant"),
|
|
2976
|
+
parts
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
function adaptFormTool(value) {
|
|
2980
|
+
if (!isRecord(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
|
|
2981
|
+
return void 0;
|
|
2982
|
+
}
|
|
2983
|
+
return { messageID: value.messageID, callID: value.id };
|
|
2984
|
+
}
|
|
2985
|
+
function adaptV2FormWire(value) {
|
|
2986
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
2987
|
+
return null;
|
|
2988
|
+
}
|
|
2989
|
+
return value;
|
|
2990
|
+
}
|
|
2991
|
+
function adaptV2FormField(value, header) {
|
|
2992
|
+
if (!isRecord(value)) return null;
|
|
2993
|
+
const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
|
|
2994
|
+
if (!question) return null;
|
|
2995
|
+
const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
|
|
2996
|
+
if (!isRecord(option)) return [];
|
|
2997
|
+
const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
|
|
2998
|
+
if (!label) return [];
|
|
2999
|
+
return [
|
|
3000
|
+
{
|
|
3001
|
+
label,
|
|
3002
|
+
description: typeof option.description === "string" ? option.description : ""
|
|
3003
|
+
}
|
|
3004
|
+
];
|
|
3005
|
+
}) : [];
|
|
3006
|
+
return { question, header, options };
|
|
3007
|
+
}
|
|
3008
|
+
function adaptV2Form(value) {
|
|
3009
|
+
const form = adaptV2FormWire(value);
|
|
3010
|
+
if (!form || !Array.isArray(form.fields)) return null;
|
|
3011
|
+
const header = typeof form.title === "string" ? form.title : "";
|
|
3012
|
+
const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
|
|
3013
|
+
if (questions.length === 0) return null;
|
|
3014
|
+
const tool = isRecord(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
|
|
3015
|
+
return {
|
|
3016
|
+
id: form.id,
|
|
3017
|
+
sessionID: form.sessionID,
|
|
3018
|
+
questions,
|
|
3019
|
+
...tool ? { tool } : {},
|
|
3020
|
+
raw: form
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
function adaptV2FormList(value) {
|
|
3024
|
+
if (!isRecord(value) || !Array.isArray(value.data)) return null;
|
|
3025
|
+
return value.data.map(adaptV2Form).filter((form) => form !== null);
|
|
3026
|
+
}
|
|
3027
|
+
function adaptPattern(value) {
|
|
3028
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
3029
|
+
if (Array.isArray(value) && value.every((pattern) => typeof pattern === "string")) {
|
|
3030
|
+
return value;
|
|
3031
|
+
}
|
|
3032
|
+
return void 0;
|
|
3033
|
+
}
|
|
3034
|
+
function adaptV2PermissionWire(value) {
|
|
3035
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
3036
|
+
return null;
|
|
3037
|
+
}
|
|
3038
|
+
if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
|
|
3039
|
+
return value;
|
|
3040
|
+
}
|
|
3041
|
+
function adaptV2Permission(value) {
|
|
3042
|
+
const permission = adaptV2PermissionWire(value);
|
|
3043
|
+
if (!permission) return null;
|
|
3044
|
+
const type = permission.permission ?? permission.action;
|
|
3045
|
+
if (!type) return null;
|
|
3046
|
+
const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
|
|
3047
|
+
const time = isRecord(permission.time) ? finiteNumber(permission.time.created) !== void 0 ? { created: finiteNumber(permission.time.created) } : void 0 : void 0;
|
|
3048
|
+
return {
|
|
3049
|
+
id: permission.id,
|
|
3050
|
+
type,
|
|
3051
|
+
sessionID: permission.sessionID,
|
|
3052
|
+
metadata: isRecord(permission.metadata) ? permission.metadata : {},
|
|
3053
|
+
raw: permission,
|
|
3054
|
+
...pattern !== void 0 ? { pattern } : {},
|
|
3055
|
+
...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
|
|
3056
|
+
...typeof permission.callID === "string" ? { callID: permission.callID } : {},
|
|
3057
|
+
...typeof permission.title === "string" ? { title: permission.title } : {},
|
|
3058
|
+
...time ? { time } : {}
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
function adaptV2PermissionList(value) {
|
|
3062
|
+
if (!isRecord(value) || !Array.isArray(value.data)) return null;
|
|
3063
|
+
return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
|
|
3064
|
+
}
|
|
3065
|
+
function adaptV2Session(value) {
|
|
3066
|
+
if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) return null;
|
|
3067
|
+
const time = isRecord(value.time) ? {
|
|
3068
|
+
...finiteNumber(value.time.created) !== void 0 ? { created: finiteNumber(value.time.created) } : {},
|
|
3069
|
+
...finiteNumber(value.time.updated) !== void 0 ? { updated: finiteNumber(value.time.updated) } : {}
|
|
3070
|
+
} : void 0;
|
|
3071
|
+
return {
|
|
3072
|
+
id: value.id,
|
|
3073
|
+
...typeof value.title === "string" ? { title: value.title } : {},
|
|
3074
|
+
...typeof value.parentID === "string" ? { parentID: value.parentID } : {},
|
|
3075
|
+
...time && Object.keys(time).length > 0 ? { time } : {}
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
function adaptV2SessionList(value) {
|
|
3079
|
+
if (!isRecord(value) || !Array.isArray(value.data) || !isRecord(value.cursor)) return null;
|
|
3080
|
+
return {
|
|
3081
|
+
data: value.data.map(adaptV2Session).filter((session) => session !== null),
|
|
3082
|
+
cursor: value.cursor
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
function adaptV2Location(value) {
|
|
3086
|
+
const candidates = [
|
|
3087
|
+
value,
|
|
3088
|
+
isRecord(value) ? value.data : void 0,
|
|
3089
|
+
isRecord(value) ? value.location : void 0
|
|
3090
|
+
];
|
|
3091
|
+
for (const candidate of candidates) {
|
|
3092
|
+
if (!isRecord(candidate) || typeof candidate.directory !== "string") continue;
|
|
3093
|
+
const directory = candidate.directory.trim();
|
|
3094
|
+
if (directory) return directory;
|
|
3095
|
+
}
|
|
3096
|
+
return null;
|
|
3097
|
+
}
|
|
3098
|
+
function adaptV2Messages(value) {
|
|
3099
|
+
if (!isRecord(value) || !Array.isArray(value.data)) return [];
|
|
3100
|
+
return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
|
|
3101
|
+
}
|
|
3102
|
+
async function readJson(response) {
|
|
3103
|
+
try {
|
|
3104
|
+
return await response.json();
|
|
3105
|
+
} catch (error2) {
|
|
3106
|
+
throw new Error(
|
|
3107
|
+
`OpenCode V2 response was not valid JSON: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
async function readData(client, path, init) {
|
|
3112
|
+
const response = await client.request(path, init);
|
|
3113
|
+
const body = await readJson(response);
|
|
3114
|
+
if (!isRecord(body) || !("data" in body)) {
|
|
3115
|
+
throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
|
|
3116
|
+
}
|
|
3117
|
+
return body.data;
|
|
3118
|
+
}
|
|
3119
|
+
var OpenCodeV2PromptAckError = class extends Error {
|
|
3120
|
+
constructor(message) {
|
|
3121
|
+
super(message);
|
|
3122
|
+
this.name = "OpenCodeV2PromptAckError";
|
|
3123
|
+
}
|
|
3124
|
+
};
|
|
3125
|
+
async function getOpenCodeDirectoryV2(client) {
|
|
3126
|
+
try {
|
|
3127
|
+
return adaptV2Location(await readJson(await client.request("/api/location")));
|
|
3128
|
+
} catch (error2) {
|
|
3129
|
+
console.error(
|
|
3130
|
+
`[getOpenCodeDirectoryV2] GET /api/location failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3131
|
+
);
|
|
3132
|
+
return null;
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
async function createV2Session(client, directory) {
|
|
3136
|
+
const data = await readData(client, "/api/session", {
|
|
3137
|
+
method: "POST",
|
|
3138
|
+
headers: { "Content-Type": "application/json" },
|
|
3139
|
+
body: JSON.stringify({ location: { directory } })
|
|
3140
|
+
});
|
|
3141
|
+
if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
3142
|
+
throw new Error("OpenCode V2 create session response was missing data.id");
|
|
3143
|
+
}
|
|
3144
|
+
return data.id;
|
|
3145
|
+
}
|
|
3146
|
+
async function getV2Session(client, sessionId) {
|
|
3147
|
+
const data = await readData(client, `/api/session/${encodeURIComponent(sessionId)}`);
|
|
3148
|
+
const session = adaptV2Session(data);
|
|
3149
|
+
if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
|
|
3150
|
+
return session;
|
|
3151
|
+
}
|
|
3152
|
+
async function listV2SessionPage(client, cursor) {
|
|
3153
|
+
const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
|
|
3154
|
+
try {
|
|
3155
|
+
return adaptV2SessionList(await readJson(await client.request(path)));
|
|
3156
|
+
} catch (error2) {
|
|
3157
|
+
console.error(
|
|
3158
|
+
`[listV2SessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3159
|
+
);
|
|
3160
|
+
return null;
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
async function listV2Sessions(client) {
|
|
3164
|
+
const sessions = [];
|
|
3165
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3166
|
+
let cursor;
|
|
3167
|
+
let hasNextPage = true;
|
|
3168
|
+
try {
|
|
3169
|
+
while (hasNextPage) {
|
|
3170
|
+
const page = await listV2SessionPage(client, cursor);
|
|
3171
|
+
if (!page) return null;
|
|
3172
|
+
sessions.push(...page.data);
|
|
3173
|
+
const next = page.cursor.next;
|
|
3174
|
+
if (next === void 0 || next === null) {
|
|
3175
|
+
hasNextPage = false;
|
|
3176
|
+
continue;
|
|
3177
|
+
}
|
|
3178
|
+
if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
|
|
3179
|
+
throw new Error("OpenCode V2 session list contained an invalid next cursor");
|
|
3180
|
+
}
|
|
3181
|
+
seenCursors.add(next);
|
|
3182
|
+
cursor = next;
|
|
3183
|
+
}
|
|
3184
|
+
return sessions;
|
|
3185
|
+
} catch (error2) {
|
|
3186
|
+
console.error(
|
|
3187
|
+
`[listV2Sessions] session pagination failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3188
|
+
);
|
|
3189
|
+
return null;
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
async function deleteV2Session(client, sessionId) {
|
|
3193
|
+
try {
|
|
3194
|
+
await client.request(`/api/session/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
|
|
3195
|
+
return true;
|
|
3196
|
+
} catch (error2) {
|
|
3197
|
+
console.error(
|
|
3198
|
+
`[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3199
|
+
);
|
|
3200
|
+
return false;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
async function v2SessionExists(client, sessionId) {
|
|
3204
|
+
try {
|
|
3205
|
+
const response = await client.request(
|
|
3206
|
+
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
3207
|
+
void 0,
|
|
3208
|
+
{ allowStatuses: [404] }
|
|
3209
|
+
);
|
|
3210
|
+
return response.status === 404 ? false : true;
|
|
3211
|
+
} catch (error2) {
|
|
3212
|
+
console.error(
|
|
3213
|
+
`[v2SessionExists] GET /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3214
|
+
);
|
|
3215
|
+
return null;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
async function sendV2Prompt(client, sessionId, text) {
|
|
3219
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/prompt`;
|
|
3220
|
+
const response = await client.request(path, {
|
|
3221
|
+
method: "POST",
|
|
3222
|
+
headers: { "Content-Type": "application/json" },
|
|
3223
|
+
body: JSON.stringify({ text, delivery: "queue" })
|
|
3224
|
+
});
|
|
3225
|
+
let body;
|
|
3226
|
+
try {
|
|
3227
|
+
body = await readJson(response);
|
|
3228
|
+
} catch (error2) {
|
|
3229
|
+
throw new OpenCodeV2PromptAckError(
|
|
3230
|
+
`OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
const data = isRecord(body) && "data" in body ? body.data : void 0;
|
|
3234
|
+
if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
3235
|
+
throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
|
|
3236
|
+
}
|
|
3237
|
+
return data.id;
|
|
3238
|
+
}
|
|
3239
|
+
async function getV2SessionMessages(client, sessionId) {
|
|
3240
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
|
|
3241
|
+
try {
|
|
3242
|
+
const body = await readJson(await client.request(path));
|
|
3243
|
+
if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) return null;
|
|
3244
|
+
return adaptV2Messages(body);
|
|
3245
|
+
} catch (error2) {
|
|
3246
|
+
console.error(
|
|
3247
|
+
`[getV2SessionMessages] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3248
|
+
);
|
|
3249
|
+
return null;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
async function listV2Forms(client, sessionId) {
|
|
3253
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/form`;
|
|
3254
|
+
try {
|
|
3255
|
+
return adaptV2FormList(await readJson(await client.request(path)));
|
|
3256
|
+
} catch (error2) {
|
|
3257
|
+
console.error(
|
|
3258
|
+
`[listV2Forms] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3259
|
+
);
|
|
3260
|
+
return null;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
async function listV2Permissions(client, sessionId) {
|
|
3264
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/permission`;
|
|
3265
|
+
try {
|
|
3266
|
+
return adaptV2PermissionList(await readJson(await client.request(path)));
|
|
3267
|
+
} catch (error2) {
|
|
3268
|
+
console.error(
|
|
3269
|
+
`[listV2Permissions] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3270
|
+
);
|
|
3271
|
+
return null;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
async function getV2ActiveSessions(client) {
|
|
3275
|
+
try {
|
|
3276
|
+
const body = await readJson(await client.request("/api/session/active"));
|
|
3277
|
+
if (!isRecord(body) || !isRecord(body.data)) return null;
|
|
3278
|
+
return body.data;
|
|
3279
|
+
} catch (error2) {
|
|
3280
|
+
console.error(
|
|
3281
|
+
`[getV2ActiveSessions] GET /api/session/active failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3282
|
+
);
|
|
3283
|
+
return null;
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
async function isV2SessionOngoing(client, sessionId) {
|
|
3287
|
+
const activeSessions = await getV2ActiveSessions(client);
|
|
3288
|
+
if (activeSessions === null) return null;
|
|
3289
|
+
return Object.prototype.hasOwnProperty.call(activeSessions, sessionId);
|
|
3290
|
+
}
|
|
3291
|
+
function sessionErrorReason(value) {
|
|
3292
|
+
if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
|
|
3293
|
+
if (isRecord(value)) {
|
|
3294
|
+
const data = isRecord(value.data) ? value.data : void 0;
|
|
3295
|
+
const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
|
|
3296
|
+
if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
3297
|
+
}
|
|
3298
|
+
return "OpenCode reported a session error with no details";
|
|
3299
|
+
}
|
|
3300
|
+
function adaptV2SessionErrorEvent(value) {
|
|
3301
|
+
let parsed = value;
|
|
3302
|
+
if (typeof value === "string") {
|
|
3303
|
+
try {
|
|
3304
|
+
parsed = JSON.parse(value);
|
|
3305
|
+
} catch (error2) {
|
|
3306
|
+
void error2;
|
|
3307
|
+
return null;
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
if (!isRecord(parsed)) return null;
|
|
3311
|
+
try {
|
|
3312
|
+
const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
|
|
3313
|
+
if (establishedShape) return establishedShape;
|
|
3314
|
+
} catch (error2) {
|
|
3315
|
+
void error2;
|
|
3316
|
+
}
|
|
3317
|
+
const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord);
|
|
3318
|
+
for (const event of candidates) {
|
|
3319
|
+
if (event.type !== "session.error") continue;
|
|
3320
|
+
const properties = [event.properties, event.data, event].find(isRecord);
|
|
3321
|
+
if (!properties) continue;
|
|
3322
|
+
const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
|
|
3323
|
+
if (!sessionId) continue;
|
|
3324
|
+
return {
|
|
3325
|
+
sessionId,
|
|
3326
|
+
reason: sessionErrorReason(properties.error ?? properties)
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
return null;
|
|
3330
|
+
}
|
|
3331
|
+
async function readV2SessionErrorStream(client, options) {
|
|
3332
|
+
let reader = null;
|
|
3333
|
+
try {
|
|
3334
|
+
const response = await client.request("/api/event", {
|
|
3335
|
+
headers: { accept: "text/event-stream" },
|
|
3336
|
+
signal: options.signal
|
|
3337
|
+
});
|
|
3338
|
+
if (!response.ok || !response.body) {
|
|
3339
|
+
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
3340
|
+
}
|
|
3341
|
+
reader = response.body.getReader();
|
|
3342
|
+
const decoder = new TextDecoder();
|
|
3343
|
+
let buffer = "";
|
|
3344
|
+
const processLine = (line) => {
|
|
3345
|
+
const trimmed = line.trimEnd();
|
|
3346
|
+
if (!trimmed.startsWith("data:")) return;
|
|
3347
|
+
const event = adaptV2SessionErrorEvent(trimmed.slice("data:".length).replace(/^ /, ""));
|
|
3348
|
+
if (event) options.onSessionError(event);
|
|
3349
|
+
};
|
|
3350
|
+
while (true) {
|
|
3351
|
+
const { done, value } = await reader.read();
|
|
3352
|
+
if (done) return { reason: "ended" };
|
|
3353
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3354
|
+
const lines = buffer.split("\n");
|
|
3355
|
+
buffer = lines.pop() ?? "";
|
|
3356
|
+
for (const line of lines) processLine(line);
|
|
3357
|
+
}
|
|
3358
|
+
} catch (error2) {
|
|
3359
|
+
if (options.signal.aborted) return { reason: "aborted" };
|
|
3360
|
+
return {
|
|
3361
|
+
reason: "unavailable",
|
|
3362
|
+
detail: error2 instanceof Error ? error2.message : String(error2)
|
|
3363
|
+
};
|
|
3364
|
+
} finally {
|
|
3365
|
+
if (reader) void reader.cancel().catch(() => void 0);
|
|
3366
|
+
}
|
|
2767
3367
|
}
|
|
2768
3368
|
|
|
2769
3369
|
// src/lib/opencode/session.ts
|
|
3370
|
+
var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
2770
3371
|
function timedFetch(input, init) {
|
|
2771
3372
|
return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
|
|
2772
3373
|
}
|
|
3374
|
+
function requestWithClient(port, client, path, init, options) {
|
|
3375
|
+
return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
|
|
3376
|
+
}
|
|
2773
3377
|
function opencodeBase(port) {
|
|
2774
3378
|
return `http://127.0.0.1:${port}`;
|
|
2775
3379
|
}
|
|
2776
|
-
async function getOpenCodeDirectory(port) {
|
|
3380
|
+
async function getOpenCodeDirectory(port, client) {
|
|
2777
3381
|
try {
|
|
2778
|
-
const res = await
|
|
3382
|
+
const res = await requestWithClient(port, client, "/path");
|
|
2779
3383
|
if (!res.ok) return null;
|
|
2780
3384
|
const body = await res.json();
|
|
2781
3385
|
const dir = typeof body.directory === "string" && body.directory || typeof body.worktree === "string" && body.worktree || typeof body.path?.cwd === "string" && body.path.cwd || typeof body.path?.directory === "string" && body.path.directory || null;
|
|
2782
3386
|
return dir && dir.trim() ? dir.trim() : null;
|
|
2783
|
-
} catch {
|
|
3387
|
+
} catch (error2) {
|
|
3388
|
+
console.error(
|
|
3389
|
+
`[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3390
|
+
);
|
|
2784
3391
|
return null;
|
|
2785
3392
|
}
|
|
2786
3393
|
}
|
|
@@ -2824,16 +3431,48 @@ function isAssistantInFlight(m) {
|
|
|
2824
3431
|
if (completedOf(m) == null) return true;
|
|
2825
3432
|
return finishOf(m) === "tool-calls";
|
|
2826
3433
|
}
|
|
2827
|
-
async function getSessionMessages(port, sessionId) {
|
|
3434
|
+
async function getSessionMessages(port, sessionId, client) {
|
|
2828
3435
|
try {
|
|
2829
|
-
const
|
|
3436
|
+
const path = `/session/${sessionId}/message`;
|
|
3437
|
+
const res = await requestWithClient(port, client, path);
|
|
2830
3438
|
if (!res.ok) return null;
|
|
2831
3439
|
const body = await res.json();
|
|
2832
3440
|
return Array.isArray(body) ? body : null;
|
|
2833
|
-
} catch {
|
|
3441
|
+
} catch (error2) {
|
|
3442
|
+
console.error(
|
|
3443
|
+
`[getSessionMessages] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3444
|
+
);
|
|
2834
3445
|
return null;
|
|
2835
3446
|
}
|
|
2836
3447
|
}
|
|
3448
|
+
async function fetchSessionMessages(port, sessionId, client) {
|
|
3449
|
+
const response = await requestWithClient(port, client, `/session/${sessionId}/message`);
|
|
3450
|
+
if (!response.ok) return null;
|
|
3451
|
+
const body = await response.json();
|
|
3452
|
+
return Array.isArray(body) ? body : null;
|
|
3453
|
+
}
|
|
3454
|
+
async function pollSessionMessagesForRedrive(port, sessionId, client) {
|
|
3455
|
+
try {
|
|
3456
|
+
const response = await requestWithClient(
|
|
3457
|
+
port,
|
|
3458
|
+
client,
|
|
3459
|
+
`/session/${sessionId}/message`,
|
|
3460
|
+
void 0,
|
|
3461
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
3462
|
+
);
|
|
3463
|
+
if (!response.ok) {
|
|
3464
|
+
return { ok: false, status: response.status, body: await response.text(), malformed: false };
|
|
3465
|
+
}
|
|
3466
|
+
const body = await response.json();
|
|
3467
|
+
if (!Array.isArray(body)) return { ok: false, status: null, body: "", malformed: true };
|
|
3468
|
+
return { ok: true, messages: body };
|
|
3469
|
+
} catch (error2) {
|
|
3470
|
+
console.error(
|
|
3471
|
+
`[pollSessionMessagesForRedrive] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3472
|
+
);
|
|
3473
|
+
return { ok: false, status: null, body: "", malformed: false };
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
2837
3476
|
function isSessionActivelyGenerating(messages) {
|
|
2838
3477
|
if (!messages || messages.length === 0) return false;
|
|
2839
3478
|
const last = messages[messages.length - 1];
|
|
@@ -2854,27 +3493,37 @@ function sessionLastActivityMs(session) {
|
|
|
2854
3493
|
}
|
|
2855
3494
|
return null;
|
|
2856
3495
|
}
|
|
2857
|
-
async function listSessions(port) {
|
|
3496
|
+
async function listSessions(port, client) {
|
|
3497
|
+
if (client?.version === "v2") return listV2Sessions(client);
|
|
2858
3498
|
try {
|
|
2859
|
-
const res = await
|
|
3499
|
+
const res = await requestWithClient(port, client, "/session");
|
|
2860
3500
|
if (!res.ok) return null;
|
|
2861
3501
|
const body = await res.json();
|
|
2862
3502
|
return Array.isArray(body) ? body : null;
|
|
2863
|
-
} catch {
|
|
3503
|
+
} catch (error2) {
|
|
3504
|
+
console.error(
|
|
3505
|
+
`[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3506
|
+
);
|
|
2864
3507
|
return null;
|
|
2865
3508
|
}
|
|
2866
3509
|
}
|
|
2867
|
-
async function deleteSession(port, id) {
|
|
3510
|
+
async function deleteSession(port, id, client) {
|
|
3511
|
+
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
2868
3512
|
try {
|
|
2869
|
-
const res = await
|
|
3513
|
+
const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
|
|
2870
3514
|
return res.status >= 200 && res.status < 300;
|
|
2871
|
-
} catch {
|
|
3515
|
+
} catch (error2) {
|
|
3516
|
+
console.error(
|
|
3517
|
+
`[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3518
|
+
);
|
|
2872
3519
|
return false;
|
|
2873
3520
|
}
|
|
2874
3521
|
}
|
|
2875
|
-
async function sessionExists(port, id) {
|
|
3522
|
+
async function sessionExists(port, id, client) {
|
|
2876
3523
|
try {
|
|
2877
|
-
const res = await
|
|
3524
|
+
const res = await requestWithClient(port, client, `/session/${id}`, void 0, {
|
|
3525
|
+
allowStatuses: [404]
|
|
3526
|
+
});
|
|
2878
3527
|
if (res.status >= 200 && res.status < 300) return true;
|
|
2879
3528
|
if (res.status === 404) return false;
|
|
2880
3529
|
return null;
|
|
@@ -2882,9 +3531,22 @@ async function sessionExists(port, id) {
|
|
|
2882
3531
|
return null;
|
|
2883
3532
|
}
|
|
2884
3533
|
}
|
|
2885
|
-
async function
|
|
3534
|
+
async function getOpenCodeSession(port, id, client) {
|
|
2886
3535
|
try {
|
|
2887
|
-
const
|
|
3536
|
+
const response = await requestWithClient(port, client, `/session/${id}`);
|
|
3537
|
+
const body = await response.json();
|
|
3538
|
+
return body && typeof body === "object" && !Array.isArray(body) ? body : null;
|
|
3539
|
+
} catch (error2) {
|
|
3540
|
+
console.error(
|
|
3541
|
+
`[getOpenCodeSession] GET /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3542
|
+
);
|
|
3543
|
+
return null;
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
async function getSessionStatuses(port, client) {
|
|
3547
|
+
if (client?.version === "v2") return null;
|
|
3548
|
+
try {
|
|
3549
|
+
const res = await requestWithClient(port, client, "/session/status");
|
|
2888
3550
|
if (!res.ok) {
|
|
2889
3551
|
console.error(
|
|
2890
3552
|
`[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
|
|
@@ -2906,22 +3568,28 @@ async function getSessionStatuses(port) {
|
|
|
2906
3568
|
return null;
|
|
2907
3569
|
}
|
|
2908
3570
|
}
|
|
2909
|
-
async function isSessionOngoing(port, id) {
|
|
2910
|
-
|
|
3571
|
+
async function isSessionOngoing(port, id, client) {
|
|
3572
|
+
if (client?.version === "v2") return isV2SessionOngoing(client, id);
|
|
3573
|
+
const map = await getSessionStatuses(port, client);
|
|
2911
3574
|
if (map == null) return null;
|
|
2912
3575
|
const entry = map[id];
|
|
2913
3576
|
return entry != null && entry.type !== "idle";
|
|
2914
3577
|
}
|
|
2915
|
-
async function createOpenCodeSession(port, directory) {
|
|
2916
|
-
const
|
|
2917
|
-
if (directory && directory.trim())
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
3578
|
+
async function createOpenCodeSession(port, directory, client) {
|
|
3579
|
+
const path = new URL(`${opencodeBase(port)}/session`);
|
|
3580
|
+
if (directory && directory.trim()) path.searchParams.set("directory", directory.trim());
|
|
3581
|
+
const requestPath = `${path.pathname}${path.search}`;
|
|
3582
|
+
const response = await requestWithClient(
|
|
3583
|
+
port,
|
|
3584
|
+
client,
|
|
3585
|
+
requestPath,
|
|
3586
|
+
{
|
|
3587
|
+
method: "POST",
|
|
3588
|
+
headers: { "Content-Type": "application/json" },
|
|
3589
|
+
body: JSON.stringify({})
|
|
3590
|
+
},
|
|
3591
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
3592
|
+
);
|
|
2925
3593
|
if (!response.ok) {
|
|
2926
3594
|
const text = await response.text().catch(() => "");
|
|
2927
3595
|
throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ""}`);
|
|
@@ -2929,10 +3597,16 @@ async function createOpenCodeSession(port, directory) {
|
|
|
2929
3597
|
const data = await response.json();
|
|
2930
3598
|
return data.id;
|
|
2931
3599
|
}
|
|
2932
|
-
async function getModelAttachmentCapability(port, model) {
|
|
3600
|
+
async function getModelAttachmentCapability(port, model, client) {
|
|
2933
3601
|
const { model: baseModel } = splitModelVariant(model);
|
|
3602
|
+
if (client?.version === "v2") {
|
|
3603
|
+
console.error(
|
|
3604
|
+
`[getModelAttachmentCapability] V2 provider capabilities are unavailable; using text-only fallback (port ${port})`
|
|
3605
|
+
);
|
|
3606
|
+
return null;
|
|
3607
|
+
}
|
|
2934
3608
|
try {
|
|
2935
|
-
const res = await
|
|
3609
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
2936
3610
|
if (!res.ok) {
|
|
2937
3611
|
console.error(
|
|
2938
3612
|
`[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
@@ -3052,19 +3726,43 @@ function applyModelOptions(body, options) {
|
|
|
3052
3726
|
}
|
|
3053
3727
|
if (variant) body.variant = variant;
|
|
3054
3728
|
}
|
|
3729
|
+
async function listOpenCodeQuestions(port, client) {
|
|
3730
|
+
try {
|
|
3731
|
+
const response = await requestWithClient(port, client, "/question");
|
|
3732
|
+
const body = await response.json();
|
|
3733
|
+
return Array.isArray(body) ? body : null;
|
|
3734
|
+
} catch (error2) {
|
|
3735
|
+
console.error(
|
|
3736
|
+
`[listOpenCodeQuestions] GET /question failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3737
|
+
);
|
|
3738
|
+
return null;
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
async function listOpenCodePermissions(port, client) {
|
|
3742
|
+
try {
|
|
3743
|
+
const response = await requestWithClient(port, client, "/permission");
|
|
3744
|
+
const body = await response.json();
|
|
3745
|
+
return Array.isArray(body) ? body : null;
|
|
3746
|
+
} catch (error2) {
|
|
3747
|
+
console.error(
|
|
3748
|
+
`[listOpenCodePermissions] GET /permission failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3749
|
+
);
|
|
3750
|
+
return null;
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3055
3753
|
function messageText(m) {
|
|
3056
3754
|
if (!m || !Array.isArray(m.parts)) return "";
|
|
3057
3755
|
return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
3058
3756
|
}
|
|
3059
|
-
async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
3060
|
-
const before = await getSessionMessages(port, sessionId);
|
|
3757
|
+
async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
|
|
3758
|
+
const before = await getSessionMessages(port, sessionId, client);
|
|
3061
3759
|
const knownUserIds = new Set(
|
|
3062
3760
|
(before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
|
|
3063
3761
|
);
|
|
3064
3762
|
const parts = [{ type: "text", text: content }];
|
|
3065
3763
|
let pendingOutcomes = null;
|
|
3066
3764
|
if (attachments && attachments.inputs.length > 0) {
|
|
3067
|
-
const capable = await getModelAttachmentCapability(port, options?.model);
|
|
3765
|
+
const capable = await getModelAttachmentCapability(port, options?.model, client);
|
|
3068
3766
|
const {
|
|
3069
3767
|
parts: fileParts,
|
|
3070
3768
|
outcomes,
|
|
@@ -3077,11 +3775,17 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
3077
3775
|
parts
|
|
3078
3776
|
};
|
|
3079
3777
|
applyModelOptions(body, options);
|
|
3080
|
-
const res = await
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3778
|
+
const res = await requestWithClient(
|
|
3779
|
+
port,
|
|
3780
|
+
client,
|
|
3781
|
+
`/session/${sessionId}/prompt_async`,
|
|
3782
|
+
{
|
|
3783
|
+
method: "POST",
|
|
3784
|
+
headers: { "Content-Type": "application/json" },
|
|
3785
|
+
body: JSON.stringify(body)
|
|
3786
|
+
},
|
|
3787
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
3788
|
+
);
|
|
3085
3789
|
if (res.status < 200 || res.status >= 300) {
|
|
3086
3790
|
const text = await res.text().catch(() => "");
|
|
3087
3791
|
const { variant } = splitModelVariant(options?.model);
|
|
@@ -3092,7 +3796,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
3092
3796
|
const READ_BACK_ATTEMPTS = 5;
|
|
3093
3797
|
const READ_BACK_DELAY_MS = 150;
|
|
3094
3798
|
for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
|
|
3095
|
-
const after = await getSessionMessages(port, sessionId);
|
|
3799
|
+
const after = await getSessionMessages(port, sessionId, client);
|
|
3096
3800
|
if (after) {
|
|
3097
3801
|
let best = null;
|
|
3098
3802
|
for (const m of after) {
|
|
@@ -3195,7 +3899,7 @@ function collectSubagentSessions(messages, userMessageId) {
|
|
|
3195
3899
|
}
|
|
3196
3900
|
return refs;
|
|
3197
3901
|
}
|
|
3198
|
-
function
|
|
3902
|
+
function finiteNumber2(value) {
|
|
3199
3903
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3200
3904
|
}
|
|
3201
3905
|
function taskCallModel(value) {
|
|
@@ -3224,8 +3928,8 @@ function collectTaskCalls(messages, userMessageId) {
|
|
|
3224
3928
|
parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
|
|
3225
3929
|
model: taskCallModel(metadata?.model),
|
|
3226
3930
|
status: part.state.status ?? "unknown",
|
|
3227
|
-
timeStart:
|
|
3228
|
-
timeEnd:
|
|
3931
|
+
timeStart: finiteNumber2(part.state.time?.start),
|
|
3932
|
+
timeEnd: finiteNumber2(part.state.time?.end)
|
|
3229
3933
|
});
|
|
3230
3934
|
}
|
|
3231
3935
|
}
|
|
@@ -3240,7 +3944,7 @@ function attributeTaskCallUsage(messages, windows) {
|
|
|
3240
3944
|
const unattributed = [];
|
|
3241
3945
|
for (const message of messages ?? []) {
|
|
3242
3946
|
if (roleOf(message) !== "assistant") continue;
|
|
3243
|
-
const created =
|
|
3947
|
+
const created = finiteNumber2(createdOf(message));
|
|
3244
3948
|
const matching = created === null ? [] : eligibleWindows.filter(
|
|
3245
3949
|
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
3246
3950
|
);
|
|
@@ -3481,9 +4185,51 @@ function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageI
|
|
|
3481
4185
|
}
|
|
3482
4186
|
return hasLaterUser && hasStartedLaterUser;
|
|
3483
4187
|
}
|
|
3484
|
-
async function hasAnyConfiguredProvider(port) {
|
|
4188
|
+
async function hasAnyConfiguredProvider(port, client) {
|
|
4189
|
+
if (client?.version === "v2") {
|
|
4190
|
+
const directory = await getOpenCodeDirectoryV2(client);
|
|
4191
|
+
if (!directory) {
|
|
4192
|
+
console.error(
|
|
4193
|
+
`[hasAnyConfiguredProvider] V2 working directory was unavailable (port ${port})`
|
|
4194
|
+
);
|
|
4195
|
+
return null;
|
|
4196
|
+
}
|
|
4197
|
+
const path = `/api/integration?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
|
|
4198
|
+
try {
|
|
4199
|
+
const res = await client.request(path);
|
|
4200
|
+
if (!res.ok) {
|
|
4201
|
+
console.error(
|
|
4202
|
+
`[hasAnyConfiguredProvider] GET ${path} returned HTTP ${res.status} (port ${port})`
|
|
4203
|
+
);
|
|
4204
|
+
return null;
|
|
4205
|
+
}
|
|
4206
|
+
const body = await res.json();
|
|
4207
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
|
|
4208
|
+
console.error(
|
|
4209
|
+
`[hasAnyConfiguredProvider] GET ${path} body had no integration data array (port ${port})`
|
|
4210
|
+
);
|
|
4211
|
+
return null;
|
|
4212
|
+
}
|
|
4213
|
+
for (const integration of body.data) {
|
|
4214
|
+
if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
4215
|
+
console.error(
|
|
4216
|
+
`[hasAnyConfiguredProvider] GET ${path} body contained an invalid integration (port ${port})`
|
|
4217
|
+
);
|
|
4218
|
+
return null;
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
return body.data.some(
|
|
4222
|
+
(integration) => Array.isArray(integration.connections) && integration.connections.length > 0
|
|
4223
|
+
);
|
|
4224
|
+
} catch (error2) {
|
|
4225
|
+
console.error(
|
|
4226
|
+
`[hasAnyConfiguredProvider] GET ${path} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4227
|
+
);
|
|
4228
|
+
return null;
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
3485
4231
|
try {
|
|
3486
|
-
const res = await
|
|
4232
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
3487
4233
|
if (!res.ok) {
|
|
3488
4234
|
console.error(
|
|
3489
4235
|
`[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
@@ -3512,7 +4258,7 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3512
4258
|
return null;
|
|
3513
4259
|
}
|
|
3514
4260
|
}
|
|
3515
|
-
function
|
|
4261
|
+
function sessionErrorReason2(error2) {
|
|
3516
4262
|
const record = typeof error2 === "object" && error2 !== null ? error2 : null;
|
|
3517
4263
|
const data = record?.data;
|
|
3518
4264
|
const dataRecord = typeof data === "object" && data !== null ? data : null;
|
|
@@ -3542,16 +4288,20 @@ function parseSessionErrorFrame(data) {
|
|
|
3542
4288
|
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
3543
4289
|
return {
|
|
3544
4290
|
sessionId,
|
|
3545
|
-
reason:
|
|
4291
|
+
reason: sessionErrorReason2(propertiesRecord.error)
|
|
3546
4292
|
};
|
|
3547
4293
|
}
|
|
3548
|
-
async function readSessionErrorStream(port, options) {
|
|
4294
|
+
async function readSessionErrorStream(port, options, client) {
|
|
4295
|
+
if (client?.version === "v2") return readV2SessionErrorStream(client, options);
|
|
3549
4296
|
let reader = null;
|
|
3550
4297
|
try {
|
|
3551
|
-
const response = await
|
|
4298
|
+
const response = await (client?.request("/event", {
|
|
3552
4299
|
headers: { accept: "text/event-stream" },
|
|
3553
4300
|
signal: options.signal
|
|
3554
|
-
})
|
|
4301
|
+
}) ?? fetch(`${opencodeBase(port)}/event`, {
|
|
4302
|
+
headers: { accept: "text/event-stream" },
|
|
4303
|
+
signal: options.signal
|
|
4304
|
+
}));
|
|
3555
4305
|
if (!response.ok || !response.body) {
|
|
3556
4306
|
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
3557
4307
|
}
|
|
@@ -3582,9 +4332,10 @@ async function readSessionErrorStream(port, options) {
|
|
|
3582
4332
|
if (reader) void reader.cancel().catch(() => void 0);
|
|
3583
4333
|
}
|
|
3584
4334
|
}
|
|
3585
|
-
async function reloadProviderCache(port) {
|
|
4335
|
+
async function reloadProviderCache(port, client) {
|
|
4336
|
+
if (client?.version === "v2") return;
|
|
3586
4337
|
try {
|
|
3587
|
-
const res = await
|
|
4338
|
+
const res = await requestWithClient(port, client, "/config", {
|
|
3588
4339
|
method: "PATCH",
|
|
3589
4340
|
headers: { "Content-Type": "application/json" },
|
|
3590
4341
|
body: JSON.stringify({})
|
|
@@ -3962,10 +4713,11 @@ var STRIP_RES = /* @__PURE__ */ new Set([
|
|
|
3962
4713
|
"content-length"
|
|
3963
4714
|
]);
|
|
3964
4715
|
var StreamForwarder = class {
|
|
3965
|
-
constructor(ws, port, callbacks = {}) {
|
|
4716
|
+
constructor(ws, port, callbacks = {}, options = {}) {
|
|
3966
4717
|
this.ws = ws;
|
|
3967
4718
|
this.port = port;
|
|
3968
4719
|
this.callbacks = callbacks;
|
|
4720
|
+
this.options = options;
|
|
3969
4721
|
}
|
|
3970
4722
|
inflight = /* @__PURE__ */ new Map();
|
|
3971
4723
|
/**
|
|
@@ -4049,7 +4801,15 @@ var StreamForwarder = class {
|
|
|
4049
4801
|
}
|
|
4050
4802
|
const fwdHeaders = {};
|
|
4051
4803
|
for (const [k, v] of Object.entries(headers ?? {})) {
|
|
4052
|
-
|
|
4804
|
+
const lower = k.toLowerCase();
|
|
4805
|
+
if (STRIP_REQ.has(lower)) continue;
|
|
4806
|
+
if (this.options.openCodePassword !== void 0 && this.options.openCodePassword !== null) {
|
|
4807
|
+
if (lower === "authorization") continue;
|
|
4808
|
+
}
|
|
4809
|
+
fwdHeaders[k] = v;
|
|
4810
|
+
}
|
|
4811
|
+
if (this.options.openCodePassword !== void 0 && this.options.openCodePassword !== null) {
|
|
4812
|
+
fwdHeaders.Authorization = buildOpenCodeBasicAuthHeader(this.options.openCodePassword);
|
|
4053
4813
|
}
|
|
4054
4814
|
this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });
|
|
4055
4815
|
const body = bodyPromise ? await bodyPromise : void 0;
|
|
@@ -4162,6 +4922,7 @@ function connectTunnel(options) {
|
|
|
4162
4922
|
agentId,
|
|
4163
4923
|
authHeader,
|
|
4164
4924
|
port,
|
|
4925
|
+
openCodePassword,
|
|
4165
4926
|
onConnected,
|
|
4166
4927
|
onDisconnected,
|
|
4167
4928
|
onError,
|
|
@@ -4179,11 +4940,16 @@ function connectTunnel(options) {
|
|
|
4179
4940
|
Authorization: authHeader
|
|
4180
4941
|
}
|
|
4181
4942
|
});
|
|
4182
|
-
const forwarder = new StreamForwarder(
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4943
|
+
const forwarder = new StreamForwarder(
|
|
4944
|
+
ws,
|
|
4945
|
+
port,
|
|
4946
|
+
{
|
|
4947
|
+
onHead: () => onResponse?.(),
|
|
4948
|
+
onDrainPing: () => onDrainPing?.(),
|
|
4949
|
+
onUsageRearmPing: () => onUsageRearmPing?.()
|
|
4950
|
+
},
|
|
4951
|
+
{ openCodePassword }
|
|
4952
|
+
);
|
|
4187
4953
|
const connectionTimeout = setTimeout(() => {
|
|
4188
4954
|
ws.close();
|
|
4189
4955
|
reject(new Error("Connection timeout"));
|
|
@@ -4326,6 +5092,7 @@ var RunnerConnection = class {
|
|
|
4326
5092
|
agentId: this.resolvedAgentId,
|
|
4327
5093
|
authHeader: this.opts.getAuthHeader(),
|
|
4328
5094
|
port: this.opts.port,
|
|
5095
|
+
openCodePassword: this.opts.openCodePassword,
|
|
4329
5096
|
onConnected: (agentId) => {
|
|
4330
5097
|
this.reconnectAttempt = 0;
|
|
4331
5098
|
this.reconnecting = false;
|
|
@@ -4506,33 +5273,73 @@ function parseCodexUsageHeaders(headers) {
|
|
|
4506
5273
|
function normalizeProbeModel(model) {
|
|
4507
5274
|
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
4508
5275
|
}
|
|
4509
|
-
|
|
5276
|
+
function isRecord2(value) {
|
|
5277
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5278
|
+
}
|
|
5279
|
+
function unsupportedProbeModels(reason, port) {
|
|
5280
|
+
console.error(`[resolveProbeModels] ${reason} (port ${port})`);
|
|
5281
|
+
return { status: "unsupported", reason };
|
|
5282
|
+
}
|
|
5283
|
+
async function resolveV1ProbeModels(client, port) {
|
|
4510
5284
|
try {
|
|
4511
|
-
const
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
if (!res.ok) {
|
|
4516
|
-
console.error(
|
|
4517
|
-
`[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
4518
|
-
);
|
|
4519
|
-
return [];
|
|
5285
|
+
const response = await client.request("/config/providers");
|
|
5286
|
+
const body = await response.json();
|
|
5287
|
+
if (!isRecord2(body) || !Array.isArray(body.providers)) {
|
|
5288
|
+
return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
|
|
4520
5289
|
}
|
|
4521
|
-
const
|
|
4522
|
-
|
|
4523
|
-
|
|
5290
|
+
const provider = body.providers.find(
|
|
5291
|
+
(candidate) => isRecord2(candidate) && candidate.id === "openai"
|
|
5292
|
+
);
|
|
5293
|
+
if (!provider || !isRecord2(provider.models)) return { status: "supported", models: [] };
|
|
5294
|
+
const defaults2 = isRecord2(body.default) ? body.default : void 0;
|
|
4524
5295
|
const candidates = [
|
|
4525
|
-
...typeof
|
|
5296
|
+
...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
|
|
4526
5297
|
...Object.keys(provider.models)
|
|
4527
5298
|
].map(normalizeProbeModel);
|
|
4528
|
-
return [...new Set(candidates)].slice(0, 4);
|
|
5299
|
+
return { status: "supported", models: [...new Set(candidates)].slice(0, 4) };
|
|
4529
5300
|
} catch (err) {
|
|
4530
|
-
|
|
4531
|
-
`
|
|
5301
|
+
return unsupportedProbeModels(
|
|
5302
|
+
`V1 GET /config/providers failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
5303
|
+
port
|
|
5304
|
+
);
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
async function resolveV2ProbeModels(client, port) {
|
|
5308
|
+
const directory = await getOpenCodeDirectoryV2(client);
|
|
5309
|
+
if (!directory) {
|
|
5310
|
+
return unsupportedProbeModels("V2 working directory could not be verified", port);
|
|
5311
|
+
}
|
|
5312
|
+
const path = `/api/provider?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
|
|
5313
|
+
try {
|
|
5314
|
+
const response = await client.request(path);
|
|
5315
|
+
const body = await response.json();
|
|
5316
|
+
if (!isRecord2(body) || !Array.isArray(body.data)) {
|
|
5317
|
+
return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
|
|
5318
|
+
}
|
|
5319
|
+
const provider = body.data.find(
|
|
5320
|
+
(candidate) => isRecord2(candidate) && candidate.id === "openai"
|
|
5321
|
+
);
|
|
5322
|
+
if (!provider) return { status: "supported", models: [] };
|
|
5323
|
+
if (!isRecord2(provider.models)) {
|
|
5324
|
+
return unsupportedProbeModels(
|
|
5325
|
+
"V2 provider response has no safe OpenAI model catalogue",
|
|
5326
|
+
port
|
|
5327
|
+
);
|
|
5328
|
+
}
|
|
5329
|
+
return {
|
|
5330
|
+
status: "supported",
|
|
5331
|
+
models: [...new Set(Object.keys(provider.models).map(normalizeProbeModel))].slice(0, 4)
|
|
5332
|
+
};
|
|
5333
|
+
} catch (err) {
|
|
5334
|
+
return unsupportedProbeModels(
|
|
5335
|
+
`V2 GET ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
5336
|
+
port
|
|
4532
5337
|
);
|
|
4533
|
-
return [];
|
|
4534
5338
|
}
|
|
4535
5339
|
}
|
|
5340
|
+
async function resolveProbeModels(port, client = createOpenCodeClient({ port, version: "v1" })) {
|
|
5341
|
+
return client.version === "v2" ? resolveV2ProbeModels(client, port) : resolveV1ProbeModels(client, port);
|
|
5342
|
+
}
|
|
4536
5343
|
function hasPrimaryHeaders(headers) {
|
|
4537
5344
|
return [
|
|
4538
5345
|
"x-codex-primary-used-percent",
|
|
@@ -4540,7 +5347,7 @@ function hasPrimaryHeaders(headers) {
|
|
|
4540
5347
|
"x-codex-primary-reset-at"
|
|
4541
5348
|
].some((name) => headers.has(name));
|
|
4542
5349
|
}
|
|
4543
|
-
async function getOpenAiUsage(port) {
|
|
5350
|
+
async function getOpenAiUsage(port, client) {
|
|
4544
5351
|
const credentials2 = readOpenCodeChatGptCredentials();
|
|
4545
5352
|
if (!credentials2) {
|
|
4546
5353
|
throw new OpenAiUsageError(
|
|
@@ -4555,12 +5362,16 @@ async function getOpenAiUsage(port) {
|
|
|
4555
5362
|
);
|
|
4556
5363
|
}
|
|
4557
5364
|
const subscription = parseChatGptIdentity(credentials2.accessToken);
|
|
4558
|
-
const
|
|
4559
|
-
if (models.length === 0) {
|
|
4560
|
-
|
|
5365
|
+
const lookup = await resolveProbeModels(port, client);
|
|
5366
|
+
if (lookup.status === "unsupported" || lookup.models.length === 0) {
|
|
5367
|
+
const detail = lookup.status === "unsupported" ? ` ${lookup.reason}.` : "";
|
|
5368
|
+
throw new OpenAiUsageError(
|
|
5369
|
+
`No supported OpenAI probe model is available.${detail}`,
|
|
5370
|
+
"no_probe_model"
|
|
5371
|
+
);
|
|
4561
5372
|
}
|
|
4562
5373
|
let lastStatus;
|
|
4563
|
-
for (const model of models) {
|
|
5374
|
+
for (const model of lookup.models) {
|
|
4564
5375
|
let res;
|
|
4565
5376
|
try {
|
|
4566
5377
|
res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
|
|
@@ -5379,6 +6190,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5379
6190
|
maxActiveSessions;
|
|
5380
6191
|
watcherStallMs;
|
|
5381
6192
|
wedgeWarningIntervalMs;
|
|
6193
|
+
openCodeClient;
|
|
5382
6194
|
/** Cache of conversationId → opencode sessionId. */
|
|
5383
6195
|
sessions = /* @__PURE__ */ new Map();
|
|
5384
6196
|
/**
|
|
@@ -5497,10 +6309,15 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5497
6309
|
* does NOT suppress DONE delivery. A row parked here whose reply later COMPLETES
|
|
5498
6310
|
* in opencode must still be delivered via `markDone` on the next drain — so
|
|
5499
6311
|
* `readoptOne` computes `state` FIRST and this set is checked only on the
|
|
5500
|
-
* non-done path. It is cleared once the row leaves
|
|
5501
|
-
*
|
|
6312
|
+
* non-done path. It is cleared once the row leaves both processing and pending
|
|
6313
|
+
* lists, so it can never leak. A V2 prompt
|
|
6314
|
+
* acknowledgement with no usable id also uses this fence: OpenCode accepted the
|
|
6315
|
+
* turn, but there is no safe id to watch, so a failed `markFailed` report must not
|
|
6316
|
+
* allow the pending row to post the prompt again.
|
|
5502
6317
|
*/
|
|
5503
6318
|
dontRedispatch = /* @__PURE__ */ new Set();
|
|
6319
|
+
/** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
|
|
6320
|
+
pendingMessageIds = /* @__PURE__ */ new Set();
|
|
5504
6321
|
/**
|
|
5505
6322
|
* "markDone for this row is TERMINALLY undeliverable" (Bug 4). Set ONLY when a
|
|
5506
6323
|
* re-adopted DONE row's `markDone` returned a terminal 4xx (a status that will
|
|
@@ -5623,7 +6440,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5623
6440
|
*/
|
|
5624
6441
|
attachmentsSkippedSignalled = /* @__PURE__ */ new Set();
|
|
5625
6442
|
/**
|
|
5626
|
-
* Cache of the opencode root directory
|
|
6443
|
+
* Cache of the opencode root directory from the selected client's location lookup.
|
|
6444
|
+
* Resolved lazily on
|
|
5627
6445
|
* first session creation so drain-created sessions are rooted at the project
|
|
5628
6446
|
* directory and thus visible in `opencode web`'s session list. `undefined` =
|
|
5629
6447
|
* not yet resolved; `null` = resolved-but-unavailable (don't keep retrying).
|
|
@@ -5717,6 +6535,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5717
6535
|
config.fetchImpl ?? fetch,
|
|
5718
6536
|
config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
|
|
5719
6537
|
);
|
|
6538
|
+
this.openCodeClient = config.openCodeClient ?? createOpenCodeClient({
|
|
6539
|
+
port: config.port,
|
|
6540
|
+
version: "v1",
|
|
6541
|
+
fetchImpl: config.fetchImpl
|
|
6542
|
+
});
|
|
5720
6543
|
this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5721
6544
|
this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
|
|
5722
6545
|
this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
|
|
@@ -5728,9 +6551,39 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5728
6551
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
5729
6552
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
5730
6553
|
}
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
6554
|
+
get isV2() {
|
|
6555
|
+
return this.openCodeClient.version === "v2";
|
|
6556
|
+
}
|
|
6557
|
+
async getSessionMessages(sessionId) {
|
|
6558
|
+
return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6559
|
+
}
|
|
6560
|
+
async getSubagentSessionMessages(sessionId) {
|
|
6561
|
+
return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : getSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6562
|
+
}
|
|
6563
|
+
async getTelemetrySubagentSessionMessages(sessionId) {
|
|
6564
|
+
if (this.isV2) return getV2SessionMessages(this.openCodeClient, sessionId);
|
|
6565
|
+
return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6566
|
+
}
|
|
6567
|
+
async listSessions() {
|
|
6568
|
+
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient);
|
|
6569
|
+
}
|
|
6570
|
+
async sessionExists(sessionId) {
|
|
6571
|
+
return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
|
|
6572
|
+
}
|
|
6573
|
+
async isSessionOngoing(sessionId) {
|
|
6574
|
+
return this.isV2 ? isV2SessionOngoing(this.openCodeClient, sessionId) : isSessionOngoing(this.port, sessionId, this.openCodeClient);
|
|
6575
|
+
}
|
|
6576
|
+
async getOpenCodeDirectory() {
|
|
6577
|
+
return this.isV2 ? getOpenCodeDirectoryV2(this.openCodeClient) : getOpenCodeDirectory(this.port, this.openCodeClient);
|
|
6578
|
+
}
|
|
6579
|
+
async createOpenCodeSession(directory) {
|
|
6580
|
+
return this.isV2 ? createV2Session(this.openCodeClient, directory) : createOpenCodeSession(this.port, directory, this.openCodeClient);
|
|
6581
|
+
}
|
|
6582
|
+
async hasAnyConfiguredProvider() {
|
|
6583
|
+
return hasAnyConfiguredProvider(this.port, this.openCodeClient);
|
|
6584
|
+
}
|
|
6585
|
+
async readOpenCodeSessionErrorStream(options) {
|
|
6586
|
+
return this.isV2 ? readV2SessionErrorStream(this.openCodeClient, options) : readSessionErrorStream(this.port, options, this.openCodeClient);
|
|
5734
6587
|
}
|
|
5735
6588
|
/**
|
|
5736
6589
|
* Drain all pending channel conversations once: poll → dispatch → register.
|
|
@@ -5808,6 +6661,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5808
6661
|
async runDrain() {
|
|
5809
6662
|
let dispatched = 0;
|
|
5810
6663
|
try {
|
|
6664
|
+
this.pendingMessageIds.clear();
|
|
5811
6665
|
const conversations = await this.getPendingConversations();
|
|
5812
6666
|
if (this.recycleRequestedFlag) {
|
|
5813
6667
|
this.stop();
|
|
@@ -6032,6 +6886,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6032
6886
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
6033
6887
|
this.ensureSessionErrorStream();
|
|
6034
6888
|
const messages = await this.getPendingMessages(conv.id);
|
|
6889
|
+
for (const message of messages) this.pendingMessageIds.add(message.id);
|
|
6035
6890
|
let dispatched = 0;
|
|
6036
6891
|
let skippedAlreadyDispatched = 0;
|
|
6037
6892
|
if (refusedSessionId && messages.length > 0) {
|
|
@@ -6045,6 +6900,15 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6045
6900
|
skippedAlreadyDispatched += 1;
|
|
6046
6901
|
continue;
|
|
6047
6902
|
}
|
|
6903
|
+
if (this.dontRedispatch.has(message.id)) {
|
|
6904
|
+
this.log({
|
|
6905
|
+
level: "warn",
|
|
6906
|
+
message: `Message ${message.id.slice(0, 8)} is fenced after an untrackable OpenCode turn \u2014 skipping re-dispatch`,
|
|
6907
|
+
conversation_id: conv.id,
|
|
6908
|
+
message_id: message.id
|
|
6909
|
+
});
|
|
6910
|
+
break;
|
|
6911
|
+
}
|
|
6048
6912
|
const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
|
|
6049
6913
|
if (effectiveOpencodeMessageId) {
|
|
6050
6914
|
const outcome = await this.resolveRedrive(
|
|
@@ -6073,15 +6937,55 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6073
6937
|
conversation_id: conv.id,
|
|
6074
6938
|
message_id: message.id
|
|
6075
6939
|
});
|
|
6076
|
-
const sendAttachments = this.buildSendAttachments(conv, message);
|
|
6077
|
-
|
|
6940
|
+
const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(conv, message);
|
|
6941
|
+
if (this.isV2 && message.attachments && message.attachments.length > 0) {
|
|
6942
|
+
this.signalAttachmentsSkipped(
|
|
6943
|
+
conv.id,
|
|
6944
|
+
message.id,
|
|
6945
|
+
message.attachments.map((attachment, index) => ({
|
|
6946
|
+
index,
|
|
6947
|
+
mime: attachment.mime,
|
|
6948
|
+
...attachment.filename ? { filename: attachment.filename } : {},
|
|
6949
|
+
status: "skipped"
|
|
6950
|
+
})),
|
|
6951
|
+
false
|
|
6952
|
+
);
|
|
6953
|
+
}
|
|
6954
|
+
opencodeMessageId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, message.content) : await this.dispatchLocked(
|
|
6078
6955
|
sessionId,
|
|
6079
|
-
() => sendPromptAsync(
|
|
6956
|
+
() => sendPromptAsync(
|
|
6957
|
+
this.port,
|
|
6958
|
+
sessionId,
|
|
6959
|
+
message.content,
|
|
6960
|
+
options,
|
|
6961
|
+
sendAttachments,
|
|
6962
|
+
this.openCodeClient
|
|
6963
|
+
)
|
|
6080
6964
|
);
|
|
6081
6965
|
} catch (err) {
|
|
6082
6966
|
if (err instanceof ChannelAuthError) throw err;
|
|
6967
|
+
if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
|
|
6968
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
6969
|
+
this.dontRedispatch.add(message.id);
|
|
6970
|
+
this.log({
|
|
6971
|
+
level: "error",
|
|
6972
|
+
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
|
|
6973
|
+
conversation_id: conv.id,
|
|
6974
|
+
message_id: message.id
|
|
6975
|
+
});
|
|
6976
|
+
await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
|
|
6977
|
+
this.log({
|
|
6978
|
+
level: "warn",
|
|
6979
|
+
message: `markFailed PATCH for V2 dispatch failure on message ${message.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
6980
|
+
conversation_id: conv.id,
|
|
6981
|
+
message_id: message.id
|
|
6982
|
+
});
|
|
6983
|
+
void this.postSignal(conv.id, message.id, "ack_untrackable");
|
|
6984
|
+
});
|
|
6985
|
+
break;
|
|
6986
|
+
}
|
|
6083
6987
|
this.dispatched.delete(message.id);
|
|
6084
|
-
const exists = await sessionExists(
|
|
6988
|
+
const exists = await this.sessionExists(sessionId);
|
|
6085
6989
|
if (exists === false) {
|
|
6086
6990
|
this.sessions.delete(conv.id);
|
|
6087
6991
|
this.log({
|
|
@@ -6130,6 +7034,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6130
7034
|
break;
|
|
6131
7035
|
}
|
|
6132
7036
|
if (opencodeMessageId === null) {
|
|
7037
|
+
if (this.isV2) {
|
|
7038
|
+
throw new Error("V2 prompt dispatch completed without an acknowledged message id");
|
|
7039
|
+
}
|
|
6133
7040
|
const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
|
|
6134
7041
|
if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
6135
7042
|
this.log({
|
|
@@ -6277,29 +7184,38 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6277
7184
|
*/
|
|
6278
7185
|
async pollSessionMessagesForRedrive(conv, message, sessionId) {
|
|
6279
7186
|
try {
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
7187
|
+
if (this.isV2) {
|
|
7188
|
+
const messages = await this.getSessionMessages(sessionId);
|
|
7189
|
+
if (messages === null) {
|
|
7190
|
+
this.log({
|
|
7191
|
+
level: "warn",
|
|
7192
|
+
message: `Re-drive: failed to poll V2 session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} \u2014 treating as unreadable this tick`,
|
|
7193
|
+
conversation_id: conv.id,
|
|
7194
|
+
message_id: message.id
|
|
7195
|
+
});
|
|
7196
|
+
return { ok: false, signature: null };
|
|
7197
|
+
}
|
|
7198
|
+
return { ok: true, messages };
|
|
6291
7199
|
}
|
|
6292
|
-
const
|
|
6293
|
-
|
|
7200
|
+
const polledV1 = await pollSessionMessagesForRedrive(
|
|
7201
|
+
this.port,
|
|
7202
|
+
sessionId,
|
|
7203
|
+
this.openCodeClient
|
|
7204
|
+
);
|
|
7205
|
+
if (!polledV1.ok) {
|
|
7206
|
+
const normalized = normalizeRedrivePollFailureBody(polledV1.body);
|
|
6294
7207
|
this.log({
|
|
6295
7208
|
level: "warn",
|
|
6296
|
-
message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned a non-array message body \u2014 treating as unreadable this tick`,
|
|
7209
|
+
message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned ${polledV1.malformed ? "a non-array message body" : `HTTP ${polledV1.status ?? "a network error"}${normalized ? `: ${normalized}` : ""}`} \u2014 treating as unreadable this tick`,
|
|
6297
7210
|
conversation_id: conv.id,
|
|
6298
7211
|
message_id: message.id
|
|
6299
7212
|
});
|
|
6300
|
-
return {
|
|
7213
|
+
return {
|
|
7214
|
+
ok: false,
|
|
7215
|
+
signature: polledV1.status === null && !polledV1.malformed ? null : polledV1.malformed ? "non-array message body" : `HTTP ${polledV1.status}${normalized ? `: ${normalized}` : ""}`
|
|
7216
|
+
};
|
|
6301
7217
|
}
|
|
6302
|
-
return { ok: true, messages:
|
|
7218
|
+
return { ok: true, messages: polledV1.messages };
|
|
6303
7219
|
} catch (err) {
|
|
6304
7220
|
this.log({
|
|
6305
7221
|
level: "warn",
|
|
@@ -6358,11 +7274,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6358
7274
|
}
|
|
6359
7275
|
const state = messageRunState(messages, ocId ?? "");
|
|
6360
7276
|
if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
|
|
6361
|
-
const ongoing = await isSessionOngoing(
|
|
7277
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
6362
7278
|
if (ongoing === false) {
|
|
6363
7279
|
this.log({
|
|
6364
7280
|
level: "info",
|
|
6365
|
-
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
|
|
7281
|
+
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`,
|
|
6366
7282
|
conversation_id: conv.id,
|
|
6367
7283
|
message_id: message.id
|
|
6368
7284
|
});
|
|
@@ -6375,7 +7291,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6375
7291
|
return this.settleRedrive(conv, sessionId, message, ocId, messages, state);
|
|
6376
7292
|
}
|
|
6377
7293
|
if (state === "running" || state === "queued") {
|
|
6378
|
-
const ongoing = await isSessionOngoing(
|
|
7294
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
6379
7295
|
if (ongoing === true) {
|
|
6380
7296
|
if (state === "queued") {
|
|
6381
7297
|
const siblingOcIds = this.siblingOpencodeMessageIds(
|
|
@@ -6835,7 +7751,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6835
7751
|
};
|
|
6836
7752
|
}
|
|
6837
7753
|
if (bound) {
|
|
6838
|
-
const exists = await sessionExists(
|
|
7754
|
+
const exists = await this.sessionExists(bound);
|
|
6839
7755
|
if (exists === false) {
|
|
6840
7756
|
this.log({
|
|
6841
7757
|
level: "debug",
|
|
@@ -6868,7 +7784,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6868
7784
|
*/
|
|
6869
7785
|
async createAndBindSession(conversationId) {
|
|
6870
7786
|
const directory = await this.resolveOpenCodeDirectory();
|
|
6871
|
-
const sessionId = await createOpenCodeSession(
|
|
7787
|
+
const sessionId = await this.createOpenCodeSession(directory);
|
|
6872
7788
|
this.sessions.set(conversationId, sessionId);
|
|
6873
7789
|
await this.persistSession(conversationId, sessionId).catch((err) => {
|
|
6874
7790
|
this.log({
|
|
@@ -6880,17 +7796,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6880
7796
|
return sessionId;
|
|
6881
7797
|
}
|
|
6882
7798
|
/**
|
|
6883
|
-
* Lazily resolve (and cache) opencode's root directory via
|
|
7799
|
+
* Lazily resolve (and cache) opencode's root directory via the selected client's
|
|
7800
|
+
* location lookup.
|
|
6884
7801
|
* Resolved once per driver: `undefined` until first lookup, then the directory
|
|
6885
|
-
* string or `null` if unavailable (we don't keep retrying a
|
|
7802
|
+
* string or `null` if unavailable (we don't keep retrying a failed lookup).
|
|
6886
7803
|
*/
|
|
6887
7804
|
async resolveOpenCodeDirectory() {
|
|
6888
7805
|
if (this.opencodeDirectory !== void 0) return this.opencodeDirectory;
|
|
6889
|
-
this.opencodeDirectory = await getOpenCodeDirectory(
|
|
7806
|
+
this.opencodeDirectory = await this.getOpenCodeDirectory();
|
|
6890
7807
|
if (!this.opencodeDirectory) {
|
|
6891
7808
|
this.log({
|
|
6892
7809
|
level: "warn",
|
|
6893
|
-
message: "Could not determine opencode directory (
|
|
7810
|
+
message: "Could not determine opencode directory (location lookup failed) \u2014 new sessions may not appear in opencode web"
|
|
6894
7811
|
});
|
|
6895
7812
|
}
|
|
6896
7813
|
return this.opencodeDirectory;
|
|
@@ -7324,7 +8241,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7324
8241
|
while (!this.stopped && !signal.aborted) {
|
|
7325
8242
|
const openedAt = this.now();
|
|
7326
8243
|
try {
|
|
7327
|
-
const outcome = await
|
|
8244
|
+
const outcome = await this.readOpenCodeSessionErrorStream({
|
|
7328
8245
|
signal,
|
|
7329
8246
|
onSessionError: (event) => this.handleSessionError(event)
|
|
7330
8247
|
});
|
|
@@ -7415,7 +8332,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7415
8332
|
}
|
|
7416
8333
|
async failFromSessionError(watcher, event, inFlight) {
|
|
7417
8334
|
try {
|
|
7418
|
-
const messages = await getSessionMessages(
|
|
8335
|
+
const messages = await this.getSessionMessages(event.sessionId);
|
|
7419
8336
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7420
8337
|
if (state !== "queued") {
|
|
7421
8338
|
this.log({
|
|
@@ -7467,7 +8384,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7467
8384
|
* markDone (done) exactly once per transition;
|
|
7468
8385
|
* 2. applies the idle-path re-dispatch guard (a dispatched message that never
|
|
7469
8386
|
* APPEARS → re-dispatch — D1 obligation 2);
|
|
7470
|
-
* 3. polls `/question` + `/permission
|
|
8387
|
+
* 3. polls V1's global `/question` + `/permission`, or V2's
|
|
8388
|
+
* `/api/session/:id/form` + `/api/session/:id/permission`, and surfaces
|
|
7471
8389
|
* NEW ones via `reportInteraction`, carrying the PAUSED message's own
|
|
7472
8390
|
* `source_message_id`;
|
|
7473
8391
|
* 4. drops messages that completed or timed out from the in-flight set.
|
|
@@ -7493,11 +8411,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7493
8411
|
if (watcher.generation !== generation) return;
|
|
7494
8412
|
let messages = null;
|
|
7495
8413
|
try {
|
|
7496
|
-
|
|
7497
|
-
if (res.ok) {
|
|
7498
|
-
const body = await res.json();
|
|
7499
|
-
messages = Array.isArray(body) ? body : null;
|
|
7500
|
-
}
|
|
8414
|
+
messages = await this.getSessionMessages(sessionId);
|
|
7501
8415
|
} catch {
|
|
7502
8416
|
}
|
|
7503
8417
|
if (messages != null && messages.length > 0) {
|
|
@@ -7730,7 +8644,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7730
8644
|
inFlight.b2LastDescendantCheckMs = this.now();
|
|
7731
8645
|
const [descendantOngoing, rootOngoing] = await Promise.all([
|
|
7732
8646
|
this.isAnyDescendantSessionOngoing(sessionId),
|
|
7733
|
-
isSessionOngoing(
|
|
8647
|
+
this.isSessionOngoing(sessionId)
|
|
7734
8648
|
]);
|
|
7735
8649
|
if (isB2AbandonmentConfirmed({
|
|
7736
8650
|
pinnedForMs,
|
|
@@ -7790,7 +8704,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7790
8704
|
});
|
|
7791
8705
|
}
|
|
7792
8706
|
const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;
|
|
7793
|
-
const ongoing = await isSessionOngoing(
|
|
8707
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
7794
8708
|
if (isAmbiguousFinishResolved({
|
|
7795
8709
|
pinnedForMs,
|
|
7796
8710
|
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
@@ -7980,7 +8894,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7980
8894
|
...this.doneUndeliverable,
|
|
7981
8895
|
...this.readoptPollUnresolvedSignalled
|
|
7982
8896
|
]) {
|
|
7983
|
-
if (!stillProcessing.has(id)) {
|
|
8897
|
+
if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
|
|
7984
8898
|
const cleared = this.dontRedispatch.delete(id);
|
|
7985
8899
|
const clearedUndeliverable = this.doneUndeliverable.delete(id);
|
|
7986
8900
|
this.readoptPollUnresolvedSignalled.delete(id);
|
|
@@ -8013,23 +8927,32 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8013
8927
|
for (const [sessionId, sessionRows] of bySession) {
|
|
8014
8928
|
let messages;
|
|
8015
8929
|
try {
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8930
|
+
if (this.isV2) {
|
|
8931
|
+
const snapshot = await this.getSessionMessages(sessionId);
|
|
8932
|
+
if (snapshot === null) {
|
|
8933
|
+
this.log({
|
|
8934
|
+
level: "warn",
|
|
8935
|
+
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
|
|
8936
|
+
});
|
|
8937
|
+
continue;
|
|
8938
|
+
}
|
|
8939
|
+
messages = snapshot;
|
|
8940
|
+
} else {
|
|
8941
|
+
const polled = await pollSessionMessagesForRedrive(
|
|
8942
|
+
this.port,
|
|
8943
|
+
sessionId,
|
|
8944
|
+
this.openCodeClient
|
|
8945
|
+
);
|
|
8946
|
+
if (!polled.ok) {
|
|
8947
|
+
const normalized = normalizeRedrivePollFailureBody(polled.body);
|
|
8948
|
+
this.log({
|
|
8949
|
+
level: "warn",
|
|
8950
|
+
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned ${polled.malformed ? "a non-array message body" : `HTTP ${polled.status ?? "a network error"}${normalized ? `: ${normalized}` : ""}`} \u2014 skipping this session this tick`
|
|
8951
|
+
});
|
|
8952
|
+
continue;
|
|
8953
|
+
}
|
|
8954
|
+
messages = polled.messages;
|
|
8031
8955
|
}
|
|
8032
|
-
messages = body;
|
|
8033
8956
|
} catch (err) {
|
|
8034
8957
|
this.log({
|
|
8035
8958
|
level: "warn",
|
|
@@ -8038,7 +8961,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8038
8961
|
continue;
|
|
8039
8962
|
}
|
|
8040
8963
|
const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
|
|
8041
|
-
const sessionOngoing = anyUntracked ? await isSessionOngoing(
|
|
8964
|
+
const sessionOngoing = anyUntracked ? await this.isSessionOngoing(sessionId) : null;
|
|
8042
8965
|
for (const row of sessionRows) {
|
|
8043
8966
|
await this.readoptOne(sessionId, row, messages, sessionOngoing);
|
|
8044
8967
|
}
|
|
@@ -8085,7 +9008,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8085
9008
|
if (restartAborted) {
|
|
8086
9009
|
this.log({
|
|
8087
9010
|
level: "info",
|
|
8088
|
-
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
|
|
9011
|
+
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`,
|
|
8089
9012
|
conversation_id: row.conversation_id,
|
|
8090
9013
|
message_id: row.id
|
|
8091
9014
|
});
|
|
@@ -8163,7 +9086,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8163
9086
|
const finish = reply?.info?.finish ?? reply?.finish;
|
|
8164
9087
|
this.log({
|
|
8165
9088
|
level: "info",
|
|
8166
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} has an ambiguous finish ("${finish ?? "(absent)"}") but session ${sessionId.slice(0, 8)} is confirmed not-ongoing per
|
|
9089
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} has an ambiguous finish ("${finish ?? "(absent)"}") but session ${sessionId.slice(0, 8)} is confirmed not-ongoing per the active-session status check \u2014 delivering the existing reply instead of re-dispatching`,
|
|
8167
9090
|
conversation_id: row.conversation_id,
|
|
8168
9091
|
message_id: row.id
|
|
8169
9092
|
});
|
|
@@ -8172,7 +9095,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8172
9095
|
}
|
|
8173
9096
|
this.log({
|
|
8174
9097
|
level: "info",
|
|
8175
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per
|
|
9098
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check (absent/idle) \u2014 re-dispatching from scratch (status-gated recovery)`,
|
|
8176
9099
|
conversation_id: row.conversation_id,
|
|
8177
9100
|
message_id: row.id
|
|
8178
9101
|
});
|
|
@@ -8182,7 +9105,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8182
9105
|
if (ongoing === true) {
|
|
8183
9106
|
this.log({
|
|
8184
9107
|
level: "debug",
|
|
8185
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per
|
|
9108
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per the active-session status check (busy/retry) \u2014 re-attaching watcher (no re-dispatch)`,
|
|
8186
9109
|
conversation_id: row.conversation_id,
|
|
8187
9110
|
message_id: row.id
|
|
8188
9111
|
});
|
|
@@ -8190,7 +9113,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8190
9113
|
if (shape === "b1") {
|
|
8191
9114
|
this.log({
|
|
8192
9115
|
level: "debug",
|
|
8193
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but
|
|
9116
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but the active-session status check was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 NOT latching a b1 row on a transient status blip; leaving it un-tracked to re-evaluate on the next drain`,
|
|
8194
9117
|
conversation_id: row.conversation_id,
|
|
8195
9118
|
message_id: row.id
|
|
8196
9119
|
});
|
|
@@ -8202,7 +9125,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8202
9125
|
}
|
|
8203
9126
|
this.log({
|
|
8204
9127
|
level: "debug",
|
|
8205
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but
|
|
9128
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but the active-session status check was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 falling back to the #253 preamble + descendant cross-check`,
|
|
8206
9129
|
conversation_id: row.conversation_id,
|
|
8207
9130
|
message_id: row.id
|
|
8208
9131
|
});
|
|
@@ -8382,19 +9305,90 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8382
9305
|
conversation_id: row.conversation_id,
|
|
8383
9306
|
message_id: row.id
|
|
8384
9307
|
});
|
|
8385
|
-
this.awaitingReadopt.add(row.id);
|
|
9308
|
+
if (!this.isV2) this.awaitingReadopt.add(row.id);
|
|
8386
9309
|
const readoptConv = this.convForRow(sessionId, row);
|
|
8387
9310
|
const readoptMessage = this.queuedMessageForRow(row);
|
|
8388
|
-
const sendAttachments = this.buildSendAttachments(readoptConv, readoptMessage);
|
|
9311
|
+
const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(readoptConv, readoptMessage);
|
|
9312
|
+
if (this.isV2 && row.attachments && row.attachments.length > 0) {
|
|
9313
|
+
this.signalAttachmentsSkipped(
|
|
9314
|
+
row.conversation_id,
|
|
9315
|
+
row.id,
|
|
9316
|
+
row.attachments.map((attachment, index) => ({
|
|
9317
|
+
index,
|
|
9318
|
+
mime: attachment.mime,
|
|
9319
|
+
...attachment.filename ? { filename: attachment.filename } : {},
|
|
9320
|
+
status: "skipped"
|
|
9321
|
+
})),
|
|
9322
|
+
false
|
|
9323
|
+
);
|
|
9324
|
+
}
|
|
8389
9325
|
let ocId;
|
|
8390
9326
|
try {
|
|
8391
|
-
ocId = await this.dispatchLocked(
|
|
9327
|
+
ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
|
|
8392
9328
|
sessionId,
|
|
8393
|
-
() => sendPromptAsync(
|
|
9329
|
+
() => sendPromptAsync(
|
|
9330
|
+
this.port,
|
|
9331
|
+
sessionId,
|
|
9332
|
+
row.content,
|
|
9333
|
+
options,
|
|
9334
|
+
sendAttachments,
|
|
9335
|
+
this.openCodeClient
|
|
9336
|
+
)
|
|
8394
9337
|
);
|
|
8395
9338
|
} catch (err) {
|
|
8396
9339
|
this.awaitingReadopt.delete(row.id);
|
|
8397
9340
|
if (err instanceof ChannelAuthError) throw err;
|
|
9341
|
+
if (this.isV2) {
|
|
9342
|
+
const errorMessage3 = err instanceof Error ? err.message : String(err);
|
|
9343
|
+
const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
|
|
9344
|
+
if (!invalidPromptAck) {
|
|
9345
|
+
const exists = await this.sessionExists(sessionId);
|
|
9346
|
+
if (exists === false) {
|
|
9347
|
+
this.log({
|
|
9348
|
+
level: "warn",
|
|
9349
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was deleted mid-dispatch \u2014 deferring to the next drain: ${errorMessage3}`,
|
|
9350
|
+
conversation_id: row.conversation_id,
|
|
9351
|
+
message_id: row.id
|
|
9352
|
+
});
|
|
9353
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
9354
|
+
return;
|
|
9355
|
+
}
|
|
9356
|
+
if (exists === null) {
|
|
9357
|
+
this.log({
|
|
9358
|
+
level: "warn",
|
|
9359
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) existence could not be confirmed \u2014 deferring to the next drain: ${errorMessage3}`,
|
|
9360
|
+
conversation_id: row.conversation_id,
|
|
9361
|
+
message_id: row.id
|
|
9362
|
+
});
|
|
9363
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
9364
|
+
return;
|
|
9365
|
+
}
|
|
9366
|
+
} else {
|
|
9367
|
+
this.dontRedispatch.add(row.id);
|
|
9368
|
+
}
|
|
9369
|
+
this.log({
|
|
9370
|
+
level: "error",
|
|
9371
|
+
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage3}`,
|
|
9372
|
+
conversation_id: row.conversation_id,
|
|
9373
|
+
message_id: row.id
|
|
9374
|
+
});
|
|
9375
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
|
|
9376
|
+
(markErr) => {
|
|
9377
|
+
this.log({
|
|
9378
|
+
level: "warn",
|
|
9379
|
+
message: `markFailed PATCH for V2 re-adopt dispatch failure on message ${row.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
9380
|
+
conversation_id: row.conversation_id,
|
|
9381
|
+
message_id: row.id
|
|
9382
|
+
});
|
|
9383
|
+
if (invalidPromptAck) {
|
|
9384
|
+
void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
|
|
9385
|
+
} else {
|
|
9386
|
+
this.signalDispatchNotStarted(readoptConv, readoptMessage, "failure_unreported");
|
|
9387
|
+
}
|
|
9388
|
+
}
|
|
9389
|
+
);
|
|
9390
|
+
return;
|
|
9391
|
+
}
|
|
8398
9392
|
this.log({
|
|
8399
9393
|
level: "warn",
|
|
8400
9394
|
message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -8405,6 +9399,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8405
9399
|
return;
|
|
8406
9400
|
}
|
|
8407
9401
|
if (ocId === null) {
|
|
9402
|
+
if (this.isV2) {
|
|
9403
|
+
this.dontRedispatch.add(row.id);
|
|
9404
|
+
this.log({
|
|
9405
|
+
level: "error",
|
|
9406
|
+
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} completed without an acknowledged message id`,
|
|
9407
|
+
conversation_id: row.conversation_id,
|
|
9408
|
+
message_id: row.id
|
|
9409
|
+
});
|
|
9410
|
+
void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
|
|
9411
|
+
return;
|
|
9412
|
+
}
|
|
8408
9413
|
this.awaitingReadopt.delete(row.id);
|
|
8409
9414
|
const streak = this.recordUnconfirmedDispatch(row.id, sessionId);
|
|
8410
9415
|
if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
@@ -8526,12 +9531,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8526
9531
|
this.dispatched.delete(evidentMessageId);
|
|
8527
9532
|
}
|
|
8528
9533
|
/**
|
|
8529
|
-
* Poll `/question` + `/permission
|
|
8530
|
-
* via `reportInteraction` (Task 3.5),
|
|
8531
|
-
* `source_message_id` so the server @mentions
|
|
8532
|
-
* concurrency. Dedups by interaction id across ticks
|
|
9534
|
+
* Poll V1's global `/question` + `/permission`, or V2's watched-session form and
|
|
9535
|
+
* permission routes, and surface NEW ones via `reportInteraction` (Task 3.5),
|
|
9536
|
+
* carrying the PAUSED message's own `source_message_id` so the server @mentions
|
|
9537
|
+
* the correct person under concurrency. Dedups by interaction id across ticks
|
|
9538
|
+
* (reused per-session sets).
|
|
8533
9539
|
*
|
|
8534
|
-
* The interaction is attributed to the in-flight message it paused on.
|
|
9540
|
+
* The interaction is attributed to the in-flight message it paused on. OpenCode
|
|
8535
9541
|
* stamps a `messageID` on a permission (and `tool.messageID` on a question) =
|
|
8536
9542
|
* the assistant message id, whose `parentID` is the user message id — but the
|
|
8537
9543
|
* simplest robust attribution here is: the single in-flight message that is
|
|
@@ -8554,16 +9560,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8554
9560
|
let permissionsPolledOk = true;
|
|
8555
9561
|
let questions = [];
|
|
8556
9562
|
try {
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
questions = body;
|
|
8562
|
-
} else {
|
|
8563
|
-
questionsPolledOk = false;
|
|
8564
|
-
}
|
|
9563
|
+
if (this.isV2) {
|
|
9564
|
+
const forms = await listV2Forms(this.openCodeClient, sessionId);
|
|
9565
|
+
if (forms === null) questionsPolledOk = false;
|
|
9566
|
+
else questions = forms;
|
|
8565
9567
|
} else {
|
|
8566
|
-
|
|
9568
|
+
const listed = await listOpenCodeQuestions(this.port, this.openCodeClient);
|
|
9569
|
+
if (listed === null) questionsPolledOk = false;
|
|
9570
|
+
else questions = listed;
|
|
8567
9571
|
}
|
|
8568
9572
|
} catch {
|
|
8569
9573
|
questionsPolledOk = false;
|
|
@@ -8583,16 +9587,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8583
9587
|
}
|
|
8584
9588
|
let permissions = [];
|
|
8585
9589
|
try {
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
permissions = body;
|
|
8591
|
-
} else {
|
|
8592
|
-
permissionsPolledOk = false;
|
|
8593
|
-
}
|
|
9590
|
+
if (this.isV2) {
|
|
9591
|
+
const listed = await listV2Permissions(this.openCodeClient, sessionId);
|
|
9592
|
+
if (listed === null) permissionsPolledOk = false;
|
|
9593
|
+
else permissions = listed;
|
|
8594
9594
|
} else {
|
|
8595
|
-
|
|
9595
|
+
const listed = await listOpenCodePermissions(this.port, this.openCodeClient);
|
|
9596
|
+
if (listed === null) permissionsPolledOk = false;
|
|
9597
|
+
else permissions = listed;
|
|
8596
9598
|
}
|
|
8597
9599
|
} catch {
|
|
8598
9600
|
permissionsPolledOk = false;
|
|
@@ -8686,10 +9688,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8686
9688
|
if (cached !== void 0) return cached;
|
|
8687
9689
|
let parent = void 0;
|
|
8688
9690
|
try {
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
9691
|
+
if (this.isV2) {
|
|
9692
|
+
const session = await getV2Session(this.openCodeClient, sessionId);
|
|
9693
|
+
parent = null;
|
|
9694
|
+
const candidate = session.parentID;
|
|
9695
|
+
if (typeof candidate === "string") parent = candidate;
|
|
9696
|
+
} else {
|
|
9697
|
+
const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
|
|
9698
|
+
parent = typeof body?.parentID === "string" ? body.parentID : body === null ? void 0 : null;
|
|
8693
9699
|
}
|
|
8694
9700
|
} catch {
|
|
8695
9701
|
parent = void 0;
|
|
@@ -8743,18 +9749,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8743
9749
|
if (cached) return cached;
|
|
8744
9750
|
const pending = (async () => {
|
|
8745
9751
|
try {
|
|
8746
|
-
const
|
|
8747
|
-
if (
|
|
9752
|
+
const messages2 = await this.getTelemetrySubagentSessionMessages(sessionId);
|
|
9753
|
+
if (messages2 === null) {
|
|
8748
9754
|
this.log({
|
|
8749
9755
|
level: "warn",
|
|
8750
|
-
message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)}
|
|
9756
|
+
message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} was unreadable \u2014 omitting invocation telemetry`,
|
|
8751
9757
|
message_id: messageId
|
|
8752
9758
|
});
|
|
8753
9759
|
return null;
|
|
8754
9760
|
}
|
|
8755
|
-
|
|
8756
|
-
if (!Array.isArray(body)) throw new Error("response body was not a message array");
|
|
8757
|
-
return body;
|
|
9761
|
+
return messages2;
|
|
8758
9762
|
} catch (err) {
|
|
8759
9763
|
this.log({
|
|
8760
9764
|
level: "warn",
|
|
@@ -8895,21 +9899,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8895
9899
|
const cached = this.sessionTitles.get(sessionId);
|
|
8896
9900
|
if (cached != null) return cached;
|
|
8897
9901
|
try {
|
|
8898
|
-
|
|
8899
|
-
if (
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
return title;
|
|
8905
|
-
}
|
|
8906
|
-
return null;
|
|
9902
|
+
let title = "";
|
|
9903
|
+
if (this.isV2) {
|
|
9904
|
+
title = (await getV2Session(this.openCodeClient, sessionId)).title?.trim() ?? "";
|
|
9905
|
+
} else {
|
|
9906
|
+
const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
|
|
9907
|
+
title = typeof body?.title === "string" ? body.title.trim() : "";
|
|
8907
9908
|
}
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
9909
|
+
if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
|
|
9910
|
+
this.sessionTitles.set(sessionId, title);
|
|
9911
|
+
return title;
|
|
9912
|
+
}
|
|
9913
|
+
return null;
|
|
8913
9914
|
} catch (err) {
|
|
8914
9915
|
this.log({
|
|
8915
9916
|
level: "debug",
|
|
@@ -9007,7 +10008,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9007
10008
|
* `SessionStatus` only.
|
|
9008
10009
|
*/
|
|
9009
10010
|
async isAnyDescendantSessionAlive(rootSessionId) {
|
|
9010
|
-
const sessions = await listSessions(
|
|
10011
|
+
const sessions = await this.listSessions();
|
|
9011
10012
|
if (!sessions) {
|
|
9012
10013
|
this.log({
|
|
9013
10014
|
level: "warn",
|
|
@@ -9018,7 +10019,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9018
10019
|
for (const candidate of sessions) {
|
|
9019
10020
|
if (!candidate?.id || candidate.id === rootSessionId) continue;
|
|
9020
10021
|
if (!await this.sessionBelongsTo(candidate.id, rootSessionId)) continue;
|
|
9021
|
-
const childMsgs = await
|
|
10022
|
+
const childMsgs = await this.getSubagentSessionMessages(candidate.id);
|
|
9022
10023
|
if (isSessionActivelyGenerating(childMsgs)) {
|
|
9023
10024
|
return true;
|
|
9024
10025
|
}
|
|
@@ -9080,7 +10081,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9080
10081
|
* `isB2AbandonmentConfirmed`.
|
|
9081
10082
|
*/
|
|
9082
10083
|
async isAnyDescendantSessionOngoing(rootSessionId) {
|
|
9083
|
-
const sessions = await listSessions(
|
|
10084
|
+
const sessions = await this.listSessions();
|
|
9084
10085
|
if (!sessions) {
|
|
9085
10086
|
this.log({
|
|
9086
10087
|
level: "warn",
|
|
@@ -9097,7 +10098,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9097
10098
|
continue;
|
|
9098
10099
|
}
|
|
9099
10100
|
if (membership === false) continue;
|
|
9100
|
-
const ongoing = await isSessionOngoing(
|
|
10101
|
+
const ongoing = await this.isSessionOngoing(candidate.id);
|
|
9101
10102
|
if (ongoing === true) return true;
|
|
9102
10103
|
if (ongoing === null) indeterminate = true;
|
|
9103
10104
|
}
|
|
@@ -9437,7 +10438,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9437
10438
|
const classified = messageFailure(messages, userMessageId);
|
|
9438
10439
|
if (classified != null) return classified;
|
|
9439
10440
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
9440
|
-
const hasProvider = await hasAnyConfiguredProvider(
|
|
10441
|
+
const hasProvider = await this.hasAnyConfiguredProvider();
|
|
9441
10442
|
return applyZeroProviderFallback(
|
|
9442
10443
|
classified,
|
|
9443
10444
|
hasProvider,
|
|
@@ -9510,7 +10511,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9510
10511
|
const succeededProviders = /* @__PURE__ */ new Set();
|
|
9511
10512
|
for (const ref of refs) {
|
|
9512
10513
|
try {
|
|
9513
|
-
const childMessages = await
|
|
10514
|
+
const childMessages = await this.getSubagentSessionMessages(ref.sessionId);
|
|
9514
10515
|
if (childMessages === null) {
|
|
9515
10516
|
this.log({
|
|
9516
10517
|
level: "debug",
|
|
@@ -9841,6 +10842,7 @@ Port ${port} is already in use.`));
|
|
|
9841
10842
|
|
|
9842
10843
|
// src/commands/ensure-opencode-v2.ts
|
|
9843
10844
|
import chalk6 from "chalk";
|
|
10845
|
+
import ora3 from "ora";
|
|
9844
10846
|
import { select as select3 } from "@inquirer/prompts";
|
|
9845
10847
|
async function probeOpenCode2WithoutPassword(port) {
|
|
9846
10848
|
try {
|
|
@@ -9866,11 +10868,7 @@ function unknownPasswordError(port) {
|
|
|
9866
10868
|
`OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
|
|
9867
10869
|
);
|
|
9868
10870
|
}
|
|
9869
|
-
|
|
9870
|
-
return new Error(
|
|
9871
|
-
"OpenCode V2 (opencode2) session support is incomplete \u2014 see https://github.com/sroze/evident/issues/2075. Not starting a new opencode2 process."
|
|
9872
|
-
);
|
|
9873
|
-
}
|
|
10871
|
+
var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
|
|
9874
10872
|
async function ensureOpenCode2Running(ctx) {
|
|
9875
10873
|
const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
|
|
9876
10874
|
if (initialHealth.authFailed) {
|
|
@@ -9913,13 +10911,37 @@ Port ${port} is already in use.`));
|
|
|
9913
10911
|
}
|
|
9914
10912
|
}
|
|
9915
10913
|
if (!ctx.interactive) {
|
|
9916
|
-
|
|
10914
|
+
ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
|
|
10915
|
+
const { child: proc, password } = await startOpenCode2(port, {
|
|
10916
|
+
inheritStdio: ctx.inheritStdio
|
|
10917
|
+
});
|
|
10918
|
+
const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
|
|
10919
|
+
if (!health.healthy) {
|
|
10920
|
+
return {
|
|
10921
|
+
port,
|
|
10922
|
+
process: proc,
|
|
10923
|
+
version: null,
|
|
10924
|
+
notReadyReason: `it did not become healthy within ${Math.round(ctx.startTimeoutMs / 1e3)}s`,
|
|
10925
|
+
password
|
|
10926
|
+
};
|
|
10927
|
+
}
|
|
10928
|
+
ctx.log(`OpenCode V2 started on port ${port}${health.version ? ` (v${health.version})` : ""}`);
|
|
10929
|
+
return {
|
|
10930
|
+
port,
|
|
10931
|
+
process: proc,
|
|
10932
|
+
version: health.version ?? null,
|
|
10933
|
+
notReadyReason: null,
|
|
10934
|
+
password
|
|
10935
|
+
};
|
|
9917
10936
|
}
|
|
9918
|
-
console.log(chalk6.yellow(`
|
|
9919
|
-
${v2SessionSupportIncompleteError().message}`));
|
|
9920
10937
|
const action = await select3({
|
|
9921
10938
|
message: "OpenCode V2 is not running. What would you like to do?",
|
|
9922
10939
|
choices: [
|
|
10940
|
+
{
|
|
10941
|
+
name: "Start OpenCode V2 for me",
|
|
10942
|
+
value: "start",
|
|
10943
|
+
description: `Run 'opencode2 serve --port ${port}'`
|
|
10944
|
+
},
|
|
9923
10945
|
{
|
|
9924
10946
|
name: "Show me the command",
|
|
9925
10947
|
value: "manual",
|
|
@@ -9940,6 +10962,25 @@ ${v2SessionSupportIncompleteError().message}`));
|
|
|
9940
10962
|
blank();
|
|
9941
10963
|
throw new Error("Please start OpenCode V2 manually");
|
|
9942
10964
|
}
|
|
10965
|
+
if (action === "start") {
|
|
10966
|
+
const spinner = ora3("Starting OpenCode V2...").start();
|
|
10967
|
+
const { child: proc, password } = await startOpenCode2(port, {
|
|
10968
|
+
inheritStdio: ctx.inheritStdio
|
|
10969
|
+
});
|
|
10970
|
+
const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
|
|
10971
|
+
if (!health.healthy) {
|
|
10972
|
+
spinner.fail("Failed to start OpenCode V2");
|
|
10973
|
+
throw new Error("OpenCode V2 failed to start");
|
|
10974
|
+
}
|
|
10975
|
+
spinner.stop();
|
|
10976
|
+
return {
|
|
10977
|
+
port,
|
|
10978
|
+
process: proc,
|
|
10979
|
+
version: health.version ?? null,
|
|
10980
|
+
notReadyReason: null,
|
|
10981
|
+
password
|
|
10982
|
+
};
|
|
10983
|
+
}
|
|
9943
10984
|
return {
|
|
9944
10985
|
port,
|
|
9945
10986
|
process: null,
|
|
@@ -10833,7 +11874,7 @@ async function driveChannels(state, driver) {
|
|
|
10833
11874
|
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
10834
11875
|
if (opencodeAuthApplied) state.openaiUsageRearm?.();
|
|
10835
11876
|
if (claudeCredentialApplied || opencodeAuthApplied) {
|
|
10836
|
-
void reloadProviderCache(state.port).catch(
|
|
11877
|
+
void reloadProviderCache(state.port, state.opencodeClient ?? void 0).catch(
|
|
10837
11878
|
(error2) => logActivity(state, {
|
|
10838
11879
|
type: "error",
|
|
10839
11880
|
error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
@@ -10958,7 +11999,7 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
|
|
|
10958
11999
|
async function runSweep(state, driver, config) {
|
|
10959
12000
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
10960
12001
|
try {
|
|
10961
|
-
const sessions = await listSessions(state.port);
|
|
12002
|
+
const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
|
|
10962
12003
|
if (sessions === null) {
|
|
10963
12004
|
logActivity(state, {
|
|
10964
12005
|
type: "info",
|
|
@@ -10988,7 +12029,7 @@ async function runSweep(state, driver, config) {
|
|
|
10988
12029
|
});
|
|
10989
12030
|
continue;
|
|
10990
12031
|
}
|
|
10991
|
-
if (await deleteSession(state.port, id)) deleted++;
|
|
12032
|
+
if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
|
|
10992
12033
|
else failed++;
|
|
10993
12034
|
}
|
|
10994
12035
|
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
@@ -11496,6 +12537,8 @@ async function run(options) {
|
|
|
11496
12537
|
opencodeConnected: false,
|
|
11497
12538
|
opencodeVersion: null,
|
|
11498
12539
|
opencodeApiVersion: "v1",
|
|
12540
|
+
opencodePassword: null,
|
|
12541
|
+
opencodeClient: null,
|
|
11499
12542
|
sessionDbProvenanceAnomaly: false,
|
|
11500
12543
|
opencodeProcess: null,
|
|
11501
12544
|
stopOpenCodeLogTail: null,
|
|
@@ -11650,7 +12693,7 @@ async function run(options) {
|
|
|
11650
12693
|
console.log(chalk7.bold("Evident Run"));
|
|
11651
12694
|
console.log(chalk7.dim("-".repeat(40)));
|
|
11652
12695
|
}
|
|
11653
|
-
const spinner = interactive && !state.json ?
|
|
12696
|
+
const spinner = interactive && !state.json ? ora4("Validating runner...").start() : null;
|
|
11654
12697
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
11655
12698
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
11656
12699
|
spinner?.fail("Authentication failed");
|
|
@@ -11771,7 +12814,7 @@ async function run(options) {
|
|
|
11771
12814
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
11772
12815
|
}
|
|
11773
12816
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
11774
|
-
const ocSpinner = interactive && !state.json ?
|
|
12817
|
+
const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
|
|
11775
12818
|
try {
|
|
11776
12819
|
const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
|
|
11777
12820
|
port: state.port,
|
|
@@ -11792,6 +12835,18 @@ async function run(options) {
|
|
|
11792
12835
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
11793
12836
|
state.opencodeVersion = oc.version;
|
|
11794
12837
|
state.opencodeApiVersion = opencodeVersion;
|
|
12838
|
+
let opencodePassword = null;
|
|
12839
|
+
if (opencodeVersion === "v2" && "password" in oc) {
|
|
12840
|
+
const value = oc.password;
|
|
12841
|
+
if (typeof value === "string" || value === null) opencodePassword = value;
|
|
12842
|
+
}
|
|
12843
|
+
state.opencodePassword = opencodePassword;
|
|
12844
|
+
const openCodeClient = createOpenCodeClient({
|
|
12845
|
+
port: state.port,
|
|
12846
|
+
version: state.opencodeApiVersion,
|
|
12847
|
+
password: state.opencodePassword
|
|
12848
|
+
});
|
|
12849
|
+
state.opencodeClient = openCodeClient;
|
|
11795
12850
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
11796
12851
|
try {
|
|
11797
12852
|
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
@@ -11838,9 +12893,9 @@ async function run(options) {
|
|
|
11838
12893
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
11839
12894
|
}
|
|
11840
12895
|
}
|
|
11841
|
-
await reloadProviderCache(state.port);
|
|
12896
|
+
await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
|
|
11842
12897
|
const noProviderWarning = buildNoProviderWarning(
|
|
11843
|
-
await hasAnyConfiguredProvider(state.port)
|
|
12898
|
+
await hasAnyConfiguredProvider(state.port, state.opencodeClient ?? void 0)
|
|
11844
12899
|
);
|
|
11845
12900
|
if (noProviderWarning) {
|
|
11846
12901
|
log2(state, noProviderWarning, "warn");
|
|
@@ -11963,11 +13018,12 @@ async function run(options) {
|
|
|
11963
13018
|
});
|
|
11964
13019
|
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
11965
13020
|
}
|
|
11966
|
-
const tunnelSpinner = interactive && !state.json ?
|
|
13021
|
+
const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
|
|
11967
13022
|
const channelDriver = new ChannelDriver({
|
|
11968
13023
|
agentId: state.agentId,
|
|
11969
13024
|
port: state.port,
|
|
11970
13025
|
apiUrl: getApiUrlConfig(),
|
|
13026
|
+
openCodeClient: state.opencodeClient ?? void 0,
|
|
11971
13027
|
getAuthHeader: () => state.authHeader,
|
|
11972
13028
|
conversationFilter: state.conversationFilter,
|
|
11973
13029
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
@@ -11993,6 +13049,7 @@ async function run(options) {
|
|
|
11993
13049
|
agentId: state.agentId,
|
|
11994
13050
|
getAuthHeader: () => state.authHeader,
|
|
11995
13051
|
port: state.port,
|
|
13052
|
+
openCodePassword: state.opencodePassword,
|
|
11996
13053
|
isRunning: () => state.running,
|
|
11997
13054
|
events: {
|
|
11998
13055
|
onConnected: (agentId, isReconnect) => {
|
|
@@ -12137,7 +13194,7 @@ async function run(options) {
|
|
|
12137
13194
|
state.openaiUsageTimer = timer;
|
|
12138
13195
|
},
|
|
12139
13196
|
fetchUsage: async () => {
|
|
12140
|
-
const usage = await getOpenAiUsage(state.port);
|
|
13197
|
+
const usage = await getOpenAiUsage(state.port, state.opencodeClient ?? void 0);
|
|
12141
13198
|
if (usage.subscription === null) {
|
|
12142
13199
|
logActivity(state, {
|
|
12143
13200
|
type: "info",
|