backeer-opencode 0.8.2 → 0.10.0

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.
@@ -1860,8 +1860,7 @@ import { homedir } from "node:os";
1860
1860
  import { join as join2 } from "node:path";
1861
1861
 
1862
1862
  // src/utilities.js
1863
- var MAX_TRACKED_ITEMS = 500;
1864
- var limit = (value, length = 1e3) => String(value ?? "").slice(0, length);
1863
+ var formatText = (value) => String(value ?? "");
1865
1864
  function enabled(value, fallback = true) {
1866
1865
  if (typeof value === "boolean") return value;
1867
1866
  if (typeof value !== "string") return fallback;
@@ -1875,10 +1874,9 @@ function stableHash(value) {
1875
1874
  }
1876
1875
  return (hash >>> 0).toString(16).padStart(8, "0");
1877
1876
  }
1878
- function rememberBounded(map, key, value) {
1877
+ function remember(map, key, value) {
1879
1878
  map.delete(key);
1880
1879
  map.set(key, value);
1881
- while (map.size > MAX_TRACKED_ITEMS) map.delete(map.keys().next().value);
1882
1880
  }
1883
1881
 
1884
1882
  // src/reporter.js
@@ -1905,10 +1903,10 @@ function createOpenCodeReporter(client, levelValue) {
1905
1903
  const body = {
1906
1904
  service: "backeer-opencode",
1907
1905
  level: kind === "warning" ? "warn" : kind === "applied" || kind === "success" ? "info" : kind,
1908
- message: limit(message, 500)
1906
+ message: formatText(message)
1909
1907
  };
1910
1908
  if (level === "debug" && detail) {
1911
- body.extra = { detail: limit(detail?.message ?? detail, 500) };
1909
+ body.extra = { detail: formatText(detail?.message ?? detail) };
1912
1910
  }
1913
1911
  setTimeout(() => {
1914
1912
  void Promise.resolve(method.call(client.app, { body })).catch(() => {
@@ -1927,7 +1925,6 @@ function createOpenCodeReporter(client, levelValue) {
1927
1925
  function rememberRecent(map, key, value) {
1928
1926
  map.delete(key);
1929
1927
  map.set(key, value);
1930
- while (map.size > 100) map.delete(map.keys().next().value);
1931
1928
  }
1932
1929
 
1933
1930
  // src/live-bridge-config.js
@@ -1941,14 +1938,14 @@ function readCredential(file) {
1941
1938
  try {
1942
1939
  descriptor = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
1943
1940
  const stat = fstatSync(descriptor);
1944
- if (!stat.isFile() || stat.size > 4096 || process.platform !== "win32" && ((stat.mode & 63) !== 0 || stat.uid !== process.getuid())) {
1941
+ if (!stat.isFile() || process.platform !== "win32" && ((stat.mode & 63) !== 0 || stat.uid !== process.getuid())) {
1945
1942
  throw new Error("invalid_file");
1946
1943
  }
1947
1944
  const token = readFileSync(descriptor, "utf8").trim();
1948
- if (!/^[A-Za-z0-9_-]{32,256}$/.test(token)) throw new Error("invalid_token");
1945
+ if (!/^[A-Za-z0-9_-]{32,}$/.test(token)) throw new Error("invalid_token");
1949
1946
  return token;
1950
1947
  } catch {
1951
- throw new Error("Live control credential must be an owner-only regular file containing a 32\u2013256 character token");
1948
+ throw new Error("Live control credential must be an owner-only regular file containing a token of at least 32 characters");
1952
1949
  } finally {
1953
1950
  if (descriptor !== void 0) closeSync(descriptor);
1954
1951
  }
@@ -2079,12 +2076,12 @@ function createDeliveryTracker() {
2079
2076
  return {
2080
2077
  begin(key) {
2081
2078
  if (states.has(key)) return false;
2082
- rememberBounded(states, key, "publishing");
2079
+ remember(states, key, "publishing");
2083
2080
  return true;
2084
2081
  },
2085
2082
  succeed(key) {
2086
2083
  if (states.get(key) === "publishing") {
2087
- rememberBounded(states, key, "published");
2084
+ remember(states, key, "published");
2088
2085
  }
2089
2086
  },
2090
2087
  fail(key) {
@@ -2101,7 +2098,7 @@ function createCompletionTracker() {
2101
2098
  let state = states.get(key);
2102
2099
  if (!state) {
2103
2100
  state = { complete: false, publishing: false, revision: 0 };
2104
- rememberBounded(states, key, state);
2101
+ remember(states, key, state);
2105
2102
  }
2106
2103
  return state;
2107
2104
  }
@@ -2137,8 +2134,89 @@ function createCompletionTracker() {
2137
2134
  };
2138
2135
  }
2139
2136
 
2137
+ // src/live-bridge-events.js
2138
+ var ID = /^[A-Za-z0-9_-]+$/;
2139
+ var validBridgeID = (value) => typeof value === "string" && ID.test(value);
2140
+ function createBridgeSanitizer(secrets = []) {
2141
+ const known = secrets.filter((value) => typeof value === "string" && value.length > 0);
2142
+ const assistants = /* @__PURE__ */ new Map();
2143
+ function text(value) {
2144
+ if (typeof value !== "string") return "";
2145
+ let result = value;
2146
+ for (const secret of known) result = result.split(secret).join("[REDACTED]");
2147
+ result = result.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-]*PRIVATE KEY-----|$)/g, "[REDACTED PRIVATE KEY]").replace(/\bBearer\s+[^\s"'<>]+/gi, "Bearer [REDACTED]").replace(/\b(?:sk|bgi)_[A-Za-z0-9_-]{12,}|\bsk-[A-Za-z0-9_-]{12,}/g, "[REDACTED]").replace(/((?:api[_-]?key|access[_-]?token|secret|password|authorization)\s*[=:]\s*)[^\s,;]+/gi, "$1[REDACTED]");
2148
+ return result;
2149
+ }
2150
+ function content(key, value) {
2151
+ return { [key]: text(value) };
2152
+ }
2153
+ function sanitize(event) {
2154
+ const p = event?.properties ?? event?.data ?? {};
2155
+ const sessionID = p.sessionID ?? p.info?.sessionID ?? p.part?.sessionID;
2156
+ if (!validBridgeID(sessionID) || sessionID === "global") return void 0;
2157
+ const base = { type: event.type, sessionID, data: {} };
2158
+ if (event.type === "message.updated") {
2159
+ const info = p.info;
2160
+ if (!validBridgeID(info?.id)) return void 0;
2161
+ if (info.role === "assistant") {
2162
+ assistants.set(info.id, sessionID);
2163
+ }
2164
+ return info.role === "assistant" ? { ...base, data: {
2165
+ messageID: info.id,
2166
+ completed: !!info.time?.completed,
2167
+ failed: !!info.error
2168
+ } } : void 0;
2169
+ }
2170
+ if (event.type === "message.part.updated") {
2171
+ const part = p.part;
2172
+ if (assistants.get(part?.messageID) !== sessionID) return void 0;
2173
+ if (part.type === "text") return { ...base, type: "assistant.text", data: {
2174
+ messageID: part.messageID,
2175
+ ...content("text", part.text),
2176
+ replace: true
2177
+ } };
2178
+ if (part.type !== "tool") return void 0;
2179
+ const state = part.state ?? {};
2180
+ return { ...base, type: "tool.status", data: {
2181
+ tool: text(part.tool, 80),
2182
+ status: text(state.status, 40),
2183
+ ...state.status === "completed" ? content("output", state.output) : {},
2184
+ ...state.status === "error" ? { failed: true } : {}
2185
+ } };
2186
+ }
2187
+ if (event.type === "session.text.ended") return { ...base, type: "assistant.text", data: {
2188
+ messageID: validBridgeID(p.assistantMessageID) ? p.assistantMessageID : void 0,
2189
+ ...content("text", p.text),
2190
+ replace: true
2191
+ } };
2192
+ if (["session.tool.called", "session.tool.success", "session.tool.failed"].includes(event.type)) {
2193
+ return { ...base, type: "tool.status", data: {
2194
+ status: event.type.split(".").at(-1),
2195
+ ...event.type === "session.tool.success" ? content(
2196
+ "output",
2197
+ (p.content ?? []).filter((item) => item?.type === "text").map((item) => item.text).join("\n")
2198
+ ) : {}
2199
+ } };
2200
+ }
2201
+ if (["permission.asked", "question.asked", "question.v2.asked"].includes(event.type)) {
2202
+ return { ...base, type: "approval.waiting", data: {
2203
+ kind: event.type.split(".")[0],
2204
+ ...event.type !== "permission.asked" && validBridgeID(p.id ?? p.requestID) ? { questionId: p.id ?? p.requestID } : {}
2205
+ } };
2206
+ }
2207
+ if (["permission.replied", "question.replied", "question.v2.replied", "question.rejected", "question.v2.rejected"].includes(event.type)) {
2208
+ return { ...base, type: "approval.settled", data: { kind: event.type.split(".")[0] } };
2209
+ }
2210
+ if (event.type === "session.status") return { ...base, data: {
2211
+ status: ["idle", "busy", "retry"].includes(p.status?.type) ? p.status.type : "unknown"
2212
+ } };
2213
+ if (["session.idle", "session.error", "session.deleted"].includes(event.type)) return base;
2214
+ return void 0;
2215
+ }
2216
+ return { sanitize, text, clear: () => assistants.clear() };
2217
+ }
2218
+
2140
2219
  // src/http.js
2141
- var RETRY_DELAYS_MS = [0, 500, 1500];
2142
2220
  var HttpError = class extends Error {
2143
2221
  constructor(message, status, responseBody = "") {
2144
2222
  super(message);
@@ -2169,16 +2247,16 @@ function sleep(milliseconds, signal2) {
2169
2247
  });
2170
2248
  }
2171
2249
  async function postJsonWithRetry(config, url, body, options = {}) {
2172
- const retryDelays = options.retryDelays ?? RETRY_DELAYS_MS;
2173
- for (let attempt = 0; attempt < retryDelays.length; attempt++) {
2174
- if (retryDelays[attempt] > 0) {
2175
- await sleep(retryDelays[attempt], options.signal);
2176
- }
2177
- if (options.signal?.aborted) throw new DOMException("Aborted", "AbortError");
2250
+ const retryDelays = options.retryDelays;
2251
+ const signal2 = options.signal ?? config.signal;
2252
+ for (let attempt = 0; retryDelays === void 0 || attempt < retryDelays.length; attempt++) {
2253
+ const retryDelay = retryDelays?.[attempt] ?? (attempt === 0 ? 0 : 3e3);
2254
+ if (retryDelay > 0) await (options.sleepImplementation ?? sleep)(retryDelay, signal2);
2255
+ if (signal2?.aborted) throw new DOMException("Aborted", "AbortError");
2178
2256
  const controller = new AbortController();
2179
2257
  const abort = () => controller.abort();
2180
2258
  const timeout = setTimeout(abort, options.timeoutMs ?? 5e3);
2181
- options.signal?.addEventListener("abort", abort, { once: true });
2259
+ signal2?.addEventListener("abort", abort, { once: true });
2182
2260
  try {
2183
2261
  const response = await fetch(url, {
2184
2262
  method: "POST",
@@ -2190,22 +2268,22 @@ async function postJsonWithRetry(config, url, body, options = {}) {
2190
2268
  body: JSON.stringify(body),
2191
2269
  signal: controller.signal
2192
2270
  });
2193
- const responseBody = limit(await response.text(), 1e3);
2271
+ const responseBody = await response.text();
2194
2272
  if (response.ok) return { status: response.status, responseBody };
2195
- if (!isRetryable(response.status) || attempt === retryDelays.length - 1) {
2273
+ if (!isRetryable(response.status) || retryDelays !== void 0 && attempt === retryDelays.length - 1) {
2196
2274
  throw new HttpError(
2197
2275
  `HTTP request failed with status ${response.status}`,
2198
2276
  response.status,
2199
- responseBody
2277
+ createBridgeSanitizer([config.token]).text(responseBody)
2200
2278
  );
2201
2279
  }
2202
2280
  } catch (error) {
2203
- if (options.signal?.aborted) throw new DOMException("Aborted", "AbortError");
2281
+ if (signal2?.aborted) throw new DOMException("Aborted", "AbortError");
2204
2282
  if (error instanceof HttpError) throw error;
2205
- if (attempt === retryDelays.length - 1) throw error;
2283
+ if (retryDelays !== void 0 && attempt === retryDelays.length - 1) throw error;
2206
2284
  } finally {
2207
2285
  clearTimeout(timeout);
2208
- options.signal?.removeEventListener("abort", abort);
2286
+ signal2?.removeEventListener("abort", abort);
2209
2287
  }
2210
2288
  }
2211
2289
  throw new Error("HTTP retry loop ended unexpectedly");
@@ -2228,19 +2306,19 @@ async function publishPermission(config, reporter, request, directory, identity,
2228
2306
  const permissionType = request.permission ?? request.action ?? "unknown";
2229
2307
  const resources = Array.isArray(request.patterns) ? request.patterns.join(", ") : Array.isArray(request.resources) ? request.resources.join(", ") : "";
2230
2308
  const payload = [
2231
- { key: "Permission", value: limit(permissionType) },
2232
- { key: "Session", value: limit(identity.sessionID) },
2233
- { key: "Request ID", value: limit(identity.requestID) },
2234
- { key: "Patterns", value: limit(resources) },
2235
- { key: "Working directory", value: limit(directory) },
2309
+ { key: "Permission", value: formatText(permissionType) },
2310
+ { key: "Session", value: formatText(identity.sessionID) },
2311
+ { key: "Request ID", value: formatText(identity.requestID) },
2312
+ { key: "Patterns", value: formatText(resources) },
2313
+ { key: "Working directory", value: formatText(directory) },
2236
2314
  { key: "Action protocol", value: ACTION_PROTOCOL },
2237
2315
  { key: "Available decisions", value: "once, always, reject" }
2238
2316
  ].filter((item) => item.value.length > 0);
2239
2317
  const result = await postJsonWithRetry(config, config.eventsEndpoint, {
2240
2318
  type: "application",
2241
2319
  severity: "warning",
2242
- title: `OpenCode permission required: ${limit(permissionType, 200)}`,
2243
- body: resources ? `OpenCode is waiting for approval to access: ${limit(resources, 500)}` : `OpenCode is waiting for approval for: ${limit(permissionType, 500)}`,
2320
+ title: `OpenCode permission required: ${formatText(permissionType)}`,
2321
+ body: resources ? `OpenCode is waiting for approval to access: ${formatText(resources)}` : `OpenCode is waiting for approval for: ${formatText(permissionType)}`,
2244
2322
  payload,
2245
2323
  externalId: identity.externalId,
2246
2324
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2263,8 +2341,8 @@ function questionIdentity(request) {
2263
2341
  function formatQuestionOptions(options) {
2264
2342
  if (!Array.isArray(options)) return "";
2265
2343
  return options.map((option) => {
2266
- const label = limit(option?.label, 200);
2267
- const description = limit(option?.description, 500);
2344
+ const label = formatText(option?.label);
2345
+ const description = formatText(option?.description);
2268
2346
  if (!label) return "";
2269
2347
  return description ? `${label} \u2014 ${description}` : label;
2270
2348
  }).filter(Boolean).join(" | ");
@@ -2277,7 +2355,7 @@ async function resolveSessionTitle(client, directory, sessionID, reporter) {
2277
2355
  query: { directory },
2278
2356
  throwOnError: true
2279
2357
  });
2280
- return limit((result?.data ?? result)?.title);
2358
+ return formatText((result?.data ?? result)?.title);
2281
2359
  } catch (error) {
2282
2360
  reporter.debug("Session title unavailable", error);
2283
2361
  return "";
@@ -2294,43 +2372,43 @@ async function publishQuestion(config, reporter, request, identity, client, dire
2294
2372
  );
2295
2373
  const payload = [
2296
2374
  { key: "OpenCode session", value: sessionTitle },
2297
- { key: "Session", value: limit(identity.sessionID) },
2298
- { key: "Request ID", value: limit(identity.requestID) },
2375
+ { key: "Session", value: formatText(identity.sessionID) },
2376
+ { key: "Request ID", value: formatText(identity.requestID) },
2299
2377
  { key: "Action protocol", value: QUESTION_ACTION_PROTOCOL },
2300
2378
  ...questions.flatMap((question, index) => {
2301
2379
  const suffix = questions.length > 1 ? ` ${index + 1}` : "";
2302
2380
  return [
2303
- { key: `Question title${suffix}`, value: limit(question?.header) },
2304
- { key: `Question${suffix}`, value: limit(question?.question) },
2381
+ { key: `Question title${suffix}`, value: formatText(question?.header) },
2382
+ { key: `Question${suffix}`, value: formatText(question?.question) },
2305
2383
  {
2306
2384
  key: `Options${suffix}`,
2307
- value: limit(formatQuestionOptions(question?.options))
2385
+ value: formatText(formatQuestionOptions(question?.options))
2308
2386
  }
2309
2387
  ];
2310
2388
  })
2311
2389
  ].filter((item) => item.value.length > 0);
2312
2390
  const firstQuestion = questions[0];
2313
2391
  const actionQuestions = questions.map((question) => ({
2314
- header: limit(question?.header, 200),
2315
- question: limit(question?.question, 2e3),
2316
- options: Array.isArray(question?.options) ? question.options.slice(0, 20).map((option) => ({
2317
- label: limit(option?.label, 200),
2318
- description: limit(option?.description, 500)
2392
+ header: formatText(question?.header),
2393
+ question: formatText(question?.question),
2394
+ options: Array.isArray(question?.options) ? question.options.map((option) => ({
2395
+ label: formatText(option?.label),
2396
+ description: formatText(option?.description)
2319
2397
  })).filter((option) => option.label) : [],
2320
2398
  multiple: question?.multiple === true,
2321
2399
  custom: question?.custom !== false
2322
2400
  }));
2323
2401
  const body = questions.map((question) => {
2324
- const header = limit(question?.header, 200);
2325
- const text = limit(question?.question, 500);
2326
- const options = limit(formatQuestionOptions(question?.options), 500);
2402
+ const header = formatText(question?.header);
2403
+ const text = formatText(question?.question);
2404
+ const options = formatText(formatQuestionOptions(question?.options));
2327
2405
  return [header, text, options ? `Options: ${options}` : ""].filter(Boolean).join("\n");
2328
2406
  }).join("\n\n");
2329
2407
  const result = await postJsonWithRetry(config, config.eventsEndpoint, {
2330
2408
  type: "application",
2331
2409
  severity: "info",
2332
- title: `OpenCode question: ${limit(firstQuestion?.header, 200) || "Question"}`,
2333
- body: limit(body, 2e3),
2410
+ title: `OpenCode question: ${formatText(firstQuestion?.header) || "Question"}`,
2411
+ body: formatText(body),
2334
2412
  payload,
2335
2413
  externalId: identity.externalId,
2336
2414
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2359,19 +2437,19 @@ async function publishTodoCompletion(config, reporter, request, eventID, client,
2359
2437
  request.sessionID,
2360
2438
  reporter
2361
2439
  );
2362
- const correlation = typeof eventID === "string" && eventID.length > 0 ? limit(eventID, 200) : stableHash(JSON.stringify(todos));
2440
+ const correlation = typeof eventID === "string" && eventID.length > 0 ? formatText(eventID) : stableHash(JSON.stringify(todos));
2363
2441
  const externalId = `opencode-todo:${request.sessionID}:${correlation}`;
2364
- const visibleTodos = todos.slice(0, 20);
2442
+ const visibleTodos = todos;
2365
2443
  const payload = [
2366
2444
  { key: "OpenCode session", value: sessionTitle },
2367
2445
  { key: "Completed tasks", value: String(todos.length) },
2368
2446
  ...visibleTodos.flatMap((todo, index) => [
2369
- { key: `Task ${index + 1}`, value: limit(todo?.content) },
2370
- { key: `Priority ${index + 1}`, value: limit(todo?.priority, 100) }
2447
+ { key: `Task ${index + 1}`, value: formatText(todo?.content) },
2448
+ { key: `Priority ${index + 1}`, value: formatText(todo?.priority) }
2371
2449
  ]),
2372
2450
  ...todos.length > visibleTodos.length ? [{ key: "Additional tasks", value: String(todos.length - visibleTodos.length) }] : []
2373
2451
  ].filter((item) => item.value.length > 0);
2374
- const summary = todos.slice(0, 5).map((todo) => limit(todo?.content, 200)).filter(Boolean).join(" \u2022 ");
2452
+ const summary = todos.map((todo) => formatText(todo?.content)).filter(Boolean).join(" \u2022 ");
2375
2453
  const location = sessionTitle ? ` in ${sessionTitle}` : "";
2376
2454
  const body = [
2377
2455
  `All ${todos.length} OpenCode tasks completed${location}.`,
@@ -2381,7 +2459,7 @@ async function publishTodoCompletion(config, reporter, request, eventID, client,
2381
2459
  type: "application",
2382
2460
  severity: "info",
2383
2461
  title: "OpenCode todo list completed",
2384
- body: limit(body, 2e3),
2462
+ body: formatText(body),
2385
2463
  payload,
2386
2464
  externalId,
2387
2465
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2408,7 +2486,7 @@ import { randomUUID } from "node:crypto";
2408
2486
  import { statSync } from "node:fs";
2409
2487
 
2410
2488
  // src/updates.js
2411
- var PLUGIN_VERSION = "0.8.2";
2489
+ var PLUGIN_VERSION = "0.10.0";
2412
2490
  var PACKAGE_NAME = "backeer-opencode";
2413
2491
  var NPM_REGISTRY_URL = "https://registry.npmjs.org";
2414
2492
  var CHECK_TIMEOUT_MS = 3e3;
@@ -2564,7 +2642,7 @@ async function probeSource(config, fetchImplementation = globalThis.fetch) {
2564
2642
  headers: { Authorization: `Bearer ${config.token}` },
2565
2643
  signal: controller.signal
2566
2644
  });
2567
- const responseBody = limit(await response.text(), 2e3);
2645
+ const responseBody = formatText(await response.text());
2568
2646
  if (!response.ok) {
2569
2647
  throw new HttpError(
2570
2648
  `Gateway source check failed with status ${response.status}`,
@@ -2611,7 +2689,7 @@ async function buildDoctorReport(config, { fetchImplementation = globalThis.fetc
2611
2689
  text: result?.capabilities?.apns === true ? "Gateway APNs delivery is enabled" : "Gateway APNs delivery is disabled"
2612
2690
  });
2613
2691
  } catch (error) {
2614
- const detail = error instanceof HttpError && error.status === 401 ? "Source authentication rejected (HTTP 401)" : error?.name === "AbortError" ? "Gateway source check timed out" : `Gateway source check failed: ${limit(error?.message || error, 300)}`;
2692
+ const detail = error instanceof HttpError && error.status === 401 ? "Source authentication rejected (HTTP 401)" : error?.name === "AbortError" ? "Gateway source check timed out" : `Gateway source check failed: ${formatText(error?.message || error)}`;
2615
2693
  checks.push({ ok: false, text: detail });
2616
2694
  }
2617
2695
  }
@@ -2648,7 +2726,6 @@ async function publishTestNotification(config, reporter) {
2648
2726
  }
2649
2727
 
2650
2728
  // src/feedback-session-store.js
2651
- var MAX_QUEUED_MESSAGES = 20;
2652
2729
  var FeedbackSessionStore = class {
2653
2730
  constructor({ send, reporter, flushDelayMs = 150 }) {
2654
2731
  this.send = send;
@@ -2666,10 +2743,9 @@ var FeedbackSessionStore = class {
2666
2743
  blockers: /* @__PURE__ */ new Set(),
2667
2744
  queue: [],
2668
2745
  queued: /* @__PURE__ */ new Set(),
2669
- queueOverflowReported: false,
2670
2746
  flushTimer: void 0
2671
2747
  };
2672
- rememberBounded(this.sessions, sessionID, state);
2748
+ remember(this.sessions, sessionID, state);
2673
2749
  }
2674
2750
  return state;
2675
2751
  }
@@ -2704,16 +2780,8 @@ var FeedbackSessionStore = class {
2704
2780
  const state = this.stateFor(sessionID);
2705
2781
  const key = `${sessionID}:${kind}:${message}`;
2706
2782
  if (state.queued.has(key)) return;
2707
- state.queue.push({ kind, message: limit(message, 300), key });
2783
+ state.queue.push({ kind, message: formatText(message), key });
2708
2784
  state.queued.add(key);
2709
- if (state.queue.length > MAX_QUEUED_MESSAGES) {
2710
- const removed = state.queue.shift();
2711
- state.queued.delete(removed.key);
2712
- if (!state.queueOverflowReported) {
2713
- state.queueOverflowReported = true;
2714
- this.reporter.warning("Conversation feedback queue full \u2014 oldest message discarded");
2715
- }
2716
- }
2717
2785
  this.scheduleFlush(sessionID, state);
2718
2786
  }
2719
2787
  setIdle(sessionID, idle) {
@@ -2826,7 +2894,7 @@ async function sendSessionMessage(client, sessionID, text) {
2826
2894
  }
2827
2895
  function formatBackeerFeedback(kind, message) {
2828
2896
  const symbol = FEEDBACK_SYMBOLS[kind] ?? FEEDBACK_SYMBOLS.info;
2829
- return `\u25A3 Backeer | ${symbol} ${limit(message, 300)}`;
2897
+ return `\u25A3 Backeer | ${symbol} ${formatText(message)}`;
2830
2898
  }
2831
2899
  function formatBackeerFeedbackBatch(items) {
2832
2900
  if (items.length === 1) {
@@ -2836,7 +2904,7 @@ function formatBackeerFeedbackBatch(items) {
2836
2904
  "\u25A3 Backeer",
2837
2905
  ...items.map((item) => {
2838
2906
  const symbol = FEEDBACK_SYMBOLS[item.kind] ?? FEEDBACK_SYMBOLS.info;
2839
- return `${symbol} ${limit(item.message, 300)}`;
2907
+ return `${symbol} ${formatText(item.message)}`;
2840
2908
  })
2841
2909
  ].join("\n");
2842
2910
  }
@@ -3084,17 +3152,17 @@ var URL_WITH_QUERY = /\bhttps?:\/\/[^\s?#]+(?:\?[^\s#]*)?(?:#[^\s]*)?/gi;
3084
3152
  var WINDOWS_PATH = /\b[A-Za-z]:\\(?:[^\\\s]+\\)+[^\s]*/g;
3085
3153
  var POSIX_PATH = /(^|\s)\/(?:[^/\s]+\/)+[^\s]*/g;
3086
3154
  function sanitizeErrorMessage(value) {
3087
- return limit(value, 2e3).replace(CONTROL_CHARACTERS, " ").replace(BEARER_TOKEN, "Bearer [redacted]").replace(SECRET_ASSIGNMENT, "$1=[redacted]").replace(URL_WITH_QUERY, (value2) => {
3155
+ return formatText(value).replace(CONTROL_CHARACTERS, " ").replace(BEARER_TOKEN, "Bearer [redacted]").replace(SECRET_ASSIGNMENT, "$1=[redacted]").replace(URL_WITH_QUERY, (value2) => {
3088
3156
  try {
3089
3157
  const url = new URL(value2);
3090
3158
  return `${url.origin}${url.pathname}`;
3091
3159
  } catch {
3092
3160
  return "[url redacted]";
3093
3161
  }
3094
- }).replace(WINDOWS_PATH, "[path redacted]").replace(POSIX_PATH, "$1[path redacted]").replace(/\s+/g, " ").trim().slice(0, 500);
3162
+ }).replace(WINDOWS_PATH, "[path redacted]").replace(POSIX_PATH, "$1[path redacted]").replace(/\s+/g, " ").trim();
3095
3163
  }
3096
3164
  function safeErrorDetails(error) {
3097
- const name = limit(error?.name || "OpenCodeError", 100);
3165
+ const name = formatText(error?.name || "OpenCodeError");
3098
3166
  const message = sanitizeErrorMessage(error?.data?.message ?? error?.message);
3099
3167
  const statusCode = Number.isInteger(error?.data?.statusCode) ? String(error.data.statusCode) : "";
3100
3168
  return { name, message, statusCode };
@@ -3107,7 +3175,7 @@ async function resolveSessionTitle2(client, directory, sessionID, reporter) {
3107
3175
  query: { directory },
3108
3176
  throwOnError: true
3109
3177
  });
3110
- return limit((result?.data ?? result)?.title);
3178
+ return formatText((result?.data ?? result)?.title);
3111
3179
  } catch (error) {
3112
3180
  reporter.debug("Session title unavailable", error);
3113
3181
  return "";
@@ -3214,7 +3282,7 @@ async function publishSessionError({
3214
3282
  // Backeer/iOS currently support info, warning and critical severities.
3215
3283
  severity: "critical",
3216
3284
  title: "OpenCode task failed",
3217
- body: limit(body, 1e3),
3285
+ body: formatText(body),
3218
3286
  payload: [
3219
3287
  { key: "OpenCode session", value: sessionTitle },
3220
3288
  { key: "Outcome", value: "Failed" },
@@ -3226,7 +3294,7 @@ async function publishSessionError({
3226
3294
  }, "Task failure sent");
3227
3295
  }
3228
3296
  async function publishOpenCodeUpdate({ config, reporter, version }) {
3229
- const safeVersion = limit(version, 100);
3297
+ const safeVersion = formatText(version);
3230
3298
  await publish(config, reporter, void 0, void 0, {
3231
3299
  severity: "info",
3232
3300
  title: "OpenCode update available",
@@ -3236,8 +3304,8 @@ async function publishOpenCodeUpdate({ config, reporter, version }) {
3236
3304
  }, "OpenCode update sent");
3237
3305
  }
3238
3306
  function errorCorrelation(event, sessionState) {
3239
- if (typeof event?.id === "string" && event.id) return limit(event.id, 200);
3240
- if (sessionState?.lastAssistant?.id) return limit(sessionState.lastAssistant.id, 200);
3307
+ if (typeof event?.id === "string" && event.id) return formatText(event.id);
3308
+ if (sessionState?.lastAssistant?.id) return formatText(sessionState.lastAssistant.id);
3241
3309
  const details = safeErrorDetails(event?.properties?.error);
3242
3310
  return stableHash(`${details.name}:${details.statusCode}:${details.message}`);
3243
3311
  }
@@ -3277,7 +3345,7 @@ function createLifecycleController({
3277
3345
  lastAssistant: void 0,
3278
3346
  completedMessageID: ""
3279
3347
  };
3280
- rememberBounded(sessions, sessionID, state);
3348
+ remember(sessions, sessionID, state);
3281
3349
  }
3282
3350
  return state;
3283
3351
  }
@@ -3312,7 +3380,7 @@ function createLifecycleController({
3312
3380
  client,
3313
3381
  directory,
3314
3382
  sessionID,
3315
- messageID: limit(assistant.id, 200),
3383
+ messageID: formatText(assistant.id),
3316
3384
  durationMs,
3317
3385
  conversation
3318
3386
  }),
@@ -3359,7 +3427,7 @@ function createLifecycleController({
3359
3427
  client,
3360
3428
  directory,
3361
3429
  sessionID,
3362
- correlation: limit(correlation, 200),
3430
+ correlation: formatText(correlation),
3363
3431
  attempt: status.attempt,
3364
3432
  next: status.next,
3365
3433
  conversation
@@ -3412,7 +3480,7 @@ function createLifecycleController({
3412
3480
  const version = event.properties?.version;
3413
3481
  if (typeof version !== "string" || !version.trim()) return;
3414
3482
  if (publishedUpdates.has(version)) return;
3415
- rememberBounded(publishedUpdates, version, true);
3483
+ remember(publishedUpdates, version, true);
3416
3484
  dispatch(
3417
3485
  "OpenCode update could not be sent",
3418
3486
  sendUpdate({ config, reporter, version: version.trim() })
@@ -3428,7 +3496,7 @@ function createLifecycleController({
3428
3496
 
3429
3497
  // src/live-bridge.js
3430
3498
  import { createServer } from "node:http";
3431
- import { randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
3499
+ import { randomUUID as randomUUID3, timingSafeEqual, createHash as createHash2 } from "node:crypto";
3432
3500
  import { resolve as resolve2 } from "node:path";
3433
3501
 
3434
3502
  // src/permission-command.js
@@ -3622,7 +3690,7 @@ async function openCommandStream(config, state, signal2, fetchImplementation) {
3622
3690
  signal: signal2
3623
3691
  });
3624
3692
  if (!response.ok) {
3625
- const responseBody = limit(await response.text(), 500);
3693
+ const responseBody = formatText(await response.text());
3626
3694
  throw new HttpError(
3627
3695
  `Backeer command stream failed with status ${response.status}`,
3628
3696
  response.status,
@@ -3654,7 +3722,7 @@ async function consumeConnection({
3654
3722
  }
3655
3723
  }
3656
3724
  function streamErrorState(error, lastLoggedError, reconnectDelay, reporter) {
3657
- const message = error instanceof HttpError ? `${error.message}: ${error.responseBody}` : limit(error, 500);
3725
+ const message = error instanceof HttpError ? `${error.message}: ${error.responseBody}` : formatText(error);
3658
3726
  if (message !== lastLoggedError) {
3659
3727
  const authenticationFailure = error instanceof HttpError && [401, 403].includes(error.status);
3660
3728
  reporter.error(
@@ -3664,7 +3732,7 @@ function streamErrorState(error, lastLoggedError, reconnectDelay, reporter) {
3664
3732
  }
3665
3733
  return {
3666
3734
  lastLoggedError: message,
3667
- reconnectDelay: error instanceof HttpError && [401, 403, 404].includes(error.status) ? 6e4 : reconnectDelay
3735
+ reconnectDelay: error instanceof HttpError && [401, 403, 404].includes(error.status) ? 3e3 : reconnectDelay
3668
3736
  };
3669
3737
  }
3670
3738
  async function runCommandStream(config, reporter, {
@@ -3677,7 +3745,7 @@ async function runCommandStream(config, reporter, {
3677
3745
  stableConnectionMs = 5e3
3678
3746
  }) {
3679
3747
  const state = { lastEventID: "", framesReceived: 0 };
3680
- let reconnectDelay = 1e3;
3748
+ let reconnectDelay = 3e3;
3681
3749
  let lastLoggedError = "";
3682
3750
  while (!signal2.aborted && hasPending()) {
3683
3751
  try {
@@ -3696,7 +3764,7 @@ async function runCommandStream(config, reporter, {
3696
3764
  now,
3697
3765
  stableConnectionMs,
3698
3766
  onStable: () => {
3699
- reconnectDelay = 1e3;
3767
+ reconnectDelay = 3e3;
3700
3768
  lastLoggedError = "";
3701
3769
  }
3702
3770
  });
@@ -3708,7 +3776,7 @@ async function runCommandStream(config, reporter, {
3708
3776
  }
3709
3777
  if (!signal2.aborted && hasPending()) {
3710
3778
  await sleepImplementation(reconnectDelay, signal2);
3711
- reconnectDelay = Math.min(reconnectDelay * 2, 6e4);
3779
+ reconnectDelay = 3e3;
3712
3780
  }
3713
3781
  }
3714
3782
  }
@@ -3796,7 +3864,7 @@ async function replyToQuestion(client, pending, response) {
3796
3864
  }
3797
3865
  async function postQuestionState(config, eventID, path, body = {}) {
3798
3866
  const url = new URL(`/v1/events/${encodeURIComponent(eventID)}/${path}`, config.origin);
3799
- await postJsonWithRetry(config, url, body, { retryDelays: [0, 500] });
3867
+ await postJsonWithRetry(config, url, body);
3800
3868
  }
3801
3869
  function createPermissionController({
3802
3870
  config,
@@ -3852,7 +3920,7 @@ function createPermissionController({
3852
3920
  async function handleCommand(command) {
3853
3921
  if (!command || typeof command !== "object") return;
3854
3922
  if (!["opencode_permission_reply", "opencode_question_reply"].includes(command.type)) return;
3855
- if (typeof command.id !== "string" || command.id.length > 200) return;
3923
+ if (typeof command.id !== "string") return;
3856
3924
  const previousAcknowledgement = completedCommands.get(command.id);
3857
3925
  if (previousAcknowledgement) {
3858
3926
  await acknowledgeCommand(config, command.id, previousAcknowledgement).catch((error) => reporter.warning("Decision receipt retry failed", error));
@@ -3902,7 +3970,7 @@ function createPermissionController({
3902
3970
  externalId: command.externalId,
3903
3971
  processedAt: (/* @__PURE__ */ new Date()).toISOString()
3904
3972
  };
3905
- rememberBounded(completedCommands, command.id, acknowledgement);
3973
+ remember(completedCommands, command.id, acknowledgement);
3906
3974
  if (isQuestion) {
3907
3975
  reportQuestionAcknowledgement(acknowledgement, pending, reporter, conversation);
3908
3976
  } else {
@@ -4009,8 +4077,8 @@ async function commonDirectory(directory) {
4009
4077
  return realpath(stdout.trim());
4010
4078
  }
4011
4079
  function approvedWorktreeSessions(value) {
4012
- if (!Array.isArray(value) || value.length > 16) return [];
4013
- return value.filter((item) => /^[A-Za-z0-9_-]{1,160}$/.test(item?.sessionID) && item.sessionID !== "global" && typeof item.directory === "string" && isAbsolute2(item.directory) && item.directory.length <= 4096);
4080
+ if (!Array.isArray(value)) return [];
4081
+ return value.filter((item) => /^[A-Za-z0-9_-]+$/.test(item?.sessionID) && item.sessionID !== "global" && typeof item.directory === "string" && isAbsolute2(item.directory));
4014
4082
  }
4015
4083
  function createSessionScope(directory, { resolveCommon = commonDirectory, approvedSessions = [] } = {}) {
4016
4084
  const observed = /* @__PURE__ */ new Map();
@@ -4021,7 +4089,6 @@ function createSessionScope(directory, { resolveCommon = commonDirectory, approv
4021
4089
  if (location === directory) {
4022
4090
  if (info.id) {
4023
4091
  observed.set(info.id, info.projectID);
4024
- if (observed.size > 256) observed.delete(observed.keys().next().value);
4025
4092
  }
4026
4093
  return true;
4027
4094
  }
@@ -4047,24 +4114,23 @@ function createV2LiveClient(ctx) {
4047
4114
  if (disposed) return info;
4048
4115
  await ownsSession(info);
4049
4116
  known.set(id, info);
4050
- if (known.size > 100) known.delete(known.keys().next().value);
4051
4117
  return info;
4052
4118
  }
4053
4119
  return {
4054
4120
  async list() {
4055
4121
  const refreshed = [];
4056
4122
  if (typeof ctx.session.list === "function") {
4057
- const result = await ctx.session.list({ directory: ctx.location.directory, limit: 100, order: "desc" });
4123
+ const result = await ctx.session.list({ directory: ctx.location.directory, order: "desc" });
4058
4124
  const values = Array.isArray(result) ? result : result?.data;
4059
4125
  if (!Array.isArray(values)) throw new Error("Invalid native session list");
4060
- for (const info of values.slice(0, 100)) {
4126
+ for (const info of values) {
4061
4127
  if (await ownsSession(info)) {
4062
4128
  refreshed.push(info);
4063
4129
  known.set(info.id, info);
4064
4130
  }
4065
4131
  }
4066
4132
  }
4067
- for (const id of [...known.keys()].slice(0, 100)) {
4133
+ for (const id of [...known.keys()]) {
4068
4134
  if (refreshed.some((info) => info.id === id)) continue;
4069
4135
  try {
4070
4136
  refreshed.push(await get(id));
@@ -4166,100 +4232,13 @@ function createLiveConnectionNotifier(config, reporter) {
4166
4232
  };
4167
4233
  }
4168
4234
 
4169
- // src/live-bridge-events.js
4170
- var ID = /^[A-Za-z0-9_-]{1,160}$/;
4171
- var validBridgeID = (value) => typeof value === "string" && ID.test(value);
4172
- function createBridgeSanitizer(secrets = []) {
4173
- const known = secrets.filter((value) => typeof value === "string" && value.length > 0);
4174
- const assistants = /* @__PURE__ */ new Map();
4175
- function clipped(value, max = 12e3) {
4176
- if (typeof value !== "string") return { value: "", truncated: false };
4177
- let result = value.slice(0, 64e3);
4178
- for (const secret of known) result = result.split(secret).join("[REDACTED]");
4179
- result = result.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-]*PRIVATE KEY-----|$)/g, "[REDACTED PRIVATE KEY]").replace(/\bBearer\s+[^\s"'<>]+/gi, "Bearer [REDACTED]").replace(/\b(?:sk|bgi)_[A-Za-z0-9_-]{12,}|\bsk-[A-Za-z0-9_-]{12,}/g, "[REDACTED]").replace(/((?:api[_-]?key|access[_-]?token|secret|password|authorization)\s*[=:]\s*)[^\s,;]+/gi, "$1[REDACTED]");
4180
- return { value: result.slice(0, max), truncated: value.length > 64e3 || result.length > max };
4181
- }
4182
- function text(value, max) {
4183
- return clipped(value, max).value;
4184
- }
4185
- function content(key, value) {
4186
- const result = clipped(value);
4187
- return { [key]: result.value, ...result.truncated ? { truncated: true } : {} };
4188
- }
4189
- function sanitize(event) {
4190
- const p = event?.properties ?? event?.data ?? {};
4191
- const sessionID = p.sessionID ?? p.info?.sessionID ?? p.part?.sessionID;
4192
- if (!validBridgeID(sessionID) || sessionID === "global") return void 0;
4193
- const base = { type: event.type, sessionID, data: {} };
4194
- if (event.type === "message.updated") {
4195
- const info = p.info;
4196
- if (!validBridgeID(info?.id)) return void 0;
4197
- if (info.role === "assistant") {
4198
- assistants.set(info.id, sessionID);
4199
- if (assistants.size > 256) assistants.delete(assistants.keys().next().value);
4200
- }
4201
- return info.role === "assistant" ? { ...base, data: {
4202
- messageID: info.id,
4203
- completed: !!info.time?.completed,
4204
- failed: !!info.error
4205
- } } : void 0;
4206
- }
4207
- if (event.type === "message.part.updated") {
4208
- const part = p.part;
4209
- if (assistants.get(part?.messageID) !== sessionID) return void 0;
4210
- if (part.type === "text") return { ...base, type: "assistant.text", data: {
4211
- messageID: part.messageID,
4212
- ...content("text", part.text),
4213
- replace: true
4214
- } };
4215
- if (part.type !== "tool") return void 0;
4216
- const state = part.state ?? {};
4217
- return { ...base, type: "tool.status", data: {
4218
- tool: text(part.tool, 80),
4219
- status: text(state.status, 40),
4220
- ...state.status === "completed" ? content("output", state.output) : {},
4221
- ...state.status === "error" ? { failed: true } : {}
4222
- } };
4223
- }
4224
- if (event.type === "session.text.ended") return { ...base, type: "assistant.text", data: {
4225
- messageID: validBridgeID(p.assistantMessageID) ? p.assistantMessageID : void 0,
4226
- ...content("text", p.text),
4227
- replace: true
4228
- } };
4229
- if (["session.tool.called", "session.tool.success", "session.tool.failed"].includes(event.type)) {
4230
- return { ...base, type: "tool.status", data: {
4231
- status: event.type.split(".").at(-1),
4232
- ...event.type === "session.tool.success" ? content(
4233
- "output",
4234
- (p.content ?? []).filter((item) => item?.type === "text").map((item) => item.text).join("\n")
4235
- ) : {}
4236
- } };
4237
- }
4238
- if (["permission.asked", "question.asked", "question.v2.asked"].includes(event.type)) {
4239
- return { ...base, type: "approval.waiting", data: {
4240
- kind: event.type.split(".")[0],
4241
- ...event.type !== "permission.asked" && validBridgeID(p.id ?? p.requestID) ? { questionId: p.id ?? p.requestID } : {}
4242
- } };
4243
- }
4244
- if (["permission.replied", "question.replied", "question.v2.replied", "question.rejected", "question.v2.rejected"].includes(event.type)) {
4245
- return { ...base, type: "approval.settled", data: { kind: event.type.split(".")[0] } };
4246
- }
4247
- if (event.type === "session.status") return { ...base, data: {
4248
- status: ["idle", "busy", "retry"].includes(p.status?.type) ? p.status.type : "unknown"
4249
- } };
4250
- if (["session.idle", "session.error", "session.deleted"].includes(event.type)) return base;
4251
- return void 0;
4252
- }
4253
- return { sanitize, text, clear: () => assistants.clear() };
4254
- }
4255
-
4256
4235
  // src/live-questions.js
4257
4236
  import { createHash } from "node:crypto";
4258
4237
  var invalid = (code = "invalid_question_answer", status = 422) => Object.assign(new Error(code), { code, status });
4259
4238
  function questionSnapshot(pending, text) {
4260
- if (!validBridgeID(pending?.id) || !validBridgeID(pending?.sessionID) || pending.sessionID === "global" || !Array.isArray(pending.questions) || !pending.questions.length || pending.questions.length > 16) throw invalid("unsupported_question");
4239
+ if (!validBridgeID(pending?.id) || !validBridgeID(pending?.sessionID) || pending.sessionID === "global" || !Array.isArray(pending.questions) || !pending.questions.length) throw invalid("unsupported_question");
4261
4240
  const questions = pending.questions.map((question, index) => {
4262
- if (typeof question.question !== "string" || !Array.isArray(question.options) || question.options.length > 32 || question.options.some((option) => typeof option?.label !== "string") || new Set(question.options.map((option) => option.label)).size !== question.options.length) {
4241
+ if (typeof question.question !== "string" || !Array.isArray(question.options) || question.options.some((option) => typeof option?.label !== "string") || new Set(question.options.map((option) => option.label)).size !== question.options.length) {
4263
4242
  throw invalid("unsupported_question");
4264
4243
  }
4265
4244
  return {
@@ -4282,14 +4261,13 @@ function questionSnapshot(pending, text) {
4282
4261
  ...pending.nativeRevision ? { nativeRevision: pending.nativeRevision } : {}
4283
4262
  })).digest("hex");
4284
4263
  const result = { questionId: pending.id, sessionID: pending.sessionID, revision, questions };
4285
- if (Buffer.byteLength(JSON.stringify(result)) > 3e4) throw invalid("question_too_large");
4286
4264
  return result;
4287
4265
  }
4288
4266
  function resolveQuestionAnswers(pending, answers) {
4289
4267
  if (!Array.isArray(answers) || answers.length !== pending.questions.length) throw invalid();
4290
4268
  const values = answers.map((answer, index) => {
4291
4269
  const question = pending.questions[index];
4292
- if (!answer || !Array.isArray(answer.choices) || answer.choices.length > 32 || Object.keys(answer).some((key) => !["choices", "text"].includes(key)) || new Set(answer.choices).size !== answer.choices.length || answer.choices.some((choice) => !Number.isInteger(choice) || choice < 0 || choice >= question.options.length) || answer.text !== void 0 && (typeof answer.text !== "string" || !answer.text.trim() || answer.text.length > 8e3)) throw invalid();
4270
+ if (!answer || !Array.isArray(answer.choices) || Object.keys(answer).some((key) => !["choices", "text"].includes(key)) || new Set(answer.choices).size !== answer.choices.length || answer.choices.some((choice) => !Number.isInteger(choice) || choice < 0 || choice >= question.options.length) || answer.text !== void 0 && (typeof answer.text !== "string" || !answer.text.trim())) throw invalid();
4293
4271
  if (answer.text !== void 0 && question.custom !== true) throw invalid("custom_not_allowed");
4294
4272
  return [
4295
4273
  ...answer.choices.map((choice) => question.options[choice].label),
@@ -4302,7 +4280,6 @@ function resolveQuestionAnswers(pending, answers) {
4302
4280
  }
4303
4281
 
4304
4282
  // src/live-bridge.js
4305
- var LIMITS = { events: 256, bytes: 2e6, clients: 8, commands: 1024, commandTTL: 864e5, body: 32768, requests: 16 };
4306
4283
  function failure(status, code) {
4307
4284
  return Object.assign(new Error(code), { status, code });
4308
4285
  }
@@ -4314,10 +4291,7 @@ function json(response, status, value) {
4314
4291
  }
4315
4292
  async function readBody(request) {
4316
4293
  const chunks = [];
4317
- let bytes = 0;
4318
4294
  for await (const chunk of request) {
4319
- bytes += chunk.length;
4320
- if (bytes > LIMITS.body) throw failure(413, "body_too_large");
4321
4295
  chunks.push(chunk);
4322
4296
  }
4323
4297
  try {
@@ -4342,45 +4316,44 @@ async function createLiveBridge({ config, client, directory, reporter }) {
4342
4316
  const sanitizer = createBridgeSanitizer([token, config.token]);
4343
4317
  const epoch = randomUUID3();
4344
4318
  const events = [], streams = /* @__PURE__ */ new Set(), commands = /* @__PURE__ */ new Map(), states = /* @__PURE__ */ new Map();
4345
- let sequence = 0, bytes = 0, active = 0, disposed = false, disposeTask;
4346
- let lookups = 0;
4347
- const lookupWaiters = /* @__PURE__ */ new Set();
4319
+ let sequence = 0, disposed = false, disposeTask;
4320
+ const lookupWaiters = /* @__PURE__ */ new Set(), lookups = /* @__PURE__ */ new Map();
4348
4321
  const cursor = () => `${epoch}:${sequence}`;
4349
- function lookup(id) {
4322
+ function lookup(id, signal2) {
4350
4323
  if (disposed) return Promise.reject(failure(503, "bridge_closed"));
4351
- if (lookups >= 16) return Promise.reject(failure(429, "session_lookup_busy"));
4352
- lookups++;
4324
+ let entry = lookups.get(id);
4325
+ if (!entry) {
4326
+ entry = { waiters: /* @__PURE__ */ new Set() };
4327
+ lookups.set(id, entry);
4328
+ const settled = (ok, value) => {
4329
+ lookups.delete(id);
4330
+ for (const finish of [...entry.waiters]) finish(ok, value);
4331
+ };
4332
+ Promise.resolve().then(() => api.get(id)).then((value) => settled(true, value), (error) => settled(false, error));
4333
+ }
4353
4334
  return new Promise((resolve3, reject) => {
4354
- let finished = false;
4355
- const finish = (complete, value) => {
4356
- if (finished) return;
4357
- finished = true;
4358
- clearTimeout(timer);
4335
+ const finish = (ok, value) => {
4336
+ if (!entry.waiters.delete(finish)) return;
4359
4337
  lookupWaiters.delete(cancel);
4360
- complete(value);
4338
+ signal2?.removeEventListener("abort", aborted);
4339
+ if (ok) resolve3(value);
4340
+ else reject(value);
4361
4341
  };
4362
- const cancel = () => finish(reject, failure(503, "bridge_closed"));
4363
- const timer = setTimeout(() => finish(reject, failure(504, "session_lookup_timed_out")), 1e3);
4342
+ const cancel = () => finish(false, failure(503, "bridge_closed"));
4343
+ const aborted = () => finish(false, failure(499, "request_cancelled"));
4344
+ entry.waiters.add(finish);
4364
4345
  lookupWaiters.add(cancel);
4365
- Promise.resolve().then(() => api.get(id)).then(
4366
- (value) => {
4367
- lookups--;
4368
- finish(resolve3, value);
4369
- },
4370
- (error) => {
4371
- lookups--;
4372
- finish(reject, error);
4373
- }
4374
- );
4346
+ signal2?.addEventListener("abort", aborted, { once: true });
4347
+ if (signal2?.aborted) aborted();
4375
4348
  });
4376
4349
  }
4377
- async function session(id) {
4350
+ async function session(id, signal2) {
4378
4351
  if (!validBridgeID(id) || id === "global") throw failure(400, "invalid_session");
4379
4352
  let info;
4380
4353
  try {
4381
- info = await lookup(id);
4354
+ info = await lookup(id, signal2);
4382
4355
  } catch (error) {
4383
- if (["session_lookup_busy", "session_lookup_timed_out", "bridge_closed"].includes(error.code)) throw error;
4356
+ if (["bridge_closed", "request_cancelled"].includes(error.code)) throw error;
4384
4357
  throw failure(404, "session_not_found");
4385
4358
  }
4386
4359
  if (info?.id !== id || typeof info.time?.archived === "number") {
@@ -4402,18 +4375,12 @@ async function createLiveBridge({ config, client, directory, reporter }) {
4402
4375
  const removed = streams.delete(stream);
4403
4376
  clearInterval(stream.heartbeat);
4404
4377
  stream.queue.length = 0;
4405
- stream.queuedBytes = 0;
4406
4378
  if (removed && !streams.size && !disposed) connectionNotifications.update(false);
4407
4379
  }
4408
4380
  function write(stream, value) {
4409
4381
  if (!streams.has(stream)) return;
4410
4382
  if (stream.blocked) {
4411
4383
  stream.queue.push(value);
4412
- stream.queuedBytes += Buffer.byteLength(value);
4413
- if (stream.queuedBytes > LIMITS.bytes) {
4414
- removeStream(stream);
4415
- stream.response.destroy();
4416
- }
4417
4384
  return;
4418
4385
  }
4419
4386
  if (!stream.response.write(value)) {
@@ -4433,25 +4400,13 @@ async function createLiveBridge({ config, client, directory, reporter }) {
4433
4400
  if (safe.type === "session.status") states.set(safe.sessionID, safe.data.status);
4434
4401
  if (safe.type === "session.idle") states.set(safe.sessionID, "idle");
4435
4402
  if (safe.type === "session.deleted") states.delete(safe.sessionID);
4436
- if (states.size > 256) states.delete(states.keys().next().value);
4437
4403
  const record = { id: `${epoch}:${++sequence}`, ...safe, timestamp: Date.now() };
4438
- let frame = `id: ${record.id}
4439
- event: backeer.event
4440
- data: ${JSON.stringify(record)}
4441
-
4442
- `;
4443
- if (Buffer.byteLength(frame) > 32768) {
4444
- record.data = { truncated: true };
4445
- frame = `id: ${record.id}
4404
+ const frame = `id: ${record.id}
4446
4405
  event: backeer.event
4447
4406
  data: ${JSON.stringify(record)}
4448
4407
 
4449
4408
  `;
4450
- }
4451
- const size = Buffer.byteLength(frame);
4452
- events.push({ sequence, sessionID: record.sessionID, frame, size });
4453
- bytes += size;
4454
- while (events.length > LIMITS.events || bytes > LIMITS.bytes) bytes -= events.shift().size;
4409
+ events.push({ sequence, sessionID: record.sessionID, frame });
4455
4410
  for (const stream of streams) if (!stream.sessionID || stream.sessionID === record.sessionID) write(stream, frame);
4456
4411
  }
4457
4412
  function authorized(request) {
@@ -4462,7 +4417,7 @@ data: ${JSON.stringify(record)}
4462
4417
  }
4463
4418
  function parseCursor(value) {
4464
4419
  if (!value) return { after: 0 };
4465
- const match = /^([a-f0-9-]{36}):(\d{1,16})$/.exec(value);
4420
+ const match = /^([a-f0-9-]{36}):(\d+)$/.exec(value);
4466
4421
  if (!match) throw failure(400, "invalid_cursor");
4467
4422
  if (match[1] !== epoch) return { gap: "stream_restarted" };
4468
4423
  const after = Number(match[2]);
@@ -4471,9 +4426,6 @@ data: ${JSON.stringify(record)}
4471
4426
  return { after };
4472
4427
  }
4473
4428
  function cachedCommand(requestId, sessionID, signature) {
4474
- for (const [id, command] of commands) {
4475
- if (command.settled && Date.now() - command.created >= LIMITS.commandTTL) commands.delete(id);
4476
- }
4477
4429
  const existing = commands.get(requestId);
4478
4430
  if (existing && (existing.sessionID !== sessionID || existing.signature !== signature)) {
4479
4431
  throw failure(409, "request_id_conflict");
@@ -4483,7 +4435,6 @@ data: ${JSON.stringify(record)}
4483
4435
  async function execute(requestId, sessionID, signature, operation, questionId) {
4484
4436
  let command = cachedCommand(requestId, sessionID, signature);
4485
4437
  if (!command) {
4486
- if (commands.size >= LIMITS.commands) throw failure(429, "command_window_full");
4487
4438
  if (questionId && [...commands.values()].some((item) => item.sessionID === sessionID && item.questionId === questionId)) {
4488
4439
  throw failure(409, "question_reply_already_submitted");
4489
4440
  }
@@ -4491,8 +4442,6 @@ data: ${JSON.stringify(record)}
4491
4442
  sessionID,
4492
4443
  signature,
4493
4444
  questionId,
4494
- created: Date.now(),
4495
- settled: false,
4496
4445
  result: {
4497
4446
  requestId,
4498
4447
  sessionID,
@@ -4501,9 +4450,7 @@ data: ${JSON.stringify(record)}
4501
4450
  }
4502
4451
  };
4503
4452
  commands.set(requestId, command);
4504
- command.task = Promise.resolve().then(operation).finally(() => {
4505
- command.settled = true;
4506
- });
4453
+ command.task = Promise.resolve().then(operation).then(() => void 0);
4507
4454
  }
4508
4455
  await command.task;
4509
4456
  return command.result;
@@ -4513,7 +4460,6 @@ data: ${JSON.stringify(record)}
4513
4460
  const pending = await api.questions(sessionID);
4514
4461
  if (!Array.isArray(pending)) throw failure(502, "invalid_question_list");
4515
4462
  const own = pending.filter((item) => item.sessionID === sessionID);
4516
- if (own.length > 16) throw failure(413, "too_many_questions");
4517
4463
  return own;
4518
4464
  }
4519
4465
  async function route(request, response) {
@@ -4521,10 +4467,9 @@ data: ${JSON.stringify(record)}
4521
4467
  if (request.headers["x-backeer-project"] !== void 0 && request.headers["x-backeer-project"] !== encodeURIComponent(directory)) {
4522
4468
  return json(response, 409, { error: "project_mismatch" });
4523
4469
  }
4524
- if (active >= LIMITS.requests) return json(response, 429, { error: "bridge_busy" });
4525
- active++;
4526
- const timeout = setTimeout(() => json(response, 504, { error: "request_timed_out" }), 15e3);
4527
- timeout.unref();
4470
+ const controller = new AbortController();
4471
+ const closed = () => controller.abort();
4472
+ response.once("close", closed);
4528
4473
  try {
4529
4474
  const url = new URL(request.url, "http://127.0.0.1");
4530
4475
  if (url.username || url.password) throw failure(400, "invalid_url");
@@ -4533,7 +4478,7 @@ data: ${JSON.stringify(record)}
4533
4478
  const values = Array.isArray(result2) ? result2 : result2?.data;
4534
4479
  if (!Array.isArray(values)) throw failure(502, "invalid_session_list");
4535
4480
  const own = [];
4536
- for (const info of values.slice(0, 100)) {
4481
+ for (const info of values) {
4537
4482
  if (!validBridgeID(info?.id) || typeof info.time?.archived === "number") continue;
4538
4483
  if (api.ownsSession ? await api.ownsSession(info) : (info.location?.directory ?? info.directory) === directory) own.push(info);
4539
4484
  }
@@ -4562,11 +4507,9 @@ data: ${JSON.stringify(record)}
4562
4507
  }
4563
4508
  if (request.method === "GET" && url.pathname === "/v1/events") {
4564
4509
  const selected = url.searchParams.get("sessionID") || void 0;
4565
- if (selected) await session(selected);
4510
+ if (selected) await session(selected, controller.signal);
4566
4511
  const parsed = parseCursor(url.searchParams.get("after") ?? request.headers["last-event-id"]);
4567
4512
  if (response.headersSent) return;
4568
- if (streams.size >= LIMITS.clients) throw failure(429, "too_many_streams");
4569
- clearTimeout(timeout);
4570
4513
  response.writeHead(200, {
4571
4514
  "content-type": "text/event-stream",
4572
4515
  "cache-control": "no-cache, no-transform",
@@ -4585,8 +4528,7 @@ data: ${JSON.stringify({ reason: parsed.gap, cursor: cursor() })}
4585
4528
  sessionID: selected,
4586
4529
  heartbeat: void 0,
4587
4530
  blocked: false,
4588
- queue: [],
4589
- queuedBytes: 0
4531
+ queue: []
4590
4532
  };
4591
4533
  streams.add(stream);
4592
4534
  connectionNotifications.update(true);
@@ -4597,7 +4539,6 @@ data: ${JSON.stringify({ reason: parsed.gap, cursor: cursor() })}
4597
4539
  stream.blocked = false;
4598
4540
  while (streams.has(stream) && !stream.blocked && stream.queue.length) {
4599
4541
  const next = stream.queue.shift();
4600
- stream.queuedBytes -= Buffer.byteLength(next);
4601
4542
  write(stream, next);
4602
4543
  }
4603
4544
  });
@@ -4615,13 +4556,12 @@ retry: ${LIVE_RECONNECT_MS}
4615
4556
  }
4616
4557
  return;
4617
4558
  }
4618
- const match = /^\/v1\/sessions\/([A-Za-z0-9_-]{1,160})(\/instructions|\/questions(?:\/([A-Za-z0-9_-]{1,160})\/answers)?)?$/.exec(url.pathname);
4559
+ const match = /^\/v1\/sessions\/([A-Za-z0-9_-]+)(\/instructions|\/questions(?:\/([A-Za-z0-9_-]+)\/answers)?)?$/.exec(url.pathname);
4619
4560
  if (!match) throw failure(404, "route_not_found");
4620
- if (request.method === "GET" && !match[2]) return json(response, 200, await session(match[1]));
4561
+ if (request.method === "GET" && !match[2]) return json(response, 200, await session(match[1], controller.signal));
4621
4562
  if (request.method === "GET" && match[2] === "/questions") {
4622
- await session(match[1]);
4563
+ await session(match[1], controller.signal);
4623
4564
  const result2 = { questions: (await questions(match[1])).map((item) => questionSnapshot(item, sanitizer.text)) };
4624
- if (Buffer.byteLength(JSON.stringify(result2)) > 24e4) throw failure(413, "questions_too_large");
4625
4565
  return json(response, 200, result2);
4626
4566
  }
4627
4567
  if (request.method !== "POST" || !match[2]) throw failure(405, "method_not_allowed");
@@ -4629,8 +4569,8 @@ retry: ${LIVE_RECONNECT_MS}
4629
4569
  const body = await readBody(request);
4630
4570
  if (match[3]) {
4631
4571
  if (!body || !validBridgeID(body.requestId) || !/^[a-f0-9]{64}$/.test(body.revision) || Object.keys(body).some((key) => !["requestId", "revision", "answers"].includes(key))) throw failure(400, "invalid_question_answer");
4632
- await session(match[1]);
4633
- const signature2 = JSON.stringify({ questionId: match[3], revision: body.revision, answers: body.answers });
4572
+ await session(match[1], controller.signal);
4573
+ const signature2 = createHash2("sha256").update(JSON.stringify({ questionId: match[3], revision: body.revision, answers: body.answers })).digest("hex");
4634
4574
  const existing2 = cachedCommand(body.requestId, match[1], signature2);
4635
4575
  if (existing2) {
4636
4576
  await existing2.task;
@@ -4651,26 +4591,25 @@ retry: ${LIVE_RECONNECT_MS}
4651
4591
  ));
4652
4592
  }
4653
4593
  if (match[2] !== "/instructions") throw failure(405, "method_not_allowed");
4654
- if (!body || !validBridgeID(body.requestId) || typeof body.prompt !== "string" || !body.prompt.trim() || body.prompt.length > 16e3 || Object.keys(body).some((key) => !["requestId", "prompt"].includes(key))) throw failure(400, "invalid_instruction");
4655
- await session(match[1]);
4594
+ if (!body || !validBridgeID(body.requestId) || typeof body.prompt !== "string" || !body.prompt.trim() || Object.keys(body).some((key) => !["requestId", "prompt"].includes(key))) throw failure(400, "invalid_instruction");
4595
+ await session(match[1], controller.signal);
4656
4596
  if (response.headersSent || disposed) return;
4657
- const signature = JSON.stringify({ prompt: body.prompt });
4597
+ const signature = createHash2("sha256").update(JSON.stringify({ prompt: body.prompt })).digest("hex");
4658
4598
  const existing = cachedCommand(body.requestId, match[1], signature);
4659
4599
  const result = await execute(body.requestId, match[1], signature, () => api.prompt(match[1], body.prompt));
4660
4600
  return json(response, existing ? 200 : 202, result);
4661
4601
  } catch (error) {
4662
4602
  json(response, error.status ?? 502, { error: error.code ?? "opencode_request_failed" });
4663
4603
  } finally {
4664
- active--;
4665
- clearTimeout(timeout);
4604
+ response.removeListener("close", closed);
4666
4605
  }
4667
4606
  }
4668
4607
  const server = createServer((request, response) => {
4669
4608
  void route(request, response);
4670
4609
  });
4671
- server.requestTimeout = 15e3;
4672
- server.headersTimeout = 1e4;
4673
- server.keepAliveTimeout = 5e3;
4610
+ server.requestTimeout = 0;
4611
+ server.headersTimeout = 0;
4612
+ server.keepAliveTimeout = 0;
4674
4613
  await new Promise((resolve3, reject) => {
4675
4614
  server.once("error", reject);
4676
4615
  server.listen(options.port ?? 43871, "127.0.0.1", () => {
@@ -4698,6 +4637,7 @@ retry: ${LIVE_RECONNECT_MS}
4698
4637
  events.length = 0;
4699
4638
  states.clear();
4700
4639
  commands.clear();
4640
+ lookups.clear();
4701
4641
  sanitizer.clear();
4702
4642
  api.dispose?.();
4703
4643
  });
@@ -4719,6 +4659,8 @@ async function BackeerPlugin({ client, directory, feedback, v2 = false }, option
4719
4659
  bootstrapReporter.error("Plugin disabled \u2014 invalid configuration", error);
4720
4660
  return {};
4721
4661
  }
4662
+ const shutdown = new AbortController();
4663
+ config.signal = shutdown.signal;
4722
4664
  const reporter = createOpenCodeReporter(client, config.logLevel);
4723
4665
  const conversation = feedback ?? createConversationFeedback({ client, reporter });
4724
4666
  if (config.liveBridge.error) reporter.error("Live control disabled", config.liveBridge.error);
@@ -4878,6 +4820,7 @@ async function BackeerPlugin({ client, directory, feedback, v2 = false }, option
4878
4820
  await Promise.all([handleNotificationEvent(event), liveBridge.handle(event)]);
4879
4821
  },
4880
4822
  dispose: async () => {
4823
+ shutdown.abort();
4881
4824
  await liveBridge.dispose();
4882
4825
  await permissionController.dispose();
4883
4826
  lifecycleController.dispose();
@@ -5461,7 +5404,7 @@ async function getFormApi(ctx, sessionID, { makeClient = exports_client.make, ow
5461
5404
  }
5462
5405
 
5463
5406
  // src/opencode-v2-client.js
5464
- import { createHash as createHash2 } from "node:crypto";
5407
+ import { createHash as createHash3 } from "node:crypto";
5465
5408
  async function nativeReply(operation) {
5466
5409
  try {
5467
5410
  return await operation();
@@ -5487,7 +5430,7 @@ function questionProperties(form) {
5487
5430
  return {
5488
5431
  id: form.id,
5489
5432
  sessionID: form.sessionID,
5490
- nativeRevision: createHash2("sha256").update(JSON.stringify({
5433
+ nativeRevision: createHash3("sha256").update(JSON.stringify({
5491
5434
  id: form.id,
5492
5435
  sessionID: form.sessionID,
5493
5436
  title: form.title,