@evident-ai/cli 3.4.1-dev.38181ff → 3.4.1-dev.388b76b
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 +1322 -250
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1178,11 +1178,11 @@ function stripQuery(url) {
|
|
|
1178
1178
|
}
|
|
1179
1179
|
|
|
1180
1180
|
// src/commands/run.ts
|
|
1181
|
-
import
|
|
1181
|
+
import ora4 from "ora";
|
|
1182
1182
|
import { select as select4 } from "@inquirer/prompts";
|
|
1183
1183
|
|
|
1184
1184
|
// src/lib/telemetry.ts
|
|
1185
|
-
var CLI_VERSION = (true ? "3.4.1-dev.
|
|
1185
|
+
var CLI_VERSION = (true ? "3.4.1-dev.388b76b" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1186
1186
|
function getCliVersion() {
|
|
1187
1187
|
return CLI_VERSION;
|
|
1188
1188
|
}
|
|
@@ -1656,6 +1656,11 @@ function isSessionDbRecoveryRecord(value) {
|
|
|
1656
1656
|
);
|
|
1657
1657
|
}
|
|
1658
1658
|
|
|
1659
|
+
// src/lib/opencode/auth.ts
|
|
1660
|
+
function buildOpenCodeBasicAuthHeader(password) {
|
|
1661
|
+
return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1659
1664
|
// src/lib/opencode/health.ts
|
|
1660
1665
|
async function checkOpenCodeHealth(port) {
|
|
1661
1666
|
try {
|
|
@@ -1673,6 +1678,27 @@ async function checkOpenCodeHealth(port) {
|
|
|
1673
1678
|
return { healthy: false, error: message };
|
|
1674
1679
|
}
|
|
1675
1680
|
}
|
|
1681
|
+
async function checkOpenCode2Health(port, password) {
|
|
1682
|
+
try {
|
|
1683
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
1684
|
+
headers: {
|
|
1685
|
+
Authorization: buildOpenCodeBasicAuthHeader(password)
|
|
1686
|
+
},
|
|
1687
|
+
signal: AbortSignal.timeout(2e3)
|
|
1688
|
+
});
|
|
1689
|
+
if (response.status === 401) {
|
|
1690
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
1691
|
+
}
|
|
1692
|
+
if (!response.ok) {
|
|
1693
|
+
return { healthy: false, error: `HTTP ${response.status}` };
|
|
1694
|
+
}
|
|
1695
|
+
const data = await response.json().catch(() => ({}));
|
|
1696
|
+
return { healthy: true, version: data.version };
|
|
1697
|
+
} catch (error2) {
|
|
1698
|
+
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1699
|
+
return { healthy: false, error: message };
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1676
1702
|
async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
1677
1703
|
const startTime = Date.now();
|
|
1678
1704
|
while (Date.now() - startTime < timeoutMs) {
|
|
@@ -1684,6 +1710,61 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
1684
1710
|
}
|
|
1685
1711
|
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
1686
1712
|
}
|
|
1713
|
+
async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
|
|
1714
|
+
const startTime = Date.now();
|
|
1715
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
1716
|
+
const health = await checkOpenCode2Health(port, password);
|
|
1717
|
+
if (health.healthy || health.authFailed) {
|
|
1718
|
+
return health;
|
|
1719
|
+
}
|
|
1720
|
+
await new Promise((resolve4) => setTimeout(resolve4, 1e3));
|
|
1721
|
+
}
|
|
1722
|
+
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/lib/http-timeout.ts
|
|
1726
|
+
var REQUEST_TIMEOUT_MS = 6e4;
|
|
1727
|
+
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
1728
|
+
return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/lib/opencode/client.ts
|
|
1732
|
+
function redactPassword(message, password) {
|
|
1733
|
+
return message.replaceAll(password, "[redacted]");
|
|
1734
|
+
}
|
|
1735
|
+
function createOpenCodeClient(options) {
|
|
1736
|
+
const password = options.password ?? null;
|
|
1737
|
+
const fetchImpl = withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
|
|
1738
|
+
const baseUrl = `http://127.0.0.1:${options.port}`;
|
|
1739
|
+
return {
|
|
1740
|
+
port: options.port,
|
|
1741
|
+
version: options.version,
|
|
1742
|
+
password,
|
|
1743
|
+
async request(path, init, requestOptions) {
|
|
1744
|
+
const requestInit = options.version === "v2" && password !== null ? (() => {
|
|
1745
|
+
const headers = new Headers(init?.headers);
|
|
1746
|
+
headers.set("Authorization", buildOpenCodeBasicAuthHeader(password));
|
|
1747
|
+
return { ...init, headers };
|
|
1748
|
+
})() : init;
|
|
1749
|
+
try {
|
|
1750
|
+
const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
|
|
1751
|
+
if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
|
|
1752
|
+
const body = await response.text();
|
|
1753
|
+
throw new Error(
|
|
1754
|
+
`OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
return response;
|
|
1758
|
+
} catch (error2) {
|
|
1759
|
+
if (options.version === "v2" && password !== null) {
|
|
1760
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1761
|
+
throw new Error(redactPassword(message, password));
|
|
1762
|
+
}
|
|
1763
|
+
throw error2;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1687
1768
|
|
|
1688
1769
|
// src/lib/opencode/session-db-boot.ts
|
|
1689
1770
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -2347,15 +2428,23 @@ function isQueueValidatedVersion(version2) {
|
|
|
2347
2428
|
if (!version2) return false;
|
|
2348
2429
|
return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version2);
|
|
2349
2430
|
}
|
|
2350
|
-
function buildOpenCodeVersionWarning(version2) {
|
|
2351
|
-
if (
|
|
2352
|
-
const detected = version2 ? `v${version2}` : "unknown";
|
|
2431
|
+
function buildOpenCodeVersionWarning(version2, major) {
|
|
2432
|
+
if (major === "v2") return null;
|
|
2353
2433
|
const validated = QUEUE_VALIDATED_OPENCODE_VERSIONS.map((v) => `v${v}`).join(", ");
|
|
2354
|
-
|
|
2434
|
+
if (!version2) {
|
|
2435
|
+
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.`;
|
|
2436
|
+
}
|
|
2437
|
+
if (isQueueValidatedVersion(version2)) return null;
|
|
2438
|
+
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.`;
|
|
2439
|
+
}
|
|
2440
|
+
function reportedOpenCodeVersion(input) {
|
|
2441
|
+
if (!input.connected) return null;
|
|
2442
|
+
return input.version || `${input.major}-unknown`;
|
|
2355
2443
|
}
|
|
2356
2444
|
|
|
2357
2445
|
// src/lib/opencode/process.ts
|
|
2358
2446
|
import { execSync, spawn as spawn3 } from "child_process";
|
|
2447
|
+
import { randomBytes } from "node:crypto";
|
|
2359
2448
|
|
|
2360
2449
|
// src/lib/process-stop.ts
|
|
2361
2450
|
async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
@@ -2414,6 +2503,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
2414
2503
|
// src/lib/opencode/process.ts
|
|
2415
2504
|
var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
|
|
2416
2505
|
var VALID_OPENCODE_LOG_LEVELS = /* @__PURE__ */ new Set(["DEBUG", "INFO", "WARN", "ERROR"]);
|
|
2506
|
+
var VALID_OPENCODE2_LOG_LEVELS = /* @__PURE__ */ new Set([
|
|
2507
|
+
"all",
|
|
2508
|
+
"trace",
|
|
2509
|
+
"debug",
|
|
2510
|
+
"info",
|
|
2511
|
+
"warn",
|
|
2512
|
+
"warning",
|
|
2513
|
+
"error",
|
|
2514
|
+
"fatal",
|
|
2515
|
+
"none"
|
|
2516
|
+
]);
|
|
2417
2517
|
function resolveOpenCodeLogLevel(env) {
|
|
2418
2518
|
const raw = env.OPENCODE_LOG_LEVEL;
|
|
2419
2519
|
if (!raw) return "INFO";
|
|
@@ -2424,6 +2524,16 @@ function resolveOpenCodeLogLevel(env) {
|
|
|
2424
2524
|
);
|
|
2425
2525
|
return "INFO";
|
|
2426
2526
|
}
|
|
2527
|
+
function resolveOpenCode2LogLevel(env) {
|
|
2528
|
+
const raw = env.OPENCODE_LOG_LEVEL;
|
|
2529
|
+
if (!raw) return "info";
|
|
2530
|
+
const lower = raw.toLowerCase();
|
|
2531
|
+
if (VALID_OPENCODE2_LOG_LEVELS.has(lower)) return lower;
|
|
2532
|
+
console.warn(
|
|
2533
|
+
`startOpenCode2: ignoring invalid OPENCODE_LOG_LEVEL "${raw}" (expected all|trace|debug|info|warn|warning|error|fatal|none) \u2014 using info`
|
|
2534
|
+
);
|
|
2535
|
+
return "info";
|
|
2536
|
+
}
|
|
2427
2537
|
function getProcessCwd(pid) {
|
|
2428
2538
|
const platform = process.platform;
|
|
2429
2539
|
try {
|
|
@@ -2606,6 +2716,37 @@ async function startOpenCode(port, options = {}) {
|
|
|
2606
2716
|
});
|
|
2607
2717
|
return child;
|
|
2608
2718
|
}
|
|
2719
|
+
async function startOpenCode2(port, options = {}) {
|
|
2720
|
+
const password = randomBytes(24).toString("hex");
|
|
2721
|
+
let command = "opencode2";
|
|
2722
|
+
const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
|
|
2723
|
+
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
|
|
2724
|
+
try {
|
|
2725
|
+
execSync("which opencode2", { stdio: "ignore" });
|
|
2726
|
+
} catch {
|
|
2727
|
+
command = "npx";
|
|
2728
|
+
args = [
|
|
2729
|
+
"-y",
|
|
2730
|
+
"-p",
|
|
2731
|
+
"@opencode-ai/cli@beta",
|
|
2732
|
+
"--",
|
|
2733
|
+
"opencode2",
|
|
2734
|
+
"serve",
|
|
2735
|
+
"--port",
|
|
2736
|
+
port.toString(),
|
|
2737
|
+
"--hostname",
|
|
2738
|
+
"127.0.0.1",
|
|
2739
|
+
...logLevel
|
|
2740
|
+
];
|
|
2741
|
+
}
|
|
2742
|
+
const child = spawn3(command, args, {
|
|
2743
|
+
env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
|
|
2744
|
+
detached: true,
|
|
2745
|
+
stdio: options.inheritStdio ? "inherit" : "ignore",
|
|
2746
|
+
cwd: process.cwd()
|
|
2747
|
+
});
|
|
2748
|
+
return { child, password };
|
|
2749
|
+
}
|
|
2609
2750
|
function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
|
|
2610
2751
|
const sendSignal = (signal) => {
|
|
2611
2752
|
if (process.platform === "win32") {
|
|
@@ -2753,27 +2894,500 @@ function buildNoProviderWarning(hasProvider) {
|
|
|
2753
2894
|
return "Warning: opencode has no authenticated model provider configured, so it won't be able to answer prompts. Run `opencode auth login` to set one up (see https://opencode.ai for details).";
|
|
2754
2895
|
}
|
|
2755
2896
|
|
|
2756
|
-
// src/lib/
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2897
|
+
// src/lib/opencode/session-v2.ts
|
|
2898
|
+
function isRecord(value) {
|
|
2899
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2900
|
+
}
|
|
2901
|
+
function finiteNumber(value) {
|
|
2902
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2903
|
+
}
|
|
2904
|
+
function adaptTime(value) {
|
|
2905
|
+
if (!isRecord(value)) return void 0;
|
|
2906
|
+
const created = finiteNumber(value.created);
|
|
2907
|
+
const completed = finiteNumber(value.completed);
|
|
2908
|
+
if (created === void 0 && completed === void 0) return void 0;
|
|
2909
|
+
return {
|
|
2910
|
+
...created !== void 0 ? { created } : {},
|
|
2911
|
+
...completed !== void 0 ? { completed } : {}
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
function adaptTokens(value) {
|
|
2915
|
+
if (!isRecord(value)) return void 0;
|
|
2916
|
+
const input = finiteNumber(value.input);
|
|
2917
|
+
const output = finiteNumber(value.output);
|
|
2918
|
+
const reasoning = finiteNumber(value.reasoning);
|
|
2919
|
+
const cache = isRecord(value.cache) ? {
|
|
2920
|
+
...finiteNumber(value.cache.read) !== void 0 ? { read: finiteNumber(value.cache.read) } : {},
|
|
2921
|
+
...finiteNumber(value.cache.write) !== void 0 ? { write: finiteNumber(value.cache.write) } : {}
|
|
2922
|
+
} : void 0;
|
|
2923
|
+
if (input === void 0 && output === void 0 && reasoning === void 0 && !cache) {
|
|
2924
|
+
return void 0;
|
|
2925
|
+
}
|
|
2926
|
+
return {
|
|
2927
|
+
...input !== void 0 ? { input } : {},
|
|
2928
|
+
...output !== void 0 ? { output } : {},
|
|
2929
|
+
...reasoning !== void 0 ? { reasoning } : {},
|
|
2930
|
+
...cache ? { cache } : {}
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
function adaptMessageInfo(value, role) {
|
|
2934
|
+
const info = {
|
|
2935
|
+
id: value.id,
|
|
2936
|
+
role
|
|
2937
|
+
};
|
|
2938
|
+
const time = adaptTime(value.time);
|
|
2939
|
+
if (time) info.time = time;
|
|
2940
|
+
if (typeof value.finish === "string") info.finish = value.finish;
|
|
2941
|
+
if ("error" in value) info.error = value.error;
|
|
2942
|
+
if (typeof value.agent === "string") info.agent = value.agent;
|
|
2943
|
+
if (isRecord(value.model)) {
|
|
2944
|
+
if (typeof value.model.id === "string") info.modelID = value.model.id;
|
|
2945
|
+
if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
|
|
2946
|
+
}
|
|
2947
|
+
if (typeof value.cost === "number" && Number.isFinite(value.cost)) info.cost = value.cost;
|
|
2948
|
+
const tokens = adaptTokens(value.tokens);
|
|
2949
|
+
if (tokens) info.tokens = tokens;
|
|
2950
|
+
return info;
|
|
2951
|
+
}
|
|
2952
|
+
function adaptV2Message(value) {
|
|
2953
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.type !== "string") {
|
|
2954
|
+
return null;
|
|
2955
|
+
}
|
|
2956
|
+
if (value.type === "user") {
|
|
2957
|
+
if (typeof value.text !== "string") return null;
|
|
2958
|
+
return {
|
|
2959
|
+
info: adaptMessageInfo(value, "user"),
|
|
2960
|
+
parts: [{ type: "text", text: value.text }]
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
|
|
2964
|
+
const parts = [];
|
|
2965
|
+
for (const content of value.content) {
|
|
2966
|
+
if (!isRecord(content) || typeof content.type !== "string") return null;
|
|
2967
|
+
if (content.type === "text") {
|
|
2968
|
+
if (typeof content.text !== "string") return null;
|
|
2969
|
+
parts.push({ type: "text", text: content.text });
|
|
2970
|
+
} else {
|
|
2971
|
+
parts.push({ type: content.type });
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
return {
|
|
2975
|
+
info: adaptMessageInfo(value, "assistant"),
|
|
2976
|
+
parts
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
function adaptFormTool(value) {
|
|
2980
|
+
if (!isRecord(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
|
|
2981
|
+
return void 0;
|
|
2982
|
+
}
|
|
2983
|
+
return { messageID: value.messageID, callID: value.id };
|
|
2984
|
+
}
|
|
2985
|
+
function adaptV2FormWire(value) {
|
|
2986
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
2987
|
+
return null;
|
|
2988
|
+
}
|
|
2989
|
+
return value;
|
|
2990
|
+
}
|
|
2991
|
+
function adaptV2FormField(value, header) {
|
|
2992
|
+
if (!isRecord(value)) return null;
|
|
2993
|
+
const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
|
|
2994
|
+
if (!question) return null;
|
|
2995
|
+
const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
|
|
2996
|
+
if (!isRecord(option)) return [];
|
|
2997
|
+
const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
|
|
2998
|
+
if (!label) return [];
|
|
2999
|
+
return [
|
|
3000
|
+
{
|
|
3001
|
+
label,
|
|
3002
|
+
description: typeof option.description === "string" ? option.description : ""
|
|
3003
|
+
}
|
|
3004
|
+
];
|
|
3005
|
+
}) : [];
|
|
3006
|
+
return { question, header, options };
|
|
3007
|
+
}
|
|
3008
|
+
function adaptV2Form(value) {
|
|
3009
|
+
const form = adaptV2FormWire(value);
|
|
3010
|
+
if (!form || !Array.isArray(form.fields)) return null;
|
|
3011
|
+
const header = typeof form.title === "string" ? form.title : "";
|
|
3012
|
+
const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
|
|
3013
|
+
if (questions.length === 0) return null;
|
|
3014
|
+
const tool = isRecord(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
|
|
3015
|
+
return {
|
|
3016
|
+
id: form.id,
|
|
3017
|
+
sessionID: form.sessionID,
|
|
3018
|
+
questions,
|
|
3019
|
+
...tool ? { tool } : {},
|
|
3020
|
+
raw: form
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
function adaptV2FormList(value) {
|
|
3024
|
+
if (!isRecord(value) || !Array.isArray(value.data)) return null;
|
|
3025
|
+
return value.data.map(adaptV2Form).filter((form) => form !== null);
|
|
3026
|
+
}
|
|
3027
|
+
function adaptPattern(value) {
|
|
3028
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
3029
|
+
if (Array.isArray(value) && value.every((pattern) => typeof pattern === "string")) {
|
|
3030
|
+
return value;
|
|
3031
|
+
}
|
|
3032
|
+
return void 0;
|
|
3033
|
+
}
|
|
3034
|
+
function adaptV2PermissionWire(value) {
|
|
3035
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
3036
|
+
return null;
|
|
3037
|
+
}
|
|
3038
|
+
if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
|
|
3039
|
+
return value;
|
|
3040
|
+
}
|
|
3041
|
+
function adaptV2Permission(value) {
|
|
3042
|
+
const permission = adaptV2PermissionWire(value);
|
|
3043
|
+
if (!permission) return null;
|
|
3044
|
+
const type = permission.permission ?? permission.action;
|
|
3045
|
+
if (!type) return null;
|
|
3046
|
+
const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
|
|
3047
|
+
const time = isRecord(permission.time) ? finiteNumber(permission.time.created) !== void 0 ? { created: finiteNumber(permission.time.created) } : void 0 : void 0;
|
|
3048
|
+
return {
|
|
3049
|
+
id: permission.id,
|
|
3050
|
+
type,
|
|
3051
|
+
sessionID: permission.sessionID,
|
|
3052
|
+
metadata: isRecord(permission.metadata) ? permission.metadata : {},
|
|
3053
|
+
raw: permission,
|
|
3054
|
+
...pattern !== void 0 ? { pattern } : {},
|
|
3055
|
+
...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
|
|
3056
|
+
...typeof permission.callID === "string" ? { callID: permission.callID } : {},
|
|
3057
|
+
...typeof permission.title === "string" ? { title: permission.title } : {},
|
|
3058
|
+
...time ? { time } : {}
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
function adaptV2PermissionList(value) {
|
|
3062
|
+
if (!isRecord(value) || !Array.isArray(value.data)) return null;
|
|
3063
|
+
return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
|
|
3064
|
+
}
|
|
3065
|
+
function adaptV2Session(value) {
|
|
3066
|
+
if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) return null;
|
|
3067
|
+
const time = isRecord(value.time) ? {
|
|
3068
|
+
...finiteNumber(value.time.created) !== void 0 ? { created: finiteNumber(value.time.created) } : {},
|
|
3069
|
+
...finiteNumber(value.time.updated) !== void 0 ? { updated: finiteNumber(value.time.updated) } : {}
|
|
3070
|
+
} : void 0;
|
|
3071
|
+
return {
|
|
3072
|
+
id: value.id,
|
|
3073
|
+
...typeof value.title === "string" ? { title: value.title } : {},
|
|
3074
|
+
...typeof value.parentID === "string" ? { parentID: value.parentID } : {},
|
|
3075
|
+
...time && Object.keys(time).length > 0 ? { time } : {}
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
function adaptV2SessionList(value) {
|
|
3079
|
+
if (!isRecord(value) || !Array.isArray(value.data) || !isRecord(value.cursor)) return null;
|
|
3080
|
+
return {
|
|
3081
|
+
data: value.data.map(adaptV2Session).filter((session) => session !== null),
|
|
3082
|
+
cursor: value.cursor
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
function adaptV2Location(value) {
|
|
3086
|
+
const candidates = [
|
|
3087
|
+
value,
|
|
3088
|
+
isRecord(value) ? value.data : void 0,
|
|
3089
|
+
isRecord(value) ? value.location : void 0
|
|
3090
|
+
];
|
|
3091
|
+
for (const candidate of candidates) {
|
|
3092
|
+
if (!isRecord(candidate) || typeof candidate.directory !== "string") continue;
|
|
3093
|
+
const directory = candidate.directory.trim();
|
|
3094
|
+
if (directory) return directory;
|
|
3095
|
+
}
|
|
3096
|
+
return null;
|
|
3097
|
+
}
|
|
3098
|
+
function adaptV2Messages(value) {
|
|
3099
|
+
if (!isRecord(value) || !Array.isArray(value.data)) return [];
|
|
3100
|
+
return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
|
|
3101
|
+
}
|
|
3102
|
+
async function readJson(response) {
|
|
3103
|
+
try {
|
|
3104
|
+
return await response.json();
|
|
3105
|
+
} catch (error2) {
|
|
3106
|
+
throw new Error(
|
|
3107
|
+
`OpenCode V2 response was not valid JSON: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
async function readData(client, path, init) {
|
|
3112
|
+
const response = await client.request(path, init);
|
|
3113
|
+
const body = await readJson(response);
|
|
3114
|
+
if (!isRecord(body) || !("data" in body)) {
|
|
3115
|
+
throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
|
|
3116
|
+
}
|
|
3117
|
+
return body.data;
|
|
3118
|
+
}
|
|
3119
|
+
var OpenCodeV2PromptAckError = class extends Error {
|
|
3120
|
+
constructor(message) {
|
|
3121
|
+
super(message);
|
|
3122
|
+
this.name = "OpenCodeV2PromptAckError";
|
|
3123
|
+
}
|
|
3124
|
+
};
|
|
3125
|
+
async function getOpenCodeDirectoryV2(client) {
|
|
3126
|
+
try {
|
|
3127
|
+
return adaptV2Location(await readJson(await client.request("/api/location")));
|
|
3128
|
+
} catch (error2) {
|
|
3129
|
+
console.error(
|
|
3130
|
+
`[getOpenCodeDirectoryV2] GET /api/location failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3131
|
+
);
|
|
3132
|
+
return null;
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
async function createV2Session(client, directory) {
|
|
3136
|
+
const data = await readData(client, "/api/session", {
|
|
3137
|
+
method: "POST",
|
|
3138
|
+
headers: { "Content-Type": "application/json" },
|
|
3139
|
+
body: JSON.stringify({ location: { directory } })
|
|
3140
|
+
});
|
|
3141
|
+
if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
3142
|
+
throw new Error("OpenCode V2 create session response was missing data.id");
|
|
3143
|
+
}
|
|
3144
|
+
return data.id;
|
|
3145
|
+
}
|
|
3146
|
+
async function getV2Session(client, sessionId) {
|
|
3147
|
+
const data = await readData(client, `/api/session/${encodeURIComponent(sessionId)}`);
|
|
3148
|
+
const session = adaptV2Session(data);
|
|
3149
|
+
if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
|
|
3150
|
+
return session;
|
|
3151
|
+
}
|
|
3152
|
+
async function listV2SessionPage(client, cursor) {
|
|
3153
|
+
const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
|
|
3154
|
+
try {
|
|
3155
|
+
return adaptV2SessionList(await readJson(await client.request(path)));
|
|
3156
|
+
} catch (error2) {
|
|
3157
|
+
console.error(
|
|
3158
|
+
`[listV2SessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3159
|
+
);
|
|
3160
|
+
return null;
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
async function listV2Sessions(client) {
|
|
3164
|
+
const sessions = [];
|
|
3165
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3166
|
+
let cursor;
|
|
3167
|
+
let hasNextPage = true;
|
|
3168
|
+
try {
|
|
3169
|
+
while (hasNextPage) {
|
|
3170
|
+
const page = await listV2SessionPage(client, cursor);
|
|
3171
|
+
if (!page) return null;
|
|
3172
|
+
sessions.push(...page.data);
|
|
3173
|
+
const next = page.cursor.next;
|
|
3174
|
+
if (next === void 0 || next === null) {
|
|
3175
|
+
hasNextPage = false;
|
|
3176
|
+
continue;
|
|
3177
|
+
}
|
|
3178
|
+
if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
|
|
3179
|
+
throw new Error("OpenCode V2 session list contained an invalid next cursor");
|
|
3180
|
+
}
|
|
3181
|
+
seenCursors.add(next);
|
|
3182
|
+
cursor = next;
|
|
3183
|
+
}
|
|
3184
|
+
return sessions;
|
|
3185
|
+
} catch (error2) {
|
|
3186
|
+
console.error(
|
|
3187
|
+
`[listV2Sessions] session pagination failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3188
|
+
);
|
|
3189
|
+
return null;
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
async function deleteV2Session(client, sessionId) {
|
|
3193
|
+
try {
|
|
3194
|
+
await client.request(`/api/session/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
|
|
3195
|
+
return true;
|
|
3196
|
+
} catch (error2) {
|
|
3197
|
+
console.error(
|
|
3198
|
+
`[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3199
|
+
);
|
|
3200
|
+
return false;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
async function v2SessionExists(client, sessionId) {
|
|
3204
|
+
try {
|
|
3205
|
+
const response = await client.request(
|
|
3206
|
+
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
3207
|
+
void 0,
|
|
3208
|
+
{ allowStatuses: [404] }
|
|
3209
|
+
);
|
|
3210
|
+
return response.status === 404 ? false : true;
|
|
3211
|
+
} catch (error2) {
|
|
3212
|
+
console.error(
|
|
3213
|
+
`[v2SessionExists] GET /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3214
|
+
);
|
|
3215
|
+
return null;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
async function sendV2Prompt(client, sessionId, text) {
|
|
3219
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/prompt`;
|
|
3220
|
+
const response = await client.request(path, {
|
|
3221
|
+
method: "POST",
|
|
3222
|
+
headers: { "Content-Type": "application/json" },
|
|
3223
|
+
body: JSON.stringify({ text, delivery: "queue" })
|
|
3224
|
+
});
|
|
3225
|
+
let body;
|
|
3226
|
+
try {
|
|
3227
|
+
body = await readJson(response);
|
|
3228
|
+
} catch (error2) {
|
|
3229
|
+
throw new OpenCodeV2PromptAckError(
|
|
3230
|
+
`OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
const data = isRecord(body) && "data" in body ? body.data : void 0;
|
|
3234
|
+
if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
3235
|
+
throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
|
|
3236
|
+
}
|
|
3237
|
+
return data.id;
|
|
3238
|
+
}
|
|
3239
|
+
async function getV2SessionMessages(client, sessionId) {
|
|
3240
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
|
|
3241
|
+
try {
|
|
3242
|
+
const body = await readJson(await client.request(path));
|
|
3243
|
+
if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) return null;
|
|
3244
|
+
return adaptV2Messages(body);
|
|
3245
|
+
} catch (error2) {
|
|
3246
|
+
console.error(
|
|
3247
|
+
`[getV2SessionMessages] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3248
|
+
);
|
|
3249
|
+
return null;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
async function listV2Forms(client, sessionId) {
|
|
3253
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/form`;
|
|
3254
|
+
try {
|
|
3255
|
+
return adaptV2FormList(await readJson(await client.request(path)));
|
|
3256
|
+
} catch (error2) {
|
|
3257
|
+
console.error(
|
|
3258
|
+
`[listV2Forms] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3259
|
+
);
|
|
3260
|
+
return null;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
async function listV2Permissions(client, sessionId) {
|
|
3264
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/permission`;
|
|
3265
|
+
try {
|
|
3266
|
+
return adaptV2PermissionList(await readJson(await client.request(path)));
|
|
3267
|
+
} catch (error2) {
|
|
3268
|
+
console.error(
|
|
3269
|
+
`[listV2Permissions] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3270
|
+
);
|
|
3271
|
+
return null;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
async function getV2ActiveSessions(client) {
|
|
3275
|
+
try {
|
|
3276
|
+
const body = await readJson(await client.request("/api/session/active"));
|
|
3277
|
+
if (!isRecord(body) || !isRecord(body.data)) return null;
|
|
3278
|
+
return body.data;
|
|
3279
|
+
} catch (error2) {
|
|
3280
|
+
console.error(
|
|
3281
|
+
`[getV2ActiveSessions] GET /api/session/active failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3282
|
+
);
|
|
3283
|
+
return null;
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
async function isV2SessionOngoing(client, sessionId) {
|
|
3287
|
+
const activeSessions = await getV2ActiveSessions(client);
|
|
3288
|
+
if (activeSessions === null) return null;
|
|
3289
|
+
return Object.prototype.hasOwnProperty.call(activeSessions, sessionId);
|
|
3290
|
+
}
|
|
3291
|
+
function sessionErrorReason(value) {
|
|
3292
|
+
if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
|
|
3293
|
+
if (isRecord(value)) {
|
|
3294
|
+
const data = isRecord(value.data) ? value.data : void 0;
|
|
3295
|
+
const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
|
|
3296
|
+
if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
3297
|
+
}
|
|
3298
|
+
return "OpenCode reported a session error with no details";
|
|
3299
|
+
}
|
|
3300
|
+
function adaptV2SessionErrorEvent(value) {
|
|
3301
|
+
let parsed = value;
|
|
3302
|
+
if (typeof value === "string") {
|
|
3303
|
+
try {
|
|
3304
|
+
parsed = JSON.parse(value);
|
|
3305
|
+
} catch (error2) {
|
|
3306
|
+
void error2;
|
|
3307
|
+
return null;
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
if (!isRecord(parsed)) return null;
|
|
3311
|
+
try {
|
|
3312
|
+
const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
|
|
3313
|
+
if (establishedShape) return establishedShape;
|
|
3314
|
+
} catch (error2) {
|
|
3315
|
+
void error2;
|
|
3316
|
+
}
|
|
3317
|
+
const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord);
|
|
3318
|
+
for (const event of candidates) {
|
|
3319
|
+
if (event.type !== "session.error") continue;
|
|
3320
|
+
const properties = [event.properties, event.data, event].find(isRecord);
|
|
3321
|
+
if (!properties) continue;
|
|
3322
|
+
const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
|
|
3323
|
+
if (!sessionId) continue;
|
|
3324
|
+
return {
|
|
3325
|
+
sessionId,
|
|
3326
|
+
reason: sessionErrorReason(properties.error ?? properties)
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
return null;
|
|
3330
|
+
}
|
|
3331
|
+
async function readV2SessionErrorStream(client, options) {
|
|
3332
|
+
let reader = null;
|
|
3333
|
+
try {
|
|
3334
|
+
const response = await client.request("/api/event", {
|
|
3335
|
+
headers: { accept: "text/event-stream" },
|
|
3336
|
+
signal: options.signal
|
|
3337
|
+
});
|
|
3338
|
+
if (!response.ok || !response.body) {
|
|
3339
|
+
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
3340
|
+
}
|
|
3341
|
+
reader = response.body.getReader();
|
|
3342
|
+
const decoder = new TextDecoder();
|
|
3343
|
+
let buffer = "";
|
|
3344
|
+
const processLine = (line) => {
|
|
3345
|
+
const trimmed = line.trimEnd();
|
|
3346
|
+
if (!trimmed.startsWith("data:")) return;
|
|
3347
|
+
const event = adaptV2SessionErrorEvent(trimmed.slice("data:".length).replace(/^ /, ""));
|
|
3348
|
+
if (event) options.onSessionError(event);
|
|
3349
|
+
};
|
|
3350
|
+
while (true) {
|
|
3351
|
+
const { done, value } = await reader.read();
|
|
3352
|
+
if (done) return { reason: "ended" };
|
|
3353
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3354
|
+
const lines = buffer.split("\n");
|
|
3355
|
+
buffer = lines.pop() ?? "";
|
|
3356
|
+
for (const line of lines) processLine(line);
|
|
3357
|
+
}
|
|
3358
|
+
} catch (error2) {
|
|
3359
|
+
if (options.signal.aborted) return { reason: "aborted" };
|
|
3360
|
+
return {
|
|
3361
|
+
reason: "unavailable",
|
|
3362
|
+
detail: error2 instanceof Error ? error2.message : String(error2)
|
|
3363
|
+
};
|
|
3364
|
+
} finally {
|
|
3365
|
+
if (reader) void reader.cancel().catch(() => void 0);
|
|
3366
|
+
}
|
|
2760
3367
|
}
|
|
2761
3368
|
|
|
2762
3369
|
// src/lib/opencode/session.ts
|
|
3370
|
+
var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
2763
3371
|
function timedFetch(input, init) {
|
|
2764
3372
|
return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
|
|
2765
3373
|
}
|
|
3374
|
+
function requestWithClient(port, client, path, init, options) {
|
|
3375
|
+
return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
|
|
3376
|
+
}
|
|
2766
3377
|
function opencodeBase(port) {
|
|
2767
3378
|
return `http://127.0.0.1:${port}`;
|
|
2768
3379
|
}
|
|
2769
|
-
async function getOpenCodeDirectory(port) {
|
|
3380
|
+
async function getOpenCodeDirectory(port, client) {
|
|
2770
3381
|
try {
|
|
2771
|
-
const res = await
|
|
3382
|
+
const res = await requestWithClient(port, client, "/path");
|
|
2772
3383
|
if (!res.ok) return null;
|
|
2773
3384
|
const body = await res.json();
|
|
2774
3385
|
const dir = typeof body.directory === "string" && body.directory || typeof body.worktree === "string" && body.worktree || typeof body.path?.cwd === "string" && body.path.cwd || typeof body.path?.directory === "string" && body.path.directory || null;
|
|
2775
3386
|
return dir && dir.trim() ? dir.trim() : null;
|
|
2776
|
-
} catch {
|
|
3387
|
+
} catch (error2) {
|
|
3388
|
+
console.error(
|
|
3389
|
+
`[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3390
|
+
);
|
|
2777
3391
|
return null;
|
|
2778
3392
|
}
|
|
2779
3393
|
}
|
|
@@ -2817,16 +3431,48 @@ function isAssistantInFlight(m) {
|
|
|
2817
3431
|
if (completedOf(m) == null) return true;
|
|
2818
3432
|
return finishOf(m) === "tool-calls";
|
|
2819
3433
|
}
|
|
2820
|
-
async function getSessionMessages(port, sessionId) {
|
|
3434
|
+
async function getSessionMessages(port, sessionId, client) {
|
|
2821
3435
|
try {
|
|
2822
|
-
const
|
|
3436
|
+
const path = `/session/${sessionId}/message`;
|
|
3437
|
+
const res = await requestWithClient(port, client, path);
|
|
2823
3438
|
if (!res.ok) return null;
|
|
2824
3439
|
const body = await res.json();
|
|
2825
3440
|
return Array.isArray(body) ? body : null;
|
|
2826
|
-
} catch {
|
|
3441
|
+
} catch (error2) {
|
|
3442
|
+
console.error(
|
|
3443
|
+
`[getSessionMessages] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3444
|
+
);
|
|
2827
3445
|
return null;
|
|
2828
3446
|
}
|
|
2829
3447
|
}
|
|
3448
|
+
async function fetchSessionMessages(port, sessionId, client) {
|
|
3449
|
+
const response = await requestWithClient(port, client, `/session/${sessionId}/message`);
|
|
3450
|
+
if (!response.ok) return null;
|
|
3451
|
+
const body = await response.json();
|
|
3452
|
+
return Array.isArray(body) ? body : null;
|
|
3453
|
+
}
|
|
3454
|
+
async function pollSessionMessagesForRedrive(port, sessionId, client) {
|
|
3455
|
+
try {
|
|
3456
|
+
const response = await requestWithClient(
|
|
3457
|
+
port,
|
|
3458
|
+
client,
|
|
3459
|
+
`/session/${sessionId}/message`,
|
|
3460
|
+
void 0,
|
|
3461
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
3462
|
+
);
|
|
3463
|
+
if (!response.ok) {
|
|
3464
|
+
return { ok: false, status: response.status, body: await response.text(), malformed: false };
|
|
3465
|
+
}
|
|
3466
|
+
const body = await response.json();
|
|
3467
|
+
if (!Array.isArray(body)) return { ok: false, status: null, body: "", malformed: true };
|
|
3468
|
+
return { ok: true, messages: body };
|
|
3469
|
+
} catch (error2) {
|
|
3470
|
+
console.error(
|
|
3471
|
+
`[pollSessionMessagesForRedrive] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3472
|
+
);
|
|
3473
|
+
return { ok: false, status: null, body: "", malformed: false };
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
2830
3476
|
function isSessionActivelyGenerating(messages) {
|
|
2831
3477
|
if (!messages || messages.length === 0) return false;
|
|
2832
3478
|
const last = messages[messages.length - 1];
|
|
@@ -2847,27 +3493,37 @@ function sessionLastActivityMs(session) {
|
|
|
2847
3493
|
}
|
|
2848
3494
|
return null;
|
|
2849
3495
|
}
|
|
2850
|
-
async function listSessions(port) {
|
|
3496
|
+
async function listSessions(port, client) {
|
|
3497
|
+
if (client?.version === "v2") return listV2Sessions(client);
|
|
2851
3498
|
try {
|
|
2852
|
-
const res = await
|
|
3499
|
+
const res = await requestWithClient(port, client, "/session");
|
|
2853
3500
|
if (!res.ok) return null;
|
|
2854
3501
|
const body = await res.json();
|
|
2855
3502
|
return Array.isArray(body) ? body : null;
|
|
2856
|
-
} catch {
|
|
3503
|
+
} catch (error2) {
|
|
3504
|
+
console.error(
|
|
3505
|
+
`[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3506
|
+
);
|
|
2857
3507
|
return null;
|
|
2858
3508
|
}
|
|
2859
3509
|
}
|
|
2860
|
-
async function deleteSession(port, id) {
|
|
3510
|
+
async function deleteSession(port, id, client) {
|
|
3511
|
+
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
2861
3512
|
try {
|
|
2862
|
-
const res = await
|
|
3513
|
+
const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
|
|
2863
3514
|
return res.status >= 200 && res.status < 300;
|
|
2864
|
-
} catch {
|
|
3515
|
+
} catch (error2) {
|
|
3516
|
+
console.error(
|
|
3517
|
+
`[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3518
|
+
);
|
|
2865
3519
|
return false;
|
|
2866
3520
|
}
|
|
2867
3521
|
}
|
|
2868
|
-
async function sessionExists(port, id) {
|
|
3522
|
+
async function sessionExists(port, id, client) {
|
|
2869
3523
|
try {
|
|
2870
|
-
const res = await
|
|
3524
|
+
const res = await requestWithClient(port, client, `/session/${id}`, void 0, {
|
|
3525
|
+
allowStatuses: [404]
|
|
3526
|
+
});
|
|
2871
3527
|
if (res.status >= 200 && res.status < 300) return true;
|
|
2872
3528
|
if (res.status === 404) return false;
|
|
2873
3529
|
return null;
|
|
@@ -2875,9 +3531,22 @@ async function sessionExists(port, id) {
|
|
|
2875
3531
|
return null;
|
|
2876
3532
|
}
|
|
2877
3533
|
}
|
|
2878
|
-
async function
|
|
3534
|
+
async function getOpenCodeSession(port, id, client) {
|
|
3535
|
+
try {
|
|
3536
|
+
const response = await requestWithClient(port, client, `/session/${id}`);
|
|
3537
|
+
const body = await response.json();
|
|
3538
|
+
return body && typeof body === "object" && !Array.isArray(body) ? body : null;
|
|
3539
|
+
} catch (error2) {
|
|
3540
|
+
console.error(
|
|
3541
|
+
`[getOpenCodeSession] GET /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3542
|
+
);
|
|
3543
|
+
return null;
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
async function getSessionStatuses(port, client) {
|
|
3547
|
+
if (client?.version === "v2") return null;
|
|
2879
3548
|
try {
|
|
2880
|
-
const res = await
|
|
3549
|
+
const res = await requestWithClient(port, client, "/session/status");
|
|
2881
3550
|
if (!res.ok) {
|
|
2882
3551
|
console.error(
|
|
2883
3552
|
`[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
|
|
@@ -2899,22 +3568,28 @@ async function getSessionStatuses(port) {
|
|
|
2899
3568
|
return null;
|
|
2900
3569
|
}
|
|
2901
3570
|
}
|
|
2902
|
-
async function isSessionOngoing(port, id) {
|
|
2903
|
-
|
|
3571
|
+
async function isSessionOngoing(port, id, client) {
|
|
3572
|
+
if (client?.version === "v2") return isV2SessionOngoing(client, id);
|
|
3573
|
+
const map = await getSessionStatuses(port, client);
|
|
2904
3574
|
if (map == null) return null;
|
|
2905
3575
|
const entry = map[id];
|
|
2906
3576
|
return entry != null && entry.type !== "idle";
|
|
2907
3577
|
}
|
|
2908
|
-
async function createOpenCodeSession(port, directory) {
|
|
2909
|
-
const
|
|
2910
|
-
if (directory && directory.trim())
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
3578
|
+
async function createOpenCodeSession(port, directory, client) {
|
|
3579
|
+
const path = new URL(`${opencodeBase(port)}/session`);
|
|
3580
|
+
if (directory && directory.trim()) path.searchParams.set("directory", directory.trim());
|
|
3581
|
+
const requestPath = `${path.pathname}${path.search}`;
|
|
3582
|
+
const response = await requestWithClient(
|
|
3583
|
+
port,
|
|
3584
|
+
client,
|
|
3585
|
+
requestPath,
|
|
3586
|
+
{
|
|
3587
|
+
method: "POST",
|
|
3588
|
+
headers: { "Content-Type": "application/json" },
|
|
3589
|
+
body: JSON.stringify({})
|
|
3590
|
+
},
|
|
3591
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
3592
|
+
);
|
|
2918
3593
|
if (!response.ok) {
|
|
2919
3594
|
const text = await response.text().catch(() => "");
|
|
2920
3595
|
throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ""}`);
|
|
@@ -2922,10 +3597,16 @@ async function createOpenCodeSession(port, directory) {
|
|
|
2922
3597
|
const data = await response.json();
|
|
2923
3598
|
return data.id;
|
|
2924
3599
|
}
|
|
2925
|
-
async function getModelAttachmentCapability(port, model) {
|
|
3600
|
+
async function getModelAttachmentCapability(port, model, client) {
|
|
2926
3601
|
const { model: baseModel } = splitModelVariant(model);
|
|
3602
|
+
if (client?.version === "v2") {
|
|
3603
|
+
console.error(
|
|
3604
|
+
`[getModelAttachmentCapability] V2 provider capabilities are unavailable; using text-only fallback (port ${port})`
|
|
3605
|
+
);
|
|
3606
|
+
return null;
|
|
3607
|
+
}
|
|
2927
3608
|
try {
|
|
2928
|
-
const res = await
|
|
3609
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
2929
3610
|
if (!res.ok) {
|
|
2930
3611
|
console.error(
|
|
2931
3612
|
`[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
@@ -3043,21 +3724,45 @@ function applyModelOptions(body, options) {
|
|
|
3043
3724
|
};
|
|
3044
3725
|
}
|
|
3045
3726
|
}
|
|
3046
|
-
if (variant) body.variant = variant;
|
|
3727
|
+
if (variant) body.variant = variant;
|
|
3728
|
+
}
|
|
3729
|
+
async function listOpenCodeQuestions(port, client) {
|
|
3730
|
+
try {
|
|
3731
|
+
const response = await requestWithClient(port, client, "/question");
|
|
3732
|
+
const body = await response.json();
|
|
3733
|
+
return Array.isArray(body) ? body : null;
|
|
3734
|
+
} catch (error2) {
|
|
3735
|
+
console.error(
|
|
3736
|
+
`[listOpenCodeQuestions] GET /question failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3737
|
+
);
|
|
3738
|
+
return null;
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
async function listOpenCodePermissions(port, client) {
|
|
3742
|
+
try {
|
|
3743
|
+
const response = await requestWithClient(port, client, "/permission");
|
|
3744
|
+
const body = await response.json();
|
|
3745
|
+
return Array.isArray(body) ? body : null;
|
|
3746
|
+
} catch (error2) {
|
|
3747
|
+
console.error(
|
|
3748
|
+
`[listOpenCodePermissions] GET /permission failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3749
|
+
);
|
|
3750
|
+
return null;
|
|
3751
|
+
}
|
|
3047
3752
|
}
|
|
3048
3753
|
function messageText(m) {
|
|
3049
3754
|
if (!m || !Array.isArray(m.parts)) return "";
|
|
3050
3755
|
return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
3051
3756
|
}
|
|
3052
|
-
async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
3053
|
-
const before = await getSessionMessages(port, sessionId);
|
|
3757
|
+
async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
|
|
3758
|
+
const before = await getSessionMessages(port, sessionId, client);
|
|
3054
3759
|
const knownUserIds = new Set(
|
|
3055
3760
|
(before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
|
|
3056
3761
|
);
|
|
3057
3762
|
const parts = [{ type: "text", text: content }];
|
|
3058
3763
|
let pendingOutcomes = null;
|
|
3059
3764
|
if (attachments && attachments.inputs.length > 0) {
|
|
3060
|
-
const capable = await getModelAttachmentCapability(port, options?.model);
|
|
3765
|
+
const capable = await getModelAttachmentCapability(port, options?.model, client);
|
|
3061
3766
|
const {
|
|
3062
3767
|
parts: fileParts,
|
|
3063
3768
|
outcomes,
|
|
@@ -3070,11 +3775,17 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
3070
3775
|
parts
|
|
3071
3776
|
};
|
|
3072
3777
|
applyModelOptions(body, options);
|
|
3073
|
-
const res = await
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3778
|
+
const res = await requestWithClient(
|
|
3779
|
+
port,
|
|
3780
|
+
client,
|
|
3781
|
+
`/session/${sessionId}/prompt_async`,
|
|
3782
|
+
{
|
|
3783
|
+
method: "POST",
|
|
3784
|
+
headers: { "Content-Type": "application/json" },
|
|
3785
|
+
body: JSON.stringify(body)
|
|
3786
|
+
},
|
|
3787
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
3788
|
+
);
|
|
3078
3789
|
if (res.status < 200 || res.status >= 300) {
|
|
3079
3790
|
const text = await res.text().catch(() => "");
|
|
3080
3791
|
const { variant } = splitModelVariant(options?.model);
|
|
@@ -3085,7 +3796,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
3085
3796
|
const READ_BACK_ATTEMPTS = 5;
|
|
3086
3797
|
const READ_BACK_DELAY_MS = 150;
|
|
3087
3798
|
for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
|
|
3088
|
-
const after = await getSessionMessages(port, sessionId);
|
|
3799
|
+
const after = await getSessionMessages(port, sessionId, client);
|
|
3089
3800
|
if (after) {
|
|
3090
3801
|
let best = null;
|
|
3091
3802
|
for (const m of after) {
|
|
@@ -3188,7 +3899,7 @@ function collectSubagentSessions(messages, userMessageId) {
|
|
|
3188
3899
|
}
|
|
3189
3900
|
return refs;
|
|
3190
3901
|
}
|
|
3191
|
-
function
|
|
3902
|
+
function finiteNumber2(value) {
|
|
3192
3903
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3193
3904
|
}
|
|
3194
3905
|
function taskCallModel(value) {
|
|
@@ -3217,8 +3928,8 @@ function collectTaskCalls(messages, userMessageId) {
|
|
|
3217
3928
|
parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
|
|
3218
3929
|
model: taskCallModel(metadata?.model),
|
|
3219
3930
|
status: part.state.status ?? "unknown",
|
|
3220
|
-
timeStart:
|
|
3221
|
-
timeEnd:
|
|
3931
|
+
timeStart: finiteNumber2(part.state.time?.start),
|
|
3932
|
+
timeEnd: finiteNumber2(part.state.time?.end)
|
|
3222
3933
|
});
|
|
3223
3934
|
}
|
|
3224
3935
|
}
|
|
@@ -3233,7 +3944,7 @@ function attributeTaskCallUsage(messages, windows) {
|
|
|
3233
3944
|
const unattributed = [];
|
|
3234
3945
|
for (const message of messages ?? []) {
|
|
3235
3946
|
if (roleOf(message) !== "assistant") continue;
|
|
3236
|
-
const created =
|
|
3947
|
+
const created = finiteNumber2(createdOf(message));
|
|
3237
3948
|
const matching = created === null ? [] : eligibleWindows.filter(
|
|
3238
3949
|
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
3239
3950
|
);
|
|
@@ -3474,9 +4185,51 @@ function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageI
|
|
|
3474
4185
|
}
|
|
3475
4186
|
return hasLaterUser && hasStartedLaterUser;
|
|
3476
4187
|
}
|
|
3477
|
-
async function hasAnyConfiguredProvider(port) {
|
|
4188
|
+
async function hasAnyConfiguredProvider(port, client) {
|
|
4189
|
+
if (client?.version === "v2") {
|
|
4190
|
+
const directory = await getOpenCodeDirectoryV2(client);
|
|
4191
|
+
if (!directory) {
|
|
4192
|
+
console.error(
|
|
4193
|
+
`[hasAnyConfiguredProvider] V2 working directory was unavailable (port ${port})`
|
|
4194
|
+
);
|
|
4195
|
+
return null;
|
|
4196
|
+
}
|
|
4197
|
+
const path = `/api/integration?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
|
|
4198
|
+
try {
|
|
4199
|
+
const res = await client.request(path);
|
|
4200
|
+
if (!res.ok) {
|
|
4201
|
+
console.error(
|
|
4202
|
+
`[hasAnyConfiguredProvider] GET ${path} returned HTTP ${res.status} (port ${port})`
|
|
4203
|
+
);
|
|
4204
|
+
return null;
|
|
4205
|
+
}
|
|
4206
|
+
const body = await res.json();
|
|
4207
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
|
|
4208
|
+
console.error(
|
|
4209
|
+
`[hasAnyConfiguredProvider] GET ${path} body had no integration data array (port ${port})`
|
|
4210
|
+
);
|
|
4211
|
+
return null;
|
|
4212
|
+
}
|
|
4213
|
+
for (const integration of body.data) {
|
|
4214
|
+
if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
4215
|
+
console.error(
|
|
4216
|
+
`[hasAnyConfiguredProvider] GET ${path} body contained an invalid integration (port ${port})`
|
|
4217
|
+
);
|
|
4218
|
+
return null;
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
return body.data.some(
|
|
4222
|
+
(integration) => Array.isArray(integration.connections) && integration.connections.length > 0
|
|
4223
|
+
);
|
|
4224
|
+
} catch (error2) {
|
|
4225
|
+
console.error(
|
|
4226
|
+
`[hasAnyConfiguredProvider] GET ${path} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4227
|
+
);
|
|
4228
|
+
return null;
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
3478
4231
|
try {
|
|
3479
|
-
const res = await
|
|
4232
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
3480
4233
|
if (!res.ok) {
|
|
3481
4234
|
console.error(
|
|
3482
4235
|
`[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
@@ -3505,7 +4258,7 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3505
4258
|
return null;
|
|
3506
4259
|
}
|
|
3507
4260
|
}
|
|
3508
|
-
function
|
|
4261
|
+
function sessionErrorReason2(error2) {
|
|
3509
4262
|
const record = typeof error2 === "object" && error2 !== null ? error2 : null;
|
|
3510
4263
|
const data = record?.data;
|
|
3511
4264
|
const dataRecord = typeof data === "object" && data !== null ? data : null;
|
|
@@ -3535,16 +4288,20 @@ function parseSessionErrorFrame(data) {
|
|
|
3535
4288
|
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
3536
4289
|
return {
|
|
3537
4290
|
sessionId,
|
|
3538
|
-
reason:
|
|
4291
|
+
reason: sessionErrorReason2(propertiesRecord.error)
|
|
3539
4292
|
};
|
|
3540
4293
|
}
|
|
3541
|
-
async function readSessionErrorStream(port, options) {
|
|
4294
|
+
async function readSessionErrorStream(port, options, client) {
|
|
4295
|
+
if (client?.version === "v2") return readV2SessionErrorStream(client, options);
|
|
3542
4296
|
let reader = null;
|
|
3543
4297
|
try {
|
|
3544
|
-
const response = await
|
|
4298
|
+
const response = await (client?.request("/event", {
|
|
3545
4299
|
headers: { accept: "text/event-stream" },
|
|
3546
4300
|
signal: options.signal
|
|
3547
|
-
})
|
|
4301
|
+
}) ?? fetch(`${opencodeBase(port)}/event`, {
|
|
4302
|
+
headers: { accept: "text/event-stream" },
|
|
4303
|
+
signal: options.signal
|
|
4304
|
+
}));
|
|
3548
4305
|
if (!response.ok || !response.body) {
|
|
3549
4306
|
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
3550
4307
|
}
|
|
@@ -3575,9 +4332,10 @@ async function readSessionErrorStream(port, options) {
|
|
|
3575
4332
|
if (reader) void reader.cancel().catch(() => void 0);
|
|
3576
4333
|
}
|
|
3577
4334
|
}
|
|
3578
|
-
async function reloadProviderCache(port) {
|
|
4335
|
+
async function reloadProviderCache(port, client) {
|
|
4336
|
+
if (client?.version === "v2") return;
|
|
3579
4337
|
try {
|
|
3580
|
-
const res = await
|
|
4338
|
+
const res = await requestWithClient(port, client, "/config", {
|
|
3581
4339
|
method: "PATCH",
|
|
3582
4340
|
headers: { "Content-Type": "application/json" },
|
|
3583
4341
|
body: JSON.stringify({})
|
|
@@ -3955,10 +4713,11 @@ var STRIP_RES = /* @__PURE__ */ new Set([
|
|
|
3955
4713
|
"content-length"
|
|
3956
4714
|
]);
|
|
3957
4715
|
var StreamForwarder = class {
|
|
3958
|
-
constructor(ws, port, callbacks = {}) {
|
|
4716
|
+
constructor(ws, port, callbacks = {}, options = {}) {
|
|
3959
4717
|
this.ws = ws;
|
|
3960
4718
|
this.port = port;
|
|
3961
4719
|
this.callbacks = callbacks;
|
|
4720
|
+
this.options = options;
|
|
3962
4721
|
}
|
|
3963
4722
|
inflight = /* @__PURE__ */ new Map();
|
|
3964
4723
|
/**
|
|
@@ -4042,7 +4801,15 @@ var StreamForwarder = class {
|
|
|
4042
4801
|
}
|
|
4043
4802
|
const fwdHeaders = {};
|
|
4044
4803
|
for (const [k, v] of Object.entries(headers ?? {})) {
|
|
4045
|
-
|
|
4804
|
+
const lower = k.toLowerCase();
|
|
4805
|
+
if (STRIP_REQ.has(lower)) continue;
|
|
4806
|
+
if (this.options.openCodePassword !== void 0 && this.options.openCodePassword !== null) {
|
|
4807
|
+
if (lower === "authorization") continue;
|
|
4808
|
+
}
|
|
4809
|
+
fwdHeaders[k] = v;
|
|
4810
|
+
}
|
|
4811
|
+
if (this.options.openCodePassword !== void 0 && this.options.openCodePassword !== null) {
|
|
4812
|
+
fwdHeaders.Authorization = buildOpenCodeBasicAuthHeader(this.options.openCodePassword);
|
|
4046
4813
|
}
|
|
4047
4814
|
this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });
|
|
4048
4815
|
const body = bodyPromise ? await bodyPromise : void 0;
|
|
@@ -4155,6 +4922,7 @@ function connectTunnel(options) {
|
|
|
4155
4922
|
agentId,
|
|
4156
4923
|
authHeader,
|
|
4157
4924
|
port,
|
|
4925
|
+
openCodePassword,
|
|
4158
4926
|
onConnected,
|
|
4159
4927
|
onDisconnected,
|
|
4160
4928
|
onError,
|
|
@@ -4172,11 +4940,16 @@ function connectTunnel(options) {
|
|
|
4172
4940
|
Authorization: authHeader
|
|
4173
4941
|
}
|
|
4174
4942
|
});
|
|
4175
|
-
const forwarder = new StreamForwarder(
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4943
|
+
const forwarder = new StreamForwarder(
|
|
4944
|
+
ws,
|
|
4945
|
+
port,
|
|
4946
|
+
{
|
|
4947
|
+
onHead: () => onResponse?.(),
|
|
4948
|
+
onDrainPing: () => onDrainPing?.(),
|
|
4949
|
+
onUsageRearmPing: () => onUsageRearmPing?.()
|
|
4950
|
+
},
|
|
4951
|
+
{ openCodePassword }
|
|
4952
|
+
);
|
|
4180
4953
|
const connectionTimeout = setTimeout(() => {
|
|
4181
4954
|
ws.close();
|
|
4182
4955
|
reject(new Error("Connection timeout"));
|
|
@@ -4319,6 +5092,7 @@ var RunnerConnection = class {
|
|
|
4319
5092
|
agentId: this.resolvedAgentId,
|
|
4320
5093
|
authHeader: this.opts.getAuthHeader(),
|
|
4321
5094
|
port: this.opts.port,
|
|
5095
|
+
openCodePassword: this.opts.openCodePassword,
|
|
4322
5096
|
onConnected: (agentId) => {
|
|
4323
5097
|
this.reconnectAttempt = 0;
|
|
4324
5098
|
this.reconnecting = false;
|
|
@@ -4499,33 +5273,73 @@ function parseCodexUsageHeaders(headers) {
|
|
|
4499
5273
|
function normalizeProbeModel(model) {
|
|
4500
5274
|
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
4501
5275
|
}
|
|
4502
|
-
|
|
5276
|
+
function isRecord2(value) {
|
|
5277
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5278
|
+
}
|
|
5279
|
+
function unsupportedProbeModels(reason, port) {
|
|
5280
|
+
console.error(`[resolveProbeModels] ${reason} (port ${port})`);
|
|
5281
|
+
return { status: "unsupported", reason };
|
|
5282
|
+
}
|
|
5283
|
+
async function resolveV1ProbeModels(client, port) {
|
|
4503
5284
|
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 [];
|
|
5285
|
+
const response = await client.request("/config/providers");
|
|
5286
|
+
const body = await response.json();
|
|
5287
|
+
if (!isRecord2(body) || !Array.isArray(body.providers)) {
|
|
5288
|
+
return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
|
|
4513
5289
|
}
|
|
4514
|
-
const
|
|
4515
|
-
|
|
4516
|
-
|
|
5290
|
+
const provider = body.providers.find(
|
|
5291
|
+
(candidate) => isRecord2(candidate) && candidate.id === "openai"
|
|
5292
|
+
);
|
|
5293
|
+
if (!provider || !isRecord2(provider.models)) return { status: "supported", models: [] };
|
|
5294
|
+
const defaults2 = isRecord2(body.default) ? body.default : void 0;
|
|
4517
5295
|
const candidates = [
|
|
4518
|
-
...typeof
|
|
5296
|
+
...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
|
|
4519
5297
|
...Object.keys(provider.models)
|
|
4520
5298
|
].map(normalizeProbeModel);
|
|
4521
|
-
return [...new Set(candidates)].slice(0, 4);
|
|
5299
|
+
return { status: "supported", models: [...new Set(candidates)].slice(0, 4) };
|
|
4522
5300
|
} catch (err) {
|
|
4523
|
-
|
|
4524
|
-
`
|
|
5301
|
+
return unsupportedProbeModels(
|
|
5302
|
+
`V1 GET /config/providers failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
5303
|
+
port
|
|
5304
|
+
);
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
async function resolveV2ProbeModels(client, port) {
|
|
5308
|
+
const directory = await getOpenCodeDirectoryV2(client);
|
|
5309
|
+
if (!directory) {
|
|
5310
|
+
return unsupportedProbeModels("V2 working directory could not be verified", port);
|
|
5311
|
+
}
|
|
5312
|
+
const path = `/api/provider?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
|
|
5313
|
+
try {
|
|
5314
|
+
const response = await client.request(path);
|
|
5315
|
+
const body = await response.json();
|
|
5316
|
+
if (!isRecord2(body) || !Array.isArray(body.data)) {
|
|
5317
|
+
return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
|
|
5318
|
+
}
|
|
5319
|
+
const provider = body.data.find(
|
|
5320
|
+
(candidate) => isRecord2(candidate) && candidate.id === "openai"
|
|
5321
|
+
);
|
|
5322
|
+
if (!provider) return { status: "supported", models: [] };
|
|
5323
|
+
if (!isRecord2(provider.models)) {
|
|
5324
|
+
return unsupportedProbeModels(
|
|
5325
|
+
"V2 provider response has no safe OpenAI model catalogue",
|
|
5326
|
+
port
|
|
5327
|
+
);
|
|
5328
|
+
}
|
|
5329
|
+
return {
|
|
5330
|
+
status: "supported",
|
|
5331
|
+
models: [...new Set(Object.keys(provider.models).map(normalizeProbeModel))].slice(0, 4)
|
|
5332
|
+
};
|
|
5333
|
+
} catch (err) {
|
|
5334
|
+
return unsupportedProbeModels(
|
|
5335
|
+
`V2 GET ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
5336
|
+
port
|
|
4525
5337
|
);
|
|
4526
|
-
return [];
|
|
4527
5338
|
}
|
|
4528
5339
|
}
|
|
5340
|
+
async function resolveProbeModels(port, client = createOpenCodeClient({ port, version: "v1" })) {
|
|
5341
|
+
return client.version === "v2" ? resolveV2ProbeModels(client, port) : resolveV1ProbeModels(client, port);
|
|
5342
|
+
}
|
|
4529
5343
|
function hasPrimaryHeaders(headers) {
|
|
4530
5344
|
return [
|
|
4531
5345
|
"x-codex-primary-used-percent",
|
|
@@ -4533,7 +5347,7 @@ function hasPrimaryHeaders(headers) {
|
|
|
4533
5347
|
"x-codex-primary-reset-at"
|
|
4534
5348
|
].some((name) => headers.has(name));
|
|
4535
5349
|
}
|
|
4536
|
-
async function getOpenAiUsage(port) {
|
|
5350
|
+
async function getOpenAiUsage(port, client) {
|
|
4537
5351
|
const credentials2 = readOpenCodeChatGptCredentials();
|
|
4538
5352
|
if (!credentials2) {
|
|
4539
5353
|
throw new OpenAiUsageError(
|
|
@@ -4548,12 +5362,16 @@ async function getOpenAiUsage(port) {
|
|
|
4548
5362
|
);
|
|
4549
5363
|
}
|
|
4550
5364
|
const subscription = parseChatGptIdentity(credentials2.accessToken);
|
|
4551
|
-
const
|
|
4552
|
-
if (models.length === 0) {
|
|
4553
|
-
|
|
5365
|
+
const lookup = await resolveProbeModels(port, client);
|
|
5366
|
+
if (lookup.status === "unsupported" || lookup.models.length === 0) {
|
|
5367
|
+
const detail = lookup.status === "unsupported" ? ` ${lookup.reason}.` : "";
|
|
5368
|
+
throw new OpenAiUsageError(
|
|
5369
|
+
`No supported OpenAI probe model is available.${detail}`,
|
|
5370
|
+
"no_probe_model"
|
|
5371
|
+
);
|
|
4554
5372
|
}
|
|
4555
5373
|
let lastStatus;
|
|
4556
|
-
for (const model of models) {
|
|
5374
|
+
for (const model of lookup.models) {
|
|
4557
5375
|
let res;
|
|
4558
5376
|
try {
|
|
4559
5377
|
res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
|
|
@@ -5372,6 +6190,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5372
6190
|
maxActiveSessions;
|
|
5373
6191
|
watcherStallMs;
|
|
5374
6192
|
wedgeWarningIntervalMs;
|
|
6193
|
+
openCodeClient;
|
|
5375
6194
|
/** Cache of conversationId → opencode sessionId. */
|
|
5376
6195
|
sessions = /* @__PURE__ */ new Map();
|
|
5377
6196
|
/**
|
|
@@ -5480,20 +6299,23 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5480
6299
|
*/
|
|
5481
6300
|
readopted = /* @__PURE__ */ new Set();
|
|
5482
6301
|
/**
|
|
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.
|
|
6302
|
+
* Readopt give-up fence. Set when recovery declines to start or continue a turn
|
|
6303
|
+
* for a row that is still `processing`, so the next drain does not re-dispatch or
|
|
6304
|
+
* re-attach it before the cron safety net acts. It suppresses only non-done
|
|
6305
|
+
* recovery paths; DONE delivery still runs. Clear it when
|
|
6306
|
+
* `!stillProcessing.has(id)`, because leaving `processing` hands the row back to
|
|
6307
|
+
* normal processing.
|
|
5495
6308
|
*/
|
|
5496
6309
|
dontRedispatch = /* @__PURE__ */ new Set();
|
|
6310
|
+
/**
|
|
6311
|
+
* Untrackable-ack fence. Set after OpenCode accepts a prompt without returning a
|
|
6312
|
+
* usable message id, because another POST could create a duplicate turn. Keep it
|
|
6313
|
+
* fenced while the row is `processing` or `pending`; clear it only when the row
|
|
6314
|
+
* is absent from both lists.
|
|
6315
|
+
*/
|
|
6316
|
+
untrackableAck = /* @__PURE__ */ new Set();
|
|
6317
|
+
/** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
|
|
6318
|
+
pendingMessageIds = /* @__PURE__ */ new Set();
|
|
5497
6319
|
/**
|
|
5498
6320
|
* "markDone for this row is TERMINALLY undeliverable" (Bug 4). Set ONLY when a
|
|
5499
6321
|
* re-adopted DONE row's `markDone` returned a terminal 4xx (a status that will
|
|
@@ -5616,7 +6438,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5616
6438
|
*/
|
|
5617
6439
|
attachmentsSkippedSignalled = /* @__PURE__ */ new Set();
|
|
5618
6440
|
/**
|
|
5619
|
-
* Cache of the opencode root directory
|
|
6441
|
+
* Cache of the opencode root directory from the selected client's location lookup.
|
|
6442
|
+
* Resolved lazily on
|
|
5620
6443
|
* first session creation so drain-created sessions are rooted at the project
|
|
5621
6444
|
* directory and thus visible in `opencode web`'s session list. `undefined` =
|
|
5622
6445
|
* not yet resolved; `null` = resolved-but-unavailable (don't keep retrying).
|
|
@@ -5710,6 +6533,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5710
6533
|
config.fetchImpl ?? fetch,
|
|
5711
6534
|
config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
|
|
5712
6535
|
);
|
|
6536
|
+
this.openCodeClient = config.openCodeClient ?? createOpenCodeClient({
|
|
6537
|
+
port: config.port,
|
|
6538
|
+
version: "v1",
|
|
6539
|
+
fetchImpl: config.fetchImpl
|
|
6540
|
+
});
|
|
5713
6541
|
this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5714
6542
|
this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
|
|
5715
6543
|
this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
|
|
@@ -5721,9 +6549,39 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5721
6549
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
5722
6550
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
5723
6551
|
}
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
6552
|
+
get isV2() {
|
|
6553
|
+
return this.openCodeClient.version === "v2";
|
|
6554
|
+
}
|
|
6555
|
+
async getSessionMessages(sessionId) {
|
|
6556
|
+
return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6557
|
+
}
|
|
6558
|
+
async getSubagentSessionMessages(sessionId) {
|
|
6559
|
+
return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : getSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6560
|
+
}
|
|
6561
|
+
async getTelemetrySubagentSessionMessages(sessionId) {
|
|
6562
|
+
if (this.isV2) return getV2SessionMessages(this.openCodeClient, sessionId);
|
|
6563
|
+
return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6564
|
+
}
|
|
6565
|
+
async listSessions() {
|
|
6566
|
+
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient);
|
|
6567
|
+
}
|
|
6568
|
+
async sessionExists(sessionId) {
|
|
6569
|
+
return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
|
|
6570
|
+
}
|
|
6571
|
+
async isSessionOngoing(sessionId) {
|
|
6572
|
+
return this.isV2 ? isV2SessionOngoing(this.openCodeClient, sessionId) : isSessionOngoing(this.port, sessionId, this.openCodeClient);
|
|
6573
|
+
}
|
|
6574
|
+
async getOpenCodeDirectory() {
|
|
6575
|
+
return this.isV2 ? getOpenCodeDirectoryV2(this.openCodeClient) : getOpenCodeDirectory(this.port, this.openCodeClient);
|
|
6576
|
+
}
|
|
6577
|
+
async createOpenCodeSession(directory) {
|
|
6578
|
+
return this.isV2 ? createV2Session(this.openCodeClient, directory) : createOpenCodeSession(this.port, directory, this.openCodeClient);
|
|
6579
|
+
}
|
|
6580
|
+
async hasAnyConfiguredProvider() {
|
|
6581
|
+
return hasAnyConfiguredProvider(this.port, this.openCodeClient);
|
|
6582
|
+
}
|
|
6583
|
+
async readOpenCodeSessionErrorStream(options) {
|
|
6584
|
+
return this.isV2 ? readV2SessionErrorStream(this.openCodeClient, options) : readSessionErrorStream(this.port, options, this.openCodeClient);
|
|
5727
6585
|
}
|
|
5728
6586
|
/**
|
|
5729
6587
|
* Drain all pending channel conversations once: poll → dispatch → register.
|
|
@@ -5801,6 +6659,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5801
6659
|
async runDrain() {
|
|
5802
6660
|
let dispatched = 0;
|
|
5803
6661
|
try {
|
|
6662
|
+
this.pendingMessageIds.clear();
|
|
5804
6663
|
const conversations = await this.getPendingConversations();
|
|
5805
6664
|
if (this.recycleRequestedFlag) {
|
|
5806
6665
|
this.stop();
|
|
@@ -6025,6 +6884,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6025
6884
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
6026
6885
|
this.ensureSessionErrorStream();
|
|
6027
6886
|
const messages = await this.getPendingMessages(conv.id);
|
|
6887
|
+
for (const message of messages) this.pendingMessageIds.add(message.id);
|
|
6028
6888
|
let dispatched = 0;
|
|
6029
6889
|
let skippedAlreadyDispatched = 0;
|
|
6030
6890
|
if (refusedSessionId && messages.length > 0) {
|
|
@@ -6038,6 +6898,15 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6038
6898
|
skippedAlreadyDispatched += 1;
|
|
6039
6899
|
continue;
|
|
6040
6900
|
}
|
|
6901
|
+
if (this.untrackableAck.has(message.id)) {
|
|
6902
|
+
this.log({
|
|
6903
|
+
level: "warn",
|
|
6904
|
+
message: `Message ${message.id.slice(0, 8)} is fenced after an untrackable OpenCode turn \u2014 skipping re-dispatch`,
|
|
6905
|
+
conversation_id: conv.id,
|
|
6906
|
+
message_id: message.id
|
|
6907
|
+
});
|
|
6908
|
+
break;
|
|
6909
|
+
}
|
|
6041
6910
|
const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
|
|
6042
6911
|
if (effectiveOpencodeMessageId) {
|
|
6043
6912
|
const outcome = await this.resolveRedrive(
|
|
@@ -6066,15 +6935,55 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6066
6935
|
conversation_id: conv.id,
|
|
6067
6936
|
message_id: message.id
|
|
6068
6937
|
});
|
|
6069
|
-
const sendAttachments = this.buildSendAttachments(conv, message);
|
|
6070
|
-
|
|
6938
|
+
const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(conv, message);
|
|
6939
|
+
if (this.isV2 && message.attachments && message.attachments.length > 0) {
|
|
6940
|
+
this.signalAttachmentsSkipped(
|
|
6941
|
+
conv.id,
|
|
6942
|
+
message.id,
|
|
6943
|
+
message.attachments.map((attachment, index) => ({
|
|
6944
|
+
index,
|
|
6945
|
+
mime: attachment.mime,
|
|
6946
|
+
...attachment.filename ? { filename: attachment.filename } : {},
|
|
6947
|
+
status: "skipped"
|
|
6948
|
+
})),
|
|
6949
|
+
false
|
|
6950
|
+
);
|
|
6951
|
+
}
|
|
6952
|
+
opencodeMessageId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, message.content) : await this.dispatchLocked(
|
|
6071
6953
|
sessionId,
|
|
6072
|
-
() => sendPromptAsync(
|
|
6954
|
+
() => sendPromptAsync(
|
|
6955
|
+
this.port,
|
|
6956
|
+
sessionId,
|
|
6957
|
+
message.content,
|
|
6958
|
+
options,
|
|
6959
|
+
sendAttachments,
|
|
6960
|
+
this.openCodeClient
|
|
6961
|
+
)
|
|
6073
6962
|
);
|
|
6074
6963
|
} catch (err) {
|
|
6075
6964
|
if (err instanceof ChannelAuthError) throw err;
|
|
6965
|
+
if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
|
|
6966
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
6967
|
+
this.untrackableAck.add(message.id);
|
|
6968
|
+
this.log({
|
|
6969
|
+
level: "error",
|
|
6970
|
+
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
|
|
6971
|
+
conversation_id: conv.id,
|
|
6972
|
+
message_id: message.id
|
|
6973
|
+
});
|
|
6974
|
+
await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
|
|
6975
|
+
this.log({
|
|
6976
|
+
level: "warn",
|
|
6977
|
+
message: `markFailed PATCH for V2 dispatch failure on message ${message.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
6978
|
+
conversation_id: conv.id,
|
|
6979
|
+
message_id: message.id
|
|
6980
|
+
});
|
|
6981
|
+
void this.postSignal(conv.id, message.id, "ack_untrackable");
|
|
6982
|
+
});
|
|
6983
|
+
break;
|
|
6984
|
+
}
|
|
6076
6985
|
this.dispatched.delete(message.id);
|
|
6077
|
-
const exists = await sessionExists(
|
|
6986
|
+
const exists = await this.sessionExists(sessionId);
|
|
6078
6987
|
if (exists === false) {
|
|
6079
6988
|
this.sessions.delete(conv.id);
|
|
6080
6989
|
this.log({
|
|
@@ -6123,6 +7032,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6123
7032
|
break;
|
|
6124
7033
|
}
|
|
6125
7034
|
if (opencodeMessageId === null) {
|
|
7035
|
+
if (this.isV2) {
|
|
7036
|
+
throw new Error("V2 prompt dispatch completed without an acknowledged message id");
|
|
7037
|
+
}
|
|
6126
7038
|
const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
|
|
6127
7039
|
if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
6128
7040
|
this.log({
|
|
@@ -6270,29 +7182,38 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6270
7182
|
*/
|
|
6271
7183
|
async pollSessionMessagesForRedrive(conv, message, sessionId) {
|
|
6272
7184
|
try {
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
7185
|
+
if (this.isV2) {
|
|
7186
|
+
const messages = await this.getSessionMessages(sessionId);
|
|
7187
|
+
if (messages === null) {
|
|
7188
|
+
this.log({
|
|
7189
|
+
level: "warn",
|
|
7190
|
+
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`,
|
|
7191
|
+
conversation_id: conv.id,
|
|
7192
|
+
message_id: message.id
|
|
7193
|
+
});
|
|
7194
|
+
return { ok: false, signature: null };
|
|
7195
|
+
}
|
|
7196
|
+
return { ok: true, messages };
|
|
6284
7197
|
}
|
|
6285
|
-
const
|
|
6286
|
-
|
|
7198
|
+
const polledV1 = await pollSessionMessagesForRedrive(
|
|
7199
|
+
this.port,
|
|
7200
|
+
sessionId,
|
|
7201
|
+
this.openCodeClient
|
|
7202
|
+
);
|
|
7203
|
+
if (!polledV1.ok) {
|
|
7204
|
+
const normalized = normalizeRedrivePollFailureBody(polledV1.body);
|
|
6287
7205
|
this.log({
|
|
6288
7206
|
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`,
|
|
7207
|
+
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
7208
|
conversation_id: conv.id,
|
|
6291
7209
|
message_id: message.id
|
|
6292
7210
|
});
|
|
6293
|
-
return {
|
|
7211
|
+
return {
|
|
7212
|
+
ok: false,
|
|
7213
|
+
signature: polledV1.status === null && !polledV1.malformed ? null : polledV1.malformed ? "non-array message body" : `HTTP ${polledV1.status}${normalized ? `: ${normalized}` : ""}`
|
|
7214
|
+
};
|
|
6294
7215
|
}
|
|
6295
|
-
return { ok: true, messages:
|
|
7216
|
+
return { ok: true, messages: polledV1.messages };
|
|
6296
7217
|
} catch (err) {
|
|
6297
7218
|
this.log({
|
|
6298
7219
|
level: "warn",
|
|
@@ -6351,11 +7272,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6351
7272
|
}
|
|
6352
7273
|
const state = messageRunState(messages, ocId ?? "");
|
|
6353
7274
|
if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
|
|
6354
|
-
const ongoing = await isSessionOngoing(
|
|
7275
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
6355
7276
|
if (ongoing === false) {
|
|
6356
7277
|
this.log({
|
|
6357
7278
|
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
|
|
7279
|
+
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
7280
|
conversation_id: conv.id,
|
|
6360
7281
|
message_id: message.id
|
|
6361
7282
|
});
|
|
@@ -6368,7 +7289,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6368
7289
|
return this.settleRedrive(conv, sessionId, message, ocId, messages, state);
|
|
6369
7290
|
}
|
|
6370
7291
|
if (state === "running" || state === "queued") {
|
|
6371
|
-
const ongoing = await isSessionOngoing(
|
|
7292
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
6372
7293
|
if (ongoing === true) {
|
|
6373
7294
|
if (state === "queued") {
|
|
6374
7295
|
const siblingOcIds = this.siblingOpencodeMessageIds(
|
|
@@ -6828,7 +7749,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6828
7749
|
};
|
|
6829
7750
|
}
|
|
6830
7751
|
if (bound) {
|
|
6831
|
-
const exists = await sessionExists(
|
|
7752
|
+
const exists = await this.sessionExists(bound);
|
|
6832
7753
|
if (exists === false) {
|
|
6833
7754
|
this.log({
|
|
6834
7755
|
level: "debug",
|
|
@@ -6861,7 +7782,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6861
7782
|
*/
|
|
6862
7783
|
async createAndBindSession(conversationId) {
|
|
6863
7784
|
const directory = await this.resolveOpenCodeDirectory();
|
|
6864
|
-
const sessionId = await createOpenCodeSession(
|
|
7785
|
+
const sessionId = await this.createOpenCodeSession(directory);
|
|
6865
7786
|
this.sessions.set(conversationId, sessionId);
|
|
6866
7787
|
await this.persistSession(conversationId, sessionId).catch((err) => {
|
|
6867
7788
|
this.log({
|
|
@@ -6873,17 +7794,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6873
7794
|
return sessionId;
|
|
6874
7795
|
}
|
|
6875
7796
|
/**
|
|
6876
|
-
* Lazily resolve (and cache) opencode's root directory via
|
|
7797
|
+
* Lazily resolve (and cache) opencode's root directory via the selected client's
|
|
7798
|
+
* location lookup.
|
|
6877
7799
|
* Resolved once per driver: `undefined` until first lookup, then the directory
|
|
6878
|
-
* string or `null` if unavailable (we don't keep retrying a
|
|
7800
|
+
* string or `null` if unavailable (we don't keep retrying a failed lookup).
|
|
6879
7801
|
*/
|
|
6880
7802
|
async resolveOpenCodeDirectory() {
|
|
6881
7803
|
if (this.opencodeDirectory !== void 0) return this.opencodeDirectory;
|
|
6882
|
-
this.opencodeDirectory = await getOpenCodeDirectory(
|
|
7804
|
+
this.opencodeDirectory = await this.getOpenCodeDirectory();
|
|
6883
7805
|
if (!this.opencodeDirectory) {
|
|
6884
7806
|
this.log({
|
|
6885
7807
|
level: "warn",
|
|
6886
|
-
message: "Could not determine opencode directory (
|
|
7808
|
+
message: "Could not determine opencode directory (location lookup failed) \u2014 new sessions may not appear in opencode web"
|
|
6887
7809
|
});
|
|
6888
7810
|
}
|
|
6889
7811
|
return this.opencodeDirectory;
|
|
@@ -7317,7 +8239,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7317
8239
|
while (!this.stopped && !signal.aborted) {
|
|
7318
8240
|
const openedAt = this.now();
|
|
7319
8241
|
try {
|
|
7320
|
-
const outcome = await
|
|
8242
|
+
const outcome = await this.readOpenCodeSessionErrorStream({
|
|
7321
8243
|
signal,
|
|
7322
8244
|
onSessionError: (event) => this.handleSessionError(event)
|
|
7323
8245
|
});
|
|
@@ -7408,7 +8330,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7408
8330
|
}
|
|
7409
8331
|
async failFromSessionError(watcher, event, inFlight) {
|
|
7410
8332
|
try {
|
|
7411
|
-
const messages = await getSessionMessages(
|
|
8333
|
+
const messages = await this.getSessionMessages(event.sessionId);
|
|
7412
8334
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7413
8335
|
if (state !== "queued") {
|
|
7414
8336
|
this.log({
|
|
@@ -7460,7 +8382,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7460
8382
|
* markDone (done) exactly once per transition;
|
|
7461
8383
|
* 2. applies the idle-path re-dispatch guard (a dispatched message that never
|
|
7462
8384
|
* APPEARS → re-dispatch — D1 obligation 2);
|
|
7463
|
-
* 3. polls `/question` + `/permission
|
|
8385
|
+
* 3. polls V1's global `/question` + `/permission`, or V2's
|
|
8386
|
+
* `/api/session/:id/form` + `/api/session/:id/permission`, and surfaces
|
|
7464
8387
|
* NEW ones via `reportInteraction`, carrying the PAUSED message's own
|
|
7465
8388
|
* `source_message_id`;
|
|
7466
8389
|
* 4. drops messages that completed or timed out from the in-flight set.
|
|
@@ -7486,11 +8409,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7486
8409
|
if (watcher.generation !== generation) return;
|
|
7487
8410
|
let messages = null;
|
|
7488
8411
|
try {
|
|
7489
|
-
|
|
7490
|
-
if (res.ok) {
|
|
7491
|
-
const body = await res.json();
|
|
7492
|
-
messages = Array.isArray(body) ? body : null;
|
|
7493
|
-
}
|
|
8412
|
+
messages = await this.getSessionMessages(sessionId);
|
|
7494
8413
|
} catch {
|
|
7495
8414
|
}
|
|
7496
8415
|
if (messages != null && messages.length > 0) {
|
|
@@ -7723,7 +8642,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7723
8642
|
inFlight.b2LastDescendantCheckMs = this.now();
|
|
7724
8643
|
const [descendantOngoing, rootOngoing] = await Promise.all([
|
|
7725
8644
|
this.isAnyDescendantSessionOngoing(sessionId),
|
|
7726
|
-
isSessionOngoing(
|
|
8645
|
+
this.isSessionOngoing(sessionId)
|
|
7727
8646
|
]);
|
|
7728
8647
|
if (isB2AbandonmentConfirmed({
|
|
7729
8648
|
pinnedForMs,
|
|
@@ -7783,7 +8702,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7783
8702
|
});
|
|
7784
8703
|
}
|
|
7785
8704
|
const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;
|
|
7786
|
-
const ongoing = await isSessionOngoing(
|
|
8705
|
+
const ongoing = await this.isSessionOngoing(sessionId);
|
|
7787
8706
|
if (isAmbiguousFinishResolved({
|
|
7788
8707
|
pinnedForMs,
|
|
7789
8708
|
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
@@ -7966,7 +8885,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7966
8885
|
*/
|
|
7967
8886
|
async readoptProcessing() {
|
|
7968
8887
|
const rows = await this.getProcessingMessages();
|
|
7969
|
-
if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
|
|
8888
|
+
if (this.dontRedispatch.size > 0 || this.untrackableAck.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
|
|
7970
8889
|
const stillProcessing = new Set(rows.map((r) => r.id));
|
|
7971
8890
|
for (const id of [
|
|
7972
8891
|
...this.dontRedispatch,
|
|
@@ -7986,6 +8905,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7986
8905
|
}
|
|
7987
8906
|
}
|
|
7988
8907
|
}
|
|
8908
|
+
for (const id of [...this.untrackableAck]) {
|
|
8909
|
+
if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
|
|
8910
|
+
this.untrackableAck.delete(id);
|
|
8911
|
+
this.log({
|
|
8912
|
+
level: "debug",
|
|
8913
|
+
message: `Re-adopt: message ${id.slice(0, 8)} left processing and pending \u2014 cleared untrackable-ack fence`,
|
|
8914
|
+
message_id: id
|
|
8915
|
+
});
|
|
8916
|
+
}
|
|
8917
|
+
}
|
|
7989
8918
|
}
|
|
7990
8919
|
if (rows.length === 0) return;
|
|
7991
8920
|
const bySession = /* @__PURE__ */ new Map();
|
|
@@ -8006,23 +8935,32 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8006
8935
|
for (const [sessionId, sessionRows] of bySession) {
|
|
8007
8936
|
let messages;
|
|
8008
8937
|
try {
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8938
|
+
if (this.isV2) {
|
|
8939
|
+
const snapshot = await this.getSessionMessages(sessionId);
|
|
8940
|
+
if (snapshot === null) {
|
|
8941
|
+
this.log({
|
|
8942
|
+
level: "warn",
|
|
8943
|
+
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
|
|
8944
|
+
});
|
|
8945
|
+
continue;
|
|
8946
|
+
}
|
|
8947
|
+
messages = snapshot;
|
|
8948
|
+
} else {
|
|
8949
|
+
const polled = await pollSessionMessagesForRedrive(
|
|
8950
|
+
this.port,
|
|
8951
|
+
sessionId,
|
|
8952
|
+
this.openCodeClient
|
|
8953
|
+
);
|
|
8954
|
+
if (!polled.ok) {
|
|
8955
|
+
const normalized = normalizeRedrivePollFailureBody(polled.body);
|
|
8956
|
+
this.log({
|
|
8957
|
+
level: "warn",
|
|
8958
|
+
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`
|
|
8959
|
+
});
|
|
8960
|
+
continue;
|
|
8961
|
+
}
|
|
8962
|
+
messages = polled.messages;
|
|
8024
8963
|
}
|
|
8025
|
-
messages = body;
|
|
8026
8964
|
} catch (err) {
|
|
8027
8965
|
this.log({
|
|
8028
8966
|
level: "warn",
|
|
@@ -8031,7 +8969,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8031
8969
|
continue;
|
|
8032
8970
|
}
|
|
8033
8971
|
const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
|
|
8034
|
-
const sessionOngoing = anyUntracked ? await isSessionOngoing(
|
|
8972
|
+
const sessionOngoing = anyUntracked ? await this.isSessionOngoing(sessionId) : null;
|
|
8035
8973
|
for (const row of sessionRows) {
|
|
8036
8974
|
await this.readoptOne(sessionId, row, messages, sessionOngoing);
|
|
8037
8975
|
}
|
|
@@ -8078,7 +9016,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8078
9016
|
if (restartAborted) {
|
|
8079
9017
|
this.log({
|
|
8080
9018
|
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
|
|
9019
|
+
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
9020
|
conversation_id: row.conversation_id,
|
|
8083
9021
|
message_id: row.id
|
|
8084
9022
|
});
|
|
@@ -8133,10 +9071,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8133
9071
|
}
|
|
8134
9072
|
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
8135
9073
|
this.dontRedispatch.delete(row.id);
|
|
9074
|
+
this.untrackableAck.delete(row.id);
|
|
8136
9075
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
8137
9076
|
return;
|
|
8138
9077
|
}
|
|
8139
|
-
if (this.dontRedispatch.has(row.id)) {
|
|
9078
|
+
if (this.dontRedispatch.has(row.id) || this.untrackableAck.has(row.id)) {
|
|
8140
9079
|
this.log({
|
|
8141
9080
|
level: "debug",
|
|
8142
9081
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
@@ -8156,7 +9095,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8156
9095
|
const finish = reply?.info?.finish ?? reply?.finish;
|
|
8157
9096
|
this.log({
|
|
8158
9097
|
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
|
|
9098
|
+
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
9099
|
conversation_id: row.conversation_id,
|
|
8161
9100
|
message_id: row.id
|
|
8162
9101
|
});
|
|
@@ -8165,7 +9104,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8165
9104
|
}
|
|
8166
9105
|
this.log({
|
|
8167
9106
|
level: "info",
|
|
8168
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per
|
|
9107
|
+
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
9108
|
conversation_id: row.conversation_id,
|
|
8170
9109
|
message_id: row.id
|
|
8171
9110
|
});
|
|
@@ -8175,7 +9114,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8175
9114
|
if (ongoing === true) {
|
|
8176
9115
|
this.log({
|
|
8177
9116
|
level: "debug",
|
|
8178
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per
|
|
9117
|
+
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
9118
|
conversation_id: row.conversation_id,
|
|
8180
9119
|
message_id: row.id
|
|
8181
9120
|
});
|
|
@@ -8183,7 +9122,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8183
9122
|
if (shape === "b1") {
|
|
8184
9123
|
this.log({
|
|
8185
9124
|
level: "debug",
|
|
8186
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but
|
|
9125
|
+
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
9126
|
conversation_id: row.conversation_id,
|
|
8188
9127
|
message_id: row.id
|
|
8189
9128
|
});
|
|
@@ -8195,7 +9134,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8195
9134
|
}
|
|
8196
9135
|
this.log({
|
|
8197
9136
|
level: "debug",
|
|
8198
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but
|
|
9137
|
+
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
9138
|
conversation_id: row.conversation_id,
|
|
8200
9139
|
message_id: row.id
|
|
8201
9140
|
});
|
|
@@ -8245,7 +9184,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8245
9184
|
* extracted (#1493 Task 2.4) so the ambiguous-finish guard above can call the
|
|
8246
9185
|
* SAME delivery instead of duplicating it.
|
|
8247
9186
|
*
|
|
8248
|
-
* EVEN IF the row was previously parked
|
|
9187
|
+
* EVEN IF the row was previously parked by either recovery fence (a give-up stops
|
|
8249
9188
|
* re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY like the watcher's
|
|
8250
9189
|
* `settleMessageDone`: auth re-throws; terminal → park in `doneUndeliverable` +
|
|
8251
9190
|
* leave for cron; transient → log + leave for the next drain (the still-
|
|
@@ -8312,6 +9251,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8312
9251
|
await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
|
|
8313
9252
|
}
|
|
8314
9253
|
this.dontRedispatch.delete(row.id);
|
|
9254
|
+
this.untrackableAck.delete(row.id);
|
|
8315
9255
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
8316
9256
|
}
|
|
8317
9257
|
/**
|
|
@@ -8375,19 +9315,90 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8375
9315
|
conversation_id: row.conversation_id,
|
|
8376
9316
|
message_id: row.id
|
|
8377
9317
|
});
|
|
8378
|
-
this.awaitingReadopt.add(row.id);
|
|
9318
|
+
if (!this.isV2) this.awaitingReadopt.add(row.id);
|
|
8379
9319
|
const readoptConv = this.convForRow(sessionId, row);
|
|
8380
9320
|
const readoptMessage = this.queuedMessageForRow(row);
|
|
8381
|
-
const sendAttachments = this.buildSendAttachments(readoptConv, readoptMessage);
|
|
9321
|
+
const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(readoptConv, readoptMessage);
|
|
9322
|
+
if (this.isV2 && row.attachments && row.attachments.length > 0) {
|
|
9323
|
+
this.signalAttachmentsSkipped(
|
|
9324
|
+
row.conversation_id,
|
|
9325
|
+
row.id,
|
|
9326
|
+
row.attachments.map((attachment, index) => ({
|
|
9327
|
+
index,
|
|
9328
|
+
mime: attachment.mime,
|
|
9329
|
+
...attachment.filename ? { filename: attachment.filename } : {},
|
|
9330
|
+
status: "skipped"
|
|
9331
|
+
})),
|
|
9332
|
+
false
|
|
9333
|
+
);
|
|
9334
|
+
}
|
|
8382
9335
|
let ocId;
|
|
8383
9336
|
try {
|
|
8384
|
-
ocId = await this.dispatchLocked(
|
|
9337
|
+
ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
|
|
8385
9338
|
sessionId,
|
|
8386
|
-
() => sendPromptAsync(
|
|
9339
|
+
() => sendPromptAsync(
|
|
9340
|
+
this.port,
|
|
9341
|
+
sessionId,
|
|
9342
|
+
row.content,
|
|
9343
|
+
options,
|
|
9344
|
+
sendAttachments,
|
|
9345
|
+
this.openCodeClient
|
|
9346
|
+
)
|
|
8387
9347
|
);
|
|
8388
9348
|
} catch (err) {
|
|
8389
9349
|
this.awaitingReadopt.delete(row.id);
|
|
8390
9350
|
if (err instanceof ChannelAuthError) throw err;
|
|
9351
|
+
if (this.isV2) {
|
|
9352
|
+
const errorMessage3 = err instanceof Error ? err.message : String(err);
|
|
9353
|
+
const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
|
|
9354
|
+
if (!invalidPromptAck) {
|
|
9355
|
+
const exists = await this.sessionExists(sessionId);
|
|
9356
|
+
if (exists === false) {
|
|
9357
|
+
this.log({
|
|
9358
|
+
level: "warn",
|
|
9359
|
+
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}`,
|
|
9360
|
+
conversation_id: row.conversation_id,
|
|
9361
|
+
message_id: row.id
|
|
9362
|
+
});
|
|
9363
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
9364
|
+
return;
|
|
9365
|
+
}
|
|
9366
|
+
if (exists === null) {
|
|
9367
|
+
this.log({
|
|
9368
|
+
level: "warn",
|
|
9369
|
+
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}`,
|
|
9370
|
+
conversation_id: row.conversation_id,
|
|
9371
|
+
message_id: row.id
|
|
9372
|
+
});
|
|
9373
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
9374
|
+
return;
|
|
9375
|
+
}
|
|
9376
|
+
} else {
|
|
9377
|
+
this.untrackableAck.add(row.id);
|
|
9378
|
+
}
|
|
9379
|
+
this.log({
|
|
9380
|
+
level: "error",
|
|
9381
|
+
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage3}`,
|
|
9382
|
+
conversation_id: row.conversation_id,
|
|
9383
|
+
message_id: row.id
|
|
9384
|
+
});
|
|
9385
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
|
|
9386
|
+
(markErr) => {
|
|
9387
|
+
this.log({
|
|
9388
|
+
level: "warn",
|
|
9389
|
+
message: `markFailed PATCH for V2 re-adopt dispatch failure on message ${row.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
9390
|
+
conversation_id: row.conversation_id,
|
|
9391
|
+
message_id: row.id
|
|
9392
|
+
});
|
|
9393
|
+
if (invalidPromptAck) {
|
|
9394
|
+
void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
|
|
9395
|
+
} else {
|
|
9396
|
+
this.signalDispatchNotStarted(readoptConv, readoptMessage, "failure_unreported");
|
|
9397
|
+
}
|
|
9398
|
+
}
|
|
9399
|
+
);
|
|
9400
|
+
return;
|
|
9401
|
+
}
|
|
8391
9402
|
this.log({
|
|
8392
9403
|
level: "warn",
|
|
8393
9404
|
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 +9530,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8519
9530
|
this.dispatched.delete(evidentMessageId);
|
|
8520
9531
|
}
|
|
8521
9532
|
/**
|
|
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
|
|
9533
|
+
* Poll V1's global `/question` + `/permission`, or V2's watched-session form and
|
|
9534
|
+
* permission routes, and surface NEW ones via `reportInteraction` (Task 3.5),
|
|
9535
|
+
* carrying the PAUSED message's own `source_message_id` so the server @mentions
|
|
9536
|
+
* the correct person under concurrency. Dedups by interaction id across ticks
|
|
9537
|
+
* (reused per-session sets).
|
|
8526
9538
|
*
|
|
8527
|
-
* The interaction is attributed to the in-flight message it paused on.
|
|
9539
|
+
* The interaction is attributed to the in-flight message it paused on. OpenCode
|
|
8528
9540
|
* stamps a `messageID` on a permission (and `tool.messageID` on a question) =
|
|
8529
9541
|
* the assistant message id, whose `parentID` is the user message id — but the
|
|
8530
9542
|
* simplest robust attribution here is: the single in-flight message that is
|
|
@@ -8547,16 +9559,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8547
9559
|
let permissionsPolledOk = true;
|
|
8548
9560
|
let questions = [];
|
|
8549
9561
|
try {
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
questions = body;
|
|
8555
|
-
} else {
|
|
8556
|
-
questionsPolledOk = false;
|
|
8557
|
-
}
|
|
9562
|
+
if (this.isV2) {
|
|
9563
|
+
const forms = await listV2Forms(this.openCodeClient, sessionId);
|
|
9564
|
+
if (forms === null) questionsPolledOk = false;
|
|
9565
|
+
else questions = forms;
|
|
8558
9566
|
} else {
|
|
8559
|
-
|
|
9567
|
+
const listed = await listOpenCodeQuestions(this.port, this.openCodeClient);
|
|
9568
|
+
if (listed === null) questionsPolledOk = false;
|
|
9569
|
+
else questions = listed;
|
|
8560
9570
|
}
|
|
8561
9571
|
} catch {
|
|
8562
9572
|
questionsPolledOk = false;
|
|
@@ -8576,16 +9586,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8576
9586
|
}
|
|
8577
9587
|
let permissions = [];
|
|
8578
9588
|
try {
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
permissions = body;
|
|
8584
|
-
} else {
|
|
8585
|
-
permissionsPolledOk = false;
|
|
8586
|
-
}
|
|
9589
|
+
if (this.isV2) {
|
|
9590
|
+
const listed = await listV2Permissions(this.openCodeClient, sessionId);
|
|
9591
|
+
if (listed === null) permissionsPolledOk = false;
|
|
9592
|
+
else permissions = listed;
|
|
8587
9593
|
} else {
|
|
8588
|
-
|
|
9594
|
+
const listed = await listOpenCodePermissions(this.port, this.openCodeClient);
|
|
9595
|
+
if (listed === null) permissionsPolledOk = false;
|
|
9596
|
+
else permissions = listed;
|
|
8589
9597
|
}
|
|
8590
9598
|
} catch {
|
|
8591
9599
|
permissionsPolledOk = false;
|
|
@@ -8679,10 +9687,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8679
9687
|
if (cached !== void 0) return cached;
|
|
8680
9688
|
let parent = void 0;
|
|
8681
9689
|
try {
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
9690
|
+
if (this.isV2) {
|
|
9691
|
+
const session = await getV2Session(this.openCodeClient, sessionId);
|
|
9692
|
+
parent = null;
|
|
9693
|
+
const candidate = session.parentID;
|
|
9694
|
+
if (typeof candidate === "string") parent = candidate;
|
|
9695
|
+
} else {
|
|
9696
|
+
const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
|
|
9697
|
+
parent = typeof body?.parentID === "string" ? body.parentID : body === null ? void 0 : null;
|
|
8686
9698
|
}
|
|
8687
9699
|
} catch {
|
|
8688
9700
|
parent = void 0;
|
|
@@ -8736,18 +9748,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8736
9748
|
if (cached) return cached;
|
|
8737
9749
|
const pending = (async () => {
|
|
8738
9750
|
try {
|
|
8739
|
-
const
|
|
8740
|
-
if (
|
|
9751
|
+
const messages2 = await this.getTelemetrySubagentSessionMessages(sessionId);
|
|
9752
|
+
if (messages2 === null) {
|
|
8741
9753
|
this.log({
|
|
8742
9754
|
level: "warn",
|
|
8743
|
-
message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)}
|
|
9755
|
+
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
9756
|
message_id: messageId
|
|
8745
9757
|
});
|
|
8746
9758
|
return null;
|
|
8747
9759
|
}
|
|
8748
|
-
|
|
8749
|
-
if (!Array.isArray(body)) throw new Error("response body was not a message array");
|
|
8750
|
-
return body;
|
|
9760
|
+
return messages2;
|
|
8751
9761
|
} catch (err) {
|
|
8752
9762
|
this.log({
|
|
8753
9763
|
level: "warn",
|
|
@@ -8888,21 +9898,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8888
9898
|
const cached = this.sessionTitles.get(sessionId);
|
|
8889
9899
|
if (cached != null) return cached;
|
|
8890
9900
|
try {
|
|
8891
|
-
|
|
8892
|
-
if (
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
return title;
|
|
8898
|
-
}
|
|
8899
|
-
return null;
|
|
9901
|
+
let title = "";
|
|
9902
|
+
if (this.isV2) {
|
|
9903
|
+
title = (await getV2Session(this.openCodeClient, sessionId)).title?.trim() ?? "";
|
|
9904
|
+
} else {
|
|
9905
|
+
const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
|
|
9906
|
+
title = typeof body?.title === "string" ? body.title.trim() : "";
|
|
8900
9907
|
}
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
9908
|
+
if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
|
|
9909
|
+
this.sessionTitles.set(sessionId, title);
|
|
9910
|
+
return title;
|
|
9911
|
+
}
|
|
9912
|
+
return null;
|
|
8906
9913
|
} catch (err) {
|
|
8907
9914
|
this.log({
|
|
8908
9915
|
level: "debug",
|
|
@@ -9000,7 +10007,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9000
10007
|
* `SessionStatus` only.
|
|
9001
10008
|
*/
|
|
9002
10009
|
async isAnyDescendantSessionAlive(rootSessionId) {
|
|
9003
|
-
const sessions = await listSessions(
|
|
10010
|
+
const sessions = await this.listSessions();
|
|
9004
10011
|
if (!sessions) {
|
|
9005
10012
|
this.log({
|
|
9006
10013
|
level: "warn",
|
|
@@ -9011,7 +10018,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9011
10018
|
for (const candidate of sessions) {
|
|
9012
10019
|
if (!candidate?.id || candidate.id === rootSessionId) continue;
|
|
9013
10020
|
if (!await this.sessionBelongsTo(candidate.id, rootSessionId)) continue;
|
|
9014
|
-
const childMsgs = await
|
|
10021
|
+
const childMsgs = await this.getSubagentSessionMessages(candidate.id);
|
|
9015
10022
|
if (isSessionActivelyGenerating(childMsgs)) {
|
|
9016
10023
|
return true;
|
|
9017
10024
|
}
|
|
@@ -9073,7 +10080,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9073
10080
|
* `isB2AbandonmentConfirmed`.
|
|
9074
10081
|
*/
|
|
9075
10082
|
async isAnyDescendantSessionOngoing(rootSessionId) {
|
|
9076
|
-
const sessions = await listSessions(
|
|
10083
|
+
const sessions = await this.listSessions();
|
|
9077
10084
|
if (!sessions) {
|
|
9078
10085
|
this.log({
|
|
9079
10086
|
level: "warn",
|
|
@@ -9090,7 +10097,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9090
10097
|
continue;
|
|
9091
10098
|
}
|
|
9092
10099
|
if (membership === false) continue;
|
|
9093
|
-
const ongoing = await isSessionOngoing(
|
|
10100
|
+
const ongoing = await this.isSessionOngoing(candidate.id);
|
|
9094
10101
|
if (ongoing === true) return true;
|
|
9095
10102
|
if (ongoing === null) indeterminate = true;
|
|
9096
10103
|
}
|
|
@@ -9430,7 +10437,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9430
10437
|
const classified = messageFailure(messages, userMessageId);
|
|
9431
10438
|
if (classified != null) return classified;
|
|
9432
10439
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
9433
|
-
const hasProvider = await hasAnyConfiguredProvider(
|
|
10440
|
+
const hasProvider = await this.hasAnyConfiguredProvider();
|
|
9434
10441
|
return applyZeroProviderFallback(
|
|
9435
10442
|
classified,
|
|
9436
10443
|
hasProvider,
|
|
@@ -9503,7 +10510,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9503
10510
|
const succeededProviders = /* @__PURE__ */ new Set();
|
|
9504
10511
|
for (const ref of refs) {
|
|
9505
10512
|
try {
|
|
9506
|
-
const childMessages = await
|
|
10513
|
+
const childMessages = await this.getSubagentSessionMessages(ref.sessionId);
|
|
9507
10514
|
if (childMessages === null) {
|
|
9508
10515
|
this.log({
|
|
9509
10516
|
level: "debug",
|
|
@@ -9834,6 +10841,7 @@ Port ${port} is already in use.`));
|
|
|
9834
10841
|
|
|
9835
10842
|
// src/commands/ensure-opencode-v2.ts
|
|
9836
10843
|
import chalk6 from "chalk";
|
|
10844
|
+
import ora3 from "ora";
|
|
9837
10845
|
import { select as select3 } from "@inquirer/prompts";
|
|
9838
10846
|
async function probeOpenCode2WithoutPassword(port) {
|
|
9839
10847
|
try {
|
|
@@ -9859,11 +10867,7 @@ function unknownPasswordError(port) {
|
|
|
9859
10867
|
`OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
|
|
9860
10868
|
);
|
|
9861
10869
|
}
|
|
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
|
-
}
|
|
10870
|
+
var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
|
|
9867
10871
|
async function ensureOpenCode2Running(ctx) {
|
|
9868
10872
|
const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
|
|
9869
10873
|
if (initialHealth.authFailed) {
|
|
@@ -9906,13 +10910,37 @@ Port ${port} is already in use.`));
|
|
|
9906
10910
|
}
|
|
9907
10911
|
}
|
|
9908
10912
|
if (!ctx.interactive) {
|
|
9909
|
-
|
|
10913
|
+
ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
|
|
10914
|
+
const { child: proc, password } = await startOpenCode2(port, {
|
|
10915
|
+
inheritStdio: ctx.inheritStdio
|
|
10916
|
+
});
|
|
10917
|
+
const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
|
|
10918
|
+
if (!health.healthy) {
|
|
10919
|
+
return {
|
|
10920
|
+
port,
|
|
10921
|
+
process: proc,
|
|
10922
|
+
version: null,
|
|
10923
|
+
notReadyReason: `it did not become healthy within ${Math.round(ctx.startTimeoutMs / 1e3)}s`,
|
|
10924
|
+
password
|
|
10925
|
+
};
|
|
10926
|
+
}
|
|
10927
|
+
ctx.log(`OpenCode V2 started on port ${port}${health.version ? ` (v${health.version})` : ""}`);
|
|
10928
|
+
return {
|
|
10929
|
+
port,
|
|
10930
|
+
process: proc,
|
|
10931
|
+
version: health.version ?? null,
|
|
10932
|
+
notReadyReason: null,
|
|
10933
|
+
password
|
|
10934
|
+
};
|
|
9910
10935
|
}
|
|
9911
|
-
console.log(chalk6.yellow(`
|
|
9912
|
-
${v2SessionSupportIncompleteError().message}`));
|
|
9913
10936
|
const action = await select3({
|
|
9914
10937
|
message: "OpenCode V2 is not running. What would you like to do?",
|
|
9915
10938
|
choices: [
|
|
10939
|
+
{
|
|
10940
|
+
name: "Start OpenCode V2 for me",
|
|
10941
|
+
value: "start",
|
|
10942
|
+
description: `Run 'opencode2 serve --port ${port}'`
|
|
10943
|
+
},
|
|
9916
10944
|
{
|
|
9917
10945
|
name: "Show me the command",
|
|
9918
10946
|
value: "manual",
|
|
@@ -9933,6 +10961,25 @@ ${v2SessionSupportIncompleteError().message}`));
|
|
|
9933
10961
|
blank();
|
|
9934
10962
|
throw new Error("Please start OpenCode V2 manually");
|
|
9935
10963
|
}
|
|
10964
|
+
if (action === "start") {
|
|
10965
|
+
const spinner = ora3("Starting OpenCode V2...").start();
|
|
10966
|
+
const { child: proc, password } = await startOpenCode2(port, {
|
|
10967
|
+
inheritStdio: ctx.inheritStdio
|
|
10968
|
+
});
|
|
10969
|
+
const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
|
|
10970
|
+
if (!health.healthy) {
|
|
10971
|
+
spinner.fail("Failed to start OpenCode V2");
|
|
10972
|
+
throw new Error("OpenCode V2 failed to start");
|
|
10973
|
+
}
|
|
10974
|
+
spinner.stop();
|
|
10975
|
+
return {
|
|
10976
|
+
port,
|
|
10977
|
+
process: proc,
|
|
10978
|
+
version: health.version ?? null,
|
|
10979
|
+
notReadyReason: null,
|
|
10980
|
+
password
|
|
10981
|
+
};
|
|
10982
|
+
}
|
|
9936
10983
|
return {
|
|
9937
10984
|
port,
|
|
9938
10985
|
process: null,
|
|
@@ -10826,7 +11873,7 @@ async function driveChannels(state, driver) {
|
|
|
10826
11873
|
lastSeenOpencodeAuthApplies = opencodeAuthApplies;
|
|
10827
11874
|
if (opencodeAuthApplied) state.openaiUsageRearm?.();
|
|
10828
11875
|
if (claudeCredentialApplied || opencodeAuthApplied) {
|
|
10829
|
-
void reloadProviderCache(state.port).catch(
|
|
11876
|
+
void reloadProviderCache(state.port, state.opencodeClient ?? void 0).catch(
|
|
10830
11877
|
(error2) => logActivity(state, {
|
|
10831
11878
|
type: "error",
|
|
10832
11879
|
error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
@@ -10951,7 +11998,7 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
|
|
|
10951
11998
|
async function runSweep(state, driver, config) {
|
|
10952
11999
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
10953
12000
|
try {
|
|
10954
|
-
const sessions = await listSessions(state.port);
|
|
12001
|
+
const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
|
|
10955
12002
|
if (sessions === null) {
|
|
10956
12003
|
logActivity(state, {
|
|
10957
12004
|
type: "info",
|
|
@@ -10981,7 +12028,7 @@ async function runSweep(state, driver, config) {
|
|
|
10981
12028
|
});
|
|
10982
12029
|
continue;
|
|
10983
12030
|
}
|
|
10984
|
-
if (await deleteSession(state.port, id)) deleted++;
|
|
12031
|
+
if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
|
|
10985
12032
|
else failed++;
|
|
10986
12033
|
}
|
|
10987
12034
|
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
@@ -11488,6 +12535,9 @@ async function run(options) {
|
|
|
11488
12535
|
connected: false,
|
|
11489
12536
|
opencodeConnected: false,
|
|
11490
12537
|
opencodeVersion: null,
|
|
12538
|
+
opencodeApiVersion: "v1",
|
|
12539
|
+
opencodePassword: null,
|
|
12540
|
+
opencodeClient: null,
|
|
11491
12541
|
sessionDbProvenanceAnomaly: false,
|
|
11492
12542
|
opencodeProcess: null,
|
|
11493
12543
|
stopOpenCodeLogTail: null,
|
|
@@ -11642,7 +12692,7 @@ async function run(options) {
|
|
|
11642
12692
|
console.log(chalk7.bold("Evident Run"));
|
|
11643
12693
|
console.log(chalk7.dim("-".repeat(40)));
|
|
11644
12694
|
}
|
|
11645
|
-
const spinner = interactive && !state.json ?
|
|
12695
|
+
const spinner = interactive && !state.json ? ora4("Validating runner...").start() : null;
|
|
11646
12696
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
11647
12697
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
11648
12698
|
spinner?.fail("Authentication failed");
|
|
@@ -11763,7 +12813,7 @@ async function run(options) {
|
|
|
11763
12813
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
11764
12814
|
}
|
|
11765
12815
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
11766
|
-
const ocSpinner = interactive && !state.json ?
|
|
12816
|
+
const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
|
|
11767
12817
|
try {
|
|
11768
12818
|
const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
|
|
11769
12819
|
port: state.port,
|
|
@@ -11783,6 +12833,19 @@ async function run(options) {
|
|
|
11783
12833
|
state.port = oc.port;
|
|
11784
12834
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
11785
12835
|
state.opencodeVersion = oc.version;
|
|
12836
|
+
state.opencodeApiVersion = opencodeVersion;
|
|
12837
|
+
let opencodePassword = null;
|
|
12838
|
+
if (opencodeVersion === "v2" && "password" in oc) {
|
|
12839
|
+
const value = oc.password;
|
|
12840
|
+
if (typeof value === "string" || value === null) opencodePassword = value;
|
|
12841
|
+
}
|
|
12842
|
+
state.opencodePassword = opencodePassword;
|
|
12843
|
+
const openCodeClient = createOpenCodeClient({
|
|
12844
|
+
port: state.port,
|
|
12845
|
+
version: state.opencodeApiVersion,
|
|
12846
|
+
password: state.opencodePassword
|
|
12847
|
+
});
|
|
12848
|
+
state.opencodeClient = openCodeClient;
|
|
11786
12849
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
11787
12850
|
try {
|
|
11788
12851
|
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
@@ -11819,16 +12882,19 @@ async function run(options) {
|
|
|
11819
12882
|
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
12883
|
logActivity(state, { type: "info", level: "warn", message });
|
|
11821
12884
|
} else {
|
|
11822
|
-
const versionWarning = buildOpenCodeVersionWarning(
|
|
12885
|
+
const versionWarning = buildOpenCodeVersionWarning(
|
|
12886
|
+
state.opencodeVersion,
|
|
12887
|
+
state.opencodeApiVersion
|
|
12888
|
+
);
|
|
11823
12889
|
if (versionWarning) {
|
|
11824
12890
|
log2(state, versionWarning, "warn");
|
|
11825
12891
|
if (state.interactive && !state.json) {
|
|
11826
12892
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
11827
12893
|
}
|
|
11828
12894
|
}
|
|
11829
|
-
await reloadProviderCache(state.port);
|
|
12895
|
+
await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
|
|
11830
12896
|
const noProviderWarning = buildNoProviderWarning(
|
|
11831
|
-
await hasAnyConfiguredProvider(state.port)
|
|
12897
|
+
await hasAnyConfiguredProvider(state.port, state.opencodeClient ?? void 0)
|
|
11832
12898
|
);
|
|
11833
12899
|
if (noProviderWarning) {
|
|
11834
12900
|
log2(state, noProviderWarning, "warn");
|
|
@@ -11951,11 +13017,12 @@ async function run(options) {
|
|
|
11951
13017
|
});
|
|
11952
13018
|
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
11953
13019
|
}
|
|
11954
|
-
const tunnelSpinner = interactive && !state.json ?
|
|
13020
|
+
const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
|
|
11955
13021
|
const channelDriver = new ChannelDriver({
|
|
11956
13022
|
agentId: state.agentId,
|
|
11957
13023
|
port: state.port,
|
|
11958
13024
|
apiUrl: getApiUrlConfig(),
|
|
13025
|
+
openCodeClient: state.opencodeClient ?? void 0,
|
|
11959
13026
|
getAuthHeader: () => state.authHeader,
|
|
11960
13027
|
conversationFilter: state.conversationFilter,
|
|
11961
13028
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
@@ -11981,6 +13048,7 @@ async function run(options) {
|
|
|
11981
13048
|
agentId: state.agentId,
|
|
11982
13049
|
getAuthHeader: () => state.authHeader,
|
|
11983
13050
|
port: state.port,
|
|
13051
|
+
openCodePassword: state.opencodePassword,
|
|
11984
13052
|
isRunning: () => state.running,
|
|
11985
13053
|
events: {
|
|
11986
13054
|
onConnected: (agentId, isReconnect) => {
|
|
@@ -12005,7 +13073,11 @@ async function run(options) {
|
|
|
12005
13073
|
emitAgentConnected(state.agentId, {
|
|
12006
13074
|
port: state.port,
|
|
12007
13075
|
cli_version: getCliVersion(),
|
|
12008
|
-
opencode_version:
|
|
13076
|
+
opencode_version: reportedOpenCodeVersion({
|
|
13077
|
+
version: state.opencodeVersion,
|
|
13078
|
+
major: state.opencodeApiVersion,
|
|
13079
|
+
connected: state.opencodeConnected
|
|
13080
|
+
})
|
|
12009
13081
|
});
|
|
12010
13082
|
if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
|
|
12011
13083
|
if (state.interactive) displayStatus(state);
|
|
@@ -12121,7 +13193,7 @@ async function run(options) {
|
|
|
12121
13193
|
state.openaiUsageTimer = timer;
|
|
12122
13194
|
},
|
|
12123
13195
|
fetchUsage: async () => {
|
|
12124
|
-
const usage = await getOpenAiUsage(state.port);
|
|
13196
|
+
const usage = await getOpenAiUsage(state.port, state.opencodeClient ?? void 0);
|
|
12125
13197
|
if (usage.subscription === null) {
|
|
12126
13198
|
logActivity(state, {
|
|
12127
13199
|
type: "info",
|