@evident-ai/cli 3.4.1-dev.69d24ff → 3.4.1-dev.71ade33
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 +1372 -247
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1138,6 +1138,37 @@ var TelemetryEventTypes = {
|
|
|
1138
1138
|
RUNNER_ACTIVITY: "runner.activity"
|
|
1139
1139
|
};
|
|
1140
1140
|
|
|
1141
|
+
// ../../packages/types/src/tunnel/binary-frame.ts
|
|
1142
|
+
var BINARY_FRAME_REQ_DATA = 1;
|
|
1143
|
+
var BINARY_FRAME_RES_DATA = 2;
|
|
1144
|
+
var textEncoder = new TextEncoder();
|
|
1145
|
+
var textDecoder = new TextDecoder();
|
|
1146
|
+
function encodeBinaryBodyFrame(type, sid, payload) {
|
|
1147
|
+
const sidBytes = textEncoder.encode(sid);
|
|
1148
|
+
if (sidBytes.length === 0 || sidBytes.length > 255) {
|
|
1149
|
+
throw new RangeError("sid must contain between 1 and 255 UTF-8 bytes");
|
|
1150
|
+
}
|
|
1151
|
+
const frame = new Uint8Array(2 + sidBytes.length + payload.length);
|
|
1152
|
+
frame[0] = type;
|
|
1153
|
+
frame[1] = sidBytes.length;
|
|
1154
|
+
frame.set(sidBytes, 2);
|
|
1155
|
+
frame.set(payload, 2 + sidBytes.length);
|
|
1156
|
+
return frame;
|
|
1157
|
+
}
|
|
1158
|
+
function decodeBinaryBodyFrame(bytes) {
|
|
1159
|
+
if (bytes.length < 2) return null;
|
|
1160
|
+
const type = bytes[0];
|
|
1161
|
+
if (type !== BINARY_FRAME_REQ_DATA && type !== BINARY_FRAME_RES_DATA) return null;
|
|
1162
|
+
const sidLen = bytes[1];
|
|
1163
|
+
if (sidLen === 0 || bytes.length < 2 + sidLen) return null;
|
|
1164
|
+
const payloadOffset = 2 + sidLen;
|
|
1165
|
+
return {
|
|
1166
|
+
type,
|
|
1167
|
+
sid: textDecoder.decode(bytes.subarray(2, payloadOffset)),
|
|
1168
|
+
payload: bytes.subarray(payloadOffset)
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1141
1172
|
// ../../packages/types/src/tunnel/index.ts
|
|
1142
1173
|
var MAX_FRAME_BYTES = 256 * 1024;
|
|
1143
1174
|
var TUNNEL_DRAIN_PING_PATH = "/__evident/drain";
|
|
@@ -1178,11 +1209,11 @@ function stripQuery(url) {
|
|
|
1178
1209
|
}
|
|
1179
1210
|
|
|
1180
1211
|
// src/commands/run.ts
|
|
1181
|
-
import
|
|
1212
|
+
import ora4 from "ora";
|
|
1182
1213
|
import { select as select4 } from "@inquirer/prompts";
|
|
1183
1214
|
|
|
1184
1215
|
// src/lib/telemetry.ts
|
|
1185
|
-
var CLI_VERSION = (true ? "3.4.1-dev.
|
|
1216
|
+
var CLI_VERSION = (true ? "3.4.1-dev.71ade33" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1186
1217
|
function getCliVersion() {
|
|
1187
1218
|
return CLI_VERSION;
|
|
1188
1219
|
}
|
|
@@ -1656,6 +1687,11 @@ function isSessionDbRecoveryRecord(value) {
|
|
|
1656
1687
|
);
|
|
1657
1688
|
}
|
|
1658
1689
|
|
|
1690
|
+
// src/lib/opencode/auth.ts
|
|
1691
|
+
function buildOpenCodeBasicAuthHeader(password) {
|
|
1692
|
+
return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1659
1695
|
// src/lib/opencode/health.ts
|
|
1660
1696
|
async function checkOpenCodeHealth(port) {
|
|
1661
1697
|
try {
|
|
@@ -1673,6 +1709,27 @@ async function checkOpenCodeHealth(port) {
|
|
|
1673
1709
|
return { healthy: false, error: message };
|
|
1674
1710
|
}
|
|
1675
1711
|
}
|
|
1712
|
+
async function checkOpenCode2Health(port, password) {
|
|
1713
|
+
try {
|
|
1714
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
1715
|
+
headers: {
|
|
1716
|
+
Authorization: buildOpenCodeBasicAuthHeader(password)
|
|
1717
|
+
},
|
|
1718
|
+
signal: AbortSignal.timeout(2e3)
|
|
1719
|
+
});
|
|
1720
|
+
if (response.status === 401) {
|
|
1721
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
1722
|
+
}
|
|
1723
|
+
if (!response.ok) {
|
|
1724
|
+
return { healthy: false, error: `HTTP ${response.status}` };
|
|
1725
|
+
}
|
|
1726
|
+
const data = await response.json().catch(() => ({}));
|
|
1727
|
+
return { healthy: true, version: data.version };
|
|
1728
|
+
} catch (error2) {
|
|
1729
|
+
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1730
|
+
return { healthy: false, error: message };
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1676
1733
|
async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
1677
1734
|
const startTime = Date.now();
|
|
1678
1735
|
while (Date.now() - startTime < timeoutMs) {
|
|
@@ -1684,6 +1741,61 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
1684
1741
|
}
|
|
1685
1742
|
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
1686
1743
|
}
|
|
1744
|
+
async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
|
|
1745
|
+
const startTime = Date.now();
|
|
1746
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
1747
|
+
const health = await checkOpenCode2Health(port, password);
|
|
1748
|
+
if (health.healthy || health.authFailed) {
|
|
1749
|
+
return health;
|
|
1750
|
+
}
|
|
1751
|
+
await new Promise((resolve4) => setTimeout(resolve4, 1e3));
|
|
1752
|
+
}
|
|
1753
|
+
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// src/lib/http-timeout.ts
|
|
1757
|
+
var REQUEST_TIMEOUT_MS = 6e4;
|
|
1758
|
+
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
1759
|
+
return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// src/lib/opencode/client.ts
|
|
1763
|
+
function redactPassword(message, password) {
|
|
1764
|
+
return message.replaceAll(password, "[redacted]");
|
|
1765
|
+
}
|
|
1766
|
+
function createOpenCodeClient(options) {
|
|
1767
|
+
const password = options.password ?? null;
|
|
1768
|
+
const fetchImpl = withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
|
|
1769
|
+
const baseUrl = `http://127.0.0.1:${options.port}`;
|
|
1770
|
+
return {
|
|
1771
|
+
port: options.port,
|
|
1772
|
+
version: options.version,
|
|
1773
|
+
password,
|
|
1774
|
+
async request(path, init, requestOptions) {
|
|
1775
|
+
const requestInit = options.version === "v2" && password !== null ? (() => {
|
|
1776
|
+
const headers = new Headers(init?.headers);
|
|
1777
|
+
headers.set("Authorization", buildOpenCodeBasicAuthHeader(password));
|
|
1778
|
+
return { ...init, headers };
|
|
1779
|
+
})() : init;
|
|
1780
|
+
try {
|
|
1781
|
+
const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
|
|
1782
|
+
if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
|
|
1783
|
+
const body = await response.text();
|
|
1784
|
+
throw new Error(
|
|
1785
|
+
`OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
return response;
|
|
1789
|
+
} catch (error2) {
|
|
1790
|
+
if (options.version === "v2" && password !== null) {
|
|
1791
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1792
|
+
throw new Error(redactPassword(message, password));
|
|
1793
|
+
}
|
|
1794
|
+
throw error2;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1687
1799
|
|
|
1688
1800
|
// src/lib/opencode/session-db-boot.ts
|
|
1689
1801
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -2363,6 +2475,7 @@ function reportedOpenCodeVersion(input) {
|
|
|
2363
2475
|
|
|
2364
2476
|
// src/lib/opencode/process.ts
|
|
2365
2477
|
import { execSync, spawn as spawn3 } from "child_process";
|
|
2478
|
+
import { randomBytes } from "node:crypto";
|
|
2366
2479
|
|
|
2367
2480
|
// src/lib/process-stop.ts
|
|
2368
2481
|
async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
@@ -2421,6 +2534,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
2421
2534
|
// src/lib/opencode/process.ts
|
|
2422
2535
|
var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
|
|
2423
2536
|
var VALID_OPENCODE_LOG_LEVELS = /* @__PURE__ */ new Set(["DEBUG", "INFO", "WARN", "ERROR"]);
|
|
2537
|
+
var VALID_OPENCODE2_LOG_LEVELS = /* @__PURE__ */ new Set([
|
|
2538
|
+
"all",
|
|
2539
|
+
"trace",
|
|
2540
|
+
"debug",
|
|
2541
|
+
"info",
|
|
2542
|
+
"warn",
|
|
2543
|
+
"warning",
|
|
2544
|
+
"error",
|
|
2545
|
+
"fatal",
|
|
2546
|
+
"none"
|
|
2547
|
+
]);
|
|
2424
2548
|
function resolveOpenCodeLogLevel(env) {
|
|
2425
2549
|
const raw = env.OPENCODE_LOG_LEVEL;
|
|
2426
2550
|
if (!raw) return "INFO";
|
|
@@ -2431,6 +2555,16 @@ function resolveOpenCodeLogLevel(env) {
|
|
|
2431
2555
|
);
|
|
2432
2556
|
return "INFO";
|
|
2433
2557
|
}
|
|
2558
|
+
function resolveOpenCode2LogLevel(env) {
|
|
2559
|
+
const raw = env.OPENCODE_LOG_LEVEL;
|
|
2560
|
+
if (!raw) return "info";
|
|
2561
|
+
const lower = raw.toLowerCase();
|
|
2562
|
+
if (VALID_OPENCODE2_LOG_LEVELS.has(lower)) return lower;
|
|
2563
|
+
console.warn(
|
|
2564
|
+
`startOpenCode2: ignoring invalid OPENCODE_LOG_LEVEL "${raw}" (expected all|trace|debug|info|warn|warning|error|fatal|none) \u2014 using info`
|
|
2565
|
+
);
|
|
2566
|
+
return "info";
|
|
2567
|
+
}
|
|
2434
2568
|
function getProcessCwd(pid) {
|
|
2435
2569
|
const platform = process.platform;
|
|
2436
2570
|
try {
|
|
@@ -2613,6 +2747,37 @@ async function startOpenCode(port, options = {}) {
|
|
|
2613
2747
|
});
|
|
2614
2748
|
return child;
|
|
2615
2749
|
}
|
|
2750
|
+
async function startOpenCode2(port, options = {}) {
|
|
2751
|
+
const password = randomBytes(24).toString("hex");
|
|
2752
|
+
let command = "opencode2";
|
|
2753
|
+
const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
|
|
2754
|
+
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
|
|
2755
|
+
try {
|
|
2756
|
+
execSync("which opencode2", { stdio: "ignore" });
|
|
2757
|
+
} catch {
|
|
2758
|
+
command = "npx";
|
|
2759
|
+
args = [
|
|
2760
|
+
"-y",
|
|
2761
|
+
"-p",
|
|
2762
|
+
"@opencode-ai/cli@beta",
|
|
2763
|
+
"--",
|
|
2764
|
+
"opencode2",
|
|
2765
|
+
"serve",
|
|
2766
|
+
"--port",
|
|
2767
|
+
port.toString(),
|
|
2768
|
+
"--hostname",
|
|
2769
|
+
"127.0.0.1",
|
|
2770
|
+
...logLevel
|
|
2771
|
+
];
|
|
2772
|
+
}
|
|
2773
|
+
const child = spawn3(command, args, {
|
|
2774
|
+
env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
|
|
2775
|
+
detached: true,
|
|
2776
|
+
stdio: options.inheritStdio ? "inherit" : "ignore",
|
|
2777
|
+
cwd: process.cwd()
|
|
2778
|
+
});
|
|
2779
|
+
return { child, password };
|
|
2780
|
+
}
|
|
2616
2781
|
function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
|
|
2617
2782
|
const sendSignal = (signal) => {
|
|
2618
2783
|
if (process.platform === "win32") {
|
|
@@ -2760,27 +2925,500 @@ function buildNoProviderWarning(hasProvider) {
|
|
|
2760
2925
|
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
2926
|
}
|
|
2762
2927
|
|
|
2763
|
-
// src/lib/
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2928
|
+
// src/lib/opencode/session-v2.ts
|
|
2929
|
+
function isRecord(value) {
|
|
2930
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2931
|
+
}
|
|
2932
|
+
function finiteNumber(value) {
|
|
2933
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2934
|
+
}
|
|
2935
|
+
function adaptTime(value) {
|
|
2936
|
+
if (!isRecord(value)) return void 0;
|
|
2937
|
+
const created = finiteNumber(value.created);
|
|
2938
|
+
const completed = finiteNumber(value.completed);
|
|
2939
|
+
if (created === void 0 && completed === void 0) return void 0;
|
|
2940
|
+
return {
|
|
2941
|
+
...created !== void 0 ? { created } : {},
|
|
2942
|
+
...completed !== void 0 ? { completed } : {}
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
function adaptTokens(value) {
|
|
2946
|
+
if (!isRecord(value)) return void 0;
|
|
2947
|
+
const input = finiteNumber(value.input);
|
|
2948
|
+
const output = finiteNumber(value.output);
|
|
2949
|
+
const reasoning = finiteNumber(value.reasoning);
|
|
2950
|
+
const cache = isRecord(value.cache) ? {
|
|
2951
|
+
...finiteNumber(value.cache.read) !== void 0 ? { read: finiteNumber(value.cache.read) } : {},
|
|
2952
|
+
...finiteNumber(value.cache.write) !== void 0 ? { write: finiteNumber(value.cache.write) } : {}
|
|
2953
|
+
} : void 0;
|
|
2954
|
+
if (input === void 0 && output === void 0 && reasoning === void 0 && !cache) {
|
|
2955
|
+
return void 0;
|
|
2956
|
+
}
|
|
2957
|
+
return {
|
|
2958
|
+
...input !== void 0 ? { input } : {},
|
|
2959
|
+
...output !== void 0 ? { output } : {},
|
|
2960
|
+
...reasoning !== void 0 ? { reasoning } : {},
|
|
2961
|
+
...cache ? { cache } : {}
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2964
|
+
function adaptMessageInfo(value, role) {
|
|
2965
|
+
const info = {
|
|
2966
|
+
id: value.id,
|
|
2967
|
+
role
|
|
2968
|
+
};
|
|
2969
|
+
const time = adaptTime(value.time);
|
|
2970
|
+
if (time) info.time = time;
|
|
2971
|
+
if (typeof value.finish === "string") info.finish = value.finish;
|
|
2972
|
+
if ("error" in value) info.error = value.error;
|
|
2973
|
+
if (typeof value.agent === "string") info.agent = value.agent;
|
|
2974
|
+
if (isRecord(value.model)) {
|
|
2975
|
+
if (typeof value.model.id === "string") info.modelID = value.model.id;
|
|
2976
|
+
if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
|
|
2977
|
+
}
|
|
2978
|
+
if (typeof value.cost === "number" && Number.isFinite(value.cost)) info.cost = value.cost;
|
|
2979
|
+
const tokens = adaptTokens(value.tokens);
|
|
2980
|
+
if (tokens) info.tokens = tokens;
|
|
2981
|
+
return info;
|
|
2982
|
+
}
|
|
2983
|
+
function adaptV2Message(value) {
|
|
2984
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.type !== "string") {
|
|
2985
|
+
return null;
|
|
2986
|
+
}
|
|
2987
|
+
if (value.type === "user") {
|
|
2988
|
+
if (typeof value.text !== "string") return null;
|
|
2989
|
+
return {
|
|
2990
|
+
info: adaptMessageInfo(value, "user"),
|
|
2991
|
+
parts: [{ type: "text", text: value.text }]
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
|
|
2995
|
+
const parts = [];
|
|
2996
|
+
for (const content of value.content) {
|
|
2997
|
+
if (!isRecord(content) || typeof content.type !== "string") return null;
|
|
2998
|
+
if (content.type === "text") {
|
|
2999
|
+
if (typeof content.text !== "string") return null;
|
|
3000
|
+
parts.push({ type: "text", text: content.text });
|
|
3001
|
+
} else {
|
|
3002
|
+
parts.push({ type: content.type });
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
return {
|
|
3006
|
+
info: adaptMessageInfo(value, "assistant"),
|
|
3007
|
+
parts
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
function adaptFormTool(value) {
|
|
3011
|
+
if (!isRecord(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
|
|
3012
|
+
return void 0;
|
|
3013
|
+
}
|
|
3014
|
+
return { messageID: value.messageID, callID: value.id };
|
|
3015
|
+
}
|
|
3016
|
+
function adaptV2FormWire(value) {
|
|
3017
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
3018
|
+
return null;
|
|
3019
|
+
}
|
|
3020
|
+
return value;
|
|
3021
|
+
}
|
|
3022
|
+
function adaptV2FormField(value, header) {
|
|
3023
|
+
if (!isRecord(value)) return null;
|
|
3024
|
+
const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
|
|
3025
|
+
if (!question) return null;
|
|
3026
|
+
const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
|
|
3027
|
+
if (!isRecord(option)) return [];
|
|
3028
|
+
const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
|
|
3029
|
+
if (!label) return [];
|
|
3030
|
+
return [
|
|
3031
|
+
{
|
|
3032
|
+
label,
|
|
3033
|
+
description: typeof option.description === "string" ? option.description : ""
|
|
3034
|
+
}
|
|
3035
|
+
];
|
|
3036
|
+
}) : [];
|
|
3037
|
+
return { question, header, options };
|
|
3038
|
+
}
|
|
3039
|
+
function adaptV2Form(value) {
|
|
3040
|
+
const form = adaptV2FormWire(value);
|
|
3041
|
+
if (!form || !Array.isArray(form.fields)) return null;
|
|
3042
|
+
const header = typeof form.title === "string" ? form.title : "";
|
|
3043
|
+
const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
|
|
3044
|
+
if (questions.length === 0) return null;
|
|
3045
|
+
const tool = isRecord(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
|
|
3046
|
+
return {
|
|
3047
|
+
id: form.id,
|
|
3048
|
+
sessionID: form.sessionID,
|
|
3049
|
+
questions,
|
|
3050
|
+
...tool ? { tool } : {},
|
|
3051
|
+
raw: form
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
function adaptV2FormList(value) {
|
|
3055
|
+
if (!isRecord(value) || !Array.isArray(value.data)) return null;
|
|
3056
|
+
return value.data.map(adaptV2Form).filter((form) => form !== null);
|
|
3057
|
+
}
|
|
3058
|
+
function adaptPattern(value) {
|
|
3059
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
3060
|
+
if (Array.isArray(value) && value.every((pattern) => typeof pattern === "string")) {
|
|
3061
|
+
return value;
|
|
3062
|
+
}
|
|
3063
|
+
return void 0;
|
|
3064
|
+
}
|
|
3065
|
+
function adaptV2PermissionWire(value) {
|
|
3066
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
3067
|
+
return null;
|
|
3068
|
+
}
|
|
3069
|
+
if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
|
|
3070
|
+
return value;
|
|
3071
|
+
}
|
|
3072
|
+
function adaptV2Permission(value) {
|
|
3073
|
+
const permission = adaptV2PermissionWire(value);
|
|
3074
|
+
if (!permission) return null;
|
|
3075
|
+
const type = permission.permission ?? permission.action;
|
|
3076
|
+
if (!type) return null;
|
|
3077
|
+
const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
|
|
3078
|
+
const time = isRecord(permission.time) ? finiteNumber(permission.time.created) !== void 0 ? { created: finiteNumber(permission.time.created) } : void 0 : void 0;
|
|
3079
|
+
return {
|
|
3080
|
+
id: permission.id,
|
|
3081
|
+
type,
|
|
3082
|
+
sessionID: permission.sessionID,
|
|
3083
|
+
metadata: isRecord(permission.metadata) ? permission.metadata : {},
|
|
3084
|
+
raw: permission,
|
|
3085
|
+
...pattern !== void 0 ? { pattern } : {},
|
|
3086
|
+
...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
|
|
3087
|
+
...typeof permission.callID === "string" ? { callID: permission.callID } : {},
|
|
3088
|
+
...typeof permission.title === "string" ? { title: permission.title } : {},
|
|
3089
|
+
...time ? { time } : {}
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
function adaptV2PermissionList(value) {
|
|
3093
|
+
if (!isRecord(value) || !Array.isArray(value.data)) return null;
|
|
3094
|
+
return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
|
|
3095
|
+
}
|
|
3096
|
+
function adaptV2Session(value) {
|
|
3097
|
+
if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) return null;
|
|
3098
|
+
const time = isRecord(value.time) ? {
|
|
3099
|
+
...finiteNumber(value.time.created) !== void 0 ? { created: finiteNumber(value.time.created) } : {},
|
|
3100
|
+
...finiteNumber(value.time.updated) !== void 0 ? { updated: finiteNumber(value.time.updated) } : {}
|
|
3101
|
+
} : void 0;
|
|
3102
|
+
return {
|
|
3103
|
+
id: value.id,
|
|
3104
|
+
...typeof value.title === "string" ? { title: value.title } : {},
|
|
3105
|
+
...typeof value.parentID === "string" ? { parentID: value.parentID } : {},
|
|
3106
|
+
...time && Object.keys(time).length > 0 ? { time } : {}
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
3109
|
+
function adaptV2SessionList(value) {
|
|
3110
|
+
if (!isRecord(value) || !Array.isArray(value.data) || !isRecord(value.cursor)) return null;
|
|
3111
|
+
return {
|
|
3112
|
+
data: value.data.map(adaptV2Session).filter((session) => session !== null),
|
|
3113
|
+
cursor: value.cursor
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
function adaptV2Location(value) {
|
|
3117
|
+
const candidates = [
|
|
3118
|
+
value,
|
|
3119
|
+
isRecord(value) ? value.data : void 0,
|
|
3120
|
+
isRecord(value) ? value.location : void 0
|
|
3121
|
+
];
|
|
3122
|
+
for (const candidate of candidates) {
|
|
3123
|
+
if (!isRecord(candidate) || typeof candidate.directory !== "string") continue;
|
|
3124
|
+
const directory = candidate.directory.trim();
|
|
3125
|
+
if (directory) return directory;
|
|
3126
|
+
}
|
|
3127
|
+
return null;
|
|
3128
|
+
}
|
|
3129
|
+
function adaptV2Messages(value) {
|
|
3130
|
+
if (!isRecord(value) || !Array.isArray(value.data)) return [];
|
|
3131
|
+
return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
|
|
3132
|
+
}
|
|
3133
|
+
async function readJson(response) {
|
|
3134
|
+
try {
|
|
3135
|
+
return await response.json();
|
|
3136
|
+
} catch (error2) {
|
|
3137
|
+
throw new Error(
|
|
3138
|
+
`OpenCode V2 response was not valid JSON: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
async function readData(client, path, init) {
|
|
3143
|
+
const response = await client.request(path, init);
|
|
3144
|
+
const body = await readJson(response);
|
|
3145
|
+
if (!isRecord(body) || !("data" in body)) {
|
|
3146
|
+
throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
|
|
3147
|
+
}
|
|
3148
|
+
return body.data;
|
|
3149
|
+
}
|
|
3150
|
+
var OpenCodeV2PromptAckError = class extends Error {
|
|
3151
|
+
constructor(message) {
|
|
3152
|
+
super(message);
|
|
3153
|
+
this.name = "OpenCodeV2PromptAckError";
|
|
3154
|
+
}
|
|
3155
|
+
};
|
|
3156
|
+
async function getOpenCodeDirectoryV2(client) {
|
|
3157
|
+
try {
|
|
3158
|
+
return adaptV2Location(await readJson(await client.request("/api/location")));
|
|
3159
|
+
} catch (error2) {
|
|
3160
|
+
console.error(
|
|
3161
|
+
`[getOpenCodeDirectoryV2] GET /api/location failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3162
|
+
);
|
|
3163
|
+
return null;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
async function createV2Session(client, directory) {
|
|
3167
|
+
const data = await readData(client, "/api/session", {
|
|
3168
|
+
method: "POST",
|
|
3169
|
+
headers: { "Content-Type": "application/json" },
|
|
3170
|
+
body: JSON.stringify({ location: { directory } })
|
|
3171
|
+
});
|
|
3172
|
+
if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
3173
|
+
throw new Error("OpenCode V2 create session response was missing data.id");
|
|
3174
|
+
}
|
|
3175
|
+
return data.id;
|
|
3176
|
+
}
|
|
3177
|
+
async function getV2Session(client, sessionId) {
|
|
3178
|
+
const data = await readData(client, `/api/session/${encodeURIComponent(sessionId)}`);
|
|
3179
|
+
const session = adaptV2Session(data);
|
|
3180
|
+
if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
|
|
3181
|
+
return session;
|
|
3182
|
+
}
|
|
3183
|
+
async function listV2SessionPage(client, cursor) {
|
|
3184
|
+
const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
|
|
3185
|
+
try {
|
|
3186
|
+
return adaptV2SessionList(await readJson(await client.request(path)));
|
|
3187
|
+
} catch (error2) {
|
|
3188
|
+
console.error(
|
|
3189
|
+
`[listV2SessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3190
|
+
);
|
|
3191
|
+
return null;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
async function listV2Sessions(client) {
|
|
3195
|
+
const sessions = [];
|
|
3196
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3197
|
+
let cursor;
|
|
3198
|
+
let hasNextPage = true;
|
|
3199
|
+
try {
|
|
3200
|
+
while (hasNextPage) {
|
|
3201
|
+
const page = await listV2SessionPage(client, cursor);
|
|
3202
|
+
if (!page) return null;
|
|
3203
|
+
sessions.push(...page.data);
|
|
3204
|
+
const next = page.cursor.next;
|
|
3205
|
+
if (next === void 0 || next === null) {
|
|
3206
|
+
hasNextPage = false;
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3209
|
+
if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
|
|
3210
|
+
throw new Error("OpenCode V2 session list contained an invalid next cursor");
|
|
3211
|
+
}
|
|
3212
|
+
seenCursors.add(next);
|
|
3213
|
+
cursor = next;
|
|
3214
|
+
}
|
|
3215
|
+
return sessions;
|
|
3216
|
+
} catch (error2) {
|
|
3217
|
+
console.error(
|
|
3218
|
+
`[listV2Sessions] session pagination failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3219
|
+
);
|
|
3220
|
+
return null;
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
async function deleteV2Session(client, sessionId) {
|
|
3224
|
+
try {
|
|
3225
|
+
await client.request(`/api/session/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
|
|
3226
|
+
return true;
|
|
3227
|
+
} catch (error2) {
|
|
3228
|
+
console.error(
|
|
3229
|
+
`[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3230
|
+
);
|
|
3231
|
+
return false;
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
async function v2SessionExists(client, sessionId) {
|
|
3235
|
+
try {
|
|
3236
|
+
const response = await client.request(
|
|
3237
|
+
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
3238
|
+
void 0,
|
|
3239
|
+
{ allowStatuses: [404] }
|
|
3240
|
+
);
|
|
3241
|
+
return response.status === 404 ? false : true;
|
|
3242
|
+
} catch (error2) {
|
|
3243
|
+
console.error(
|
|
3244
|
+
`[v2SessionExists] GET /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3245
|
+
);
|
|
3246
|
+
return null;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
async function sendV2Prompt(client, sessionId, text) {
|
|
3250
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/prompt`;
|
|
3251
|
+
const response = await client.request(path, {
|
|
3252
|
+
method: "POST",
|
|
3253
|
+
headers: { "Content-Type": "application/json" },
|
|
3254
|
+
body: JSON.stringify({ text, delivery: "queue" })
|
|
3255
|
+
});
|
|
3256
|
+
let body;
|
|
3257
|
+
try {
|
|
3258
|
+
body = await readJson(response);
|
|
3259
|
+
} catch (error2) {
|
|
3260
|
+
throw new OpenCodeV2PromptAckError(
|
|
3261
|
+
`OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3262
|
+
);
|
|
3263
|
+
}
|
|
3264
|
+
const data = isRecord(body) && "data" in body ? body.data : void 0;
|
|
3265
|
+
if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
3266
|
+
throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
|
|
3267
|
+
}
|
|
3268
|
+
return data.id;
|
|
3269
|
+
}
|
|
3270
|
+
async function getV2SessionMessages(client, sessionId) {
|
|
3271
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
|
|
3272
|
+
try {
|
|
3273
|
+
const body = await readJson(await client.request(path));
|
|
3274
|
+
if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) return null;
|
|
3275
|
+
return adaptV2Messages(body);
|
|
3276
|
+
} catch (error2) {
|
|
3277
|
+
console.error(
|
|
3278
|
+
`[getV2SessionMessages] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3279
|
+
);
|
|
3280
|
+
return null;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
async function listV2Forms(client, sessionId) {
|
|
3284
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/form`;
|
|
3285
|
+
try {
|
|
3286
|
+
return adaptV2FormList(await readJson(await client.request(path)));
|
|
3287
|
+
} catch (error2) {
|
|
3288
|
+
console.error(
|
|
3289
|
+
`[listV2Forms] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3290
|
+
);
|
|
3291
|
+
return null;
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
async function listV2Permissions(client, sessionId) {
|
|
3295
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/permission`;
|
|
3296
|
+
try {
|
|
3297
|
+
return adaptV2PermissionList(await readJson(await client.request(path)));
|
|
3298
|
+
} catch (error2) {
|
|
3299
|
+
console.error(
|
|
3300
|
+
`[listV2Permissions] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3301
|
+
);
|
|
3302
|
+
return null;
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
async function getV2ActiveSessions(client) {
|
|
3306
|
+
try {
|
|
3307
|
+
const body = await readJson(await client.request("/api/session/active"));
|
|
3308
|
+
if (!isRecord(body) || !isRecord(body.data)) return null;
|
|
3309
|
+
return body.data;
|
|
3310
|
+
} catch (error2) {
|
|
3311
|
+
console.error(
|
|
3312
|
+
`[getV2ActiveSessions] GET /api/session/active failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3313
|
+
);
|
|
3314
|
+
return null;
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
async function isV2SessionOngoing(client, sessionId) {
|
|
3318
|
+
const activeSessions = await getV2ActiveSessions(client);
|
|
3319
|
+
if (activeSessions === null) return null;
|
|
3320
|
+
return Object.prototype.hasOwnProperty.call(activeSessions, sessionId);
|
|
3321
|
+
}
|
|
3322
|
+
function sessionErrorReason(value) {
|
|
3323
|
+
if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
|
|
3324
|
+
if (isRecord(value)) {
|
|
3325
|
+
const data = isRecord(value.data) ? value.data : void 0;
|
|
3326
|
+
const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
|
|
3327
|
+
if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
3328
|
+
}
|
|
3329
|
+
return "OpenCode reported a session error with no details";
|
|
3330
|
+
}
|
|
3331
|
+
function adaptV2SessionErrorEvent(value) {
|
|
3332
|
+
let parsed = value;
|
|
3333
|
+
if (typeof value === "string") {
|
|
3334
|
+
try {
|
|
3335
|
+
parsed = JSON.parse(value);
|
|
3336
|
+
} catch (error2) {
|
|
3337
|
+
void error2;
|
|
3338
|
+
return null;
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
if (!isRecord(parsed)) return null;
|
|
3342
|
+
try {
|
|
3343
|
+
const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
|
|
3344
|
+
if (establishedShape) return establishedShape;
|
|
3345
|
+
} catch (error2) {
|
|
3346
|
+
void error2;
|
|
3347
|
+
}
|
|
3348
|
+
const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord);
|
|
3349
|
+
for (const event of candidates) {
|
|
3350
|
+
if (event.type !== "session.error") continue;
|
|
3351
|
+
const properties = [event.properties, event.data, event].find(isRecord);
|
|
3352
|
+
if (!properties) continue;
|
|
3353
|
+
const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
|
|
3354
|
+
if (!sessionId) continue;
|
|
3355
|
+
return {
|
|
3356
|
+
sessionId,
|
|
3357
|
+
reason: sessionErrorReason(properties.error ?? properties)
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
return null;
|
|
3361
|
+
}
|
|
3362
|
+
async function readV2SessionErrorStream(client, options) {
|
|
3363
|
+
let reader = null;
|
|
3364
|
+
try {
|
|
3365
|
+
const response = await client.request("/api/event", {
|
|
3366
|
+
headers: { accept: "text/event-stream" },
|
|
3367
|
+
signal: options.signal
|
|
3368
|
+
});
|
|
3369
|
+
if (!response.ok || !response.body) {
|
|
3370
|
+
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
3371
|
+
}
|
|
3372
|
+
reader = response.body.getReader();
|
|
3373
|
+
const decoder = new TextDecoder();
|
|
3374
|
+
let buffer = "";
|
|
3375
|
+
const processLine = (line) => {
|
|
3376
|
+
const trimmed = line.trimEnd();
|
|
3377
|
+
if (!trimmed.startsWith("data:")) return;
|
|
3378
|
+
const event = adaptV2SessionErrorEvent(trimmed.slice("data:".length).replace(/^ /, ""));
|
|
3379
|
+
if (event) options.onSessionError(event);
|
|
3380
|
+
};
|
|
3381
|
+
while (true) {
|
|
3382
|
+
const { done, value } = await reader.read();
|
|
3383
|
+
if (done) return { reason: "ended" };
|
|
3384
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3385
|
+
const lines = buffer.split("\n");
|
|
3386
|
+
buffer = lines.pop() ?? "";
|
|
3387
|
+
for (const line of lines) processLine(line);
|
|
3388
|
+
}
|
|
3389
|
+
} catch (error2) {
|
|
3390
|
+
if (options.signal.aborted) return { reason: "aborted" };
|
|
3391
|
+
return {
|
|
3392
|
+
reason: "unavailable",
|
|
3393
|
+
detail: error2 instanceof Error ? error2.message : String(error2)
|
|
3394
|
+
};
|
|
3395
|
+
} finally {
|
|
3396
|
+
if (reader) void reader.cancel().catch(() => void 0);
|
|
3397
|
+
}
|
|
2767
3398
|
}
|
|
2768
3399
|
|
|
2769
3400
|
// src/lib/opencode/session.ts
|
|
3401
|
+
var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
2770
3402
|
function timedFetch(input, init) {
|
|
2771
3403
|
return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
|
|
2772
3404
|
}
|
|
3405
|
+
function requestWithClient(port, client, path, init, options) {
|
|
3406
|
+
return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
|
|
3407
|
+
}
|
|
2773
3408
|
function opencodeBase(port) {
|
|
2774
3409
|
return `http://127.0.0.1:${port}`;
|
|
2775
3410
|
}
|
|
2776
|
-
async function getOpenCodeDirectory(port) {
|
|
3411
|
+
async function getOpenCodeDirectory(port, client) {
|
|
2777
3412
|
try {
|
|
2778
|
-
const res = await
|
|
3413
|
+
const res = await requestWithClient(port, client, "/path");
|
|
2779
3414
|
if (!res.ok) return null;
|
|
2780
3415
|
const body = await res.json();
|
|
2781
3416
|
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
3417
|
return dir && dir.trim() ? dir.trim() : null;
|
|
2783
|
-
} catch {
|
|
3418
|
+
} catch (error2) {
|
|
3419
|
+
console.error(
|
|
3420
|
+
`[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3421
|
+
);
|
|
2784
3422
|
return null;
|
|
2785
3423
|
}
|
|
2786
3424
|
}
|
|
@@ -2824,16 +3462,48 @@ function isAssistantInFlight(m) {
|
|
|
2824
3462
|
if (completedOf(m) == null) return true;
|
|
2825
3463
|
return finishOf(m) === "tool-calls";
|
|
2826
3464
|
}
|
|
2827
|
-
async function getSessionMessages(port, sessionId) {
|
|
3465
|
+
async function getSessionMessages(port, sessionId, client) {
|
|
2828
3466
|
try {
|
|
2829
|
-
const
|
|
3467
|
+
const path = `/session/${sessionId}/message`;
|
|
3468
|
+
const res = await requestWithClient(port, client, path);
|
|
2830
3469
|
if (!res.ok) return null;
|
|
2831
3470
|
const body = await res.json();
|
|
2832
3471
|
return Array.isArray(body) ? body : null;
|
|
2833
|
-
} catch {
|
|
3472
|
+
} catch (error2) {
|
|
3473
|
+
console.error(
|
|
3474
|
+
`[getSessionMessages] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3475
|
+
);
|
|
2834
3476
|
return null;
|
|
2835
3477
|
}
|
|
2836
3478
|
}
|
|
3479
|
+
async function fetchSessionMessages(port, sessionId, client) {
|
|
3480
|
+
const response = await requestWithClient(port, client, `/session/${sessionId}/message`);
|
|
3481
|
+
if (!response.ok) return null;
|
|
3482
|
+
const body = await response.json();
|
|
3483
|
+
return Array.isArray(body) ? body : null;
|
|
3484
|
+
}
|
|
3485
|
+
async function pollSessionMessagesForRedrive(port, sessionId, client) {
|
|
3486
|
+
try {
|
|
3487
|
+
const response = await requestWithClient(
|
|
3488
|
+
port,
|
|
3489
|
+
client,
|
|
3490
|
+
`/session/${sessionId}/message`,
|
|
3491
|
+
void 0,
|
|
3492
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
3493
|
+
);
|
|
3494
|
+
if (!response.ok) {
|
|
3495
|
+
return { ok: false, status: response.status, body: await response.text(), malformed: false };
|
|
3496
|
+
}
|
|
3497
|
+
const body = await response.json();
|
|
3498
|
+
if (!Array.isArray(body)) return { ok: false, status: null, body: "", malformed: true };
|
|
3499
|
+
return { ok: true, messages: body };
|
|
3500
|
+
} catch (error2) {
|
|
3501
|
+
console.error(
|
|
3502
|
+
`[pollSessionMessagesForRedrive] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3503
|
+
);
|
|
3504
|
+
return { ok: false, status: null, body: "", malformed: false };
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
2837
3507
|
function isSessionActivelyGenerating(messages) {
|
|
2838
3508
|
if (!messages || messages.length === 0) return false;
|
|
2839
3509
|
const last = messages[messages.length - 1];
|
|
@@ -2854,27 +3524,37 @@ function sessionLastActivityMs(session) {
|
|
|
2854
3524
|
}
|
|
2855
3525
|
return null;
|
|
2856
3526
|
}
|
|
2857
|
-
async function listSessions(port) {
|
|
3527
|
+
async function listSessions(port, client) {
|
|
3528
|
+
if (client?.version === "v2") return listV2Sessions(client);
|
|
2858
3529
|
try {
|
|
2859
|
-
const res = await
|
|
3530
|
+
const res = await requestWithClient(port, client, "/session");
|
|
2860
3531
|
if (!res.ok) return null;
|
|
2861
3532
|
const body = await res.json();
|
|
2862
3533
|
return Array.isArray(body) ? body : null;
|
|
2863
|
-
} catch {
|
|
3534
|
+
} catch (error2) {
|
|
3535
|
+
console.error(
|
|
3536
|
+
`[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3537
|
+
);
|
|
2864
3538
|
return null;
|
|
2865
3539
|
}
|
|
2866
3540
|
}
|
|
2867
|
-
async function deleteSession(port, id) {
|
|
3541
|
+
async function deleteSession(port, id, client) {
|
|
3542
|
+
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
2868
3543
|
try {
|
|
2869
|
-
const res = await
|
|
3544
|
+
const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
|
|
2870
3545
|
return res.status >= 200 && res.status < 300;
|
|
2871
|
-
} catch {
|
|
3546
|
+
} catch (error2) {
|
|
3547
|
+
console.error(
|
|
3548
|
+
`[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3549
|
+
);
|
|
2872
3550
|
return false;
|
|
2873
3551
|
}
|
|
2874
3552
|
}
|
|
2875
|
-
async function sessionExists(port, id) {
|
|
3553
|
+
async function sessionExists(port, id, client) {
|
|
2876
3554
|
try {
|
|
2877
|
-
const res = await
|
|
3555
|
+
const res = await requestWithClient(port, client, `/session/${id}`, void 0, {
|
|
3556
|
+
allowStatuses: [404]
|
|
3557
|
+
});
|
|
2878
3558
|
if (res.status >= 200 && res.status < 300) return true;
|
|
2879
3559
|
if (res.status === 404) return false;
|
|
2880
3560
|
return null;
|
|
@@ -2882,9 +3562,22 @@ async function sessionExists(port, id) {
|
|
|
2882
3562
|
return null;
|
|
2883
3563
|
}
|
|
2884
3564
|
}
|
|
2885
|
-
async function
|
|
3565
|
+
async function getOpenCodeSession(port, id, client) {
|
|
3566
|
+
try {
|
|
3567
|
+
const response = await requestWithClient(port, client, `/session/${id}`);
|
|
3568
|
+
const body = await response.json();
|
|
3569
|
+
return body && typeof body === "object" && !Array.isArray(body) ? body : null;
|
|
3570
|
+
} catch (error2) {
|
|
3571
|
+
console.error(
|
|
3572
|
+
`[getOpenCodeSession] GET /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3573
|
+
);
|
|
3574
|
+
return null;
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
async function getSessionStatuses(port, client) {
|
|
3578
|
+
if (client?.version === "v2") return null;
|
|
2886
3579
|
try {
|
|
2887
|
-
const res = await
|
|
3580
|
+
const res = await requestWithClient(port, client, "/session/status");
|
|
2888
3581
|
if (!res.ok) {
|
|
2889
3582
|
console.error(
|
|
2890
3583
|
`[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
|
|
@@ -2906,22 +3599,28 @@ async function getSessionStatuses(port) {
|
|
|
2906
3599
|
return null;
|
|
2907
3600
|
}
|
|
2908
3601
|
}
|
|
2909
|
-
async function isSessionOngoing(port, id) {
|
|
2910
|
-
|
|
3602
|
+
async function isSessionOngoing(port, id, client) {
|
|
3603
|
+
if (client?.version === "v2") return isV2SessionOngoing(client, id);
|
|
3604
|
+
const map = await getSessionStatuses(port, client);
|
|
2911
3605
|
if (map == null) return null;
|
|
2912
3606
|
const entry = map[id];
|
|
2913
3607
|
return entry != null && entry.type !== "idle";
|
|
2914
3608
|
}
|
|
2915
|
-
async function createOpenCodeSession(port, directory) {
|
|
2916
|
-
const
|
|
2917
|
-
if (directory && directory.trim())
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
3609
|
+
async function createOpenCodeSession(port, directory, client) {
|
|
3610
|
+
const path = new URL(`${opencodeBase(port)}/session`);
|
|
3611
|
+
if (directory && directory.trim()) path.searchParams.set("directory", directory.trim());
|
|
3612
|
+
const requestPath = `${path.pathname}${path.search}`;
|
|
3613
|
+
const response = await requestWithClient(
|
|
3614
|
+
port,
|
|
3615
|
+
client,
|
|
3616
|
+
requestPath,
|
|
3617
|
+
{
|
|
3618
|
+
method: "POST",
|
|
3619
|
+
headers: { "Content-Type": "application/json" },
|
|
3620
|
+
body: JSON.stringify({})
|
|
3621
|
+
},
|
|
3622
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
3623
|
+
);
|
|
2925
3624
|
if (!response.ok) {
|
|
2926
3625
|
const text = await response.text().catch(() => "");
|
|
2927
3626
|
throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ""}`);
|
|
@@ -2929,10 +3628,16 @@ async function createOpenCodeSession(port, directory) {
|
|
|
2929
3628
|
const data = await response.json();
|
|
2930
3629
|
return data.id;
|
|
2931
3630
|
}
|
|
2932
|
-
async function getModelAttachmentCapability(port, model) {
|
|
3631
|
+
async function getModelAttachmentCapability(port, model, client) {
|
|
2933
3632
|
const { model: baseModel } = splitModelVariant(model);
|
|
3633
|
+
if (client?.version === "v2") {
|
|
3634
|
+
console.error(
|
|
3635
|
+
`[getModelAttachmentCapability] V2 provider capabilities are unavailable; using text-only fallback (port ${port})`
|
|
3636
|
+
);
|
|
3637
|
+
return null;
|
|
3638
|
+
}
|
|
2934
3639
|
try {
|
|
2935
|
-
const res = await
|
|
3640
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
2936
3641
|
if (!res.ok) {
|
|
2937
3642
|
console.error(
|
|
2938
3643
|
`[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
@@ -3050,21 +3755,45 @@ function applyModelOptions(body, options) {
|
|
|
3050
3755
|
};
|
|
3051
3756
|
}
|
|
3052
3757
|
}
|
|
3053
|
-
if (variant) body.variant = variant;
|
|
3758
|
+
if (variant) body.variant = variant;
|
|
3759
|
+
}
|
|
3760
|
+
async function listOpenCodeQuestions(port, client) {
|
|
3761
|
+
try {
|
|
3762
|
+
const response = await requestWithClient(port, client, "/question");
|
|
3763
|
+
const body = await response.json();
|
|
3764
|
+
return Array.isArray(body) ? body : null;
|
|
3765
|
+
} catch (error2) {
|
|
3766
|
+
console.error(
|
|
3767
|
+
`[listOpenCodeQuestions] GET /question failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3768
|
+
);
|
|
3769
|
+
return null;
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
async function listOpenCodePermissions(port, client) {
|
|
3773
|
+
try {
|
|
3774
|
+
const response = await requestWithClient(port, client, "/permission");
|
|
3775
|
+
const body = await response.json();
|
|
3776
|
+
return Array.isArray(body) ? body : null;
|
|
3777
|
+
} catch (error2) {
|
|
3778
|
+
console.error(
|
|
3779
|
+
`[listOpenCodePermissions] GET /permission failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3780
|
+
);
|
|
3781
|
+
return null;
|
|
3782
|
+
}
|
|
3054
3783
|
}
|
|
3055
3784
|
function messageText(m) {
|
|
3056
3785
|
if (!m || !Array.isArray(m.parts)) return "";
|
|
3057
3786
|
return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
3058
3787
|
}
|
|
3059
|
-
async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
3060
|
-
const before = await getSessionMessages(port, sessionId);
|
|
3788
|
+
async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
|
|
3789
|
+
const before = await getSessionMessages(port, sessionId, client);
|
|
3061
3790
|
const knownUserIds = new Set(
|
|
3062
3791
|
(before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
|
|
3063
3792
|
);
|
|
3064
3793
|
const parts = [{ type: "text", text: content }];
|
|
3065
3794
|
let pendingOutcomes = null;
|
|
3066
3795
|
if (attachments && attachments.inputs.length > 0) {
|
|
3067
|
-
const capable = await getModelAttachmentCapability(port, options?.model);
|
|
3796
|
+
const capable = await getModelAttachmentCapability(port, options?.model, client);
|
|
3068
3797
|
const {
|
|
3069
3798
|
parts: fileParts,
|
|
3070
3799
|
outcomes,
|
|
@@ -3077,11 +3806,17 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
3077
3806
|
parts
|
|
3078
3807
|
};
|
|
3079
3808
|
applyModelOptions(body, options);
|
|
3080
|
-
const res = await
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3809
|
+
const res = await requestWithClient(
|
|
3810
|
+
port,
|
|
3811
|
+
client,
|
|
3812
|
+
`/session/${sessionId}/prompt_async`,
|
|
3813
|
+
{
|
|
3814
|
+
method: "POST",
|
|
3815
|
+
headers: { "Content-Type": "application/json" },
|
|
3816
|
+
body: JSON.stringify(body)
|
|
3817
|
+
},
|
|
3818
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
3819
|
+
);
|
|
3085
3820
|
if (res.status < 200 || res.status >= 300) {
|
|
3086
3821
|
const text = await res.text().catch(() => "");
|
|
3087
3822
|
const { variant } = splitModelVariant(options?.model);
|
|
@@ -3092,7 +3827,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
3092
3827
|
const READ_BACK_ATTEMPTS = 5;
|
|
3093
3828
|
const READ_BACK_DELAY_MS = 150;
|
|
3094
3829
|
for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
|
|
3095
|
-
const after = await getSessionMessages(port, sessionId);
|
|
3830
|
+
const after = await getSessionMessages(port, sessionId, client);
|
|
3096
3831
|
if (after) {
|
|
3097
3832
|
let best = null;
|
|
3098
3833
|
for (const m of after) {
|
|
@@ -3195,7 +3930,7 @@ function collectSubagentSessions(messages, userMessageId) {
|
|
|
3195
3930
|
}
|
|
3196
3931
|
return refs;
|
|
3197
3932
|
}
|
|
3198
|
-
function
|
|
3933
|
+
function finiteNumber2(value) {
|
|
3199
3934
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3200
3935
|
}
|
|
3201
3936
|
function taskCallModel(value) {
|
|
@@ -3224,8 +3959,8 @@ function collectTaskCalls(messages, userMessageId) {
|
|
|
3224
3959
|
parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
|
|
3225
3960
|
model: taskCallModel(metadata?.model),
|
|
3226
3961
|
status: part.state.status ?? "unknown",
|
|
3227
|
-
timeStart:
|
|
3228
|
-
timeEnd:
|
|
3962
|
+
timeStart: finiteNumber2(part.state.time?.start),
|
|
3963
|
+
timeEnd: finiteNumber2(part.state.time?.end)
|
|
3229
3964
|
});
|
|
3230
3965
|
}
|
|
3231
3966
|
}
|
|
@@ -3240,7 +3975,7 @@ function attributeTaskCallUsage(messages, windows) {
|
|
|
3240
3975
|
const unattributed = [];
|
|
3241
3976
|
for (const message of messages ?? []) {
|
|
3242
3977
|
if (roleOf(message) !== "assistant") continue;
|
|
3243
|
-
const created =
|
|
3978
|
+
const created = finiteNumber2(createdOf(message));
|
|
3244
3979
|
const matching = created === null ? [] : eligibleWindows.filter(
|
|
3245
3980
|
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
3246
3981
|
);
|
|
@@ -3481,9 +4216,51 @@ function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageI
|
|
|
3481
4216
|
}
|
|
3482
4217
|
return hasLaterUser && hasStartedLaterUser;
|
|
3483
4218
|
}
|
|
3484
|
-
async function hasAnyConfiguredProvider(port) {
|
|
4219
|
+
async function hasAnyConfiguredProvider(port, client) {
|
|
4220
|
+
if (client?.version === "v2") {
|
|
4221
|
+
const directory = await getOpenCodeDirectoryV2(client);
|
|
4222
|
+
if (!directory) {
|
|
4223
|
+
console.error(
|
|
4224
|
+
`[hasAnyConfiguredProvider] V2 working directory was unavailable (port ${port})`
|
|
4225
|
+
);
|
|
4226
|
+
return null;
|
|
4227
|
+
}
|
|
4228
|
+
const path = `/api/integration?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
|
|
4229
|
+
try {
|
|
4230
|
+
const res = await client.request(path);
|
|
4231
|
+
if (!res.ok) {
|
|
4232
|
+
console.error(
|
|
4233
|
+
`[hasAnyConfiguredProvider] GET ${path} returned HTTP ${res.status} (port ${port})`
|
|
4234
|
+
);
|
|
4235
|
+
return null;
|
|
4236
|
+
}
|
|
4237
|
+
const body = await res.json();
|
|
4238
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
|
|
4239
|
+
console.error(
|
|
4240
|
+
`[hasAnyConfiguredProvider] GET ${path} body had no integration data array (port ${port})`
|
|
4241
|
+
);
|
|
4242
|
+
return null;
|
|
4243
|
+
}
|
|
4244
|
+
for (const integration of body.data) {
|
|
4245
|
+
if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
4246
|
+
console.error(
|
|
4247
|
+
`[hasAnyConfiguredProvider] GET ${path} body contained an invalid integration (port ${port})`
|
|
4248
|
+
);
|
|
4249
|
+
return null;
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
return body.data.some(
|
|
4253
|
+
(integration) => Array.isArray(integration.connections) && integration.connections.length > 0
|
|
4254
|
+
);
|
|
4255
|
+
} catch (error2) {
|
|
4256
|
+
console.error(
|
|
4257
|
+
`[hasAnyConfiguredProvider] GET ${path} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4258
|
+
);
|
|
4259
|
+
return null;
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
3485
4262
|
try {
|
|
3486
|
-
const res = await
|
|
4263
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
3487
4264
|
if (!res.ok) {
|
|
3488
4265
|
console.error(
|
|
3489
4266
|
`[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
@@ -3512,7 +4289,7 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3512
4289
|
return null;
|
|
3513
4290
|
}
|
|
3514
4291
|
}
|
|
3515
|
-
function
|
|
4292
|
+
function sessionErrorReason2(error2) {
|
|
3516
4293
|
const record = typeof error2 === "object" && error2 !== null ? error2 : null;
|
|
3517
4294
|
const data = record?.data;
|
|
3518
4295
|
const dataRecord = typeof data === "object" && data !== null ? data : null;
|
|
@@ -3542,16 +4319,20 @@ function parseSessionErrorFrame(data) {
|
|
|
3542
4319
|
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
3543
4320
|
return {
|
|
3544
4321
|
sessionId,
|
|
3545
|
-
reason:
|
|
4322
|
+
reason: sessionErrorReason2(propertiesRecord.error)
|
|
3546
4323
|
};
|
|
3547
4324
|
}
|
|
3548
|
-
async function readSessionErrorStream(port, options) {
|
|
4325
|
+
async function readSessionErrorStream(port, options, client) {
|
|
4326
|
+
if (client?.version === "v2") return readV2SessionErrorStream(client, options);
|
|
3549
4327
|
let reader = null;
|
|
3550
4328
|
try {
|
|
3551
|
-
const response = await
|
|
4329
|
+
const response = await (client?.request("/event", {
|
|
3552
4330
|
headers: { accept: "text/event-stream" },
|
|
3553
4331
|
signal: options.signal
|
|
3554
|
-
})
|
|
4332
|
+
}) ?? fetch(`${opencodeBase(port)}/event`, {
|
|
4333
|
+
headers: { accept: "text/event-stream" },
|
|
4334
|
+
signal: options.signal
|
|
4335
|
+
}));
|
|
3555
4336
|
if (!response.ok || !response.body) {
|
|
3556
4337
|
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
3557
4338
|
}
|
|
@@ -3582,9 +4363,10 @@ async function readSessionErrorStream(port, options) {
|
|
|
3582
4363
|
if (reader) void reader.cancel().catch(() => void 0);
|
|
3583
4364
|
}
|
|
3584
4365
|
}
|
|
3585
|
-
async function reloadProviderCache(port) {
|
|
4366
|
+
async function reloadProviderCache(port, client) {
|
|
4367
|
+
if (client?.version === "v2") return;
|
|
3586
4368
|
try {
|
|
3587
|
-
const res = await
|
|
4369
|
+
const res = await requestWithClient(port, client, "/config", {
|
|
3588
4370
|
method: "PATCH",
|
|
3589
4371
|
headers: { "Content-Type": "application/json" },
|
|
3590
4372
|
body: JSON.stringify({})
|
|
@@ -3962,12 +4744,14 @@ var STRIP_RES = /* @__PURE__ */ new Set([
|
|
|
3962
4744
|
"content-length"
|
|
3963
4745
|
]);
|
|
3964
4746
|
var StreamForwarder = class {
|
|
3965
|
-
constructor(ws, port, callbacks = {}) {
|
|
4747
|
+
constructor(ws, port, callbacks = {}, options = {}) {
|
|
3966
4748
|
this.ws = ws;
|
|
3967
4749
|
this.port = port;
|
|
3968
4750
|
this.callbacks = callbacks;
|
|
4751
|
+
this.options = options;
|
|
3969
4752
|
}
|
|
3970
4753
|
inflight = /* @__PURE__ */ new Map();
|
|
4754
|
+
binaryFramesSupported = false;
|
|
3971
4755
|
/**
|
|
3972
4756
|
* Handle an edge→agent frame. Unknown frame types are ignored.
|
|
3973
4757
|
*/
|
|
@@ -3987,6 +4771,12 @@ var StreamForwarder = class {
|
|
|
3987
4771
|
break;
|
|
3988
4772
|
}
|
|
3989
4773
|
}
|
|
4774
|
+
handleBinaryBodyFrame(sid, payload) {
|
|
4775
|
+
this.inflight.get(sid)?.pushBody?.(payload);
|
|
4776
|
+
}
|
|
4777
|
+
setBinaryFramesSupported(supported) {
|
|
4778
|
+
this.binaryFramesSupported = supported;
|
|
4779
|
+
}
|
|
3990
4780
|
/**
|
|
3991
4781
|
* Abort every in-flight stream (e.g. on WebSocket close).
|
|
3992
4782
|
*/
|
|
@@ -4049,7 +4839,15 @@ var StreamForwarder = class {
|
|
|
4049
4839
|
}
|
|
4050
4840
|
const fwdHeaders = {};
|
|
4051
4841
|
for (const [k, v] of Object.entries(headers ?? {})) {
|
|
4052
|
-
|
|
4842
|
+
const lower = k.toLowerCase();
|
|
4843
|
+
if (STRIP_REQ.has(lower)) continue;
|
|
4844
|
+
if (this.options.openCodePassword !== void 0 && this.options.openCodePassword !== null) {
|
|
4845
|
+
if (lower === "authorization") continue;
|
|
4846
|
+
}
|
|
4847
|
+
fwdHeaders[k] = v;
|
|
4848
|
+
}
|
|
4849
|
+
if (this.options.openCodePassword !== void 0 && this.options.openCodePassword !== null) {
|
|
4850
|
+
fwdHeaders.Authorization = buildOpenCodeBasicAuthHeader(this.options.openCodePassword);
|
|
4053
4851
|
}
|
|
4054
4852
|
this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });
|
|
4055
4853
|
const body = bodyPromise ? await bodyPromise : void 0;
|
|
@@ -4096,7 +4894,13 @@ var StreamForwarder = class {
|
|
|
4096
4894
|
const chunk = Buffer.from(value);
|
|
4097
4895
|
for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
|
|
4098
4896
|
const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
|
|
4099
|
-
this.
|
|
4897
|
+
if (this.binaryFramesSupported) {
|
|
4898
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
4899
|
+
this.ws.send(encodeBinaryBodyFrame(BINARY_FRAME_RES_DATA, sid, slice));
|
|
4900
|
+
}
|
|
4901
|
+
} else {
|
|
4902
|
+
this.send({ type: "res_data", sid, b64: slice.toString("base64") });
|
|
4903
|
+
}
|
|
4100
4904
|
}
|
|
4101
4905
|
}
|
|
4102
4906
|
}
|
|
@@ -4113,6 +4917,11 @@ var StreamForwarder = class {
|
|
|
4113
4917
|
|
|
4114
4918
|
// src/lib/tunnel/connection.ts
|
|
4115
4919
|
var FAILURE_REASON_HEADER_LC = FORWARD_FAILURE_REASON_HEADER.toLowerCase();
|
|
4920
|
+
function toUint8Array(data) {
|
|
4921
|
+
if (Array.isArray(data)) return Buffer.concat(data);
|
|
4922
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
4923
|
+
return data;
|
|
4924
|
+
}
|
|
4116
4925
|
var TunnelUpgradeRejectedError = class extends Error {
|
|
4117
4926
|
constructor(message, reason) {
|
|
4118
4927
|
super(message);
|
|
@@ -4162,6 +4971,7 @@ function connectTunnel(options) {
|
|
|
4162
4971
|
agentId,
|
|
4163
4972
|
authHeader,
|
|
4164
4973
|
port,
|
|
4974
|
+
openCodePassword,
|
|
4165
4975
|
onConnected,
|
|
4166
4976
|
onDisconnected,
|
|
4167
4977
|
onError,
|
|
@@ -4176,14 +4986,20 @@ function connectTunnel(options) {
|
|
|
4176
4986
|
return new Promise((resolve4, reject) => {
|
|
4177
4987
|
const ws = new WebSocket2(url, {
|
|
4178
4988
|
headers: {
|
|
4179
|
-
Authorization: authHeader
|
|
4989
|
+
Authorization: authHeader,
|
|
4990
|
+
"X-Evident-Tunnel-Binary-Frames": "1"
|
|
4180
4991
|
}
|
|
4181
4992
|
});
|
|
4182
|
-
const forwarder = new StreamForwarder(
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4993
|
+
const forwarder = new StreamForwarder(
|
|
4994
|
+
ws,
|
|
4995
|
+
port,
|
|
4996
|
+
{
|
|
4997
|
+
onHead: () => onResponse?.(),
|
|
4998
|
+
onDrainPing: () => onDrainPing?.(),
|
|
4999
|
+
onUsageRearmPing: () => onUsageRearmPing?.()
|
|
5000
|
+
},
|
|
5001
|
+
{ openCodePassword }
|
|
5002
|
+
);
|
|
4187
5003
|
const connectionTimeout = setTimeout(() => {
|
|
4188
5004
|
ws.close();
|
|
4189
5005
|
reject(new Error("Connection timeout"));
|
|
@@ -4220,7 +5036,25 @@ function connectTunnel(options) {
|
|
|
4220
5036
|
ws.on("open", () => {
|
|
4221
5037
|
onInfo?.("WebSocket connection established");
|
|
4222
5038
|
});
|
|
4223
|
-
ws.on("message", (data) => {
|
|
5039
|
+
ws.on("message", (data, isBinary) => {
|
|
5040
|
+
if (isBinary) {
|
|
5041
|
+
try {
|
|
5042
|
+
const frame = decodeBinaryBodyFrame(toUint8Array(data));
|
|
5043
|
+
if (frame === null) {
|
|
5044
|
+
onError?.("Failed to handle binary message: invalid frame");
|
|
5045
|
+
return;
|
|
5046
|
+
}
|
|
5047
|
+
if (frame.type !== BINARY_FRAME_REQ_DATA) {
|
|
5048
|
+
onError?.("Failed to handle binary message: unexpected frame type");
|
|
5049
|
+
return;
|
|
5050
|
+
}
|
|
5051
|
+
forwarder.handleBinaryBodyFrame(frame.sid, frame.payload);
|
|
5052
|
+
} catch (error2) {
|
|
5053
|
+
const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
5054
|
+
onError?.(`Failed to handle binary message: ${errorMessage3}`);
|
|
5055
|
+
}
|
|
5056
|
+
return;
|
|
5057
|
+
}
|
|
4224
5058
|
let message;
|
|
4225
5059
|
try {
|
|
4226
5060
|
message = JSON.parse(data.toString());
|
|
@@ -4236,6 +5070,7 @@ function connectTunnel(options) {
|
|
|
4236
5070
|
switch (message.type) {
|
|
4237
5071
|
case "connected": {
|
|
4238
5072
|
clearTimeout(connectionTimeout);
|
|
5073
|
+
forwarder.setBinaryFramesSupported(message.binary_frames === true);
|
|
4239
5074
|
const connectedAgentId = message.agent_id ?? agentId;
|
|
4240
5075
|
onConnected?.(connectedAgentId);
|
|
4241
5076
|
resolve4({
|
|
@@ -4326,6 +5161,7 @@ var RunnerConnection = class {
|
|
|
4326
5161
|
agentId: this.resolvedAgentId,
|
|
4327
5162
|
authHeader: this.opts.getAuthHeader(),
|
|
4328
5163
|
port: this.opts.port,
|
|
5164
|
+
openCodePassword: this.opts.openCodePassword,
|
|
4329
5165
|
onConnected: (agentId) => {
|
|
4330
5166
|
this.reconnectAttempt = 0;
|
|
4331
5167
|
this.reconnecting = false;
|
|
@@ -4506,33 +5342,73 @@ function parseCodexUsageHeaders(headers) {
|
|
|
4506
5342
|
function normalizeProbeModel(model) {
|
|
4507
5343
|
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
4508
5344
|
}
|
|
4509
|
-
|
|
5345
|
+
function isRecord2(value) {
|
|
5346
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5347
|
+
}
|
|
5348
|
+
function unsupportedProbeModels(reason, port) {
|
|
5349
|
+
console.error(`[resolveProbeModels] ${reason} (port ${port})`);
|
|
5350
|
+
return { status: "unsupported", reason };
|
|
5351
|
+
}
|
|
5352
|
+
async function resolveV1ProbeModels(client, port) {
|
|
4510
5353
|
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 [];
|
|
5354
|
+
const response = await client.request("/config/providers");
|
|
5355
|
+
const body = await response.json();
|
|
5356
|
+
if (!isRecord2(body) || !Array.isArray(body.providers)) {
|
|
5357
|
+
return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
|
|
4520
5358
|
}
|
|
4521
|
-
const
|
|
4522
|
-
|
|
4523
|
-
|
|
5359
|
+
const provider = body.providers.find(
|
|
5360
|
+
(candidate) => isRecord2(candidate) && candidate.id === "openai"
|
|
5361
|
+
);
|
|
5362
|
+
if (!provider || !isRecord2(provider.models)) return { status: "supported", models: [] };
|
|
5363
|
+
const defaults2 = isRecord2(body.default) ? body.default : void 0;
|
|
4524
5364
|
const candidates = [
|
|
4525
|
-
...typeof
|
|
5365
|
+
...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
|
|
4526
5366
|
...Object.keys(provider.models)
|
|
4527
5367
|
].map(normalizeProbeModel);
|
|
4528
|
-
return [...new Set(candidates)].slice(0, 4);
|
|
5368
|
+
return { status: "supported", models: [...new Set(candidates)].slice(0, 4) };
|
|
4529
5369
|
} catch (err) {
|
|
4530
|
-
|
|
4531
|
-
`
|
|
5370
|
+
return unsupportedProbeModels(
|
|
5371
|
+
`V1 GET /config/providers failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
5372
|
+
port
|
|
5373
|
+
);
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
5376
|
+
async function resolveV2ProbeModels(client, port) {
|
|
5377
|
+
const directory = await getOpenCodeDirectoryV2(client);
|
|
5378
|
+
if (!directory) {
|
|
5379
|
+
return unsupportedProbeModels("V2 working directory could not be verified", port);
|
|
5380
|
+
}
|
|
5381
|
+
const path = `/api/provider?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
|
|
5382
|
+
try {
|
|
5383
|
+
const response = await client.request(path);
|
|
5384
|
+
const body = await response.json();
|
|
5385
|
+
if (!isRecord2(body) || !Array.isArray(body.data)) {
|
|
5386
|
+
return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
|
|
5387
|
+
}
|
|
5388
|
+
const provider = body.data.find(
|
|
5389
|
+
(candidate) => isRecord2(candidate) && candidate.id === "openai"
|
|
5390
|
+
);
|
|
5391
|
+
if (!provider) return { status: "supported", models: [] };
|
|
5392
|
+
if (!isRecord2(provider.models)) {
|
|
5393
|
+
return unsupportedProbeModels(
|
|
5394
|
+
"V2 provider response has no safe OpenAI model catalogue",
|
|
5395
|
+
port
|
|
5396
|
+
);
|
|
5397
|
+
}
|
|
5398
|
+
return {
|
|
5399
|
+
status: "supported",
|
|
5400
|
+
models: [...new Set(Object.keys(provider.models).map(normalizeProbeModel))].slice(0, 4)
|
|
5401
|
+
};
|
|
5402
|
+
} catch (err) {
|
|
5403
|
+
return unsupportedProbeModels(
|
|
5404
|
+
`V2 GET ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
5405
|
+
port
|
|
4532
5406
|
);
|
|
4533
|
-
return [];
|
|
4534
5407
|
}
|
|
4535
5408
|
}
|
|
5409
|
+
async function resolveProbeModels(port, client = createOpenCodeClient({ port, version: "v1" })) {
|
|
5410
|
+
return client.version === "v2" ? resolveV2ProbeModels(client, port) : resolveV1ProbeModels(client, port);
|
|
5411
|
+
}
|
|
4536
5412
|
function hasPrimaryHeaders(headers) {
|
|
4537
5413
|
return [
|
|
4538
5414
|
"x-codex-primary-used-percent",
|
|
@@ -4540,7 +5416,7 @@ function hasPrimaryHeaders(headers) {
|
|
|
4540
5416
|
"x-codex-primary-reset-at"
|
|
4541
5417
|
].some((name) => headers.has(name));
|
|
4542
5418
|
}
|
|
4543
|
-
async function getOpenAiUsage(port) {
|
|
5419
|
+
async function getOpenAiUsage(port, client) {
|
|
4544
5420
|
const credentials2 = readOpenCodeChatGptCredentials();
|
|
4545
5421
|
if (!credentials2) {
|
|
4546
5422
|
throw new OpenAiUsageError(
|
|
@@ -4555,12 +5431,16 @@ async function getOpenAiUsage(port) {
|
|
|
4555
5431
|
);
|
|
4556
5432
|
}
|
|
4557
5433
|
const subscription = parseChatGptIdentity(credentials2.accessToken);
|
|
4558
|
-
const
|
|
4559
|
-
if (models.length === 0) {
|
|
4560
|
-
|
|
5434
|
+
const lookup = await resolveProbeModels(port, client);
|
|
5435
|
+
if (lookup.status === "unsupported" || lookup.models.length === 0) {
|
|
5436
|
+
const detail = lookup.status === "unsupported" ? ` ${lookup.reason}.` : "";
|
|
5437
|
+
throw new OpenAiUsageError(
|
|
5438
|
+
`No supported OpenAI probe model is available.${detail}`,
|
|
5439
|
+
"no_probe_model"
|
|
5440
|
+
);
|
|
4561
5441
|
}
|
|
4562
5442
|
let lastStatus;
|
|
4563
|
-
for (const model of models) {
|
|
5443
|
+
for (const model of lookup.models) {
|
|
4564
5444
|
let res;
|
|
4565
5445
|
try {
|
|
4566
5446
|
res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
|
|
@@ -5379,6 +6259,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5379
6259
|
maxActiveSessions;
|
|
5380
6260
|
watcherStallMs;
|
|
5381
6261
|
wedgeWarningIntervalMs;
|
|
6262
|
+
openCodeClient;
|
|
5382
6263
|
/** Cache of conversationId → opencode sessionId. */
|
|
5383
6264
|
sessions = /* @__PURE__ */ new Map();
|
|
5384
6265
|
/**
|
|
@@ -5487,20 +6368,23 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5487
6368
|
*/
|
|
5488
6369
|
readopted = /* @__PURE__ */ new Set();
|
|
5489
6370
|
/**
|
|
5490
|
-
*
|
|
5491
|
-
*
|
|
5492
|
-
*
|
|
5493
|
-
*
|
|
5494
|
-
*
|
|
5495
|
-
*
|
|
5496
|
-
* CRITICAL (Bugbot #202): this suppresses ONLY the dispatch/re-attach paths, it
|
|
5497
|
-
* does NOT suppress DONE delivery. A row parked here whose reply later COMPLETES
|
|
5498
|
-
* in opencode must still be delivered via `markDone` on the next drain — so
|
|
5499
|
-
* `readoptOne` computes `state` FIRST and this set is checked only on the
|
|
5500
|
-
* non-done path. It is cleared once the row leaves the processing list (cron
|
|
5501
|
-
* reset → it drains normally as `pending`), so it can never leak.
|
|
6371
|
+
* Readopt give-up fence. Set when recovery declines to start or continue a turn
|
|
6372
|
+
* for a row that is still `processing`, so the next drain does not re-dispatch or
|
|
6373
|
+
* re-attach it before the cron safety net acts. It suppresses only non-done
|
|
6374
|
+
* recovery paths; DONE delivery still runs. Clear it when
|
|
6375
|
+
* `!stillProcessing.has(id)`, because leaving `processing` hands the row back to
|
|
6376
|
+
* normal processing.
|
|
5502
6377
|
*/
|
|
5503
6378
|
dontRedispatch = /* @__PURE__ */ new Set();
|
|
6379
|
+
/**
|
|
6380
|
+
* Untrackable-ack fence. Set after OpenCode accepts a prompt without returning a
|
|
6381
|
+
* usable message id, because another POST could create a duplicate turn. Keep it
|
|
6382
|
+
* fenced while the row is `processing` or `pending`; clear it only when the row
|
|
6383
|
+
* is absent from both lists.
|
|
6384
|
+
*/
|
|
6385
|
+
untrackableAck = /* @__PURE__ */ new Set();
|
|
6386
|
+
/** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
|
|
6387
|
+
pendingMessageIds = /* @__PURE__ */ new Set();
|
|
5504
6388
|
/**
|
|
5505
6389
|
* "markDone for this row is TERMINALLY undeliverable" (Bug 4). Set ONLY when a
|
|
5506
6390
|
* re-adopted DONE row's `markDone` returned a terminal 4xx (a status that will
|
|
@@ -5623,7 +6507,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5623
6507
|
*/
|
|
5624
6508
|
attachmentsSkippedSignalled = /* @__PURE__ */ new Set();
|
|
5625
6509
|
/**
|
|
5626
|
-
* Cache of the opencode root directory
|
|
6510
|
+
* Cache of the opencode root directory from the selected client's location lookup.
|
|
6511
|
+
* Resolved lazily on
|
|
5627
6512
|
* first session creation so drain-created sessions are rooted at the project
|
|
5628
6513
|
* directory and thus visible in `opencode web`'s session list. `undefined` =
|
|
5629
6514
|
* not yet resolved; `null` = resolved-but-unavailable (don't keep retrying).
|
|
@@ -5717,6 +6602,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5717
6602
|
config.fetchImpl ?? fetch,
|
|
5718
6603
|
config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
|
|
5719
6604
|
);
|
|
6605
|
+
this.openCodeClient = config.openCodeClient ?? createOpenCodeClient({
|
|
6606
|
+
port: config.port,
|
|
6607
|
+
version: "v1",
|
|
6608
|
+
fetchImpl: config.fetchImpl
|
|
6609
|
+
});
|
|
5720
6610
|
this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5721
6611
|
this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
|
|
5722
6612
|
this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
|
|
@@ -5728,9 +6618,39 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5728
6618
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
5729
6619
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
5730
6620
|
}
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
6621
|
+
get isV2() {
|
|
6622
|
+
return this.openCodeClient.version === "v2";
|
|
6623
|
+
}
|
|
6624
|
+
async getSessionMessages(sessionId) {
|
|
6625
|
+
return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6626
|
+
}
|
|
6627
|
+
async getSubagentSessionMessages(sessionId) {
|
|
6628
|
+
return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : getSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6629
|
+
}
|
|
6630
|
+
async getTelemetrySubagentSessionMessages(sessionId) {
|
|
6631
|
+
if (this.isV2) return getV2SessionMessages(this.openCodeClient, sessionId);
|
|
6632
|
+
return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6633
|
+
}
|
|
6634
|
+
async listSessions() {
|
|
6635
|
+
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient);
|
|
6636
|
+
}
|
|
6637
|
+
async sessionExists(sessionId) {
|
|
6638
|
+
return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
|
|
6639
|
+
}
|
|
6640
|
+
async isSessionOngoing(sessionId) {
|
|
6641
|
+
return this.isV2 ? isV2SessionOngoing(this.openCodeClient, sessionId) : isSessionOngoing(this.port, sessionId, this.openCodeClient);
|
|
6642
|
+
}
|
|
6643
|
+
async getOpenCodeDirectory() {
|
|
6644
|
+
return this.isV2 ? getOpenCodeDirectoryV2(this.openCodeClient) : getOpenCodeDirectory(this.port, this.openCodeClient);
|
|
6645
|
+
}
|
|
6646
|
+
async createOpenCodeSession(directory) {
|
|
6647
|
+
return this.isV2 ? createV2Session(this.openCodeClient, directory) : createOpenCodeSession(this.port, directory, this.openCodeClient);
|
|
6648
|
+
}
|
|
6649
|
+
async hasAnyConfiguredProvider() {
|
|
6650
|
+
return hasAnyConfiguredProvider(this.port, this.openCodeClient);
|
|
6651
|
+
}
|
|
6652
|
+
async readOpenCodeSessionErrorStream(options) {
|
|
6653
|
+
return this.isV2 ? readV2SessionErrorStream(this.openCodeClient, options) : readSessionErrorStream(this.port, options, this.openCodeClient);
|
|
5734
6654
|
}
|
|
5735
6655
|
/**
|
|
5736
6656
|
* Drain all pending channel conversations once: poll → dispatch → register.
|
|
@@ -5808,6 +6728,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5808
6728
|
async runDrain() {
|
|
5809
6729
|
let dispatched = 0;
|
|
5810
6730
|
try {
|
|
6731
|
+
this.pendingMessageIds.clear();
|
|
5811
6732
|
const conversations = await this.getPendingConversations();
|
|
5812
6733
|
if (this.recycleRequestedFlag) {
|
|
5813
6734
|
this.stop();
|
|
@@ -6032,6 +6953,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6032
6953
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
6033
6954
|
this.ensureSessionErrorStream();
|
|
6034
6955
|
const messages = await this.getPendingMessages(conv.id);
|
|
6956
|
+
for (const message of messages) this.pendingMessageIds.add(message.id);
|
|
6035
6957
|
let dispatched = 0;
|
|
6036
6958
|
let skippedAlreadyDispatched = 0;
|
|
6037
6959
|
if (refusedSessionId && messages.length > 0) {
|
|
@@ -6045,6 +6967,15 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6045
6967
|
skippedAlreadyDispatched += 1;
|
|
6046
6968
|
continue;
|
|
6047
6969
|
}
|
|
6970
|
+
if (this.untrackableAck.has(message.id)) {
|
|
6971
|
+
this.log({
|
|
6972
|
+
level: "warn",
|
|
6973
|
+
message: `Message ${message.id.slice(0, 8)} is fenced after an untrackable OpenCode turn \u2014 skipping re-dispatch`,
|
|
6974
|
+
conversation_id: conv.id,
|
|
6975
|
+
message_id: message.id
|
|
6976
|
+
});
|
|
6977
|
+
break;
|
|
6978
|
+
}
|
|
6048
6979
|
const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
|
|
6049
6980
|
if (effectiveOpencodeMessageId) {
|
|
6050
6981
|
const outcome = await this.resolveRedrive(
|
|
@@ -6073,15 +7004,55 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6073
7004
|
conversation_id: conv.id,
|
|
6074
7005
|
message_id: message.id
|
|
6075
7006
|
});
|
|
6076
|
-
const sendAttachments = this.buildSendAttachments(conv, message);
|
|
6077
|
-
|
|
7007
|
+
const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(conv, message);
|
|
7008
|
+
if (this.isV2 && message.attachments && message.attachments.length > 0) {
|
|
7009
|
+
this.signalAttachmentsSkipped(
|
|
7010
|
+
conv.id,
|
|
7011
|
+
message.id,
|
|
7012
|
+
message.attachments.map((attachment, index) => ({
|
|
7013
|
+
index,
|
|
7014
|
+
mime: attachment.mime,
|
|
7015
|
+
...attachment.filename ? { filename: attachment.filename } : {},
|
|
7016
|
+
status: "skipped"
|
|
7017
|
+
})),
|
|
7018
|
+
false
|
|
7019
|
+
);
|
|
7020
|
+
}
|
|
7021
|
+
opencodeMessageId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, message.content) : await this.dispatchLocked(
|
|
6078
7022
|
sessionId,
|
|
6079
|
-
() => sendPromptAsync(
|
|
7023
|
+
() => sendPromptAsync(
|
|
7024
|
+
this.port,
|
|
7025
|
+
sessionId,
|
|
7026
|
+
message.content,
|
|
7027
|
+
options,
|
|
7028
|
+
sendAttachments,
|
|
7029
|
+
this.openCodeClient
|
|
7030
|
+
)
|
|
6080
7031
|
);
|
|
6081
7032
|
} catch (err) {
|
|
6082
7033
|
if (err instanceof ChannelAuthError) throw err;
|
|
7034
|
+
if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
|
|
7035
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
7036
|
+
this.untrackableAck.add(message.id);
|
|
7037
|
+
this.log({
|
|
7038
|
+
level: "error",
|
|
7039
|
+
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
|
|
7040
|
+
conversation_id: conv.id,
|
|
7041
|
+
message_id: message.id
|
|
7042
|
+
});
|
|
7043
|
+
await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
|
|
7044
|
+
this.log({
|
|
7045
|
+
level: "warn",
|
|
7046
|
+
message: `markFailed PATCH for V2 dispatch failure on message ${message.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
7047
|
+
conversation_id: conv.id,
|
|
7048
|
+
message_id: message.id
|
|
7049
|
+
});
|
|
7050
|
+
void this.postSignal(conv.id, message.id, "ack_untrackable");
|
|
7051
|
+
});
|
|
7052
|
+
break;
|
|
7053
|
+
}
|
|
6083
7054
|
this.dispatched.delete(message.id);
|
|
6084
|
-
const exists = await sessionExists(
|
|
7055
|
+
const exists = await this.sessionExists(sessionId);
|
|
6085
7056
|
if (exists === false) {
|
|
6086
7057
|
this.sessions.delete(conv.id);
|
|
6087
7058
|
this.log({
|
|
@@ -6130,6 +7101,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6130
7101
|
break;
|
|
6131
7102
|
}
|
|
6132
7103
|
if (opencodeMessageId === null) {
|
|
7104
|
+
if (this.isV2) {
|
|
7105
|
+
throw new Error("V2 prompt dispatch completed without an acknowledged message id");
|
|
7106
|
+
}
|
|
6133
7107
|
const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
|
|
6134
7108
|
if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
6135
7109
|
this.log({
|
|
@@ -6277,29 +7251,38 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6277
7251
|
*/
|
|
6278
7252
|
async pollSessionMessagesForRedrive(conv, message, sessionId) {
|
|
6279
7253
|
try {
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
7254
|
+
if (this.isV2) {
|
|
7255
|
+
const messages = await this.getSessionMessages(sessionId);
|
|
7256
|
+
if (messages === null) {
|
|
7257
|
+
this.log({
|
|
7258
|
+
level: "warn",
|
|
7259
|
+
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`,
|
|
7260
|
+
conversation_id: conv.id,
|
|
7261
|
+
message_id: message.id
|
|
7262
|
+
});
|
|
7263
|
+
return { ok: false, signature: null };
|
|
7264
|
+
}
|
|
7265
|
+
return { ok: true, messages };
|
|
6291
7266
|
}
|
|
6292
|
-
const
|
|
6293
|
-
|
|
7267
|
+
const polledV1 = await pollSessionMessagesForRedrive(
|
|
7268
|
+
this.port,
|
|
7269
|
+
sessionId,
|
|
7270
|
+
this.openCodeClient
|
|
7271
|
+
);
|
|
7272
|
+
if (!polledV1.ok) {
|
|
7273
|
+
const normalized = normalizeRedrivePollFailureBody(polledV1.body);
|
|
6294
7274
|
this.log({
|
|
6295
7275
|
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`,
|
|
7276
|
+
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
7277
|
conversation_id: conv.id,
|
|
6298
7278
|
message_id: message.id
|
|
6299
7279
|
});
|
|
6300
|
-
return {
|
|
7280
|
+
return {
|
|
7281
|
+
ok: false,
|
|
7282
|
+
signature: polledV1.status === null && !polledV1.malformed ? null : polledV1.malformed ? "non-array message body" : `HTTP ${polledV1.status}${normalized ? `: ${normalized}` : ""}`
|
|
7283
|
+
};
|
|
6301
7284
|
}
|
|
6302
|
-
return { ok: true, messages:
|
|
7285
|
+
return { ok: true, messages: polledV1.messages };
|
|
6303
7286
|
} catch (err) {
|
|
6304
7287
|
this.log({
|
|
6305
7288
|
level: "warn",
|
|
@@ -6358,11 +7341,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6358
7341
|
}
|
|
6359
7342
|
const state = messageRunState(messages, ocId ?? "");
|
|
6360
7343
|
if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
|
|
6361
|
-
const ongoing = await isSessionOngoing(
|
|
7344
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
6362
7345
|
if (ongoing === false) {
|
|
6363
7346
|
this.log({
|
|
6364
7347
|
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
|
|
7348
|
+
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
7349
|
conversation_id: conv.id,
|
|
6367
7350
|
message_id: message.id
|
|
6368
7351
|
});
|
|
@@ -6375,7 +7358,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6375
7358
|
return this.settleRedrive(conv, sessionId, message, ocId, messages, state);
|
|
6376
7359
|
}
|
|
6377
7360
|
if (state === "running" || state === "queued") {
|
|
6378
|
-
const ongoing = await isSessionOngoing(
|
|
7361
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
6379
7362
|
if (ongoing === true) {
|
|
6380
7363
|
if (state === "queued") {
|
|
6381
7364
|
const siblingOcIds = this.siblingOpencodeMessageIds(
|
|
@@ -6835,7 +7818,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6835
7818
|
};
|
|
6836
7819
|
}
|
|
6837
7820
|
if (bound) {
|
|
6838
|
-
const exists = await sessionExists(
|
|
7821
|
+
const exists = await this.sessionExists(bound);
|
|
6839
7822
|
if (exists === false) {
|
|
6840
7823
|
this.log({
|
|
6841
7824
|
level: "debug",
|
|
@@ -6868,7 +7851,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6868
7851
|
*/
|
|
6869
7852
|
async createAndBindSession(conversationId) {
|
|
6870
7853
|
const directory = await this.resolveOpenCodeDirectory();
|
|
6871
|
-
const sessionId = await createOpenCodeSession(
|
|
7854
|
+
const sessionId = await this.createOpenCodeSession(directory);
|
|
6872
7855
|
this.sessions.set(conversationId, sessionId);
|
|
6873
7856
|
await this.persistSession(conversationId, sessionId).catch((err) => {
|
|
6874
7857
|
this.log({
|
|
@@ -6880,17 +7863,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6880
7863
|
return sessionId;
|
|
6881
7864
|
}
|
|
6882
7865
|
/**
|
|
6883
|
-
* Lazily resolve (and cache) opencode's root directory via
|
|
7866
|
+
* Lazily resolve (and cache) opencode's root directory via the selected client's
|
|
7867
|
+
* location lookup.
|
|
6884
7868
|
* Resolved once per driver: `undefined` until first lookup, then the directory
|
|
6885
|
-
* string or `null` if unavailable (we don't keep retrying a
|
|
7869
|
+
* string or `null` if unavailable (we don't keep retrying a failed lookup).
|
|
6886
7870
|
*/
|
|
6887
7871
|
async resolveOpenCodeDirectory() {
|
|
6888
7872
|
if (this.opencodeDirectory !== void 0) return this.opencodeDirectory;
|
|
6889
|
-
this.opencodeDirectory = await getOpenCodeDirectory(
|
|
7873
|
+
this.opencodeDirectory = await this.getOpenCodeDirectory();
|
|
6890
7874
|
if (!this.opencodeDirectory) {
|
|
6891
7875
|
this.log({
|
|
6892
7876
|
level: "warn",
|
|
6893
|
-
message: "Could not determine opencode directory (
|
|
7877
|
+
message: "Could not determine opencode directory (location lookup failed) \u2014 new sessions may not appear in opencode web"
|
|
6894
7878
|
});
|
|
6895
7879
|
}
|
|
6896
7880
|
return this.opencodeDirectory;
|
|
@@ -7324,7 +8308,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7324
8308
|
while (!this.stopped && !signal.aborted) {
|
|
7325
8309
|
const openedAt = this.now();
|
|
7326
8310
|
try {
|
|
7327
|
-
const outcome = await
|
|
8311
|
+
const outcome = await this.readOpenCodeSessionErrorStream({
|
|
7328
8312
|
signal,
|
|
7329
8313
|
onSessionError: (event) => this.handleSessionError(event)
|
|
7330
8314
|
});
|
|
@@ -7415,7 +8399,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7415
8399
|
}
|
|
7416
8400
|
async failFromSessionError(watcher, event, inFlight) {
|
|
7417
8401
|
try {
|
|
7418
|
-
const messages = await getSessionMessages(
|
|
8402
|
+
const messages = await this.getSessionMessages(event.sessionId);
|
|
7419
8403
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7420
8404
|
if (state !== "queued") {
|
|
7421
8405
|
this.log({
|
|
@@ -7467,7 +8451,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7467
8451
|
* markDone (done) exactly once per transition;
|
|
7468
8452
|
* 2. applies the idle-path re-dispatch guard (a dispatched message that never
|
|
7469
8453
|
* APPEARS → re-dispatch — D1 obligation 2);
|
|
7470
|
-
* 3. polls `/question` + `/permission
|
|
8454
|
+
* 3. polls V1's global `/question` + `/permission`, or V2's
|
|
8455
|
+
* `/api/session/:id/form` + `/api/session/:id/permission`, and surfaces
|
|
7471
8456
|
* NEW ones via `reportInteraction`, carrying the PAUSED message's own
|
|
7472
8457
|
* `source_message_id`;
|
|
7473
8458
|
* 4. drops messages that completed or timed out from the in-flight set.
|
|
@@ -7493,11 +8478,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7493
8478
|
if (watcher.generation !== generation) return;
|
|
7494
8479
|
let messages = null;
|
|
7495
8480
|
try {
|
|
7496
|
-
|
|
7497
|
-
if (res.ok) {
|
|
7498
|
-
const body = await res.json();
|
|
7499
|
-
messages = Array.isArray(body) ? body : null;
|
|
7500
|
-
}
|
|
8481
|
+
messages = await this.getSessionMessages(sessionId);
|
|
7501
8482
|
} catch {
|
|
7502
8483
|
}
|
|
7503
8484
|
if (messages != null && messages.length > 0) {
|
|
@@ -7730,7 +8711,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7730
8711
|
inFlight.b2LastDescendantCheckMs = this.now();
|
|
7731
8712
|
const [descendantOngoing, rootOngoing] = await Promise.all([
|
|
7732
8713
|
this.isAnyDescendantSessionOngoing(sessionId),
|
|
7733
|
-
isSessionOngoing(
|
|
8714
|
+
this.isSessionOngoing(sessionId)
|
|
7734
8715
|
]);
|
|
7735
8716
|
if (isB2AbandonmentConfirmed({
|
|
7736
8717
|
pinnedForMs,
|
|
@@ -7790,7 +8771,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7790
8771
|
});
|
|
7791
8772
|
}
|
|
7792
8773
|
const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;
|
|
7793
|
-
const ongoing = await isSessionOngoing(
|
|
8774
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
7794
8775
|
if (isAmbiguousFinishResolved({
|
|
7795
8776
|
pinnedForMs,
|
|
7796
8777
|
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
@@ -7973,7 +8954,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7973
8954
|
*/
|
|
7974
8955
|
async readoptProcessing() {
|
|
7975
8956
|
const rows = await this.getProcessingMessages();
|
|
7976
|
-
if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
|
|
8957
|
+
if (this.dontRedispatch.size > 0 || this.untrackableAck.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
|
|
7977
8958
|
const stillProcessing = new Set(rows.map((r) => r.id));
|
|
7978
8959
|
for (const id of [
|
|
7979
8960
|
...this.dontRedispatch,
|
|
@@ -7993,6 +8974,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7993
8974
|
}
|
|
7994
8975
|
}
|
|
7995
8976
|
}
|
|
8977
|
+
for (const id of [...this.untrackableAck]) {
|
|
8978
|
+
if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
|
|
8979
|
+
this.untrackableAck.delete(id);
|
|
8980
|
+
this.log({
|
|
8981
|
+
level: "debug",
|
|
8982
|
+
message: `Re-adopt: message ${id.slice(0, 8)} left processing and pending \u2014 cleared untrackable-ack fence`,
|
|
8983
|
+
message_id: id
|
|
8984
|
+
});
|
|
8985
|
+
}
|
|
8986
|
+
}
|
|
7996
8987
|
}
|
|
7997
8988
|
if (rows.length === 0) return;
|
|
7998
8989
|
const bySession = /* @__PURE__ */ new Map();
|
|
@@ -8013,23 +9004,32 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8013
9004
|
for (const [sessionId, sessionRows] of bySession) {
|
|
8014
9005
|
let messages;
|
|
8015
9006
|
try {
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
9007
|
+
if (this.isV2) {
|
|
9008
|
+
const snapshot = await this.getSessionMessages(sessionId);
|
|
9009
|
+
if (snapshot === null) {
|
|
9010
|
+
this.log({
|
|
9011
|
+
level: "warn",
|
|
9012
|
+
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
|
|
9013
|
+
});
|
|
9014
|
+
continue;
|
|
9015
|
+
}
|
|
9016
|
+
messages = snapshot;
|
|
9017
|
+
} else {
|
|
9018
|
+
const polled = await pollSessionMessagesForRedrive(
|
|
9019
|
+
this.port,
|
|
9020
|
+
sessionId,
|
|
9021
|
+
this.openCodeClient
|
|
9022
|
+
);
|
|
9023
|
+
if (!polled.ok) {
|
|
9024
|
+
const normalized = normalizeRedrivePollFailureBody(polled.body);
|
|
9025
|
+
this.log({
|
|
9026
|
+
level: "warn",
|
|
9027
|
+
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`
|
|
9028
|
+
});
|
|
9029
|
+
continue;
|
|
9030
|
+
}
|
|
9031
|
+
messages = polled.messages;
|
|
8031
9032
|
}
|
|
8032
|
-
messages = body;
|
|
8033
9033
|
} catch (err) {
|
|
8034
9034
|
this.log({
|
|
8035
9035
|
level: "warn",
|
|
@@ -8038,7 +9038,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8038
9038
|
continue;
|
|
8039
9039
|
}
|
|
8040
9040
|
const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
|
|
8041
|
-
const sessionOngoing = anyUntracked ? await isSessionOngoing(
|
|
9041
|
+
const sessionOngoing = anyUntracked ? await this.isSessionOngoing(sessionId) : null;
|
|
8042
9042
|
for (const row of sessionRows) {
|
|
8043
9043
|
await this.readoptOne(sessionId, row, messages, sessionOngoing);
|
|
8044
9044
|
}
|
|
@@ -8085,7 +9085,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8085
9085
|
if (restartAborted) {
|
|
8086
9086
|
this.log({
|
|
8087
9087
|
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
|
|
9088
|
+
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
9089
|
conversation_id: row.conversation_id,
|
|
8090
9090
|
message_id: row.id
|
|
8091
9091
|
});
|
|
@@ -8140,10 +9140,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8140
9140
|
}
|
|
8141
9141
|
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
8142
9142
|
this.dontRedispatch.delete(row.id);
|
|
9143
|
+
this.untrackableAck.delete(row.id);
|
|
8143
9144
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
8144
9145
|
return;
|
|
8145
9146
|
}
|
|
8146
|
-
if (this.dontRedispatch.has(row.id)) {
|
|
9147
|
+
if (this.dontRedispatch.has(row.id) || this.untrackableAck.has(row.id)) {
|
|
8147
9148
|
this.log({
|
|
8148
9149
|
level: "debug",
|
|
8149
9150
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
@@ -8163,7 +9164,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8163
9164
|
const finish = reply?.info?.finish ?? reply?.finish;
|
|
8164
9165
|
this.log({
|
|
8165
9166
|
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
|
|
9167
|
+
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
9168
|
conversation_id: row.conversation_id,
|
|
8168
9169
|
message_id: row.id
|
|
8169
9170
|
});
|
|
@@ -8172,7 +9173,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8172
9173
|
}
|
|
8173
9174
|
this.log({
|
|
8174
9175
|
level: "info",
|
|
8175
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per
|
|
9176
|
+
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
9177
|
conversation_id: row.conversation_id,
|
|
8177
9178
|
message_id: row.id
|
|
8178
9179
|
});
|
|
@@ -8182,7 +9183,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8182
9183
|
if (ongoing === true) {
|
|
8183
9184
|
this.log({
|
|
8184
9185
|
level: "debug",
|
|
8185
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per
|
|
9186
|
+
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
9187
|
conversation_id: row.conversation_id,
|
|
8187
9188
|
message_id: row.id
|
|
8188
9189
|
});
|
|
@@ -8190,7 +9191,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8190
9191
|
if (shape === "b1") {
|
|
8191
9192
|
this.log({
|
|
8192
9193
|
level: "debug",
|
|
8193
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but
|
|
9194
|
+
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
9195
|
conversation_id: row.conversation_id,
|
|
8195
9196
|
message_id: row.id
|
|
8196
9197
|
});
|
|
@@ -8202,7 +9203,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8202
9203
|
}
|
|
8203
9204
|
this.log({
|
|
8204
9205
|
level: "debug",
|
|
8205
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but
|
|
9206
|
+
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
9207
|
conversation_id: row.conversation_id,
|
|
8207
9208
|
message_id: row.id
|
|
8208
9209
|
});
|
|
@@ -8252,7 +9253,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8252
9253
|
* extracted (#1493 Task 2.4) so the ambiguous-finish guard above can call the
|
|
8253
9254
|
* SAME delivery instead of duplicating it.
|
|
8254
9255
|
*
|
|
8255
|
-
* EVEN IF the row was previously parked
|
|
9256
|
+
* EVEN IF the row was previously parked by either recovery fence (a give-up stops
|
|
8256
9257
|
* re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY like the watcher's
|
|
8257
9258
|
* `settleMessageDone`: auth re-throws; terminal → park in `doneUndeliverable` +
|
|
8258
9259
|
* leave for cron; transient → log + leave for the next drain (the still-
|
|
@@ -8319,6 +9320,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8319
9320
|
await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
|
|
8320
9321
|
}
|
|
8321
9322
|
this.dontRedispatch.delete(row.id);
|
|
9323
|
+
this.untrackableAck.delete(row.id);
|
|
8322
9324
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
8323
9325
|
}
|
|
8324
9326
|
/**
|
|
@@ -8382,19 +9384,90 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8382
9384
|
conversation_id: row.conversation_id,
|
|
8383
9385
|
message_id: row.id
|
|
8384
9386
|
});
|
|
8385
|
-
this.awaitingReadopt.add(row.id);
|
|
9387
|
+
if (!this.isV2) this.awaitingReadopt.add(row.id);
|
|
8386
9388
|
const readoptConv = this.convForRow(sessionId, row);
|
|
8387
9389
|
const readoptMessage = this.queuedMessageForRow(row);
|
|
8388
|
-
const sendAttachments = this.buildSendAttachments(readoptConv, readoptMessage);
|
|
9390
|
+
const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(readoptConv, readoptMessage);
|
|
9391
|
+
if (this.isV2 && row.attachments && row.attachments.length > 0) {
|
|
9392
|
+
this.signalAttachmentsSkipped(
|
|
9393
|
+
row.conversation_id,
|
|
9394
|
+
row.id,
|
|
9395
|
+
row.attachments.map((attachment, index) => ({
|
|
9396
|
+
index,
|
|
9397
|
+
mime: attachment.mime,
|
|
9398
|
+
...attachment.filename ? { filename: attachment.filename } : {},
|
|
9399
|
+
status: "skipped"
|
|
9400
|
+
})),
|
|
9401
|
+
false
|
|
9402
|
+
);
|
|
9403
|
+
}
|
|
8389
9404
|
let ocId;
|
|
8390
9405
|
try {
|
|
8391
|
-
ocId = await this.dispatchLocked(
|
|
9406
|
+
ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
|
|
8392
9407
|
sessionId,
|
|
8393
|
-
() => sendPromptAsync(
|
|
9408
|
+
() => sendPromptAsync(
|
|
9409
|
+
this.port,
|
|
9410
|
+
sessionId,
|
|
9411
|
+
row.content,
|
|
9412
|
+
options,
|
|
9413
|
+
sendAttachments,
|
|
9414
|
+
this.openCodeClient
|
|
9415
|
+
)
|
|
8394
9416
|
);
|
|
8395
9417
|
} catch (err) {
|
|
8396
9418
|
this.awaitingReadopt.delete(row.id);
|
|
8397
9419
|
if (err instanceof ChannelAuthError) throw err;
|
|
9420
|
+
if (this.isV2) {
|
|
9421
|
+
const errorMessage3 = err instanceof Error ? err.message : String(err);
|
|
9422
|
+
const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
|
|
9423
|
+
if (!invalidPromptAck) {
|
|
9424
|
+
const exists = await this.sessionExists(sessionId);
|
|
9425
|
+
if (exists === false) {
|
|
9426
|
+
this.log({
|
|
9427
|
+
level: "warn",
|
|
9428
|
+
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}`,
|
|
9429
|
+
conversation_id: row.conversation_id,
|
|
9430
|
+
message_id: row.id
|
|
9431
|
+
});
|
|
9432
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
9433
|
+
return;
|
|
9434
|
+
}
|
|
9435
|
+
if (exists === null) {
|
|
9436
|
+
this.log({
|
|
9437
|
+
level: "warn",
|
|
9438
|
+
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}`,
|
|
9439
|
+
conversation_id: row.conversation_id,
|
|
9440
|
+
message_id: row.id
|
|
9441
|
+
});
|
|
9442
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
9443
|
+
return;
|
|
9444
|
+
}
|
|
9445
|
+
} else {
|
|
9446
|
+
this.untrackableAck.add(row.id);
|
|
9447
|
+
}
|
|
9448
|
+
this.log({
|
|
9449
|
+
level: "error",
|
|
9450
|
+
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage3}`,
|
|
9451
|
+
conversation_id: row.conversation_id,
|
|
9452
|
+
message_id: row.id
|
|
9453
|
+
});
|
|
9454
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
|
|
9455
|
+
(markErr) => {
|
|
9456
|
+
this.log({
|
|
9457
|
+
level: "warn",
|
|
9458
|
+
message: `markFailed PATCH for V2 re-adopt dispatch failure on message ${row.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
9459
|
+
conversation_id: row.conversation_id,
|
|
9460
|
+
message_id: row.id
|
|
9461
|
+
});
|
|
9462
|
+
if (invalidPromptAck) {
|
|
9463
|
+
void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
|
|
9464
|
+
} else {
|
|
9465
|
+
this.signalDispatchNotStarted(readoptConv, readoptMessage, "failure_unreported");
|
|
9466
|
+
}
|
|
9467
|
+
}
|
|
9468
|
+
);
|
|
9469
|
+
return;
|
|
9470
|
+
}
|
|
8398
9471
|
this.log({
|
|
8399
9472
|
level: "warn",
|
|
8400
9473
|
message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -8526,12 +9599,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8526
9599
|
this.dispatched.delete(evidentMessageId);
|
|
8527
9600
|
}
|
|
8528
9601
|
/**
|
|
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
|
|
9602
|
+
* Poll V1's global `/question` + `/permission`, or V2's watched-session form and
|
|
9603
|
+
* permission routes, and surface NEW ones via `reportInteraction` (Task 3.5),
|
|
9604
|
+
* carrying the PAUSED message's own `source_message_id` so the server @mentions
|
|
9605
|
+
* the correct person under concurrency. Dedups by interaction id across ticks
|
|
9606
|
+
* (reused per-session sets).
|
|
8533
9607
|
*
|
|
8534
|
-
* The interaction is attributed to the in-flight message it paused on.
|
|
9608
|
+
* The interaction is attributed to the in-flight message it paused on. OpenCode
|
|
8535
9609
|
* stamps a `messageID` on a permission (and `tool.messageID` on a question) =
|
|
8536
9610
|
* the assistant message id, whose `parentID` is the user message id — but the
|
|
8537
9611
|
* simplest robust attribution here is: the single in-flight message that is
|
|
@@ -8554,16 +9628,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8554
9628
|
let permissionsPolledOk = true;
|
|
8555
9629
|
let questions = [];
|
|
8556
9630
|
try {
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
questions = body;
|
|
8562
|
-
} else {
|
|
8563
|
-
questionsPolledOk = false;
|
|
8564
|
-
}
|
|
9631
|
+
if (this.isV2) {
|
|
9632
|
+
const forms = await listV2Forms(this.openCodeClient, sessionId);
|
|
9633
|
+
if (forms === null) questionsPolledOk = false;
|
|
9634
|
+
else questions = forms;
|
|
8565
9635
|
} else {
|
|
8566
|
-
|
|
9636
|
+
const listed = await listOpenCodeQuestions(this.port, this.openCodeClient);
|
|
9637
|
+
if (listed === null) questionsPolledOk = false;
|
|
9638
|
+
else questions = listed;
|
|
8567
9639
|
}
|
|
8568
9640
|
} catch {
|
|
8569
9641
|
questionsPolledOk = false;
|
|
@@ -8583,16 +9655,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8583
9655
|
}
|
|
8584
9656
|
let permissions = [];
|
|
8585
9657
|
try {
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
permissions = body;
|
|
8591
|
-
} else {
|
|
8592
|
-
permissionsPolledOk = false;
|
|
8593
|
-
}
|
|
9658
|
+
if (this.isV2) {
|
|
9659
|
+
const listed = await listV2Permissions(this.openCodeClient, sessionId);
|
|
9660
|
+
if (listed === null) permissionsPolledOk = false;
|
|
9661
|
+
else permissions = listed;
|
|
8594
9662
|
} else {
|
|
8595
|
-
|
|
9663
|
+
const listed = await listOpenCodePermissions(this.port, this.openCodeClient);
|
|
9664
|
+
if (listed === null) permissionsPolledOk = false;
|
|
9665
|
+
else permissions = listed;
|
|
8596
9666
|
}
|
|
8597
9667
|
} catch {
|
|
8598
9668
|
permissionsPolledOk = false;
|
|
@@ -8686,10 +9756,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8686
9756
|
if (cached !== void 0) return cached;
|
|
8687
9757
|
let parent = void 0;
|
|
8688
9758
|
try {
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
9759
|
+
if (this.isV2) {
|
|
9760
|
+
const session = await getV2Session(this.openCodeClient, sessionId);
|
|
9761
|
+
parent = null;
|
|
9762
|
+
const candidate = session.parentID;
|
|
9763
|
+
if (typeof candidate === "string") parent = candidate;
|
|
9764
|
+
} else {
|
|
9765
|
+
const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
|
|
9766
|
+
parent = typeof body?.parentID === "string" ? body.parentID : body === null ? void 0 : null;
|
|
8693
9767
|
}
|
|
8694
9768
|
} catch {
|
|
8695
9769
|
parent = void 0;
|
|
@@ -8743,18 +9817,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8743
9817
|
if (cached) return cached;
|
|
8744
9818
|
const pending = (async () => {
|
|
8745
9819
|
try {
|
|
8746
|
-
const
|
|
8747
|
-
if (
|
|
9820
|
+
const messages2 = await this.getTelemetrySubagentSessionMessages(sessionId);
|
|
9821
|
+
if (messages2 === null) {
|
|
8748
9822
|
this.log({
|
|
8749
9823
|
level: "warn",
|
|
8750
|
-
message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)}
|
|
9824
|
+
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
9825
|
message_id: messageId
|
|
8752
9826
|
});
|
|
8753
9827
|
return null;
|
|
8754
9828
|
}
|
|
8755
|
-
|
|
8756
|
-
if (!Array.isArray(body)) throw new Error("response body was not a message array");
|
|
8757
|
-
return body;
|
|
9829
|
+
return messages2;
|
|
8758
9830
|
} catch (err) {
|
|
8759
9831
|
this.log({
|
|
8760
9832
|
level: "warn",
|
|
@@ -8895,21 +9967,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8895
9967
|
const cached = this.sessionTitles.get(sessionId);
|
|
8896
9968
|
if (cached != null) return cached;
|
|
8897
9969
|
try {
|
|
8898
|
-
|
|
8899
|
-
if (
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
return title;
|
|
8905
|
-
}
|
|
8906
|
-
return null;
|
|
9970
|
+
let title = "";
|
|
9971
|
+
if (this.isV2) {
|
|
9972
|
+
title = (await getV2Session(this.openCodeClient, sessionId)).title?.trim() ?? "";
|
|
9973
|
+
} else {
|
|
9974
|
+
const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
|
|
9975
|
+
title = typeof body?.title === "string" ? body.title.trim() : "";
|
|
8907
9976
|
}
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
9977
|
+
if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
|
|
9978
|
+
this.sessionTitles.set(sessionId, title);
|
|
9979
|
+
return title;
|
|
9980
|
+
}
|
|
9981
|
+
return null;
|
|
8913
9982
|
} catch (err) {
|
|
8914
9983
|
this.log({
|
|
8915
9984
|
level: "debug",
|
|
@@ -9007,7 +10076,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9007
10076
|
* `SessionStatus` only.
|
|
9008
10077
|
*/
|
|
9009
10078
|
async isAnyDescendantSessionAlive(rootSessionId) {
|
|
9010
|
-
const sessions = await listSessions(
|
|
10079
|
+
const sessions = await this.listSessions();
|
|
9011
10080
|
if (!sessions) {
|
|
9012
10081
|
this.log({
|
|
9013
10082
|
level: "warn",
|
|
@@ -9018,7 +10087,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9018
10087
|
for (const candidate of sessions) {
|
|
9019
10088
|
if (!candidate?.id || candidate.id === rootSessionId) continue;
|
|
9020
10089
|
if (!await this.sessionBelongsTo(candidate.id, rootSessionId)) continue;
|
|
9021
|
-
const childMsgs = await
|
|
10090
|
+
const childMsgs = await this.getSubagentSessionMessages(candidate.id);
|
|
9022
10091
|
if (isSessionActivelyGenerating(childMsgs)) {
|
|
9023
10092
|
return true;
|
|
9024
10093
|
}
|
|
@@ -9080,7 +10149,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9080
10149
|
* `isB2AbandonmentConfirmed`.
|
|
9081
10150
|
*/
|
|
9082
10151
|
async isAnyDescendantSessionOngoing(rootSessionId) {
|
|
9083
|
-
const sessions = await listSessions(
|
|
10152
|
+
const sessions = await this.listSessions();
|
|
9084
10153
|
if (!sessions) {
|
|
9085
10154
|
this.log({
|
|
9086
10155
|
level: "warn",
|
|
@@ -9097,7 +10166,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9097
10166
|
continue;
|
|
9098
10167
|
}
|
|
9099
10168
|
if (membership === false) continue;
|
|
9100
|
-
const ongoing = await isSessionOngoing(
|
|
10169
|
+
const ongoing = await this.isSessionOngoing(candidate.id);
|
|
9101
10170
|
if (ongoing === true) return true;
|
|
9102
10171
|
if (ongoing === null) indeterminate = true;
|
|
9103
10172
|
}
|
|
@@ -9437,7 +10506,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9437
10506
|
const classified = messageFailure(messages, userMessageId);
|
|
9438
10507
|
if (classified != null) return classified;
|
|
9439
10508
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
9440
|
-
const hasProvider = await hasAnyConfiguredProvider(
|
|
10509
|
+
const hasProvider = await this.hasAnyConfiguredProvider();
|
|
9441
10510
|
return applyZeroProviderFallback(
|
|
9442
10511
|
classified,
|
|
9443
10512
|
hasProvider,
|
|
@@ -9510,7 +10579,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9510
10579
|
const succeededProviders = /* @__PURE__ */ new Set();
|
|
9511
10580
|
for (const ref of refs) {
|
|
9512
10581
|
try {
|
|
9513
|
-
const childMessages = await
|
|
10582
|
+
const childMessages = await this.getSubagentSessionMessages(ref.sessionId);
|
|
9514
10583
|
if (childMessages === null) {
|
|
9515
10584
|
this.log({
|
|
9516
10585
|
level: "debug",
|
|
@@ -9841,6 +10910,7 @@ Port ${port} is already in use.`));
|
|
|
9841
10910
|
|
|
9842
10911
|
// src/commands/ensure-opencode-v2.ts
|
|
9843
10912
|
import chalk6 from "chalk";
|
|
10913
|
+
import ora3 from "ora";
|
|
9844
10914
|
import { select as select3 } from "@inquirer/prompts";
|
|
9845
10915
|
async function probeOpenCode2WithoutPassword(port) {
|
|
9846
10916
|
try {
|
|
@@ -9866,11 +10936,7 @@ function unknownPasswordError(port) {
|
|
|
9866
10936
|
`OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
|
|
9867
10937
|
);
|
|
9868
10938
|
}
|
|
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
|
-
}
|
|
10939
|
+
var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
|
|
9874
10940
|
async function ensureOpenCode2Running(ctx) {
|
|
9875
10941
|
const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
|
|
9876
10942
|
if (initialHealth.authFailed) {
|
|
@@ -9913,13 +10979,37 @@ Port ${port} is already in use.`));
|
|
|
9913
10979
|
}
|
|
9914
10980
|
}
|
|
9915
10981
|
if (!ctx.interactive) {
|
|
9916
|
-
|
|
10982
|
+
ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
|
|
10983
|
+
const { child: proc, password } = await startOpenCode2(port, {
|
|
10984
|
+
inheritStdio: ctx.inheritStdio
|
|
10985
|
+
});
|
|
10986
|
+
const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
|
|
10987
|
+
if (!health.healthy) {
|
|
10988
|
+
return {
|
|
10989
|
+
port,
|
|
10990
|
+
process: proc,
|
|
10991
|
+
version: null,
|
|
10992
|
+
notReadyReason: `it did not become healthy within ${Math.round(ctx.startTimeoutMs / 1e3)}s`,
|
|
10993
|
+
password
|
|
10994
|
+
};
|
|
10995
|
+
}
|
|
10996
|
+
ctx.log(`OpenCode V2 started on port ${port}${health.version ? ` (v${health.version})` : ""}`);
|
|
10997
|
+
return {
|
|
10998
|
+
port,
|
|
10999
|
+
process: proc,
|
|
11000
|
+
version: health.version ?? null,
|
|
11001
|
+
notReadyReason: null,
|
|
11002
|
+
password
|
|
11003
|
+
};
|
|
9917
11004
|
}
|
|
9918
|
-
console.log(chalk6.yellow(`
|
|
9919
|
-
${v2SessionSupportIncompleteError().message}`));
|
|
9920
11005
|
const action = await select3({
|
|
9921
11006
|
message: "OpenCode V2 is not running. What would you like to do?",
|
|
9922
11007
|
choices: [
|
|
11008
|
+
{
|
|
11009
|
+
name: "Start OpenCode V2 for me",
|
|
11010
|
+
value: "start",
|
|
11011
|
+
description: `Run 'opencode2 serve --port ${port}'`
|
|
11012
|
+
},
|
|
9923
11013
|
{
|
|
9924
11014
|
name: "Show me the command",
|
|
9925
11015
|
value: "manual",
|
|
@@ -9940,6 +11030,25 @@ ${v2SessionSupportIncompleteError().message}`));
|
|
|
9940
11030
|
blank();
|
|
9941
11031
|
throw new Error("Please start OpenCode V2 manually");
|
|
9942
11032
|
}
|
|
11033
|
+
if (action === "start") {
|
|
11034
|
+
const spinner = ora3("Starting OpenCode V2...").start();
|
|
11035
|
+
const { child: proc, password } = await startOpenCode2(port, {
|
|
11036
|
+
inheritStdio: ctx.inheritStdio
|
|
11037
|
+
});
|
|
11038
|
+
const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
|
|
11039
|
+
if (!health.healthy) {
|
|
11040
|
+
spinner.fail("Failed to start OpenCode V2");
|
|
11041
|
+
throw new Error("OpenCode V2 failed to start");
|
|
11042
|
+
}
|
|
11043
|
+
spinner.stop();
|
|
11044
|
+
return {
|
|
11045
|
+
port,
|
|
11046
|
+
process: proc,
|
|
11047
|
+
version: health.version ?? null,
|
|
11048
|
+
notReadyReason: null,
|
|
11049
|
+
password
|
|
11050
|
+
};
|
|
11051
|
+
}
|
|
9943
11052
|
return {
|
|
9944
11053
|
port,
|
|
9945
11054
|
process: null,
|
|
@@ -10833,7 +11942,7 @@ async function driveChannels(state, driver) {
|
|
|
10833
11942
|
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
10834
11943
|
if (opencodeAuthApplied) state.openaiUsageRearm?.();
|
|
10835
11944
|
if (claudeCredentialApplied || opencodeAuthApplied) {
|
|
10836
|
-
void reloadProviderCache(state.port).catch(
|
|
11945
|
+
void reloadProviderCache(state.port, state.opencodeClient ?? void 0).catch(
|
|
10837
11946
|
(error2) => logActivity(state, {
|
|
10838
11947
|
type: "error",
|
|
10839
11948
|
error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
@@ -10958,7 +12067,7 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
|
|
|
10958
12067
|
async function runSweep(state, driver, config) {
|
|
10959
12068
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
10960
12069
|
try {
|
|
10961
|
-
const sessions = await listSessions(state.port);
|
|
12070
|
+
const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
|
|
10962
12071
|
if (sessions === null) {
|
|
10963
12072
|
logActivity(state, {
|
|
10964
12073
|
type: "info",
|
|
@@ -10988,7 +12097,7 @@ async function runSweep(state, driver, config) {
|
|
|
10988
12097
|
});
|
|
10989
12098
|
continue;
|
|
10990
12099
|
}
|
|
10991
|
-
if (await deleteSession(state.port, id)) deleted++;
|
|
12100
|
+
if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
|
|
10992
12101
|
else failed++;
|
|
10993
12102
|
}
|
|
10994
12103
|
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
@@ -11496,6 +12605,8 @@ async function run(options) {
|
|
|
11496
12605
|
opencodeConnected: false,
|
|
11497
12606
|
opencodeVersion: null,
|
|
11498
12607
|
opencodeApiVersion: "v1",
|
|
12608
|
+
opencodePassword: null,
|
|
12609
|
+
opencodeClient: null,
|
|
11499
12610
|
sessionDbProvenanceAnomaly: false,
|
|
11500
12611
|
opencodeProcess: null,
|
|
11501
12612
|
stopOpenCodeLogTail: null,
|
|
@@ -11650,7 +12761,7 @@ async function run(options) {
|
|
|
11650
12761
|
console.log(chalk7.bold("Evident Run"));
|
|
11651
12762
|
console.log(chalk7.dim("-".repeat(40)));
|
|
11652
12763
|
}
|
|
11653
|
-
const spinner = interactive && !state.json ?
|
|
12764
|
+
const spinner = interactive && !state.json ? ora4("Validating runner...").start() : null;
|
|
11654
12765
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
11655
12766
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
11656
12767
|
spinner?.fail("Authentication failed");
|
|
@@ -11771,7 +12882,7 @@ async function run(options) {
|
|
|
11771
12882
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
11772
12883
|
}
|
|
11773
12884
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
11774
|
-
const ocSpinner = interactive && !state.json ?
|
|
12885
|
+
const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
|
|
11775
12886
|
try {
|
|
11776
12887
|
const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
|
|
11777
12888
|
port: state.port,
|
|
@@ -11792,6 +12903,18 @@ async function run(options) {
|
|
|
11792
12903
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
11793
12904
|
state.opencodeVersion = oc.version;
|
|
11794
12905
|
state.opencodeApiVersion = opencodeVersion;
|
|
12906
|
+
let opencodePassword = null;
|
|
12907
|
+
if (opencodeVersion === "v2" && "password" in oc) {
|
|
12908
|
+
const value = oc.password;
|
|
12909
|
+
if (typeof value === "string" || value === null) opencodePassword = value;
|
|
12910
|
+
}
|
|
12911
|
+
state.opencodePassword = opencodePassword;
|
|
12912
|
+
const openCodeClient = createOpenCodeClient({
|
|
12913
|
+
port: state.port,
|
|
12914
|
+
version: state.opencodeApiVersion,
|
|
12915
|
+
password: state.opencodePassword
|
|
12916
|
+
});
|
|
12917
|
+
state.opencodeClient = openCodeClient;
|
|
11795
12918
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
11796
12919
|
try {
|
|
11797
12920
|
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
@@ -11838,9 +12961,9 @@ async function run(options) {
|
|
|
11838
12961
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
11839
12962
|
}
|
|
11840
12963
|
}
|
|
11841
|
-
await reloadProviderCache(state.port);
|
|
12964
|
+
await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
|
|
11842
12965
|
const noProviderWarning = buildNoProviderWarning(
|
|
11843
|
-
await hasAnyConfiguredProvider(state.port)
|
|
12966
|
+
await hasAnyConfiguredProvider(state.port, state.opencodeClient ?? void 0)
|
|
11844
12967
|
);
|
|
11845
12968
|
if (noProviderWarning) {
|
|
11846
12969
|
log2(state, noProviderWarning, "warn");
|
|
@@ -11963,11 +13086,12 @@ async function run(options) {
|
|
|
11963
13086
|
});
|
|
11964
13087
|
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
11965
13088
|
}
|
|
11966
|
-
const tunnelSpinner = interactive && !state.json ?
|
|
13089
|
+
const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
|
|
11967
13090
|
const channelDriver = new ChannelDriver({
|
|
11968
13091
|
agentId: state.agentId,
|
|
11969
13092
|
port: state.port,
|
|
11970
13093
|
apiUrl: getApiUrlConfig(),
|
|
13094
|
+
openCodeClient: state.opencodeClient ?? void 0,
|
|
11971
13095
|
getAuthHeader: () => state.authHeader,
|
|
11972
13096
|
conversationFilter: state.conversationFilter,
|
|
11973
13097
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
@@ -11993,6 +13117,7 @@ async function run(options) {
|
|
|
11993
13117
|
agentId: state.agentId,
|
|
11994
13118
|
getAuthHeader: () => state.authHeader,
|
|
11995
13119
|
port: state.port,
|
|
13120
|
+
openCodePassword: state.opencodePassword,
|
|
11996
13121
|
isRunning: () => state.running,
|
|
11997
13122
|
events: {
|
|
11998
13123
|
onConnected: (agentId, isReconnect) => {
|
|
@@ -12137,7 +13262,7 @@ async function run(options) {
|
|
|
12137
13262
|
state.openaiUsageTimer = timer;
|
|
12138
13263
|
},
|
|
12139
13264
|
fetchUsage: async () => {
|
|
12140
|
-
const usage = await getOpenAiUsage(state.port);
|
|
13265
|
+
const usage = await getOpenAiUsage(state.port, state.opencodeClient ?? void 0);
|
|
12141
13266
|
if (usage.subscription === null) {
|
|
12142
13267
|
logActivity(state, {
|
|
12143
13268
|
type: "info",
|