@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 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((resolve5) => {
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
- resolve5();
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((resolve5) => setTimeout(resolve5, ms));
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((resolve5) => {
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
- resolve5(data.trim());
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
- resolve5(chunk.toString().trim());
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 homedir8 } from "node:os";
1184
- import { isAbsolute as isAbsolute4, join as join16, parse, resolve as resolvePath2 } from "node:path";
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.00837f1" : void 0) ?? process.env.npm_package_version ?? "unknown";
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 stat2 = lstatSync(current);
2051
- if (stat2.isSymbolicLink()) {
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(stat2) {
2103
- if (stat2.isDirectory()) return "d";
2104
- if (stat2.isFile()) return "f";
2105
- if (stat2.isSymbolicLink()) return "l";
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, stat2) {
2109
- const mode = (stat2.mode & 4095).toString(8).padStart(4, "0");
2110
- return `${name}:${statType(stat2)}:${stat2.uid}:${stat2.gid}:${mode}`;
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 stat2 = lstatSync(current);
2137
- entries.push(renderStat(name, stat2));
2138
- finalStat = stat2;
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((resolve5) => {
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
- resolve5(result);
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((resolve5) => setTimeout(resolve5, 1e3));
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((resolve5) => setTimeout(resolve5, 1e3));
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 timedFetch(input, init) {
4173
- return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
4174
+ function isRecord5(value) {
4175
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4174
4176
  }
4175
- function requestWithClient(port, client, path, init, options) {
4176
- return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
4177
+ function finiteNumber(value) {
4178
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
4177
4179
  }
4178
- function opencodeBase(port) {
4179
- return `http://127.0.0.1:${port}`;
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
- async function getOpenCodeDirectory(port, client) {
4182
- try {
4183
- const res = await requestWithClient(port, client, "/path");
4184
- if (!res.ok) return null;
4185
- const body = await res.json();
4186
- 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;
4187
- return dir && dir.trim() ? dir.trim() : null;
4188
- } catch (error2) {
4189
- console.error(
4190
- `[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
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 roleOf2(m) {
4196
- if (!m || typeof m !== "object") return void 0;
4197
- if (typeof m.role === "string") return m.role;
4198
- const infoRole = m.info?.role;
4199
- return typeof infoRole === "string" ? infoRole : void 0;
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 completedOf(m) {
4202
- if (!m || typeof m !== "object") return void 0;
4203
- return m.info?.time?.completed ?? m.time?.completed;
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 createdOf(m) {
4206
- if (!m || typeof m !== "object") return void 0;
4207
- return m.info?.time?.created ?? m.time?.created;
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 idOf2(m) {
4210
- if (!m || typeof m !== "object") return void 0;
4211
- if (typeof m.id === "string") return m.id;
4212
- const infoId = m.info?.id;
4213
- return typeof infoId === "string" ? infoId : void 0;
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 parentIdOf2(m) {
4216
- if (!m || typeof m !== "object") return void 0;
4217
- if (typeof m.parentID === "string") return m.parentID;
4218
- const infoParent = m.info?.parentID;
4219
- return typeof infoParent === "string" ? infoParent : void 0;
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 finishOf(m) {
4222
- if (!m || typeof m !== "object") return void 0;
4223
- if (typeof m.finish === "string") return m.finish;
4224
- const infoFinish = m.info?.finish;
4225
- return typeof infoFinish === "string" ? infoFinish : void 0;
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 errorOf(m) {
4228
- if (!m || typeof m !== "object") return void 0;
4229
- return m.info?.error ?? m.error;
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 isTerminalStopAnswer(m) {
4232
- return completedOf(m) != null && errorOf(m) == null && finishOf(m) === "stop";
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 isAssistantInFlight(m) {
4235
- if (completedOf(m) == null) return true;
4236
- if (errorOf(m) != null) return false;
4237
- return finishOf(m) === "tool-calls";
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
- async function getSessionMessages(port, sessionId, client) {
4240
- try {
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
- async function fetchSessionMessages(port, sessionId, client) {
4254
- const response = await requestWithClient(port, client, `/session/${sessionId}/message`);
4255
- if (!response.ok) return null;
4256
- const body = await response.json();
4257
- return Array.isArray(body) ? body : null;
4258
- }
4259
- async function pollSessionMessagesForRedrive(port, sessionId, client) {
4260
- try {
4261
- const response = await requestWithClient(
4262
- port,
4263
- client,
4264
- `/session/${sessionId}/message`,
4265
- void 0,
4266
- { allowStatuses: ALL_HTTP_STATUSES }
4267
- );
4268
- if (!response.ok) {
4269
- return { ok: false, status: response.status, body: await response.text(), malformed: false };
4270
- }
4271
- const body = await response.json();
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 isSessionActivelyGenerating(messages) {
4282
- if (!messages || messages.length === 0) return false;
4283
- const last = messages[messages.length - 1];
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
- var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
4288
- function sessionLastActivityMs(session) {
4289
- const candidates = [
4290
- session.time?.updated,
4291
- session.time?.created,
4292
- session.time_updated,
4293
- session.time_created,
4294
- session.updated,
4295
- session.created
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 c of candidates) {
4298
- if (typeof c === "number" && Number.isFinite(c)) return c;
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 sessionListPath(options) {
4303
- const params = new URLSearchParams();
4304
- if (options?.limit !== void 0) params.set("limit", String(options.limit));
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 listSessionPage(port, client, options) {
4309
- const path = sessionListPath(options);
4392
+ async function readJson(response) {
4310
4393
  try {
4311
- const res = await requestWithClient(
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
- if (options?.signal?.aborted) return null;
4326
- const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
4327
- console.error(`[listSessionPage] GET ${path} failed (port ${port}): ${errorName}`);
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 listSessions(port, client, options) {
4332
- if (client?.version === "v2") return listV2Sessions(client, options?.signal);
4333
- return listSessionPage(port, client, options);
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
- async function listAllSessions(port, client, signal) {
4336
- if (client?.version === "v2") {
4337
- const sessions2 = await listV2Sessions(client, signal);
4338
- return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
4409
+ var OpenCodeV2PromptAckError = class extends Error {
4410
+ constructor(message) {
4411
+ super(message);
4412
+ this.name = "OpenCodeV2PromptAckError";
4339
4413
  }
4340
- const sessions = await listSessionPage(port, client, {
4341
- limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS,
4342
- signal
4343
- });
4344
- if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
4345
- if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
4346
- return { sessions: null, reason: "session-enumeration-limit-bound" };
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
- var INTERRUPT_NETWORK_CODES = /* @__PURE__ */ new Set([
4351
- "ABORT_ERR",
4352
- "EAI_AGAIN",
4353
- "ECONNREFUSED",
4354
- "ECONNRESET",
4355
- "ENETUNREACH",
4356
- "ENOTFOUND",
4357
- "ETIMEDOUT",
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
- const name = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
4375
- return redactDiagnosticText(`request threw ${name}`).slice(0, 120);
4442
+ return data.id;
4376
4443
  }
4377
- async function interruptSession(port, id, client) {
4378
- if (client?.version === "v2") {
4379
- try {
4380
- await interruptV2Session(client, id);
4381
- return { kind: "confirmed_stopped" };
4382
- } catch (error2) {
4383
- return { kind: "not_confirmed", diagnostic: interruptFailureDiagnostic(error2) };
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
- const res = await requestWithClient(
4388
- port,
4389
- client,
4390
- `/session/${id}/abort`,
4391
- { method: "POST" },
4392
- { allowStatuses: ALL_HTTP_STATUSES }
4393
- );
4394
- if (res.status >= 200 && res.status < 300) {
4395
- return { kind: "confirmed_stopped", status: res.status };
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 { kind: "not_confirmed", status: res.status };
4492
+ return sessions;
4398
4493
  } catch (error2) {
4399
- return { kind: "threw", diagnostic: interruptFailureDiagnostic(error2) };
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 deleteSession(port, id, client) {
4403
- if (client?.version === "v2") return deleteV2Session(client, id);
4501
+ async function deleteV2Session(client, sessionId) {
4404
4502
  try {
4405
- const res = await requestWithClient(
4406
- port,
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 (res.status >= 200 && res.status < 300) {
4413
- return { ok: true, status: res.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 res.text();
4417
- return body ? { ok: false, status: res.status, body } : { ok: false, status: res.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
- `[deleteSession] DELETE /session/${id} response body read failed for HTTP ${res.status}: ${error2 instanceof Error ? error2.message : String(error2)}`
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: res.status };
4518
+ return { ok: false, status: response.status };
4423
4519
  }
4424
4520
  } catch (error2) {
4425
4521
  console.error(
4426
- `[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
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 sessionExists(port, id, client) {
4527
+ async function v2SessionExists(client, sessionId) {
4432
4528
  try {
4433
- const res = await requestWithClient(port, client, `/session/${id}`, void 0, {
4434
- allowStatuses: [404]
4435
- });
4436
- if (res.status >= 200 && res.status < 300) return true;
4437
- if (res.status === 404) return false;
4438
- return null;
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 getOpenCodeSession(port, id, client) {
4444
- try {
4445
- const response = await requestWithClient(port, client, `/session/${id}`);
4446
- const body = await response.json();
4447
- return body && typeof body === "object" && !Array.isArray(body) ? body : null;
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
- console.error(
4450
- `[getOpenCodeSession] GET /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
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 getSessionStatuses(port, client) {
4456
- if (client?.version === "v2") return null;
4590
+ async function getV2SessionMessages(client, sessionId) {
4591
+ const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
4457
4592
  try {
4458
- const res = await requestWithClient(port, client, "/session/status");
4459
- if (!res.ok) {
4460
- console.error(
4461
- `[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
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
- `[getSessionStatuses] GET /session/status failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
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 isSessionOngoing(port, id, client) {
4481
- if (client?.version === "v2") return isV2SessionOngoing(client, id);
4482
- const map = await getSessionStatuses(port, client);
4483
- if (map == null) return null;
4484
- const entry = map[id];
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
- `[getModelAttachmentCapability] V2 provider capabilities are unavailable; using text-only fallback (port ${port})`
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
- const res = await requestWithClient(port, client, "/config/providers");
4519
- if (!res.ok) {
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
- `[getModelAttachmentCapability] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
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 isPinnedProviderLoaded(port, model, client) {
4572
- if (client?.version === "v2") return null;
4573
- const { model: baseModel } = splitModelVariant(model);
4574
- if (!baseModel) return null;
4575
- const slash = baseModel.indexOf("/");
4576
- if (slash <= 0) return null;
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 res = await requestWithClient(port, client, "/config/providers");
4580
- if (!res.ok) {
4581
- console.error(
4582
- `[isPinnedProviderLoaded] GET /config/providers returned HTTP ${res.status} (port ${port})`
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
- `[isPinnedProviderLoaded] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
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 buildFileParts(attachments, capable) {
4618
- const outcomes = [];
4619
- const parts = [];
4620
- const capabilityUnknown = capable === null;
4621
- if (capable !== true) {
4622
- for (const a of attachments.inputs) {
4623
- outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: "skipped" });
4624
- }
4625
- return { parts, outcomes, capabilityUnknown };
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
- for (const a of attachments.inputs) {
4628
- let dataUrl = null;
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
- dataUrl = await attachments.fetchDataUrl(a.index);
4631
- } catch (err) {
4632
- console.error(
4633
- `[buildFileParts] attachment ${a.index} (${a.mime}) fetch threw \u2014 omitting: ${err instanceof Error ? err.message : String(err)}`
4634
- );
4635
- dataUrl = null;
4660
+ parsed = JSON.parse(value);
4661
+ } catch (error2) {
4662
+ void error2;
4663
+ return null;
4636
4664
  }
4637
- if (dataUrl !== null && typeof dataUrl === "object") {
4638
- outcomes.push({
4639
- index: a.index,
4640
- mime: a.mime,
4641
- filename: a.filename,
4642
- status: "failed",
4643
- reason: "needs_reauth"
4644
- });
4645
- continue;
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
- if (dataUrl == null) {
4648
- outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: "failed" });
4649
- continue;
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
- parts.push({
4652
- type: "file",
4653
- mime: a.mime,
4654
- url: dataUrl,
4655
- ...a.filename ? { filename: a.filename } : {}
4656
- });
4657
- outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: "sent" });
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
- function splitModelVariant(raw) {
4662
- const value = raw?.trim();
4663
- if (!value) return {};
4664
- const hashIndex = value.indexOf("#");
4665
- if (hashIndex === -1) return { model: value };
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 applyModelOptions(body, options) {
4671
- if (options?.agent) body.agent = options.agent;
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
- async function listOpenCodeQuestions(port, client) {
4733
+ function opencodeBase(port) {
4734
+ return `http://127.0.0.1:${port}`;
4735
+ }
4736
+ async function getOpenCodeDirectory(port, client) {
4685
4737
  try {
4686
- const response = await requestWithClient(port, client, "/question");
4687
- const body = await response.json();
4688
- return Array.isArray(body) ? body : null;
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
- `[listOpenCodeQuestions] GET /question failed: ${error2 instanceof Error ? error2.message : String(error2)}`
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 listOpenCodePermissions(port, client) {
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 response = await requestWithClient(port, client, "/permission");
4699
- const body = await response.json();
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
- `[listOpenCodePermissions] GET /permission failed: ${error2 instanceof Error ? error2.message : String(error2)}`
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 messageText(m) {
4709
- if (!m || !Array.isArray(m.parts)) return "";
4710
- return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
4711
- }
4712
- function findDispatchedUserMessageId(messages, content, excludeIds) {
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 sendPromptAsync(port, sessionId, content, options, attachments, client, callbacks) {
4726
- const before = await getSessionMessages(port, sessionId, client);
4727
- const knownUserIds = new Set(
4728
- (before ?? []).filter((m) => roleOf2(m) === "user").map((m) => idOf2(m)).filter((id) => typeof id === "string")
4729
- );
4730
- if (callbacks) {
4731
- try {
4732
- callbacks.onKnownUserIds(before === null ? null : knownUserIds);
4733
- } catch (error2) {
4734
- callbacks.onKnownUserIdsError(error2);
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
- const parts = [{ type: "text", text: content }];
4738
- let pendingOutcomes = null;
4739
- if (attachments && attachments.inputs.length > 0) {
4740
- const capable = await getModelAttachmentCapability(port, options?.model, client);
4741
- const {
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 findAssistantReplyAfter(messages, userMessageId) {
4787
- if (!messages || messages.length === 0) return null;
4788
- const byParent = messages.find(
4789
- (m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
4790
- );
4791
- if (byParent) return byParent;
4792
- const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
4793
- if (userIndex === -1) return null;
4794
- for (let i = userIndex + 1; i < messages.length; i++) {
4795
- if (roleOf2(messages[i]) === "assistant") return messages[i];
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 findLastAssistantReplyFor(messages, userMessageId) {
4800
- if (!messages || messages.length === 0) return null;
4801
- let lastCorrelated = null;
4802
- for (let i = messages.length - 1; i >= 0; i--) {
4803
- const m = messages[i];
4804
- if (roleOf2(m) !== "assistant" || parentIdOf2(m) !== userMessageId) continue;
4805
- lastCorrelated = m;
4806
- break;
4807
- }
4808
- if (lastCorrelated) {
4809
- if (errorOf(lastCorrelated) == null) return lastCorrelated;
4810
- for (let i = messages.length - 1; i >= 0; i--) {
4811
- const m = messages[i];
4812
- if (roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId && isTerminalStopAnswer(m)) {
4813
- return m;
4814
- }
4815
- }
4816
- return lastCorrelated;
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 assistantMessagesInTurnBlock(messages, userMessageId) {
4839
- const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
4840
- if (userIndex === -1) return [];
4841
- const assistants = [];
4842
- for (let i = userIndex + 1; i < messages.length; i++) {
4843
- const message = messages[i];
4844
- const role = roleOf2(message);
4845
- if (role === "user") break;
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
- return assistants;
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
- function collectSubagentSessions(messages, userMessageId) {
4851
- if (!messages || messages.length === 0) return [];
4852
- const byParent = messages.filter(
4853
- (message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
4854
- );
4855
- const assistants = byParent.length > 0 ? byParent : assistantMessagesInTurnBlock(messages, userMessageId);
4856
- const refs = [];
4857
- const seen = /* @__PURE__ */ new Set();
4858
- for (const message of assistants) {
4859
- const parts = Array.isArray(message.parts) ? message.parts : [];
4860
- for (const part of parts) {
4861
- if (!part || typeof part !== "object" || part.type !== "tool") continue;
4862
- const childSession = part.childSession;
4863
- if (childSession) {
4864
- if (seen.has(childSession.sessionId)) continue;
4865
- seen.add(childSession.sessionId);
4866
- refs.push(childSession);
4867
- continue;
4868
- }
4869
- if (part.tool !== "task") continue;
4870
- const state = part.state;
4871
- if (!state || typeof state !== "object") continue;
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
- return refs;
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 collectTaskCalls(messages, userMessageId) {
4897
- if (!messages || messages.length === 0) return [];
4898
- const calls = [];
4899
- for (const message of messages) {
4900
- if (roleOf2(message) !== "assistant" || parentIdOf2(message) !== userMessageId) continue;
4901
- for (const part of message.parts ?? []) {
4902
- if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
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
- return calls;
4921
- }
4922
- function attributeTaskCallUsage(messages, windows) {
4923
- const eligibleWindows = windows.filter(
4924
- (window) => window.timeStart !== null && Number.isFinite(window.timeStart)
4925
- );
4926
- const assignments = /* @__PURE__ */ new Map();
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 (matching.length === 0) {
4936
- unattributed.push(message);
4937
- continue;
4953
+ if (res.status >= 200 && res.status < 300) {
4954
+ return { kind: "confirmed_stopped", status: res.status };
4938
4955
  }
4939
- matching.sort((a, b) => a.timeStart - b.timeStart);
4940
- assignments.get(matching[0].callID)?.push(message);
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 sumAssistantUsage(messages) {
4951
- if (!messages || messages.length === 0) return null;
4952
- const hasTrailingError = errorOf(messages[messages.length - 1]) != null;
4953
- const hasTerminalStopAnswer = messages.some(isTerminalStopAnswer);
4954
- const selected = hasTrailingError && hasTerminalStopAnswer ? messages.filter((message) => errorOf(message) == null) : messages;
4955
- let sawAnyUsage = false;
4956
- let inputSum = 0;
4957
- let outputSum = 0;
4958
- let reasoningSum = 0;
4959
- let cacheReadSum = 0;
4960
- let cacheWriteSum = 0;
4961
- let costSum = 0;
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
- if (!sawAnyUsage) return null;
4994
- return {
4995
- usage_provider_id: providerId,
4996
- usage_model_id: modelId,
4997
- usage_tokens_input: inputSum,
4998
- usage_tokens_output: outputSum,
4999
- usage_tokens_reasoning: reasoningSum,
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
- } else {
5082
- rendered = "The agent run failed.";
5083
- }
5084
- const toolPart = Array.isArray(reply?.parts) ? reply.parts.find(
5085
- (part) => part && typeof part === "object" && part.type === "tool" && part.state?.status === "error"
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
- var CLAUDE_AUTH_EXPIRY_MESSAGES = Object.freeze([
5117
- "Claude Code credentials are unavailable or expired. Run `claude` to refresh them."
5118
- ]);
5119
- function extractReplyErrorShape(reply) {
5120
- const error2 = errorOf(reply);
5121
- if (error2 == null || typeof error2 !== "object") {
5122
- return { name: null, statusCode: null };
5123
- }
5124
- const e = error2;
5125
- const name = typeof e.name === "string" ? e.name.slice(0, 100) : null;
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 findLatestSubagentAuthOutcome(messages, sinceMs) {
5170
- if (!messages || messages.length === 0) return null;
5171
- for (let i = messages.length - 1; i >= 0; i--) {
5172
- const message = messages[i];
5173
- if (roleOf2(message) !== "assistant") continue;
5174
- const created = createdOf(message);
5175
- if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
5176
- const failure = classifyReplyAuthError(message);
5177
- if (failure) {
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 hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
5209
- if (!messages || messages.length === 0) return false;
5210
- const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
5211
- if (userIndex === -1) return false;
5212
- let hasLaterUser = false;
5213
- let hasStartedLaterUser = false;
5214
- for (let i = userIndex + 1; i < messages.length; i++) {
5215
- const message = messages[i];
5216
- if (roleOf2(message) !== "user") continue;
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
- return hasLaterUser && hasStartedLaterUser;
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
- `[hasAnyConfiguredProvider] V2 working directory was unavailable (port ${port})`
5027
+ `[getSessionStatuses] GET /session/status body was not a plain object (port ${port})`
5236
5028
  );
5237
5029
  return null;
5238
5030
  }
5239
- const query = `?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
5240
- const integrationPath = `/api/integration${query}`;
5241
- let integrationReady = null;
5242
- try {
5243
- const res = await client.request(integrationPath);
5244
- if (!res.ok) {
5245
- console.error(
5246
- `[hasAnyConfiguredProvider] GET ${integrationPath} returned HTTP ${res.status} (port ${port})`
5247
- );
5248
- } else {
5249
- const body = await res.json();
5250
- if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
5251
- console.error(
5252
- `[hasAnyConfiguredProvider] GET ${integrationPath} body had no integration data array (port ${port})`
5253
- );
5254
- } else {
5255
- let valid = true;
5256
- for (const integration of body.data) {
5257
- if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
5258
- console.error(
5259
- `[hasAnyConfiguredProvider] GET ${integrationPath} body contained an invalid integration (port ${port})`
5260
- );
5261
- valid = false;
5262
- break;
5263
- }
5264
- }
5265
- if (valid) {
5266
- integrationReady = body.data.some(
5267
- (integration) => integration.connections.length > 0
5268
- );
5269
- }
5270
- }
5271
- }
5272
- } catch (error2) {
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
- `[hasAnyConfiguredProvider] GET ${integrationPath} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
5080
+ `[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
5275
5081
  );
5082
+ return null;
5276
5083
  }
5277
- if (integrationReady === true) return true;
5278
- const providerPath = `/api/provider${query}`;
5279
- let providerReady = null;
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
- `[hasAnyConfiguredProvider] GET ${providerPath} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
5088
+ `[getModelAttachmentCapability] GET /config/providers body had no providers array (port ${port})`
5313
5089
  );
5090
+ return null;
5314
5091
  }
5315
- if (providerReady === true) return true;
5316
- if (integrationReady === null || providerReady === null) return null;
5317
- return false;
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
- `[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
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 (!body || typeof body !== "object" || Array.isArray(body)) {
5146
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
5329
5147
  console.error(
5330
- `[hasAnyConfiguredProvider] GET /config/providers body was not a plain object (port ${port})`
5148
+ `[isPinnedProviderLoaded] GET /config/providers body was not a plain object (port ${port})`
5331
5149
  );
5332
5150
  return null;
5333
5151
  }
5334
- const defaults2 = body.default;
5335
- if (!defaults2 || typeof defaults2 !== "object" || Array.isArray(defaults2)) {
5152
+ const providers = body.providers;
5153
+ if (!Array.isArray(providers)) {
5336
5154
  console.error(
5337
- `[hasAnyConfiguredProvider] GET /config/providers body had no \`default\` object (port ${port})`
5155
+ `[isPinnedProviderLoaded] GET /config/providers body had no providers array (port ${port})`
5338
5156
  );
5339
5157
  return null;
5340
5158
  }
5341
- return Object.keys(defaults2).length > 0;
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
- `[hasAnyConfiguredProvider] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
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 sessionErrorReason(error2) {
5350
- const record = typeof error2 === "object" && error2 !== null ? error2 : null;
5351
- const data = record?.data;
5352
- const dataRecord = typeof data === "object" && data !== null ? data : null;
5353
- 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";
5354
- const reason = rawReason.replace(/\s+/g, " ").trim().slice(0, 500);
5355
- return reason || "OpenCode reported a session error with no details";
5356
- }
5357
- function parseSessionErrorFrame(data) {
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 propertiesRecord = properties;
5375
- const sessionId = propertiesRecord.sessionID;
5376
- if (typeof sessionId !== "string" || sessionId.length === 0) return null;
5377
- return {
5378
- sessionId,
5379
- reason: sessionErrorReason(propertiesRecord.error)
5380
- };
5381
- }
5382
- async function readSessionErrorStream(port, options, client) {
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
- reader = response.body.getReader();
5397
- const decoder = new TextDecoder();
5398
- let buffer = "";
5399
- const processLine = (line) => {
5400
- const trimmed = line.trimEnd();
5401
- if (!trimmed.startsWith("data:")) return;
5402
- const event = parseSessionErrorFrame(trimmed.slice("data:".length).replace(/^ /, ""));
5403
- if (event) options.onSessionError(event);
5404
- };
5405
- while (true) {
5406
- const { done, value } = await reader.read();
5407
- if (done) return { reason: "ended" };
5408
- buffer += decoder.decode(value, { stream: true });
5409
- const lines = buffer.split("\n");
5410
- buffer = lines.pop() ?? "";
5411
- for (const line of lines) processLine(line);
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 disposeAndReloadProviderConfig(port, client) {
5424
- if (client?.version === "v2") return { outcome: "skipped_v2" };
5243
+ async function listOpenCodeQuestions(port, client) {
5425
5244
  try {
5426
- const res = await requestWithClient(
5427
- port,
5428
- client,
5429
- "/config",
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
- `[disposeAndReloadProviderConfig] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
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 probeOpenCodeRequestReadiness(port, client) {
5255
+ async function listOpenCodePermissions(port, client) {
5456
5256
  try {
5457
- const res = await requestWithClient(port, client, "/config", void 0, {
5458
- allowStatuses: ALL_HTTP_STATUSES
5459
- });
5460
- const body = await res.text();
5461
- return { ok: res.ok, status: res.status, bodyEmpty: body.trim().length === 0, detail: null };
5462
- } catch (err) {
5463
- const detail = err instanceof Error ? err.message : String(err);
5464
- console.error(`[probeOpenCodeRequestReadiness] GET /config failed (port ${port}): ${detail}`);
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
- // src/lib/opencode/session-v2.ts
5470
- var ALL_HTTP_STATUSES2 = Array.from({ length: 500 }, (_, index) => index + 100);
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 finiteNumber2(value) {
5475
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
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 adaptTime(value) {
5478
- if (!isRecord5(value)) return void 0;
5479
- const created = finiteNumber2(value.created);
5480
- const completed = finiteNumber2(value.completed);
5481
- if (created === void 0 && completed === void 0) return void 0;
5482
- return {
5483
- ...created !== void 0 ? { created } : {},
5484
- ...completed !== void 0 ? { completed } : {}
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 adaptTokens(value) {
5488
- if (!isRecord5(value)) return void 0;
5489
- const input = finiteNumber2(value.input);
5490
- const output = finiteNumber2(value.output);
5491
- const reasoning = finiteNumber2(value.reasoning);
5492
- const cache = isRecord5(value.cache) ? {
5493
- ...finiteNumber2(value.cache.read) !== void 0 ? { read: finiteNumber2(value.cache.read) } : {},
5494
- ...finiteNumber2(value.cache.write) !== void 0 ? { write: finiteNumber2(value.cache.write) } : {}
5495
- } : void 0;
5496
- if (input === void 0 && output === void 0 && reasoning === void 0 && !cache) {
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 adaptMessageInfo(value, role) {
5507
- const info = {
5508
- id: value.id,
5509
- role
5510
- };
5511
- const time = adaptTime(value.time);
5512
- if (time) info.time = time;
5513
- if (typeof value.finish === "string") info.finish = value.finish;
5514
- if ("error" in value) info.error = value.error;
5515
- if (typeof value.agent === "string") info.agent = value.agent;
5516
- if (isRecord5(value.model)) {
5517
- if (typeof value.model.id === "string") info.modelID = value.model.id;
5518
- if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
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
- if (typeof value.cost === "number" && Number.isFinite(value.cost)) info.cost = value.cost;
5521
- const tokens = adaptTokens(value.tokens);
5522
- if (tokens) info.tokens = tokens;
5523
- return info;
5443
+ return refs;
5524
5444
  }
5525
- function v2ChildSessionEvidence(content) {
5526
- const state = isRecord5(content.state) ? content.state : null;
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 adaptV2Message(value) {
5534
- if (!isRecord5(value) || typeof value.id !== "string" || typeof value.type !== "string") {
5535
- return null;
5536
- }
5537
- if (value.type === "user") {
5538
- if (typeof value.text !== "string") return null;
5539
- return {
5540
- info: adaptMessageInfo(value, "user"),
5541
- parts: [{ type: "text", text: value.text }]
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
- if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
5545
- const parts = [];
5546
- for (const content of value.content) {
5547
- if (!isRecord5(content) || typeof content.type !== "string") return null;
5548
- const toolPart = projectV2ToolContent(content);
5549
- if (toolPart) {
5550
- const childSession = v2ChildSessionEvidence(content);
5551
- parts.push(childSession ? { ...toolPart, childSession } : toolPart);
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
- if (content.type === "text") {
5555
- if (typeof content.text !== "string") return null;
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
- info: adaptMessageInfo(value, "assistant"),
5563
- parts
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 adaptFormTool(value) {
5567
- if (!isRecord5(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
5568
- return void 0;
5569
- }
5570
- return { messageID: value.messageID, callID: value.id };
5571
- }
5572
- function adaptV2FormWire(value) {
5573
- if (!isRecord5(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
5574
- return null;
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 value;
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
- id: form.id,
5604
- sessionID: form.sessionID,
5605
- questions,
5606
- ...tool ? { tool } : {},
5607
- raw: form
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 adaptV2FormList(value) {
5611
- if (!isRecord5(value) || !Array.isArray(value.data)) return null;
5612
- return value.data.map(adaptV2Form).filter((form) => form !== null);
5613
- }
5614
- function adaptPattern(value) {
5615
- if (typeof value === "string" && value.length > 0) return value;
5616
- if (Array.isArray(value) && value.every((pattern) => typeof pattern === "string")) {
5617
- return value;
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
- return void 0;
5578
+ if (correlated.length === 0) return null;
5579
+ return sumAssistantUsage(correlated);
5620
5580
  }
5621
- function adaptV2PermissionWire(value) {
5622
- if (!isRecord5(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
5623
- return null;
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 (typeof value.permission !== "string" && typeof value.action !== "string") return null;
5626
- return value;
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 adaptV2Permission(value) {
5629
- const permission = adaptV2PermissionWire(value);
5630
- if (!permission) return null;
5631
- const type = permission.permission ?? permission.action;
5632
- if (!type) return null;
5633
- const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
5634
- const time = isRecord5(permission.time) ? finiteNumber2(permission.time.created) !== void 0 ? { created: finiteNumber2(permission.time.created) } : void 0 : void 0;
5635
- return {
5636
- id: permission.id,
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 adaptV2PermissionList(value) {
5649
- if (!isRecord5(value) || !Array.isArray(value.data)) return null;
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 adaptV2Session(value) {
5653
- if (!isRecord5(value) || typeof value.id !== "string" || value.id.length === 0) return null;
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 adaptV2SessionList(value) {
5666
- if (!isRecord5(value) || !Array.isArray(value.data) || !isRecord5(value.cursor)) return null;
5667
- return {
5668
- data: value.data.map(adaptV2Session).filter((session) => session !== null),
5669
- cursor: value.cursor
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 adaptV2Location(value) {
5673
- const candidates = [
5674
- value,
5675
- isRecord5(value) ? value.data : void 0,
5676
- isRecord5(value) ? value.location : void 0
5677
- ];
5678
- for (const candidate of candidates) {
5679
- if (!isRecord5(candidate) || typeof candidate.directory !== "string") continue;
5680
- const directory = candidate.directory.trim();
5681
- if (directory) return directory;
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
- return null;
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 adaptV2Messages(value) {
5686
- if (!isRecord5(value) || !Array.isArray(value.data)) return [];
5687
- return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
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
- async function readJson(response) {
5690
- try {
5691
- return await response.json();
5692
- } catch (error2) {
5693
- throw new Error(
5694
- `OpenCode V2 response was not valid JSON: ${error2 instanceof Error ? error2.message : String(error2)}`
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
- async function readData(client, path, init) {
5699
- const response = await client.request(path, init);
5700
- const body = await readJson(response);
5701
- if (!isRecord5(body) || !("data" in body)) {
5702
- throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
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
- return body.data;
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
- var OpenCodeV2PromptAckError = class extends Error {
5707
- constructor(message) {
5708
- super(message);
5709
- this.name = "OpenCodeV2PromptAckError";
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
- var OpenCodeV2ModelSwitchError = class extends Error {
5713
- statusCode;
5714
- constructor(error2) {
5715
- super(error2 instanceof Error ? error2.message : String(error2));
5716
- this.name = "OpenCodeV2ModelSwitchError";
5717
- this.statusCode = error2 instanceof OpenCodeRequestError ? error2.statusCode : null;
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
- async function getOpenCodeDirectoryV2(client) {
5721
- try {
5722
- return adaptV2Location(await readJson(await client.request("/api/location")));
5723
- } catch (error2) {
5724
- console.error(
5725
- `[getOpenCodeDirectoryV2] GET /api/location failed: ${error2 instanceof Error ? error2.message : String(error2)}`
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
- async function createV2Session(client, directory) {
5731
- const data = await readData(client, "/api/session", {
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
- async function getV2Session(client, sessionId) {
5742
- const data = await readData(client, `/api/session/${encodeURIComponent(sessionId)}`);
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
- async function listV2SessionPage(client, cursor, signal) {
5748
- const operation = cursor ? "GET /api/session (next page)" : "GET /api/session";
5749
- const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
5750
- try {
5751
- const response = await client.request(path, signal ? { signal } : void 0, {
5752
- allowStatuses: ALL_HTTP_STATUSES2
5753
- });
5754
- if (!response.ok) {
5755
- console.error(
5756
- `[listV2SessionPage] ${operation} returned HTTP ${response.status} (port ${client.port})`
5757
- );
5758
- return null;
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
- async function listV2Sessions(client, signal) {
5769
- const sessions = [];
5770
- const seenCursors = /* @__PURE__ */ new Set();
5771
- let cursor;
5772
- let hasNextPage = true;
5773
- try {
5774
- while (hasNextPage) {
5775
- const page = await listV2SessionPage(client, cursor, signal);
5776
- if (!page) return null;
5777
- sessions.push(...page.data);
5778
- const next = page.cursor.next;
5779
- if (next === void 0 || next === null) {
5780
- hasNextPage = false;
5781
- continue;
5782
- }
5783
- if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
5784
- throw new Error("OpenCode V2 session list contained an invalid next cursor");
5785
- }
5786
- seenCursors.add(next);
5787
- cursor = next;
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 deleteV2Session(client, sessionId) {
5799
- try {
5800
- const response = await client.request(
5801
- `/api/session/${encodeURIComponent(sessionId)}`,
5802
- { method: "DELETE" },
5803
- { allowStatuses: ALL_HTTP_STATUSES2 }
5804
- );
5805
- if (response.status >= 200 && response.status < 300) {
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 body = await response.text();
5810
- return body ? { ok: false, status: response.status, body } : { ok: false, status: response.status };
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
- `[deleteV2Session] DELETE /api/session/${sessionId} response body read failed for HTTP ${response.status}: ${error2 instanceof Error ? error2.message : String(error2)}`
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
- } catch (error2) {
5818
- console.error(
5819
- `[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
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(`/api/session/${encodeURIComponent(sessionId)}/model`, {
5859
- method: "POST",
5860
- headers: { "Content-Type": "application/json" },
5861
- body: JSON.stringify({ model: modelReference })
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
- throw new OpenCodeV2ModelSwitchError(error2);
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 body = await readJson(await client.request("/api/session/active"));
5930
- if (!isRecord5(body) || !isRecord5(body.data)) return null;
5931
- return body.data;
5932
- } catch (error2) {
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
- `[getV2ActiveSessions] GET /api/session/active failed: ${error2 instanceof Error ? error2.message : String(error2)}`
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
- async function isV2SessionOngoing(client, sessionId) {
5940
- const activeSessions = await getV2ActiveSessions(client);
5941
- if (activeSessions === null) return null;
5942
- return Object.prototype.hasOwnProperty.call(activeSessions, sessionId);
5943
- }
5944
- function sessionErrorReason2(value) {
5945
- if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
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 adaptV2SessionErrorEvent(value) {
5954
- let parsed = value;
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
- const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
5966
- if (establishedShape) return establishedShape;
5919
+ parsed = JSON.parse(data);
5967
5920
  } catch (error2) {
5968
5921
  void error2;
5922
+ return null;
5969
5923
  }
5970
- const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord5);
5971
- for (const event of candidates) {
5972
- if (event.type !== "session.error") continue;
5973
- const properties = [event.properties, event.data, event].find(isRecord5);
5974
- if (!properties) continue;
5975
- const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
5976
- if (!sessionId) continue;
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
- return null;
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 readV2SessionErrorStream(client, options) {
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.request("/api/event", {
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 = adaptV2SessionErrorEvent(trimmed.slice("data:".length).replace(/^ /, ""));
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 (error2) {
5972
+ } catch (err) {
6012
5973
  if (options.signal.aborted) return { reason: "aborted" };
6013
5974
  return {
6014
5975
  reason: "unavailable",
6015
- detail: error2 instanceof Error ? error2.message : String(error2)
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((resolve5) => setTimeout(resolve5, milliseconds));
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((resolve5) => {
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
- resolve5(result);
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
- resolve5({
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((resolve5, reject) => {
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
- resolve5(result);
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((resolve5) => {
7823
- stream.resume = resolve5;
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((resolve5) => {
8113
+ bodyPromise = new Promise((resolve6) => {
7891
8114
  pushBody = (buf) => {
7892
8115
  chunks.push(buf);
7893
8116
  };
7894
8117
  endBody = () => {
7895
- resolve5(Buffer.concat(chunks));
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((resolve5, reject) => {
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
- { openCodePassword }
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
- resolve5({
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 homedir6 } from "node:os";
8961
+ import { homedir as homedir7 } from "node:os";
8734
8962
 
8735
8963
  // src/lib/runner-file-sync.ts
8736
- import { join as join13 } from "node:path";
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 open3, realpath, rename, unlink } from "node:fs/promises";
8741
- 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";
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 refuse("file_sync_disabled", "File sync is not enabled on this runner.", {
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 refuse(
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 = expandAndValidate(requestedPath, homeDir);
8991
+ const candidate = expandAndValidatePath(requestedPath, homeDir);
8764
8992
  if (candidate === null) {
8765
- return refuse("invalid_path", "The requested path is not a valid absolute file path.", {
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
- dirname9(candidate)
9000
+ dirname10(candidate)
8773
9001
  );
8774
- const realTarget = join12(existingAncestor, ...missingSegments, basename2(candidate));
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 refuse("path_not_allowed", "The runner does not allow writing to that location.", {
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 realpath(dirname9(realTarget));
8785
- if (realParent !== dirname9(realTarget) || !contains(allowedDirectory, realTarget)) {
8786
- return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
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 refuse("write_failed", `The runner could not write the file (${errno}).`, {
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 (!isAbsolute2(directory)) {
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 && contains(realDirectory, realTarget)) {
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 (!isAbsolute2(entry)) {
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(dirname9(entry));
8860
- if (realParent !== null && join12(realParent, basename2(entry)) === realTarget) {
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 realpath(directory);
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 realpath(directory);
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 = join12(current, segment);
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 = join12(dirname9(realTarget), `.evident-push-${randomUUID()}.tmp`);
9098
+ const temporaryPath = join13(dirname10(realTarget), `.evident-push-${randomUUID()}.tmp`);
8906
9099
  let handle;
8907
9100
  try {
8908
- handle = await open3(temporaryPath, "wx", FILE_MODE);
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 refuse(code, message, fields) {
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("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
9036
- return expanded === join13(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
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("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
9040
- return expanded === join13(homeDir, ...OPENCODE_AUTH_SEGMENTS);
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 ?? homedir6();
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 this.getOpenCodeDirectory();
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((resolve5) => {
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
- resolve5(null);
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((resolve5) => {
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
- resolve5(result);
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 isAbsolute3, join as join14, resolve as resolve4 } from "node:path";
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 = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
15832
- const target = isFile(join14(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
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, join14(cwd, target));
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 ${join14(cwd, target)}`);
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 homedir7 } from "node:os";
15855
- import { join as join15 } from "node:path";
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 = homedir7()) {
15923
- return join15(env.XDG_CACHE_HOME ?? join15(homeDir, ".cache"), "opencode");
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(join15(cwd, "opencode.jsonc"), "utf8"));
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(join15(cwd, "opencode.json"), "utf8"));
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 = homedir7(),
16154
+ homeDir = homedir8(),
15961
16155
  log: log3
15962
16156
  }) {
15963
- const packagesRoot = join15(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
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
- join15(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
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((resolve5) => {
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
- resolve5(value);
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("~/") ? join16(homeDir, trimmed.slice(2)) : trimmed;
16572
- if (!isAbsolute4(expanded)) {
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: homedir8(), env: process.env });
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: homedir8(),
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((resolve5) => setTimeout(resolve5, CREDENTIAL_ADOPTION_PROBE_INTERVAL_MS));
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((resolve5) => setTimeout(resolve5, CHANNEL_POLL_INTERVAL_MS));
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 join16(homedir8(), ".local", "share", "opencode", "opencode.db");
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(homedir8(), process.env);
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(homedir8(), process.env),
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(homedir8());
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(homedir8(), process.env),
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((resolve5) => {
17888
- timeout = setTimeout(() => resolve5(false), budgetMs);
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(homedir8(), process.env);
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(homedir8());
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((resolve5) => {
18366
- timer = setTimeout(() => resolve5(false), telemetryBudgetMs);
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, homedir8());
18385
- fileSyncExactFiles = resolveFileSyncExactFiles(options.enableFileSyncToExactFile, homedir8());
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: homedir8(),
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(homedir8(), process.env),
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: homedir8(),
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(homedir8(), process.env),
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: homedir8(),
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) => {