@evident-ai/cli 3.6.1-dev.00837f1 → 3.6.1-dev.0cc3c83
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/dist/index.js +1911 -1712
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -335,14 +335,14 @@ function blank() {
|
|
|
335
335
|
console.log();
|
|
336
336
|
}
|
|
337
337
|
function waitForEnter(prompt = "Press Enter to continue...") {
|
|
338
|
-
return new Promise((
|
|
338
|
+
return new Promise((resolve6) => {
|
|
339
339
|
process.stdout.write(chalk.dim(prompt));
|
|
340
340
|
const handler = () => {
|
|
341
341
|
process.stdin.removeListener("data", handler);
|
|
342
342
|
process.stdin.setRawMode?.(false);
|
|
343
343
|
process.stdin.pause();
|
|
344
344
|
console.log();
|
|
345
|
-
|
|
345
|
+
resolve6();
|
|
346
346
|
};
|
|
347
347
|
if (process.stdin.isTTY) {
|
|
348
348
|
process.stdin.setRawMode?.(true);
|
|
@@ -352,7 +352,7 @@ function waitForEnter(prompt = "Press Enter to continue...") {
|
|
|
352
352
|
});
|
|
353
353
|
}
|
|
354
354
|
function sleep(ms) {
|
|
355
|
-
return new Promise((
|
|
355
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
// src/commands/login.ts
|
|
@@ -430,19 +430,19 @@ async function tokenLogin() {
|
|
|
430
430
|
);
|
|
431
431
|
blank();
|
|
432
432
|
process.stdout.write("Paste token: ");
|
|
433
|
-
const token = await new Promise((
|
|
433
|
+
const token = await new Promise((resolve6) => {
|
|
434
434
|
let data = "";
|
|
435
435
|
process.stdin.setEncoding("utf8");
|
|
436
436
|
process.stdin.on("data", (chunk) => {
|
|
437
437
|
data += chunk;
|
|
438
438
|
});
|
|
439
439
|
process.stdin.on("end", () => {
|
|
440
|
-
|
|
440
|
+
resolve6(data.trim());
|
|
441
441
|
});
|
|
442
442
|
if (process.stdin.isTTY) {
|
|
443
443
|
process.stdin.once("data", (chunk) => {
|
|
444
444
|
process.stdin.pause();
|
|
445
|
-
|
|
445
|
+
resolve6(chunk.toString().trim());
|
|
446
446
|
});
|
|
447
447
|
process.stdin.resume();
|
|
448
448
|
}
|
|
@@ -1180,8 +1180,8 @@ async function claudeUsage() {
|
|
|
1180
1180
|
|
|
1181
1181
|
// src/commands/run.ts
|
|
1182
1182
|
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
|
|
1183
|
-
import { homedir as
|
|
1184
|
-
import { isAbsolute as
|
|
1183
|
+
import { homedir as homedir9 } from "node:os";
|
|
1184
|
+
import { isAbsolute as isAbsolute6, join as join17, parse, resolve as resolvePath2 } from "node:path";
|
|
1185
1185
|
import chalk7 from "chalk";
|
|
1186
1186
|
|
|
1187
1187
|
// ../../packages/types/src/agents/index.ts
|
|
@@ -1335,6 +1335,8 @@ function decodeBinaryBodyFrame(bytes) {
|
|
|
1335
1335
|
var MAX_FRAME_BYTES = 256 * 1024;
|
|
1336
1336
|
var TUNNEL_DRAIN_PING_PATH = "/__evident/drain";
|
|
1337
1337
|
var TUNNEL_USAGE_REARM_PING_PATH = "/__evident/usage-rearm";
|
|
1338
|
+
var TUNNEL_FILE_READ_PATH = "/__evident/file";
|
|
1339
|
+
var TUNNEL_FILE_READ_MAX_BYTES = 5 * 1024 * 1024;
|
|
1338
1340
|
|
|
1339
1341
|
// ../../packages/types/src/runner-files.ts
|
|
1340
1342
|
var MAX_FILE_PUSH_BYTES = 64 * 1024;
|
|
@@ -1475,7 +1477,7 @@ import ora4 from "ora";
|
|
|
1475
1477
|
import { select as select4 } from "@inquirer/prompts";
|
|
1476
1478
|
|
|
1477
1479
|
// src/lib/telemetry.ts
|
|
1478
|
-
var CLI_VERSION = (true ? "3.6.1-dev.
|
|
1480
|
+
var CLI_VERSION = (true ? "3.6.1-dev.0cc3c83" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1479
1481
|
function getCliVersion() {
|
|
1480
1482
|
return CLI_VERSION;
|
|
1481
1483
|
}
|
|
@@ -2047,8 +2049,8 @@ function resolveExistingPath(absolutePath) {
|
|
|
2047
2049
|
let current = candidate;
|
|
2048
2050
|
while (current) {
|
|
2049
2051
|
try {
|
|
2050
|
-
const
|
|
2051
|
-
if (
|
|
2052
|
+
const stat3 = lstatSync(current);
|
|
2053
|
+
if (stat3.isSymbolicLink()) {
|
|
2052
2054
|
if (remaining.length === 0) {
|
|
2053
2055
|
return { path: join2(realpathSync(dirname3(current)), basename(current)) };
|
|
2054
2056
|
}
|
|
@@ -2099,15 +2101,15 @@ function classifyScope(dir, roots) {
|
|
|
2099
2101
|
function unresolvedRootForPath(path, unresolved) {
|
|
2100
2102
|
return unresolved.find((spec) => isWithin(path, spec.configured));
|
|
2101
2103
|
}
|
|
2102
|
-
function statType(
|
|
2103
|
-
if (
|
|
2104
|
-
if (
|
|
2105
|
-
if (
|
|
2104
|
+
function statType(stat3) {
|
|
2105
|
+
if (stat3.isDirectory()) return "d";
|
|
2106
|
+
if (stat3.isFile()) return "f";
|
|
2107
|
+
if (stat3.isSymbolicLink()) return "l";
|
|
2106
2108
|
return "o";
|
|
2107
2109
|
}
|
|
2108
|
-
function renderStat(name,
|
|
2109
|
-
const mode = (
|
|
2110
|
-
return `${name}:${statType(
|
|
2110
|
+
function renderStat(name, stat3) {
|
|
2111
|
+
const mode = (stat3.mode & 4095).toString(8).padStart(4, "0");
|
|
2112
|
+
return `${name}:${statType(stat3)}:${stat3.uid}:${stat3.gid}:${mode}`;
|
|
2111
2113
|
}
|
|
2112
2114
|
function renderStatError(name, error2) {
|
|
2113
2115
|
const code = getErrorCode(error2);
|
|
@@ -2133,9 +2135,9 @@ function inspectChain(rootReal, rootToken, absolutePath, resolutionFailure) {
|
|
|
2133
2135
|
break;
|
|
2134
2136
|
}
|
|
2135
2137
|
try {
|
|
2136
|
-
const
|
|
2137
|
-
entries.push(renderStat(name,
|
|
2138
|
-
finalStat =
|
|
2138
|
+
const stat3 = lstatSync(current);
|
|
2139
|
+
entries.push(renderStat(name, stat3));
|
|
2140
|
+
finalStat = stat3;
|
|
2139
2141
|
} catch (error2) {
|
|
2140
2142
|
entries.push(renderStatError(name, error2));
|
|
2141
2143
|
if (getErrorCode(error2) !== "ENOENT") status2 = "stat_failed";
|
|
@@ -3355,7 +3357,7 @@ function appendError(stderr, error2) {
|
|
|
3355
3357
|
${message}`;
|
|
3356
3358
|
}
|
|
3357
3359
|
function runSynchroniser(args, opts) {
|
|
3358
|
-
return new Promise((
|
|
3360
|
+
return new Promise((resolve6) => {
|
|
3359
3361
|
let child;
|
|
3360
3362
|
let stdout = "";
|
|
3361
3363
|
let stderr = "";
|
|
@@ -3375,7 +3377,7 @@ function runSynchroniser(args, opts) {
|
|
|
3375
3377
|
if (closeFallbackTimer.handle) clearTimeout(closeFallbackTimer.handle);
|
|
3376
3378
|
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
3377
3379
|
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
3378
|
-
|
|
3380
|
+
resolve6(result);
|
|
3379
3381
|
};
|
|
3380
3382
|
const armErrorCloseFallback = () => {
|
|
3381
3383
|
if (closeFallbackTimer.handle) return;
|
|
@@ -4046,7 +4048,7 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
4046
4048
|
if (health.healthy || health.authFailed) {
|
|
4047
4049
|
return health;
|
|
4048
4050
|
}
|
|
4049
|
-
await new Promise((
|
|
4051
|
+
await new Promise((resolve6) => setTimeout(resolve6, 1e3));
|
|
4050
4052
|
}
|
|
4051
4053
|
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
4052
4054
|
}
|
|
@@ -4057,7 +4059,7 @@ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
|
|
|
4057
4059
|
if (health.healthy || health.authFailed) {
|
|
4058
4060
|
return health;
|
|
4059
4061
|
}
|
|
4060
|
-
await new Promise((
|
|
4062
|
+
await new Promise((resolve6) => setTimeout(resolve6, 1e3));
|
|
4061
4063
|
}
|
|
4062
4064
|
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
4063
4065
|
}
|
|
@@ -4167,1827 +4169,1786 @@ function createOpenCodeClient(options) {
|
|
|
4167
4169
|
};
|
|
4168
4170
|
}
|
|
4169
4171
|
|
|
4170
|
-
// src/lib/opencode/session.ts
|
|
4172
|
+
// src/lib/opencode/session-v2.ts
|
|
4171
4173
|
var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
4172
|
-
function
|
|
4173
|
-
return
|
|
4174
|
+
function isRecord5(value) {
|
|
4175
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4174
4176
|
}
|
|
4175
|
-
function
|
|
4176
|
-
return
|
|
4177
|
+
function finiteNumber(value) {
|
|
4178
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
4177
4179
|
}
|
|
4178
|
-
function
|
|
4179
|
-
return
|
|
4180
|
+
function adaptTime(value) {
|
|
4181
|
+
if (!isRecord5(value)) return void 0;
|
|
4182
|
+
const created = finiteNumber(value.created);
|
|
4183
|
+
const completed = finiteNumber(value.completed);
|
|
4184
|
+
if (created === void 0 && completed === void 0) return void 0;
|
|
4185
|
+
return {
|
|
4186
|
+
...created !== void 0 ? { created } : {},
|
|
4187
|
+
...completed !== void 0 ? { completed } : {}
|
|
4188
|
+
};
|
|
4180
4189
|
}
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
return null;
|
|
4190
|
+
function adaptTokens(value) {
|
|
4191
|
+
if (!isRecord5(value)) return void 0;
|
|
4192
|
+
const input = finiteNumber(value.input);
|
|
4193
|
+
const output = finiteNumber(value.output);
|
|
4194
|
+
const reasoning = finiteNumber(value.reasoning);
|
|
4195
|
+
const cache = isRecord5(value.cache) ? {
|
|
4196
|
+
...finiteNumber(value.cache.read) !== void 0 ? { read: finiteNumber(value.cache.read) } : {},
|
|
4197
|
+
...finiteNumber(value.cache.write) !== void 0 ? { write: finiteNumber(value.cache.write) } : {}
|
|
4198
|
+
} : void 0;
|
|
4199
|
+
if (input === void 0 && output === void 0 && reasoning === void 0 && !cache) {
|
|
4200
|
+
return void 0;
|
|
4193
4201
|
}
|
|
4202
|
+
return {
|
|
4203
|
+
...input !== void 0 ? { input } : {},
|
|
4204
|
+
...output !== void 0 ? { output } : {},
|
|
4205
|
+
...reasoning !== void 0 ? { reasoning } : {},
|
|
4206
|
+
...cache ? { cache } : {}
|
|
4207
|
+
};
|
|
4194
4208
|
}
|
|
4195
|
-
function
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4209
|
+
function adaptMessageInfo(value, role) {
|
|
4210
|
+
const info = {
|
|
4211
|
+
id: value.id,
|
|
4212
|
+
role
|
|
4213
|
+
};
|
|
4214
|
+
const time = adaptTime(value.time);
|
|
4215
|
+
if (time) info.time = time;
|
|
4216
|
+
if (typeof value.finish === "string") info.finish = value.finish;
|
|
4217
|
+
if ("error" in value) info.error = value.error;
|
|
4218
|
+
if (typeof value.agent === "string") info.agent = value.agent;
|
|
4219
|
+
if (isRecord5(value.model)) {
|
|
4220
|
+
if (typeof value.model.id === "string") info.modelID = value.model.id;
|
|
4221
|
+
if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
|
|
4222
|
+
}
|
|
4223
|
+
if (typeof value.cost === "number" && Number.isFinite(value.cost)) info.cost = value.cost;
|
|
4224
|
+
const tokens = adaptTokens(value.tokens);
|
|
4225
|
+
if (tokens) info.tokens = tokens;
|
|
4226
|
+
return info;
|
|
4200
4227
|
}
|
|
4201
|
-
function
|
|
4202
|
-
|
|
4203
|
-
|
|
4228
|
+
function v2ChildSessionEvidence(content) {
|
|
4229
|
+
const state = isRecord5(content.state) ? content.state : null;
|
|
4230
|
+
const metadata = state && isRecord5(state.metadata) ? state.metadata : null;
|
|
4231
|
+
const sessionId = typeof metadata?.sessionID === "string" && metadata.sessionID.length > 0 ? metadata.sessionID : typeof metadata?.sessionId === "string" && metadata.sessionId.length > 0 ? metadata.sessionId : null;
|
|
4232
|
+
if (sessionId === null) return null;
|
|
4233
|
+
const time = isRecord5(content.time) ? content.time : null;
|
|
4234
|
+
return { sessionId, startedAtMs: finiteNumber(time?.created) ?? null };
|
|
4204
4235
|
}
|
|
4205
|
-
function
|
|
4206
|
-
if (!
|
|
4207
|
-
|
|
4236
|
+
function adaptV2Message(value) {
|
|
4237
|
+
if (!isRecord5(value) || typeof value.id !== "string" || typeof value.type !== "string") {
|
|
4238
|
+
return null;
|
|
4239
|
+
}
|
|
4240
|
+
if (value.type === "user") {
|
|
4241
|
+
if (typeof value.text !== "string") return null;
|
|
4242
|
+
return {
|
|
4243
|
+
info: adaptMessageInfo(value, "user"),
|
|
4244
|
+
parts: [{ type: "text", text: value.text }]
|
|
4245
|
+
};
|
|
4246
|
+
}
|
|
4247
|
+
if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
|
|
4248
|
+
const parts = [];
|
|
4249
|
+
for (const content of value.content) {
|
|
4250
|
+
if (!isRecord5(content) || typeof content.type !== "string") return null;
|
|
4251
|
+
const toolPart = projectV2ToolContent(content);
|
|
4252
|
+
if (toolPart) {
|
|
4253
|
+
const childSession = v2ChildSessionEvidence(content);
|
|
4254
|
+
parts.push(childSession ? { ...toolPart, childSession } : toolPart);
|
|
4255
|
+
continue;
|
|
4256
|
+
}
|
|
4257
|
+
if (content.type === "text") {
|
|
4258
|
+
if (typeof content.text !== "string") return null;
|
|
4259
|
+
parts.push({ type: "text", text: content.text });
|
|
4260
|
+
} else {
|
|
4261
|
+
parts.push({ type: content.type });
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
return {
|
|
4265
|
+
info: adaptMessageInfo(value, "assistant"),
|
|
4266
|
+
parts
|
|
4267
|
+
};
|
|
4208
4268
|
}
|
|
4209
|
-
function
|
|
4210
|
-
if (!
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
return
|
|
4269
|
+
function adaptFormTool(value) {
|
|
4270
|
+
if (!isRecord5(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
|
|
4271
|
+
return void 0;
|
|
4272
|
+
}
|
|
4273
|
+
return { messageID: value.messageID, callID: value.id };
|
|
4214
4274
|
}
|
|
4215
|
-
function
|
|
4216
|
-
if (!
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
return
|
|
4275
|
+
function adaptV2FormWire(value) {
|
|
4276
|
+
if (!isRecord5(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
4277
|
+
return null;
|
|
4278
|
+
}
|
|
4279
|
+
return value;
|
|
4220
4280
|
}
|
|
4221
|
-
function
|
|
4222
|
-
if (!
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4281
|
+
function adaptV2FormField(value, header) {
|
|
4282
|
+
if (!isRecord5(value)) return null;
|
|
4283
|
+
const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
|
|
4284
|
+
if (!question) return null;
|
|
4285
|
+
const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
|
|
4286
|
+
if (!isRecord5(option)) return [];
|
|
4287
|
+
const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
|
|
4288
|
+
if (!label) return [];
|
|
4289
|
+
return [
|
|
4290
|
+
{
|
|
4291
|
+
label,
|
|
4292
|
+
description: typeof option.description === "string" ? option.description : ""
|
|
4293
|
+
}
|
|
4294
|
+
];
|
|
4295
|
+
}) : [];
|
|
4296
|
+
return { question, header, options };
|
|
4226
4297
|
}
|
|
4227
|
-
function
|
|
4228
|
-
|
|
4229
|
-
|
|
4298
|
+
function adaptV2Form(value) {
|
|
4299
|
+
const form = adaptV2FormWire(value);
|
|
4300
|
+
if (!form || !Array.isArray(form.fields)) return null;
|
|
4301
|
+
const header = typeof form.title === "string" ? form.title : "";
|
|
4302
|
+
const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
|
|
4303
|
+
if (questions.length === 0) return null;
|
|
4304
|
+
const tool = isRecord5(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
|
|
4305
|
+
return {
|
|
4306
|
+
id: form.id,
|
|
4307
|
+
sessionID: form.sessionID,
|
|
4308
|
+
questions,
|
|
4309
|
+
...tool ? { tool } : {},
|
|
4310
|
+
raw: form
|
|
4311
|
+
};
|
|
4230
4312
|
}
|
|
4231
|
-
function
|
|
4232
|
-
|
|
4313
|
+
function adaptV2FormList(value) {
|
|
4314
|
+
if (!isRecord5(value) || !Array.isArray(value.data)) return null;
|
|
4315
|
+
return value.data.map(adaptV2Form).filter((form) => form !== null);
|
|
4233
4316
|
}
|
|
4234
|
-
function
|
|
4235
|
-
if (
|
|
4236
|
-
if (
|
|
4237
|
-
|
|
4317
|
+
function adaptPattern(value) {
|
|
4318
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
4319
|
+
if (Array.isArray(value) && value.every((pattern) => typeof pattern === "string")) {
|
|
4320
|
+
return value;
|
|
4321
|
+
}
|
|
4322
|
+
return void 0;
|
|
4238
4323
|
}
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
const path = `/session/${sessionId}/message`;
|
|
4242
|
-
const res = await requestWithClient(port, client, path);
|
|
4243
|
-
if (!res.ok) return null;
|
|
4244
|
-
const body = await res.json();
|
|
4245
|
-
return Array.isArray(body) ? body : null;
|
|
4246
|
-
} catch (error2) {
|
|
4247
|
-
console.error(
|
|
4248
|
-
`[getSessionMessages] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4249
|
-
);
|
|
4324
|
+
function adaptV2PermissionWire(value) {
|
|
4325
|
+
if (!isRecord5(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
4250
4326
|
return null;
|
|
4251
4327
|
}
|
|
4328
|
+
if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
|
|
4329
|
+
return value;
|
|
4252
4330
|
}
|
|
4253
|
-
|
|
4254
|
-
const
|
|
4255
|
-
if (!
|
|
4256
|
-
const
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
}
|
|
4271
|
-
|
|
4272
|
-
if (!Array.isArray(body)) return { ok: false, status: null, body: "", malformed: true };
|
|
4273
|
-
return { ok: true, messages: body };
|
|
4274
|
-
} catch (error2) {
|
|
4275
|
-
console.error(
|
|
4276
|
-
`[pollSessionMessagesForRedrive] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4277
|
-
);
|
|
4278
|
-
return { ok: false, status: null, body: "", malformed: false };
|
|
4279
|
-
}
|
|
4331
|
+
function adaptV2Permission(value) {
|
|
4332
|
+
const permission = adaptV2PermissionWire(value);
|
|
4333
|
+
if (!permission) return null;
|
|
4334
|
+
const type = permission.permission ?? permission.action;
|
|
4335
|
+
if (!type) return null;
|
|
4336
|
+
const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
|
|
4337
|
+
const time = isRecord5(permission.time) ? finiteNumber(permission.time.created) !== void 0 ? { created: finiteNumber(permission.time.created) } : void 0 : void 0;
|
|
4338
|
+
return {
|
|
4339
|
+
id: permission.id,
|
|
4340
|
+
type,
|
|
4341
|
+
sessionID: permission.sessionID,
|
|
4342
|
+
metadata: isRecord5(permission.metadata) ? permission.metadata : {},
|
|
4343
|
+
raw: permission,
|
|
4344
|
+
...pattern !== void 0 ? { pattern } : {},
|
|
4345
|
+
...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
|
|
4346
|
+
...typeof permission.callID === "string" ? { callID: permission.callID } : {},
|
|
4347
|
+
...typeof permission.title === "string" ? { title: permission.title } : {},
|
|
4348
|
+
...time ? { time } : {}
|
|
4349
|
+
};
|
|
4280
4350
|
}
|
|
4281
|
-
function
|
|
4282
|
-
if (!
|
|
4283
|
-
|
|
4284
|
-
if (roleOf2(last) !== "assistant") return false;
|
|
4285
|
-
return completedOf(last) == null;
|
|
4351
|
+
function adaptV2PermissionList(value) {
|
|
4352
|
+
if (!isRecord5(value) || !Array.isArray(value.data)) return null;
|
|
4353
|
+
return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
|
|
4286
4354
|
}
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
const
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4355
|
+
function adaptV2Session(value) {
|
|
4356
|
+
if (!isRecord5(value) || typeof value.id !== "string" || value.id.length === 0) return null;
|
|
4357
|
+
const time = isRecord5(value.time) ? {
|
|
4358
|
+
...finiteNumber(value.time.created) !== void 0 ? { created: finiteNumber(value.time.created) } : {},
|
|
4359
|
+
...finiteNumber(value.time.updated) !== void 0 ? { updated: finiteNumber(value.time.updated) } : {}
|
|
4360
|
+
} : void 0;
|
|
4361
|
+
return {
|
|
4362
|
+
id: value.id,
|
|
4363
|
+
...typeof value.title === "string" ? { title: value.title } : {},
|
|
4364
|
+
...typeof value.parentID === "string" ? { parentID: value.parentID } : {},
|
|
4365
|
+
...time && Object.keys(time).length > 0 ? { time } : {}
|
|
4366
|
+
};
|
|
4367
|
+
}
|
|
4368
|
+
function adaptV2SessionList(value) {
|
|
4369
|
+
if (!isRecord5(value) || !Array.isArray(value.data) || !isRecord5(value.cursor)) return null;
|
|
4370
|
+
return {
|
|
4371
|
+
data: value.data.map(adaptV2Session).filter((session) => session !== null),
|
|
4372
|
+
cursor: value.cursor
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
function adaptV2Location(value) {
|
|
4376
|
+
const candidates = [
|
|
4377
|
+
value,
|
|
4378
|
+
isRecord5(value) ? value.data : void 0,
|
|
4379
|
+
isRecord5(value) ? value.location : void 0
|
|
4296
4380
|
];
|
|
4297
|
-
for (const
|
|
4298
|
-
if (typeof
|
|
4381
|
+
for (const candidate of candidates) {
|
|
4382
|
+
if (!isRecord5(candidate) || typeof candidate.directory !== "string") continue;
|
|
4383
|
+
const directory = candidate.directory.trim();
|
|
4384
|
+
if (directory) return directory;
|
|
4299
4385
|
}
|
|
4300
4386
|
return null;
|
|
4301
4387
|
}
|
|
4302
|
-
function
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
const query = params.toString();
|
|
4306
|
-
return query ? `/session?${query}` : "/session";
|
|
4388
|
+
function adaptV2Messages(value) {
|
|
4389
|
+
if (!isRecord5(value) || !Array.isArray(value.data)) return [];
|
|
4390
|
+
return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
|
|
4307
4391
|
}
|
|
4308
|
-
async function
|
|
4309
|
-
const path = sessionListPath(options);
|
|
4392
|
+
async function readJson(response) {
|
|
4310
4393
|
try {
|
|
4311
|
-
|
|
4312
|
-
port,
|
|
4313
|
-
client,
|
|
4314
|
-
path,
|
|
4315
|
-
options?.signal ? { signal: options.signal } : void 0,
|
|
4316
|
-
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
4317
|
-
);
|
|
4318
|
-
if (!res.ok) {
|
|
4319
|
-
console.error(`[listSessionPage] GET ${path} returned HTTP ${res.status} (port ${port})`);
|
|
4320
|
-
return null;
|
|
4321
|
-
}
|
|
4322
|
-
const body = await res.json();
|
|
4323
|
-
return Array.isArray(body) ? body : null;
|
|
4394
|
+
return await response.json();
|
|
4324
4395
|
} catch (error2) {
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
return null;
|
|
4396
|
+
throw new Error(
|
|
4397
|
+
`OpenCode V2 response was not valid JSON: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4398
|
+
);
|
|
4329
4399
|
}
|
|
4330
4400
|
}
|
|
4331
|
-
async function
|
|
4332
|
-
|
|
4333
|
-
|
|
4401
|
+
async function readData(client, path, init) {
|
|
4402
|
+
const response = await client.request(path, init);
|
|
4403
|
+
const body = await readJson(response);
|
|
4404
|
+
if (!isRecord5(body) || !("data" in body)) {
|
|
4405
|
+
throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
|
|
4406
|
+
}
|
|
4407
|
+
return body.data;
|
|
4334
4408
|
}
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4409
|
+
var OpenCodeV2PromptAckError = class extends Error {
|
|
4410
|
+
constructor(message) {
|
|
4411
|
+
super(message);
|
|
4412
|
+
this.name = "OpenCodeV2PromptAckError";
|
|
4339
4413
|
}
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4414
|
+
};
|
|
4415
|
+
var OpenCodeV2ModelSwitchError = class extends Error {
|
|
4416
|
+
statusCode;
|
|
4417
|
+
constructor(error2) {
|
|
4418
|
+
super(error2 instanceof Error ? error2.message : String(error2));
|
|
4419
|
+
this.name = "OpenCodeV2ModelSwitchError";
|
|
4420
|
+
this.statusCode = error2 instanceof OpenCodeRequestError ? error2.statusCode : null;
|
|
4421
|
+
}
|
|
4422
|
+
};
|
|
4423
|
+
async function getOpenCodeDirectoryV2(client) {
|
|
4424
|
+
try {
|
|
4425
|
+
return adaptV2Location(await readJson(await client.request("/api/location")));
|
|
4426
|
+
} catch (error2) {
|
|
4427
|
+
console.error(
|
|
4428
|
+
`[getOpenCodeDirectoryV2] GET /api/location failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4429
|
+
);
|
|
4430
|
+
return null;
|
|
4347
4431
|
}
|
|
4348
|
-
return { sessions };
|
|
4349
4432
|
}
|
|
4350
|
-
|
|
4351
|
-
"
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
"
|
|
4357
|
-
|
|
4358
|
-
"UND_ERR_CONNECT_TIMEOUT",
|
|
4359
|
-
"UND_ERR_HEADERS_TIMEOUT",
|
|
4360
|
-
"UND_ERR_SOCKET"
|
|
4361
|
-
]);
|
|
4362
|
-
function interruptFailureDiagnostic(error2) {
|
|
4363
|
-
const message = error2 instanceof Error ? error2.message : "";
|
|
4364
|
-
const httpStatus = /^OpenCode request failed: HTTP ([1-5]\d{2})(?::|$)/.exec(message)?.[1];
|
|
4365
|
-
if (httpStatus) return `HTTP ${httpStatus}`;
|
|
4366
|
-
const candidates = [error2, error2 instanceof Error ? error2.cause : void 0];
|
|
4367
|
-
for (const candidate of candidates) {
|
|
4368
|
-
if (typeof candidate !== "object" || candidate === null || !("code" in candidate)) continue;
|
|
4369
|
-
const code = candidate.code;
|
|
4370
|
-
if (typeof code === "string" && INTERRUPT_NETWORK_CODES.has(code)) {
|
|
4371
|
-
return redactDiagnosticText(`network ${code}`).slice(0, 120);
|
|
4372
|
-
}
|
|
4433
|
+
async function createV2Session(client, directory) {
|
|
4434
|
+
const data = await readData(client, "/api/session", {
|
|
4435
|
+
method: "POST",
|
|
4436
|
+
headers: { "Content-Type": "application/json" },
|
|
4437
|
+
body: JSON.stringify({ location: { directory } })
|
|
4438
|
+
});
|
|
4439
|
+
if (!isRecord5(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
4440
|
+
throw new Error("OpenCode V2 create session response was missing data.id");
|
|
4373
4441
|
}
|
|
4374
|
-
|
|
4375
|
-
return redactDiagnosticText(`request threw ${name}`).slice(0, 120);
|
|
4442
|
+
return data.id;
|
|
4376
4443
|
}
|
|
4377
|
-
async function
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4444
|
+
async function getV2Session(client, sessionId) {
|
|
4445
|
+
const data = await readData(client, `/api/session/${encodeURIComponent(sessionId)}`);
|
|
4446
|
+
const session = adaptV2Session(data);
|
|
4447
|
+
if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
|
|
4448
|
+
return session;
|
|
4449
|
+
}
|
|
4450
|
+
async function listV2SessionPage(client, cursor, signal) {
|
|
4451
|
+
const operation = cursor ? "GET /api/session (next page)" : "GET /api/session";
|
|
4452
|
+
const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
|
|
4453
|
+
try {
|
|
4454
|
+
const response = await client.request(path, signal ? { signal } : void 0, {
|
|
4455
|
+
allowStatuses: ALL_HTTP_STATUSES
|
|
4456
|
+
});
|
|
4457
|
+
if (!response.ok) {
|
|
4458
|
+
console.error(
|
|
4459
|
+
`[listV2SessionPage] ${operation} returned HTTP ${response.status} (port ${client.port})`
|
|
4460
|
+
);
|
|
4461
|
+
return null;
|
|
4384
4462
|
}
|
|
4463
|
+
return adaptV2SessionList(await readJson(response));
|
|
4464
|
+
} catch (error2) {
|
|
4465
|
+
if (signal?.aborted) return null;
|
|
4466
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4467
|
+
console.error(`[listV2SessionPage] ${operation} failed (port ${client.port}): ${errorName}`);
|
|
4468
|
+
return null;
|
|
4385
4469
|
}
|
|
4470
|
+
}
|
|
4471
|
+
async function listV2Sessions(client, signal) {
|
|
4472
|
+
const sessions = [];
|
|
4473
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
4474
|
+
let cursor;
|
|
4475
|
+
let hasNextPage = true;
|
|
4386
4476
|
try {
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4477
|
+
while (hasNextPage) {
|
|
4478
|
+
const page = await listV2SessionPage(client, cursor, signal);
|
|
4479
|
+
if (!page) return null;
|
|
4480
|
+
sessions.push(...page.data);
|
|
4481
|
+
const next = page.cursor.next;
|
|
4482
|
+
if (next === void 0 || next === null) {
|
|
4483
|
+
hasNextPage = false;
|
|
4484
|
+
continue;
|
|
4485
|
+
}
|
|
4486
|
+
if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
|
|
4487
|
+
throw new Error("OpenCode V2 session list contained an invalid next cursor");
|
|
4488
|
+
}
|
|
4489
|
+
seenCursors.add(next);
|
|
4490
|
+
cursor = next;
|
|
4396
4491
|
}
|
|
4397
|
-
return
|
|
4492
|
+
return sessions;
|
|
4398
4493
|
} catch (error2) {
|
|
4399
|
-
|
|
4494
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4495
|
+
console.error(
|
|
4496
|
+
`[listV2Sessions] GET /api/session pagination failed (port ${client.port}): ${errorName}`
|
|
4497
|
+
);
|
|
4498
|
+
return null;
|
|
4400
4499
|
}
|
|
4401
4500
|
}
|
|
4402
|
-
async function
|
|
4403
|
-
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
4501
|
+
async function deleteV2Session(client, sessionId) {
|
|
4404
4502
|
try {
|
|
4405
|
-
const
|
|
4406
|
-
|
|
4407
|
-
client,
|
|
4408
|
-
`/session/${id}`,
|
|
4503
|
+
const response = await client.request(
|
|
4504
|
+
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
4409
4505
|
{ method: "DELETE" },
|
|
4410
4506
|
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
4411
4507
|
);
|
|
4412
|
-
if (
|
|
4413
|
-
return { ok: true, status:
|
|
4508
|
+
if (response.status >= 200 && response.status < 300) {
|
|
4509
|
+
return { ok: true, status: response.status };
|
|
4414
4510
|
}
|
|
4415
4511
|
try {
|
|
4416
|
-
const body = await
|
|
4417
|
-
return body ? { ok: false, status:
|
|
4512
|
+
const body = await response.text();
|
|
4513
|
+
return body ? { ok: false, status: response.status, body } : { ok: false, status: response.status };
|
|
4418
4514
|
} catch (error2) {
|
|
4419
4515
|
console.error(
|
|
4420
|
-
`[
|
|
4516
|
+
`[deleteV2Session] DELETE /api/session/${sessionId} response body read failed for HTTP ${response.status}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4421
4517
|
);
|
|
4422
|
-
return { ok: false, status:
|
|
4518
|
+
return { ok: false, status: response.status };
|
|
4423
4519
|
}
|
|
4424
4520
|
} catch (error2) {
|
|
4425
4521
|
console.error(
|
|
4426
|
-
`[
|
|
4522
|
+
`[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4427
4523
|
);
|
|
4428
4524
|
return { ok: false, status: "threw" };
|
|
4429
4525
|
}
|
|
4430
4526
|
}
|
|
4431
|
-
async function
|
|
4527
|
+
async function v2SessionExists(client, sessionId) {
|
|
4432
4528
|
try {
|
|
4433
|
-
const
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
return
|
|
4439
|
-
} catch {
|
|
4529
|
+
const response = await client.request(
|
|
4530
|
+
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
4531
|
+
void 0,
|
|
4532
|
+
{ allowStatuses: [404] }
|
|
4533
|
+
);
|
|
4534
|
+
return response.status === 404 ? false : true;
|
|
4535
|
+
} catch (error2) {
|
|
4536
|
+
console.error(
|
|
4537
|
+
`[v2SessionExists] GET /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4538
|
+
);
|
|
4440
4539
|
return null;
|
|
4441
4540
|
}
|
|
4442
4541
|
}
|
|
4443
|
-
async function
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4542
|
+
async function sendV2Prompt(client, sessionId, text, options, onInvalidModelSelection) {
|
|
4543
|
+
const modelSelection = options?.model?.trim();
|
|
4544
|
+
const { model, variant } = splitModelVariant(modelSelection);
|
|
4545
|
+
const separator = model?.indexOf("/") ?? -1;
|
|
4546
|
+
const modelReference = model && separator > 0 && separator < model.length - 1 ? {
|
|
4547
|
+
id: model.slice(separator + 1),
|
|
4548
|
+
providerID: model.slice(0, separator),
|
|
4549
|
+
...variant ? { variant } : {}
|
|
4550
|
+
} : void 0;
|
|
4551
|
+
if (modelSelection && !modelReference) onInvalidModelSelection?.();
|
|
4552
|
+
if (options?.agent) {
|
|
4553
|
+
await client.request(`/api/session/${encodeURIComponent(sessionId)}/agent`, {
|
|
4554
|
+
method: "POST",
|
|
4555
|
+
headers: { "Content-Type": "application/json" },
|
|
4556
|
+
body: JSON.stringify({ agent: options.agent })
|
|
4557
|
+
});
|
|
4558
|
+
}
|
|
4559
|
+
if (modelReference) {
|
|
4560
|
+
try {
|
|
4561
|
+
await client.request(`/api/session/${encodeURIComponent(sessionId)}/model`, {
|
|
4562
|
+
method: "POST",
|
|
4563
|
+
headers: { "Content-Type": "application/json" },
|
|
4564
|
+
body: JSON.stringify({ model: modelReference })
|
|
4565
|
+
});
|
|
4566
|
+
} catch (error2) {
|
|
4567
|
+
throw new OpenCodeV2ModelSwitchError(error2);
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/prompt`;
|
|
4571
|
+
const response = await client.request(path, {
|
|
4572
|
+
method: "POST",
|
|
4573
|
+
headers: { "Content-Type": "application/json" },
|
|
4574
|
+
body: JSON.stringify({ text, delivery: "queue" })
|
|
4575
|
+
});
|
|
4576
|
+
let body;
|
|
4577
|
+
try {
|
|
4578
|
+
body = await readJson(response);
|
|
4448
4579
|
} catch (error2) {
|
|
4449
|
-
|
|
4450
|
-
`
|
|
4580
|
+
throw new OpenCodeV2PromptAckError(
|
|
4581
|
+
`OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4451
4582
|
);
|
|
4452
|
-
return null;
|
|
4453
4583
|
}
|
|
4584
|
+
const data = isRecord5(body) && "data" in body ? body.data : void 0;
|
|
4585
|
+
if (!isRecord5(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
4586
|
+
throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
|
|
4587
|
+
}
|
|
4588
|
+
return data.id;
|
|
4454
4589
|
}
|
|
4455
|
-
async function
|
|
4456
|
-
|
|
4590
|
+
async function getV2SessionMessages(client, sessionId) {
|
|
4591
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
|
|
4457
4592
|
try {
|
|
4458
|
-
const
|
|
4459
|
-
if (!
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
);
|
|
4463
|
-
return null;
|
|
4464
|
-
}
|
|
4465
|
-
const body = await res.json();
|
|
4466
|
-
if (body == null || typeof body !== "object" || Array.isArray(body)) {
|
|
4467
|
-
console.error(
|
|
4468
|
-
`[getSessionStatuses] GET /session/status body was not a plain object (port ${port})`
|
|
4469
|
-
);
|
|
4470
|
-
return null;
|
|
4471
|
-
}
|
|
4472
|
-
return body;
|
|
4473
|
-
} catch (err) {
|
|
4593
|
+
const body = await readJson(await client.request(path));
|
|
4594
|
+
if (!isRecord5(body) || !Array.isArray(body.data) || !isRecord5(body.cursor)) return null;
|
|
4595
|
+
return adaptV2Messages(body);
|
|
4596
|
+
} catch (error2) {
|
|
4474
4597
|
console.error(
|
|
4475
|
-
`[
|
|
4598
|
+
`[getV2SessionMessages] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4476
4599
|
);
|
|
4477
4600
|
return null;
|
|
4478
4601
|
}
|
|
4479
4602
|
}
|
|
4480
|
-
async function
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
return entry != null && entry.type !== "idle";
|
|
4486
|
-
}
|
|
4487
|
-
async function createOpenCodeSession(port, directory, client) {
|
|
4488
|
-
const path = new URL(`${opencodeBase(port)}/session`);
|
|
4489
|
-
if (directory && directory.trim()) path.searchParams.set("directory", directory.trim());
|
|
4490
|
-
const requestPath = `${path.pathname}${path.search}`;
|
|
4491
|
-
const response = await requestWithClient(
|
|
4492
|
-
port,
|
|
4493
|
-
client,
|
|
4494
|
-
requestPath,
|
|
4495
|
-
{
|
|
4496
|
-
method: "POST",
|
|
4497
|
-
headers: { "Content-Type": "application/json" },
|
|
4498
|
-
body: JSON.stringify({})
|
|
4499
|
-
},
|
|
4500
|
-
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
4501
|
-
);
|
|
4502
|
-
if (!response.ok) {
|
|
4503
|
-
const text = await response.text().catch(() => "");
|
|
4504
|
-
throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ""}`);
|
|
4505
|
-
}
|
|
4506
|
-
const data = await response.json();
|
|
4507
|
-
return data.id;
|
|
4508
|
-
}
|
|
4509
|
-
async function getModelAttachmentCapability(port, model, client) {
|
|
4510
|
-
const { model: baseModel } = splitModelVariant(model);
|
|
4511
|
-
if (client?.version === "v2") {
|
|
4603
|
+
async function listV2Forms(client, sessionId) {
|
|
4604
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/form`;
|
|
4605
|
+
try {
|
|
4606
|
+
return adaptV2FormList(await readJson(await client.request(path)));
|
|
4607
|
+
} catch (error2) {
|
|
4512
4608
|
console.error(
|
|
4513
|
-
`[
|
|
4609
|
+
`[listV2Forms] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4514
4610
|
);
|
|
4515
4611
|
return null;
|
|
4516
4612
|
}
|
|
4613
|
+
}
|
|
4614
|
+
async function listV2Permissions(client, sessionId) {
|
|
4615
|
+
const path = `/api/session/${encodeURIComponent(sessionId)}/permission`;
|
|
4517
4616
|
try {
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
console.error(
|
|
4521
|
-
`[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
4522
|
-
);
|
|
4523
|
-
return null;
|
|
4524
|
-
}
|
|
4525
|
-
const body = await res.json();
|
|
4526
|
-
const providers = Array.isArray(body?.providers) ? body.providers : null;
|
|
4527
|
-
if (!providers) {
|
|
4528
|
-
console.error(
|
|
4529
|
-
`[getModelAttachmentCapability] GET /config/providers body had no providers array (port ${port})`
|
|
4530
|
-
);
|
|
4531
|
-
return null;
|
|
4532
|
-
}
|
|
4533
|
-
const slash = baseModel ? baseModel.indexOf("/") : -1;
|
|
4534
|
-
const providerId = slash > 0 ? baseModel.slice(0, slash) : void 0;
|
|
4535
|
-
let modelId = slash > 0 ? baseModel.slice(slash + 1) : void 0;
|
|
4536
|
-
const defaults2 = body?.default && typeof body.default === "object" ? body.default : void 0;
|
|
4537
|
-
let provider = providerId ? providers.find((p) => p?.id === providerId) : void 0;
|
|
4538
|
-
if (!provider && !providerId) {
|
|
4539
|
-
const defaultProviderIds = defaults2 ? Object.keys(defaults2) : [];
|
|
4540
|
-
if (defaultProviderIds.length === 1) {
|
|
4541
|
-
provider = providers.find((p) => p?.id === defaultProviderIds[0]);
|
|
4542
|
-
}
|
|
4543
|
-
}
|
|
4544
|
-
if (!provider || !provider.models) return null;
|
|
4545
|
-
if (!modelId && defaults2 && typeof provider.id === "string") {
|
|
4546
|
-
const def = defaults2[provider.id];
|
|
4547
|
-
if (typeof def === "string") modelId = def;
|
|
4548
|
-
}
|
|
4549
|
-
if (!modelId) {
|
|
4550
|
-
if (providerId) {
|
|
4551
|
-
const keys = Object.keys(provider.models);
|
|
4552
|
-
if (keys.length === 1) modelId = keys[0];
|
|
4553
|
-
}
|
|
4554
|
-
if (!modelId) return null;
|
|
4555
|
-
}
|
|
4556
|
-
const entry = provider.models[modelId];
|
|
4557
|
-
if (!entry || typeof entry !== "object") return null;
|
|
4558
|
-
if (entry.capabilities && typeof entry.capabilities === "object") {
|
|
4559
|
-
if (typeof entry.capabilities.attachment === "boolean") {
|
|
4560
|
-
return entry.capabilities.attachment;
|
|
4561
|
-
}
|
|
4562
|
-
}
|
|
4563
|
-
return typeof entry.attachment === "boolean" ? entry.attachment : null;
|
|
4564
|
-
} catch (err) {
|
|
4617
|
+
return adaptV2PermissionList(await readJson(await client.request(path)));
|
|
4618
|
+
} catch (error2) {
|
|
4565
4619
|
console.error(
|
|
4566
|
-
`[
|
|
4620
|
+
`[listV2Permissions] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4567
4621
|
);
|
|
4568
4622
|
return null;
|
|
4569
4623
|
}
|
|
4570
4624
|
}
|
|
4571
|
-
async function
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
const providerId = baseModel.slice(0, slash);
|
|
4625
|
+
async function interruptV2Session(client, sessionId) {
|
|
4626
|
+
await client.request(`/api/session/${encodeURIComponent(sessionId)}/interrupt`, {
|
|
4627
|
+
method: "POST"
|
|
4628
|
+
});
|
|
4629
|
+
}
|
|
4630
|
+
async function getV2ActiveSessions(client) {
|
|
4578
4631
|
try {
|
|
4579
|
-
const
|
|
4580
|
-
if (!
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
);
|
|
4584
|
-
return null;
|
|
4585
|
-
}
|
|
4586
|
-
const body = await res.json();
|
|
4587
|
-
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
4588
|
-
console.error(
|
|
4589
|
-
`[isPinnedProviderLoaded] GET /config/providers body was not a plain object (port ${port})`
|
|
4590
|
-
);
|
|
4591
|
-
return null;
|
|
4592
|
-
}
|
|
4593
|
-
const providers = body.providers;
|
|
4594
|
-
if (!Array.isArray(providers)) {
|
|
4595
|
-
console.error(
|
|
4596
|
-
`[isPinnedProviderLoaded] GET /config/providers body had no providers array (port ${port})`
|
|
4597
|
-
);
|
|
4598
|
-
return null;
|
|
4599
|
-
}
|
|
4600
|
-
const providerIds = providers.filter(
|
|
4601
|
-
(provider) => provider !== null && typeof provider === "object" && typeof provider.id === "string"
|
|
4602
|
-
).map((provider) => provider.id);
|
|
4603
|
-
if (providerIds.length === 0) {
|
|
4604
|
-
console.error(
|
|
4605
|
-
`[isPinnedProviderLoaded] GET /config/providers had no provider entries with string ids (port ${port})`
|
|
4606
|
-
);
|
|
4607
|
-
return null;
|
|
4608
|
-
}
|
|
4609
|
-
return providerIds.includes(providerId);
|
|
4610
|
-
} catch (err) {
|
|
4632
|
+
const body = await readJson(await client.request("/api/session/active"));
|
|
4633
|
+
if (!isRecord5(body) || !isRecord5(body.data)) return null;
|
|
4634
|
+
return body.data;
|
|
4635
|
+
} catch (error2) {
|
|
4611
4636
|
console.error(
|
|
4612
|
-
`[
|
|
4637
|
+
`[getV2ActiveSessions] GET /api/session/active failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4613
4638
|
);
|
|
4614
4639
|
return null;
|
|
4615
4640
|
}
|
|
4616
4641
|
}
|
|
4617
|
-
async function
|
|
4618
|
-
const
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4642
|
+
async function isV2SessionOngoing(client, sessionId) {
|
|
4643
|
+
const activeSessions = await getV2ActiveSessions(client);
|
|
4644
|
+
if (activeSessions === null) return null;
|
|
4645
|
+
return Object.prototype.hasOwnProperty.call(activeSessions, sessionId);
|
|
4646
|
+
}
|
|
4647
|
+
function sessionErrorReason(value) {
|
|
4648
|
+
if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
|
|
4649
|
+
if (isRecord5(value)) {
|
|
4650
|
+
const data = isRecord5(value.data) ? value.data : void 0;
|
|
4651
|
+
const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
|
|
4652
|
+
if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
4626
4653
|
}
|
|
4627
|
-
|
|
4628
|
-
|
|
4654
|
+
return "OpenCode reported a session error with no details";
|
|
4655
|
+
}
|
|
4656
|
+
function adaptV2SessionErrorEvent(value) {
|
|
4657
|
+
let parsed = value;
|
|
4658
|
+
if (typeof value === "string") {
|
|
4629
4659
|
try {
|
|
4630
|
-
|
|
4631
|
-
} catch (
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
);
|
|
4635
|
-
dataUrl = null;
|
|
4660
|
+
parsed = JSON.parse(value);
|
|
4661
|
+
} catch (error2) {
|
|
4662
|
+
void error2;
|
|
4663
|
+
return null;
|
|
4636
4664
|
}
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4665
|
+
}
|
|
4666
|
+
if (!isRecord5(parsed)) return null;
|
|
4667
|
+
try {
|
|
4668
|
+
const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
|
|
4669
|
+
if (establishedShape) return establishedShape;
|
|
4670
|
+
} catch (error2) {
|
|
4671
|
+
void error2;
|
|
4672
|
+
}
|
|
4673
|
+
const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord5);
|
|
4674
|
+
for (const event of candidates) {
|
|
4675
|
+
if (event.type !== "session.error") continue;
|
|
4676
|
+
const properties = [event.properties, event.data, event].find(isRecord5);
|
|
4677
|
+
if (!properties) continue;
|
|
4678
|
+
const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
|
|
4679
|
+
if (!sessionId) continue;
|
|
4680
|
+
return {
|
|
4681
|
+
sessionId,
|
|
4682
|
+
reason: sessionErrorReason(properties.error ?? properties)
|
|
4683
|
+
};
|
|
4684
|
+
}
|
|
4685
|
+
return null;
|
|
4686
|
+
}
|
|
4687
|
+
async function readV2SessionErrorStream(client, options) {
|
|
4688
|
+
let reader = null;
|
|
4689
|
+
try {
|
|
4690
|
+
const response = await client.request("/api/event", {
|
|
4691
|
+
headers: { accept: "text/event-stream" },
|
|
4692
|
+
signal: options.signal
|
|
4693
|
+
});
|
|
4694
|
+
if (!response.ok || !response.body) {
|
|
4695
|
+
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
4646
4696
|
}
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4697
|
+
reader = response.body.getReader();
|
|
4698
|
+
const decoder = new TextDecoder();
|
|
4699
|
+
let buffer = "";
|
|
4700
|
+
const processLine = (line) => {
|
|
4701
|
+
const trimmed = line.trimEnd();
|
|
4702
|
+
if (!trimmed.startsWith("data:")) return;
|
|
4703
|
+
const event = adaptV2SessionErrorEvent(trimmed.slice("data:".length).replace(/^ /, ""));
|
|
4704
|
+
if (event) options.onSessionError(event);
|
|
4705
|
+
};
|
|
4706
|
+
while (true) {
|
|
4707
|
+
const { done, value } = await reader.read();
|
|
4708
|
+
if (done) return { reason: "ended" };
|
|
4709
|
+
buffer += decoder.decode(value, { stream: true });
|
|
4710
|
+
const lines = buffer.split("\n");
|
|
4711
|
+
buffer = lines.pop() ?? "";
|
|
4712
|
+
for (const line of lines) processLine(line);
|
|
4650
4713
|
}
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
}
|
|
4657
|
-
|
|
4714
|
+
} catch (error2) {
|
|
4715
|
+
if (options.signal.aborted) return { reason: "aborted" };
|
|
4716
|
+
return {
|
|
4717
|
+
reason: "unavailable",
|
|
4718
|
+
detail: error2 instanceof Error ? error2.message : String(error2)
|
|
4719
|
+
};
|
|
4720
|
+
} finally {
|
|
4721
|
+
if (reader) void reader.cancel().catch(() => void 0);
|
|
4658
4722
|
}
|
|
4659
|
-
return { parts, outcomes, capabilityUnknown };
|
|
4660
4723
|
}
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
const model = value.slice(0, hashIndex).trim() || void 0;
|
|
4667
|
-
const variant = value.slice(hashIndex + 1).trim() || void 0;
|
|
4668
|
-
return { model, variant };
|
|
4724
|
+
|
|
4725
|
+
// src/lib/opencode/session.ts
|
|
4726
|
+
var ALL_HTTP_STATUSES2 = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
4727
|
+
function timedFetch(input, init) {
|
|
4728
|
+
return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
|
|
4669
4729
|
}
|
|
4670
|
-
function
|
|
4671
|
-
|
|
4672
|
-
const { model, variant } = splitModelVariant(options?.model);
|
|
4673
|
-
if (model) {
|
|
4674
|
-
const slashIndex = model.indexOf("/");
|
|
4675
|
-
if (slashIndex !== -1) {
|
|
4676
|
-
body.model = {
|
|
4677
|
-
providerID: model.substring(0, slashIndex),
|
|
4678
|
-
modelID: model.substring(slashIndex + 1)
|
|
4679
|
-
};
|
|
4680
|
-
}
|
|
4681
|
-
}
|
|
4682
|
-
if (variant) body.variant = variant;
|
|
4730
|
+
function requestWithClient(port, client, path, init, options) {
|
|
4731
|
+
return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
|
|
4683
4732
|
}
|
|
4684
|
-
|
|
4733
|
+
function opencodeBase(port) {
|
|
4734
|
+
return `http://127.0.0.1:${port}`;
|
|
4735
|
+
}
|
|
4736
|
+
async function getOpenCodeDirectory(port, client) {
|
|
4685
4737
|
try {
|
|
4686
|
-
const
|
|
4687
|
-
|
|
4688
|
-
|
|
4738
|
+
const res = await requestWithClient(port, client, "/path");
|
|
4739
|
+
if (!res.ok) return null;
|
|
4740
|
+
const body = await res.json();
|
|
4741
|
+
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;
|
|
4742
|
+
return dir && dir.trim() ? dir.trim() : null;
|
|
4689
4743
|
} catch (error2) {
|
|
4690
4744
|
console.error(
|
|
4691
|
-
`[
|
|
4745
|
+
`[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4692
4746
|
);
|
|
4693
4747
|
return null;
|
|
4694
4748
|
}
|
|
4695
4749
|
}
|
|
4696
|
-
async function
|
|
4750
|
+
async function resolveOpenCodeDirectoryForVersion(apiVersion, port, client) {
|
|
4751
|
+
if (apiVersion === "v2") return client ? getOpenCodeDirectoryV2(client) : null;
|
|
4752
|
+
return getOpenCodeDirectory(port, client ?? void 0);
|
|
4753
|
+
}
|
|
4754
|
+
function roleOf2(m) {
|
|
4755
|
+
if (!m || typeof m !== "object") return void 0;
|
|
4756
|
+
if (typeof m.role === "string") return m.role;
|
|
4757
|
+
const infoRole = m.info?.role;
|
|
4758
|
+
return typeof infoRole === "string" ? infoRole : void 0;
|
|
4759
|
+
}
|
|
4760
|
+
function completedOf(m) {
|
|
4761
|
+
if (!m || typeof m !== "object") return void 0;
|
|
4762
|
+
return m.info?.time?.completed ?? m.time?.completed;
|
|
4763
|
+
}
|
|
4764
|
+
function createdOf(m) {
|
|
4765
|
+
if (!m || typeof m !== "object") return void 0;
|
|
4766
|
+
return m.info?.time?.created ?? m.time?.created;
|
|
4767
|
+
}
|
|
4768
|
+
function idOf2(m) {
|
|
4769
|
+
if (!m || typeof m !== "object") return void 0;
|
|
4770
|
+
if (typeof m.id === "string") return m.id;
|
|
4771
|
+
const infoId = m.info?.id;
|
|
4772
|
+
return typeof infoId === "string" ? infoId : void 0;
|
|
4773
|
+
}
|
|
4774
|
+
function parentIdOf2(m) {
|
|
4775
|
+
if (!m || typeof m !== "object") return void 0;
|
|
4776
|
+
if (typeof m.parentID === "string") return m.parentID;
|
|
4777
|
+
const infoParent = m.info?.parentID;
|
|
4778
|
+
return typeof infoParent === "string" ? infoParent : void 0;
|
|
4779
|
+
}
|
|
4780
|
+
function finishOf(m) {
|
|
4781
|
+
if (!m || typeof m !== "object") return void 0;
|
|
4782
|
+
if (typeof m.finish === "string") return m.finish;
|
|
4783
|
+
const infoFinish = m.info?.finish;
|
|
4784
|
+
return typeof infoFinish === "string" ? infoFinish : void 0;
|
|
4785
|
+
}
|
|
4786
|
+
function errorOf(m) {
|
|
4787
|
+
if (!m || typeof m !== "object") return void 0;
|
|
4788
|
+
return m.info?.error ?? m.error;
|
|
4789
|
+
}
|
|
4790
|
+
function isTerminalStopAnswer(m) {
|
|
4791
|
+
return completedOf(m) != null && errorOf(m) == null && finishOf(m) === "stop";
|
|
4792
|
+
}
|
|
4793
|
+
function isAssistantInFlight(m) {
|
|
4794
|
+
if (completedOf(m) == null) return true;
|
|
4795
|
+
if (errorOf(m) != null) return false;
|
|
4796
|
+
return finishOf(m) === "tool-calls";
|
|
4797
|
+
}
|
|
4798
|
+
async function getSessionMessages(port, sessionId, client) {
|
|
4697
4799
|
try {
|
|
4698
|
-
const
|
|
4699
|
-
const
|
|
4800
|
+
const path = `/session/${sessionId}/message`;
|
|
4801
|
+
const res = await requestWithClient(port, client, path);
|
|
4802
|
+
if (!res.ok) return null;
|
|
4803
|
+
const body = await res.json();
|
|
4700
4804
|
return Array.isArray(body) ? body : null;
|
|
4701
4805
|
} catch (error2) {
|
|
4702
4806
|
console.error(
|
|
4703
|
-
`[
|
|
4807
|
+
`[getSessionMessages] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4704
4808
|
);
|
|
4705
4809
|
return null;
|
|
4706
4810
|
}
|
|
4707
4811
|
}
|
|
4708
|
-
function
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
if (!messages || messages.length === 0) return null;
|
|
4714
|
-
let best = null;
|
|
4715
|
-
for (const message of messages) {
|
|
4716
|
-
if (roleOf2(message) !== "user") continue;
|
|
4717
|
-
const id = idOf2(message);
|
|
4718
|
-
if (typeof id !== "string" || excludeIds?.has(id)) continue;
|
|
4719
|
-
if (messageText(message) !== content) continue;
|
|
4720
|
-
const created = createdOf(message) ?? 0;
|
|
4721
|
-
if (best === null || created > best.created) best = { id, created };
|
|
4722
|
-
}
|
|
4723
|
-
return best?.id ?? null;
|
|
4812
|
+
async function fetchSessionMessages(port, sessionId, client) {
|
|
4813
|
+
const response = await requestWithClient(port, client, `/session/${sessionId}/message`);
|
|
4814
|
+
if (!response.ok) return null;
|
|
4815
|
+
const body = await response.json();
|
|
4816
|
+
return Array.isArray(body) ? body : null;
|
|
4724
4817
|
}
|
|
4725
|
-
async function
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4818
|
+
async function pollSessionMessagesForRedrive(port, sessionId, client) {
|
|
4819
|
+
try {
|
|
4820
|
+
const response = await requestWithClient(
|
|
4821
|
+
port,
|
|
4822
|
+
client,
|
|
4823
|
+
`/session/${sessionId}/message`,
|
|
4824
|
+
void 0,
|
|
4825
|
+
{ allowStatuses: ALL_HTTP_STATUSES2 }
|
|
4826
|
+
);
|
|
4827
|
+
if (!response.ok) {
|
|
4828
|
+
return { ok: false, status: response.status, body: await response.text(), malformed: false };
|
|
4735
4829
|
}
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
parts: fileParts,
|
|
4743
|
-
outcomes,
|
|
4744
|
-
capabilityUnknown
|
|
4745
|
-
} = await buildFileParts(attachments, capable);
|
|
4746
|
-
parts.push(...fileParts);
|
|
4747
|
-
if (attachments.onOutcomes) pendingOutcomes = { outcomes, capabilityUnknown };
|
|
4748
|
-
}
|
|
4749
|
-
const body = {
|
|
4750
|
-
parts
|
|
4751
|
-
};
|
|
4752
|
-
applyModelOptions(body, options);
|
|
4753
|
-
const res = await requestWithClient(
|
|
4754
|
-
port,
|
|
4755
|
-
client,
|
|
4756
|
-
`/session/${sessionId}/prompt_async`,
|
|
4757
|
-
{
|
|
4758
|
-
method: "POST",
|
|
4759
|
-
headers: { "Content-Type": "application/json" },
|
|
4760
|
-
body: JSON.stringify(body)
|
|
4761
|
-
},
|
|
4762
|
-
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
4763
|
-
);
|
|
4764
|
-
if (res.status < 200 || res.status >= 300) {
|
|
4765
|
-
const text = await res.text().catch(() => "");
|
|
4766
|
-
const { variant } = splitModelVariant(options?.model);
|
|
4767
|
-
throw new Error(
|
|
4768
|
-
`OpenCode prompt_async failed: HTTP ${res.status}${text ? `: ${text}` : ""}${variant ? ` (variant: ${variant})` : ""}`
|
|
4830
|
+
const body = await response.json();
|
|
4831
|
+
if (!Array.isArray(body)) return { ok: false, status: null, body: "", malformed: true };
|
|
4832
|
+
return { ok: true, messages: body };
|
|
4833
|
+
} catch (error2) {
|
|
4834
|
+
console.error(
|
|
4835
|
+
`[pollSessionMessagesForRedrive] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4769
4836
|
);
|
|
4837
|
+
return { ok: false, status: null, body: "", malformed: false };
|
|
4770
4838
|
}
|
|
4771
|
-
const READ_BACK_ATTEMPTS = 5;
|
|
4772
|
-
const READ_BACK_DELAY_MS = 150;
|
|
4773
|
-
for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
|
|
4774
|
-
const after = await getSessionMessages(port, sessionId, client);
|
|
4775
|
-
const dispatchedUserMessageId = findDispatchedUserMessageId(after, content, knownUserIds);
|
|
4776
|
-
if (dispatchedUserMessageId !== null) {
|
|
4777
|
-
if (pendingOutcomes && attachments?.onOutcomes) attachments.onOutcomes(pendingOutcomes);
|
|
4778
|
-
return dispatchedUserMessageId;
|
|
4779
|
-
}
|
|
4780
|
-
if (attempt < READ_BACK_ATTEMPTS - 1) {
|
|
4781
|
-
await new Promise((resolve5) => setTimeout(resolve5, READ_BACK_DELAY_MS));
|
|
4782
|
-
}
|
|
4783
|
-
}
|
|
4784
|
-
return null;
|
|
4785
4839
|
}
|
|
4786
|
-
function
|
|
4787
|
-
if (!messages || messages.length === 0) return
|
|
4788
|
-
const
|
|
4789
|
-
|
|
4790
|
-
);
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4840
|
+
function isSessionActivelyGenerating(messages) {
|
|
4841
|
+
if (!messages || messages.length === 0) return false;
|
|
4842
|
+
const last = messages[messages.length - 1];
|
|
4843
|
+
if (roleOf2(last) !== "assistant") return false;
|
|
4844
|
+
return completedOf(last) == null;
|
|
4845
|
+
}
|
|
4846
|
+
var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
|
|
4847
|
+
function sessionLastActivityMs(session) {
|
|
4848
|
+
const candidates = [
|
|
4849
|
+
session.time?.updated,
|
|
4850
|
+
session.time?.created,
|
|
4851
|
+
session.time_updated,
|
|
4852
|
+
session.time_created,
|
|
4853
|
+
session.updated,
|
|
4854
|
+
session.created
|
|
4855
|
+
];
|
|
4856
|
+
for (const c of candidates) {
|
|
4857
|
+
if (typeof c === "number" && Number.isFinite(c)) return c;
|
|
4796
4858
|
}
|
|
4797
4859
|
return null;
|
|
4798
4860
|
}
|
|
4799
|
-
function
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
|
|
4819
|
-
if (userIndex === -1) return null;
|
|
4820
|
-
let last = null;
|
|
4821
|
-
let lastAssistantIndex = -1;
|
|
4822
|
-
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
4823
|
-
const role = roleOf2(messages[i]);
|
|
4824
|
-
if (role === "user") break;
|
|
4825
|
-
if (role === "assistant") {
|
|
4826
|
-
last = messages[i];
|
|
4827
|
-
lastAssistantIndex = i;
|
|
4861
|
+
function sessionListPath(options) {
|
|
4862
|
+
const params = new URLSearchParams();
|
|
4863
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
4864
|
+
const query = params.toString();
|
|
4865
|
+
return query ? `/session?${query}` : "/session";
|
|
4866
|
+
}
|
|
4867
|
+
async function listSessionPage(port, client, options) {
|
|
4868
|
+
const path = sessionListPath(options);
|
|
4869
|
+
try {
|
|
4870
|
+
const res = await requestWithClient(
|
|
4871
|
+
port,
|
|
4872
|
+
client,
|
|
4873
|
+
path,
|
|
4874
|
+
options?.signal ? { signal: options.signal } : void 0,
|
|
4875
|
+
{ allowStatuses: ALL_HTTP_STATUSES2 }
|
|
4876
|
+
);
|
|
4877
|
+
if (!res.ok) {
|
|
4878
|
+
console.error(`[listSessionPage] GET ${path} returned HTTP ${res.status} (port ${port})`);
|
|
4879
|
+
return null;
|
|
4828
4880
|
}
|
|
4881
|
+
const body = await res.json();
|
|
4882
|
+
return Array.isArray(body) ? body : null;
|
|
4883
|
+
} catch (error2) {
|
|
4884
|
+
if (options?.signal?.aborted) return null;
|
|
4885
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4886
|
+
console.error(`[listSessionPage] GET ${path} failed (port ${port}): ${errorName}`);
|
|
4887
|
+
return null;
|
|
4829
4888
|
}
|
|
4830
|
-
if (last === null || errorOf(last) == null) return last;
|
|
4831
|
-
for (let i = lastAssistantIndex; i > userIndex; i--) {
|
|
4832
|
-
const m = messages[i];
|
|
4833
|
-
if (roleOf2(m) === "user") break;
|
|
4834
|
-
if (roleOf2(m) === "assistant" && isTerminalStopAnswer(m)) return m;
|
|
4835
|
-
}
|
|
4836
|
-
return last;
|
|
4837
4889
|
}
|
|
4838
|
-
function
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
const
|
|
4845
|
-
|
|
4846
|
-
if (role === "assistant") assistants.push(message);
|
|
4890
|
+
async function listSessions(port, client, options) {
|
|
4891
|
+
if (client?.version === "v2") return listV2Sessions(client, options?.signal);
|
|
4892
|
+
return listSessionPage(port, client, options);
|
|
4893
|
+
}
|
|
4894
|
+
async function listAllSessions(port, client, signal) {
|
|
4895
|
+
if (client?.version === "v2") {
|
|
4896
|
+
const sessions2 = await listV2Sessions(client, signal);
|
|
4897
|
+
return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
|
|
4847
4898
|
}
|
|
4848
|
-
|
|
4899
|
+
const sessions = await listSessionPage(port, client, {
|
|
4900
|
+
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS,
|
|
4901
|
+
signal
|
|
4902
|
+
});
|
|
4903
|
+
if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
|
|
4904
|
+
if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
|
|
4905
|
+
return { sessions: null, reason: "session-enumeration-limit-bound" };
|
|
4906
|
+
}
|
|
4907
|
+
return { sessions };
|
|
4849
4908
|
}
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
const metadata = state.metadata;
|
|
4873
|
-
if (!metadata || typeof metadata !== "object") continue;
|
|
4874
|
-
const sessionId = metadata.sessionId;
|
|
4875
|
-
if (typeof sessionId !== "string" || sessionId.length === 0 || seen.has(sessionId)) continue;
|
|
4876
|
-
seen.add(sessionId);
|
|
4877
|
-
const start = state.time?.start;
|
|
4878
|
-
refs.push({
|
|
4879
|
-
sessionId,
|
|
4880
|
-
startedAtMs: typeof start === "number" && Number.isFinite(start) ? start : null
|
|
4881
|
-
});
|
|
4909
|
+
var INTERRUPT_NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
4910
|
+
"ABORT_ERR",
|
|
4911
|
+
"EAI_AGAIN",
|
|
4912
|
+
"ECONNREFUSED",
|
|
4913
|
+
"ECONNRESET",
|
|
4914
|
+
"ENETUNREACH",
|
|
4915
|
+
"ENOTFOUND",
|
|
4916
|
+
"ETIMEDOUT",
|
|
4917
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
4918
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
4919
|
+
"UND_ERR_SOCKET"
|
|
4920
|
+
]);
|
|
4921
|
+
function interruptFailureDiagnostic(error2) {
|
|
4922
|
+
const message = error2 instanceof Error ? error2.message : "";
|
|
4923
|
+
const httpStatus = /^OpenCode request failed: HTTP ([1-5]\d{2})(?::|$)/.exec(message)?.[1];
|
|
4924
|
+
if (httpStatus) return `HTTP ${httpStatus}`;
|
|
4925
|
+
const candidates = [error2, error2 instanceof Error ? error2.cause : void 0];
|
|
4926
|
+
for (const candidate of candidates) {
|
|
4927
|
+
if (typeof candidate !== "object" || candidate === null || !("code" in candidate)) continue;
|
|
4928
|
+
const code = candidate.code;
|
|
4929
|
+
if (typeof code === "string" && INTERRUPT_NETWORK_CODES.has(code)) {
|
|
4930
|
+
return redactDiagnosticText(`network ${code}`).slice(0, 120);
|
|
4882
4931
|
}
|
|
4883
4932
|
}
|
|
4884
|
-
|
|
4885
|
-
}
|
|
4886
|
-
function finiteNumber(value) {
|
|
4887
|
-
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
4888
|
-
}
|
|
4889
|
-
function taskCallModel(value) {
|
|
4890
|
-
if (!value || typeof value !== "object") return null;
|
|
4891
|
-
const model = value;
|
|
4892
|
-
const modelID = typeof model.modelID === "string" ? model.modelID : void 0;
|
|
4893
|
-
const providerID = typeof model.providerID === "string" ? model.providerID : void 0;
|
|
4894
|
-
return modelID || providerID ? { modelID, providerID } : null;
|
|
4933
|
+
const name = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4934
|
+
return redactDiagnosticText(`request threw ${name}`).slice(0, 120);
|
|
4895
4935
|
}
|
|
4896
|
-
function
|
|
4897
|
-
if (
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
continue;
|
|
4904
|
-
}
|
|
4905
|
-
const rawName = part.state.input?.subagent_type;
|
|
4906
|
-
const subagentName = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : rawName === void 0 ? "general" : "unknown";
|
|
4907
|
-
const metadata = part.state.metadata;
|
|
4908
|
-
calls.push({
|
|
4909
|
-
callID: part.callID,
|
|
4910
|
-
subagentName,
|
|
4911
|
-
childSessionId: typeof metadata?.sessionId === "string" ? metadata.sessionId : null,
|
|
4912
|
-
parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
|
|
4913
|
-
model: taskCallModel(metadata?.model),
|
|
4914
|
-
status: part.state.status ?? "unknown",
|
|
4915
|
-
timeStart: finiteNumber(part.state.time?.start),
|
|
4916
|
-
timeEnd: finiteNumber(part.state.time?.end)
|
|
4917
|
-
});
|
|
4936
|
+
async function interruptSession(port, id, client) {
|
|
4937
|
+
if (client?.version === "v2") {
|
|
4938
|
+
try {
|
|
4939
|
+
await interruptV2Session(client, id);
|
|
4940
|
+
return { kind: "confirmed_stopped" };
|
|
4941
|
+
} catch (error2) {
|
|
4942
|
+
return { kind: "not_confirmed", diagnostic: interruptFailureDiagnostic(error2) };
|
|
4918
4943
|
}
|
|
4919
4944
|
}
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
for (const window of eligibleWindows) assignments.set(window.callID, []);
|
|
4928
|
-
const unattributed = [];
|
|
4929
|
-
for (const message of messages ?? []) {
|
|
4930
|
-
if (roleOf2(message) !== "assistant") continue;
|
|
4931
|
-
const created = finiteNumber(createdOf(message));
|
|
4932
|
-
const matching = created === null ? [] : eligibleWindows.filter(
|
|
4933
|
-
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
4945
|
+
try {
|
|
4946
|
+
const res = await requestWithClient(
|
|
4947
|
+
port,
|
|
4948
|
+
client,
|
|
4949
|
+
`/session/${id}/abort`,
|
|
4950
|
+
{ method: "POST" },
|
|
4951
|
+
{ allowStatuses: ALL_HTTP_STATUSES2 }
|
|
4934
4952
|
);
|
|
4935
|
-
if (
|
|
4936
|
-
|
|
4937
|
-
continue;
|
|
4953
|
+
if (res.status >= 200 && res.status < 300) {
|
|
4954
|
+
return { kind: "confirmed_stopped", status: res.status };
|
|
4938
4955
|
}
|
|
4939
|
-
|
|
4940
|
-
|
|
4956
|
+
return { kind: "not_confirmed", status: res.status };
|
|
4957
|
+
} catch (error2) {
|
|
4958
|
+
return { kind: "threw", diagnostic: interruptFailureDiagnostic(error2) };
|
|
4941
4959
|
}
|
|
4942
|
-
return {
|
|
4943
|
-
invocations: eligibleWindows.map((window) => {
|
|
4944
|
-
const assigned = assignments.get(window.callID) ?? [];
|
|
4945
|
-
return { callID: window.callID, messages: assigned, usage: sumAssistantUsage(assigned) };
|
|
4946
|
-
}),
|
|
4947
|
-
unattributed
|
|
4948
|
-
};
|
|
4949
4960
|
}
|
|
4950
|
-
function
|
|
4951
|
-
if (
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
let sawCost = false;
|
|
4963
|
-
let toolCallCount = 0;
|
|
4964
|
-
let modelId = null;
|
|
4965
|
-
let providerId = null;
|
|
4966
|
-
for (const m of selected) {
|
|
4967
|
-
toolCallCount += m.parts?.filter((part) => part.type === "tool").length ?? 0;
|
|
4968
|
-
const info = m.info;
|
|
4969
|
-
if (!info) continue;
|
|
4970
|
-
const tokens = info.tokens;
|
|
4971
|
-
if (tokens) {
|
|
4972
|
-
sawAnyUsage = true;
|
|
4973
|
-
inputSum += tokens.input ?? 0;
|
|
4974
|
-
outputSum += tokens.output ?? 0;
|
|
4975
|
-
reasoningSum += tokens.reasoning ?? 0;
|
|
4976
|
-
cacheReadSum += tokens.cache?.read ?? 0;
|
|
4977
|
-
cacheWriteSum += tokens.cache?.write ?? 0;
|
|
4978
|
-
}
|
|
4979
|
-
if (typeof info.cost === "number") {
|
|
4980
|
-
sawAnyUsage = true;
|
|
4981
|
-
sawCost = true;
|
|
4982
|
-
costSum += info.cost;
|
|
4983
|
-
}
|
|
4984
|
-
if (typeof info.modelID === "string") {
|
|
4985
|
-
sawAnyUsage = true;
|
|
4986
|
-
modelId = info.modelID;
|
|
4987
|
-
}
|
|
4988
|
-
if (typeof info.providerID === "string") {
|
|
4989
|
-
sawAnyUsage = true;
|
|
4990
|
-
providerId = info.providerID;
|
|
4961
|
+
async function deleteSession(port, id, client) {
|
|
4962
|
+
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
4963
|
+
try {
|
|
4964
|
+
const res = await requestWithClient(
|
|
4965
|
+
port,
|
|
4966
|
+
client,
|
|
4967
|
+
`/session/${id}`,
|
|
4968
|
+
{ method: "DELETE" },
|
|
4969
|
+
{ allowStatuses: ALL_HTTP_STATUSES2 }
|
|
4970
|
+
);
|
|
4971
|
+
if (res.status >= 200 && res.status < 300) {
|
|
4972
|
+
return { ok: true, status: res.status };
|
|
4991
4973
|
}
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
usage_tokens_cache_read: cacheReadSum,
|
|
5001
|
-
usage_tokens_cache_write: cacheWriteSum,
|
|
5002
|
-
// NULL means OpenCode never reported a cost; it is distinct from a genuine
|
|
5003
|
-
// zero-cost message, which sets `sawCost` with `costSum === 0`.
|
|
5004
|
-
usage_cost_usd: sawCost ? costSum : null,
|
|
5005
|
-
turn_tool_calls: toolCallCount
|
|
5006
|
-
};
|
|
5007
|
-
}
|
|
5008
|
-
function messageUsage(messages, userMessageId) {
|
|
5009
|
-
if (!messages || messages.length === 0) return null;
|
|
5010
|
-
const byParentAll = messages.filter(
|
|
5011
|
-
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
|
|
5012
|
-
);
|
|
5013
|
-
let correlated;
|
|
5014
|
-
if (byParentAll.length > 0) {
|
|
5015
|
-
correlated = byParentAll;
|
|
5016
|
-
} else {
|
|
5017
|
-
correlated = assistantMessagesInTurnBlock(messages, userMessageId);
|
|
5018
|
-
}
|
|
5019
|
-
if (correlated.length === 0) return null;
|
|
5020
|
-
return sumAssistantUsage(correlated);
|
|
5021
|
-
}
|
|
5022
|
-
function messageRunState(messages, userMessageId) {
|
|
5023
|
-
if (!messages || messages.length === 0) return "unknown";
|
|
5024
|
-
const hasUser = messages.some((m) => idOf2(m) === userMessageId);
|
|
5025
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5026
|
-
if (!hasUser) {
|
|
5027
|
-
if (!reply) return "unknown";
|
|
5028
|
-
}
|
|
5029
|
-
if (!reply) return "queued";
|
|
5030
|
-
if (isAssistantInFlight(reply)) return "running";
|
|
5031
|
-
if (errorOf(reply) != null) return "failed";
|
|
5032
|
-
if (isAmbiguousTerminalFinish(reply)) return "running";
|
|
5033
|
-
return "done";
|
|
5034
|
-
}
|
|
5035
|
-
function isPreamblePinnedRunning(messages, userMessageId) {
|
|
5036
|
-
if (messageRunState(messages, userMessageId) !== "running") return false;
|
|
5037
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5038
|
-
return completedOf(reply) != null && finishOf(reply) === "tool-calls";
|
|
5039
|
-
}
|
|
5040
|
-
function isMidFlightPinnedRunning(messages, userMessageId) {
|
|
5041
|
-
if (messageRunState(messages, userMessageId) !== "running") return false;
|
|
5042
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5043
|
-
return reply !== null && isAssistantInFlight(reply) && completedOf(reply) == null;
|
|
5044
|
-
}
|
|
5045
|
-
function isB2AbandonmentConfirmed(params) {
|
|
5046
|
-
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
|
|
5047
|
-
}
|
|
5048
|
-
function isMidFlightOrphanConfirmed(params) {
|
|
5049
|
-
return params.pinnedForMs >= params.minPinnedMs && params.notOngoingStreak >= params.minNotOngoingStreak;
|
|
5050
|
-
}
|
|
5051
|
-
function isAmbiguousTerminalFinish(m) {
|
|
5052
|
-
if (completedOf(m) == null) return false;
|
|
5053
|
-
if (errorOf(m) != null) return false;
|
|
5054
|
-
const finish = finishOf(m);
|
|
5055
|
-
return finish !== "tool-calls" && finish !== "stop";
|
|
5056
|
-
}
|
|
5057
|
-
function isAmbiguousFinishPinnedRunning(messages, userMessageId) {
|
|
5058
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5059
|
-
return isAmbiguousTerminalFinish(reply);
|
|
5060
|
-
}
|
|
5061
|
-
function isAmbiguousFinishResolved(params) {
|
|
5062
|
-
return params.sessionOngoing === false || params.sessionOngoing !== true && params.pinnedForMs >= params.maxPinnedMs;
|
|
5063
|
-
}
|
|
5064
|
-
function messageError(messages, userMessageId) {
|
|
5065
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5066
|
-
const error2 = errorOf(reply);
|
|
5067
|
-
if (error2 == null) return null;
|
|
5068
|
-
let rendered;
|
|
5069
|
-
if (typeof error2 === "string") {
|
|
5070
|
-
rendered = error2;
|
|
5071
|
-
} else if (typeof error2 === "object") {
|
|
5072
|
-
const e = error2;
|
|
5073
|
-
const dataMessage = e.data?.message;
|
|
5074
|
-
if (typeof dataMessage === "string") {
|
|
5075
|
-
rendered = dataMessage;
|
|
5076
|
-
} else if (typeof e.message === "string") {
|
|
5077
|
-
rendered = e.message;
|
|
5078
|
-
} else {
|
|
5079
|
-
rendered = "The agent run failed.";
|
|
4974
|
+
try {
|
|
4975
|
+
const body = await res.text();
|
|
4976
|
+
return body ? { ok: false, status: res.status, body } : { ok: false, status: res.status };
|
|
4977
|
+
} catch (error2) {
|
|
4978
|
+
console.error(
|
|
4979
|
+
`[deleteSession] DELETE /session/${id} response body read failed for HTTP ${res.status}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4980
|
+
);
|
|
4981
|
+
return { ok: false, status: res.status };
|
|
5080
4982
|
}
|
|
5081
|
-
}
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
) : void 0;
|
|
5087
|
-
return typeof toolPart?.tool === "string" && toolPart.tool.length > 0 ? `${rendered} (tool "${toolPart.tool}")` : rendered;
|
|
5088
|
-
}
|
|
5089
|
-
function describeAbandonedReplyShape(messages, userMessageId) {
|
|
5090
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5091
|
-
if (!reply) return null;
|
|
5092
|
-
const parts = Array.isArray(reply.parts) ? reply.parts : [];
|
|
5093
|
-
const lastTool = [...parts].reverse().find((part) => part?.type === "tool");
|
|
5094
|
-
return {
|
|
5095
|
-
partTypes: parts.slice(-12).map((part) => typeof part?.type === "string" ? part.type : "(unknown)"),
|
|
5096
|
-
droppedPartCount: Math.max(0, parts.length - 12),
|
|
5097
|
-
lastToolName: typeof lastTool?.tool === "string" && lastTool.tool.length > 0 ? lastTool.tool : null,
|
|
5098
|
-
lastToolStatus: typeof lastTool?.state?.status === "string" && lastTool.state.status.length > 0 ? lastTool.state.status : null
|
|
5099
|
-
};
|
|
5100
|
-
}
|
|
5101
|
-
function isAbortedTerminalReply(messages, userMessageId) {
|
|
5102
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5103
|
-
const error2 = errorOf(reply);
|
|
5104
|
-
if (error2 == null) return false;
|
|
5105
|
-
if (typeof error2 === "string") return error2.trim() === "Aborted";
|
|
5106
|
-
if (typeof error2 === "object") {
|
|
5107
|
-
const e = error2;
|
|
5108
|
-
if (e.name === "MessageAbortedError") return true;
|
|
5109
|
-
if (e.name === "AbortError") return true;
|
|
5110
|
-
const dataMessage = e.data?.message;
|
|
5111
|
-
const rendered = typeof dataMessage === "string" ? dataMessage : typeof e.message === "string" ? e.message : null;
|
|
5112
|
-
return rendered != null && rendered.trim() === "Aborted";
|
|
4983
|
+
} catch (error2) {
|
|
4984
|
+
console.error(
|
|
4985
|
+
`[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4986
|
+
);
|
|
4987
|
+
return { ok: false, status: "threw" };
|
|
5113
4988
|
}
|
|
5114
|
-
return false;
|
|
5115
4989
|
}
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
const data = e.data;
|
|
5127
|
-
const statusCode = typeof data?.statusCode === "number" && Number.isFinite(data.statusCode) ? data.statusCode : null;
|
|
5128
|
-
return { name, statusCode };
|
|
5129
|
-
}
|
|
5130
|
-
function classifyReplyAuthError(reply) {
|
|
5131
|
-
const error2 = errorOf(reply);
|
|
5132
|
-
if (error2 == null || typeof error2 !== "object") return null;
|
|
5133
|
-
const e = error2;
|
|
5134
|
-
const replyProviderId = reply?.info?.providerID ?? null;
|
|
5135
|
-
const replyModelId = reply?.info?.modelID ?? null;
|
|
5136
|
-
const data = e.data;
|
|
5137
|
-
if (e.name === "ProviderAuthError") {
|
|
5138
|
-
const providerId = typeof data?.providerID === "string" && data.providerID || replyProviderId;
|
|
5139
|
-
return { kind: "model_auth", providerId, modelId: replyModelId, reason: "missing" };
|
|
5140
|
-
}
|
|
5141
|
-
if (e.name === "APIError") {
|
|
5142
|
-
const statusCode = data?.statusCode;
|
|
5143
|
-
if (statusCode === 401 || statusCode === 403) {
|
|
5144
|
-
return {
|
|
5145
|
-
kind: "model_auth",
|
|
5146
|
-
providerId: replyProviderId,
|
|
5147
|
-
modelId: replyModelId,
|
|
5148
|
-
reason: "rejected"
|
|
5149
|
-
};
|
|
5150
|
-
}
|
|
5151
|
-
}
|
|
5152
|
-
const errorMessage4 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
|
|
5153
|
-
if (errorMessage4 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage4.includes(knownMessage))) {
|
|
5154
|
-
return {
|
|
5155
|
-
kind: "model_auth",
|
|
5156
|
-
providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
|
|
5157
|
-
modelId: replyModelId,
|
|
5158
|
-
reason: "rejected"
|
|
5159
|
-
};
|
|
4990
|
+
async function sessionExists(port, id, client) {
|
|
4991
|
+
try {
|
|
4992
|
+
const res = await requestWithClient(port, client, `/session/${id}`, void 0, {
|
|
4993
|
+
allowStatuses: [404]
|
|
4994
|
+
});
|
|
4995
|
+
if (res.status >= 200 && res.status < 300) return true;
|
|
4996
|
+
if (res.status === 404) return false;
|
|
4997
|
+
return null;
|
|
4998
|
+
} catch {
|
|
4999
|
+
return null;
|
|
5160
5000
|
}
|
|
5161
|
-
return null;
|
|
5162
|
-
}
|
|
5163
|
-
function messageFailure(messages, userMessageId) {
|
|
5164
|
-
return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
|
|
5165
|
-
}
|
|
5166
|
-
function isProviderModelNotFoundError(reason) {
|
|
5167
|
-
return reason.includes("Model not found:");
|
|
5168
5001
|
}
|
|
5169
|
-
function
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
const
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
if (!failure.providerId) return null;
|
|
5179
|
-
return { providerId: failure.providerId, outcome: "failed", failure };
|
|
5180
|
-
}
|
|
5181
|
-
const providerId = message.info?.providerID;
|
|
5182
|
-
if (errorOf(message) == null && typeof providerId === "string" && providerId.length > 0) {
|
|
5183
|
-
return { providerId, outcome: "succeeded" };
|
|
5184
|
-
}
|
|
5002
|
+
async function getOpenCodeSession(port, id, client) {
|
|
5003
|
+
try {
|
|
5004
|
+
const response = await requestWithClient(port, client, `/session/${id}`);
|
|
5005
|
+
const body = await response.json();
|
|
5006
|
+
return body && typeof body === "object" && !Array.isArray(body) ? body : null;
|
|
5007
|
+
} catch (error2) {
|
|
5008
|
+
console.error(
|
|
5009
|
+
`[getOpenCodeSession] GET /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
5010
|
+
);
|
|
5185
5011
|
return null;
|
|
5186
5012
|
}
|
|
5187
|
-
return null;
|
|
5188
|
-
}
|
|
5189
|
-
function findSubagentAuthOutcome(messages, sinceMs) {
|
|
5190
|
-
return findLatestSubagentAuthOutcome(messages, sinceMs);
|
|
5191
|
-
}
|
|
5192
|
-
function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProviderId, replyModelId = null) {
|
|
5193
|
-
if (classified != null) return classified;
|
|
5194
|
-
if (hasConfiguredProvider !== false) return null;
|
|
5195
|
-
return {
|
|
5196
|
-
kind: "model_auth",
|
|
5197
|
-
providerId: replyProviderId,
|
|
5198
|
-
modelId: replyModelId,
|
|
5199
|
-
reason: "missing"
|
|
5200
|
-
};
|
|
5201
|
-
}
|
|
5202
|
-
function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
5203
|
-
if (!messages || messages.length === 0) return false;
|
|
5204
|
-
return messages.some(
|
|
5205
|
-
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
5206
|
-
);
|
|
5207
5013
|
}
|
|
5208
|
-
function
|
|
5209
|
-
if (
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
hasLaterUser = true;
|
|
5218
|
-
const laterUserMessageId = idOf2(message);
|
|
5219
|
-
if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
|
|
5220
|
-
return false;
|
|
5221
|
-
}
|
|
5222
|
-
if (messages.some(
|
|
5223
|
-
(candidate) => roleOf2(candidate) === "assistant" && parentIdOf2(candidate) === laterUserMessageId
|
|
5224
|
-
)) {
|
|
5225
|
-
hasStartedLaterUser = true;
|
|
5014
|
+
async function getSessionStatuses(port, client) {
|
|
5015
|
+
if (client?.version === "v2") return null;
|
|
5016
|
+
try {
|
|
5017
|
+
const res = await requestWithClient(port, client, "/session/status");
|
|
5018
|
+
if (!res.ok) {
|
|
5019
|
+
console.error(
|
|
5020
|
+
`[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
|
|
5021
|
+
);
|
|
5022
|
+
return null;
|
|
5226
5023
|
}
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
}
|
|
5230
|
-
async function hasAnyConfiguredProvider(port, client) {
|
|
5231
|
-
if (client?.version === "v2") {
|
|
5232
|
-
const directory = await getOpenCodeDirectoryV2(client);
|
|
5233
|
-
if (!directory) {
|
|
5024
|
+
const body = await res.json();
|
|
5025
|
+
if (body == null || typeof body !== "object" || Array.isArray(body)) {
|
|
5234
5026
|
console.error(
|
|
5235
|
-
`[
|
|
5027
|
+
`[getSessionStatuses] GET /session/status body was not a plain object (port ${port})`
|
|
5236
5028
|
);
|
|
5237
5029
|
return null;
|
|
5238
5030
|
}
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5031
|
+
return body;
|
|
5032
|
+
} catch (err) {
|
|
5033
|
+
console.error(
|
|
5034
|
+
`[getSessionStatuses] GET /session/status failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
5035
|
+
);
|
|
5036
|
+
return null;
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
async function isSessionOngoing(port, id, client) {
|
|
5040
|
+
if (client?.version === "v2") return isV2SessionOngoing(client, id);
|
|
5041
|
+
const map = await getSessionStatuses(port, client);
|
|
5042
|
+
if (map == null) return null;
|
|
5043
|
+
const entry = map[id];
|
|
5044
|
+
return entry != null && entry.type !== "idle";
|
|
5045
|
+
}
|
|
5046
|
+
async function createOpenCodeSession(port, directory, client) {
|
|
5047
|
+
const path = new URL(`${opencodeBase(port)}/session`);
|
|
5048
|
+
if (directory && directory.trim()) path.searchParams.set("directory", directory.trim());
|
|
5049
|
+
const requestPath = `${path.pathname}${path.search}`;
|
|
5050
|
+
const response = await requestWithClient(
|
|
5051
|
+
port,
|
|
5052
|
+
client,
|
|
5053
|
+
requestPath,
|
|
5054
|
+
{
|
|
5055
|
+
method: "POST",
|
|
5056
|
+
headers: { "Content-Type": "application/json" },
|
|
5057
|
+
body: JSON.stringify({})
|
|
5058
|
+
},
|
|
5059
|
+
{ allowStatuses: ALL_HTTP_STATUSES2 }
|
|
5060
|
+
);
|
|
5061
|
+
if (!response.ok) {
|
|
5062
|
+
const text = await response.text().catch(() => "");
|
|
5063
|
+
throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ""}`);
|
|
5064
|
+
}
|
|
5065
|
+
const data = await response.json();
|
|
5066
|
+
return data.id;
|
|
5067
|
+
}
|
|
5068
|
+
async function getModelAttachmentCapability(port, model, client) {
|
|
5069
|
+
const { model: baseModel } = splitModelVariant(model);
|
|
5070
|
+
if (client?.version === "v2") {
|
|
5071
|
+
console.error(
|
|
5072
|
+
`[getModelAttachmentCapability] V2 provider capabilities are unavailable; using text-only fallback (port ${port})`
|
|
5073
|
+
);
|
|
5074
|
+
return null;
|
|
5075
|
+
}
|
|
5076
|
+
try {
|
|
5077
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
5078
|
+
if (!res.ok) {
|
|
5273
5079
|
console.error(
|
|
5274
|
-
`[
|
|
5080
|
+
`[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
5275
5081
|
);
|
|
5082
|
+
return null;
|
|
5276
5083
|
}
|
|
5277
|
-
|
|
5278
|
-
const
|
|
5279
|
-
|
|
5280
|
-
try {
|
|
5281
|
-
const res = await client.request(providerPath);
|
|
5282
|
-
if (!res.ok) {
|
|
5283
|
-
console.error(
|
|
5284
|
-
`[hasAnyConfiguredProvider] GET ${providerPath} returned HTTP ${res.status} (port ${port})`
|
|
5285
|
-
);
|
|
5286
|
-
} else {
|
|
5287
|
-
const body = await res.json();
|
|
5288
|
-
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
|
|
5289
|
-
console.error(
|
|
5290
|
-
`[hasAnyConfiguredProvider] GET ${providerPath} body had no provider data array (port ${port})`
|
|
5291
|
-
);
|
|
5292
|
-
} else {
|
|
5293
|
-
let valid = true;
|
|
5294
|
-
for (const provider of body.data) {
|
|
5295
|
-
if (!provider || typeof provider !== "object" || Array.isArray(provider) || typeof provider.id !== "string") {
|
|
5296
|
-
console.error(
|
|
5297
|
-
`[hasAnyConfiguredProvider] GET ${providerPath} body contained an invalid provider (port ${port})`
|
|
5298
|
-
);
|
|
5299
|
-
valid = false;
|
|
5300
|
-
break;
|
|
5301
|
-
}
|
|
5302
|
-
}
|
|
5303
|
-
if (valid) {
|
|
5304
|
-
providerReady = body.data.some(
|
|
5305
|
-
(provider) => provider.activation === "enabled"
|
|
5306
|
-
);
|
|
5307
|
-
}
|
|
5308
|
-
}
|
|
5309
|
-
}
|
|
5310
|
-
} catch (error2) {
|
|
5084
|
+
const body = await res.json();
|
|
5085
|
+
const providers = Array.isArray(body?.providers) ? body.providers : null;
|
|
5086
|
+
if (!providers) {
|
|
5311
5087
|
console.error(
|
|
5312
|
-
`[
|
|
5088
|
+
`[getModelAttachmentCapability] GET /config/providers body had no providers array (port ${port})`
|
|
5313
5089
|
);
|
|
5090
|
+
return null;
|
|
5314
5091
|
}
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5092
|
+
const slash = baseModel ? baseModel.indexOf("/") : -1;
|
|
5093
|
+
const providerId = slash > 0 ? baseModel.slice(0, slash) : void 0;
|
|
5094
|
+
let modelId = slash > 0 ? baseModel.slice(slash + 1) : void 0;
|
|
5095
|
+
const defaults2 = body?.default && typeof body.default === "object" ? body.default : void 0;
|
|
5096
|
+
let provider = providerId ? providers.find((p) => p?.id === providerId) : void 0;
|
|
5097
|
+
if (!provider && !providerId) {
|
|
5098
|
+
const defaultProviderIds = defaults2 ? Object.keys(defaults2) : [];
|
|
5099
|
+
if (defaultProviderIds.length === 1) {
|
|
5100
|
+
provider = providers.find((p) => p?.id === defaultProviderIds[0]);
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
if (!provider || !provider.models) return null;
|
|
5104
|
+
if (!modelId && defaults2 && typeof provider.id === "string") {
|
|
5105
|
+
const def = defaults2[provider.id];
|
|
5106
|
+
if (typeof def === "string") modelId = def;
|
|
5107
|
+
}
|
|
5108
|
+
if (!modelId) {
|
|
5109
|
+
if (providerId) {
|
|
5110
|
+
const keys = Object.keys(provider.models);
|
|
5111
|
+
if (keys.length === 1) modelId = keys[0];
|
|
5112
|
+
}
|
|
5113
|
+
if (!modelId) return null;
|
|
5114
|
+
}
|
|
5115
|
+
const entry = provider.models[modelId];
|
|
5116
|
+
if (!entry || typeof entry !== "object") return null;
|
|
5117
|
+
if (entry.capabilities && typeof entry.capabilities === "object") {
|
|
5118
|
+
if (typeof entry.capabilities.attachment === "boolean") {
|
|
5119
|
+
return entry.capabilities.attachment;
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
return typeof entry.attachment === "boolean" ? entry.attachment : null;
|
|
5123
|
+
} catch (err) {
|
|
5124
|
+
console.error(
|
|
5125
|
+
`[getModelAttachmentCapability] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
5126
|
+
);
|
|
5127
|
+
return null;
|
|
5318
5128
|
}
|
|
5129
|
+
}
|
|
5130
|
+
async function isPinnedProviderLoaded(port, model, client) {
|
|
5131
|
+
if (client?.version === "v2") return null;
|
|
5132
|
+
const { model: baseModel } = splitModelVariant(model);
|
|
5133
|
+
if (!baseModel) return null;
|
|
5134
|
+
const slash = baseModel.indexOf("/");
|
|
5135
|
+
if (slash <= 0) return null;
|
|
5136
|
+
const providerId = baseModel.slice(0, slash);
|
|
5319
5137
|
try {
|
|
5320
5138
|
const res = await requestWithClient(port, client, "/config/providers");
|
|
5321
5139
|
if (!res.ok) {
|
|
5322
5140
|
console.error(
|
|
5323
|
-
`[
|
|
5141
|
+
`[isPinnedProviderLoaded] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
5324
5142
|
);
|
|
5325
5143
|
return null;
|
|
5326
5144
|
}
|
|
5327
5145
|
const body = await res.json();
|
|
5328
|
-
if (
|
|
5146
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
5329
5147
|
console.error(
|
|
5330
|
-
`[
|
|
5148
|
+
`[isPinnedProviderLoaded] GET /config/providers body was not a plain object (port ${port})`
|
|
5331
5149
|
);
|
|
5332
5150
|
return null;
|
|
5333
5151
|
}
|
|
5334
|
-
const
|
|
5335
|
-
if (!
|
|
5152
|
+
const providers = body.providers;
|
|
5153
|
+
if (!Array.isArray(providers)) {
|
|
5336
5154
|
console.error(
|
|
5337
|
-
`[
|
|
5155
|
+
`[isPinnedProviderLoaded] GET /config/providers body had no providers array (port ${port})`
|
|
5338
5156
|
);
|
|
5339
5157
|
return null;
|
|
5340
5158
|
}
|
|
5341
|
-
|
|
5159
|
+
const providerIds = providers.filter(
|
|
5160
|
+
(provider) => provider !== null && typeof provider === "object" && typeof provider.id === "string"
|
|
5161
|
+
).map((provider) => provider.id);
|
|
5162
|
+
if (providerIds.length === 0) {
|
|
5163
|
+
console.error(
|
|
5164
|
+
`[isPinnedProviderLoaded] GET /config/providers had no provider entries with string ids (port ${port})`
|
|
5165
|
+
);
|
|
5166
|
+
return null;
|
|
5167
|
+
}
|
|
5168
|
+
return providerIds.includes(providerId);
|
|
5342
5169
|
} catch (err) {
|
|
5343
5170
|
console.error(
|
|
5344
|
-
`[
|
|
5171
|
+
`[isPinnedProviderLoaded] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
5345
5172
|
);
|
|
5346
5173
|
return null;
|
|
5347
5174
|
}
|
|
5348
5175
|
}
|
|
5349
|
-
function
|
|
5350
|
-
const
|
|
5351
|
-
const
|
|
5352
|
-
const
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
}
|
|
5357
|
-
|
|
5358
|
-
let parsed;
|
|
5359
|
-
try {
|
|
5360
|
-
parsed = JSON.parse(data);
|
|
5361
|
-
} catch (error2) {
|
|
5362
|
-
void error2;
|
|
5363
|
-
return null;
|
|
5364
|
-
}
|
|
5365
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
5366
|
-
const parsedRecord = parsed;
|
|
5367
|
-
const payload = parsedRecord.payload;
|
|
5368
|
-
const event = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : parsedRecord;
|
|
5369
|
-
if (event.type !== "session.error") return null;
|
|
5370
|
-
const properties = event.properties;
|
|
5371
|
-
if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
|
|
5372
|
-
return null;
|
|
5176
|
+
async function buildFileParts(attachments, capable) {
|
|
5177
|
+
const outcomes = [];
|
|
5178
|
+
const parts = [];
|
|
5179
|
+
const capabilityUnknown = capable === null;
|
|
5180
|
+
if (capable !== true) {
|
|
5181
|
+
for (const a of attachments.inputs) {
|
|
5182
|
+
outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: "skipped" });
|
|
5183
|
+
}
|
|
5184
|
+
return { parts, outcomes, capabilityUnknown };
|
|
5373
5185
|
}
|
|
5374
|
-
const
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
if (client?.version === "v2") return readV2SessionErrorStream(client, options);
|
|
5384
|
-
let reader = null;
|
|
5385
|
-
try {
|
|
5386
|
-
const response = await (client?.request("/event", {
|
|
5387
|
-
headers: { accept: "text/event-stream" },
|
|
5388
|
-
signal: options.signal
|
|
5389
|
-
}) ?? fetch(`${opencodeBase(port)}/event`, {
|
|
5390
|
-
headers: { accept: "text/event-stream" },
|
|
5391
|
-
signal: options.signal
|
|
5392
|
-
}));
|
|
5393
|
-
if (!response.ok || !response.body) {
|
|
5394
|
-
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
5186
|
+
for (const a of attachments.inputs) {
|
|
5187
|
+
let dataUrl = null;
|
|
5188
|
+
try {
|
|
5189
|
+
dataUrl = await attachments.fetchDataUrl(a.index);
|
|
5190
|
+
} catch (err) {
|
|
5191
|
+
console.error(
|
|
5192
|
+
`[buildFileParts] attachment ${a.index} (${a.mime}) fetch threw \u2014 omitting: ${err instanceof Error ? err.message : String(err)}`
|
|
5193
|
+
);
|
|
5194
|
+
dataUrl = null;
|
|
5395
5195
|
}
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5196
|
+
if (dataUrl !== null && typeof dataUrl === "object") {
|
|
5197
|
+
outcomes.push({
|
|
5198
|
+
index: a.index,
|
|
5199
|
+
mime: a.mime,
|
|
5200
|
+
filename: a.filename,
|
|
5201
|
+
status: "failed",
|
|
5202
|
+
reason: "needs_reauth"
|
|
5203
|
+
});
|
|
5204
|
+
continue;
|
|
5205
|
+
}
|
|
5206
|
+
if (dataUrl == null) {
|
|
5207
|
+
outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: "failed" });
|
|
5208
|
+
continue;
|
|
5209
|
+
}
|
|
5210
|
+
parts.push({
|
|
5211
|
+
type: "file",
|
|
5212
|
+
mime: a.mime,
|
|
5213
|
+
url: dataUrl,
|
|
5214
|
+
...a.filename ? { filename: a.filename } : {}
|
|
5215
|
+
});
|
|
5216
|
+
outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: "sent" });
|
|
5217
|
+
}
|
|
5218
|
+
return { parts, outcomes, capabilityUnknown };
|
|
5219
|
+
}
|
|
5220
|
+
function splitModelVariant(raw) {
|
|
5221
|
+
const value = raw?.trim();
|
|
5222
|
+
if (!value) return {};
|
|
5223
|
+
const hashIndex = value.indexOf("#");
|
|
5224
|
+
if (hashIndex === -1) return { model: value };
|
|
5225
|
+
const model = value.slice(0, hashIndex).trim() || void 0;
|
|
5226
|
+
const variant = value.slice(hashIndex + 1).trim() || void 0;
|
|
5227
|
+
return { model, variant };
|
|
5228
|
+
}
|
|
5229
|
+
function applyModelOptions(body, options) {
|
|
5230
|
+
if (options?.agent) body.agent = options.agent;
|
|
5231
|
+
const { model, variant } = splitModelVariant(options?.model);
|
|
5232
|
+
if (model) {
|
|
5233
|
+
const slashIndex = model.indexOf("/");
|
|
5234
|
+
if (slashIndex !== -1) {
|
|
5235
|
+
body.model = {
|
|
5236
|
+
providerID: model.substring(0, slashIndex),
|
|
5237
|
+
modelID: model.substring(slashIndex + 1)
|
|
5238
|
+
};
|
|
5412
5239
|
}
|
|
5413
|
-
} catch (err) {
|
|
5414
|
-
if (options.signal.aborted) return { reason: "aborted" };
|
|
5415
|
-
return {
|
|
5416
|
-
reason: "unavailable",
|
|
5417
|
-
detail: err instanceof Error ? err.message : String(err)
|
|
5418
|
-
};
|
|
5419
|
-
} finally {
|
|
5420
|
-
if (reader) void reader.cancel().catch(() => void 0);
|
|
5421
5240
|
}
|
|
5241
|
+
if (variant) body.variant = variant;
|
|
5422
5242
|
}
|
|
5423
|
-
async function
|
|
5424
|
-
if (client?.version === "v2") return { outcome: "skipped_v2" };
|
|
5243
|
+
async function listOpenCodeQuestions(port, client) {
|
|
5425
5244
|
try {
|
|
5426
|
-
const
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
{
|
|
5431
|
-
method: "PATCH",
|
|
5432
|
-
headers: { "Content-Type": "application/json" },
|
|
5433
|
-
body: JSON.stringify({})
|
|
5434
|
-
},
|
|
5435
|
-
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
5436
|
-
);
|
|
5437
|
-
if (!res.ok) {
|
|
5438
|
-
console.error(
|
|
5439
|
-
`[disposeAndReloadProviderConfig] PATCH /config returned HTTP ${res.status} (port ${port})`
|
|
5440
|
-
);
|
|
5441
|
-
return { outcome: "failed", status: res.status, detail: null };
|
|
5442
|
-
}
|
|
5443
|
-
return { outcome: "reloaded" };
|
|
5444
|
-
} catch (err) {
|
|
5245
|
+
const response = await requestWithClient(port, client, "/question");
|
|
5246
|
+
const body = await response.json();
|
|
5247
|
+
return Array.isArray(body) ? body : null;
|
|
5248
|
+
} catch (error2) {
|
|
5445
5249
|
console.error(
|
|
5446
|
-
`[
|
|
5250
|
+
`[listOpenCodeQuestions] GET /question failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
5447
5251
|
);
|
|
5448
|
-
return
|
|
5449
|
-
outcome: "failed",
|
|
5450
|
-
status: null,
|
|
5451
|
-
detail: err instanceof Error ? err.message : String(err)
|
|
5452
|
-
};
|
|
5252
|
+
return null;
|
|
5453
5253
|
}
|
|
5454
5254
|
}
|
|
5455
|
-
async function
|
|
5255
|
+
async function listOpenCodePermissions(port, client) {
|
|
5456
5256
|
try {
|
|
5457
|
-
const
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
return { ok: false, status: null, bodyEmpty: null, detail };
|
|
5257
|
+
const response = await requestWithClient(port, client, "/permission");
|
|
5258
|
+
const body = await response.json();
|
|
5259
|
+
return Array.isArray(body) ? body : null;
|
|
5260
|
+
} catch (error2) {
|
|
5261
|
+
console.error(
|
|
5262
|
+
`[listOpenCodePermissions] GET /permission failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
5263
|
+
);
|
|
5264
|
+
return null;
|
|
5466
5265
|
}
|
|
5467
5266
|
}
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
function isRecord5(value) {
|
|
5472
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5267
|
+
function messageText(m) {
|
|
5268
|
+
if (!m || !Array.isArray(m.parts)) return "";
|
|
5269
|
+
return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
5473
5270
|
}
|
|
5474
|
-
function
|
|
5475
|
-
|
|
5271
|
+
function findDispatchedUserMessageId(messages, content, excludeIds) {
|
|
5272
|
+
if (!messages || messages.length === 0) return null;
|
|
5273
|
+
let best = null;
|
|
5274
|
+
for (const message of messages) {
|
|
5275
|
+
if (roleOf2(message) !== "user") continue;
|
|
5276
|
+
const id = idOf2(message);
|
|
5277
|
+
if (typeof id !== "string" || excludeIds?.has(id)) continue;
|
|
5278
|
+
if (messageText(message) !== content) continue;
|
|
5279
|
+
const created = createdOf(message) ?? 0;
|
|
5280
|
+
if (best === null || created > best.created) best = { id, created };
|
|
5281
|
+
}
|
|
5282
|
+
return best?.id ?? null;
|
|
5476
5283
|
}
|
|
5477
|
-
function
|
|
5478
|
-
|
|
5479
|
-
const
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5284
|
+
async function sendPromptAsync(port, sessionId, content, options, attachments, client, callbacks) {
|
|
5285
|
+
const before = await getSessionMessages(port, sessionId, client);
|
|
5286
|
+
const knownUserIds = new Set(
|
|
5287
|
+
(before ?? []).filter((m) => roleOf2(m) === "user").map((m) => idOf2(m)).filter((id) => typeof id === "string")
|
|
5288
|
+
);
|
|
5289
|
+
if (callbacks) {
|
|
5290
|
+
try {
|
|
5291
|
+
callbacks.onKnownUserIds(before === null ? null : knownUserIds);
|
|
5292
|
+
} catch (error2) {
|
|
5293
|
+
callbacks.onKnownUserIdsError(error2);
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
5296
|
+
const parts = [{ type: "text", text: content }];
|
|
5297
|
+
let pendingOutcomes = null;
|
|
5298
|
+
if (attachments && attachments.inputs.length > 0) {
|
|
5299
|
+
const capable = await getModelAttachmentCapability(port, options?.model, client);
|
|
5300
|
+
const {
|
|
5301
|
+
parts: fileParts,
|
|
5302
|
+
outcomes,
|
|
5303
|
+
capabilityUnknown
|
|
5304
|
+
} = await buildFileParts(attachments, capable);
|
|
5305
|
+
parts.push(...fileParts);
|
|
5306
|
+
if (attachments.onOutcomes) pendingOutcomes = { outcomes, capabilityUnknown };
|
|
5307
|
+
}
|
|
5308
|
+
const body = {
|
|
5309
|
+
parts
|
|
5485
5310
|
};
|
|
5311
|
+
applyModelOptions(body, options);
|
|
5312
|
+
const res = await requestWithClient(
|
|
5313
|
+
port,
|
|
5314
|
+
client,
|
|
5315
|
+
`/session/${sessionId}/prompt_async`,
|
|
5316
|
+
{
|
|
5317
|
+
method: "POST",
|
|
5318
|
+
headers: { "Content-Type": "application/json" },
|
|
5319
|
+
body: JSON.stringify(body)
|
|
5320
|
+
},
|
|
5321
|
+
{ allowStatuses: ALL_HTTP_STATUSES2 }
|
|
5322
|
+
);
|
|
5323
|
+
if (res.status < 200 || res.status >= 300) {
|
|
5324
|
+
const text = await res.text().catch(() => "");
|
|
5325
|
+
const { variant } = splitModelVariant(options?.model);
|
|
5326
|
+
throw new Error(
|
|
5327
|
+
`OpenCode prompt_async failed: HTTP ${res.status}${text ? `: ${text}` : ""}${variant ? ` (variant: ${variant})` : ""}`
|
|
5328
|
+
);
|
|
5329
|
+
}
|
|
5330
|
+
const READ_BACK_ATTEMPTS = 5;
|
|
5331
|
+
const READ_BACK_DELAY_MS = 150;
|
|
5332
|
+
for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
|
|
5333
|
+
const after = await getSessionMessages(port, sessionId, client);
|
|
5334
|
+
const dispatchedUserMessageId = findDispatchedUserMessageId(after, content, knownUserIds);
|
|
5335
|
+
if (dispatchedUserMessageId !== null) {
|
|
5336
|
+
if (pendingOutcomes && attachments?.onOutcomes) attachments.onOutcomes(pendingOutcomes);
|
|
5337
|
+
return dispatchedUserMessageId;
|
|
5338
|
+
}
|
|
5339
|
+
if (attempt < READ_BACK_ATTEMPTS - 1) {
|
|
5340
|
+
await new Promise((resolve6) => setTimeout(resolve6, READ_BACK_DELAY_MS));
|
|
5341
|
+
}
|
|
5342
|
+
}
|
|
5343
|
+
return null;
|
|
5486
5344
|
}
|
|
5487
|
-
function
|
|
5488
|
-
if (!
|
|
5489
|
-
const
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
return void 0;
|
|
5345
|
+
function findAssistantReplyAfter(messages, userMessageId) {
|
|
5346
|
+
if (!messages || messages.length === 0) return null;
|
|
5347
|
+
const byParent = messages.find(
|
|
5348
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
|
|
5349
|
+
);
|
|
5350
|
+
if (byParent) return byParent;
|
|
5351
|
+
const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
|
|
5352
|
+
if (userIndex === -1) return null;
|
|
5353
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
5354
|
+
if (roleOf2(messages[i]) === "assistant") return messages[i];
|
|
5498
5355
|
}
|
|
5499
|
-
return
|
|
5500
|
-
...input !== void 0 ? { input } : {},
|
|
5501
|
-
...output !== void 0 ? { output } : {},
|
|
5502
|
-
...reasoning !== void 0 ? { reasoning } : {},
|
|
5503
|
-
...cache ? { cache } : {}
|
|
5504
|
-
};
|
|
5356
|
+
return null;
|
|
5505
5357
|
}
|
|
5506
|
-
function
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
if (
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5358
|
+
function findLastAssistantReplyFor(messages, userMessageId) {
|
|
5359
|
+
if (!messages || messages.length === 0) return null;
|
|
5360
|
+
let lastCorrelated = null;
|
|
5361
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
5362
|
+
const m = messages[i];
|
|
5363
|
+
if (roleOf2(m) !== "assistant" || parentIdOf2(m) !== userMessageId) continue;
|
|
5364
|
+
lastCorrelated = m;
|
|
5365
|
+
break;
|
|
5366
|
+
}
|
|
5367
|
+
if (lastCorrelated) {
|
|
5368
|
+
if (errorOf(lastCorrelated) == null) return lastCorrelated;
|
|
5369
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
5370
|
+
const m = messages[i];
|
|
5371
|
+
if (roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId && isTerminalStopAnswer(m)) {
|
|
5372
|
+
return m;
|
|
5373
|
+
}
|
|
5374
|
+
}
|
|
5375
|
+
return lastCorrelated;
|
|
5376
|
+
}
|
|
5377
|
+
const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
|
|
5378
|
+
if (userIndex === -1) return null;
|
|
5379
|
+
let last = null;
|
|
5380
|
+
let lastAssistantIndex = -1;
|
|
5381
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
5382
|
+
const role = roleOf2(messages[i]);
|
|
5383
|
+
if (role === "user") break;
|
|
5384
|
+
if (role === "assistant") {
|
|
5385
|
+
last = messages[i];
|
|
5386
|
+
lastAssistantIndex = i;
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
if (last === null || errorOf(last) == null) return last;
|
|
5390
|
+
for (let i = lastAssistantIndex; i > userIndex; i--) {
|
|
5391
|
+
const m = messages[i];
|
|
5392
|
+
if (roleOf2(m) === "user") break;
|
|
5393
|
+
if (roleOf2(m) === "assistant" && isTerminalStopAnswer(m)) return m;
|
|
5394
|
+
}
|
|
5395
|
+
return last;
|
|
5396
|
+
}
|
|
5397
|
+
function assistantMessagesInTurnBlock(messages, userMessageId) {
|
|
5398
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
5399
|
+
if (userIndex === -1) return [];
|
|
5400
|
+
const assistants = [];
|
|
5401
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
5402
|
+
const message = messages[i];
|
|
5403
|
+
const role = roleOf2(message);
|
|
5404
|
+
if (role === "user") break;
|
|
5405
|
+
if (role === "assistant") assistants.push(message);
|
|
5406
|
+
}
|
|
5407
|
+
return assistants;
|
|
5408
|
+
}
|
|
5409
|
+
function collectSubagentSessions(messages, userMessageId) {
|
|
5410
|
+
if (!messages || messages.length === 0) return [];
|
|
5411
|
+
const byParent = messages.filter(
|
|
5412
|
+
(message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
|
|
5413
|
+
);
|
|
5414
|
+
const assistants = byParent.length > 0 ? byParent : assistantMessagesInTurnBlock(messages, userMessageId);
|
|
5415
|
+
const refs = [];
|
|
5416
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5417
|
+
for (const message of assistants) {
|
|
5418
|
+
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
5419
|
+
for (const part of parts) {
|
|
5420
|
+
if (!part || typeof part !== "object" || part.type !== "tool") continue;
|
|
5421
|
+
const childSession = part.childSession;
|
|
5422
|
+
if (childSession) {
|
|
5423
|
+
if (seen.has(childSession.sessionId)) continue;
|
|
5424
|
+
seen.add(childSession.sessionId);
|
|
5425
|
+
refs.push(childSession);
|
|
5426
|
+
continue;
|
|
5427
|
+
}
|
|
5428
|
+
if (part.tool !== "task") continue;
|
|
5429
|
+
const state = part.state;
|
|
5430
|
+
if (!state || typeof state !== "object") continue;
|
|
5431
|
+
const metadata = state.metadata;
|
|
5432
|
+
if (!metadata || typeof metadata !== "object") continue;
|
|
5433
|
+
const sessionId = metadata.sessionId;
|
|
5434
|
+
if (typeof sessionId !== "string" || sessionId.length === 0 || seen.has(sessionId)) continue;
|
|
5435
|
+
seen.add(sessionId);
|
|
5436
|
+
const start = state.time?.start;
|
|
5437
|
+
refs.push({
|
|
5438
|
+
sessionId,
|
|
5439
|
+
startedAtMs: typeof start === "number" && Number.isFinite(start) ? start : null
|
|
5440
|
+
});
|
|
5441
|
+
}
|
|
5519
5442
|
}
|
|
5520
|
-
|
|
5521
|
-
const tokens = adaptTokens(value.tokens);
|
|
5522
|
-
if (tokens) info.tokens = tokens;
|
|
5523
|
-
return info;
|
|
5443
|
+
return refs;
|
|
5524
5444
|
}
|
|
5525
|
-
function
|
|
5526
|
-
|
|
5527
|
-
const metadata = state && isRecord5(state.metadata) ? state.metadata : null;
|
|
5528
|
-
const sessionId = typeof metadata?.sessionID === "string" && metadata.sessionID.length > 0 ? metadata.sessionID : typeof metadata?.sessionId === "string" && metadata.sessionId.length > 0 ? metadata.sessionId : null;
|
|
5529
|
-
if (sessionId === null) return null;
|
|
5530
|
-
const time = isRecord5(content.time) ? content.time : null;
|
|
5531
|
-
return { sessionId, startedAtMs: finiteNumber2(time?.created) ?? null };
|
|
5445
|
+
function finiteNumber2(value) {
|
|
5446
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
5532
5447
|
}
|
|
5533
|
-
function
|
|
5534
|
-
if (!
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5448
|
+
function taskCallModel(value) {
|
|
5449
|
+
if (!value || typeof value !== "object") return null;
|
|
5450
|
+
const model = value;
|
|
5451
|
+
const modelID = typeof model.modelID === "string" ? model.modelID : void 0;
|
|
5452
|
+
const providerID = typeof model.providerID === "string" ? model.providerID : void 0;
|
|
5453
|
+
return modelID || providerID ? { modelID, providerID } : null;
|
|
5454
|
+
}
|
|
5455
|
+
function collectTaskCalls(messages, userMessageId) {
|
|
5456
|
+
if (!messages || messages.length === 0) return [];
|
|
5457
|
+
const calls = [];
|
|
5458
|
+
for (const message of messages) {
|
|
5459
|
+
if (roleOf2(message) !== "assistant" || parentIdOf2(message) !== userMessageId) continue;
|
|
5460
|
+
for (const part of message.parts ?? []) {
|
|
5461
|
+
if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
|
|
5462
|
+
continue;
|
|
5463
|
+
}
|
|
5464
|
+
const rawName = part.state.input?.subagent_type;
|
|
5465
|
+
const subagentName = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : rawName === void 0 ? "general" : "unknown";
|
|
5466
|
+
const metadata = part.state.metadata;
|
|
5467
|
+
calls.push({
|
|
5468
|
+
callID: part.callID,
|
|
5469
|
+
subagentName,
|
|
5470
|
+
childSessionId: typeof metadata?.sessionId === "string" ? metadata.sessionId : null,
|
|
5471
|
+
parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
|
|
5472
|
+
model: taskCallModel(metadata?.model),
|
|
5473
|
+
status: part.state.status ?? "unknown",
|
|
5474
|
+
timeStart: finiteNumber2(part.state.time?.start),
|
|
5475
|
+
timeEnd: finiteNumber2(part.state.time?.end)
|
|
5476
|
+
});
|
|
5477
|
+
}
|
|
5543
5478
|
}
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5479
|
+
return calls;
|
|
5480
|
+
}
|
|
5481
|
+
function attributeTaskCallUsage(messages, windows) {
|
|
5482
|
+
const eligibleWindows = windows.filter(
|
|
5483
|
+
(window) => window.timeStart !== null && Number.isFinite(window.timeStart)
|
|
5484
|
+
);
|
|
5485
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
5486
|
+
for (const window of eligibleWindows) assignments.set(window.callID, []);
|
|
5487
|
+
const unattributed = [];
|
|
5488
|
+
for (const message of messages ?? []) {
|
|
5489
|
+
if (roleOf2(message) !== "assistant") continue;
|
|
5490
|
+
const created = finiteNumber2(createdOf(message));
|
|
5491
|
+
const matching = created === null ? [] : eligibleWindows.filter(
|
|
5492
|
+
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
5493
|
+
);
|
|
5494
|
+
if (matching.length === 0) {
|
|
5495
|
+
unattributed.push(message);
|
|
5552
5496
|
continue;
|
|
5553
5497
|
}
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
parts.push({ type: "text", text: content.text });
|
|
5557
|
-
} else {
|
|
5558
|
-
parts.push({ type: content.type });
|
|
5559
|
-
}
|
|
5498
|
+
matching.sort((a, b) => a.timeStart - b.timeStart);
|
|
5499
|
+
assignments.get(matching[0].callID)?.push(message);
|
|
5560
5500
|
}
|
|
5561
5501
|
return {
|
|
5562
|
-
|
|
5563
|
-
|
|
5502
|
+
invocations: eligibleWindows.map((window) => {
|
|
5503
|
+
const assigned = assignments.get(window.callID) ?? [];
|
|
5504
|
+
return { callID: window.callID, messages: assigned, usage: sumAssistantUsage(assigned) };
|
|
5505
|
+
}),
|
|
5506
|
+
unattributed
|
|
5564
5507
|
};
|
|
5565
5508
|
}
|
|
5566
|
-
function
|
|
5567
|
-
if (!
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5509
|
+
function sumAssistantUsage(messages) {
|
|
5510
|
+
if (!messages || messages.length === 0) return null;
|
|
5511
|
+
const hasTrailingError = errorOf(messages[messages.length - 1]) != null;
|
|
5512
|
+
const hasTerminalStopAnswer = messages.some(isTerminalStopAnswer);
|
|
5513
|
+
const selected = hasTrailingError && hasTerminalStopAnswer ? messages.filter((message) => errorOf(message) == null) : messages;
|
|
5514
|
+
let sawAnyUsage = false;
|
|
5515
|
+
let inputSum = 0;
|
|
5516
|
+
let outputSum = 0;
|
|
5517
|
+
let reasoningSum = 0;
|
|
5518
|
+
let cacheReadSum = 0;
|
|
5519
|
+
let cacheWriteSum = 0;
|
|
5520
|
+
let costSum = 0;
|
|
5521
|
+
let sawCost = false;
|
|
5522
|
+
let toolCallCount = 0;
|
|
5523
|
+
let modelId = null;
|
|
5524
|
+
let providerId = null;
|
|
5525
|
+
for (const m of selected) {
|
|
5526
|
+
toolCallCount += m.parts?.filter((part) => part.type === "tool").length ?? 0;
|
|
5527
|
+
const info = m.info;
|
|
5528
|
+
if (!info) continue;
|
|
5529
|
+
const tokens = info.tokens;
|
|
5530
|
+
if (tokens) {
|
|
5531
|
+
sawAnyUsage = true;
|
|
5532
|
+
inputSum += tokens.input ?? 0;
|
|
5533
|
+
outputSum += tokens.output ?? 0;
|
|
5534
|
+
reasoningSum += tokens.reasoning ?? 0;
|
|
5535
|
+
cacheReadSum += tokens.cache?.read ?? 0;
|
|
5536
|
+
cacheWriteSum += tokens.cache?.write ?? 0;
|
|
5537
|
+
}
|
|
5538
|
+
if (typeof info.cost === "number") {
|
|
5539
|
+
sawAnyUsage = true;
|
|
5540
|
+
sawCost = true;
|
|
5541
|
+
costSum += info.cost;
|
|
5542
|
+
}
|
|
5543
|
+
if (typeof info.modelID === "string") {
|
|
5544
|
+
sawAnyUsage = true;
|
|
5545
|
+
modelId = info.modelID;
|
|
5546
|
+
}
|
|
5547
|
+
if (typeof info.providerID === "string") {
|
|
5548
|
+
sawAnyUsage = true;
|
|
5549
|
+
providerId = info.providerID;
|
|
5550
|
+
}
|
|
5575
5551
|
}
|
|
5576
|
-
return
|
|
5577
|
-
}
|
|
5578
|
-
function adaptV2FormField(value, header) {
|
|
5579
|
-
if (!isRecord5(value)) return null;
|
|
5580
|
-
const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
|
|
5581
|
-
if (!question) return null;
|
|
5582
|
-
const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
|
|
5583
|
-
if (!isRecord5(option)) return [];
|
|
5584
|
-
const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
|
|
5585
|
-
if (!label) return [];
|
|
5586
|
-
return [
|
|
5587
|
-
{
|
|
5588
|
-
label,
|
|
5589
|
-
description: typeof option.description === "string" ? option.description : ""
|
|
5590
|
-
}
|
|
5591
|
-
];
|
|
5592
|
-
}) : [];
|
|
5593
|
-
return { question, header, options };
|
|
5594
|
-
}
|
|
5595
|
-
function adaptV2Form(value) {
|
|
5596
|
-
const form = adaptV2FormWire(value);
|
|
5597
|
-
if (!form || !Array.isArray(form.fields)) return null;
|
|
5598
|
-
const header = typeof form.title === "string" ? form.title : "";
|
|
5599
|
-
const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
|
|
5600
|
-
if (questions.length === 0) return null;
|
|
5601
|
-
const tool = isRecord5(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
|
|
5552
|
+
if (!sawAnyUsage) return null;
|
|
5602
5553
|
return {
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5554
|
+
usage_provider_id: providerId,
|
|
5555
|
+
usage_model_id: modelId,
|
|
5556
|
+
usage_tokens_input: inputSum,
|
|
5557
|
+
usage_tokens_output: outputSum,
|
|
5558
|
+
usage_tokens_reasoning: reasoningSum,
|
|
5559
|
+
usage_tokens_cache_read: cacheReadSum,
|
|
5560
|
+
usage_tokens_cache_write: cacheWriteSum,
|
|
5561
|
+
// NULL means OpenCode never reported a cost; it is distinct from a genuine
|
|
5562
|
+
// zero-cost message, which sets `sawCost` with `costSum === 0`.
|
|
5563
|
+
usage_cost_usd: sawCost ? costSum : null,
|
|
5564
|
+
turn_tool_calls: toolCallCount
|
|
5608
5565
|
};
|
|
5609
5566
|
}
|
|
5610
|
-
function
|
|
5611
|
-
if (!
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
if (
|
|
5617
|
-
|
|
5567
|
+
function messageUsage(messages, userMessageId) {
|
|
5568
|
+
if (!messages || messages.length === 0) return null;
|
|
5569
|
+
const byParentAll = messages.filter(
|
|
5570
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
|
|
5571
|
+
);
|
|
5572
|
+
let correlated;
|
|
5573
|
+
if (byParentAll.length > 0) {
|
|
5574
|
+
correlated = byParentAll;
|
|
5575
|
+
} else {
|
|
5576
|
+
correlated = assistantMessagesInTurnBlock(messages, userMessageId);
|
|
5618
5577
|
}
|
|
5619
|
-
|
|
5578
|
+
if (correlated.length === 0) return null;
|
|
5579
|
+
return sumAssistantUsage(correlated);
|
|
5620
5580
|
}
|
|
5621
|
-
function
|
|
5622
|
-
if (!
|
|
5623
|
-
|
|
5581
|
+
function messageRunState(messages, userMessageId) {
|
|
5582
|
+
if (!messages || messages.length === 0) return "unknown";
|
|
5583
|
+
const hasUser = messages.some((m) => idOf2(m) === userMessageId);
|
|
5584
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5585
|
+
if (!hasUser) {
|
|
5586
|
+
if (!reply) return "unknown";
|
|
5624
5587
|
}
|
|
5625
|
-
if (
|
|
5626
|
-
return
|
|
5588
|
+
if (!reply) return "queued";
|
|
5589
|
+
if (isAssistantInFlight(reply)) return "running";
|
|
5590
|
+
if (errorOf(reply) != null) return "failed";
|
|
5591
|
+
if (isAmbiguousTerminalFinish(reply)) return "running";
|
|
5592
|
+
return "done";
|
|
5627
5593
|
}
|
|
5628
|
-
function
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
type,
|
|
5638
|
-
sessionID: permission.sessionID,
|
|
5639
|
-
metadata: isRecord5(permission.metadata) ? permission.metadata : {},
|
|
5640
|
-
raw: permission,
|
|
5641
|
-
...pattern !== void 0 ? { pattern } : {},
|
|
5642
|
-
...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
|
|
5643
|
-
...typeof permission.callID === "string" ? { callID: permission.callID } : {},
|
|
5644
|
-
...typeof permission.title === "string" ? { title: permission.title } : {},
|
|
5645
|
-
...time ? { time } : {}
|
|
5646
|
-
};
|
|
5594
|
+
function isPreamblePinnedRunning(messages, userMessageId) {
|
|
5595
|
+
if (messageRunState(messages, userMessageId) !== "running") return false;
|
|
5596
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5597
|
+
return completedOf(reply) != null && finishOf(reply) === "tool-calls";
|
|
5598
|
+
}
|
|
5599
|
+
function isMidFlightPinnedRunning(messages, userMessageId) {
|
|
5600
|
+
if (messageRunState(messages, userMessageId) !== "running") return false;
|
|
5601
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5602
|
+
return reply !== null && isAssistantInFlight(reply) && completedOf(reply) == null;
|
|
5647
5603
|
}
|
|
5648
|
-
function
|
|
5649
|
-
|
|
5650
|
-
return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
|
|
5604
|
+
function isB2AbandonmentConfirmed(params) {
|
|
5605
|
+
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
|
|
5651
5606
|
}
|
|
5652
|
-
function
|
|
5653
|
-
|
|
5654
|
-
const time = isRecord5(value.time) ? {
|
|
5655
|
-
...finiteNumber2(value.time.created) !== void 0 ? { created: finiteNumber2(value.time.created) } : {},
|
|
5656
|
-
...finiteNumber2(value.time.updated) !== void 0 ? { updated: finiteNumber2(value.time.updated) } : {}
|
|
5657
|
-
} : void 0;
|
|
5658
|
-
return {
|
|
5659
|
-
id: value.id,
|
|
5660
|
-
...typeof value.title === "string" ? { title: value.title } : {},
|
|
5661
|
-
...typeof value.parentID === "string" ? { parentID: value.parentID } : {},
|
|
5662
|
-
...time && Object.keys(time).length > 0 ? { time } : {}
|
|
5663
|
-
};
|
|
5607
|
+
function isMidFlightOrphanConfirmed(params) {
|
|
5608
|
+
return params.pinnedForMs >= params.minPinnedMs && params.notOngoingStreak >= params.minNotOngoingStreak;
|
|
5664
5609
|
}
|
|
5665
|
-
function
|
|
5666
|
-
if (
|
|
5667
|
-
return
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
};
|
|
5610
|
+
function isAmbiguousTerminalFinish(m) {
|
|
5611
|
+
if (completedOf(m) == null) return false;
|
|
5612
|
+
if (errorOf(m) != null) return false;
|
|
5613
|
+
const finish = finishOf(m);
|
|
5614
|
+
return finish !== "tool-calls" && finish !== "stop";
|
|
5671
5615
|
}
|
|
5672
|
-
function
|
|
5673
|
-
const
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5616
|
+
function isAmbiguousFinishPinnedRunning(messages, userMessageId) {
|
|
5617
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5618
|
+
return isAmbiguousTerminalFinish(reply);
|
|
5619
|
+
}
|
|
5620
|
+
function isAmbiguousFinishResolved(params) {
|
|
5621
|
+
return params.sessionOngoing === false || params.sessionOngoing !== true && params.pinnedForMs >= params.maxPinnedMs;
|
|
5622
|
+
}
|
|
5623
|
+
function messageError(messages, userMessageId) {
|
|
5624
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5625
|
+
const error2 = errorOf(reply);
|
|
5626
|
+
if (error2 == null) return null;
|
|
5627
|
+
let rendered;
|
|
5628
|
+
if (typeof error2 === "string") {
|
|
5629
|
+
rendered = error2;
|
|
5630
|
+
} else if (typeof error2 === "object") {
|
|
5631
|
+
const e = error2;
|
|
5632
|
+
const dataMessage = e.data?.message;
|
|
5633
|
+
if (typeof dataMessage === "string") {
|
|
5634
|
+
rendered = dataMessage;
|
|
5635
|
+
} else if (typeof e.message === "string") {
|
|
5636
|
+
rendered = e.message;
|
|
5637
|
+
} else {
|
|
5638
|
+
rendered = "The agent run failed.";
|
|
5639
|
+
}
|
|
5640
|
+
} else {
|
|
5641
|
+
rendered = "The agent run failed.";
|
|
5682
5642
|
}
|
|
5683
|
-
|
|
5643
|
+
const toolPart = Array.isArray(reply?.parts) ? reply.parts.find(
|
|
5644
|
+
(part) => part && typeof part === "object" && part.type === "tool" && part.state?.status === "error"
|
|
5645
|
+
) : void 0;
|
|
5646
|
+
return typeof toolPart?.tool === "string" && toolPart.tool.length > 0 ? `${rendered} (tool "${toolPart.tool}")` : rendered;
|
|
5684
5647
|
}
|
|
5685
|
-
function
|
|
5686
|
-
|
|
5687
|
-
|
|
5648
|
+
function describeAbandonedReplyShape(messages, userMessageId) {
|
|
5649
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5650
|
+
if (!reply) return null;
|
|
5651
|
+
const parts = Array.isArray(reply.parts) ? reply.parts : [];
|
|
5652
|
+
const lastTool = [...parts].reverse().find((part) => part?.type === "tool");
|
|
5653
|
+
return {
|
|
5654
|
+
partTypes: parts.slice(-12).map((part) => typeof part?.type === "string" ? part.type : "(unknown)"),
|
|
5655
|
+
droppedPartCount: Math.max(0, parts.length - 12),
|
|
5656
|
+
lastToolName: typeof lastTool?.tool === "string" && lastTool.tool.length > 0 ? lastTool.tool : null,
|
|
5657
|
+
lastToolStatus: typeof lastTool?.state?.status === "string" && lastTool.state.status.length > 0 ? lastTool.state.status : null
|
|
5658
|
+
};
|
|
5688
5659
|
}
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5660
|
+
function isAbortedTerminalReply(messages, userMessageId) {
|
|
5661
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5662
|
+
const error2 = errorOf(reply);
|
|
5663
|
+
if (error2 == null) return false;
|
|
5664
|
+
if (typeof error2 === "string") return error2.trim() === "Aborted";
|
|
5665
|
+
if (typeof error2 === "object") {
|
|
5666
|
+
const e = error2;
|
|
5667
|
+
if (e.name === "MessageAbortedError") return true;
|
|
5668
|
+
if (e.name === "AbortError") return true;
|
|
5669
|
+
const dataMessage = e.data?.message;
|
|
5670
|
+
const rendered = typeof dataMessage === "string" ? dataMessage : typeof e.message === "string" ? e.message : null;
|
|
5671
|
+
return rendered != null && rendered.trim() === "Aborted";
|
|
5696
5672
|
}
|
|
5673
|
+
return false;
|
|
5697
5674
|
}
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5675
|
+
var CLAUDE_AUTH_EXPIRY_MESSAGES = Object.freeze([
|
|
5676
|
+
"Claude Code credentials are unavailable or expired. Run `claude` to refresh them."
|
|
5677
|
+
]);
|
|
5678
|
+
function extractReplyErrorShape(reply) {
|
|
5679
|
+
const error2 = errorOf(reply);
|
|
5680
|
+
if (error2 == null || typeof error2 !== "object") {
|
|
5681
|
+
return { name: null, statusCode: null };
|
|
5703
5682
|
}
|
|
5704
|
-
|
|
5683
|
+
const e = error2;
|
|
5684
|
+
const name = typeof e.name === "string" ? e.name.slice(0, 100) : null;
|
|
5685
|
+
const data = e.data;
|
|
5686
|
+
const statusCode = typeof data?.statusCode === "number" && Number.isFinite(data.statusCode) ? data.statusCode : null;
|
|
5687
|
+
return { name, statusCode };
|
|
5705
5688
|
}
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5689
|
+
function classifyReplyAuthError(reply) {
|
|
5690
|
+
const error2 = errorOf(reply);
|
|
5691
|
+
if (error2 == null || typeof error2 !== "object") return null;
|
|
5692
|
+
const e = error2;
|
|
5693
|
+
const replyProviderId = reply?.info?.providerID ?? null;
|
|
5694
|
+
const replyModelId = reply?.info?.modelID ?? null;
|
|
5695
|
+
const data = e.data;
|
|
5696
|
+
if (e.name === "ProviderAuthError") {
|
|
5697
|
+
const providerId = typeof data?.providerID === "string" && data.providerID || replyProviderId;
|
|
5698
|
+
return { kind: "model_auth", providerId, modelId: replyModelId, reason: "missing" };
|
|
5710
5699
|
}
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5700
|
+
if (e.name === "APIError") {
|
|
5701
|
+
const statusCode = data?.statusCode;
|
|
5702
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
5703
|
+
return {
|
|
5704
|
+
kind: "model_auth",
|
|
5705
|
+
providerId: replyProviderId,
|
|
5706
|
+
modelId: replyModelId,
|
|
5707
|
+
reason: "rejected"
|
|
5708
|
+
};
|
|
5709
|
+
}
|
|
5718
5710
|
}
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
return null;
|
|
5711
|
+
const errorMessage4 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
|
|
5712
|
+
if (errorMessage4 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage4.includes(knownMessage))) {
|
|
5713
|
+
return {
|
|
5714
|
+
kind: "model_auth",
|
|
5715
|
+
providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
|
|
5716
|
+
modelId: replyModelId,
|
|
5717
|
+
reason: "rejected"
|
|
5718
|
+
};
|
|
5728
5719
|
}
|
|
5720
|
+
return null;
|
|
5729
5721
|
}
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
method: "POST",
|
|
5733
|
-
headers: { "Content-Type": "application/json" },
|
|
5734
|
-
body: JSON.stringify({ location: { directory } })
|
|
5735
|
-
});
|
|
5736
|
-
if (!isRecord5(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
5737
|
-
throw new Error("OpenCode V2 create session response was missing data.id");
|
|
5738
|
-
}
|
|
5739
|
-
return data.id;
|
|
5722
|
+
function messageFailure(messages, userMessageId) {
|
|
5723
|
+
return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
|
|
5740
5724
|
}
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
const session = adaptV2Session(data);
|
|
5744
|
-
if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
|
|
5745
|
-
return session;
|
|
5725
|
+
function isProviderModelNotFoundError(reason) {
|
|
5726
|
+
return reason.includes("Model not found:");
|
|
5746
5727
|
}
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5728
|
+
function findLatestSubagentAuthOutcome(messages, sinceMs) {
|
|
5729
|
+
if (!messages || messages.length === 0) return null;
|
|
5730
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
5731
|
+
const message = messages[i];
|
|
5732
|
+
if (roleOf2(message) !== "assistant") continue;
|
|
5733
|
+
const created = createdOf(message);
|
|
5734
|
+
if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
|
|
5735
|
+
const failure = classifyReplyAuthError(message);
|
|
5736
|
+
if (failure) {
|
|
5737
|
+
if (!failure.providerId) return null;
|
|
5738
|
+
return { providerId: failure.providerId, outcome: "failed", failure };
|
|
5739
|
+
}
|
|
5740
|
+
const providerId = message.info?.providerID;
|
|
5741
|
+
if (errorOf(message) == null && typeof providerId === "string" && providerId.length > 0) {
|
|
5742
|
+
return { providerId, outcome: "succeeded" };
|
|
5759
5743
|
}
|
|
5760
|
-
return adaptV2SessionList(await readJson(response));
|
|
5761
|
-
} catch (error2) {
|
|
5762
|
-
if (signal?.aborted) return null;
|
|
5763
|
-
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
5764
|
-
console.error(`[listV2SessionPage] ${operation} failed (port ${client.port}): ${errorName}`);
|
|
5765
5744
|
return null;
|
|
5766
5745
|
}
|
|
5746
|
+
return null;
|
|
5767
5747
|
}
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5748
|
+
function findSubagentAuthOutcome(messages, sinceMs) {
|
|
5749
|
+
return findLatestSubagentAuthOutcome(messages, sinceMs);
|
|
5750
|
+
}
|
|
5751
|
+
function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProviderId, replyModelId = null) {
|
|
5752
|
+
if (classified != null) return classified;
|
|
5753
|
+
if (hasConfiguredProvider !== false) return null;
|
|
5754
|
+
return {
|
|
5755
|
+
kind: "model_auth",
|
|
5756
|
+
providerId: replyProviderId,
|
|
5757
|
+
modelId: replyModelId,
|
|
5758
|
+
reason: "missing"
|
|
5759
|
+
};
|
|
5760
|
+
}
|
|
5761
|
+
function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
5762
|
+
if (!messages || messages.length === 0) return false;
|
|
5763
|
+
return messages.some(
|
|
5764
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
5765
|
+
);
|
|
5766
|
+
}
|
|
5767
|
+
function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
|
|
5768
|
+
if (!messages || messages.length === 0) return false;
|
|
5769
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
5770
|
+
if (userIndex === -1) return false;
|
|
5771
|
+
let hasLaterUser = false;
|
|
5772
|
+
let hasStartedLaterUser = false;
|
|
5773
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
5774
|
+
const message = messages[i];
|
|
5775
|
+
if (roleOf2(message) !== "user") continue;
|
|
5776
|
+
hasLaterUser = true;
|
|
5777
|
+
const laterUserMessageId = idOf2(message);
|
|
5778
|
+
if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
|
|
5779
|
+
return false;
|
|
5780
|
+
}
|
|
5781
|
+
if (messages.some(
|
|
5782
|
+
(candidate) => roleOf2(candidate) === "assistant" && parentIdOf2(candidate) === laterUserMessageId
|
|
5783
|
+
)) {
|
|
5784
|
+
hasStartedLaterUser = true;
|
|
5788
5785
|
}
|
|
5789
|
-
return sessions;
|
|
5790
|
-
} catch (error2) {
|
|
5791
|
-
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
5792
|
-
console.error(
|
|
5793
|
-
`[listV2Sessions] GET /api/session pagination failed (port ${client.port}): ${errorName}`
|
|
5794
|
-
);
|
|
5795
|
-
return null;
|
|
5796
5786
|
}
|
|
5787
|
+
return hasLaterUser && hasStartedLaterUser;
|
|
5797
5788
|
}
|
|
5798
|
-
async function
|
|
5799
|
-
|
|
5800
|
-
const
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
return { ok: true, status: response.status };
|
|
5789
|
+
async function hasAnyConfiguredProvider(port, client) {
|
|
5790
|
+
if (client?.version === "v2") {
|
|
5791
|
+
const directory = await getOpenCodeDirectoryV2(client);
|
|
5792
|
+
if (!directory) {
|
|
5793
|
+
console.error(
|
|
5794
|
+
`[hasAnyConfiguredProvider] V2 working directory was unavailable (port ${port})`
|
|
5795
|
+
);
|
|
5796
|
+
return null;
|
|
5807
5797
|
}
|
|
5798
|
+
const query = `?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
|
|
5799
|
+
const integrationPath = `/api/integration${query}`;
|
|
5800
|
+
let integrationReady = null;
|
|
5808
5801
|
try {
|
|
5809
|
-
const
|
|
5810
|
-
|
|
5802
|
+
const res = await client.request(integrationPath);
|
|
5803
|
+
if (!res.ok) {
|
|
5804
|
+
console.error(
|
|
5805
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} returned HTTP ${res.status} (port ${port})`
|
|
5806
|
+
);
|
|
5807
|
+
} else {
|
|
5808
|
+
const body = await res.json();
|
|
5809
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
|
|
5810
|
+
console.error(
|
|
5811
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} body had no integration data array (port ${port})`
|
|
5812
|
+
);
|
|
5813
|
+
} else {
|
|
5814
|
+
let valid = true;
|
|
5815
|
+
for (const integration of body.data) {
|
|
5816
|
+
if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
5817
|
+
console.error(
|
|
5818
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} body contained an invalid integration (port ${port})`
|
|
5819
|
+
);
|
|
5820
|
+
valid = false;
|
|
5821
|
+
break;
|
|
5822
|
+
}
|
|
5823
|
+
}
|
|
5824
|
+
if (valid) {
|
|
5825
|
+
integrationReady = body.data.some(
|
|
5826
|
+
(integration) => integration.connections.length > 0
|
|
5827
|
+
);
|
|
5828
|
+
}
|
|
5829
|
+
}
|
|
5830
|
+
}
|
|
5811
5831
|
} catch (error2) {
|
|
5812
5832
|
console.error(
|
|
5813
|
-
`[
|
|
5833
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
5814
5834
|
);
|
|
5815
|
-
return { ok: false, status: response.status };
|
|
5816
5835
|
}
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
);
|
|
5821
|
-
return { ok: false, status: "threw" };
|
|
5822
|
-
}
|
|
5823
|
-
}
|
|
5824
|
-
async function v2SessionExists(client, sessionId) {
|
|
5825
|
-
try {
|
|
5826
|
-
const response = await client.request(
|
|
5827
|
-
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
5828
|
-
void 0,
|
|
5829
|
-
{ allowStatuses: [404] }
|
|
5830
|
-
);
|
|
5831
|
-
return response.status === 404 ? false : true;
|
|
5832
|
-
} catch (error2) {
|
|
5833
|
-
console.error(
|
|
5834
|
-
`[v2SessionExists] GET /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
5835
|
-
);
|
|
5836
|
-
return null;
|
|
5837
|
-
}
|
|
5838
|
-
}
|
|
5839
|
-
async function sendV2Prompt(client, sessionId, text, options, onInvalidModelSelection) {
|
|
5840
|
-
const modelSelection = options?.model?.trim();
|
|
5841
|
-
const { model, variant } = splitModelVariant(modelSelection);
|
|
5842
|
-
const separator = model?.indexOf("/") ?? -1;
|
|
5843
|
-
const modelReference = model && separator > 0 && separator < model.length - 1 ? {
|
|
5844
|
-
id: model.slice(separator + 1),
|
|
5845
|
-
providerID: model.slice(0, separator),
|
|
5846
|
-
...variant ? { variant } : {}
|
|
5847
|
-
} : void 0;
|
|
5848
|
-
if (modelSelection && !modelReference) onInvalidModelSelection?.();
|
|
5849
|
-
if (options?.agent) {
|
|
5850
|
-
await client.request(`/api/session/${encodeURIComponent(sessionId)}/agent`, {
|
|
5851
|
-
method: "POST",
|
|
5852
|
-
headers: { "Content-Type": "application/json" },
|
|
5853
|
-
body: JSON.stringify({ agent: options.agent })
|
|
5854
|
-
});
|
|
5855
|
-
}
|
|
5856
|
-
if (modelReference) {
|
|
5836
|
+
if (integrationReady === true) return true;
|
|
5837
|
+
const providerPath = `/api/provider${query}`;
|
|
5838
|
+
let providerReady = null;
|
|
5857
5839
|
try {
|
|
5858
|
-
await client.request(
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5840
|
+
const res = await client.request(providerPath);
|
|
5841
|
+
if (!res.ok) {
|
|
5842
|
+
console.error(
|
|
5843
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} returned HTTP ${res.status} (port ${port})`
|
|
5844
|
+
);
|
|
5845
|
+
} else {
|
|
5846
|
+
const body = await res.json();
|
|
5847
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
|
|
5848
|
+
console.error(
|
|
5849
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} body had no provider data array (port ${port})`
|
|
5850
|
+
);
|
|
5851
|
+
} else {
|
|
5852
|
+
let valid = true;
|
|
5853
|
+
for (const provider of body.data) {
|
|
5854
|
+
if (!provider || typeof provider !== "object" || Array.isArray(provider) || typeof provider.id !== "string") {
|
|
5855
|
+
console.error(
|
|
5856
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} body contained an invalid provider (port ${port})`
|
|
5857
|
+
);
|
|
5858
|
+
valid = false;
|
|
5859
|
+
break;
|
|
5860
|
+
}
|
|
5861
|
+
}
|
|
5862
|
+
if (valid) {
|
|
5863
|
+
providerReady = body.data.some(
|
|
5864
|
+
(provider) => provider.activation === "enabled"
|
|
5865
|
+
);
|
|
5866
|
+
}
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
5863
5869
|
} catch (error2) {
|
|
5864
|
-
|
|
5870
|
+
console.error(
|
|
5871
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
5872
|
+
);
|
|
5865
5873
|
}
|
|
5874
|
+
if (providerReady === true) return true;
|
|
5875
|
+
if (integrationReady === null || providerReady === null) return null;
|
|
5876
|
+
return false;
|
|
5866
5877
|
}
|
|
5867
|
-
const path = `/api/session/${encodeURIComponent(sessionId)}/prompt`;
|
|
5868
|
-
const response = await client.request(path, {
|
|
5869
|
-
method: "POST",
|
|
5870
|
-
headers: { "Content-Type": "application/json" },
|
|
5871
|
-
body: JSON.stringify({ text, delivery: "queue" })
|
|
5872
|
-
});
|
|
5873
|
-
let body;
|
|
5874
|
-
try {
|
|
5875
|
-
body = await readJson(response);
|
|
5876
|
-
} catch (error2) {
|
|
5877
|
-
throw new OpenCodeV2PromptAckError(
|
|
5878
|
-
`OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
5879
|
-
);
|
|
5880
|
-
}
|
|
5881
|
-
const data = isRecord5(body) && "data" in body ? body.data : void 0;
|
|
5882
|
-
if (!isRecord5(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
5883
|
-
throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
|
|
5884
|
-
}
|
|
5885
|
-
return data.id;
|
|
5886
|
-
}
|
|
5887
|
-
async function getV2SessionMessages(client, sessionId) {
|
|
5888
|
-
const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
|
|
5889
|
-
try {
|
|
5890
|
-
const body = await readJson(await client.request(path));
|
|
5891
|
-
if (!isRecord5(body) || !Array.isArray(body.data) || !isRecord5(body.cursor)) return null;
|
|
5892
|
-
return adaptV2Messages(body);
|
|
5893
|
-
} catch (error2) {
|
|
5894
|
-
console.error(
|
|
5895
|
-
`[getV2SessionMessages] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
5896
|
-
);
|
|
5897
|
-
return null;
|
|
5898
|
-
}
|
|
5899
|
-
}
|
|
5900
|
-
async function listV2Forms(client, sessionId) {
|
|
5901
|
-
const path = `/api/session/${encodeURIComponent(sessionId)}/form`;
|
|
5902
|
-
try {
|
|
5903
|
-
return adaptV2FormList(await readJson(await client.request(path)));
|
|
5904
|
-
} catch (error2) {
|
|
5905
|
-
console.error(
|
|
5906
|
-
`[listV2Forms] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
5907
|
-
);
|
|
5908
|
-
return null;
|
|
5909
|
-
}
|
|
5910
|
-
}
|
|
5911
|
-
async function listV2Permissions(client, sessionId) {
|
|
5912
|
-
const path = `/api/session/${encodeURIComponent(sessionId)}/permission`;
|
|
5913
|
-
try {
|
|
5914
|
-
return adaptV2PermissionList(await readJson(await client.request(path)));
|
|
5915
|
-
} catch (error2) {
|
|
5916
|
-
console.error(
|
|
5917
|
-
`[listV2Permissions] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
5918
|
-
);
|
|
5919
|
-
return null;
|
|
5920
|
-
}
|
|
5921
|
-
}
|
|
5922
|
-
async function interruptV2Session(client, sessionId) {
|
|
5923
|
-
await client.request(`/api/session/${encodeURIComponent(sessionId)}/interrupt`, {
|
|
5924
|
-
method: "POST"
|
|
5925
|
-
});
|
|
5926
|
-
}
|
|
5927
|
-
async function getV2ActiveSessions(client) {
|
|
5928
5878
|
try {
|
|
5929
|
-
const
|
|
5930
|
-
if (!
|
|
5931
|
-
|
|
5932
|
-
|
|
5879
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
5880
|
+
if (!res.ok) {
|
|
5881
|
+
console.error(
|
|
5882
|
+
`[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
5883
|
+
);
|
|
5884
|
+
return null;
|
|
5885
|
+
}
|
|
5886
|
+
const body = await res.json();
|
|
5887
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
5888
|
+
console.error(
|
|
5889
|
+
`[hasAnyConfiguredProvider] GET /config/providers body was not a plain object (port ${port})`
|
|
5890
|
+
);
|
|
5891
|
+
return null;
|
|
5892
|
+
}
|
|
5893
|
+
const defaults2 = body.default;
|
|
5894
|
+
if (!defaults2 || typeof defaults2 !== "object" || Array.isArray(defaults2)) {
|
|
5895
|
+
console.error(
|
|
5896
|
+
`[hasAnyConfiguredProvider] GET /config/providers body had no \`default\` object (port ${port})`
|
|
5897
|
+
);
|
|
5898
|
+
return null;
|
|
5899
|
+
}
|
|
5900
|
+
return Object.keys(defaults2).length > 0;
|
|
5901
|
+
} catch (err) {
|
|
5933
5902
|
console.error(
|
|
5934
|
-
`[
|
|
5903
|
+
`[hasAnyConfiguredProvider] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
5935
5904
|
);
|
|
5936
5905
|
return null;
|
|
5937
5906
|
}
|
|
5938
5907
|
}
|
|
5939
|
-
|
|
5940
|
-
const
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
if (isRecord5(value)) {
|
|
5947
|
-
const data = isRecord5(value.data) ? value.data : void 0;
|
|
5948
|
-
const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
|
|
5949
|
-
if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
5950
|
-
}
|
|
5951
|
-
return "OpenCode reported a session error with no details";
|
|
5908
|
+
function sessionErrorReason2(error2) {
|
|
5909
|
+
const record = typeof error2 === "object" && error2 !== null ? error2 : null;
|
|
5910
|
+
const data = record?.data;
|
|
5911
|
+
const dataRecord = typeof data === "object" && data !== null ? data : null;
|
|
5912
|
+
const rawReason = typeof dataRecord?.message === "string" && dataRecord.message || typeof record?.message === "string" && record.message || typeof error2 === "string" && error2 || typeof record?.name === "string" && record.name || "OpenCode reported a session error with no details";
|
|
5913
|
+
const reason = rawReason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
5914
|
+
return reason || "OpenCode reported a session error with no details";
|
|
5952
5915
|
}
|
|
5953
|
-
function
|
|
5954
|
-
let parsed
|
|
5955
|
-
if (typeof value === "string") {
|
|
5956
|
-
try {
|
|
5957
|
-
parsed = JSON.parse(value);
|
|
5958
|
-
} catch (error2) {
|
|
5959
|
-
void error2;
|
|
5960
|
-
return null;
|
|
5961
|
-
}
|
|
5962
|
-
}
|
|
5963
|
-
if (!isRecord5(parsed)) return null;
|
|
5916
|
+
function parseSessionErrorFrame(data) {
|
|
5917
|
+
let parsed;
|
|
5964
5918
|
try {
|
|
5965
|
-
|
|
5966
|
-
if (establishedShape) return establishedShape;
|
|
5919
|
+
parsed = JSON.parse(data);
|
|
5967
5920
|
} catch (error2) {
|
|
5968
5921
|
void error2;
|
|
5922
|
+
return null;
|
|
5969
5923
|
}
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
return
|
|
5978
|
-
sessionId,
|
|
5979
|
-
reason: sessionErrorReason2(properties.error ?? properties)
|
|
5980
|
-
};
|
|
5924
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
5925
|
+
const parsedRecord = parsed;
|
|
5926
|
+
const payload = parsedRecord.payload;
|
|
5927
|
+
const event = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : parsedRecord;
|
|
5928
|
+
if (event.type !== "session.error") return null;
|
|
5929
|
+
const properties = event.properties;
|
|
5930
|
+
if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
|
|
5931
|
+
return null;
|
|
5981
5932
|
}
|
|
5982
|
-
|
|
5933
|
+
const propertiesRecord = properties;
|
|
5934
|
+
const sessionId = propertiesRecord.sessionID;
|
|
5935
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
5936
|
+
return {
|
|
5937
|
+
sessionId,
|
|
5938
|
+
reason: sessionErrorReason2(propertiesRecord.error)
|
|
5939
|
+
};
|
|
5983
5940
|
}
|
|
5984
|
-
async function
|
|
5941
|
+
async function readSessionErrorStream(port, options, client) {
|
|
5942
|
+
if (client?.version === "v2") return readV2SessionErrorStream(client, options);
|
|
5985
5943
|
let reader = null;
|
|
5986
5944
|
try {
|
|
5987
|
-
const response = await client
|
|
5945
|
+
const response = await (client?.request("/event", {
|
|
5988
5946
|
headers: { accept: "text/event-stream" },
|
|
5989
5947
|
signal: options.signal
|
|
5990
|
-
})
|
|
5948
|
+
}) ?? fetch(`${opencodeBase(port)}/event`, {
|
|
5949
|
+
headers: { accept: "text/event-stream" },
|
|
5950
|
+
signal: options.signal
|
|
5951
|
+
}));
|
|
5991
5952
|
if (!response.ok || !response.body) {
|
|
5992
5953
|
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
5993
5954
|
}
|
|
@@ -5997,7 +5958,7 @@ async function readV2SessionErrorStream(client, options) {
|
|
|
5997
5958
|
const processLine = (line) => {
|
|
5998
5959
|
const trimmed = line.trimEnd();
|
|
5999
5960
|
if (!trimmed.startsWith("data:")) return;
|
|
6000
|
-
const event =
|
|
5961
|
+
const event = parseSessionErrorFrame(trimmed.slice("data:".length).replace(/^ /, ""));
|
|
6001
5962
|
if (event) options.onSessionError(event);
|
|
6002
5963
|
};
|
|
6003
5964
|
while (true) {
|
|
@@ -6008,16 +5969,61 @@ async function readV2SessionErrorStream(client, options) {
|
|
|
6008
5969
|
buffer = lines.pop() ?? "";
|
|
6009
5970
|
for (const line of lines) processLine(line);
|
|
6010
5971
|
}
|
|
6011
|
-
} catch (
|
|
5972
|
+
} catch (err) {
|
|
6012
5973
|
if (options.signal.aborted) return { reason: "aborted" };
|
|
6013
5974
|
return {
|
|
6014
5975
|
reason: "unavailable",
|
|
6015
|
-
detail:
|
|
5976
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
6016
5977
|
};
|
|
6017
5978
|
} finally {
|
|
6018
5979
|
if (reader) void reader.cancel().catch(() => void 0);
|
|
6019
5980
|
}
|
|
6020
5981
|
}
|
|
5982
|
+
async function disposeAndReloadProviderConfig(port, client) {
|
|
5983
|
+
if (client?.version === "v2") return { outcome: "skipped_v2" };
|
|
5984
|
+
try {
|
|
5985
|
+
const res = await requestWithClient(
|
|
5986
|
+
port,
|
|
5987
|
+
client,
|
|
5988
|
+
"/config",
|
|
5989
|
+
{
|
|
5990
|
+
method: "PATCH",
|
|
5991
|
+
headers: { "Content-Type": "application/json" },
|
|
5992
|
+
body: JSON.stringify({})
|
|
5993
|
+
},
|
|
5994
|
+
{ allowStatuses: ALL_HTTP_STATUSES2 }
|
|
5995
|
+
);
|
|
5996
|
+
if (!res.ok) {
|
|
5997
|
+
console.error(
|
|
5998
|
+
`[disposeAndReloadProviderConfig] PATCH /config returned HTTP ${res.status} (port ${port})`
|
|
5999
|
+
);
|
|
6000
|
+
return { outcome: "failed", status: res.status, detail: null };
|
|
6001
|
+
}
|
|
6002
|
+
return { outcome: "reloaded" };
|
|
6003
|
+
} catch (err) {
|
|
6004
|
+
console.error(
|
|
6005
|
+
`[disposeAndReloadProviderConfig] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
6006
|
+
);
|
|
6007
|
+
return {
|
|
6008
|
+
outcome: "failed",
|
|
6009
|
+
status: null,
|
|
6010
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
6011
|
+
};
|
|
6012
|
+
}
|
|
6013
|
+
}
|
|
6014
|
+
async function probeOpenCodeRequestReadiness(port, client) {
|
|
6015
|
+
try {
|
|
6016
|
+
const res = await requestWithClient(port, client, "/config", void 0, {
|
|
6017
|
+
allowStatuses: ALL_HTTP_STATUSES2
|
|
6018
|
+
});
|
|
6019
|
+
const body = await res.text();
|
|
6020
|
+
return { ok: res.ok, status: res.status, bodyEmpty: body.trim().length === 0, detail: null };
|
|
6021
|
+
} catch (err) {
|
|
6022
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
6023
|
+
console.error(`[probeOpenCodeRequestReadiness] GET /config failed (port ${port}): ${detail}`);
|
|
6024
|
+
return { ok: false, status: null, bodyEmpty: null, detail };
|
|
6025
|
+
}
|
|
6026
|
+
}
|
|
6021
6027
|
|
|
6022
6028
|
// src/lib/opencode/credential-import-v2.ts
|
|
6023
6029
|
import { readFileSync as readFileSync8 } from "node:fs";
|
|
@@ -6266,7 +6272,7 @@ var INTEGRATION_LIST_RETRY_DELAY_MS = 1e3;
|
|
|
6266
6272
|
function isRecord7(value) {
|
|
6267
6273
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6268
6274
|
}
|
|
6269
|
-
var sleep2 = (milliseconds) => new Promise((
|
|
6275
|
+
var sleep2 = (milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds));
|
|
6270
6276
|
function emptyResult(unsupported = [], outcome = "completed") {
|
|
6271
6277
|
return { outcome, imported: [], alreadyConnected: [], unsupported, failed: [] };
|
|
6272
6278
|
}
|
|
@@ -6583,7 +6589,7 @@ function parseSynchroniserEnv(stdout) {
|
|
|
6583
6589
|
return values;
|
|
6584
6590
|
}
|
|
6585
6591
|
function runCommand(command, args, options) {
|
|
6586
|
-
return new Promise((
|
|
6592
|
+
return new Promise((resolve6) => {
|
|
6587
6593
|
let child;
|
|
6588
6594
|
let stdout = "";
|
|
6589
6595
|
let stderr = "";
|
|
@@ -6592,7 +6598,7 @@ function runCommand(command, args, options) {
|
|
|
6592
6598
|
if (settled) return;
|
|
6593
6599
|
settled = true;
|
|
6594
6600
|
if (timer) clearTimeout(timer);
|
|
6595
|
-
|
|
6601
|
+
resolve6(result);
|
|
6596
6602
|
};
|
|
6597
6603
|
try {
|
|
6598
6604
|
child = spawn2(command, args, {
|
|
@@ -6600,7 +6606,7 @@ function runCommand(command, args, options) {
|
|
|
6600
6606
|
stdio: ["ignore", "pipe", "pipe"]
|
|
6601
6607
|
});
|
|
6602
6608
|
} catch (error2) {
|
|
6603
|
-
|
|
6609
|
+
resolve6({
|
|
6604
6610
|
code: null,
|
|
6605
6611
|
stdout,
|
|
6606
6612
|
stderr: error2 instanceof Error ? error2.message : String(error2),
|
|
@@ -7127,7 +7133,7 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
7127
7133
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
7128
7134
|
return { outcome: "exited", code: child.exitCode, signal: child.signalCode };
|
|
7129
7135
|
}
|
|
7130
|
-
return new Promise((
|
|
7136
|
+
return new Promise((resolve6, reject) => {
|
|
7131
7137
|
let forced = false;
|
|
7132
7138
|
let settled = false;
|
|
7133
7139
|
const timer = setTimeout(() => {
|
|
@@ -7147,7 +7153,7 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
7147
7153
|
settled = true;
|
|
7148
7154
|
clearTimeout(timer);
|
|
7149
7155
|
child.removeListener("exit", onExit);
|
|
7150
|
-
|
|
7156
|
+
resolve6(result);
|
|
7151
7157
|
};
|
|
7152
7158
|
const fail = (error2) => {
|
|
7153
7159
|
if (settled) return;
|
|
@@ -7744,6 +7750,176 @@ import WebSocket2 from "ws";
|
|
|
7744
7750
|
|
|
7745
7751
|
// src/lib/tunnel/forwarding.ts
|
|
7746
7752
|
import WebSocket from "ws";
|
|
7753
|
+
|
|
7754
|
+
// src/lib/file-read.ts
|
|
7755
|
+
import { constants } from "node:fs";
|
|
7756
|
+
import { open as open3, realpath as realpath2, stat as stat2 } from "node:fs/promises";
|
|
7757
|
+
import { extname, isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
|
|
7758
|
+
import { homedir as homedir6 } from "node:os";
|
|
7759
|
+
|
|
7760
|
+
// src/lib/path-validation.ts
|
|
7761
|
+
import { basename as basename2, dirname as dirname9, isAbsolute as isAbsolute2, join as join12, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
|
7762
|
+
import { realpath } from "node:fs/promises";
|
|
7763
|
+
function expandAndValidatePath(requestedPath, homeDir) {
|
|
7764
|
+
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
7765
|
+
return null;
|
|
7766
|
+
}
|
|
7767
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join12(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
7768
|
+
if (expanded.split(/[/\\]/).includes("..") || !isAbsolute2(expanded)) {
|
|
7769
|
+
return null;
|
|
7770
|
+
}
|
|
7771
|
+
const candidate = resolve3(expanded);
|
|
7772
|
+
const name = basename2(candidate);
|
|
7773
|
+
return name === "" || name === "." || name === ".." ? null : candidate;
|
|
7774
|
+
}
|
|
7775
|
+
async function resolveNearestExistingAncestor(directory) {
|
|
7776
|
+
const missingSegments = [];
|
|
7777
|
+
let current = directory;
|
|
7778
|
+
for (; ; ) {
|
|
7779
|
+
try {
|
|
7780
|
+
return { existingAncestor: await realpath(current), missingSegments };
|
|
7781
|
+
} catch (err) {
|
|
7782
|
+
const parent = dirname9(current);
|
|
7783
|
+
if (err.code !== "ENOENT" || parent === current) {
|
|
7784
|
+
throw err;
|
|
7785
|
+
}
|
|
7786
|
+
missingSegments.unshift(basename2(current));
|
|
7787
|
+
current = parent;
|
|
7788
|
+
}
|
|
7789
|
+
}
|
|
7790
|
+
}
|
|
7791
|
+
function isPathContained(directory, target) {
|
|
7792
|
+
const rel = relative2(directory, target);
|
|
7793
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute2(rel);
|
|
7794
|
+
}
|
|
7795
|
+
|
|
7796
|
+
// src/lib/file-read.ts
|
|
7797
|
+
var FILE_MIME_TYPES = {
|
|
7798
|
+
".png": "image/png",
|
|
7799
|
+
".jpg": "image/jpeg",
|
|
7800
|
+
".jpeg": "image/jpeg",
|
|
7801
|
+
".gif": "image/gif",
|
|
7802
|
+
".webp": "image/webp",
|
|
7803
|
+
".mp4": "video/mp4",
|
|
7804
|
+
".mov": "video/quicktime"
|
|
7805
|
+
};
|
|
7806
|
+
async function readProjectFile(projectRoot, method, requestedPath, openFile = open3) {
|
|
7807
|
+
if (method !== "GET") return refuse(405, "method_not_allowed");
|
|
7808
|
+
if (!projectRoot || !isAbsolute3(projectRoot)) return refuse(503, "project_root_unavailable");
|
|
7809
|
+
if (!requestedPath || requestedPath.length > 4096 || requestedPath.includes("\0")) {
|
|
7810
|
+
return refuse(400, "invalid_path");
|
|
7811
|
+
}
|
|
7812
|
+
let lexicalRoot;
|
|
7813
|
+
let realRoot;
|
|
7814
|
+
try {
|
|
7815
|
+
lexicalRoot = resolve4(projectRoot);
|
|
7816
|
+
realRoot = await realpath2(lexicalRoot);
|
|
7817
|
+
} catch (err) {
|
|
7818
|
+
log("warn", "tunnel_file_read_root_unavailable", { ...errorFields(err) });
|
|
7819
|
+
return refuse(503, "project_root_unavailable");
|
|
7820
|
+
}
|
|
7821
|
+
const decodedPath = decodeRequestedPath(requestedPath);
|
|
7822
|
+
if (decodedPath.path === null) return refuse(400, "invalid_path", decodedPath.error);
|
|
7823
|
+
const pathValue = decodedPath.path;
|
|
7824
|
+
if (pathValue.split(/[/\\]/).includes("..")) return refuse(400, "invalid_path");
|
|
7825
|
+
const absolutePath = pathValue === "~" || pathValue.startsWith("~/") ? expandAndValidatePath(pathValue, homedir6()) : expandAndValidatePath(
|
|
7826
|
+
isAbsolute3(pathValue) ? pathValue : resolve4(lexicalRoot, pathValue),
|
|
7827
|
+
homedir6()
|
|
7828
|
+
);
|
|
7829
|
+
if (!absolutePath) return refuse(400, "invalid_path");
|
|
7830
|
+
let realTarget;
|
|
7831
|
+
try {
|
|
7832
|
+
realTarget = await realpath2(absolutePath);
|
|
7833
|
+
} catch (err) {
|
|
7834
|
+
if (err.code === "ENOENT") return refuse(404, "not_found");
|
|
7835
|
+
log("warn", "tunnel_file_read_path_resolution_failed", { ...errorFields(err) });
|
|
7836
|
+
return refuse(400, "unresolvable_path");
|
|
7837
|
+
}
|
|
7838
|
+
if (realTarget !== realRoot && !isPathContained(realRoot, realTarget)) {
|
|
7839
|
+
return refuse(
|
|
7840
|
+
403,
|
|
7841
|
+
isPathContained(lexicalRoot, absolutePath) ? "symlink_outside_project" : "path_outside_project"
|
|
7842
|
+
);
|
|
7843
|
+
}
|
|
7844
|
+
let fileStats;
|
|
7845
|
+
try {
|
|
7846
|
+
fileStats = await stat2(realTarget);
|
|
7847
|
+
} catch (err) {
|
|
7848
|
+
if (err.code === "ENOENT") return refuse(404, "not_found");
|
|
7849
|
+
log("warn", "tunnel_file_read_stat_failed", { ...errorFields(err) });
|
|
7850
|
+
return refuse(500, "read_failed");
|
|
7851
|
+
}
|
|
7852
|
+
if (fileStats.isDirectory()) return refuse(409, "is_directory");
|
|
7853
|
+
if (!fileStats.isFile()) return refuse(422, "not_regular_file");
|
|
7854
|
+
if (fileStats.size > TUNNEL_FILE_READ_MAX_BYTES) return refuse(413, "file_too_large");
|
|
7855
|
+
const mimeType = FILE_MIME_TYPES[extname(realTarget).toLowerCase()];
|
|
7856
|
+
if (!mimeType) return refuse(415, "unsupported_type");
|
|
7857
|
+
let handle;
|
|
7858
|
+
try {
|
|
7859
|
+
handle = await openFile(realTarget, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
7860
|
+
const openedStats = await handle.stat();
|
|
7861
|
+
if (!openedStats.isFile()) return refuse(422, "not_regular_file");
|
|
7862
|
+
if (openedStats.size > TUNNEL_FILE_READ_MAX_BYTES) return refuse(413, "file_too_large");
|
|
7863
|
+
const bytes = Buffer.alloc(TUNNEL_FILE_READ_MAX_BYTES + 1);
|
|
7864
|
+
let length = 0;
|
|
7865
|
+
while (length < bytes.length) {
|
|
7866
|
+
const { bytesRead } = await handle.read(bytes, length, bytes.length - length, null);
|
|
7867
|
+
if (bytesRead === 0) break;
|
|
7868
|
+
length += bytesRead;
|
|
7869
|
+
}
|
|
7870
|
+
if (length > TUNNEL_FILE_READ_MAX_BYTES) return refuse(413, "file_too_large");
|
|
7871
|
+
const body = bytes.subarray(0, length);
|
|
7872
|
+
return {
|
|
7873
|
+
status: 200,
|
|
7874
|
+
headers: {
|
|
7875
|
+
"content-type": mimeType,
|
|
7876
|
+
"content-length": String(body.length)
|
|
7877
|
+
},
|
|
7878
|
+
body
|
|
7879
|
+
};
|
|
7880
|
+
} catch (err) {
|
|
7881
|
+
log("error", "tunnel_file_read_failed", { ...errorFields(err) });
|
|
7882
|
+
return refuse(500, "read_failed");
|
|
7883
|
+
} finally {
|
|
7884
|
+
try {
|
|
7885
|
+
await handle?.close();
|
|
7886
|
+
} catch (err) {
|
|
7887
|
+
log("warn", "tunnel_file_read_close_failed", { ...errorFields(err) });
|
|
7888
|
+
}
|
|
7889
|
+
}
|
|
7890
|
+
}
|
|
7891
|
+
function decodeRequestedPath(target) {
|
|
7892
|
+
if (/^file:/i.test(target)) {
|
|
7893
|
+
try {
|
|
7894
|
+
const url = new URL(target);
|
|
7895
|
+
if (url.protocol !== "file:" || url.host) return { path: null };
|
|
7896
|
+
const rawPath = target.slice(target.indexOf(":") + 1).split(/[?#]/, 1)[0];
|
|
7897
|
+
return { path: decodeURIComponent(rawPath) };
|
|
7898
|
+
} catch (error2) {
|
|
7899
|
+
return { path: null, error: error2 };
|
|
7900
|
+
}
|
|
7901
|
+
}
|
|
7902
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(target)) return { path: null };
|
|
7903
|
+
return { path: target };
|
|
7904
|
+
}
|
|
7905
|
+
function refuse(status2, reason, error2) {
|
|
7906
|
+
const body = Buffer.from(JSON.stringify({ reason }));
|
|
7907
|
+
log("warn", "tunnel_file_read_refused", {
|
|
7908
|
+
reason,
|
|
7909
|
+
status: status2,
|
|
7910
|
+
...error2 === void 0 ? {} : errorFields(error2)
|
|
7911
|
+
});
|
|
7912
|
+
return {
|
|
7913
|
+
status: status2,
|
|
7914
|
+
headers: {
|
|
7915
|
+
"content-type": "application/json",
|
|
7916
|
+
"content-length": String(body.length)
|
|
7917
|
+
},
|
|
7918
|
+
body
|
|
7919
|
+
};
|
|
7920
|
+
}
|
|
7921
|
+
|
|
7922
|
+
// src/lib/tunnel/forwarding.ts
|
|
7747
7923
|
var LOOPBACK_HOST = "127.0.0.1";
|
|
7748
7924
|
var FLOW_PAUSE_MAX_MS = 9e4;
|
|
7749
7925
|
var STRIP_REQ = /* @__PURE__ */ new Set([
|
|
@@ -7771,6 +7947,7 @@ var StreamForwarder = class {
|
|
|
7771
7947
|
}
|
|
7772
7948
|
inflight = /* @__PURE__ */ new Map();
|
|
7773
7949
|
binaryFramesSupported = false;
|
|
7950
|
+
projectRoot = null;
|
|
7774
7951
|
/**
|
|
7775
7952
|
* Handle an edge→agent frame. Unknown frame types are ignored.
|
|
7776
7953
|
*/
|
|
@@ -7819,8 +7996,8 @@ var StreamForwarder = class {
|
|
|
7819
7996
|
pauseStream(sid, stream) {
|
|
7820
7997
|
if (!stream.paused) {
|
|
7821
7998
|
stream.paused = true;
|
|
7822
|
-
stream.pausePromise = new Promise((
|
|
7823
|
-
stream.resume =
|
|
7999
|
+
stream.pausePromise = new Promise((resolve6) => {
|
|
8000
|
+
stream.resume = resolve6;
|
|
7824
8001
|
});
|
|
7825
8002
|
}
|
|
7826
8003
|
if (stream.pauseTimer !== null) clearTimeout(stream.pauseTimer);
|
|
@@ -7854,10 +8031,56 @@ var StreamForwarder = class {
|
|
|
7854
8031
|
this.ws.send(JSON.stringify(frame));
|
|
7855
8032
|
}
|
|
7856
8033
|
}
|
|
8034
|
+
async resolveProjectRoot() {
|
|
8035
|
+
if (this.projectRoot !== null) return this.projectRoot;
|
|
8036
|
+
if (!this.options.resolveProjectRoot) return null;
|
|
8037
|
+
try {
|
|
8038
|
+
this.projectRoot = (await this.options.resolveProjectRoot())?.trim() || null;
|
|
8039
|
+
return this.projectRoot;
|
|
8040
|
+
} catch (err) {
|
|
8041
|
+
log("warn", "tunnel_file_read_root_resolution_failed", { ...errorFields(err) });
|
|
8042
|
+
return null;
|
|
8043
|
+
}
|
|
8044
|
+
}
|
|
8045
|
+
async handleFileRead(sid, method, path) {
|
|
8046
|
+
const queryStart = path.indexOf("?");
|
|
8047
|
+
const query = queryStart < 0 ? "" : path.slice(queryStart + 1);
|
|
8048
|
+
const requestedPath = new URLSearchParams(query).get("path");
|
|
8049
|
+
const projectRoot = method === "GET" ? await this.resolveProjectRoot() : null;
|
|
8050
|
+
const result = await readProjectFile(projectRoot, method, requestedPath);
|
|
8051
|
+
try {
|
|
8052
|
+
this.send({ type: "head", sid, status: result.status, headers: result.headers });
|
|
8053
|
+
for (let offset = 0; offset < result.body.length; offset += MAX_FRAME_BYTES) {
|
|
8054
|
+
const chunk = result.body.subarray(offset, offset + MAX_FRAME_BYTES);
|
|
8055
|
+
if (this.binaryFramesSupported && this.ws.readyState === WebSocket.OPEN) {
|
|
8056
|
+
this.ws.send(encodeBinaryBodyFrame(BINARY_FRAME_RES_DATA, sid, chunk));
|
|
8057
|
+
} else {
|
|
8058
|
+
this.send({ type: "res_data", sid, b64: chunk.toString("base64") });
|
|
8059
|
+
}
|
|
8060
|
+
}
|
|
8061
|
+
this.send({ type: "res_end", sid });
|
|
8062
|
+
} catch (err) {
|
|
8063
|
+
log("error", "tunnel_file_read_response_failed", { sid, ...errorFields(err) });
|
|
8064
|
+
}
|
|
8065
|
+
}
|
|
7857
8066
|
async handleOpen(frame) {
|
|
7858
8067
|
const { sid, method, path, headers, has_body } = frame;
|
|
7859
8068
|
const correlationId = headers?.[CORRELATION_ID_HEADER];
|
|
7860
8069
|
const startedAt = Date.now();
|
|
8070
|
+
const pathName = stripQuery(path);
|
|
8071
|
+
if (pathName === TUNNEL_FILE_READ_PATH) {
|
|
8072
|
+
try {
|
|
8073
|
+
await this.handleFileRead(sid, method, path);
|
|
8074
|
+
} catch (err) {
|
|
8075
|
+
log("error", "tunnel_file_read_handler_failed", { sid, ...errorFields(err) });
|
|
8076
|
+
this.send({
|
|
8077
|
+
type: "res_err",
|
|
8078
|
+
sid,
|
|
8079
|
+
message: "The runner could not read the requested file."
|
|
8080
|
+
});
|
|
8081
|
+
}
|
|
8082
|
+
return;
|
|
8083
|
+
}
|
|
7861
8084
|
if (path !== TUNNEL_DRAIN_PING_PATH && path !== TUNNEL_USAGE_REARM_PING_PATH) {
|
|
7862
8085
|
this.callbacks.onOpen?.(sid, method, path);
|
|
7863
8086
|
}
|
|
@@ -7887,12 +8110,12 @@ var StreamForwarder = class {
|
|
|
7887
8110
|
let endBody;
|
|
7888
8111
|
if (has_body) {
|
|
7889
8112
|
const chunks = [];
|
|
7890
|
-
bodyPromise = new Promise((
|
|
8113
|
+
bodyPromise = new Promise((resolve6) => {
|
|
7891
8114
|
pushBody = (buf) => {
|
|
7892
8115
|
chunks.push(buf);
|
|
7893
8116
|
};
|
|
7894
8117
|
endBody = () => {
|
|
7895
|
-
|
|
8118
|
+
resolve6(Buffer.concat(chunks));
|
|
7896
8119
|
};
|
|
7897
8120
|
});
|
|
7898
8121
|
}
|
|
@@ -8059,11 +8282,12 @@ function connectTunnel(options) {
|
|
|
8059
8282
|
onInfo,
|
|
8060
8283
|
onWarning,
|
|
8061
8284
|
onDrainPing,
|
|
8062
|
-
onUsageRearmPing
|
|
8285
|
+
onUsageRearmPing,
|
|
8286
|
+
resolveProjectRoot
|
|
8063
8287
|
} = options;
|
|
8064
8288
|
const tunnelUrl = getTunnelUrlConfig();
|
|
8065
8289
|
const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
|
|
8066
|
-
return new Promise((
|
|
8290
|
+
return new Promise((resolve6, reject) => {
|
|
8067
8291
|
const ws = new WebSocket2(url, {
|
|
8068
8292
|
headers: {
|
|
8069
8293
|
Authorization: authHeader,
|
|
@@ -8078,7 +8302,10 @@ function connectTunnel(options) {
|
|
|
8078
8302
|
onDrainPing: () => onDrainPing?.(),
|
|
8079
8303
|
onUsageRearmPing: () => onUsageRearmPing?.()
|
|
8080
8304
|
},
|
|
8081
|
-
{
|
|
8305
|
+
{
|
|
8306
|
+
openCodePassword,
|
|
8307
|
+
...resolveProjectRoot ? { resolveProjectRoot } : {}
|
|
8308
|
+
}
|
|
8082
8309
|
);
|
|
8083
8310
|
const connectionTimeout = setTimeout(() => {
|
|
8084
8311
|
ws.close();
|
|
@@ -8174,7 +8401,7 @@ function connectTunnel(options) {
|
|
|
8174
8401
|
const connectedAgentId = message.agent_id ?? agentId;
|
|
8175
8402
|
relayConnectionId = message.connection_id;
|
|
8176
8403
|
onConnected?.(connectedAgentId, relayConnectionId);
|
|
8177
|
-
|
|
8404
|
+
resolve6({
|
|
8178
8405
|
ws,
|
|
8179
8406
|
close: () => ws.close(1e3, "CLI shutdown")
|
|
8180
8407
|
});
|
|
@@ -8273,6 +8500,7 @@ var RunnerConnection = class _RunnerConnection {
|
|
|
8273
8500
|
authHeader: this.opts.getAuthHeader(),
|
|
8274
8501
|
port: this.opts.port,
|
|
8275
8502
|
openCodePassword: this.opts.openCodePassword,
|
|
8503
|
+
...this.opts.resolveProjectRoot ? { resolveProjectRoot: this.opts.resolveProjectRoot } : {},
|
|
8276
8504
|
onConnected: (agentId, relayConnectionId) => {
|
|
8277
8505
|
const attempt = this.reconnectAttempt + 1;
|
|
8278
8506
|
this.reconnectAttempt = 0;
|
|
@@ -8730,28 +8958,28 @@ function createSessionStoreHealth(options = {}) {
|
|
|
8730
8958
|
}
|
|
8731
8959
|
|
|
8732
8960
|
// src/lib/channels/driver.ts
|
|
8733
|
-
import { homedir as
|
|
8961
|
+
import { homedir as homedir7 } from "node:os";
|
|
8734
8962
|
|
|
8735
8963
|
// src/lib/runner-file-sync.ts
|
|
8736
|
-
import { join as
|
|
8964
|
+
import { join as join14 } from "node:path";
|
|
8737
8965
|
|
|
8738
8966
|
// src/lib/file-push.ts
|
|
8739
8967
|
import { randomUUID } from "node:crypto";
|
|
8740
|
-
import { chmod, mkdir, open as
|
|
8741
|
-
import { basename as
|
|
8968
|
+
import { chmod, mkdir, open as open4, realpath as realpath3, rename, unlink } from "node:fs/promises";
|
|
8969
|
+
import { basename as basename3, dirname as dirname10, isAbsolute as isAbsolute4, join as join13 } from "node:path";
|
|
8742
8970
|
var FILE_MODE = 384;
|
|
8743
8971
|
var DIRECTORY_MODE = 448;
|
|
8744
8972
|
async function writePushedFile(request3) {
|
|
8745
8973
|
const { requestedPath, content, allowedDirectories, allowedFiles, homeDir } = request3;
|
|
8746
8974
|
const bytes = content.byteLength;
|
|
8747
8975
|
if (allowedDirectories.length === 0 && allowedFiles.length === 0) {
|
|
8748
|
-
return
|
|
8976
|
+
return refuse2("file_sync_disabled", "File sync is not enabled on this runner.", {
|
|
8749
8977
|
path: requestedPath,
|
|
8750
8978
|
bytes
|
|
8751
8979
|
});
|
|
8752
8980
|
}
|
|
8753
8981
|
if (bytes > MAX_FILE_PUSH_BYTES) {
|
|
8754
|
-
return
|
|
8982
|
+
return refuse2(
|
|
8755
8983
|
"file_too_large",
|
|
8756
8984
|
`File is ${bytes} bytes; the limit is ${MAX_FILE_PUSH_BYTES}.`,
|
|
8757
8985
|
{
|
|
@@ -8760,30 +8988,30 @@ async function writePushedFile(request3) {
|
|
|
8760
8988
|
}
|
|
8761
8989
|
);
|
|
8762
8990
|
}
|
|
8763
|
-
const candidate =
|
|
8991
|
+
const candidate = expandAndValidatePath(requestedPath, homeDir);
|
|
8764
8992
|
if (candidate === null) {
|
|
8765
|
-
return
|
|
8993
|
+
return refuse2("invalid_path", "The requested path is not a valid absolute file path.", {
|
|
8766
8994
|
path: requestedPath,
|
|
8767
8995
|
bytes
|
|
8768
8996
|
});
|
|
8769
8997
|
}
|
|
8770
8998
|
try {
|
|
8771
8999
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
8772
|
-
|
|
9000
|
+
dirname10(candidate)
|
|
8773
9001
|
);
|
|
8774
|
-
const realTarget =
|
|
9002
|
+
const realTarget = join13(existingAncestor, ...missingSegments, basename3(candidate));
|
|
8775
9003
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget) ?? await findMatchingAllowedFile(allowedFiles, realTarget);
|
|
8776
9004
|
if (allowedDirectory === null) {
|
|
8777
|
-
return
|
|
9005
|
+
return refuse2("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
8778
9006
|
path: realTarget,
|
|
8779
9007
|
bytes
|
|
8780
9008
|
});
|
|
8781
9009
|
}
|
|
8782
9010
|
if (missingSegments.length > 0) {
|
|
8783
9011
|
await createMissingDirectories(existingAncestor, missingSegments);
|
|
8784
|
-
const realParent = await
|
|
8785
|
-
if (realParent !==
|
|
8786
|
-
return
|
|
9012
|
+
const realParent = await realpath3(dirname10(realTarget));
|
|
9013
|
+
if (realParent !== dirname10(realTarget) || !isPathContained(allowedDirectory, realTarget)) {
|
|
9014
|
+
return refuse2("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
8787
9015
|
path: realTarget,
|
|
8788
9016
|
bytes,
|
|
8789
9017
|
reason: "parent_changed_after_create"
|
|
@@ -8795,7 +9023,7 @@ async function writePushedFile(request3) {
|
|
|
8795
9023
|
return { ok: true, path: realTarget };
|
|
8796
9024
|
} catch (err) {
|
|
8797
9025
|
const errno = err.code ?? "UNKNOWN";
|
|
8798
|
-
return
|
|
9026
|
+
return refuse2("write_failed", `The runner could not write the file (${errno}).`, {
|
|
8799
9027
|
path: candidate,
|
|
8800
9028
|
bytes,
|
|
8801
9029
|
errno,
|
|
@@ -8803,45 +9031,14 @@ async function writePushedFile(request3) {
|
|
|
8803
9031
|
});
|
|
8804
9032
|
}
|
|
8805
9033
|
}
|
|
8806
|
-
function expandAndValidate(requestedPath, homeDir) {
|
|
8807
|
-
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
8808
|
-
return null;
|
|
8809
|
-
}
|
|
8810
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join12(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
8811
|
-
if (expanded.split(/[/\\]/).includes("..")) {
|
|
8812
|
-
return null;
|
|
8813
|
-
}
|
|
8814
|
-
if (!isAbsolute2(expanded)) {
|
|
8815
|
-
return null;
|
|
8816
|
-
}
|
|
8817
|
-
const candidate = resolve3(expanded);
|
|
8818
|
-
const name = basename2(candidate);
|
|
8819
|
-
return name === "" || name === "." || name === ".." ? null : candidate;
|
|
8820
|
-
}
|
|
8821
|
-
async function resolveNearestExistingAncestor(directory) {
|
|
8822
|
-
const missingSegments = [];
|
|
8823
|
-
let current = directory;
|
|
8824
|
-
for (; ; ) {
|
|
8825
|
-
try {
|
|
8826
|
-
return { existingAncestor: await realpath(current), missingSegments };
|
|
8827
|
-
} catch (err) {
|
|
8828
|
-
const parent = dirname9(current);
|
|
8829
|
-
if (err.code !== "ENOENT" || parent === current) {
|
|
8830
|
-
throw err;
|
|
8831
|
-
}
|
|
8832
|
-
missingSegments.unshift(basename2(current));
|
|
8833
|
-
current = parent;
|
|
8834
|
-
}
|
|
8835
|
-
}
|
|
8836
|
-
}
|
|
8837
9034
|
async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
|
|
8838
9035
|
for (const directory of allowedDirectories) {
|
|
8839
|
-
if (!
|
|
9036
|
+
if (!isAbsolute4(directory)) {
|
|
8840
9037
|
log("warn", "file_push_allowed_directory_skipped", { directory, reason: "not_absolute" });
|
|
8841
9038
|
continue;
|
|
8842
9039
|
}
|
|
8843
9040
|
const realDirectory = await realpathCreatingIfMissing(directory);
|
|
8844
|
-
if (realDirectory !== null &&
|
|
9041
|
+
if (realDirectory !== null && isPathContained(realDirectory, realTarget)) {
|
|
8845
9042
|
return realDirectory;
|
|
8846
9043
|
}
|
|
8847
9044
|
}
|
|
@@ -8849,15 +9046,15 @@ async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
|
|
|
8849
9046
|
}
|
|
8850
9047
|
async function findMatchingAllowedFile(allowedFiles, realTarget) {
|
|
8851
9048
|
for (const entry of allowedFiles) {
|
|
8852
|
-
if (!
|
|
9049
|
+
if (!isAbsolute4(entry)) {
|
|
8853
9050
|
log("warn", "file_push_allowed_directory_skipped", {
|
|
8854
9051
|
directory: entry,
|
|
8855
9052
|
reason: "not_absolute_file"
|
|
8856
9053
|
});
|
|
8857
9054
|
continue;
|
|
8858
9055
|
}
|
|
8859
|
-
const realParent = await realpathCreatingIfMissing(
|
|
8860
|
-
if (realParent !== null &&
|
|
9056
|
+
const realParent = await realpathCreatingIfMissing(dirname10(entry));
|
|
9057
|
+
if (realParent !== null && join13(realParent, basename3(entry)) === realTarget) {
|
|
8861
9058
|
return realParent;
|
|
8862
9059
|
}
|
|
8863
9060
|
}
|
|
@@ -8865,7 +9062,7 @@ async function findMatchingAllowedFile(allowedFiles, realTarget) {
|
|
|
8865
9062
|
}
|
|
8866
9063
|
async function realpathCreatingIfMissing(directory) {
|
|
8867
9064
|
try {
|
|
8868
|
-
return await
|
|
9065
|
+
return await realpath3(directory);
|
|
8869
9066
|
} catch (err) {
|
|
8870
9067
|
if (err.code !== "ENOENT") {
|
|
8871
9068
|
log("warn", "file_push_allowed_directory_skipped", {
|
|
@@ -8879,7 +9076,7 @@ async function realpathCreatingIfMissing(directory) {
|
|
|
8879
9076
|
try {
|
|
8880
9077
|
await mkdir(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
8881
9078
|
await chmod(directory, DIRECTORY_MODE);
|
|
8882
|
-
return await
|
|
9079
|
+
return await realpath3(directory);
|
|
8883
9080
|
} catch (err) {
|
|
8884
9081
|
log("warn", "file_push_allowed_directory_skipped", {
|
|
8885
9082
|
directory,
|
|
@@ -8889,23 +9086,19 @@ async function realpathCreatingIfMissing(directory) {
|
|
|
8889
9086
|
return null;
|
|
8890
9087
|
}
|
|
8891
9088
|
}
|
|
8892
|
-
function contains(realDirectory, realTarget) {
|
|
8893
|
-
const rel = relative2(realDirectory, realTarget);
|
|
8894
|
-
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute2(rel);
|
|
8895
|
-
}
|
|
8896
9089
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
8897
9090
|
let current = existingAncestor;
|
|
8898
9091
|
for (const segment of missingSegments) {
|
|
8899
|
-
current =
|
|
9092
|
+
current = join13(current, segment);
|
|
8900
9093
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
8901
9094
|
await chmod(current, DIRECTORY_MODE);
|
|
8902
9095
|
}
|
|
8903
9096
|
}
|
|
8904
9097
|
async function writeAtomically(realTarget, content) {
|
|
8905
|
-
const temporaryPath =
|
|
9098
|
+
const temporaryPath = join13(dirname10(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
8906
9099
|
let handle;
|
|
8907
9100
|
try {
|
|
8908
|
-
handle = await
|
|
9101
|
+
handle = await open4(temporaryPath, "wx", FILE_MODE);
|
|
8909
9102
|
await handle.writeFile(content);
|
|
8910
9103
|
await handle.chmod(FILE_MODE);
|
|
8911
9104
|
await handle.close();
|
|
@@ -8931,7 +9124,7 @@ async function discardTemporaryFile(temporaryPath, handle) {
|
|
|
8931
9124
|
}
|
|
8932
9125
|
}
|
|
8933
9126
|
}
|
|
8934
|
-
function
|
|
9127
|
+
function refuse2(code, message, fields) {
|
|
8935
9128
|
log(code === "write_failed" ? "error" : "warn", "file_push_refused", { code, ...fields });
|
|
8936
9129
|
return { ok: false, code, message };
|
|
8937
9130
|
}
|
|
@@ -9032,12 +9225,12 @@ var NOT_APPLIED = {
|
|
|
9032
9225
|
opencodeAuthApplied: false
|
|
9033
9226
|
};
|
|
9034
9227
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
9035
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
9036
|
-
return expanded ===
|
|
9228
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join14(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
9229
|
+
return expanded === join14(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
9037
9230
|
}
|
|
9038
9231
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
9039
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
9040
|
-
return expanded ===
|
|
9232
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join14(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
9233
|
+
return expanded === join14(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
9041
9234
|
}
|
|
9042
9235
|
async function applyOne(options, file) {
|
|
9043
9236
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -9853,7 +10046,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9853
10046
|
this.now = config.now ?? (() => Date.now());
|
|
9854
10047
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
9855
10048
|
this.fileSyncExactFiles = config.fileSyncExactFiles ?? [];
|
|
9856
|
-
this.homeDir = config.homeDir ??
|
|
10049
|
+
this.homeDir = config.homeDir ?? homedir7();
|
|
9857
10050
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
9858
10051
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
9859
10052
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
@@ -9883,9 +10076,6 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9883
10076
|
async isSessionOngoing(sessionId) {
|
|
9884
10077
|
return this.isV2 ? isV2SessionOngoing(this.openCodeClient, sessionId) : isSessionOngoing(this.port, sessionId, this.openCodeClient);
|
|
9885
10078
|
}
|
|
9886
|
-
async getOpenCodeDirectory() {
|
|
9887
|
-
return this.isV2 ? getOpenCodeDirectoryV2(this.openCodeClient) : getOpenCodeDirectory(this.port, this.openCodeClient);
|
|
9888
|
-
}
|
|
9889
10079
|
async createOpenCodeSession(directory) {
|
|
9890
10080
|
return this.isV2 ? createV2Session(this.openCodeClient, directory) : createOpenCodeSession(this.port, directory, this.openCodeClient);
|
|
9891
10081
|
}
|
|
@@ -11393,7 +11583,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11393
11583
|
*/
|
|
11394
11584
|
async resolveOpenCodeDirectory() {
|
|
11395
11585
|
if (this.opencodeDirectory !== void 0) return this.opencodeDirectory;
|
|
11396
|
-
this.opencodeDirectory = await
|
|
11586
|
+
this.opencodeDirectory = await resolveOpenCodeDirectoryForVersion(
|
|
11587
|
+
this.openCodeClient.version,
|
|
11588
|
+
this.port,
|
|
11589
|
+
this.openCodeClient
|
|
11590
|
+
);
|
|
11397
11591
|
if (!this.opencodeDirectory) {
|
|
11398
11592
|
this.log({
|
|
11399
11593
|
level: "warn",
|
|
@@ -14241,14 +14435,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
14241
14435
|
const fetchChildWithoutBlocking = async (sessionId) => {
|
|
14242
14436
|
const pending = fetchChildMessages(sessionId);
|
|
14243
14437
|
let timer;
|
|
14244
|
-
const timeout = new Promise((
|
|
14438
|
+
const timeout = new Promise((resolve6) => {
|
|
14245
14439
|
timer = setTimeout(() => {
|
|
14246
14440
|
this.log({
|
|
14247
14441
|
level: "warn",
|
|
14248
14442
|
message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} was slow \u2014 omitting invocation telemetry without delaying completion`,
|
|
14249
14443
|
message_id: messageId
|
|
14250
14444
|
});
|
|
14251
|
-
|
|
14445
|
+
resolve6(null);
|
|
14252
14446
|
}, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
|
|
14253
14447
|
});
|
|
14254
14448
|
try {
|
|
@@ -15533,7 +15727,7 @@ function commandError2(result) {
|
|
|
15533
15727
|
return result.stderr.trim() || `process exited with code ${result.code ?? "null"}`;
|
|
15534
15728
|
}
|
|
15535
15729
|
var runCommand2 = (command, args, opts) => {
|
|
15536
|
-
return new Promise((
|
|
15730
|
+
return new Promise((resolve6) => {
|
|
15537
15731
|
let child;
|
|
15538
15732
|
let stdout = "";
|
|
15539
15733
|
let stderr = "";
|
|
@@ -15543,7 +15737,7 @@ var runCommand2 = (command, args, opts) => {
|
|
|
15543
15737
|
if (settled) return;
|
|
15544
15738
|
settled = true;
|
|
15545
15739
|
if (timer.handle) clearTimeout(timer.handle);
|
|
15546
|
-
|
|
15740
|
+
resolve6(result);
|
|
15547
15741
|
};
|
|
15548
15742
|
try {
|
|
15549
15743
|
child = spawn5(command, args, { env: opts.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -15799,7 +15993,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
15799
15993
|
// src/lib/opencode/config-overlay.ts
|
|
15800
15994
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
15801
15995
|
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
15802
|
-
import { isAbsolute as
|
|
15996
|
+
import { isAbsolute as isAbsolute5, join as join15, resolve as resolve5 } from "node:path";
|
|
15803
15997
|
function parseConfigContent(content) {
|
|
15804
15998
|
if (!content?.trim()) return {};
|
|
15805
15999
|
const parsed = JSON.parse(content);
|
|
@@ -15828,8 +16022,8 @@ function applyRunnerOpenCodeConfig({
|
|
|
15828
16022
|
log3("runner OpenCode config is not configured; using the baked project config", "debug");
|
|
15829
16023
|
return;
|
|
15830
16024
|
}
|
|
15831
|
-
const source =
|
|
15832
|
-
const target = isFile(
|
|
16025
|
+
const source = isAbsolute5(overlayPath) ? overlayPath : resolve5(cwd, overlayPath);
|
|
16026
|
+
const target = isFile(join15(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
15833
16027
|
if (!isFile(source)) {
|
|
15834
16028
|
log3(
|
|
15835
16029
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -15837,7 +16031,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
15837
16031
|
);
|
|
15838
16032
|
return;
|
|
15839
16033
|
}
|
|
15840
|
-
copyFileSync(source,
|
|
16034
|
+
copyFileSync(source, join15(cwd, target));
|
|
15841
16035
|
try {
|
|
15842
16036
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
15843
16037
|
stdio: "ignore"
|
|
@@ -15846,13 +16040,13 @@ function applyRunnerOpenCodeConfig({
|
|
|
15846
16040
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
15847
16041
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
15848
16042
|
}
|
|
15849
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
16043
|
+
log3(`Applied runner OpenCode config ${source} to ${join15(cwd, target)}`);
|
|
15850
16044
|
}
|
|
15851
16045
|
|
|
15852
16046
|
// src/lib/opencode/runtime-provenance.ts
|
|
15853
16047
|
import { readFileSync as readFileSync11, readdirSync } from "node:fs";
|
|
15854
|
-
import { homedir as
|
|
15855
|
-
import { join as
|
|
16048
|
+
import { homedir as homedir8 } from "node:os";
|
|
16049
|
+
import { join as join16 } from "node:path";
|
|
15856
16050
|
var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
15857
16051
|
var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
|
|
15858
16052
|
function errorKind(error2) {
|
|
@@ -15919,8 +16113,8 @@ function parseJsonc(value) {
|
|
|
15919
16113
|
}
|
|
15920
16114
|
return JSON.parse(output);
|
|
15921
16115
|
}
|
|
15922
|
-
function openCodeCacheRoot(env = process.env, homeDir =
|
|
15923
|
-
return
|
|
16116
|
+
function openCodeCacheRoot(env = process.env, homeDir = homedir8()) {
|
|
16117
|
+
return join16(env.XDG_CACHE_HOME ?? join16(homeDir, ".cache"), "opencode");
|
|
15924
16118
|
}
|
|
15925
16119
|
function readConfiguredPluginSpecs({
|
|
15926
16120
|
cwd = process.cwd(),
|
|
@@ -15929,10 +16123,10 @@ function readConfiguredPluginSpecs({
|
|
|
15929
16123
|
let config;
|
|
15930
16124
|
try {
|
|
15931
16125
|
try {
|
|
15932
|
-
config = parseJsonc(readFileSync11(
|
|
16126
|
+
config = parseJsonc(readFileSync11(join16(cwd, "opencode.jsonc"), "utf8"));
|
|
15933
16127
|
} catch (error2) {
|
|
15934
16128
|
if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
|
|
15935
|
-
config = parseJsonc(readFileSync11(
|
|
16129
|
+
config = parseJsonc(readFileSync11(join16(cwd, "opencode.json"), "utf8"));
|
|
15936
16130
|
}
|
|
15937
16131
|
} catch (error2) {
|
|
15938
16132
|
log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
|
|
@@ -15957,10 +16151,10 @@ function readConfiguredPluginSpecs({
|
|
|
15957
16151
|
function resolveInstalledPluginVersions({
|
|
15958
16152
|
cacheRoot,
|
|
15959
16153
|
env = process.env,
|
|
15960
|
-
homeDir =
|
|
16154
|
+
homeDir = homedir8(),
|
|
15961
16155
|
log: log3
|
|
15962
16156
|
}) {
|
|
15963
|
-
const packagesRoot =
|
|
16157
|
+
const packagesRoot = join16(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
|
|
15964
16158
|
let directories;
|
|
15965
16159
|
try {
|
|
15966
16160
|
directories = readdirSync(packagesRoot, { withFileTypes: true });
|
|
@@ -15982,7 +16176,7 @@ function resolveInstalledPluginVersions({
|
|
|
15982
16176
|
try {
|
|
15983
16177
|
const packageJson = JSON.parse(
|
|
15984
16178
|
readFileSync11(
|
|
15985
|
-
|
|
16179
|
+
join16(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
|
|
15986
16180
|
"utf8"
|
|
15987
16181
|
)
|
|
15988
16182
|
);
|
|
@@ -16017,14 +16211,14 @@ function errorMessage3(error2) {
|
|
|
16017
16211
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
16018
16212
|
}
|
|
16019
16213
|
function waitForSettlement(promise, timeoutMs) {
|
|
16020
|
-
return new Promise((
|
|
16214
|
+
return new Promise((resolve6) => {
|
|
16021
16215
|
let settled = false;
|
|
16022
16216
|
const timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
|
|
16023
16217
|
const finish = (value) => {
|
|
16024
16218
|
if (settled) return;
|
|
16025
16219
|
settled = true;
|
|
16026
16220
|
clearTimeout(timer);
|
|
16027
|
-
|
|
16221
|
+
resolve6(value);
|
|
16028
16222
|
};
|
|
16029
16223
|
promise.then(
|
|
16030
16224
|
() => finish(true),
|
|
@@ -16568,8 +16762,8 @@ function resolveFileSyncEntry(entry, homeDir, flagName, labels) {
|
|
|
16568
16762
|
if (trimmed === "") {
|
|
16569
16763
|
throw new Error(`${flagName} requires a ${labels.noun} path (got an empty value)`);
|
|
16570
16764
|
}
|
|
16571
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
16572
|
-
if (!
|
|
16765
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join17(homeDir, trimmed.slice(2)) : trimmed;
|
|
16766
|
+
if (!isAbsolute6(expanded)) {
|
|
16573
16767
|
throw new Error(`${flagName} requires an absolute ${labels.noun} path; got "${entry}"`);
|
|
16574
16768
|
}
|
|
16575
16769
|
const normalized = resolvePath2(expanded);
|
|
@@ -16741,7 +16935,7 @@ function logActivity(state, entry) {
|
|
|
16741
16935
|
}
|
|
16742
16936
|
function reportSessionDbRecovery(state) {
|
|
16743
16937
|
try {
|
|
16744
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
16938
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir9(), env: process.env });
|
|
16745
16939
|
for (const record of report.records) {
|
|
16746
16940
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
16747
16941
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -16822,7 +17016,7 @@ async function importOpenCodeV2CredentialsForState(state) {
|
|
|
16822
17016
|
}
|
|
16823
17017
|
const result = await importOpenCodeV2Credentials({
|
|
16824
17018
|
request: client.request,
|
|
16825
|
-
homeDir:
|
|
17019
|
+
homeDir: homedir9(),
|
|
16826
17020
|
directory,
|
|
16827
17021
|
guard: state.opencodeV2CredentialImportGuard,
|
|
16828
17022
|
log: (message, level = "info") => logActivity(state, { type: "info", level, message })
|
|
@@ -16997,7 +17191,7 @@ async function adoptCredentialGeneration(state, generation, adoption, activeSess
|
|
|
16997
17191
|
return;
|
|
16998
17192
|
}
|
|
16999
17193
|
if (attempts < CREDENTIAL_ADOPTION_PROBE_ATTEMPTS) {
|
|
17000
|
-
await new Promise((
|
|
17194
|
+
await new Promise((resolve6) => setTimeout(resolve6, CREDENTIAL_ADOPTION_PROBE_INTERVAL_MS));
|
|
17001
17195
|
}
|
|
17002
17196
|
}
|
|
17003
17197
|
logCredentialAdoptionActivity(
|
|
@@ -17198,7 +17392,7 @@ async function driveChannels(state, driver) {
|
|
|
17198
17392
|
);
|
|
17199
17393
|
}
|
|
17200
17394
|
}
|
|
17201
|
-
await new Promise((
|
|
17395
|
+
await new Promise((resolve6) => setTimeout(resolve6, CHANNEL_POLL_INTERVAL_MS));
|
|
17202
17396
|
const cycleMs = performance.now() - cycleStartedAtMs;
|
|
17203
17397
|
if (idleThisCycle) idleMs += cycleMs;
|
|
17204
17398
|
if (unreachableThisCycle) unreachableMs += cycleMs;
|
|
@@ -17243,7 +17437,7 @@ function shouldWarnForReclaimSkip(reason) {
|
|
|
17243
17437
|
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
17244
17438
|
}
|
|
17245
17439
|
function sessionDbPath() {
|
|
17246
|
-
return
|
|
17440
|
+
return join17(homedir9(), ".local", "share", "opencode", "opencode.db");
|
|
17247
17441
|
}
|
|
17248
17442
|
function logSessionCleanupActivity(state, phase, entry) {
|
|
17249
17443
|
logActivity(state, {
|
|
@@ -17340,7 +17534,7 @@ function logSessionArchiveSummary(state, phase, summary) {
|
|
|
17340
17534
|
}
|
|
17341
17535
|
}
|
|
17342
17536
|
async function drainSessionDeletionReports(state) {
|
|
17343
|
-
const path = sessionDeletionReportPath(
|
|
17537
|
+
const path = sessionDeletionReportPath(homedir9(), process.env);
|
|
17344
17538
|
let delivered = 0;
|
|
17345
17539
|
let failure = null;
|
|
17346
17540
|
try {
|
|
@@ -17580,7 +17774,7 @@ async function runSweep(state, driver, config) {
|
|
|
17580
17774
|
if (deletionRecords.length > 0) {
|
|
17581
17775
|
try {
|
|
17582
17776
|
const dropped = appendSessionDeletionRecords(
|
|
17583
|
-
sessionDeletionReportPath(
|
|
17777
|
+
sessionDeletionReportPath(homedir9(), process.env),
|
|
17584
17778
|
deletionRecords
|
|
17585
17779
|
);
|
|
17586
17780
|
if (dropped > 0) {
|
|
@@ -17698,7 +17892,7 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
17698
17892
|
for (const warning2 of config.warnings) {
|
|
17699
17893
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
17700
17894
|
}
|
|
17701
|
-
const dbBytes = statSessionDbBytes(
|
|
17895
|
+
const dbBytes = statSessionDbBytes(homedir9());
|
|
17702
17896
|
void (async () => {
|
|
17703
17897
|
const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
17704
17898
|
if (reclaimAvailability !== null) {
|
|
@@ -17797,7 +17991,7 @@ async function runSessionArchiveBatch(state, phase, sessions, budgetMs, signal)
|
|
|
17797
17991
|
return null;
|
|
17798
17992
|
}
|
|
17799
17993
|
const bindings = readSessionBindings(
|
|
17800
|
-
sessionBindingLedgerPath(
|
|
17994
|
+
sessionBindingLedgerPath(homedir9(), process.env),
|
|
17801
17995
|
(message) => logSessionArchiveActivity(state, phase, {
|
|
17802
17996
|
type: "info",
|
|
17803
17997
|
level: "warn",
|
|
@@ -17884,8 +18078,8 @@ async function runSessionArchiveShutdownFlush(state, budgetMs) {
|
|
|
17884
18078
|
let timeout;
|
|
17885
18079
|
const settled = await Promise.race([
|
|
17886
18080
|
previousRun.then(() => true),
|
|
17887
|
-
new Promise((
|
|
17888
|
-
timeout = setTimeout(() =>
|
|
18081
|
+
new Promise((resolve6) => {
|
|
18082
|
+
timeout = setTimeout(() => resolve6(false), budgetMs);
|
|
17889
18083
|
})
|
|
17890
18084
|
]);
|
|
17891
18085
|
if (timeout) clearTimeout(timeout);
|
|
@@ -17920,7 +18114,7 @@ async function runSessionArchiveShutdownFlush(state, budgetMs) {
|
|
|
17920
18114
|
function scheduleSessionArchive(state) {
|
|
17921
18115
|
const config = state.sessionArchiveConfig;
|
|
17922
18116
|
if (!config.enabled) return;
|
|
17923
|
-
state.sessionArchiveWatermarkPath = sessionArchiveWatermarkPath(
|
|
18117
|
+
state.sessionArchiveWatermarkPath = sessionArchiveWatermarkPath(homedir9(), process.env);
|
|
17924
18118
|
state.sessionArchiveWatermarks = readWatermarks(
|
|
17925
18119
|
state.sessionArchiveWatermarkPath,
|
|
17926
18120
|
(message) => logSessionArchiveActivity(state, "startup", {
|
|
@@ -18124,7 +18318,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
18124
18318
|
});
|
|
18125
18319
|
return;
|
|
18126
18320
|
}
|
|
18127
|
-
const { collect, stop } = createResourceUsageCollector(
|
|
18321
|
+
const { collect, stop } = createResourceUsageCollector(homedir9());
|
|
18128
18322
|
state.stopResourceUsageSampling = stop;
|
|
18129
18323
|
let consecutiveFailures = 0;
|
|
18130
18324
|
const tick = async () => {
|
|
@@ -18362,8 +18556,8 @@ async function beginGracefulShutdown(state, trigger) {
|
|
|
18362
18556
|
return true;
|
|
18363
18557
|
}
|
|
18364
18558
|
);
|
|
18365
|
-
const timedOut = new Promise((
|
|
18366
|
-
timer = setTimeout(() =>
|
|
18559
|
+
const timedOut = new Promise((resolve6) => {
|
|
18560
|
+
timer = setTimeout(() => resolve6(false), telemetryBudgetMs);
|
|
18367
18561
|
});
|
|
18368
18562
|
if (!await Promise.race([flushed, timedOut])) {
|
|
18369
18563
|
log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
|
|
@@ -18381,8 +18575,8 @@ async function run(options) {
|
|
|
18381
18575
|
let fileSyncExactFiles;
|
|
18382
18576
|
try {
|
|
18383
18577
|
logLevel = resolveLogLevel(options);
|
|
18384
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
18385
|
-
fileSyncExactFiles = resolveFileSyncExactFiles(options.enableFileSyncToExactFile,
|
|
18578
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir9());
|
|
18579
|
+
fileSyncExactFiles = resolveFileSyncExactFiles(options.enableFileSyncToExactFile, homedir9());
|
|
18386
18580
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
18387
18581
|
throw new Error(
|
|
18388
18582
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -18692,7 +18886,7 @@ async function run(options) {
|
|
|
18692
18886
|
state.credentialSync?.arm();
|
|
18693
18887
|
const dependencyInstallDiagnosticReporter = createDependencyInstallDiagnosticReporter({
|
|
18694
18888
|
context: {
|
|
18695
|
-
homeDir:
|
|
18889
|
+
homeDir: homedir9(),
|
|
18696
18890
|
cwd: process.cwd(),
|
|
18697
18891
|
microvmId: process.env.MICROVM_ID?.trim(),
|
|
18698
18892
|
euid: process.getuid?.(),
|
|
@@ -18715,7 +18909,7 @@ async function run(options) {
|
|
|
18715
18909
|
dependencyInstallDiagnosticReporter
|
|
18716
18910
|
);
|
|
18717
18911
|
const openCodeLogTail = tailOpenCodeLogFile(
|
|
18718
|
-
resolveOpenCodeLogPath(
|
|
18912
|
+
resolveOpenCodeLogPath(homedir9(), process.env),
|
|
18719
18913
|
openCodeActivityForwarder
|
|
18720
18914
|
);
|
|
18721
18915
|
state.stopOpenCodeLogTail = () => {
|
|
@@ -18973,7 +19167,7 @@ async function run(options) {
|
|
|
18973
19167
|
const provenance = checkSessionDbProvenance({
|
|
18974
19168
|
dbPath: sessionDbPath(),
|
|
18975
19169
|
currentVersion: state.opencodeVersion,
|
|
18976
|
-
homeDir:
|
|
19170
|
+
homeDir: homedir9(),
|
|
18977
19171
|
env: process.env
|
|
18978
19172
|
});
|
|
18979
19173
|
if (provenance.anomaly) {
|
|
@@ -19157,7 +19351,7 @@ async function run(options) {
|
|
|
19157
19351
|
}
|
|
19158
19352
|
const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
|
|
19159
19353
|
const recordBinding = state.sessionArchiveConfig.enabled ? (conversationId, sessionId) => appendSessionBinding(
|
|
19160
|
-
sessionBindingLedgerPath(
|
|
19354
|
+
sessionBindingLedgerPath(homedir9(), process.env),
|
|
19161
19355
|
{
|
|
19162
19356
|
conversation_id: conversationId,
|
|
19163
19357
|
opencode_session_id: sessionId,
|
|
@@ -19179,7 +19373,7 @@ async function run(options) {
|
|
|
19179
19373
|
// `file_sync_disabled` on the ack, not silently ignored.
|
|
19180
19374
|
fileSyncDirectories,
|
|
19181
19375
|
fileSyncExactFiles,
|
|
19182
|
-
homeDir:
|
|
19376
|
+
homeDir: homedir9(),
|
|
19183
19377
|
maxActiveSessions,
|
|
19184
19378
|
log: (entry) => (
|
|
19185
19379
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -19199,6 +19393,11 @@ async function run(options) {
|
|
|
19199
19393
|
getAuthHeader: () => state.authHeader,
|
|
19200
19394
|
port: state.port,
|
|
19201
19395
|
openCodePassword: state.opencodePassword,
|
|
19396
|
+
resolveProjectRoot: () => resolveOpenCodeDirectoryForVersion(
|
|
19397
|
+
state.opencodeApiVersion,
|
|
19398
|
+
state.port,
|
|
19399
|
+
state.opencodeClient
|
|
19400
|
+
),
|
|
19202
19401
|
isRunning: () => state.running,
|
|
19203
19402
|
events: {
|
|
19204
19403
|
onConnected: (agentId, isReconnect, transition) => {
|