@evident-ai/cli 3.4.1-dev.7393414 → 3.4.1-dev.7703679
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 +1454 -254
- 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.
|
|
1216
|
+
var CLI_VERSION = (true ? "3.4.1-dev.7703679" : 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";
|
|
@@ -2347,15 +2459,23 @@ function isQueueValidatedVersion(version2) {
|
|
|
2347
2459
|
if (!version2) return false;
|
|
2348
2460
|
return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version2);
|
|
2349
2461
|
}
|
|
2350
|
-
function buildOpenCodeVersionWarning(version2) {
|
|
2351
|
-
if (
|
|
2352
|
-
const detected = version2 ? `v${version2}` : "unknown";
|
|
2462
|
+
function buildOpenCodeVersionWarning(version2, major) {
|
|
2463
|
+
if (major === "v2") return null;
|
|
2353
2464
|
const validated = QUEUE_VALIDATED_OPENCODE_VERSIONS.map((v) => `v${v}`).join(", ");
|
|
2354
|
-
|
|
2465
|
+
if (!version2) {
|
|
2466
|
+
return `Warning: the running opencode's version could not be determined from its health response, so queue validation could not be checked (validated: ${validated}). Compare against \`opencode --version\`; continuing anyway.`;
|
|
2467
|
+
}
|
|
2468
|
+
if (isQueueValidatedVersion(version2)) return null;
|
|
2469
|
+
return `Warning: opencode v${version2} is not a queue-validated version (validated: ${validated}). Native message queuing \u2014 which channel (Slack) message handling relies on \u2014 is unverified on this version; queued/follow-up messages may behave unexpectedly. Continuing anyway. Bumping the validated set requires re-running the queue validation.`;
|
|
2470
|
+
}
|
|
2471
|
+
function reportedOpenCodeVersion(input) {
|
|
2472
|
+
if (!input.connected) return null;
|
|
2473
|
+
return input.version || `${input.major}-unknown`;
|
|
2355
2474
|
}
|
|
2356
2475
|
|
|
2357
2476
|
// src/lib/opencode/process.ts
|
|
2358
2477
|
import { execSync, spawn as spawn3 } from "child_process";
|
|
2478
|
+
import { randomBytes } from "node:crypto";
|
|
2359
2479
|
|
|
2360
2480
|
// src/lib/process-stop.ts
|
|
2361
2481
|
async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
@@ -2414,6 +2534,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
2414
2534
|
// src/lib/opencode/process.ts
|
|
2415
2535
|
var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
|
|
2416
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
|
+
]);
|
|
2417
2548
|
function resolveOpenCodeLogLevel(env) {
|
|
2418
2549
|
const raw = env.OPENCODE_LOG_LEVEL;
|
|
2419
2550
|
if (!raw) return "INFO";
|
|
@@ -2424,6 +2555,16 @@ function resolveOpenCodeLogLevel(env) {
|
|
|
2424
2555
|
);
|
|
2425
2556
|
return "INFO";
|
|
2426
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
|
+
}
|
|
2427
2568
|
function getProcessCwd(pid) {
|
|
2428
2569
|
const platform = process.platform;
|
|
2429
2570
|
try {
|
|
@@ -2606,6 +2747,37 @@ async function startOpenCode(port, options = {}) {
|
|
|
2606
2747
|
});
|
|
2607
2748
|
return child;
|
|
2608
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
|
+
}
|
|
2609
2781
|
function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
|
|
2610
2782
|
const sendSignal = (signal) => {
|
|
2611
2783
|
if (process.platform === "win32") {
|
|
@@ -2753,27 +2925,500 @@ function buildNoProviderWarning(hasProvider) {
|
|
|
2753
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).";
|
|
2754
2926
|
}
|
|
2755
2927
|
|
|
2756
|
-
// src/lib/
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
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
|
+
}
|
|
2760
3398
|
}
|
|
2761
3399
|
|
|
2762
3400
|
// src/lib/opencode/session.ts
|
|
3401
|
+
var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
2763
3402
|
function timedFetch(input, init) {
|
|
2764
3403
|
return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
|
|
2765
3404
|
}
|
|
3405
|
+
function requestWithClient(port, client, path, init, options) {
|
|
3406
|
+
return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
|
|
3407
|
+
}
|
|
2766
3408
|
function opencodeBase(port) {
|
|
2767
3409
|
return `http://127.0.0.1:${port}`;
|
|
2768
3410
|
}
|
|
2769
|
-
async function getOpenCodeDirectory(port) {
|
|
3411
|
+
async function getOpenCodeDirectory(port, client) {
|
|
2770
3412
|
try {
|
|
2771
|
-
const res = await
|
|
3413
|
+
const res = await requestWithClient(port, client, "/path");
|
|
2772
3414
|
if (!res.ok) return null;
|
|
2773
3415
|
const body = await res.json();
|
|
2774
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;
|
|
2775
3417
|
return dir && dir.trim() ? dir.trim() : null;
|
|
2776
|
-
} catch {
|
|
3418
|
+
} catch (error2) {
|
|
3419
|
+
console.error(
|
|
3420
|
+
`[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3421
|
+
);
|
|
2777
3422
|
return null;
|
|
2778
3423
|
}
|
|
2779
3424
|
}
|
|
@@ -2817,16 +3462,48 @@ function isAssistantInFlight(m) {
|
|
|
2817
3462
|
if (completedOf(m) == null) return true;
|
|
2818
3463
|
return finishOf(m) === "tool-calls";
|
|
2819
3464
|
}
|
|
2820
|
-
async function getSessionMessages(port, sessionId) {
|
|
3465
|
+
async function getSessionMessages(port, sessionId, client) {
|
|
2821
3466
|
try {
|
|
2822
|
-
const
|
|
3467
|
+
const path = `/session/${sessionId}/message`;
|
|
3468
|
+
const res = await requestWithClient(port, client, path);
|
|
2823
3469
|
if (!res.ok) return null;
|
|
2824
3470
|
const body = await res.json();
|
|
2825
3471
|
return Array.isArray(body) ? body : null;
|
|
2826
|
-
} catch {
|
|
3472
|
+
} catch (error2) {
|
|
3473
|
+
console.error(
|
|
3474
|
+
`[getSessionMessages] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3475
|
+
);
|
|
2827
3476
|
return null;
|
|
2828
3477
|
}
|
|
2829
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
|
+
}
|
|
2830
3507
|
function isSessionActivelyGenerating(messages) {
|
|
2831
3508
|
if (!messages || messages.length === 0) return false;
|
|
2832
3509
|
const last = messages[messages.length - 1];
|
|
@@ -2847,27 +3524,37 @@ function sessionLastActivityMs(session) {
|
|
|
2847
3524
|
}
|
|
2848
3525
|
return null;
|
|
2849
3526
|
}
|
|
2850
|
-
async function listSessions(port) {
|
|
3527
|
+
async function listSessions(port, client) {
|
|
3528
|
+
if (client?.version === "v2") return listV2Sessions(client);
|
|
2851
3529
|
try {
|
|
2852
|
-
const res = await
|
|
3530
|
+
const res = await requestWithClient(port, client, "/session");
|
|
2853
3531
|
if (!res.ok) return null;
|
|
2854
3532
|
const body = await res.json();
|
|
2855
3533
|
return Array.isArray(body) ? body : null;
|
|
2856
|
-
} catch {
|
|
3534
|
+
} catch (error2) {
|
|
3535
|
+
console.error(
|
|
3536
|
+
`[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3537
|
+
);
|
|
2857
3538
|
return null;
|
|
2858
3539
|
}
|
|
2859
3540
|
}
|
|
2860
|
-
async function deleteSession(port, id) {
|
|
3541
|
+
async function deleteSession(port, id, client) {
|
|
3542
|
+
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
2861
3543
|
try {
|
|
2862
|
-
const res = await
|
|
3544
|
+
const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
|
|
2863
3545
|
return res.status >= 200 && res.status < 300;
|
|
2864
|
-
} catch {
|
|
3546
|
+
} catch (error2) {
|
|
3547
|
+
console.error(
|
|
3548
|
+
`[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3549
|
+
);
|
|
2865
3550
|
return false;
|
|
2866
3551
|
}
|
|
2867
3552
|
}
|
|
2868
|
-
async function sessionExists(port, id) {
|
|
3553
|
+
async function sessionExists(port, id, client) {
|
|
2869
3554
|
try {
|
|
2870
|
-
const res = await
|
|
3555
|
+
const res = await requestWithClient(port, client, `/session/${id}`, void 0, {
|
|
3556
|
+
allowStatuses: [404]
|
|
3557
|
+
});
|
|
2871
3558
|
if (res.status >= 200 && res.status < 300) return true;
|
|
2872
3559
|
if (res.status === 404) return false;
|
|
2873
3560
|
return null;
|
|
@@ -2875,9 +3562,22 @@ async function sessionExists(port, id) {
|
|
|
2875
3562
|
return null;
|
|
2876
3563
|
}
|
|
2877
3564
|
}
|
|
2878
|
-
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;
|
|
2879
3579
|
try {
|
|
2880
|
-
const res = await
|
|
3580
|
+
const res = await requestWithClient(port, client, "/session/status");
|
|
2881
3581
|
if (!res.ok) {
|
|
2882
3582
|
console.error(
|
|
2883
3583
|
`[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
|
|
@@ -2899,22 +3599,28 @@ async function getSessionStatuses(port) {
|
|
|
2899
3599
|
return null;
|
|
2900
3600
|
}
|
|
2901
3601
|
}
|
|
2902
|
-
async function isSessionOngoing(port, id) {
|
|
2903
|
-
|
|
3602
|
+
async function isSessionOngoing(port, id, client) {
|
|
3603
|
+
if (client?.version === "v2") return isV2SessionOngoing(client, id);
|
|
3604
|
+
const map = await getSessionStatuses(port, client);
|
|
2904
3605
|
if (map == null) return null;
|
|
2905
3606
|
const entry = map[id];
|
|
2906
3607
|
return entry != null && entry.type !== "idle";
|
|
2907
3608
|
}
|
|
2908
|
-
async function createOpenCodeSession(port, directory) {
|
|
2909
|
-
const
|
|
2910
|
-
if (directory && directory.trim())
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
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
|
+
);
|
|
2918
3624
|
if (!response.ok) {
|
|
2919
3625
|
const text = await response.text().catch(() => "");
|
|
2920
3626
|
throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ""}`);
|
|
@@ -2922,10 +3628,16 @@ async function createOpenCodeSession(port, directory) {
|
|
|
2922
3628
|
const data = await response.json();
|
|
2923
3629
|
return data.id;
|
|
2924
3630
|
}
|
|
2925
|
-
async function getModelAttachmentCapability(port, model) {
|
|
3631
|
+
async function getModelAttachmentCapability(port, model, client) {
|
|
2926
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
|
+
}
|
|
2927
3639
|
try {
|
|
2928
|
-
const res = await
|
|
3640
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
2929
3641
|
if (!res.ok) {
|
|
2930
3642
|
console.error(
|
|
2931
3643
|
`[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
@@ -3045,19 +3757,43 @@ function applyModelOptions(body, options) {
|
|
|
3045
3757
|
}
|
|
3046
3758
|
if (variant) body.variant = variant;
|
|
3047
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
|
+
}
|
|
3783
|
+
}
|
|
3048
3784
|
function messageText(m) {
|
|
3049
3785
|
if (!m || !Array.isArray(m.parts)) return "";
|
|
3050
3786
|
return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
3051
3787
|
}
|
|
3052
|
-
async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
3053
|
-
const before = await getSessionMessages(port, sessionId);
|
|
3788
|
+
async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
|
|
3789
|
+
const before = await getSessionMessages(port, sessionId, client);
|
|
3054
3790
|
const knownUserIds = new Set(
|
|
3055
3791
|
(before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
|
|
3056
3792
|
);
|
|
3057
3793
|
const parts = [{ type: "text", text: content }];
|
|
3058
3794
|
let pendingOutcomes = null;
|
|
3059
3795
|
if (attachments && attachments.inputs.length > 0) {
|
|
3060
|
-
const capable = await getModelAttachmentCapability(port, options?.model);
|
|
3796
|
+
const capable = await getModelAttachmentCapability(port, options?.model, client);
|
|
3061
3797
|
const {
|
|
3062
3798
|
parts: fileParts,
|
|
3063
3799
|
outcomes,
|
|
@@ -3070,11 +3806,17 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
3070
3806
|
parts
|
|
3071
3807
|
};
|
|
3072
3808
|
applyModelOptions(body, options);
|
|
3073
|
-
const res = await
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
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
|
+
);
|
|
3078
3820
|
if (res.status < 200 || res.status >= 300) {
|
|
3079
3821
|
const text = await res.text().catch(() => "");
|
|
3080
3822
|
const { variant } = splitModelVariant(options?.model);
|
|
@@ -3085,7 +3827,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
3085
3827
|
const READ_BACK_ATTEMPTS = 5;
|
|
3086
3828
|
const READ_BACK_DELAY_MS = 150;
|
|
3087
3829
|
for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
|
|
3088
|
-
const after = await getSessionMessages(port, sessionId);
|
|
3830
|
+
const after = await getSessionMessages(port, sessionId, client);
|
|
3089
3831
|
if (after) {
|
|
3090
3832
|
let best = null;
|
|
3091
3833
|
for (const m of after) {
|
|
@@ -3188,7 +3930,7 @@ function collectSubagentSessions(messages, userMessageId) {
|
|
|
3188
3930
|
}
|
|
3189
3931
|
return refs;
|
|
3190
3932
|
}
|
|
3191
|
-
function
|
|
3933
|
+
function finiteNumber2(value) {
|
|
3192
3934
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3193
3935
|
}
|
|
3194
3936
|
function taskCallModel(value) {
|
|
@@ -3217,8 +3959,8 @@ function collectTaskCalls(messages, userMessageId) {
|
|
|
3217
3959
|
parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
|
|
3218
3960
|
model: taskCallModel(metadata?.model),
|
|
3219
3961
|
status: part.state.status ?? "unknown",
|
|
3220
|
-
timeStart:
|
|
3221
|
-
timeEnd:
|
|
3962
|
+
timeStart: finiteNumber2(part.state.time?.start),
|
|
3963
|
+
timeEnd: finiteNumber2(part.state.time?.end)
|
|
3222
3964
|
});
|
|
3223
3965
|
}
|
|
3224
3966
|
}
|
|
@@ -3233,7 +3975,7 @@ function attributeTaskCallUsage(messages, windows) {
|
|
|
3233
3975
|
const unattributed = [];
|
|
3234
3976
|
for (const message of messages ?? []) {
|
|
3235
3977
|
if (roleOf(message) !== "assistant") continue;
|
|
3236
|
-
const created =
|
|
3978
|
+
const created = finiteNumber2(createdOf(message));
|
|
3237
3979
|
const matching = created === null ? [] : eligibleWindows.filter(
|
|
3238
3980
|
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
3239
3981
|
);
|
|
@@ -3472,11 +4214,53 @@ function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageI
|
|
|
3472
4214
|
hasStartedLaterUser = true;
|
|
3473
4215
|
}
|
|
3474
4216
|
}
|
|
3475
|
-
return hasLaterUser && hasStartedLaterUser;
|
|
3476
|
-
}
|
|
3477
|
-
async function hasAnyConfiguredProvider(port) {
|
|
4217
|
+
return hasLaterUser && hasStartedLaterUser;
|
|
4218
|
+
}
|
|
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
|
+
}
|
|
3478
4262
|
try {
|
|
3479
|
-
const res = await
|
|
4263
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
3480
4264
|
if (!res.ok) {
|
|
3481
4265
|
console.error(
|
|
3482
4266
|
`[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
@@ -3505,7 +4289,7 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3505
4289
|
return null;
|
|
3506
4290
|
}
|
|
3507
4291
|
}
|
|
3508
|
-
function
|
|
4292
|
+
function sessionErrorReason2(error2) {
|
|
3509
4293
|
const record = typeof error2 === "object" && error2 !== null ? error2 : null;
|
|
3510
4294
|
const data = record?.data;
|
|
3511
4295
|
const dataRecord = typeof data === "object" && data !== null ? data : null;
|
|
@@ -3535,16 +4319,20 @@ function parseSessionErrorFrame(data) {
|
|
|
3535
4319
|
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
3536
4320
|
return {
|
|
3537
4321
|
sessionId,
|
|
3538
|
-
reason:
|
|
4322
|
+
reason: sessionErrorReason2(propertiesRecord.error)
|
|
3539
4323
|
};
|
|
3540
4324
|
}
|
|
3541
|
-
async function readSessionErrorStream(port, options) {
|
|
4325
|
+
async function readSessionErrorStream(port, options, client) {
|
|
4326
|
+
if (client?.version === "v2") return readV2SessionErrorStream(client, options);
|
|
3542
4327
|
let reader = null;
|
|
3543
4328
|
try {
|
|
3544
|
-
const response = await
|
|
4329
|
+
const response = await (client?.request("/event", {
|
|
3545
4330
|
headers: { accept: "text/event-stream" },
|
|
3546
4331
|
signal: options.signal
|
|
3547
|
-
})
|
|
4332
|
+
}) ?? fetch(`${opencodeBase(port)}/event`, {
|
|
4333
|
+
headers: { accept: "text/event-stream" },
|
|
4334
|
+
signal: options.signal
|
|
4335
|
+
}));
|
|
3548
4336
|
if (!response.ok || !response.body) {
|
|
3549
4337
|
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
3550
4338
|
}
|
|
@@ -3575,9 +4363,10 @@ async function readSessionErrorStream(port, options) {
|
|
|
3575
4363
|
if (reader) void reader.cancel().catch(() => void 0);
|
|
3576
4364
|
}
|
|
3577
4365
|
}
|
|
3578
|
-
async function reloadProviderCache(port) {
|
|
4366
|
+
async function reloadProviderCache(port, client) {
|
|
4367
|
+
if (client?.version === "v2") return;
|
|
3579
4368
|
try {
|
|
3580
|
-
const res = await
|
|
4369
|
+
const res = await requestWithClient(port, client, "/config", {
|
|
3581
4370
|
method: "PATCH",
|
|
3582
4371
|
headers: { "Content-Type": "application/json" },
|
|
3583
4372
|
body: JSON.stringify({})
|
|
@@ -3955,12 +4744,14 @@ var STRIP_RES = /* @__PURE__ */ new Set([
|
|
|
3955
4744
|
"content-length"
|
|
3956
4745
|
]);
|
|
3957
4746
|
var StreamForwarder = class {
|
|
3958
|
-
constructor(ws, port, callbacks = {}) {
|
|
4747
|
+
constructor(ws, port, callbacks = {}, options = {}) {
|
|
3959
4748
|
this.ws = ws;
|
|
3960
4749
|
this.port = port;
|
|
3961
4750
|
this.callbacks = callbacks;
|
|
4751
|
+
this.options = options;
|
|
3962
4752
|
}
|
|
3963
4753
|
inflight = /* @__PURE__ */ new Map();
|
|
4754
|
+
binaryFramesSupported = false;
|
|
3964
4755
|
/**
|
|
3965
4756
|
* Handle an edge→agent frame. Unknown frame types are ignored.
|
|
3966
4757
|
*/
|
|
@@ -3980,6 +4771,12 @@ var StreamForwarder = class {
|
|
|
3980
4771
|
break;
|
|
3981
4772
|
}
|
|
3982
4773
|
}
|
|
4774
|
+
handleBinaryBodyFrame(sid, payload) {
|
|
4775
|
+
this.inflight.get(sid)?.pushBody?.(payload);
|
|
4776
|
+
}
|
|
4777
|
+
setBinaryFramesSupported(supported) {
|
|
4778
|
+
this.binaryFramesSupported = supported;
|
|
4779
|
+
}
|
|
3983
4780
|
/**
|
|
3984
4781
|
* Abort every in-flight stream (e.g. on WebSocket close).
|
|
3985
4782
|
*/
|
|
@@ -4042,7 +4839,15 @@ var StreamForwarder = class {
|
|
|
4042
4839
|
}
|
|
4043
4840
|
const fwdHeaders = {};
|
|
4044
4841
|
for (const [k, v] of Object.entries(headers ?? {})) {
|
|
4045
|
-
|
|
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);
|
|
4046
4851
|
}
|
|
4047
4852
|
this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });
|
|
4048
4853
|
const body = bodyPromise ? await bodyPromise : void 0;
|
|
@@ -4089,7 +4894,13 @@ var StreamForwarder = class {
|
|
|
4089
4894
|
const chunk = Buffer.from(value);
|
|
4090
4895
|
for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
|
|
4091
4896
|
const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
|
|
4092
|
-
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
|
+
}
|
|
4093
4904
|
}
|
|
4094
4905
|
}
|
|
4095
4906
|
}
|
|
@@ -4106,6 +4917,11 @@ var StreamForwarder = class {
|
|
|
4106
4917
|
|
|
4107
4918
|
// src/lib/tunnel/connection.ts
|
|
4108
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
|
+
}
|
|
4109
4925
|
var TunnelUpgradeRejectedError = class extends Error {
|
|
4110
4926
|
constructor(message, reason) {
|
|
4111
4927
|
super(message);
|
|
@@ -4155,6 +4971,7 @@ function connectTunnel(options) {
|
|
|
4155
4971
|
agentId,
|
|
4156
4972
|
authHeader,
|
|
4157
4973
|
port,
|
|
4974
|
+
openCodePassword,
|
|
4158
4975
|
onConnected,
|
|
4159
4976
|
onDisconnected,
|
|
4160
4977
|
onError,
|
|
@@ -4169,14 +4986,20 @@ function connectTunnel(options) {
|
|
|
4169
4986
|
return new Promise((resolve4, reject) => {
|
|
4170
4987
|
const ws = new WebSocket2(url, {
|
|
4171
4988
|
headers: {
|
|
4172
|
-
Authorization: authHeader
|
|
4989
|
+
Authorization: authHeader,
|
|
4990
|
+
"X-Evident-Tunnel-Binary-Frames": "1"
|
|
4173
4991
|
}
|
|
4174
4992
|
});
|
|
4175
|
-
const forwarder = new StreamForwarder(
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4993
|
+
const forwarder = new StreamForwarder(
|
|
4994
|
+
ws,
|
|
4995
|
+
port,
|
|
4996
|
+
{
|
|
4997
|
+
onHead: () => onResponse?.(),
|
|
4998
|
+
onDrainPing: () => onDrainPing?.(),
|
|
4999
|
+
onUsageRearmPing: () => onUsageRearmPing?.()
|
|
5000
|
+
},
|
|
5001
|
+
{ openCodePassword }
|
|
5002
|
+
);
|
|
4180
5003
|
const connectionTimeout = setTimeout(() => {
|
|
4181
5004
|
ws.close();
|
|
4182
5005
|
reject(new Error("Connection timeout"));
|
|
@@ -4213,7 +5036,25 @@ function connectTunnel(options) {
|
|
|
4213
5036
|
ws.on("open", () => {
|
|
4214
5037
|
onInfo?.("WebSocket connection established");
|
|
4215
5038
|
});
|
|
4216
|
-
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
|
+
}
|
|
4217
5058
|
let message;
|
|
4218
5059
|
try {
|
|
4219
5060
|
message = JSON.parse(data.toString());
|
|
@@ -4229,6 +5070,7 @@ function connectTunnel(options) {
|
|
|
4229
5070
|
switch (message.type) {
|
|
4230
5071
|
case "connected": {
|
|
4231
5072
|
clearTimeout(connectionTimeout);
|
|
5073
|
+
forwarder.setBinaryFramesSupported(message.binary_frames === true);
|
|
4232
5074
|
const connectedAgentId = message.agent_id ?? agentId;
|
|
4233
5075
|
onConnected?.(connectedAgentId);
|
|
4234
5076
|
resolve4({
|
|
@@ -4319,6 +5161,7 @@ var RunnerConnection = class {
|
|
|
4319
5161
|
agentId: this.resolvedAgentId,
|
|
4320
5162
|
authHeader: this.opts.getAuthHeader(),
|
|
4321
5163
|
port: this.opts.port,
|
|
5164
|
+
openCodePassword: this.opts.openCodePassword,
|
|
4322
5165
|
onConnected: (agentId) => {
|
|
4323
5166
|
this.reconnectAttempt = 0;
|
|
4324
5167
|
this.reconnecting = false;
|
|
@@ -4499,33 +5342,73 @@ function parseCodexUsageHeaders(headers) {
|
|
|
4499
5342
|
function normalizeProbeModel(model) {
|
|
4500
5343
|
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
4501
5344
|
}
|
|
4502
|
-
|
|
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) {
|
|
4503
5353
|
try {
|
|
4504
|
-
const
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
if (!res.ok) {
|
|
4509
|
-
console.error(
|
|
4510
|
-
`[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
4511
|
-
);
|
|
4512
|
-
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);
|
|
4513
5358
|
}
|
|
4514
|
-
const
|
|
4515
|
-
|
|
4516
|
-
|
|
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;
|
|
4517
5364
|
const candidates = [
|
|
4518
|
-
...typeof
|
|
5365
|
+
...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
|
|
4519
5366
|
...Object.keys(provider.models)
|
|
4520
5367
|
].map(normalizeProbeModel);
|
|
4521
|
-
return [...new Set(candidates)].slice(0, 4);
|
|
5368
|
+
return { status: "supported", models: [...new Set(candidates)].slice(0, 4) };
|
|
4522
5369
|
} catch (err) {
|
|
4523
|
-
|
|
4524
|
-
`
|
|
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
|
|
4525
5406
|
);
|
|
4526
|
-
return [];
|
|
4527
5407
|
}
|
|
4528
5408
|
}
|
|
5409
|
+
async function resolveProbeModels(port, client = createOpenCodeClient({ port, version: "v1" })) {
|
|
5410
|
+
return client.version === "v2" ? resolveV2ProbeModels(client, port) : resolveV1ProbeModels(client, port);
|
|
5411
|
+
}
|
|
4529
5412
|
function hasPrimaryHeaders(headers) {
|
|
4530
5413
|
return [
|
|
4531
5414
|
"x-codex-primary-used-percent",
|
|
@@ -4533,7 +5416,7 @@ function hasPrimaryHeaders(headers) {
|
|
|
4533
5416
|
"x-codex-primary-reset-at"
|
|
4534
5417
|
].some((name) => headers.has(name));
|
|
4535
5418
|
}
|
|
4536
|
-
async function getOpenAiUsage(port) {
|
|
5419
|
+
async function getOpenAiUsage(port, client) {
|
|
4537
5420
|
const credentials2 = readOpenCodeChatGptCredentials();
|
|
4538
5421
|
if (!credentials2) {
|
|
4539
5422
|
throw new OpenAiUsageError(
|
|
@@ -4548,12 +5431,16 @@ async function getOpenAiUsage(port) {
|
|
|
4548
5431
|
);
|
|
4549
5432
|
}
|
|
4550
5433
|
const subscription = parseChatGptIdentity(credentials2.accessToken);
|
|
4551
|
-
const
|
|
4552
|
-
if (models.length === 0) {
|
|
4553
|
-
|
|
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
|
+
);
|
|
4554
5441
|
}
|
|
4555
5442
|
let lastStatus;
|
|
4556
|
-
for (const model of models) {
|
|
5443
|
+
for (const model of lookup.models) {
|
|
4557
5444
|
let res;
|
|
4558
5445
|
try {
|
|
4559
5446
|
res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
|
|
@@ -5372,6 +6259,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5372
6259
|
maxActiveSessions;
|
|
5373
6260
|
watcherStallMs;
|
|
5374
6261
|
wedgeWarningIntervalMs;
|
|
6262
|
+
openCodeClient;
|
|
5375
6263
|
/** Cache of conversationId → opencode sessionId. */
|
|
5376
6264
|
sessions = /* @__PURE__ */ new Map();
|
|
5377
6265
|
/**
|
|
@@ -5480,20 +6368,26 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5480
6368
|
*/
|
|
5481
6369
|
readopted = /* @__PURE__ */ new Set();
|
|
5482
6370
|
/**
|
|
5483
|
-
*
|
|
5484
|
-
*
|
|
5485
|
-
*
|
|
5486
|
-
*
|
|
5487
|
-
*
|
|
5488
|
-
*
|
|
5489
|
-
* CRITICAL (Bugbot #202): this suppresses ONLY the dispatch/re-attach paths, it
|
|
5490
|
-
* does NOT suppress DONE delivery. A row parked here whose reply later COMPLETES
|
|
5491
|
-
* in opencode must still be delivered via `markDone` on the next drain — so
|
|
5492
|
-
* `readoptOne` computes `state` FIRST and this set is checked only on the
|
|
5493
|
-
* non-done path. It is cleared once the row leaves the processing list (cron
|
|
5494
|
-
* 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.
|
|
5495
6377
|
*/
|
|
5496
6378
|
dontRedispatch = /* @__PURE__ */ new Set();
|
|
6379
|
+
/**
|
|
6380
|
+
* Untrackable-ack fence. Stores the report error so a failed terminal report can
|
|
6381
|
+
* be retried without posting the accepted prompt again. Keep it fenced while the
|
|
6382
|
+
* row is `processing` or `pending`; clear it only when absent from both lists.
|
|
6383
|
+
*/
|
|
6384
|
+
untrackableAck = /* @__PURE__ */ new Map();
|
|
6385
|
+
/** Messages whose untrackable-ack failure signal was emitted while fenced. */
|
|
6386
|
+
untrackableAckSignalled = /* @__PURE__ */ new Set();
|
|
6387
|
+
/** Messages whose first untrackable-ack fence warning was already logged. */
|
|
6388
|
+
untrackableAckWarned = /* @__PURE__ */ new Set();
|
|
6389
|
+
/** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
|
|
6390
|
+
pendingMessageIds = /* @__PURE__ */ new Set();
|
|
5497
6391
|
/**
|
|
5498
6392
|
* "markDone for this row is TERMINALLY undeliverable" (Bug 4). Set ONLY when a
|
|
5499
6393
|
* re-adopted DONE row's `markDone` returned a terminal 4xx (a status that will
|
|
@@ -5616,7 +6510,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5616
6510
|
*/
|
|
5617
6511
|
attachmentsSkippedSignalled = /* @__PURE__ */ new Set();
|
|
5618
6512
|
/**
|
|
5619
|
-
* Cache of the opencode root directory
|
|
6513
|
+
* Cache of the opencode root directory from the selected client's location lookup.
|
|
6514
|
+
* Resolved lazily on
|
|
5620
6515
|
* first session creation so drain-created sessions are rooted at the project
|
|
5621
6516
|
* directory and thus visible in `opencode web`'s session list. `undefined` =
|
|
5622
6517
|
* not yet resolved; `null` = resolved-but-unavailable (don't keep retrying).
|
|
@@ -5710,6 +6605,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5710
6605
|
config.fetchImpl ?? fetch,
|
|
5711
6606
|
config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
|
|
5712
6607
|
);
|
|
6608
|
+
this.openCodeClient = config.openCodeClient ?? createOpenCodeClient({
|
|
6609
|
+
port: config.port,
|
|
6610
|
+
version: "v1",
|
|
6611
|
+
fetchImpl: config.fetchImpl
|
|
6612
|
+
});
|
|
5713
6613
|
this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5714
6614
|
this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
|
|
5715
6615
|
this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
|
|
@@ -5721,9 +6621,39 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5721
6621
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
5722
6622
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
5723
6623
|
}
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
6624
|
+
get isV2() {
|
|
6625
|
+
return this.openCodeClient.version === "v2";
|
|
6626
|
+
}
|
|
6627
|
+
async getSessionMessages(sessionId) {
|
|
6628
|
+
return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6629
|
+
}
|
|
6630
|
+
async getSubagentSessionMessages(sessionId) {
|
|
6631
|
+
return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : getSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6632
|
+
}
|
|
6633
|
+
async getTelemetrySubagentSessionMessages(sessionId) {
|
|
6634
|
+
if (this.isV2) return getV2SessionMessages(this.openCodeClient, sessionId);
|
|
6635
|
+
return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6636
|
+
}
|
|
6637
|
+
async listSessions() {
|
|
6638
|
+
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient);
|
|
6639
|
+
}
|
|
6640
|
+
async sessionExists(sessionId) {
|
|
6641
|
+
return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
|
|
6642
|
+
}
|
|
6643
|
+
async isSessionOngoing(sessionId) {
|
|
6644
|
+
return this.isV2 ? isV2SessionOngoing(this.openCodeClient, sessionId) : isSessionOngoing(this.port, sessionId, this.openCodeClient);
|
|
6645
|
+
}
|
|
6646
|
+
async getOpenCodeDirectory() {
|
|
6647
|
+
return this.isV2 ? getOpenCodeDirectoryV2(this.openCodeClient) : getOpenCodeDirectory(this.port, this.openCodeClient);
|
|
6648
|
+
}
|
|
6649
|
+
async createOpenCodeSession(directory) {
|
|
6650
|
+
return this.isV2 ? createV2Session(this.openCodeClient, directory) : createOpenCodeSession(this.port, directory, this.openCodeClient);
|
|
6651
|
+
}
|
|
6652
|
+
async hasAnyConfiguredProvider() {
|
|
6653
|
+
return hasAnyConfiguredProvider(this.port, this.openCodeClient);
|
|
6654
|
+
}
|
|
6655
|
+
async readOpenCodeSessionErrorStream(options) {
|
|
6656
|
+
return this.isV2 ? readV2SessionErrorStream(this.openCodeClient, options) : readSessionErrorStream(this.port, options, this.openCodeClient);
|
|
5727
6657
|
}
|
|
5728
6658
|
/**
|
|
5729
6659
|
* Drain all pending channel conversations once: poll → dispatch → register.
|
|
@@ -5801,6 +6731,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5801
6731
|
async runDrain() {
|
|
5802
6732
|
let dispatched = 0;
|
|
5803
6733
|
try {
|
|
6734
|
+
this.pendingMessageIds.clear();
|
|
5804
6735
|
const conversations = await this.getPendingConversations();
|
|
5805
6736
|
if (this.recycleRequestedFlag) {
|
|
5806
6737
|
this.stop();
|
|
@@ -6025,6 +6956,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6025
6956
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
6026
6957
|
this.ensureSessionErrorStream();
|
|
6027
6958
|
const messages = await this.getPendingMessages(conv.id);
|
|
6959
|
+
for (const message of messages) this.pendingMessageIds.add(message.id);
|
|
6028
6960
|
let dispatched = 0;
|
|
6029
6961
|
let skippedAlreadyDispatched = 0;
|
|
6030
6962
|
if (refusedSessionId && messages.length > 0) {
|
|
@@ -6038,6 +6970,27 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6038
6970
|
skippedAlreadyDispatched += 1;
|
|
6039
6971
|
continue;
|
|
6040
6972
|
}
|
|
6973
|
+
if (this.untrackableAck.has(message.id)) {
|
|
6974
|
+
const firstFenceWarning = !this.untrackableAckWarned.has(message.id);
|
|
6975
|
+
this.untrackableAckWarned.add(message.id);
|
|
6976
|
+
this.log({
|
|
6977
|
+
level: firstFenceWarning ? "warn" : "debug",
|
|
6978
|
+
message: `Message ${message.id.slice(0, 8)} is fenced after an untrackable OpenCode turn \u2014 skipping re-dispatch`,
|
|
6979
|
+
conversation_id: conv.id,
|
|
6980
|
+
message_id: message.id
|
|
6981
|
+
});
|
|
6982
|
+
const errorMessage3 = this.untrackableAck.get(message.id);
|
|
6983
|
+
const reportError = await this.reportUntrackableAck(
|
|
6984
|
+
conv.id,
|
|
6985
|
+
message.id,
|
|
6986
|
+
null,
|
|
6987
|
+
errorMessage3
|
|
6988
|
+
);
|
|
6989
|
+
if (reportError === null) {
|
|
6990
|
+
continue;
|
|
6991
|
+
}
|
|
6992
|
+
break;
|
|
6993
|
+
}
|
|
6041
6994
|
const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
|
|
6042
6995
|
if (effectiveOpencodeMessageId) {
|
|
6043
6996
|
const outcome = await this.resolveRedrive(
|
|
@@ -6066,15 +7019,60 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6066
7019
|
conversation_id: conv.id,
|
|
6067
7020
|
message_id: message.id
|
|
6068
7021
|
});
|
|
6069
|
-
const sendAttachments = this.buildSendAttachments(conv, message);
|
|
6070
|
-
|
|
7022
|
+
const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(conv, message);
|
|
7023
|
+
if (this.isV2 && message.attachments && message.attachments.length > 0) {
|
|
7024
|
+
this.signalAttachmentsSkipped(
|
|
7025
|
+
conv.id,
|
|
7026
|
+
message.id,
|
|
7027
|
+
message.attachments.map((attachment, index) => ({
|
|
7028
|
+
index,
|
|
7029
|
+
mime: attachment.mime,
|
|
7030
|
+
...attachment.filename ? { filename: attachment.filename } : {},
|
|
7031
|
+
status: "skipped"
|
|
7032
|
+
})),
|
|
7033
|
+
false
|
|
7034
|
+
);
|
|
7035
|
+
}
|
|
7036
|
+
opencodeMessageId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, message.content) : await this.dispatchLocked(
|
|
6071
7037
|
sessionId,
|
|
6072
|
-
() => sendPromptAsync(
|
|
7038
|
+
() => sendPromptAsync(
|
|
7039
|
+
this.port,
|
|
7040
|
+
sessionId,
|
|
7041
|
+
message.content,
|
|
7042
|
+
options,
|
|
7043
|
+
sendAttachments,
|
|
7044
|
+
this.openCodeClient
|
|
7045
|
+
)
|
|
6073
7046
|
);
|
|
6074
7047
|
} catch (err) {
|
|
6075
7048
|
if (err instanceof ChannelAuthError) throw err;
|
|
7049
|
+
if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
|
|
7050
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
7051
|
+
this.untrackableAck.set(message.id, errorMessage4);
|
|
7052
|
+
this.log({
|
|
7053
|
+
level: "error",
|
|
7054
|
+
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
|
|
7055
|
+
conversation_id: conv.id,
|
|
7056
|
+
message_id: message.id
|
|
7057
|
+
});
|
|
7058
|
+
const markError = await this.reportUntrackableAck(
|
|
7059
|
+
conv.id,
|
|
7060
|
+
message.id,
|
|
7061
|
+
null,
|
|
7062
|
+
errorMessage4
|
|
7063
|
+
);
|
|
7064
|
+
if (markError !== null) {
|
|
7065
|
+
this.log({
|
|
7066
|
+
level: "warn",
|
|
7067
|
+
message: `markFailed PATCH for V2 dispatch failure on message ${message.id.slice(0, 8)} failed: ${markError.message}`,
|
|
7068
|
+
conversation_id: conv.id,
|
|
7069
|
+
message_id: message.id
|
|
7070
|
+
});
|
|
7071
|
+
}
|
|
7072
|
+
break;
|
|
7073
|
+
}
|
|
6076
7074
|
this.dispatched.delete(message.id);
|
|
6077
|
-
const exists = await sessionExists(
|
|
7075
|
+
const exists = await this.sessionExists(sessionId);
|
|
6078
7076
|
if (exists === false) {
|
|
6079
7077
|
this.sessions.delete(conv.id);
|
|
6080
7078
|
this.log({
|
|
@@ -6123,6 +7121,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6123
7121
|
break;
|
|
6124
7122
|
}
|
|
6125
7123
|
if (opencodeMessageId === null) {
|
|
7124
|
+
if (this.isV2) {
|
|
7125
|
+
throw new Error("V2 prompt dispatch completed without an acknowledged message id");
|
|
7126
|
+
}
|
|
6126
7127
|
const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
|
|
6127
7128
|
if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
6128
7129
|
this.log({
|
|
@@ -6270,29 +7271,38 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6270
7271
|
*/
|
|
6271
7272
|
async pollSessionMessagesForRedrive(conv, message, sessionId) {
|
|
6272
7273
|
try {
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
7274
|
+
if (this.isV2) {
|
|
7275
|
+
const messages = await this.getSessionMessages(sessionId);
|
|
7276
|
+
if (messages === null) {
|
|
7277
|
+
this.log({
|
|
7278
|
+
level: "warn",
|
|
7279
|
+
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`,
|
|
7280
|
+
conversation_id: conv.id,
|
|
7281
|
+
message_id: message.id
|
|
7282
|
+
});
|
|
7283
|
+
return { ok: false, signature: null };
|
|
7284
|
+
}
|
|
7285
|
+
return { ok: true, messages };
|
|
6284
7286
|
}
|
|
6285
|
-
const
|
|
6286
|
-
|
|
7287
|
+
const polledV1 = await pollSessionMessagesForRedrive(
|
|
7288
|
+
this.port,
|
|
7289
|
+
sessionId,
|
|
7290
|
+
this.openCodeClient
|
|
7291
|
+
);
|
|
7292
|
+
if (!polledV1.ok) {
|
|
7293
|
+
const normalized = normalizeRedrivePollFailureBody(polledV1.body);
|
|
6287
7294
|
this.log({
|
|
6288
7295
|
level: "warn",
|
|
6289
|
-
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`,
|
|
7296
|
+
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`,
|
|
6290
7297
|
conversation_id: conv.id,
|
|
6291
7298
|
message_id: message.id
|
|
6292
7299
|
});
|
|
6293
|
-
return {
|
|
7300
|
+
return {
|
|
7301
|
+
ok: false,
|
|
7302
|
+
signature: polledV1.status === null && !polledV1.malformed ? null : polledV1.malformed ? "non-array message body" : `HTTP ${polledV1.status}${normalized ? `: ${normalized}` : ""}`
|
|
7303
|
+
};
|
|
6294
7304
|
}
|
|
6295
|
-
return { ok: true, messages:
|
|
7305
|
+
return { ok: true, messages: polledV1.messages };
|
|
6296
7306
|
} catch (err) {
|
|
6297
7307
|
this.log({
|
|
6298
7308
|
level: "warn",
|
|
@@ -6351,11 +7361,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6351
7361
|
}
|
|
6352
7362
|
const state = messageRunState(messages, ocId ?? "");
|
|
6353
7363
|
if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
|
|
6354
|
-
const ongoing = await isSessionOngoing(
|
|
7364
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
6355
7365
|
if (ongoing === false) {
|
|
6356
7366
|
this.log({
|
|
6357
7367
|
level: "info",
|
|
6358
|
-
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
|
|
7368
|
+
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`,
|
|
6359
7369
|
conversation_id: conv.id,
|
|
6360
7370
|
message_id: message.id
|
|
6361
7371
|
});
|
|
@@ -6368,7 +7378,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6368
7378
|
return this.settleRedrive(conv, sessionId, message, ocId, messages, state);
|
|
6369
7379
|
}
|
|
6370
7380
|
if (state === "running" || state === "queued") {
|
|
6371
|
-
const ongoing = await isSessionOngoing(
|
|
7381
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
6372
7382
|
if (ongoing === true) {
|
|
6373
7383
|
if (state === "queued") {
|
|
6374
7384
|
const siblingOcIds = this.siblingOpencodeMessageIds(
|
|
@@ -6828,7 +7838,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6828
7838
|
};
|
|
6829
7839
|
}
|
|
6830
7840
|
if (bound) {
|
|
6831
|
-
const exists = await sessionExists(
|
|
7841
|
+
const exists = await this.sessionExists(bound);
|
|
6832
7842
|
if (exists === false) {
|
|
6833
7843
|
this.log({
|
|
6834
7844
|
level: "debug",
|
|
@@ -6861,7 +7871,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6861
7871
|
*/
|
|
6862
7872
|
async createAndBindSession(conversationId) {
|
|
6863
7873
|
const directory = await this.resolveOpenCodeDirectory();
|
|
6864
|
-
const sessionId = await createOpenCodeSession(
|
|
7874
|
+
const sessionId = await this.createOpenCodeSession(directory);
|
|
6865
7875
|
this.sessions.set(conversationId, sessionId);
|
|
6866
7876
|
await this.persistSession(conversationId, sessionId).catch((err) => {
|
|
6867
7877
|
this.log({
|
|
@@ -6873,17 +7883,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6873
7883
|
return sessionId;
|
|
6874
7884
|
}
|
|
6875
7885
|
/**
|
|
6876
|
-
* Lazily resolve (and cache) opencode's root directory via
|
|
7886
|
+
* Lazily resolve (and cache) opencode's root directory via the selected client's
|
|
7887
|
+
* location lookup.
|
|
6877
7888
|
* Resolved once per driver: `undefined` until first lookup, then the directory
|
|
6878
|
-
* string or `null` if unavailable (we don't keep retrying a
|
|
7889
|
+
* string or `null` if unavailable (we don't keep retrying a failed lookup).
|
|
6879
7890
|
*/
|
|
6880
7891
|
async resolveOpenCodeDirectory() {
|
|
6881
7892
|
if (this.opencodeDirectory !== void 0) return this.opencodeDirectory;
|
|
6882
|
-
this.opencodeDirectory = await getOpenCodeDirectory(
|
|
7893
|
+
this.opencodeDirectory = await this.getOpenCodeDirectory();
|
|
6883
7894
|
if (!this.opencodeDirectory) {
|
|
6884
7895
|
this.log({
|
|
6885
7896
|
level: "warn",
|
|
6886
|
-
message: "Could not determine opencode directory (
|
|
7897
|
+
message: "Could not determine opencode directory (location lookup failed) \u2014 new sessions may not appear in opencode web"
|
|
6887
7898
|
});
|
|
6888
7899
|
}
|
|
6889
7900
|
return this.opencodeDirectory;
|
|
@@ -7317,7 +8328,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7317
8328
|
while (!this.stopped && !signal.aborted) {
|
|
7318
8329
|
const openedAt = this.now();
|
|
7319
8330
|
try {
|
|
7320
|
-
const outcome = await
|
|
8331
|
+
const outcome = await this.readOpenCodeSessionErrorStream({
|
|
7321
8332
|
signal,
|
|
7322
8333
|
onSessionError: (event) => this.handleSessionError(event)
|
|
7323
8334
|
});
|
|
@@ -7408,7 +8419,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7408
8419
|
}
|
|
7409
8420
|
async failFromSessionError(watcher, event, inFlight) {
|
|
7410
8421
|
try {
|
|
7411
|
-
const messages = await getSessionMessages(
|
|
8422
|
+
const messages = await this.getSessionMessages(event.sessionId);
|
|
7412
8423
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7413
8424
|
if (state !== "queued") {
|
|
7414
8425
|
this.log({
|
|
@@ -7460,7 +8471,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7460
8471
|
* markDone (done) exactly once per transition;
|
|
7461
8472
|
* 2. applies the idle-path re-dispatch guard (a dispatched message that never
|
|
7462
8473
|
* APPEARS → re-dispatch — D1 obligation 2);
|
|
7463
|
-
* 3. polls `/question` + `/permission
|
|
8474
|
+
* 3. polls V1's global `/question` + `/permission`, or V2's
|
|
8475
|
+
* `/api/session/:id/form` + `/api/session/:id/permission`, and surfaces
|
|
7464
8476
|
* NEW ones via `reportInteraction`, carrying the PAUSED message's own
|
|
7465
8477
|
* `source_message_id`;
|
|
7466
8478
|
* 4. drops messages that completed or timed out from the in-flight set.
|
|
@@ -7486,11 +8498,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7486
8498
|
if (watcher.generation !== generation) return;
|
|
7487
8499
|
let messages = null;
|
|
7488
8500
|
try {
|
|
7489
|
-
|
|
7490
|
-
if (res.ok) {
|
|
7491
|
-
const body = await res.json();
|
|
7492
|
-
messages = Array.isArray(body) ? body : null;
|
|
7493
|
-
}
|
|
8501
|
+
messages = await this.getSessionMessages(sessionId);
|
|
7494
8502
|
} catch {
|
|
7495
8503
|
}
|
|
7496
8504
|
if (messages != null && messages.length > 0) {
|
|
@@ -7723,7 +8731,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7723
8731
|
inFlight.b2LastDescendantCheckMs = this.now();
|
|
7724
8732
|
const [descendantOngoing, rootOngoing] = await Promise.all([
|
|
7725
8733
|
this.isAnyDescendantSessionOngoing(sessionId),
|
|
7726
|
-
isSessionOngoing(
|
|
8734
|
+
this.isSessionOngoing(sessionId)
|
|
7727
8735
|
]);
|
|
7728
8736
|
if (isB2AbandonmentConfirmed({
|
|
7729
8737
|
pinnedForMs,
|
|
@@ -7783,7 +8791,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7783
8791
|
});
|
|
7784
8792
|
}
|
|
7785
8793
|
const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;
|
|
7786
|
-
const ongoing = await isSessionOngoing(
|
|
8794
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
7787
8795
|
if (isAmbiguousFinishResolved({
|
|
7788
8796
|
pinnedForMs,
|
|
7789
8797
|
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
@@ -7966,7 +8974,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7966
8974
|
*/
|
|
7967
8975
|
async readoptProcessing() {
|
|
7968
8976
|
const rows = await this.getProcessingMessages();
|
|
7969
|
-
if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
|
|
8977
|
+
if (this.dontRedispatch.size > 0 || this.untrackableAck.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
|
|
7970
8978
|
const stillProcessing = new Set(rows.map((r) => r.id));
|
|
7971
8979
|
for (const id of [
|
|
7972
8980
|
...this.dontRedispatch,
|
|
@@ -7986,6 +8994,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7986
8994
|
}
|
|
7987
8995
|
}
|
|
7988
8996
|
}
|
|
8997
|
+
for (const id of [...this.untrackableAck.keys()]) {
|
|
8998
|
+
if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
|
|
8999
|
+
this.untrackableAck.delete(id);
|
|
9000
|
+
this.untrackableAckSignalled.delete(id);
|
|
9001
|
+
this.untrackableAckWarned.delete(id);
|
|
9002
|
+
this.log({
|
|
9003
|
+
level: "debug",
|
|
9004
|
+
message: `Re-adopt: message ${id.slice(0, 8)} left processing and pending \u2014 cleared untrackable-ack fence`,
|
|
9005
|
+
message_id: id
|
|
9006
|
+
});
|
|
9007
|
+
}
|
|
9008
|
+
}
|
|
7989
9009
|
}
|
|
7990
9010
|
if (rows.length === 0) return;
|
|
7991
9011
|
const bySession = /* @__PURE__ */ new Map();
|
|
@@ -8006,23 +9026,32 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8006
9026
|
for (const [sessionId, sessionRows] of bySession) {
|
|
8007
9027
|
let messages;
|
|
8008
9028
|
try {
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
9029
|
+
if (this.isV2) {
|
|
9030
|
+
const snapshot = await this.getSessionMessages(sessionId);
|
|
9031
|
+
if (snapshot === null) {
|
|
9032
|
+
this.log({
|
|
9033
|
+
level: "warn",
|
|
9034
|
+
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
|
|
9035
|
+
});
|
|
9036
|
+
continue;
|
|
9037
|
+
}
|
|
9038
|
+
messages = snapshot;
|
|
9039
|
+
} else {
|
|
9040
|
+
const polled = await pollSessionMessagesForRedrive(
|
|
9041
|
+
this.port,
|
|
9042
|
+
sessionId,
|
|
9043
|
+
this.openCodeClient
|
|
9044
|
+
);
|
|
9045
|
+
if (!polled.ok) {
|
|
9046
|
+
const normalized = normalizeRedrivePollFailureBody(polled.body);
|
|
9047
|
+
this.log({
|
|
9048
|
+
level: "warn",
|
|
9049
|
+
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`
|
|
9050
|
+
});
|
|
9051
|
+
continue;
|
|
9052
|
+
}
|
|
9053
|
+
messages = polled.messages;
|
|
8024
9054
|
}
|
|
8025
|
-
messages = body;
|
|
8026
9055
|
} catch (err) {
|
|
8027
9056
|
this.log({
|
|
8028
9057
|
level: "warn",
|
|
@@ -8031,7 +9060,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8031
9060
|
continue;
|
|
8032
9061
|
}
|
|
8033
9062
|
const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
|
|
8034
|
-
const sessionOngoing = anyUntracked ? await isSessionOngoing(
|
|
9063
|
+
const sessionOngoing = anyUntracked ? await this.isSessionOngoing(sessionId) : null;
|
|
8035
9064
|
for (const row of sessionRows) {
|
|
8036
9065
|
await this.readoptOne(sessionId, row, messages, sessionOngoing);
|
|
8037
9066
|
}
|
|
@@ -8078,7 +9107,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8078
9107
|
if (restartAborted) {
|
|
8079
9108
|
this.log({
|
|
8080
9109
|
level: "info",
|
|
8081
|
-
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
|
|
9110
|
+
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`,
|
|
8082
9111
|
conversation_id: row.conversation_id,
|
|
8083
9112
|
message_id: row.id
|
|
8084
9113
|
});
|
|
@@ -8133,10 +9162,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8133
9162
|
}
|
|
8134
9163
|
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
8135
9164
|
this.dontRedispatch.delete(row.id);
|
|
9165
|
+
this.untrackableAck.delete(row.id);
|
|
9166
|
+
this.untrackableAckSignalled.delete(row.id);
|
|
9167
|
+
this.untrackableAckWarned.delete(row.id);
|
|
8136
9168
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
8137
9169
|
return;
|
|
8138
9170
|
}
|
|
8139
|
-
if (this.dontRedispatch.has(row.id)) {
|
|
9171
|
+
if (this.dontRedispatch.has(row.id) || this.untrackableAck.has(row.id)) {
|
|
8140
9172
|
this.log({
|
|
8141
9173
|
level: "debug",
|
|
8142
9174
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
@@ -8156,7 +9188,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8156
9188
|
const finish = reply?.info?.finish ?? reply?.finish;
|
|
8157
9189
|
this.log({
|
|
8158
9190
|
level: "info",
|
|
8159
|
-
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
|
|
9191
|
+
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`,
|
|
8160
9192
|
conversation_id: row.conversation_id,
|
|
8161
9193
|
message_id: row.id
|
|
8162
9194
|
});
|
|
@@ -8165,7 +9197,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8165
9197
|
}
|
|
8166
9198
|
this.log({
|
|
8167
9199
|
level: "info",
|
|
8168
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per
|
|
9200
|
+
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)`,
|
|
8169
9201
|
conversation_id: row.conversation_id,
|
|
8170
9202
|
message_id: row.id
|
|
8171
9203
|
});
|
|
@@ -8175,7 +9207,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8175
9207
|
if (ongoing === true) {
|
|
8176
9208
|
this.log({
|
|
8177
9209
|
level: "debug",
|
|
8178
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per
|
|
9210
|
+
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)`,
|
|
8179
9211
|
conversation_id: row.conversation_id,
|
|
8180
9212
|
message_id: row.id
|
|
8181
9213
|
});
|
|
@@ -8183,7 +9215,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8183
9215
|
if (shape === "b1") {
|
|
8184
9216
|
this.log({
|
|
8185
9217
|
level: "debug",
|
|
8186
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but
|
|
9218
|
+
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`,
|
|
8187
9219
|
conversation_id: row.conversation_id,
|
|
8188
9220
|
message_id: row.id
|
|
8189
9221
|
});
|
|
@@ -8195,7 +9227,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8195
9227
|
}
|
|
8196
9228
|
this.log({
|
|
8197
9229
|
level: "debug",
|
|
8198
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but
|
|
9230
|
+
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`,
|
|
8199
9231
|
conversation_id: row.conversation_id,
|
|
8200
9232
|
message_id: row.id
|
|
8201
9233
|
});
|
|
@@ -8245,7 +9277,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8245
9277
|
* extracted (#1493 Task 2.4) so the ambiguous-finish guard above can call the
|
|
8246
9278
|
* SAME delivery instead of duplicating it.
|
|
8247
9279
|
*
|
|
8248
|
-
* EVEN IF the row was previously parked
|
|
9280
|
+
* EVEN IF the row was previously parked by either recovery fence (a give-up stops
|
|
8249
9281
|
* re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY like the watcher's
|
|
8250
9282
|
* `settleMessageDone`: auth re-throws; terminal → park in `doneUndeliverable` +
|
|
8251
9283
|
* leave for cron; transient → log + leave for the next drain (the still-
|
|
@@ -8312,6 +9344,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8312
9344
|
await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
|
|
8313
9345
|
}
|
|
8314
9346
|
this.dontRedispatch.delete(row.id);
|
|
9347
|
+
this.untrackableAck.delete(row.id);
|
|
9348
|
+
this.untrackableAckSignalled.delete(row.id);
|
|
9349
|
+
this.untrackableAckWarned.delete(row.id);
|
|
8315
9350
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
8316
9351
|
}
|
|
8317
9352
|
/**
|
|
@@ -8375,19 +9410,103 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8375
9410
|
conversation_id: row.conversation_id,
|
|
8376
9411
|
message_id: row.id
|
|
8377
9412
|
});
|
|
8378
|
-
this.awaitingReadopt.add(row.id);
|
|
9413
|
+
if (!this.isV2) this.awaitingReadopt.add(row.id);
|
|
8379
9414
|
const readoptConv = this.convForRow(sessionId, row);
|
|
8380
9415
|
const readoptMessage = this.queuedMessageForRow(row);
|
|
8381
|
-
const sendAttachments = this.buildSendAttachments(readoptConv, readoptMessage);
|
|
9416
|
+
const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(readoptConv, readoptMessage);
|
|
9417
|
+
if (this.isV2 && row.attachments && row.attachments.length > 0) {
|
|
9418
|
+
this.signalAttachmentsSkipped(
|
|
9419
|
+
row.conversation_id,
|
|
9420
|
+
row.id,
|
|
9421
|
+
row.attachments.map((attachment, index) => ({
|
|
9422
|
+
index,
|
|
9423
|
+
mime: attachment.mime,
|
|
9424
|
+
...attachment.filename ? { filename: attachment.filename } : {},
|
|
9425
|
+
status: "skipped"
|
|
9426
|
+
})),
|
|
9427
|
+
false
|
|
9428
|
+
);
|
|
9429
|
+
}
|
|
8382
9430
|
let ocId;
|
|
8383
9431
|
try {
|
|
8384
|
-
ocId = await this.dispatchLocked(
|
|
9432
|
+
ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
|
|
8385
9433
|
sessionId,
|
|
8386
|
-
() => sendPromptAsync(
|
|
9434
|
+
() => sendPromptAsync(
|
|
9435
|
+
this.port,
|
|
9436
|
+
sessionId,
|
|
9437
|
+
row.content,
|
|
9438
|
+
options,
|
|
9439
|
+
sendAttachments,
|
|
9440
|
+
this.openCodeClient
|
|
9441
|
+
)
|
|
8387
9442
|
);
|
|
8388
9443
|
} catch (err) {
|
|
8389
9444
|
this.awaitingReadopt.delete(row.id);
|
|
8390
9445
|
if (err instanceof ChannelAuthError) throw err;
|
|
9446
|
+
if (this.isV2) {
|
|
9447
|
+
const errorMessage3 = err instanceof Error ? err.message : String(err);
|
|
9448
|
+
const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
|
|
9449
|
+
if (!invalidPromptAck) {
|
|
9450
|
+
const exists = await this.sessionExists(sessionId);
|
|
9451
|
+
if (exists === false) {
|
|
9452
|
+
this.log({
|
|
9453
|
+
level: "warn",
|
|
9454
|
+
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}`,
|
|
9455
|
+
conversation_id: row.conversation_id,
|
|
9456
|
+
message_id: row.id
|
|
9457
|
+
});
|
|
9458
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
9459
|
+
return;
|
|
9460
|
+
}
|
|
9461
|
+
if (exists === null) {
|
|
9462
|
+
this.log({
|
|
9463
|
+
level: "warn",
|
|
9464
|
+
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}`,
|
|
9465
|
+
conversation_id: row.conversation_id,
|
|
9466
|
+
message_id: row.id
|
|
9467
|
+
});
|
|
9468
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
9469
|
+
return;
|
|
9470
|
+
}
|
|
9471
|
+
} else {
|
|
9472
|
+
this.untrackableAck.set(row.id, errorMessage3);
|
|
9473
|
+
}
|
|
9474
|
+
this.log({
|
|
9475
|
+
level: "error",
|
|
9476
|
+
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage3}`,
|
|
9477
|
+
conversation_id: row.conversation_id,
|
|
9478
|
+
message_id: row.id
|
|
9479
|
+
});
|
|
9480
|
+
if (invalidPromptAck) {
|
|
9481
|
+
const markError = await this.reportUntrackableAck(
|
|
9482
|
+
row.conversation_id,
|
|
9483
|
+
row.id,
|
|
9484
|
+
sessionId,
|
|
9485
|
+
errorMessage3
|
|
9486
|
+
);
|
|
9487
|
+
if (markError !== null) {
|
|
9488
|
+
this.log({
|
|
9489
|
+
level: "warn",
|
|
9490
|
+
message: `markFailed PATCH for V2 re-adopt dispatch failure on message ${row.id.slice(0, 8)} failed: ${markError.message}`,
|
|
9491
|
+
conversation_id: row.conversation_id,
|
|
9492
|
+
message_id: row.id
|
|
9493
|
+
});
|
|
9494
|
+
}
|
|
9495
|
+
} else {
|
|
9496
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
|
|
9497
|
+
(markErr) => {
|
|
9498
|
+
this.log({
|
|
9499
|
+
level: "warn",
|
|
9500
|
+
message: `markFailed PATCH for V2 re-adopt dispatch failure on message ${row.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
9501
|
+
conversation_id: row.conversation_id,
|
|
9502
|
+
message_id: row.id
|
|
9503
|
+
});
|
|
9504
|
+
this.signalDispatchNotStarted(readoptConv, readoptMessage, "failure_unreported");
|
|
9505
|
+
}
|
|
9506
|
+
);
|
|
9507
|
+
}
|
|
9508
|
+
return;
|
|
9509
|
+
}
|
|
8391
9510
|
this.log({
|
|
8392
9511
|
level: "warn",
|
|
8393
9512
|
message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -8519,12 +9638,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8519
9638
|
this.dispatched.delete(evidentMessageId);
|
|
8520
9639
|
}
|
|
8521
9640
|
/**
|
|
8522
|
-
* Poll `/question` + `/permission
|
|
8523
|
-
* via `reportInteraction` (Task 3.5),
|
|
8524
|
-
* `source_message_id` so the server @mentions
|
|
8525
|
-
* concurrency. Dedups by interaction id across ticks
|
|
9641
|
+
* Poll V1's global `/question` + `/permission`, or V2's watched-session form and
|
|
9642
|
+
* permission routes, and surface NEW ones via `reportInteraction` (Task 3.5),
|
|
9643
|
+
* carrying the PAUSED message's own `source_message_id` so the server @mentions
|
|
9644
|
+
* the correct person under concurrency. Dedups by interaction id across ticks
|
|
9645
|
+
* (reused per-session sets).
|
|
8526
9646
|
*
|
|
8527
|
-
* The interaction is attributed to the in-flight message it paused on.
|
|
9647
|
+
* The interaction is attributed to the in-flight message it paused on. OpenCode
|
|
8528
9648
|
* stamps a `messageID` on a permission (and `tool.messageID` on a question) =
|
|
8529
9649
|
* the assistant message id, whose `parentID` is the user message id — but the
|
|
8530
9650
|
* simplest robust attribution here is: the single in-flight message that is
|
|
@@ -8547,16 +9667,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8547
9667
|
let permissionsPolledOk = true;
|
|
8548
9668
|
let questions = [];
|
|
8549
9669
|
try {
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
questions = body;
|
|
8555
|
-
} else {
|
|
8556
|
-
questionsPolledOk = false;
|
|
8557
|
-
}
|
|
9670
|
+
if (this.isV2) {
|
|
9671
|
+
const forms = await listV2Forms(this.openCodeClient, sessionId);
|
|
9672
|
+
if (forms === null) questionsPolledOk = false;
|
|
9673
|
+
else questions = forms;
|
|
8558
9674
|
} else {
|
|
8559
|
-
|
|
9675
|
+
const listed = await listOpenCodeQuestions(this.port, this.openCodeClient);
|
|
9676
|
+
if (listed === null) questionsPolledOk = false;
|
|
9677
|
+
else questions = listed;
|
|
8560
9678
|
}
|
|
8561
9679
|
} catch {
|
|
8562
9680
|
questionsPolledOk = false;
|
|
@@ -8576,16 +9694,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8576
9694
|
}
|
|
8577
9695
|
let permissions = [];
|
|
8578
9696
|
try {
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
permissions = body;
|
|
8584
|
-
} else {
|
|
8585
|
-
permissionsPolledOk = false;
|
|
8586
|
-
}
|
|
9697
|
+
if (this.isV2) {
|
|
9698
|
+
const listed = await listV2Permissions(this.openCodeClient, sessionId);
|
|
9699
|
+
if (listed === null) permissionsPolledOk = false;
|
|
9700
|
+
else permissions = listed;
|
|
8587
9701
|
} else {
|
|
8588
|
-
|
|
9702
|
+
const listed = await listOpenCodePermissions(this.port, this.openCodeClient);
|
|
9703
|
+
if (listed === null) permissionsPolledOk = false;
|
|
9704
|
+
else permissions = listed;
|
|
8589
9705
|
}
|
|
8590
9706
|
} catch {
|
|
8591
9707
|
permissionsPolledOk = false;
|
|
@@ -8679,10 +9795,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8679
9795
|
if (cached !== void 0) return cached;
|
|
8680
9796
|
let parent = void 0;
|
|
8681
9797
|
try {
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
9798
|
+
if (this.isV2) {
|
|
9799
|
+
const session = await getV2Session(this.openCodeClient, sessionId);
|
|
9800
|
+
parent = null;
|
|
9801
|
+
const candidate = session.parentID;
|
|
9802
|
+
if (typeof candidate === "string") parent = candidate;
|
|
9803
|
+
} else {
|
|
9804
|
+
const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
|
|
9805
|
+
parent = typeof body?.parentID === "string" ? body.parentID : body === null ? void 0 : null;
|
|
8686
9806
|
}
|
|
8687
9807
|
} catch {
|
|
8688
9808
|
parent = void 0;
|
|
@@ -8736,18 +9856,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8736
9856
|
if (cached) return cached;
|
|
8737
9857
|
const pending = (async () => {
|
|
8738
9858
|
try {
|
|
8739
|
-
const
|
|
8740
|
-
if (
|
|
9859
|
+
const messages2 = await this.getTelemetrySubagentSessionMessages(sessionId);
|
|
9860
|
+
if (messages2 === null) {
|
|
8741
9861
|
this.log({
|
|
8742
9862
|
level: "warn",
|
|
8743
|
-
message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)}
|
|
9863
|
+
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`,
|
|
8744
9864
|
message_id: messageId
|
|
8745
9865
|
});
|
|
8746
9866
|
return null;
|
|
8747
9867
|
}
|
|
8748
|
-
|
|
8749
|
-
if (!Array.isArray(body)) throw new Error("response body was not a message array");
|
|
8750
|
-
return body;
|
|
9868
|
+
return messages2;
|
|
8751
9869
|
} catch (err) {
|
|
8752
9870
|
this.log({
|
|
8753
9871
|
level: "warn",
|
|
@@ -8888,21 +10006,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8888
10006
|
const cached = this.sessionTitles.get(sessionId);
|
|
8889
10007
|
if (cached != null) return cached;
|
|
8890
10008
|
try {
|
|
8891
|
-
|
|
8892
|
-
if (
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
return title;
|
|
8898
|
-
}
|
|
8899
|
-
return null;
|
|
10009
|
+
let title = "";
|
|
10010
|
+
if (this.isV2) {
|
|
10011
|
+
title = (await getV2Session(this.openCodeClient, sessionId)).title?.trim() ?? "";
|
|
10012
|
+
} else {
|
|
10013
|
+
const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
|
|
10014
|
+
title = typeof body?.title === "string" ? body.title.trim() : "";
|
|
8900
10015
|
}
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
10016
|
+
if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
|
|
10017
|
+
this.sessionTitles.set(sessionId, title);
|
|
10018
|
+
return title;
|
|
10019
|
+
}
|
|
10020
|
+
return null;
|
|
8906
10021
|
} catch (err) {
|
|
8907
10022
|
this.log({
|
|
8908
10023
|
level: "debug",
|
|
@@ -9000,7 +10115,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9000
10115
|
* `SessionStatus` only.
|
|
9001
10116
|
*/
|
|
9002
10117
|
async isAnyDescendantSessionAlive(rootSessionId) {
|
|
9003
|
-
const sessions = await listSessions(
|
|
10118
|
+
const sessions = await this.listSessions();
|
|
9004
10119
|
if (!sessions) {
|
|
9005
10120
|
this.log({
|
|
9006
10121
|
level: "warn",
|
|
@@ -9011,7 +10126,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9011
10126
|
for (const candidate of sessions) {
|
|
9012
10127
|
if (!candidate?.id || candidate.id === rootSessionId) continue;
|
|
9013
10128
|
if (!await this.sessionBelongsTo(candidate.id, rootSessionId)) continue;
|
|
9014
|
-
const childMsgs = await
|
|
10129
|
+
const childMsgs = await this.getSubagentSessionMessages(candidate.id);
|
|
9015
10130
|
if (isSessionActivelyGenerating(childMsgs)) {
|
|
9016
10131
|
return true;
|
|
9017
10132
|
}
|
|
@@ -9073,7 +10188,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9073
10188
|
* `isB2AbandonmentConfirmed`.
|
|
9074
10189
|
*/
|
|
9075
10190
|
async isAnyDescendantSessionOngoing(rootSessionId) {
|
|
9076
|
-
const sessions = await listSessions(
|
|
10191
|
+
const sessions = await this.listSessions();
|
|
9077
10192
|
if (!sessions) {
|
|
9078
10193
|
this.log({
|
|
9079
10194
|
level: "warn",
|
|
@@ -9090,7 +10205,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9090
10205
|
continue;
|
|
9091
10206
|
}
|
|
9092
10207
|
if (membership === false) continue;
|
|
9093
|
-
const ongoing = await isSessionOngoing(
|
|
10208
|
+
const ongoing = await this.isSessionOngoing(candidate.id);
|
|
9094
10209
|
if (ongoing === true) return true;
|
|
9095
10210
|
if (ongoing === null) indeterminate = true;
|
|
9096
10211
|
}
|
|
@@ -9366,6 +10481,26 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9366
10481
|
this.clearSubagentInvocationCaches(messageId);
|
|
9367
10482
|
throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
|
|
9368
10483
|
}
|
|
10484
|
+
/**
|
|
10485
|
+
* Attempt the terminal report for an accepted prompt whose OpenCode id is not
|
|
10486
|
+
* trackable. Callers use the returned error to keep their path-specific logging
|
|
10487
|
+
* while the same report payload can be retried on a later drain.
|
|
10488
|
+
*/
|
|
10489
|
+
async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage3) {
|
|
10490
|
+
try {
|
|
10491
|
+
await this.markFailed(conversationId, messageId, sessionId, errorMessage3);
|
|
10492
|
+
return null;
|
|
10493
|
+
} catch (err) {
|
|
10494
|
+
this.signalUntrackableAck(conversationId, messageId);
|
|
10495
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
10496
|
+
}
|
|
10497
|
+
}
|
|
10498
|
+
/** Emit the untrackable-ack signal at most once while the row remains fenced. */
|
|
10499
|
+
signalUntrackableAck(conversationId, messageId) {
|
|
10500
|
+
if (this.untrackableAckSignalled.has(messageId)) return;
|
|
10501
|
+
this.untrackableAckSignalled.add(messageId);
|
|
10502
|
+
void this.postSignal(conversationId, messageId, "ack_untrackable");
|
|
10503
|
+
}
|
|
9369
10504
|
/**
|
|
9370
10505
|
* Mark a message `failed`. `sessionId` / `error` are threaded to the API ONLY
|
|
9371
10506
|
* when provided (issue #182). Three states for `sessionId`:
|
|
@@ -9430,7 +10565,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9430
10565
|
const classified = messageFailure(messages, userMessageId);
|
|
9431
10566
|
if (classified != null) return classified;
|
|
9432
10567
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
9433
|
-
const hasProvider = await hasAnyConfiguredProvider(
|
|
10568
|
+
const hasProvider = await this.hasAnyConfiguredProvider();
|
|
9434
10569
|
return applyZeroProviderFallback(
|
|
9435
10570
|
classified,
|
|
9436
10571
|
hasProvider,
|
|
@@ -9503,7 +10638,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9503
10638
|
const succeededProviders = /* @__PURE__ */ new Set();
|
|
9504
10639
|
for (const ref of refs) {
|
|
9505
10640
|
try {
|
|
9506
|
-
const childMessages = await
|
|
10641
|
+
const childMessages = await this.getSubagentSessionMessages(ref.sessionId);
|
|
9507
10642
|
if (childMessages === null) {
|
|
9508
10643
|
this.log({
|
|
9509
10644
|
level: "debug",
|
|
@@ -9834,6 +10969,7 @@ Port ${port} is already in use.`));
|
|
|
9834
10969
|
|
|
9835
10970
|
// src/commands/ensure-opencode-v2.ts
|
|
9836
10971
|
import chalk6 from "chalk";
|
|
10972
|
+
import ora3 from "ora";
|
|
9837
10973
|
import { select as select3 } from "@inquirer/prompts";
|
|
9838
10974
|
async function probeOpenCode2WithoutPassword(port) {
|
|
9839
10975
|
try {
|
|
@@ -9859,11 +10995,7 @@ function unknownPasswordError(port) {
|
|
|
9859
10995
|
`OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
|
|
9860
10996
|
);
|
|
9861
10997
|
}
|
|
9862
|
-
|
|
9863
|
-
return new Error(
|
|
9864
|
-
"OpenCode V2 (opencode2) session support is incomplete \u2014 see https://github.com/sroze/evident/issues/2075. Not starting a new opencode2 process."
|
|
9865
|
-
);
|
|
9866
|
-
}
|
|
10998
|
+
var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
|
|
9867
10999
|
async function ensureOpenCode2Running(ctx) {
|
|
9868
11000
|
const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
|
|
9869
11001
|
if (initialHealth.authFailed) {
|
|
@@ -9906,13 +11038,37 @@ Port ${port} is already in use.`));
|
|
|
9906
11038
|
}
|
|
9907
11039
|
}
|
|
9908
11040
|
if (!ctx.interactive) {
|
|
9909
|
-
|
|
11041
|
+
ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
|
|
11042
|
+
const { child: proc, password } = await startOpenCode2(port, {
|
|
11043
|
+
inheritStdio: ctx.inheritStdio
|
|
11044
|
+
});
|
|
11045
|
+
const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
|
|
11046
|
+
if (!health.healthy) {
|
|
11047
|
+
return {
|
|
11048
|
+
port,
|
|
11049
|
+
process: proc,
|
|
11050
|
+
version: null,
|
|
11051
|
+
notReadyReason: `it did not become healthy within ${Math.round(ctx.startTimeoutMs / 1e3)}s`,
|
|
11052
|
+
password
|
|
11053
|
+
};
|
|
11054
|
+
}
|
|
11055
|
+
ctx.log(`OpenCode V2 started on port ${port}${health.version ? ` (v${health.version})` : ""}`);
|
|
11056
|
+
return {
|
|
11057
|
+
port,
|
|
11058
|
+
process: proc,
|
|
11059
|
+
version: health.version ?? null,
|
|
11060
|
+
notReadyReason: null,
|
|
11061
|
+
password
|
|
11062
|
+
};
|
|
9910
11063
|
}
|
|
9911
|
-
console.log(chalk6.yellow(`
|
|
9912
|
-
${v2SessionSupportIncompleteError().message}`));
|
|
9913
11064
|
const action = await select3({
|
|
9914
11065
|
message: "OpenCode V2 is not running. What would you like to do?",
|
|
9915
11066
|
choices: [
|
|
11067
|
+
{
|
|
11068
|
+
name: "Start OpenCode V2 for me",
|
|
11069
|
+
value: "start",
|
|
11070
|
+
description: `Run 'opencode2 serve --port ${port}'`
|
|
11071
|
+
},
|
|
9916
11072
|
{
|
|
9917
11073
|
name: "Show me the command",
|
|
9918
11074
|
value: "manual",
|
|
@@ -9933,6 +11089,25 @@ ${v2SessionSupportIncompleteError().message}`));
|
|
|
9933
11089
|
blank();
|
|
9934
11090
|
throw new Error("Please start OpenCode V2 manually");
|
|
9935
11091
|
}
|
|
11092
|
+
if (action === "start") {
|
|
11093
|
+
const spinner = ora3("Starting OpenCode V2...").start();
|
|
11094
|
+
const { child: proc, password } = await startOpenCode2(port, {
|
|
11095
|
+
inheritStdio: ctx.inheritStdio
|
|
11096
|
+
});
|
|
11097
|
+
const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
|
|
11098
|
+
if (!health.healthy) {
|
|
11099
|
+
spinner.fail("Failed to start OpenCode V2");
|
|
11100
|
+
throw new Error("OpenCode V2 failed to start");
|
|
11101
|
+
}
|
|
11102
|
+
spinner.stop();
|
|
11103
|
+
return {
|
|
11104
|
+
port,
|
|
11105
|
+
process: proc,
|
|
11106
|
+
version: health.version ?? null,
|
|
11107
|
+
notReadyReason: null,
|
|
11108
|
+
password
|
|
11109
|
+
};
|
|
11110
|
+
}
|
|
9936
11111
|
return {
|
|
9937
11112
|
port,
|
|
9938
11113
|
process: null,
|
|
@@ -10826,7 +12001,7 @@ async function driveChannels(state, driver) {
|
|
|
10826
12001
|
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
10827
12002
|
if (opencodeAuthApplied) state.openaiUsageRearm?.();
|
|
10828
12003
|
if (claudeCredentialApplied || opencodeAuthApplied) {
|
|
10829
|
-
void reloadProviderCache(state.port).catch(
|
|
12004
|
+
void reloadProviderCache(state.port, state.opencodeClient ?? void 0).catch(
|
|
10830
12005
|
(error2) => logActivity(state, {
|
|
10831
12006
|
type: "error",
|
|
10832
12007
|
error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
@@ -10951,7 +12126,7 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
|
|
|
10951
12126
|
async function runSweep(state, driver, config) {
|
|
10952
12127
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
10953
12128
|
try {
|
|
10954
|
-
const sessions = await listSessions(state.port);
|
|
12129
|
+
const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
|
|
10955
12130
|
if (sessions === null) {
|
|
10956
12131
|
logActivity(state, {
|
|
10957
12132
|
type: "info",
|
|
@@ -10981,7 +12156,7 @@ async function runSweep(state, driver, config) {
|
|
|
10981
12156
|
});
|
|
10982
12157
|
continue;
|
|
10983
12158
|
}
|
|
10984
|
-
if (await deleteSession(state.port, id)) deleted++;
|
|
12159
|
+
if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
|
|
10985
12160
|
else failed++;
|
|
10986
12161
|
}
|
|
10987
12162
|
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
@@ -11488,6 +12663,9 @@ async function run(options) {
|
|
|
11488
12663
|
connected: false,
|
|
11489
12664
|
opencodeConnected: false,
|
|
11490
12665
|
opencodeVersion: null,
|
|
12666
|
+
opencodeApiVersion: "v1",
|
|
12667
|
+
opencodePassword: null,
|
|
12668
|
+
opencodeClient: null,
|
|
11491
12669
|
sessionDbProvenanceAnomaly: false,
|
|
11492
12670
|
opencodeProcess: null,
|
|
11493
12671
|
stopOpenCodeLogTail: null,
|
|
@@ -11642,7 +12820,7 @@ async function run(options) {
|
|
|
11642
12820
|
console.log(chalk7.bold("Evident Run"));
|
|
11643
12821
|
console.log(chalk7.dim("-".repeat(40)));
|
|
11644
12822
|
}
|
|
11645
|
-
const spinner = interactive && !state.json ?
|
|
12823
|
+
const spinner = interactive && !state.json ? ora4("Validating runner...").start() : null;
|
|
11646
12824
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
11647
12825
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
11648
12826
|
spinner?.fail("Authentication failed");
|
|
@@ -11763,7 +12941,7 @@ async function run(options) {
|
|
|
11763
12941
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
11764
12942
|
}
|
|
11765
12943
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
11766
|
-
const ocSpinner = interactive && !state.json ?
|
|
12944
|
+
const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
|
|
11767
12945
|
try {
|
|
11768
12946
|
const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
|
|
11769
12947
|
port: state.port,
|
|
@@ -11783,6 +12961,19 @@ async function run(options) {
|
|
|
11783
12961
|
state.port = oc.port;
|
|
11784
12962
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
11785
12963
|
state.opencodeVersion = oc.version;
|
|
12964
|
+
state.opencodeApiVersion = opencodeVersion;
|
|
12965
|
+
let opencodePassword = null;
|
|
12966
|
+
if (opencodeVersion === "v2" && "password" in oc) {
|
|
12967
|
+
const value = oc.password;
|
|
12968
|
+
if (typeof value === "string" || value === null) opencodePassword = value;
|
|
12969
|
+
}
|
|
12970
|
+
state.opencodePassword = opencodePassword;
|
|
12971
|
+
const openCodeClient = createOpenCodeClient({
|
|
12972
|
+
port: state.port,
|
|
12973
|
+
version: state.opencodeApiVersion,
|
|
12974
|
+
password: state.opencodePassword
|
|
12975
|
+
});
|
|
12976
|
+
state.opencodeClient = openCodeClient;
|
|
11786
12977
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
11787
12978
|
try {
|
|
11788
12979
|
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
@@ -11819,16 +13010,19 @@ async function run(options) {
|
|
|
11819
13010
|
const message = `OpenCode is not ready on port ${state.port}: ${oc.notReadyReason}. The runner will still come online, but messages will fail until opencode answers \u2014 raise the wait with --opencode-start-timeout <seconds> (env ${OPENCODE_START_TIMEOUT_ENV}).`;
|
|
11820
13011
|
logActivity(state, { type: "info", level: "warn", message });
|
|
11821
13012
|
} else {
|
|
11822
|
-
const versionWarning = buildOpenCodeVersionWarning(
|
|
13013
|
+
const versionWarning = buildOpenCodeVersionWarning(
|
|
13014
|
+
state.opencodeVersion,
|
|
13015
|
+
state.opencodeApiVersion
|
|
13016
|
+
);
|
|
11823
13017
|
if (versionWarning) {
|
|
11824
13018
|
log2(state, versionWarning, "warn");
|
|
11825
13019
|
if (state.interactive && !state.json) {
|
|
11826
13020
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
11827
13021
|
}
|
|
11828
13022
|
}
|
|
11829
|
-
await reloadProviderCache(state.port);
|
|
13023
|
+
await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
|
|
11830
13024
|
const noProviderWarning = buildNoProviderWarning(
|
|
11831
|
-
await hasAnyConfiguredProvider(state.port)
|
|
13025
|
+
await hasAnyConfiguredProvider(state.port, state.opencodeClient ?? void 0)
|
|
11832
13026
|
);
|
|
11833
13027
|
if (noProviderWarning) {
|
|
11834
13028
|
log2(state, noProviderWarning, "warn");
|
|
@@ -11951,11 +13145,12 @@ async function run(options) {
|
|
|
11951
13145
|
});
|
|
11952
13146
|
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
11953
13147
|
}
|
|
11954
|
-
const tunnelSpinner = interactive && !state.json ?
|
|
13148
|
+
const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
|
|
11955
13149
|
const channelDriver = new ChannelDriver({
|
|
11956
13150
|
agentId: state.agentId,
|
|
11957
13151
|
port: state.port,
|
|
11958
13152
|
apiUrl: getApiUrlConfig(),
|
|
13153
|
+
openCodeClient: state.opencodeClient ?? void 0,
|
|
11959
13154
|
getAuthHeader: () => state.authHeader,
|
|
11960
13155
|
conversationFilter: state.conversationFilter,
|
|
11961
13156
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
@@ -11981,6 +13176,7 @@ async function run(options) {
|
|
|
11981
13176
|
agentId: state.agentId,
|
|
11982
13177
|
getAuthHeader: () => state.authHeader,
|
|
11983
13178
|
port: state.port,
|
|
13179
|
+
openCodePassword: state.opencodePassword,
|
|
11984
13180
|
isRunning: () => state.running,
|
|
11985
13181
|
events: {
|
|
11986
13182
|
onConnected: (agentId, isReconnect) => {
|
|
@@ -12005,7 +13201,11 @@ async function run(options) {
|
|
|
12005
13201
|
emitAgentConnected(state.agentId, {
|
|
12006
13202
|
port: state.port,
|
|
12007
13203
|
cli_version: getCliVersion(),
|
|
12008
|
-
opencode_version:
|
|
13204
|
+
opencode_version: reportedOpenCodeVersion({
|
|
13205
|
+
version: state.opencodeVersion,
|
|
13206
|
+
major: state.opencodeApiVersion,
|
|
13207
|
+
connected: state.opencodeConnected
|
|
13208
|
+
})
|
|
12009
13209
|
});
|
|
12010
13210
|
if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
|
|
12011
13211
|
if (state.interactive) displayStatus(state);
|
|
@@ -12121,7 +13321,7 @@ async function run(options) {
|
|
|
12121
13321
|
state.openaiUsageTimer = timer;
|
|
12122
13322
|
},
|
|
12123
13323
|
fetchUsage: async () => {
|
|
12124
|
-
const usage = await getOpenAiUsage(state.port);
|
|
13324
|
+
const usage = await getOpenAiUsage(state.port, state.opencodeClient ?? void 0);
|
|
12125
13325
|
if (usage.subscription === null) {
|
|
12126
13326
|
logActivity(state, {
|
|
12127
13327
|
type: "info",
|