@hachej/boring-agent 0.1.42 → 0.1.44

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.
@@ -13,7 +13,7 @@ import {
13
13
  StopPayloadSchema,
14
14
  extractToolUiMetadata,
15
15
  sanitizeToolUiMetadata2 as sanitizeToolUiMetadata
16
- } from "../chunk-VQ7VSSSX.js";
16
+ } from "../chunk-HEEJSOFF.js";
17
17
 
18
18
  // src/server/sandbox/direct/createDirectSandbox.ts
19
19
  import { spawn } from "child_process";
@@ -305,10 +305,13 @@ function validateWorkspaceRoot(workspaceRoot) {
305
305
  }
306
306
  function buildBwrapArgs(workspaceRoot, options) {
307
307
  validateWorkspaceRoot(workspaceRoot);
308
+ const network = options?.network ?? "shared";
308
309
  const args = [
309
310
  "--unshare-all",
310
- "--share-net",
311
+ ...network === "shared" ? ["--share-net"] : [],
311
312
  "--die-with-parent",
313
+ ...options?.newSession === false ? [] : ["--new-session"],
314
+ ...options?.dropAllCapabilities ? ["--cap-drop", "ALL"] : [],
312
315
  "--tmpfs",
313
316
  "/",
314
317
  "--proc",
@@ -953,10 +956,45 @@ async function buildGlobalToolMounts(workspaceRoot) {
953
956
  }
954
957
  return args;
955
958
  }
959
+ function positiveInteger(value, name) {
960
+ if (value === void 0) return void 0;
961
+ if (!Number.isFinite(value) || value <= 0) {
962
+ throw new Error(`${name} must be a positive integer`);
963
+ }
964
+ return Math.floor(value);
965
+ }
966
+ function buildResourceLimitCommands(limits) {
967
+ if (!limits) return [];
968
+ const commands = [];
969
+ const cpuSeconds = positiveInteger(limits.cpuSeconds, "resourceLimits.cpuSeconds");
970
+ const fileSizeBlocks = positiveInteger(limits.fileSizeBlocks, "resourceLimits.fileSizeBlocks");
971
+ const maxProcesses = positiveInteger(limits.maxProcesses, "resourceLimits.maxProcesses");
972
+ const openFiles = positiveInteger(limits.openFiles, "resourceLimits.openFiles");
973
+ const virtualMemoryKb = positiveInteger(limits.virtualMemoryKb, "resourceLimits.virtualMemoryKb");
974
+ if (cpuSeconds !== void 0) commands.push(`ulimit -t ${cpuSeconds}`);
975
+ if (fileSizeBlocks !== void 0) commands.push(`ulimit -f ${fileSizeBlocks}`);
976
+ if (maxProcesses !== void 0) commands.push(`ulimit -u ${maxProcesses}`);
977
+ if (openFiles !== void 0) commands.push(`ulimit -n ${openFiles}`);
978
+ if (virtualMemoryKb !== void 0) commands.push(`ulimit -v ${virtualMemoryKb}`);
979
+ return commands;
980
+ }
981
+ function buildCommandArgs(cmd, limits) {
982
+ const limitCommands = buildResourceLimitCommands(limits);
983
+ if (limitCommands.length === 0) return ["bash", "-c", cmd];
984
+ return [
985
+ "bash",
986
+ "-c",
987
+ `${limitCommands.join("\n")}
988
+ exec bash -c "$1"`,
989
+ "boring-exec",
990
+ cmd
991
+ ];
992
+ }
956
993
  function createBwrapSandbox(opts = {}) {
994
+ const sandboxOptions = opts;
957
995
  let workspace = null;
958
- let hostWorkspaceRoot = opts.hostWorkspaceRoot;
959
- let runtimeContext = opts.runtimeContext ?? { runtimeCwd: SANDBOX_HOME2 };
996
+ let hostWorkspaceRoot = sandboxOptions.hostWorkspaceRoot;
997
+ let runtimeContext = sandboxOptions.runtimeContext ?? { runtimeCwd: SANDBOX_HOME2 };
960
998
  return {
961
999
  id: "bwrap",
962
1000
  placement: "server",
@@ -967,8 +1005,8 @@ function createBwrapSandbox(opts = {}) {
967
1005
  },
968
1006
  async init(ctx) {
969
1007
  workspace = ctx.workspace;
970
- hostWorkspaceRoot = opts.hostWorkspaceRoot ?? getNodeWorkspaceHostRoot(ctx.workspace) ?? ctx.workspace.root;
971
- runtimeContext = opts.runtimeContext ?? { runtimeCwd: SANDBOX_HOME2 };
1008
+ hostWorkspaceRoot = sandboxOptions.hostWorkspaceRoot ?? getNodeWorkspaceHostRoot(ctx.workspace) ?? ctx.workspace.root;
1009
+ runtimeContext = sandboxOptions.runtimeContext ?? { runtimeCwd: SANDBOX_HOME2 };
972
1010
  await assertBwrapAvailable();
973
1011
  },
974
1012
  async exec(cmd, opts2) {
@@ -981,12 +1019,14 @@ function createBwrapSandbox(opts = {}) {
981
1019
  const workspaceRoot = hostWorkspaceRoot ?? workspace.root;
982
1020
  const sandboxCwd = computeSandboxCwd(workspaceRoot, runtimeContext.runtimeCwd, opts2?.cwd);
983
1021
  const postWorkspaceArgs = await buildGlobalToolMounts(workspaceRoot);
984
- const baseArgs = buildBwrapArgs(workspaceRoot, { postWorkspaceArgs });
1022
+ const baseArgs = buildBwrapArgs(workspaceRoot, {
1023
+ postWorkspaceArgs,
1024
+ network: sandboxOptions.network,
1025
+ dropAllCapabilities: sandboxOptions.dropAllCapabilities
1026
+ });
985
1027
  const args = [
986
1028
  ...withSandboxCwd(baseArgs, sandboxCwd),
987
- "bash",
988
- "-c",
989
- cmd
1029
+ ...buildCommandArgs(cmd, sandboxOptions.resourceLimits)
990
1030
  ];
991
1031
  return await new Promise((resolve11, reject) => {
992
1032
  const child = spawn2("bwrap", args, {
@@ -1755,13 +1795,15 @@ async function bakeSnapshotIfNeeded(opts) {
1755
1795
  }
1756
1796
 
1757
1797
  // src/server/sandbox/snapshots/deploymentSnapshot.ts
1758
- var VERCEL_UV_BIN = "/home/vercel-sandbox/.local/bin/uv";
1798
+ var VERCEL_UV_BIN_DIR = "/workspace/.boring-agent/sdk/uv/bin";
1799
+ var VERCEL_UV_BIN = `${VERCEL_UV_BIN_DIR}/uv`;
1759
1800
  var UV_SETUP_COMMANDS = [
1760
1801
  "command -v uv >/dev/null 2>&1 || python3 -m pip install --upgrade uv",
1761
1802
  "uv --version"
1762
1803
  ];
1763
1804
  var NODE_UV_SETUP_COMMANDS = [
1764
- `[ -x ${VERCEL_UV_BIN} ] || curl -LsSf https://astral.sh/uv/install.sh | sh`,
1805
+ `mkdir -p ${VERCEL_UV_BIN_DIR}`,
1806
+ `[ -x ${VERCEL_UV_BIN} ] || curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=${VERCEL_UV_BIN_DIR} sh`,
1765
1807
  `${VERCEL_UV_BIN} --version`
1766
1808
  ];
1767
1809
  function isNodeFamilyRuntime(runtime) {
@@ -1817,6 +1859,492 @@ async function prepareVercelDeploymentSnapshot(opts) {
1817
1859
  });
1818
1860
  }
1819
1861
 
1862
+ // src/server/runtime/createServerFileSearch.ts
1863
+ var DEFAULT_LIMIT = 500;
1864
+ var MAX_LIMIT = 5e3;
1865
+ var SEARCH_TIMEOUT_MS = 5e3;
1866
+ var SEARCH_MAX_OUTPUT_BYTES = 256e3;
1867
+ function shellQuote2(value) {
1868
+ return `'${value.replaceAll("'", "'\\''")}'`;
1869
+ }
1870
+ function normalizeLimit(limit) {
1871
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
1872
+ return DEFAULT_LIMIT;
1873
+ }
1874
+ const normalized = Math.trunc(limit);
1875
+ if (normalized <= 0) return DEFAULT_LIMIT;
1876
+ return Math.min(normalized, MAX_LIMIT);
1877
+ }
1878
+ function buildFindArgs(glob) {
1879
+ const isPathShaped = glob.includes("/") || glob.includes("**");
1880
+ if (!isPathShaped) {
1881
+ return `-iname ${shellQuote2(glob)}`;
1882
+ }
1883
+ let translated = glob.replaceAll("**", "*");
1884
+ translated = translated.replace(/^\/+/, "");
1885
+ if (!translated.startsWith("*")) {
1886
+ translated = `*${translated}`;
1887
+ }
1888
+ return `-ipath ${shellQuote2(translated)}`;
1889
+ }
1890
+ function createServerFileSearch(workspace, sandbox) {
1891
+ return {
1892
+ async search(glob, limit = DEFAULT_LIMIT) {
1893
+ const safeLimit = normalizeLimit(limit);
1894
+ const command = [
1895
+ "find .",
1896
+ "-maxdepth 10",
1897
+ "\\( -path './.boring-agent' -o -path './.git' -o -path './node_modules' \\) -prune -o",
1898
+ buildFindArgs(glob),
1899
+ "-type f",
1900
+ "-print",
1901
+ `| head -n ${safeLimit}`
1902
+ ].join(" ");
1903
+ const { stdout, exitCode } = await sandbox.exec(command, {
1904
+ cwd: workspace.root,
1905
+ timeoutMs: SEARCH_TIMEOUT_MS,
1906
+ maxOutputBytes: SEARCH_MAX_OUTPUT_BYTES
1907
+ });
1908
+ if (exitCode !== 0) {
1909
+ throw new Error(`file-search failed: exit ${exitCode}`);
1910
+ }
1911
+ const decoded = new TextDecoder().decode(stdout);
1912
+ return decoded.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.length > 0).map((line) => line.replace(/^\.\//, ""));
1913
+ }
1914
+ };
1915
+ }
1916
+
1917
+ // src/server/sandbox/remote-worker/protocol.ts
1918
+ var REMOTE_WORKER_RUNTIME_CWD = "/workspace";
1919
+ var REMOTE_WORKER_PROVIDER = "remote-worker";
1920
+ var WORKER_INTERNAL_TOKEN_HEADER = "x-boring-internal-token";
1921
+ var WORKER_WORKSPACE_ID_HEADER = "x-boring-workspace-id";
1922
+ var WORKER_REQUEST_ID_HEADER = "x-boring-request-id";
1923
+
1924
+ // src/server/sandbox/remote-worker/createRemoteWorkerSandbox.ts
1925
+ function filteredEnv(env) {
1926
+ if (!env) return void 0;
1927
+ return Object.fromEntries(
1928
+ Object.entries(env).filter((entry) => typeof entry[1] === "string")
1929
+ );
1930
+ }
1931
+ function createRemoteWorkerSandbox(client) {
1932
+ return {
1933
+ id: REMOTE_WORKER_PROVIDER,
1934
+ placement: "remote",
1935
+ provider: REMOTE_WORKER_PROVIDER,
1936
+ capabilities: ["exec"],
1937
+ runtimeContext: { runtimeCwd: REMOTE_WORKER_RUNTIME_CWD },
1938
+ async init() {
1939
+ await client.health();
1940
+ },
1941
+ async exec(cmd, opts = {}) {
1942
+ const request = {
1943
+ cmd,
1944
+ cwd: opts.cwd,
1945
+ env: filteredEnv(opts.env),
1946
+ timeoutMs: opts.timeoutMs,
1947
+ maxOutputBytes: opts.maxOutputBytes
1948
+ };
1949
+ const startedAt = Date.now();
1950
+ const heartbeat = opts.onHeartbeat ? setInterval(() => opts.onHeartbeat?.(Date.now() - startedAt), 1e3) : null;
1951
+ try {
1952
+ const result = await client.exec(request, { signal: opts.signal });
1953
+ opts.onStdout?.(result.stdout);
1954
+ opts.onStderr?.(result.stderr);
1955
+ return result;
1956
+ } finally {
1957
+ if (heartbeat) clearInterval(heartbeat);
1958
+ }
1959
+ }
1960
+ };
1961
+ }
1962
+
1963
+ // src/server/sandbox/remote-worker/workerClient.ts
1964
+ import { timingSafeEqual } from "crypto";
1965
+ var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
1966
+ var DEFAULT_EXEC_TIMEOUT_MS = 3e4;
1967
+ var DEFAULT_EXEC_REQUEST_GRACE_MS = 1e4;
1968
+ var RemoteWorkerClientError = class extends Error {
1969
+ statusCode;
1970
+ code;
1971
+ details;
1972
+ constructor(message, opts) {
1973
+ super(message);
1974
+ this.name = "RemoteWorkerClientError";
1975
+ this.statusCode = opts.statusCode;
1976
+ this.code = opts.code ?? "remote_worker_error";
1977
+ this.details = opts.details;
1978
+ }
1979
+ };
1980
+ function requireNonEmpty(value, label) {
1981
+ const trimmed = value.trim();
1982
+ if (!trimmed) throw new Error(`${label} is required for ${REMOTE_WORKER_PROVIDER} mode`);
1983
+ return trimmed;
1984
+ }
1985
+ function normalizeBaseUrl(value) {
1986
+ return requireNonEmpty(value, "BORING_WORKER_BASE_URL").replace(/\/+$/, "");
1987
+ }
1988
+ function positiveInteger2(value, fallback, label) {
1989
+ if (value === void 0) return fallback;
1990
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be a positive integer`);
1991
+ return Math.trunc(value);
1992
+ }
1993
+ function encodeWorkspaceId(workspaceId) {
1994
+ return encodeURIComponent(requireNonEmpty(workspaceId, "workspaceId"));
1995
+ }
1996
+ function bytesToBase64(bytes) {
1997
+ return Buffer.from(bytes).toString("base64");
1998
+ }
1999
+ function base64ToBytes(value) {
2000
+ return new Uint8Array(Buffer.from(value, "base64"));
2001
+ }
2002
+ function makeHeaders(opts) {
2003
+ const headers = new Headers();
2004
+ headers.set(WORKER_INTERNAL_TOKEN_HEADER, requireNonEmpty(opts.token, "BORING_WORKER_INTERNAL_TOKEN"));
2005
+ headers.set(WORKER_WORKSPACE_ID_HEADER, requireNonEmpty(opts.workspaceId, "workspaceId"));
2006
+ if (opts.requestId) headers.set(WORKER_REQUEST_ID_HEADER, opts.requestId);
2007
+ if (opts.contentType) headers.set("content-type", opts.contentType);
2008
+ return headers;
2009
+ }
2010
+ async function parseError(response) {
2011
+ let payload;
2012
+ try {
2013
+ payload = await response.json();
2014
+ } catch {
2015
+ }
2016
+ return new RemoteWorkerClientError(
2017
+ payload?.error?.message ?? `remote worker request failed (${response.status})`,
2018
+ {
2019
+ statusCode: payload?.error?.statusCode ?? response.status,
2020
+ code: payload?.error?.code,
2021
+ details: payload?.error?.details
2022
+ }
2023
+ );
2024
+ }
2025
+ var RemoteWorkerClient = class {
2026
+ baseUrl;
2027
+ token;
2028
+ workspaceId;
2029
+ requestId;
2030
+ fetchImpl;
2031
+ requestTimeoutMs;
2032
+ execTimeoutMs;
2033
+ execRequestGraceMs;
2034
+ constructor(opts) {
2035
+ this.baseUrl = normalizeBaseUrl(opts.baseUrl);
2036
+ this.token = requireNonEmpty(opts.token, "BORING_WORKER_INTERNAL_TOKEN");
2037
+ this.workspaceId = requireNonEmpty(opts.workspaceId, "workspaceId");
2038
+ this.requestId = opts.requestId;
2039
+ this.fetchImpl = opts.fetchImpl ?? fetch;
2040
+ this.requestTimeoutMs = positiveInteger2(opts.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS, "requestTimeoutMs");
2041
+ this.execTimeoutMs = positiveInteger2(opts.execTimeoutMs, DEFAULT_EXEC_TIMEOUT_MS, "execTimeoutMs");
2042
+ this.execRequestGraceMs = positiveInteger2(opts.execRequestGraceMs, DEFAULT_EXEC_REQUEST_GRACE_MS, "execRequestGraceMs");
2043
+ }
2044
+ timeoutError(timeoutMs) {
2045
+ return new RemoteWorkerClientError("remote worker request timed out", {
2046
+ statusCode: 504,
2047
+ code: ErrorCode.enum.REMOTE_WORKER_TIMEOUT,
2048
+ details: { timeoutMs, retryable: true }
2049
+ });
2050
+ }
2051
+ abortedError() {
2052
+ return new RemoteWorkerClientError("remote worker request aborted", {
2053
+ statusCode: 499,
2054
+ code: ErrorCode.enum.ABORTED
2055
+ });
2056
+ }
2057
+ async fetchWithTimeout(url, init, timeoutMs) {
2058
+ const controller = new AbortController();
2059
+ let timedOut = false;
2060
+ const timer = setTimeout(() => {
2061
+ timedOut = true;
2062
+ controller.abort();
2063
+ }, timeoutMs);
2064
+ const upstreamSignal = init.signal;
2065
+ const abortFromUpstream = () => controller.abort();
2066
+ if (upstreamSignal?.aborted) {
2067
+ clearTimeout(timer);
2068
+ throw this.abortedError();
2069
+ }
2070
+ upstreamSignal?.addEventListener("abort", abortFromUpstream, { once: true });
2071
+ try {
2072
+ return await this.fetchImpl(url, { ...init, signal: controller.signal });
2073
+ } catch (error) {
2074
+ if (timedOut) throw this.timeoutError(timeoutMs);
2075
+ if (upstreamSignal?.aborted) throw this.abortedError();
2076
+ throw error;
2077
+ } finally {
2078
+ clearTimeout(timer);
2079
+ upstreamSignal?.removeEventListener("abort", abortFromUpstream);
2080
+ }
2081
+ }
2082
+ async health() {
2083
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/internal/health`, {
2084
+ headers: makeHeaders({ token: this.token, workspaceId: this.workspaceId, requestId: this.requestId })
2085
+ }, this.requestTimeoutMs);
2086
+ if (!response.ok) throw await parseError(response);
2087
+ }
2088
+ async workspace(op) {
2089
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/internal/workspaces/${encodeWorkspaceId(this.workspaceId)}/fs`, {
2090
+ method: "POST",
2091
+ headers: makeHeaders({ token: this.token, workspaceId: this.workspaceId, requestId: this.requestId, contentType: "application/json" }),
2092
+ body: JSON.stringify(op)
2093
+ }, this.requestTimeoutMs);
2094
+ if (!response.ok) throw await parseError(response);
2095
+ return await response.json();
2096
+ }
2097
+ async exec(input, opts = {}) {
2098
+ const timeoutMs = (input.timeoutMs ?? this.execTimeoutMs) + this.execRequestGraceMs;
2099
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/internal/workspaces/${encodeWorkspaceId(this.workspaceId)}/exec`, {
2100
+ method: "POST",
2101
+ headers: makeHeaders({ token: this.token, workspaceId: this.workspaceId, requestId: this.requestId, contentType: "application/json" }),
2102
+ body: JSON.stringify(input),
2103
+ signal: opts.signal
2104
+ }, timeoutMs);
2105
+ if (!response.ok) throw await parseError(response);
2106
+ const body = await response.json();
2107
+ return {
2108
+ stdout: base64ToBytes(body.stdoutBase64),
2109
+ stderr: base64ToBytes(body.stderrBase64),
2110
+ exitCode: body.exitCode,
2111
+ durationMs: body.durationMs,
2112
+ truncated: body.truncated,
2113
+ stdoutEncoding: body.stdoutEncoding,
2114
+ stderrEncoding: body.stderrEncoding
2115
+ };
2116
+ }
2117
+ watch(onEvent, onError) {
2118
+ const controller = new AbortController();
2119
+ void this.consumeEvents(controller.signal, onEvent).catch((error) => {
2120
+ if (!controller.signal.aborted) onError?.(error instanceof Error ? error : new Error(String(error)));
2121
+ });
2122
+ return { close: () => controller.abort() };
2123
+ }
2124
+ async consumeEvents(signal, onEvent) {
2125
+ const response = await this.fetchImpl(`${this.baseUrl}/internal/workspaces/${encodeWorkspaceId(this.workspaceId)}/fs/events`, {
2126
+ headers: makeHeaders({ token: this.token, workspaceId: this.workspaceId, requestId: this.requestId }),
2127
+ signal
2128
+ });
2129
+ if (!response.ok) throw await parseError(response);
2130
+ if (!response.body) throw new RemoteWorkerClientError("remote worker event stream missing body", { statusCode: 502 });
2131
+ const reader = response.body.getReader();
2132
+ const decoder2 = new TextDecoder();
2133
+ let buffer = "";
2134
+ try {
2135
+ for (; ; ) {
2136
+ const { done, value } = await reader.read();
2137
+ if (done) {
2138
+ if (signal.aborted) return;
2139
+ throw new RemoteWorkerClientError("remote worker event stream closed", {
2140
+ statusCode: 502,
2141
+ code: ErrorCode.enum.REMOTE_WORKER_STREAM_CLOSED
2142
+ });
2143
+ }
2144
+ buffer += decoder2.decode(value, { stream: true });
2145
+ let boundary = buffer.indexOf("\n\n");
2146
+ while (boundary >= 0) {
2147
+ const frame = buffer.slice(0, boundary);
2148
+ buffer = buffer.slice(boundary + 2);
2149
+ this.handleSseFrame(frame, onEvent);
2150
+ boundary = buffer.indexOf("\n\n");
2151
+ }
2152
+ }
2153
+ } finally {
2154
+ reader.releaseLock();
2155
+ }
2156
+ }
2157
+ handleSseFrame(frame, onEvent) {
2158
+ let eventName = "message";
2159
+ const dataLines = [];
2160
+ for (const line of frame.split("\n")) {
2161
+ if (line.startsWith(":")) continue;
2162
+ if (line.startsWith("event:")) eventName = line.slice("event:".length).trim();
2163
+ if (line.startsWith("data:")) dataLines.push(line.slice("data:".length).trimStart());
2164
+ }
2165
+ if (eventName !== "change" || dataLines.length === 0) return;
2166
+ const payload = JSON.parse(dataLines.join("\n"));
2167
+ onEvent(payload.event);
2168
+ }
2169
+ };
2170
+ function encodeBytesForWorker(bytes) {
2171
+ return bytesToBase64(bytes);
2172
+ }
2173
+ function decodeBytesFromWorker(value) {
2174
+ return base64ToBytes(value);
2175
+ }
2176
+ function constantTimeTokenEqual(a, b) {
2177
+ if (!a || !b) return false;
2178
+ const aBytes = Buffer.from(a);
2179
+ const bBytes = Buffer.from(b);
2180
+ if (aBytes.length !== bBytes.length) return false;
2181
+ return timingSafeEqual(aBytes, bBytes);
2182
+ }
2183
+
2184
+ // src/server/workspace/createRemoteWorkerWorkspace.ts
2185
+ function expectContent(result) {
2186
+ if ("content" in result && typeof result.content === "string") return result.content;
2187
+ throw new Error("remote worker returned invalid file content response");
2188
+ }
2189
+ function expectData(result) {
2190
+ if ("dataBase64" in result && typeof result.dataBase64 === "string") return decodeBytesFromWorker(result.dataBase64);
2191
+ throw new Error("remote worker returned invalid binary response");
2192
+ }
2193
+ function expectStat(result) {
2194
+ if ("stat" in result && result.stat) return result.stat;
2195
+ throw new Error("remote worker returned invalid stat response");
2196
+ }
2197
+ function expectEntries(result) {
2198
+ if ("entries" in result && Array.isArray(result.entries)) return result.entries;
2199
+ throw new Error("remote worker returned invalid readdir response");
2200
+ }
2201
+ function createRemoteWatcher(client) {
2202
+ const listeners = /* @__PURE__ */ new Map();
2203
+ let stream = null;
2204
+ let reconnectTimer = null;
2205
+ let closed = false;
2206
+ const clearReconnectTimer = () => {
2207
+ if (!reconnectTimer) return;
2208
+ clearTimeout(reconnectTimer);
2209
+ reconnectTimer = null;
2210
+ };
2211
+ const scheduleReconnect = () => {
2212
+ if (closed || listeners.size === 0 || reconnectTimer) return;
2213
+ reconnectTimer = setTimeout(() => {
2214
+ reconnectTimer = null;
2215
+ ensureStream();
2216
+ }, 1e3);
2217
+ };
2218
+ const handleStreamEnd = () => {
2219
+ stream = null;
2220
+ for (const options of [...listeners.values()]) {
2221
+ try {
2222
+ options?.onControlEvent?.({ type: "resync-required", reason: "remote_worker_stream_closed" });
2223
+ } catch {
2224
+ }
2225
+ }
2226
+ scheduleReconnect();
2227
+ };
2228
+ const ensureStream = () => {
2229
+ if (stream || closed || listeners.size === 0) return;
2230
+ clearReconnectTimer();
2231
+ stream = client.watch((event) => {
2232
+ for (const listener of [...listeners.keys()]) {
2233
+ try {
2234
+ listener(event);
2235
+ } catch {
2236
+ }
2237
+ }
2238
+ }, handleStreamEnd);
2239
+ };
2240
+ return {
2241
+ subscribe(listener, options) {
2242
+ if (closed) return () => {
2243
+ };
2244
+ listeners.set(listener, options);
2245
+ ensureStream();
2246
+ return () => {
2247
+ listeners.delete(listener);
2248
+ if (listeners.size === 0) {
2249
+ clearReconnectTimer();
2250
+ stream?.close();
2251
+ stream = null;
2252
+ }
2253
+ };
2254
+ },
2255
+ close() {
2256
+ if (closed) return;
2257
+ closed = true;
2258
+ listeners.clear();
2259
+ clearReconnectTimer();
2260
+ stream?.close();
2261
+ stream = null;
2262
+ }
2263
+ };
2264
+ }
2265
+ function createRemoteWorkerWorkspace(client) {
2266
+ let watcher = null;
2267
+ return {
2268
+ root: REMOTE_WORKER_RUNTIME_CWD,
2269
+ runtimeContext: { runtimeCwd: REMOTE_WORKER_RUNTIME_CWD },
2270
+ fsCapability: "best-effort",
2271
+ watch() {
2272
+ watcher ??= createRemoteWatcher(client);
2273
+ return watcher;
2274
+ },
2275
+ async readFile(path4) {
2276
+ return expectContent(await client.workspace({ op: "readFile", path: path4 }));
2277
+ },
2278
+ async readBinaryFile(path4) {
2279
+ return expectData(await client.workspace({ op: "readBinaryFile", path: path4 }));
2280
+ },
2281
+ async writeFile(path4, data) {
2282
+ await client.workspace({ op: "writeFile", path: path4, data });
2283
+ },
2284
+ async writeBinaryFile(path4, data) {
2285
+ await client.workspace({ op: "writeBinaryFile", path: path4, dataBase64: encodeBytesForWorker(data) });
2286
+ },
2287
+ async readFileWithStat(path4) {
2288
+ const result = await client.workspace({ op: "readFileWithStat", path: path4 });
2289
+ if ("content" in result && "stat" in result) return { content: result.content, stat: result.stat };
2290
+ throw new Error("remote worker returned invalid readFileWithStat response");
2291
+ },
2292
+ async writeFileWithStat(path4, data) {
2293
+ return expectStat(await client.workspace({ op: "writeFileWithStat", path: path4, data }));
2294
+ },
2295
+ async writeBinaryFileWithStat(path4, data) {
2296
+ return expectStat(await client.workspace({ op: "writeBinaryFileWithStat", path: path4, dataBase64: encodeBytesForWorker(data) }));
2297
+ },
2298
+ async unlink(path4) {
2299
+ await client.workspace({ op: "unlink", path: path4 });
2300
+ },
2301
+ async readdir(path4) {
2302
+ return expectEntries(await client.workspace({ op: "readdir", path: path4 }));
2303
+ },
2304
+ async stat(path4) {
2305
+ return expectStat(await client.workspace({ op: "stat", path: path4 }));
2306
+ },
2307
+ async mkdir(path4, opts) {
2308
+ await client.workspace({ op: "mkdir", path: path4, recursive: opts?.recursive });
2309
+ },
2310
+ async rename(from, to) {
2311
+ await client.workspace({ op: "rename", from, to });
2312
+ }
2313
+ };
2314
+ }
2315
+
2316
+ // src/server/runtime/modes/remote-worker.ts
2317
+ function requireOption(value, name) {
2318
+ const trimmed = value?.trim();
2319
+ if (!trimmed) throw new Error(`${name} is required for remote-worker mode`);
2320
+ return trimmed;
2321
+ }
2322
+ function createRemoteWorkerModeAdapter(opts = {}) {
2323
+ return {
2324
+ id: REMOTE_WORKER_PROVIDER,
2325
+ workspaceFsCapability: "best-effort",
2326
+ async create(ctx) {
2327
+ const workspaceId = requireOption(ctx.workspaceId ?? ctx.sessionId, "workspaceId");
2328
+ const client = new RemoteWorkerClient({
2329
+ baseUrl: requireOption(opts.baseUrl ?? getEnv("BORING_WORKER_BASE_URL"), "BORING_WORKER_BASE_URL"),
2330
+ token: requireOption(opts.token ?? getEnv("BORING_WORKER_INTERNAL_TOKEN"), "BORING_WORKER_INTERNAL_TOKEN"),
2331
+ workspaceId,
2332
+ requestId: ctx.requestId,
2333
+ fetchImpl: opts.fetchImpl
2334
+ });
2335
+ const workspace = createRemoteWorkerWorkspace(client);
2336
+ const sandbox = createRemoteWorkerSandbox(client);
2337
+ await sandbox.init?.({ workspace, sessionId: ctx.sessionId });
2338
+ return {
2339
+ runtimeContext: { runtimeCwd: REMOTE_WORKER_RUNTIME_CWD },
2340
+ workspace,
2341
+ sandbox,
2342
+ fileSearch: createServerFileSearch(workspace, sandbox)
2343
+ };
2344
+ }
2345
+ };
2346
+ }
2347
+
1820
2348
  // src/server/http/routes/file.ts
1821
2349
  import { dirname as dirname5, extname as extname2, relative as relative4 } from "path/posix";
1822
2350
 
@@ -2851,7 +3379,7 @@ function cloneStat(stat11) {
2851
3379
  function cloneEntries(entries) {
2852
3380
  return entries.map((entry) => ({ name: entry.name, kind: entry.kind }));
2853
3381
  }
2854
- function shellQuote2(value) {
3382
+ function shellQuote3(value) {
2855
3383
  return `'${value.replace(/'/g, `'\\''`)}'`;
2856
3384
  }
2857
3385
  async function runJson(sandbox, script) {
@@ -2931,7 +3459,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2931
3459
  async function assertRealPathWithinSandboxRoot(sandboxPath) {
2932
3460
  const isWithinRoot = await runJson(
2933
3461
  remote,
2934
- `node -e ${shellQuote2(`const fs=require('fs'); const path=require('path'); const root=fs.realpathSync(process.argv[1]); const target=fs.realpathSync(process.argv[2]); const rel=path.relative(root,target); process.stdout.write(JSON.stringify(rel===''||(!rel.startsWith('..')&&!path.isAbsolute(rel))))`)} ${shellQuote2(VERCEL_SANDBOX_REMOTE_ROOT)} ${shellQuote2(sandboxPath)}`
3462
+ `node -e ${shellQuote3(`const fs=require('fs'); const path=require('path'); const root=fs.realpathSync(process.argv[1]); const target=fs.realpathSync(process.argv[2]); const rel=path.relative(root,target); process.stdout.write(JSON.stringify(rel===''||(!rel.startsWith('..')&&!path.isAbsolute(rel))))`)} ${shellQuote3(VERCEL_SANDBOX_REMOTE_ROOT)} ${shellQuote3(sandboxPath)}`
2935
3463
  );
2936
3464
  if (!isWithinRoot) {
2937
3465
  throw Object.assign(new Error("resolved path escapes workspace root"), { code: EPERM_CODE2 });
@@ -2940,7 +3468,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2940
3468
  async function isSandboxSymlink(sandboxPath) {
2941
3469
  return await runJson(
2942
3470
  remote,
2943
- `node -e ${shellQuote2(`const fs=require('fs'); process.stdout.write(JSON.stringify(fs.lstatSync(process.argv[1]).isSymbolicLink()))`)} ${shellQuote2(sandboxPath)}`
3471
+ `node -e ${shellQuote3(`const fs=require('fs'); process.stdout.write(JSON.stringify(fs.lstatSync(process.argv[1]).isSymbolicLink()))`)} ${shellQuote3(sandboxPath)}`
2944
3472
  );
2945
3473
  }
2946
3474
  async function listDescendantPaths(relPath, sandboxPath) {
@@ -2960,7 +3488,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2960
3488
  }
2961
3489
  return await runJson(
2962
3490
  remote,
2963
- `node -e ${shellQuote2(`const fs=require('fs'); const path=require('path'); const root=process.argv[1]; const relRoot=process.argv[2]; function walk(abs,rel){ const s=fs.statSync(abs); if(!s.isDirectory()) return []; const out=[]; for (const entry of fs.readdirSync(abs,{withFileTypes:true})) { const childRel=rel==='.'?entry.name:rel+'/'+entry.name; out.push(childRel); if(entry.isDirectory()) out.push(...walk(path.join(abs,entry.name),childRel)); } return out; } process.stdout.write(JSON.stringify(walk(root,relRoot)))`)} ${shellQuote2(sandboxPath)} ${shellQuote2(relPath)}`
3491
+ `node -e ${shellQuote3(`const fs=require('fs'); const path=require('path'); const root=process.argv[1]; const relRoot=process.argv[2]; function walk(abs,rel){ const s=fs.statSync(abs); if(!s.isDirectory()) return []; const out=[]; for (const entry of fs.readdirSync(abs,{withFileTypes:true})) { const childRel=rel==='.'?entry.name:rel+'/'+entry.name; out.push(childRel); if(entry.isDirectory()) out.push(...walk(path.join(abs,entry.name),childRel)); } return out; } process.stdout.write(JSON.stringify(walk(root,relRoot)))`)} ${shellQuote3(sandboxPath)} ${shellQuote3(relPath)}`
2964
3492
  );
2965
3493
  }
2966
3494
  return {
@@ -3053,7 +3581,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3053
3581
  const version = metadataVersion;
3054
3582
  const result = await runJson(
3055
3583
  remote,
3056
- `node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); const content=fs.readFileSync(p,'utf8'); process.stdout.write(JSON.stringify({content,stat:{size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}}))`)} ${shellQuote2(sandboxPath)}`
3584
+ `node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); const content=fs.readFileSync(p,'utf8'); process.stdout.write(JSON.stringify({content,stat:{size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}}))`)} ${shellQuote3(sandboxPath)}`
3057
3585
  );
3058
3586
  if (metadataVersion === version) {
3059
3587
  statCache.set(sandboxPath, result.stat);
@@ -3081,7 +3609,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3081
3609
  };
3082
3610
  })() : await runJson(
3083
3611
  remote,
3084
- `node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote2(sandboxPath)}`
3612
+ `node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote3(sandboxPath)}`
3085
3613
  );
3086
3614
  statCache.set(sandboxPath, writtenStat2);
3087
3615
  emitChange({ op: "write", path: relPath, mtimeMs: writtenStat2.mtimeMs });
@@ -3090,7 +3618,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3090
3618
  const encoded = payload.toString("base64");
3091
3619
  const writtenStat = await runJson(
3092
3620
  remote,
3093
- `node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const data=Buffer.from(process.argv[2],'base64'); fs.writeFileSync(p,data); const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote2(sandboxPath)} ${shellQuote2(encoded)}`
3621
+ `node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const data=Buffer.from(process.argv[2],'base64'); fs.writeFileSync(p,data); const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote3(sandboxPath)} ${shellQuote3(encoded)}`
3094
3622
  );
3095
3623
  invalidateMetadataCache();
3096
3624
  statCache.set(sandboxPath, writtenStat);
@@ -3119,7 +3647,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3119
3647
  };
3120
3648
  })() : await runJson(
3121
3649
  remote,
3122
- `node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote2(sandboxPath)}`
3650
+ `node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote3(sandboxPath)}`
3123
3651
  );
3124
3652
  statCache.set(sandboxPath, writtenStat2);
3125
3653
  emitChange({ op: "write", path: relPath, mtimeMs: writtenStat2.mtimeMs });
@@ -3128,7 +3656,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3128
3656
  const encoded = payload.toString("base64");
3129
3657
  const writtenStat = await runJson(
3130
3658
  remote,
3131
- `node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const data=Buffer.from(process.argv[2],'base64'); fs.writeFileSync(p,data); const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote2(sandboxPath)} ${shellQuote2(encoded)}`
3659
+ `node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const data=Buffer.from(process.argv[2],'base64'); fs.writeFileSync(p,data); const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote3(sandboxPath)} ${shellQuote3(encoded)}`
3132
3660
  );
3133
3661
  invalidateMetadataCache();
3134
3662
  statCache.set(sandboxPath, writtenStat);
@@ -3144,7 +3672,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3144
3672
  await assertRealPathWithinSandboxRoot(sandboxPath);
3145
3673
  const descendantPaths = await isSandboxSymlink(sandboxPath) ? [] : await listDescendantPaths(relPath, sandboxPath);
3146
3674
  if (remote.fs?.rm) await remote.fs.rm(sandboxPath, { recursive: true, force: false });
3147
- else await runShell(remote, `rm -r -- ${shellQuote2(sandboxPath)}`);
3675
+ else await runShell(remote, `rm -r -- ${shellQuote3(sandboxPath)}`);
3148
3676
  invalidateMetadataCache();
3149
3677
  workspaceOpts.onMutation?.();
3150
3678
  emitChange({ op: "unlink", path: relPath });
@@ -3162,7 +3690,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3162
3690
  kind: entry.isDirectory() ? "dir" : "file"
3163
3691
  })) : await runJson(
3164
3692
  remote,
3165
- `node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const entries=fs.readdirSync(p,{withFileTypes:true}).map((e)=>({name:e.name,kind:e.isDirectory()?'dir':'file'})); process.stdout.write(JSON.stringify(entries))`)} ${shellQuote2(sandboxPath)}`
3693
+ `node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const entries=fs.readdirSync(p,{withFileTypes:true}).map((e)=>({name:e.name,kind:e.isDirectory()?'dir':'file'})); process.stdout.write(JSON.stringify(entries))`)} ${shellQuote3(sandboxPath)}`
3166
3694
  );
3167
3695
  if (metadataVersion === version) {
3168
3696
  readdirCache.set(sandboxPath, mappedEntries);
@@ -3185,7 +3713,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3185
3713
  } else {
3186
3714
  mappedStat = await runJson(
3187
3715
  remote,
3188
- `node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote2(sandboxPath)}`
3716
+ `node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote3(sandboxPath)}`
3189
3717
  );
3190
3718
  }
3191
3719
  if (metadataVersion === version) {
@@ -3196,9 +3724,9 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3196
3724
  async mkdir(relPath, opts) {
3197
3725
  const sandboxPath = toSandboxPath(relPath);
3198
3726
  if (remote.fs?.mkdir) await remote.fs.mkdir(sandboxPath, { recursive: opts?.recursive ?? false });
3199
- else if (opts?.recursive) await runShell(remote, `mkdir -p -- ${shellQuote2(sandboxPath)}`);
3727
+ else if (opts?.recursive) await runShell(remote, `mkdir -p -- ${shellQuote3(sandboxPath)}`);
3200
3728
  else if (remote.mkDir) await remote.mkDir(sandboxPath);
3201
- else await runShell(remote, `mkdir -- ${shellQuote2(sandboxPath)}`);
3729
+ else await runShell(remote, `mkdir -- ${shellQuote3(sandboxPath)}`);
3202
3730
  invalidateMetadataCache();
3203
3731
  workspaceOpts.onMutation?.();
3204
3732
  emitChange({ op: "mkdir", path: relPath });
@@ -3207,7 +3735,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3207
3735
  const fromSandboxPath = toSandboxPath(fromRelPath);
3208
3736
  const toSandboxAbsolutePath = toSandboxPath(toRelPath2);
3209
3737
  if (remote.fs?.rename) await remote.fs.rename(fromSandboxPath, toSandboxAbsolutePath);
3210
- else await runShell(remote, `mv -- ${shellQuote2(fromSandboxPath)} ${shellQuote2(toSandboxAbsolutePath)}`);
3738
+ else await runShell(remote, `mv -- ${shellQuote3(fromSandboxPath)} ${shellQuote3(toSandboxAbsolutePath)}`);
3211
3739
  invalidateMetadataCache();
3212
3740
  workspaceOpts.onMutation?.();
3213
3741
  emitChange({ op: "rename", path: toRelPath2, oldPath: fromRelPath });
@@ -4061,61 +4589,6 @@ import { spawnSync } from "child_process";
4061
4589
  // src/server/runtime/modes/direct.ts
4062
4590
  import { mkdir as mkdir8 } from "fs/promises";
4063
4591
 
4064
- // src/server/runtime/createServerFileSearch.ts
4065
- var DEFAULT_LIMIT = 500;
4066
- var MAX_LIMIT = 5e3;
4067
- var SEARCH_TIMEOUT_MS = 5e3;
4068
- var SEARCH_MAX_OUTPUT_BYTES = 256e3;
4069
- function shellQuote3(value) {
4070
- return `'${value.replaceAll("'", "'\\''")}'`;
4071
- }
4072
- function normalizeLimit(limit) {
4073
- if (typeof limit !== "number" || !Number.isFinite(limit)) {
4074
- return DEFAULT_LIMIT;
4075
- }
4076
- const normalized = Math.trunc(limit);
4077
- if (normalized <= 0) return DEFAULT_LIMIT;
4078
- return Math.min(normalized, MAX_LIMIT);
4079
- }
4080
- function buildFindArgs(glob) {
4081
- const isPathShaped = glob.includes("/") || glob.includes("**");
4082
- if (!isPathShaped) {
4083
- return `-iname ${shellQuote3(glob)}`;
4084
- }
4085
- let translated = glob.replaceAll("**", "*");
4086
- translated = translated.replace(/^\/+/, "");
4087
- if (!translated.startsWith("*")) {
4088
- translated = `*${translated}`;
4089
- }
4090
- return `-ipath ${shellQuote3(translated)}`;
4091
- }
4092
- function createServerFileSearch(workspace, sandbox) {
4093
- return {
4094
- async search(glob, limit = DEFAULT_LIMIT) {
4095
- const safeLimit = normalizeLimit(limit);
4096
- const command = [
4097
- "find .",
4098
- "-maxdepth 10",
4099
- "\\( -path './.boring-agent' -o -path './.git' -o -path './node_modules' \\) -prune -o",
4100
- buildFindArgs(glob),
4101
- "-type f",
4102
- "-print",
4103
- `| head -n ${safeLimit}`
4104
- ].join(" ");
4105
- const { stdout, exitCode } = await sandbox.exec(command, {
4106
- cwd: workspace.root,
4107
- timeoutMs: SEARCH_TIMEOUT_MS,
4108
- maxOutputBytes: SEARCH_MAX_OUTPUT_BYTES
4109
- });
4110
- if (exitCode !== 0) {
4111
- throw new Error(`file-search failed: exit ${exitCode}`);
4112
- }
4113
- const decoded = new TextDecoder().decode(stdout);
4114
- return decoded.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.length > 0).map((line) => line.replace(/^\.\//, ""));
4115
- }
4116
- };
4117
- }
4118
-
4119
4592
  // src/server/workspace/provision.ts
4120
4593
  import { cp as cp2 } from "fs/promises";
4121
4594
  async function copyTemplate(templatePath, workspaceRoot) {
@@ -6190,6 +6663,11 @@ var INFOMANIAK_PROVIDER = "infomaniak";
6190
6663
  var INFOMANIAK_API_BASE = "https://api.infomaniak.com";
6191
6664
  var DEFAULT_CUSTOM_MODEL_MAX_TOKENS = 16384;
6192
6665
  var DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW = 2e5;
6666
+ var DEFAULT_INFOMANIAK_MODELS = [
6667
+ "moonshotai/Kimi-K2.6",
6668
+ "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8",
6669
+ "Qwen/Qwen3.5-122B-A10B-FP8"
6670
+ ];
6193
6671
  function clean(value) {
6194
6672
  const trimmed = value?.trim();
6195
6673
  return trimmed && trimmed.length > 0 ? trimmed : void 0;
@@ -6213,6 +6691,19 @@ function readModelInput(name) {
6213
6691
  const parsed = raw.split(",").map((part) => part.trim()).filter((part) => part === "text" || part === "image");
6214
6692
  return parsed.length > 0 ? parsed : ["text"];
6215
6693
  }
6694
+ function readMaxTokensField(name, fallback = "max_completion_tokens") {
6695
+ const raw = clean(getEnv(name));
6696
+ return raw === "max_tokens" || raw === "max_completion_tokens" ? raw : fallback;
6697
+ }
6698
+ function buildOpenAICompletionsCompat(envPrefix, defaults = {}) {
6699
+ return {
6700
+ supportsStore: readBoolean(`${envPrefix}_SUPPORTS_STORE`, defaults.supportsStore ?? false),
6701
+ supportsDeveloperRole: readBoolean(`${envPrefix}_SUPPORTS_DEVELOPER_ROLE`, defaults.supportsDeveloperRole ?? true),
6702
+ supportsReasoningEffort: readBoolean(`${envPrefix}_SUPPORTS_REASONING_EFFORT`, defaults.supportsReasoningEffort ?? true),
6703
+ supportsUsageInStreaming: readBoolean(`${envPrefix}_SUPPORTS_USAGE_IN_STREAMING`, defaults.supportsUsageInStreaming ?? true),
6704
+ maxTokensField: readMaxTokensField(`${envPrefix}_MAX_TOKENS_FIELD`, defaults.maxTokensField)
6705
+ };
6706
+ }
6216
6707
  function readApiKeyEnv(candidates) {
6217
6708
  for (const candidate of candidates) {
6218
6709
  const envName = clean(candidate);
@@ -6225,31 +6716,23 @@ function buildOpenAICompatibleProviderConfig(opts) {
6225
6716
  baseUrl: opts.baseUrl,
6226
6717
  apiKey: opts.apiKeyEnv,
6227
6718
  api: "openai-completions",
6228
- models: [
6229
- {
6230
- id: opts.modelId,
6231
- name: opts.modelName ?? opts.modelId,
6232
- api: "openai-completions",
6233
- reasoning: readBoolean(`${opts.envPrefix}_REASONING`, true),
6234
- input: readModelInput(`${opts.envPrefix}_INPUT`),
6235
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
6236
- contextWindow: readPositiveInt(
6237
- `${opts.envPrefix}_CONTEXT_WINDOW`,
6238
- DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW
6239
- ),
6240
- maxTokens: readPositiveInt(
6241
- `${opts.envPrefix}_MAX_TOKENS`,
6242
- DEFAULT_CUSTOM_MODEL_MAX_TOKENS
6243
- ),
6244
- compat: {
6245
- supportsStore: false,
6246
- supportsDeveloperRole: true,
6247
- supportsReasoningEffort: true,
6248
- supportsUsageInStreaming: true,
6249
- maxTokensField: "max_completion_tokens"
6250
- }
6251
- }
6252
- ]
6719
+ models: opts.models.map((model) => ({
6720
+ id: model.id,
6721
+ name: model.name ?? model.id,
6722
+ api: "openai-completions",
6723
+ reasoning: readBoolean(`${opts.envPrefix}_REASONING`, true),
6724
+ input: readModelInput(`${opts.envPrefix}_INPUT`),
6725
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
6726
+ contextWindow: readPositiveInt(
6727
+ `${opts.envPrefix}_CONTEXT_WINDOW`,
6728
+ DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW
6729
+ ),
6730
+ maxTokens: readPositiveInt(
6731
+ `${opts.envPrefix}_MAX_TOKENS`,
6732
+ DEFAULT_CUSTOM_MODEL_MAX_TOKENS
6733
+ ),
6734
+ compat: buildOpenAICompletionsCompat(opts.envPrefix, opts.compatDefaults)
6735
+ }))
6253
6736
  };
6254
6737
  }
6255
6738
  function readInfomaniakBaseUrl() {
@@ -6259,25 +6742,46 @@ function readInfomaniakBaseUrl() {
6259
6742
  if (!productId) return void 0;
6260
6743
  return `${INFOMANIAK_API_BASE}/2/ai/${productId}/openai/v1`;
6261
6744
  }
6745
+ function readInfomaniakModelIds() {
6746
+ const configured = clean(getEnv("BORING_AGENT_INFOMANIAK_MODELS"))?.split(",").map((part) => part.trim()).filter(Boolean);
6747
+ const ids = configured?.length ? configured : [...DEFAULT_INFOMANIAK_MODELS];
6748
+ const legacyModel = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL"));
6749
+ if (legacyModel && !ids.includes(legacyModel)) ids.push(legacyModel);
6750
+ return Array.from(new Set(ids));
6751
+ }
6262
6752
  function readInfomaniakProvider() {
6263
- const modelId = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL")) ?? clean(getEnv("BORING_AGENT_DEFAULT_MODEL_ID"));
6753
+ const modelIds = readInfomaniakModelIds();
6754
+ const defaultModelId = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL")) ?? clean(getEnv("BORING_AGENT_DEFAULT_MODEL_ID")) ?? modelIds[0];
6264
6755
  const baseUrl = readInfomaniakBaseUrl();
6265
6756
  const apiKeyEnv = readApiKeyEnv([
6266
6757
  clean(getEnv("BORING_AGENT_INFOMANIAK_API_KEY_ENV")) ?? "",
6267
6758
  "INFOMANIAK_API_TOKEN",
6268
6759
  "BORING_AGENT_INFOMANIAK_API_KEY"
6269
6760
  ]);
6270
- if (!modelId || !baseUrl || !apiKeyEnv) return void 0;
6761
+ if (!defaultModelId || !baseUrl || !apiKeyEnv) return void 0;
6271
6762
  const provider = clean(getEnv("BORING_AGENT_INFOMANIAK_PROVIDER")) ?? INFOMANIAK_PROVIDER;
6763
+ const modelName = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL_NAME"));
6764
+ const models = modelIds.map((id) => {
6765
+ const model = { id };
6766
+ if (id === defaultModelId && modelName) model.name = modelName;
6767
+ return model;
6768
+ });
6769
+ if (!models.some((model) => model.id === defaultModelId)) {
6770
+ models.push({ id: defaultModelId, name: modelName });
6771
+ }
6272
6772
  return {
6273
6773
  provider,
6274
- model: { provider, id: modelId },
6774
+ models: models.map((model) => ({ provider, id: model.id })),
6775
+ defaultModel: { provider, id: defaultModelId },
6275
6776
  config: buildOpenAICompatibleProviderConfig({
6276
- modelId,
6277
- modelName: clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL_NAME")),
6777
+ models,
6278
6778
  apiKeyEnv,
6279
6779
  baseUrl,
6280
- envPrefix: "BORING_AGENT_INFOMANIAK"
6780
+ envPrefix: "BORING_AGENT_INFOMANIAK",
6781
+ // Infomaniak's OpenAI-compatible chat endpoint rejects OpenAI's newer
6782
+ // `developer` role (surfacing as "400 Unexpected message role"). Hosts
6783
+ // can opt back in with BORING_AGENT_INFOMANIAK_SUPPORTS_DEVELOPER_ROLE=1.
6784
+ compatDefaults: { supportsDeveloperRole: false, supportsReasoningEffort: false }
6281
6785
  })
6282
6786
  };
6283
6787
  }
@@ -6290,12 +6794,15 @@ function readCustomProvider() {
6290
6794
  "BORING_AGENT_CUSTOM_MODEL_API_KEY"
6291
6795
  ]);
6292
6796
  if (!provider || !modelId || !baseUrl || !apiKeyEnv) return void 0;
6797
+ const modelName = clean(getEnv("BORING_AGENT_CUSTOM_MODEL_NAME"));
6798
+ const model = { id: modelId };
6799
+ if (modelName) model.name = modelName;
6293
6800
  return {
6294
6801
  provider,
6295
- model: { provider, id: modelId },
6802
+ models: [{ provider, id: modelId }],
6803
+ defaultModel: { provider, id: modelId },
6296
6804
  config: buildOpenAICompatibleProviderConfig({
6297
- modelId,
6298
- modelName: clean(getEnv("BORING_AGENT_CUSTOM_MODEL_NAME")),
6805
+ models: [model],
6299
6806
  apiKeyEnv,
6300
6807
  baseUrl,
6301
6808
  envPrefix: "BORING_AGENT_CUSTOM_MODEL"
@@ -6309,7 +6816,7 @@ function registerConfiguredModelProviders(registry) {
6309
6816
  if (!provider) continue;
6310
6817
  if (seen.has(provider.provider)) continue;
6311
6818
  registry.registerProvider(provider.provider, provider.config);
6312
- registered.push(provider.model);
6819
+ registered.push(...provider.models);
6313
6820
  seen.add(provider.provider);
6314
6821
  }
6315
6822
  return registered;
@@ -6337,7 +6844,7 @@ function readConfiguredDefaultModel() {
6337
6844
  if (explicitProvider && explicitId) {
6338
6845
  return { provider: explicitProvider, id: explicitId };
6339
6846
  }
6340
- return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.model ?? readCustomProvider()?.model;
6847
+ return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.defaultModel ?? readCustomProvider()?.defaultModel;
6341
6848
  }
6342
6849
 
6343
6850
  // src/server/piPackages.ts
@@ -7191,10 +7698,10 @@ function vercelBashOps(sandbox, opts = {}) {
7191
7698
  return {
7192
7699
  exec(command, cwd, { onData, signal, timeout, env }) {
7193
7700
  const effectiveEnv = opts.mergeEnv ? opts.mergeEnv(env) : env;
7194
- const filteredEnv = effectiveEnv ? Object.fromEntries(Object.entries(effectiveEnv).filter((e) => e[1] != null)) : void 0;
7701
+ const filteredEnv2 = effectiveEnv ? Object.fromEntries(Object.entries(effectiveEnv).filter((e) => e[1] != null)) : void 0;
7195
7702
  return sandbox.exec(command, {
7196
7703
  cwd,
7197
- env: filteredEnv,
7704
+ env: filteredEnv2,
7198
7705
  signal,
7199
7706
  timeoutMs: timeout ? timeout * 1e3 : void 0,
7200
7707
  onStdout: (chunk) => onData(Buffer.from(chunk)),
@@ -7578,8 +8085,7 @@ function adaptPiTool(piTool) {
7578
8085
  }
7579
8086
  function buildFilesystemAgentTools(bundle) {
7580
8087
  const cwd = bundle.workspace.root;
7581
- const storageRoot = getRuntimeBundleStorageRoot(bundle);
7582
- if (bundle.sandbox.provider === "vercel-sandbox") {
8088
+ if (bundle.sandbox.provider === "vercel-sandbox" || bundle.sandbox.provider === "remote-worker") {
7583
8089
  return [
7584
8090
  adaptPiTool(createReadToolDefinition(cwd, { operations: vercelReadOps(bundle.workspace) })),
7585
8091
  adaptPiTool(createWriteToolDefinition(cwd, { operations: vercelWriteOps(bundle.workspace) })),
@@ -7589,6 +8095,7 @@ function buildFilesystemAgentTools(bundle) {
7589
8095
  adaptPiTool(createLsToolDefinition(cwd, { operations: vercelLsOps(bundle.workspace) }))
7590
8096
  ];
7591
8097
  }
8098
+ const storageRoot = getRuntimeBundleStorageRoot(bundle);
7592
8099
  const ops = boundFs(storageRoot, { runtimeRoot: cwd });
7593
8100
  return [
7594
8101
  adaptPiTool(createReadToolDefinition(cwd, { operations: ops.read })),
@@ -7724,9 +8231,9 @@ function directSpawnHook(workspaceRoot, runtime) {
7724
8231
  }
7725
8232
  var VERCEL_SAFE_DEFAULT_PATH = "/vercel/runtimes/node24/bin:/vercel/runtimes/node22/bin:/usr/local/bin:/usr/bin:/bin";
7726
8233
  function bashOptionsForMode(bundle, runtime) {
7727
- const storageRoot = getRuntimeBundleStorageRoot(bundle);
7728
8234
  switch (bundle.sandbox.provider) {
7729
8235
  case "vercel-sandbox":
8236
+ case "remote-worker":
7730
8237
  return {
7731
8238
  operations: vercelBashOps(bundle.sandbox, {
7732
8239
  // The pi bash tool's env may include the host process env. Never
@@ -7735,16 +8242,20 @@ function bashOptionsForMode(bundle, runtime) {
7735
8242
  mergeEnv: () => mergeRuntimeEnv(runtime, { PATH: VERCEL_SAFE_DEFAULT_PATH })
7736
8243
  })
7737
8244
  };
7738
- case "bwrap":
8245
+ case "bwrap": {
8246
+ const storageRoot = getRuntimeBundleStorageRoot(bundle);
7739
8247
  return {
7740
8248
  operations: createLocalBashOperations(),
7741
8249
  spawnHook: bwrapSpawnHook(storageRoot, runtime)
7742
8250
  };
7743
- default:
8251
+ }
8252
+ default: {
8253
+ const storageRoot = getRuntimeBundleStorageRoot(bundle);
7744
8254
  return {
7745
8255
  operations: createLocalBashOperations(),
7746
8256
  spawnHook: directSpawnHook(storageRoot, runtime)
7747
8257
  };
8258
+ }
7748
8259
  }
7749
8260
  }
7750
8261
  function isRuntimeReady(readiness) {
@@ -7921,9 +8432,21 @@ var DEFAULT_BUFFER_SIZE = 1e3;
7921
8432
  function createFsEventBroadcaster(watcher, opts = {}) {
7922
8433
  const bufferSize = opts.bufferSize ?? DEFAULT_BUFFER_SIZE;
7923
8434
  const buffer = [];
7924
- const listeners = /* @__PURE__ */ new Set();
8435
+ const listeners = /* @__PURE__ */ new Map();
7925
8436
  let nextSeq = 1;
7926
8437
  let closed = false;
8438
+ let gapAfterSeq = null;
8439
+ const handleControlEvent = (event) => {
8440
+ if (closed || event.type !== "resync-required") return;
8441
+ gapAfterSeq = nextSeq - 1;
8442
+ buffer.length = 0;
8443
+ for (const { onResyncRequired } of [...listeners.values()]) {
8444
+ try {
8445
+ onResyncRequired?.();
8446
+ } catch {
8447
+ }
8448
+ }
8449
+ };
7927
8450
  const watcherUnsub = watcher.subscribe((change) => {
7928
8451
  if (closed) return;
7929
8452
  const env = {
@@ -7934,13 +8457,13 @@ function createFsEventBroadcaster(watcher, opts = {}) {
7934
8457
  };
7935
8458
  buffer.push(env);
7936
8459
  if (buffer.length > bufferSize) buffer.shift();
7937
- for (const l of [...listeners]) {
8460
+ for (const l of [...listeners.keys()]) {
7938
8461
  try {
7939
8462
  l(env);
7940
8463
  } catch {
7941
8464
  }
7942
8465
  }
7943
- });
8466
+ }, { onControlEvent: handleControlEvent });
7944
8467
  return {
7945
8468
  subscribe(listener, sopts) {
7946
8469
  if (closed) {
@@ -7951,13 +8474,13 @@ function createFsEventBroadcaster(watcher, opts = {}) {
7951
8474
  let resyncRequired = false;
7952
8475
  if (typeof sopts?.lastSeenSeq === "number" && sopts.lastSeenSeq > 0) {
7953
8476
  const head = buffer.length > 0 ? buffer[0].seq : nextSeq;
7954
- if (sopts.lastSeenSeq < head - 1) {
8477
+ if (gapAfterSeq != null && sopts.lastSeenSeq <= gapAfterSeq || sopts.lastSeenSeq < head - 1) {
7955
8478
  resyncRequired = true;
7956
8479
  } else {
7957
8480
  replay = buffer.filter((e) => e.seq > sopts.lastSeenSeq);
7958
8481
  }
7959
8482
  }
7960
- listeners.add(listener);
8483
+ listeners.set(listener, { onResyncRequired: sopts?.onResyncRequired });
7961
8484
  return {
7962
8485
  replay,
7963
8486
  resyncRequired,
@@ -8030,7 +8553,10 @@ function fsEventsRoutes(app, opts, done) {
8030
8553
  try {
8031
8554
  sub = entry.broadcaster.subscribe(
8032
8555
  (env) => writeChange(reply.raw, env),
8033
- lastSeenSeq != null ? { lastSeenSeq } : void 0
8556
+ {
8557
+ ...lastSeenSeq != null ? { lastSeenSeq } : {},
8558
+ onResyncRequired: () => writeSse(reply.raw, "resync-required", {})
8559
+ }
8034
8560
  );
8035
8561
  } catch (error) {
8036
8562
  releaseBroadcaster(workspaceId, entry);
@@ -8041,10 +8567,6 @@ function fsEventsRoutes(app, opts, done) {
8041
8567
  }
8042
8568
  if (sub.resyncRequired) {
8043
8569
  writeSse(reply.raw, "resync-required", {});
8044
- sub.unsubscribe();
8045
- releaseBroadcaster(workspaceId, entry);
8046
- reply.raw.end();
8047
- return;
8048
8570
  }
8049
8571
  for (const env of sub.replay) writeChange(reply.raw, env);
8050
8572
  const heartbeat = setInterval(() => {
@@ -8203,19 +8725,25 @@ import { AuthStorage as AuthStorage2, ModelRegistry as ModelRegistry2 } from "@m
8203
8725
  function modelsRoutes(app, _opts, done) {
8204
8726
  const authStorage = AuthStorage2.create();
8205
8727
  const registry = ModelRegistry2.create(authStorage);
8206
- registerConfiguredModelProviders(registry);
8728
+ const configuredModels = registerConfiguredModelProviders(registry);
8729
+ const configuredModelSet = new Set(
8730
+ configuredModels.map((model) => `${model.provider}:${model.id}`)
8731
+ );
8207
8732
  app.get("/api/v1/agent/models", async (_request, reply) => {
8208
8733
  const availableModels = registry.getAvailable();
8209
8734
  const availableSet = new Set(
8210
8735
  availableModels.map((m) => `${m.provider}:${m.id}`)
8211
8736
  );
8212
- const models = registry.getAll().map((m) => ({
8737
+ const allModels = configuredModelSet.size > 0 ? registry.getAll().filter((m) => configuredModelSet.has(`${m.provider}:${m.id}`)) : registry.getAll();
8738
+ const models = allModels.map((m) => ({
8213
8739
  provider: m.provider,
8214
8740
  id: m.id,
8215
8741
  label: m.label ?? m.id,
8216
8742
  // Keep this endpoint cheap: it is fetched on chat mount, so it must never
8217
8743
  // block workspace load on deep provider auth resolution. ModelRegistry's
8218
- // available set is already derived from configured auth sources.
8744
+ // available set is already derived from configured auth sources. When
8745
+ // hosts configure launch/custom providers, those configured models are an
8746
+ // allowlist: do not leak the built-in registry's unavailable catalog.
8219
8747
  available: availableSet.has(`${m.provider}:${m.id}`)
8220
8748
  }));
8221
8749
  models.sort((a, b) => {
@@ -8684,12 +9212,13 @@ function statusCodeFromError(err) {
8684
9212
  }
8685
9213
  const parsedCode = ErrorCode.safeParse(err?.code);
8686
9214
  if (parsedCode.success && parsedCode.data === ErrorCode.enum.SESSION_NOT_FOUND) return 404;
9215
+ if (parsedCode.success && parsedCode.data === ErrorCode.enum.PAYMENT_REQUIRED) return 402;
8687
9216
  return 500;
8688
9217
  }
8689
9218
 
8690
9219
  // src/server/http/routes/systemPrompt.ts
8691
9220
  function systemPromptRoutes(app, opts, done) {
8692
- async function resolveHarness(request) {
9221
+ async function resolveHarness2(request) {
8693
9222
  if (opts.getHarness) return await opts.getHarness(request);
8694
9223
  if (opts.harness) return opts.harness;
8695
9224
  throw new Error("system prompt route requires harness or getHarness");
@@ -8708,7 +9237,7 @@ function systemPromptRoutes(app, opts, done) {
8708
9237
  }
8709
9238
  });
8710
9239
  }
8711
- const harness = await resolveHarness(request);
9240
+ const harness = await resolveHarness2(request);
8712
9241
  if (typeof harness.getSystemPrompt !== "function") {
8713
9242
  return reply.code(501).send({
8714
9243
  error: {
@@ -8866,14 +9395,24 @@ function normalizeSessionId(value, fallback) {
8866
9395
  const trimmed = value.trim();
8867
9396
  return trimmed.length > 0 ? trimmed : fallback;
8868
9397
  }
9398
+ function resolveHarness(opts, request) {
9399
+ return "getHarness" in opts ? opts.getHarness(request) : opts.harness;
9400
+ }
9401
+ function resolveWorkdir(opts, request) {
9402
+ return "getWorkdir" in opts ? opts.getWorkdir(request) : opts.workdir;
9403
+ }
8869
9404
  function commandsRoutes(app, opts, done) {
8870
9405
  app.get("/api/v1/agent/commands", async (request, reply) => {
8871
9406
  try {
8872
9407
  const query = request.query;
8873
9408
  const sessionId = normalizeSessionId(query.sessionId, opts.defaultSessionId);
8874
- const commands = await opts.harness.getSlashCommands?.(sessionId, {
9409
+ const [harness, workdir] = await Promise.all([
9410
+ resolveHarness(opts, request),
9411
+ resolveWorkdir(opts, request)
9412
+ ]);
9413
+ const commands = await harness.getSlashCommands?.(sessionId, {
8875
9414
  abortSignal: new AbortController().signal,
8876
- workdir: opts.workdir
9415
+ workdir
8877
9416
  }) ?? [];
8878
9417
  return reply.code(200).send({ commands });
8879
9418
  } catch (error) {
@@ -8882,19 +9421,23 @@ function commandsRoutes(app, opts, done) {
8882
9421
  }
8883
9422
  });
8884
9423
  app.post("/api/v1/agent/commands/execute", async (request, reply) => {
8885
- if (!opts.harness.executeSlashCommand) {
8886
- return reply.code(501).send({ error: "Command execution not supported by this harness." });
8887
- }
8888
9424
  try {
9425
+ const [harness, workdir] = await Promise.all([
9426
+ resolveHarness(opts, request),
9427
+ resolveWorkdir(opts, request)
9428
+ ]);
9429
+ if (!harness.executeSlashCommand) {
9430
+ return reply.code(501).send({ error: "Command execution not supported by this harness." });
9431
+ }
8889
9432
  const query = request.query;
8890
9433
  const body = request.body && typeof request.body === "object" ? request.body : {};
8891
9434
  const sessionId = normalizeSessionId(query.sessionId, opts.defaultSessionId);
8892
9435
  const name = typeof body.name === "string" ? body.name.trim() : "";
8893
9436
  const args = typeof body.args === "string" ? body.args : "";
8894
9437
  if (!name) return reply.code(400).send({ error: "name body field is required" });
8895
- await opts.harness.executeSlashCommand(sessionId, name, args, {
9438
+ await harness.executeSlashCommand(sessionId, name, args, {
8896
9439
  abortSignal: new AbortController().signal,
8897
- workdir: opts.workdir
9440
+ workdir
8898
9441
  });
8899
9442
  return reply.code(200).send({ ok: true });
8900
9443
  } catch (error) {
@@ -9129,9 +9672,10 @@ function gitRoutes(app, opts, done) {
9129
9672
  if (path4 === null) return;
9130
9673
  const workspaceRoot = await resolveWorkspaceRoot(request);
9131
9674
  if (!workspaceRoot) {
9132
- return reply.code(500).send({
9133
- error: { code: ERROR_CODE_INTERNAL, message: "workspace root unavailable" }
9134
- });
9675
+ return {
9676
+ enabled: false,
9677
+ reason: "Git file URLs are unavailable for this runtime."
9678
+ };
9135
9679
  }
9136
9680
  try {
9137
9681
  return await resolveGitFileUrl(workspaceRoot, path4);
@@ -9523,7 +10067,9 @@ var PiChatEventMapper = class {
9523
10067
  const mapped = [
9524
10068
  ...this.mapAgentEndFinalAssistant(event, turnId),
9525
10069
  ...this.mapAgentEndError(event, turnId, status),
9526
- this.event({ type: "agent-end", turnId, status })
10070
+ // willRetry marks a non-terminal end (auto-retry coming) so once-per-settle
10071
+ // consumers can ignore it; mirrors mapAgentEndError's own willRetry gate.
10072
+ this.event({ type: "agent-end", turnId, status, ...event.willRetry === true ? { willRetry: true } : {} })
9527
10073
  ];
9528
10074
  this.activeAssistantMessageId = void 0;
9529
10075
  this.toolCallMessageIds.clear();
@@ -10222,6 +10768,454 @@ function messageCreatedAtMs(message) {
10222
10768
  return Number.isFinite(timestamp) ? timestamp : void 0;
10223
10769
  }
10224
10770
 
10771
+ // src/server/pi-chat/metering.ts
10772
+ var meteringLogger = createLogger("pi-chat-metering");
10773
+ var defaultMeteringErrorLogger = (message, error) => {
10774
+ meteringLogger.warn(message, { error });
10775
+ };
10776
+ function promptRunId(sessionId, clientNonce) {
10777
+ return `pi-run:${sessionId}:prompt:${clientNonce}`;
10778
+ }
10779
+ function followUpRunId(sessionId, clientNonce, clientSeq) {
10780
+ return `pi-run:${sessionId}:followup:${clientNonce}:${clientSeq}`;
10781
+ }
10782
+ function followUpMatches(run2, selector) {
10783
+ const fu = run2.followUp;
10784
+ if (!fu) return false;
10785
+ if (selector.clientNonce !== void 0) return fu.clientNonce === selector.clientNonce;
10786
+ if (selector.clientSeq !== void 0) return fu.clientSeq === selector.clientSeq;
10787
+ return false;
10788
+ }
10789
+ function takeQueuedFollowUp(state, selector) {
10790
+ const index = state.queued.findIndex((run2) => followUpMatches(run2, selector));
10791
+ if (index < 0) return void 0;
10792
+ return state.queued.splice(index, 1)[0];
10793
+ }
10794
+ function isRecord3(value) {
10795
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10796
+ }
10797
+ function readTokenCount(value) {
10798
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
10799
+ }
10800
+ function normalizeMeteringUsage(value) {
10801
+ if (!isRecord3(value)) return void 0;
10802
+ const cost = isRecord3(value.cost) ? value.cost : {};
10803
+ return {
10804
+ input: readTokenCount(value.input),
10805
+ output: readTokenCount(value.output),
10806
+ cacheRead: readTokenCount(value.cacheRead),
10807
+ cacheWrite: readTokenCount(value.cacheWrite),
10808
+ cost: {
10809
+ input: readTokenCount(cost.input),
10810
+ output: readTokenCount(cost.output),
10811
+ cacheRead: readTokenCount(cost.cacheRead),
10812
+ cacheWrite: readTokenCount(cost.cacheWrite),
10813
+ total: readTokenCount(cost.total)
10814
+ }
10815
+ };
10816
+ }
10817
+ var PiChatMeteringCoordinator = class {
10818
+ sessions = /* @__PURE__ */ new Map();
10819
+ inflightOps = /* @__PURE__ */ new Set();
10820
+ runInstances = 0;
10821
+ sink;
10822
+ logError;
10823
+ constructor(sink, logError) {
10824
+ this.sink = sink;
10825
+ this.logError = logError ?? defaultMeteringErrorLogger;
10826
+ }
10827
+ /** Reserve a prompt run. Throws (fail closed) when the sink rejects. */
10828
+ async reservePrompt(input) {
10829
+ const state = this.sessionState(input.sessionId);
10830
+ const runId = promptRunId(input.sessionId, input.clientNonce);
10831
+ const existing = this.findRun(state, runId);
10832
+ if (existing) {
10833
+ await existing.reservation;
10834
+ return "duplicate";
10835
+ }
10836
+ const run2 = this.createRun(input, "prompt", runId);
10837
+ state.pendingPrompts.push(run2);
10838
+ return this.materializeReservation(state, run2, input);
10839
+ }
10840
+ /** Reserve a follow-up run. Throws (fail closed) when the sink rejects.
10841
+ * Returns 'duplicate' when this selector already has a tracked/consumed run. */
10842
+ async reserveFollowUp(input) {
10843
+ const state = this.sessionState(input.sessionId);
10844
+ const runId = followUpRunId(input.sessionId, input.clientNonce, input.clientSeq);
10845
+ const existing = this.findRun(state, runId);
10846
+ if (existing) {
10847
+ await existing.reservation;
10848
+ return "duplicate";
10849
+ }
10850
+ if (state.consumedFollowUpNonces.has(input.clientNonce)) return "duplicate";
10851
+ const run2 = this.createRun(input, "followup", runId);
10852
+ run2.followUp = { clientNonce: input.clientNonce, clientSeq: input.clientSeq };
10853
+ state.queued.push(run2);
10854
+ return this.materializeReservation(state, run2, input);
10855
+ }
10856
+ /**
10857
+ * Acquire the reservation for a freshly-registered run. The reserve is put on
10858
+ * the run's ops chain so a concurrent release/settle is ordered strictly
10859
+ * after the reservation row exists. Returns 'cancelled' (skip execution) when
10860
+ * a concurrent stop/interrupt/delete terminated the run while the reserve was
10861
+ * in flight; throws (fail closed) when the sink rejects.
10862
+ */
10863
+ async materializeReservation(state, run2, input) {
10864
+ run2.reservation = this.applyReservation(run2, input);
10865
+ const reserveOp = run2.reservation.catch(() => {
10866
+ });
10867
+ run2.ops = reserveOp;
10868
+ this.inflightOps.add(reserveOp);
10869
+ void reserveOp.finally(() => this.inflightOps.delete(reserveOp));
10870
+ try {
10871
+ await run2.reservation;
10872
+ } catch (err) {
10873
+ this.removeReservingRun(state, run2, input.sessionId);
10874
+ throw err;
10875
+ }
10876
+ return run2.terminal ? "cancelled" : "created";
10877
+ }
10878
+ /** True while a non-terminal prompt run exists for this nonce (accept →
10879
+ * agent-end). Used to suppress duplicate-nonce execution for the full run
10880
+ * lifetime, not just until the user message-start is consumed. */
10881
+ hasPromptRun(sessionId, clientNonce) {
10882
+ const state = this.sessions.get(sessionId);
10883
+ if (!state) return false;
10884
+ const run2 = this.findRun(state, promptRunId(sessionId, clientNonce));
10885
+ return run2 !== void 0 && !run2.terminal;
10886
+ }
10887
+ /** True while a non-terminal follow-up run exists for this selector (queued
10888
+ * or consumed-and-active). Used to suppress duplicate follow-up enqueues. */
10889
+ hasFollowUpRun(sessionId, selector) {
10890
+ const state = this.sessions.get(sessionId);
10891
+ if (!state) return false;
10892
+ if (selector.clientNonce !== void 0 && state.consumedFollowUpNonces.has(selector.clientNonce)) return true;
10893
+ if (state.queued.some((run2) => followUpMatches(run2, selector))) return true;
10894
+ const active = state.active;
10895
+ return active !== void 0 && !active.terminal && active.followUp !== void 0 && followUpMatches(active, selector);
10896
+ }
10897
+ /** The accepted prompt failed before/without running (sync throw or run rejection). */
10898
+ failPromptRun(sessionId, clientNonce) {
10899
+ const state = this.sessions.get(sessionId);
10900
+ if (!state) return;
10901
+ const runId = promptRunId(sessionId, clientNonce);
10902
+ const pendingIndex = state.pendingPrompts.findIndex((run2) => run2.scope.runId === runId);
10903
+ if (pendingIndex >= 0) {
10904
+ const [run2] = state.pendingPrompts.splice(pendingIndex, 1);
10905
+ if (run2) this.finishRun(run2, "error");
10906
+ } else if (state.active?.scope.runId === runId) {
10907
+ this.finishRun(state.active, "error");
10908
+ state.active = void 0;
10909
+ }
10910
+ this.pruneSession(sessionId, state);
10911
+ }
10912
+ /** A queued follow-up was rejected by the adapter before being queued. */
10913
+ failFollowUpRun(sessionId, selector) {
10914
+ const state = this.sessions.get(sessionId);
10915
+ if (!state) return;
10916
+ const run2 = takeQueuedFollowUp(state, selector);
10917
+ if (!run2) return;
10918
+ this.release(run2, "run-rejected");
10919
+ this.pruneSession(sessionId, state);
10920
+ }
10921
+ /**
10922
+ * A queued follow-up is being re-posted as a plain prompt (interrupt
10923
+ * fallback for runtimes without continueQueuedFollowUp). No
10924
+ * `followup-consumed` event will arrive, so bind its reservation to the
10925
+ * next agent-start instead.
10926
+ */
10927
+ promoteQueuedToPrompt(sessionId, selector) {
10928
+ const state = this.sessions.get(sessionId);
10929
+ if (!state) return;
10930
+ const run2 = takeQueuedFollowUp(state, selector);
10931
+ if (!run2) return;
10932
+ state.pendingPrompts.push(run2);
10933
+ }
10934
+ /**
10935
+ * A promoted-to-prompt follow-up failed before agent-start (the fallback
10936
+ * repost rejected). Release its reservation instead of stranding it in
10937
+ * pendingPrompts, where a later agent-start would otherwise misattribute
10938
+ * usage to it.
10939
+ */
10940
+ failPromotedFollowUp(sessionId, selector) {
10941
+ const state = this.sessions.get(sessionId);
10942
+ if (!state || selector.clientNonce === void 0 || selector.clientSeq === void 0) return;
10943
+ const runId = followUpRunId(sessionId, selector.clientNonce, selector.clientSeq);
10944
+ const index = state.pendingPrompts.findIndex((run3) => run3.scope.runId === runId);
10945
+ if (index < 0) return;
10946
+ const [run2] = state.pendingPrompts.splice(index, 1);
10947
+ if (run2) this.release(run2, "run-rejected");
10948
+ this.pruneSession(sessionId, state);
10949
+ }
10950
+ /**
10951
+ * Release prompt runs reserved but not yet bound to an agent-start —
10952
+ * e.g. a stop/interrupt landing in the window between acceptance and the
10953
+ * native agent_start. Without this they would sit `active` in the store
10954
+ * until their TTL, holding the user's balance. No charge.
10955
+ */
10956
+ releasePending(sessionId) {
10957
+ const state = this.sessions.get(sessionId);
10958
+ if (!state) return;
10959
+ for (const run2 of state.pendingPrompts) this.release(run2, "cancelled");
10960
+ state.pendingPrompts = [];
10961
+ this.pruneSession(sessionId, state);
10962
+ }
10963
+ /** Queue cleared via selector or entirely; release affected reservations. */
10964
+ releaseQueued(sessionId, selector) {
10965
+ const state = this.sessions.get(sessionId);
10966
+ if (!state) return;
10967
+ if (selector) {
10968
+ const run2 = takeQueuedFollowUp(state, selector);
10969
+ if (run2) this.release(run2, "queue-cleared");
10970
+ } else {
10971
+ for (const run2 of state.queued) this.release(run2, "queue-cleared");
10972
+ state.queued = [];
10973
+ }
10974
+ this.pruneSession(sessionId, state);
10975
+ }
10976
+ /** Session deleted: tear down every non-terminal run without charging. */
10977
+ releaseSession(sessionId) {
10978
+ const state = this.sessions.get(sessionId);
10979
+ if (!state) return;
10980
+ for (const run2 of state.queued) this.release(run2, "cancelled");
10981
+ for (const run2 of state.pendingPrompts) this.release(run2, "cancelled");
10982
+ if (state.active) this.finishRun(state.active, "aborted");
10983
+ this.sessions.delete(sessionId);
10984
+ }
10985
+ /**
10986
+ * Feed one native adapter event and its mapped PiChatEvents through the
10987
+ * correlation state machine. Must be called after the mapped events were
10988
+ * published (the mapper assigns turn ids during mapping).
10989
+ */
10990
+ observe(sessionId, nativeEvent, mappedEvents) {
10991
+ const state = this.sessions.get(sessionId);
10992
+ if (!state) return;
10993
+ for (const event of mappedEvents) {
10994
+ switch (event.type) {
10995
+ case "agent-start": {
10996
+ let next = state.pendingPrompts.shift();
10997
+ while (next && next.terminal) next = state.pendingPrompts.shift();
10998
+ if (next) {
10999
+ if (state.active && !state.active.terminal) this.finishRun(state.active, "ok");
11000
+ state.active = next;
11001
+ }
11002
+ break;
11003
+ }
11004
+ case "message-start": {
11005
+ if (event.role === "user" && event.clientSeq !== void 0) {
11006
+ this.consumeFollowUp(state, event);
11007
+ }
11008
+ break;
11009
+ }
11010
+ case "followup-consumed": {
11011
+ this.consumeFollowUp(state, event);
11012
+ break;
11013
+ }
11014
+ case "auto-retry-start": {
11015
+ if (state.active) {
11016
+ state.active.recordedMessageIds.clear();
11017
+ state.active.lastIdlessUsageKey = void 0;
11018
+ }
11019
+ break;
11020
+ }
11021
+ case "agent-end": {
11022
+ this.harvestAgentEndUsage(state, nativeEvent);
11023
+ if (isRecord3(nativeEvent) && nativeEvent.willRetry === true) break;
11024
+ if (state.active && !state.active.terminal) this.finishRun(state.active, event.status);
11025
+ state.active = void 0;
11026
+ break;
11027
+ }
11028
+ default:
11029
+ break;
11030
+ }
11031
+ }
11032
+ this.observeMessageEndUsage(state, nativeEvent);
11033
+ this.pruneSession(sessionId, state);
11034
+ }
11035
+ /** Promote a queued follow-up to the active run, settling the previous one. */
11036
+ consumeFollowUp(state, selector) {
11037
+ const run2 = takeQueuedFollowUp(state, selector);
11038
+ if (!run2) return;
11039
+ if (run2.followUp?.clientNonce) state.consumedFollowUpNonces.add(run2.followUp.clientNonce);
11040
+ if (state.active && !state.active.terminal) this.finishRun(state.active, "ok");
11041
+ state.active = run2;
11042
+ }
11043
+ /** Test/diagnostic hook: resolves after every queued sink call settles. */
11044
+ async flush() {
11045
+ while (this.inflightOps.size > 0) {
11046
+ await Promise.all([...this.inflightOps]);
11047
+ }
11048
+ }
11049
+ observeMessageEndUsage(state, nativeEvent) {
11050
+ if (!isRecord3(nativeEvent) || nativeEvent.type !== "message_end") return;
11051
+ this.recordAssistantUsage(state, nativeEvent.message);
11052
+ }
11053
+ /**
11054
+ * Some failure/abort paths never emit message_end; the final assistant
11055
+ * message (and its usage) only rides on agent_end's messages array. Runs
11056
+ * with already-recorded usage dedupe via recordedMessageIds.
11057
+ */
11058
+ harvestAgentEndUsage(state, nativeEvent) {
11059
+ if (!isRecord3(nativeEvent) || !Array.isArray(nativeEvent.messages)) return;
11060
+ for (let index = nativeEvent.messages.length - 1; index >= 0; index -= 1) {
11061
+ const message = nativeEvent.messages[index];
11062
+ if (!isRecord3(message) || message.role !== "assistant") continue;
11063
+ this.recordAssistantUsage(state, message, { isAgentEndFinal: true });
11064
+ return;
11065
+ }
11066
+ }
11067
+ recordAssistantUsage(state, message, opts = {}) {
11068
+ if (!isRecord3(message) || message.role !== "assistant") return;
11069
+ const usage = normalizeMeteringUsage(message.usage);
11070
+ if (!usage) return;
11071
+ const run2 = state.active ?? state.pendingPrompts[0];
11072
+ if (!run2 || run2.terminal) {
11073
+ this.logError("assistant usage arrived with no reserved run; usage not metered", { messageRole: "assistant" });
11074
+ return;
11075
+ }
11076
+ const messageId = typeof message.id === "string" && message.id.length > 0 ? message.id : void 0;
11077
+ if (messageId) {
11078
+ if (run2.recordedMessageIds.has(messageId)) return;
11079
+ run2.recordedMessageIds.add(messageId);
11080
+ } else {
11081
+ const stopReason2 = typeof message.stopReason === "string" ? message.stopReason : "";
11082
+ const signature = `sig:${usage.input}:${usage.output}:${usage.cacheRead}:${usage.cacheWrite}:${usage.cost.total}:${stopReason2}`;
11083
+ if (opts.isAgentEndFinal && signature === run2.lastIdlessUsageKey) return;
11084
+ run2.lastIdlessUsageKey = signature;
11085
+ }
11086
+ run2.usageCount += 1;
11087
+ const model = typeof message.model === "string" && message.model.length > 0 ? message.model : void 0;
11088
+ const provider = typeof message.provider === "string" && message.provider.length > 0 ? message.provider : void 0;
11089
+ const stopReason = typeof message.stopReason === "string" ? message.stopReason : void 0;
11090
+ const usageId = messageId ? `pi-usage:${run2.scope.sessionId}:message:${messageId}` : run2.reservationId ? `pi-usage:reservation:${run2.reservationId}:${run2.usageCount}` : `pi-usage:${run2.scope.runId}:${run2.instanceId}:${run2.usageCount}`;
11091
+ this.enqueue(
11092
+ run2,
11093
+ async () => {
11094
+ try {
11095
+ const result = await this.sink.recordUsage({
11096
+ ...run2.scope,
11097
+ reservationId: run2.reservationId,
11098
+ usageId,
11099
+ messageId,
11100
+ model: model || provider ? { provider, id: model } : void 0,
11101
+ usage,
11102
+ stopReason
11103
+ });
11104
+ if (result.billedMicros > 0) run2.billableUsageCount += 1;
11105
+ } catch (error) {
11106
+ const couldBill = usage.input + usage.output + usage.cacheRead + usage.cacheWrite > 0 || usage.cost.total > 0;
11107
+ if (couldBill) run2.usageWriteFailed = true;
11108
+ throw error;
11109
+ }
11110
+ },
11111
+ "recordUsage failed"
11112
+ );
11113
+ }
11114
+ /** Build a run synchronously (no await) so it can be registered before the
11115
+ * reserve sink call, closing the concurrent-duplicate race. */
11116
+ createRun(input, kind, runId) {
11117
+ return {
11118
+ scope: {
11119
+ workspaceId: input.workspaceId,
11120
+ userId: input.userId,
11121
+ sessionId: input.sessionId,
11122
+ runId,
11123
+ source: "pi-chat"
11124
+ },
11125
+ kind,
11126
+ reservationId: void 0,
11127
+ instanceId: this.runInstances += 1,
11128
+ usageCount: 0,
11129
+ billableUsageCount: 0,
11130
+ recordedMessageIds: /* @__PURE__ */ new Set(),
11131
+ lastIdlessUsageKey: void 0,
11132
+ usageWriteFailed: false,
11133
+ reservation: Promise.resolve(),
11134
+ terminal: false,
11135
+ ops: Promise.resolve()
11136
+ };
11137
+ }
11138
+ /** Acquire the host reservation; throws (fail closed) on a rejecting sink. */
11139
+ async applyReservation(run2, input) {
11140
+ const result = await this.sink.reserveRun({
11141
+ ...run2.scope,
11142
+ kind: run2.kind,
11143
+ message: input.message,
11144
+ model: input.model
11145
+ });
11146
+ run2.reservationId = result?.reservationId;
11147
+ }
11148
+ /** Remove a still-reserving run whose reservation failed, from either list. */
11149
+ removeReservingRun(state, run2, sessionId) {
11150
+ const pendingIndex = state.pendingPrompts.indexOf(run2);
11151
+ if (pendingIndex >= 0) state.pendingPrompts.splice(pendingIndex, 1);
11152
+ const queuedIndex = state.queued.indexOf(run2);
11153
+ if (queuedIndex >= 0) state.queued.splice(queuedIndex, 1);
11154
+ this.pruneSession(sessionId, state);
11155
+ }
11156
+ findRun(state, runId) {
11157
+ if (state.active && !state.active.terminal && state.active.scope.runId === runId) return state.active;
11158
+ const pending = state.pendingPrompts.find((run2) => run2.scope.runId === runId);
11159
+ if (pending) return pending;
11160
+ return state.queued.find((run2) => run2.scope.runId === runId);
11161
+ }
11162
+ finishRun(run2, status) {
11163
+ if (run2.terminal) return;
11164
+ run2.terminal = true;
11165
+ this.enqueue(
11166
+ run2,
11167
+ () => {
11168
+ if (run2.usageWriteFailed) {
11169
+ return this.sink.releaseRun({ ...run2.scope, reservationId: run2.reservationId, reason: "usage-write-failed" });
11170
+ }
11171
+ if (run2.billableUsageCount > 0) {
11172
+ return this.sink.settleRun({ ...run2.scope, reservationId: run2.reservationId, status });
11173
+ }
11174
+ const didPaidWork = status === "ok";
11175
+ if (didPaidWork) {
11176
+ return this.sink.releaseRun({ ...run2.scope, reservationId: run2.reservationId, reason: "fallback-hold-charge" });
11177
+ }
11178
+ return this.sink.releaseRun({
11179
+ ...run2.scope,
11180
+ reservationId: run2.reservationId,
11181
+ reason: status === "error" ? "error-before-usage" : "cancelled"
11182
+ });
11183
+ },
11184
+ "finishRun failed"
11185
+ );
11186
+ }
11187
+ release(run2, reason) {
11188
+ if (run2.terminal) return;
11189
+ run2.terminal = true;
11190
+ this.enqueue(
11191
+ run2,
11192
+ () => this.sink.releaseRun({ ...run2.scope, reservationId: run2.reservationId, reason }),
11193
+ "releaseRun failed"
11194
+ );
11195
+ }
11196
+ enqueue(run2, op, failureMessage) {
11197
+ const chained = run2.ops.then(op).catch((error) => {
11198
+ this.logError(`${failureMessage} (run ${run2.scope.runId})`, error);
11199
+ });
11200
+ run2.ops = chained;
11201
+ this.inflightOps.add(chained);
11202
+ void chained.finally(() => this.inflightOps.delete(chained));
11203
+ }
11204
+ sessionState(sessionId) {
11205
+ let state = this.sessions.get(sessionId);
11206
+ if (!state) {
11207
+ state = { pendingPrompts: [], queued: [], consumedFollowUpNonces: /* @__PURE__ */ new Set() };
11208
+ this.sessions.set(sessionId, state);
11209
+ }
11210
+ return state;
11211
+ }
11212
+ pruneSession(sessionId, state) {
11213
+ if (!state.active && state.pendingPrompts.length === 0 && state.queued.length === 0 && state.consumedFollowUpNonces.size === 0) {
11214
+ this.sessions.delete(sessionId);
11215
+ }
11216
+ }
11217
+ };
11218
+
10225
11219
  // src/server/pi-chat/harnessPiChatService.ts
10226
11220
  var HarnessPiChatService = class {
10227
11221
  harness;
@@ -10237,10 +11231,16 @@ var HarnessPiChatService = class {
10237
11231
  activePromptRuns = /* @__PURE__ */ new Map();
10238
11232
  syntheticPromptFailures = /* @__PURE__ */ new Map();
10239
11233
  activeSyntheticPromptErrors = /* @__PURE__ */ new Map();
11234
+ metering;
10240
11235
  constructor(options) {
10241
11236
  this.harness = options.harness;
10242
11237
  this.sessionStore = options.sessionStore;
10243
11238
  this.workdir = options.workdir;
11239
+ this.metering = options.metering ? new PiChatMeteringCoordinator(options.metering, options.meteringLogger) : void 0;
11240
+ }
11241
+ /** Test/diagnostic hook: resolves once queued metering sink calls settle. */
11242
+ async flushMetering() {
11243
+ await this.metering?.flush();
10244
11244
  }
10245
11245
  async listSessions(ctx, options) {
10246
11246
  return this.sessionStore.list(toSessionCtx(ctx), options);
@@ -10249,8 +11249,16 @@ var HarnessPiChatService = class {
10249
11249
  return this.sessionStore.create(toSessionCtx(ctx), init);
10250
11250
  }
10251
11251
  async deleteSession(ctx, sessionId) {
10252
- this.channels.get(sessionId)?.unsubscribe();
11252
+ const channel = this.channels.get(sessionId);
11253
+ if (channel) {
11254
+ const activeRun = this.activePromptRuns.get(sessionId);
11255
+ await channel.adapter.abort();
11256
+ await activeRun?.catch(() => {
11257
+ });
11258
+ }
11259
+ channel?.unsubscribe();
10253
11260
  this.channels.delete(sessionId);
11261
+ this.metering?.releaseSession(sessionId);
10254
11262
  this.messageMetadata.clearSession(sessionId);
10255
11263
  this.syntheticPromptFailures.delete(sessionId);
10256
11264
  this.activeSyntheticPromptErrors.delete(sessionId);
@@ -10300,16 +11308,35 @@ var HarnessPiChatService = class {
10300
11308
  async prompt(ctx, sessionId, payload) {
10301
11309
  const adapter = await this.getAdapter(ctx, sessionId, payload);
10302
11310
  await this.ensureChannel(ctx, sessionId, adapter);
11311
+ const outcome = await this.metering?.reservePrompt({
11312
+ workspaceId: ctx.workspaceId,
11313
+ userId: ctx.authSubject,
11314
+ sessionId,
11315
+ clientNonce: payload.clientNonce,
11316
+ message: payload.message,
11317
+ model: payload.model
11318
+ }) ?? "created";
11319
+ if (outcome === "duplicate") {
11320
+ return {
11321
+ accepted: true,
11322
+ cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0,
11323
+ clientNonce: payload.clientNonce,
11324
+ duplicate: true
11325
+ };
11326
+ }
11327
+ if (outcome === "cancelled") throw promptCancelledError();
10303
11328
  this.messageMetadata.recordPrompt(sessionId, payload);
10304
11329
  const channel = this.channels.get(sessionId);
10305
11330
  const receiptCursor = nextPromptReceiptCursor(channel);
10306
11331
  try {
10307
11332
  const run2 = this.trackActiveRun(sessionId, adapter.prompt(toPiPromptInput(payload)));
10308
11333
  run2.catch((error) => {
11334
+ this.metering?.failPromptRun(sessionId, payload.clientNonce);
10309
11335
  if (!this.messageMetadata.hasPrompt(sessionId, { clientNonce: payload.clientNonce, displayText: payload.displayMessage ?? payload.message })) return;
10310
11336
  this.publishPromptRunError(sessionId, channel, payload, error);
10311
11337
  });
10312
11338
  } catch (err) {
11339
+ this.metering?.failPromptRun(sessionId, payload.clientNonce);
10313
11340
  this.messageMetadata.removePrompt(sessionId, { clientNonce: payload.clientNonce });
10314
11341
  throw err;
10315
11342
  }
@@ -10318,6 +11345,25 @@ var HarnessPiChatService = class {
10318
11345
  async followUp(ctx, sessionId, payload) {
10319
11346
  const adapter = await this.getAdapter(ctx, sessionId, payload.message);
10320
11347
  await this.ensureChannel(ctx, sessionId, adapter);
11348
+ const outcome = await this.metering?.reserveFollowUp({
11349
+ workspaceId: ctx.workspaceId,
11350
+ userId: ctx.authSubject,
11351
+ sessionId,
11352
+ clientNonce: payload.clientNonce,
11353
+ clientSeq: payload.clientSeq,
11354
+ message: payload.message
11355
+ }) ?? "created";
11356
+ if (outcome === "duplicate") {
11357
+ return {
11358
+ accepted: true,
11359
+ queued: true,
11360
+ cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0,
11361
+ clientNonce: payload.clientNonce,
11362
+ clientSeq: payload.clientSeq,
11363
+ duplicate: true
11364
+ };
11365
+ }
11366
+ if (outcome === "cancelled") throw promptCancelledError();
10321
11367
  this.messageMetadata.recordFollowUp(sessionId, payload);
10322
11368
  try {
10323
11369
  await adapter.followUp(payload.message, {
@@ -10326,6 +11372,7 @@ var HarnessPiChatService = class {
10326
11372
  clientSeq: payload.clientSeq
10327
11373
  });
10328
11374
  } catch (err) {
11375
+ this.metering?.failFollowUpRun(sessionId, payload);
10329
11376
  this.messageMetadata.removeFollowUp(sessionId, payload);
10330
11377
  throw err;
10331
11378
  }
@@ -10337,10 +11384,14 @@ var HarnessPiChatService = class {
10337
11384
  const before = adapter.readSnapshot().followUpMessages.length;
10338
11385
  adapter.clearFollowUp(payload);
10339
11386
  const after = adapter.readSnapshot().followUpMessages.length;
10340
- if (after < before) this.messageMetadata.removeFollowUp(sessionId, payload);
11387
+ if (after < before) {
11388
+ this.messageMetadata.removeFollowUp(sessionId, payload);
11389
+ this.metering?.releaseQueued(sessionId, payload);
11390
+ }
10341
11391
  return { accepted: true, cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0, cleared: Math.max(0, before - after) };
10342
11392
  }
10343
11393
  const clearedQueue = this.clearAllFollowUps(adapter, sessionId);
11394
+ this.metering?.releaseQueued(sessionId);
10344
11395
  return { accepted: true, cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0, cleared: clearedQueue.length };
10345
11396
  }
10346
11397
  async interrupt(ctx, sessionId, _payload) {
@@ -10353,12 +11404,15 @@ var HarnessPiChatService = class {
10353
11404
  if (wasActive) await adapter.abort();
10354
11405
  await activeRun?.catch(() => {
10355
11406
  });
11407
+ this.metering?.releasePending(sessionId);
10356
11408
  if (nextFollowUp) await this.autoPostInterruptedFollowUp(sessionId, adapter, nextFollowUp);
10357
11409
  return { accepted: true, cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0 };
10358
11410
  }
10359
11411
  async stop(ctx, sessionId, _payload) {
10360
11412
  const adapter = await this.getAdapter(ctx, sessionId, "");
10361
11413
  const clearedQueue = this.clearAllFollowUps(adapter, sessionId);
11414
+ this.metering?.releaseQueued(sessionId);
11415
+ this.metering?.releasePending(sessionId);
10362
11416
  await adapter.abort();
10363
11417
  return { accepted: true, stopped: true, cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0, clearedQueue: buildPiChatQueuedFollowUps(sessionId, clearedQueue) };
10364
11418
  }
@@ -10380,13 +11434,24 @@ var HarnessPiChatService = class {
10380
11434
  const metadata = this.messageMetadata.findFollowUpForQueueItem(sessionId, followUp);
10381
11435
  this.messageMetadata.recordConsumingFollowUp(sessionId, followUp, metadata?.serverText);
10382
11436
  if (adapter.continueQueuedFollowUp) {
10383
- await this.trackActiveRun(sessionId, adapter.continueQueuedFollowUp());
11437
+ try {
11438
+ await this.trackActiveRun(sessionId, adapter.continueQueuedFollowUp());
11439
+ } catch (err) {
11440
+ this.metering?.failFollowUpRun(sessionId, followUp);
11441
+ throw err;
11442
+ }
10384
11443
  return;
10385
11444
  }
10386
11445
  if (!this.canClearAutoPostedFollowUpForFallback(adapter, followUp)) {
10387
11446
  throw new AutoPostFollowUpError("Cannot auto-post queued follow-up because this runtime cannot safely remove only the consumed queued item.");
10388
11447
  }
10389
- await this.runPrompt(sessionId, adapter, metadata?.serverText ?? followUp.displayText);
11448
+ this.metering?.promoteQueuedToPrompt(sessionId, followUp);
11449
+ try {
11450
+ await this.runPrompt(sessionId, adapter, metadata?.serverText ?? followUp.displayText);
11451
+ } catch (err) {
11452
+ this.metering?.failPromotedFollowUp(sessionId, followUp);
11453
+ throw err;
11454
+ }
10390
11455
  this.clearAutoPostedFollowUpForFallback(sessionId, adapter, followUp);
10391
11456
  }
10392
11457
  async runPrompt(sessionId, adapter, input) {
@@ -10531,10 +11596,14 @@ var HarnessPiChatService = class {
10531
11596
  const channel = { buffer, adapter, unsubscribe: () => {
10532
11597
  }, mapper, messageTurnIds: /* @__PURE__ */ new Map() };
10533
11598
  const unsubscribe = adapter.subscribe((event) => {
10534
- for (const mapped of mapper.map(event)) {
11599
+ const mappedEvents = mapper.map(event);
11600
+ const enrichedEvents = [];
11601
+ for (const mapped of mappedEvents) {
10535
11602
  const enriched = this.messageMetadata.enrichEvent(sessionId, mapped);
11603
+ enrichedEvents.push(enriched);
10536
11604
  this.publishChannelEvent(sessionId, channel, enriched);
10537
11605
  }
11606
+ this.metering?.observe(sessionId, event, enrichedEvents);
10538
11607
  });
10539
11608
  channel.unsubscribe = unsubscribe;
10540
11609
  this.channels.set(sessionId, channel);
@@ -10543,6 +11612,13 @@ var HarnessPiChatService = class {
10543
11612
  };
10544
11613
  var AutoPostFollowUpError = class extends Error {
10545
11614
  };
11615
+ function promptCancelledError() {
11616
+ return Object.assign(new Error("request cancelled before execution"), {
11617
+ statusCode: 409,
11618
+ code: ErrorCode.enum.ABORTED,
11619
+ retryable: true
11620
+ });
11621
+ }
10546
11622
  function nextPromptReceiptCursor(channel) {
10547
11623
  return (channel?.buffer.latestSeq ?? 0) + 1;
10548
11624
  }
@@ -10675,7 +11751,8 @@ async function createAgentApp(opts = {}) {
10675
11751
  templatePath
10676
11752
  });
10677
11753
  const pluginTools = [];
10678
- if (modeAdapter.workspaceFsCapability === "strong") {
11754
+ const externalPluginsEnabled = opts.externalPlugins !== false;
11755
+ if (externalPluginsEnabled && modeAdapter.workspaceFsCapability === "strong") {
10679
11756
  const pluginResult = await loadPlugins({ cwd: workspaceRoot });
10680
11757
  if (pluginResult.errors.length > 0) {
10681
11758
  for (const e of pluginResult.errors) {
@@ -10706,13 +11783,13 @@ async function createAgentApp(opts = {}) {
10706
11783
  ...opts.disableDefaultFileTools ? [] : buildFilesystemAgentTools(runtimeBundle),
10707
11784
  ...opts.extraTools ?? [],
10708
11785
  ...pluginTools,
10709
- createPluginDiagnosticsTool({
11786
+ ...externalPluginsEnabled ? [createPluginDiagnosticsTool({
10710
11787
  getLastReloadDiagnostics: () => lastReloadDiagnostics,
10711
11788
  getHarness: () => harnessRef,
10712
11789
  ...opts.getPluginDiagnostics ? {
10713
11790
  getPluginErrors: () => opts.getPluginDiagnostics({ workspaceId: sessionId, workspaceRoot })
10714
11791
  } : {}
10715
- })
11792
+ })] : []
10716
11793
  ];
10717
11794
  const harnessFactory = opts.harnessFactory ?? ((input) => createPiCodingAgentHarness({
10718
11795
  ...input,
@@ -10751,11 +11828,17 @@ async function createAgentApp(opts = {}) {
10751
11828
  await app.register(fsEventsRoutes, { workspace: runtimeBundle.workspace });
10752
11829
  await app.register(treeRoutes, { workspace: runtimeBundle.workspace });
10753
11830
  await app.register(searchRoutes, { fileSearch: runtimeBundle.fileSearch });
10754
- await app.register(gitRoutes, { getWorkspaceRoot: () => getRuntimeBundleStorageRoot(runtimeBundle) });
11831
+ await app.register(gitRoutes, {
11832
+ getWorkspaceRoot: () => {
11833
+ if (runtimeBundle.sandbox.provider === "remote-worker") return void 0;
11834
+ return getRuntimeBundleStorageRoot(runtimeBundle);
11835
+ }
11836
+ });
10755
11837
  const piChatService = new HarnessPiChatService({
10756
11838
  harness,
10757
11839
  sessionStore: harness.sessions,
10758
- workdir: runtimeBundle.workspace.root
11840
+ workdir: runtimeBundle.workspace.root,
11841
+ metering: opts.metering
10759
11842
  });
10760
11843
  await app.register(piChatRoutes, { service: piChatService });
10761
11844
  await app.register(systemPromptRoutes, { harness });
@@ -10892,7 +11975,6 @@ function basenameForUpload2(filename) {
10892
11975
  }
10893
11976
  function buildUploadAgentTools(bundle) {
10894
11977
  const { workspace } = bundle;
10895
- const storageRoot = getRuntimeBundleStorageRoot(bundle);
10896
11978
  return [
10897
11979
  {
10898
11980
  name: "upload_file",
@@ -10921,7 +12003,7 @@ function buildUploadAgentTools(bundle) {
10921
12003
  const rawDir = typeof input.directory === "string" ? input.directory.trim() : "";
10922
12004
  const dir = rawDir ? rawDir.replace(/^\.\/+/, "").replace(/\/+$/, "") : DEFAULT_UPLOAD_DIR;
10923
12005
  try {
10924
- const bytes = workspace.readBinaryFile ? Buffer.from(await workspace.readBinaryFile(filePath)) : await readFile12(join14(storageRoot, filePath));
12006
+ const bytes = workspace.readBinaryFile ? Buffer.from(await workspace.readBinaryFile(filePath)) : await readFile12(join14(getRuntimeBundleStorageRoot(bundle), filePath));
10925
12007
  if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES2) {
10926
12008
  return {
10927
12009
  content: [{ type: "text", text: `file must be between 1 byte and ${MAX_UPLOAD_BYTES2} bytes` }],
@@ -10972,9 +12054,13 @@ function pluginNameFromPath(path4) {
10972
12054
  function getAvailableModelProviders() {
10973
12055
  const authStorage = AuthStorage3.create();
10974
12056
  const registry = ModelRegistry3.create(authStorage);
10975
- registerConfiguredModelProviders(registry);
12057
+ const configuredModels = registerConfiguredModelProviders(registry);
12058
+ const configuredModelSet = new Set(
12059
+ configuredModels.map((model) => `${model.provider}:${model.id}`)
12060
+ );
12061
+ const availableModels = configuredModelSet.size > 0 ? registry.getAvailable().filter((model) => configuredModelSet.has(`${model.provider}:${model.id}`)) : registry.getAvailable();
10976
12062
  return Array.from(
10977
- new Set(registry.getAvailable().map((model) => model.provider))
12063
+ new Set(availableModels.map((model) => model.provider))
10978
12064
  ).sort((a, b) => a.localeCompare(b));
10979
12065
  }
10980
12066
  function selectRuntimeModeAdapter(mode, sandboxHandleStore) {
@@ -11075,6 +12161,7 @@ var registerAgentRoutes = async (app, opts) => {
11075
12161
  });
11076
12162
  const requestScopedRuntime = typeof opts.getWorkspaceId === "function" || typeof opts.getWorkspaceRoot === "function" || typeof opts.getTemplatePath === "function" || typeof opts.getPi === "function" || typeof opts.getSessionNamespace === "function" || typeof opts.getSystemPromptDynamic === "function";
11077
12163
  const sessionChangesTracker = new InMemorySessionChangesTracker();
12164
+ const externalPluginsEnabled = opts.externalPlugins !== false;
11078
12165
  const runtimeBindings = /* @__PURE__ */ new Map();
11079
12166
  const MAX_RUNTIME_BINDINGS = 256;
11080
12167
  function evictRuntimeBindings() {
@@ -11258,17 +12345,17 @@ var registerAgentRoutes = async (app, opts) => {
11258
12345
  }),
11259
12346
  ...buildFilesystemAgentTools(runtimeBundle),
11260
12347
  ...buildUploadAgentTools(runtimeBundle),
11261
- createPluginDiagnosticsTool({
12348
+ ...externalPluginsEnabled ? [createPluginDiagnosticsTool({
11262
12349
  // `binding` is assigned later in this function; read through thunks.
11263
12350
  getLastReloadDiagnostics: () => binding?.lastReloadDiagnostics ?? [],
11264
12351
  getHarness: () => binding?.harness,
11265
12352
  ...opts.getPluginDiagnostics ? {
11266
12353
  getPluginErrors: () => opts.getPluginDiagnostics({ workspaceId, workspaceRoot: root })
11267
12354
  } : {}
11268
- })
12355
+ })] : []
11269
12356
  ];
11270
12357
  const pluginTools = [];
11271
- if (modeAdapter.workspaceFsCapability === "strong") {
12358
+ if (externalPluginsEnabled && modeAdapter.workspaceFsCapability === "strong") {
11272
12359
  const pluginResult = await loadPlugins({ cwd: root });
11273
12360
  if (pluginResult.errors.length > 0) {
11274
12361
  for (const e of pluginResult.errors) {
@@ -11527,7 +12614,11 @@ var registerAgentRoutes = async (app, opts) => {
11527
12614
  getFileSearch: async (request) => (await getBindingForRequest(request)).runtimeBundle.fileSearch
11528
12615
  });
11529
12616
  await app.register(gitRoutes, {
11530
- getWorkspaceRoot: async (request) => getRuntimeBundleStorageRoot((await getBindingForRequest(request)).runtimeBundle)
12617
+ getWorkspaceRoot: async (request) => {
12618
+ const bundle = (await getBindingForRequest(request)).runtimeBundle;
12619
+ if (bundle.sandbox.provider === "remote-worker") return void 0;
12620
+ return getRuntimeBundleStorageRoot(bundle);
12621
+ }
11531
12622
  });
11532
12623
  await app.register(piChatRoutes, {
11533
12624
  getService: async (request) => {
@@ -11535,7 +12626,8 @@ var registerAgentRoutes = async (app, opts) => {
11535
12626
  binding.piChatService ??= new HarnessPiChatService({
11536
12627
  harness: binding.harness,
11537
12628
  sessionStore: binding.harness.sessions,
11538
- workdir: binding.runtimeBundle.workspace.root
12629
+ workdir: binding.runtimeBundle.workspace.root,
12630
+ metering: opts.metering
11539
12631
  });
11540
12632
  return binding.piChatService;
11541
12633
  }
@@ -11616,6 +12708,18 @@ var registerAgentRoutes = async (app, opts) => {
11616
12708
  catalogRoutes,
11617
12709
  staticBinding ? { tools: staticBinding.tools } : { getTools: async (request) => (await getBindingForRequest(request)).tools }
11618
12710
  );
12711
+ await app.register(
12712
+ commandsRoutes,
12713
+ staticBinding ? {
12714
+ harness: staticBinding.harness,
12715
+ defaultSessionId: sessionId,
12716
+ workdir: staticBinding.runtimeBundle.workspace.root
12717
+ } : {
12718
+ defaultSessionId: sessionId,
12719
+ getHarness: async (request) => (await getBindingForRequest(request)).harness,
12720
+ getWorkdir: async (request) => (await getBindingForRequest(request)).runtimeBundle.workspace.root
12721
+ }
12722
+ );
11619
12723
  await app.register(
11620
12724
  readyStatusRoutes,
11621
12725
  staticBinding ? { tracker: staticBinding.readyTracker } : { getTracker: async (request) => (await getBindingForRequest(request)).readyTracker }
@@ -11624,10 +12728,17 @@ var registerAgentRoutes = async (app, opts) => {
11624
12728
  export {
11625
12729
  FileHandleStore,
11626
12730
  PI_PACKAGE_RESOURCE_FILTERS,
12731
+ REMOTE_WORKER_PROVIDER,
12732
+ REMOTE_WORKER_RUNTIME_CWD,
12733
+ RemoteWorkerClient,
12734
+ RemoteWorkerClientError,
11627
12735
  UV_SETUP_COMMANDS,
11628
12736
  VERCEL_PROVISIONING_CACHE_ROOT,
11629
12737
  VERCEL_SANDBOX_WORKSPACE_ROOT,
11630
12738
  UV_SETUP_COMMANDS as VERCEL_UV_SETUP_COMMANDS,
12739
+ WORKER_INTERNAL_TOKEN_HEADER,
12740
+ WORKER_REQUEST_ID_HEADER,
12741
+ WORKER_WORKSPACE_ID_HEADER,
11631
12742
  applyCspHeaders,
11632
12743
  autoDetectMode,
11633
12744
  bakeSnapshotIfNeeded,
@@ -11635,21 +12746,28 @@ export {
11635
12746
  buildPackageHash,
11636
12747
  buildSnapshotRecipeHash,
11637
12748
  compactPiPackages,
12749
+ constantTimeTokenEqual,
11638
12750
  createAgentApp,
11639
12751
  createBwrapSandbox,
11640
12752
  createDirectSandbox,
11641
12753
  createLogger,
11642
12754
  createNodeWorkspace,
12755
+ createRemoteWorkerModeAdapter,
12756
+ createRemoteWorkerSandbox,
12757
+ createRemoteWorkerWorkspace,
11643
12758
  createResourceSettingsManager,
11644
12759
  createVercelDeploymentSnapshotProvider,
11645
12760
  createVercelProvisioningAdapter,
11646
12761
  createVercelSandboxWorkspace,
12762
+ decodeBytesFromWorker,
12763
+ encodeBytesForWorker,
11647
12764
  fileRoutes,
11648
12765
  getBoringAgentPathEntries,
11649
12766
  getBoringAgentRuntimeEnv,
11650
12767
  getBoringAgentRuntimePaths,
11651
12768
  hasBwrap,
11652
12769
  mergePiPackageSources,
12770
+ normalizeMeteringUsage,
11653
12771
  piPackageSourceKey,
11654
12772
  prepareDeploymentSnapshot,
11655
12773
  prepareVercelDeploymentSnapshot,