@hachej/boring-agent 0.1.42 → 0.1.43

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-YUDMSYAO.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, {
@@ -1817,6 +1857,492 @@ async function prepareVercelDeploymentSnapshot(opts) {
1817
1857
  });
1818
1858
  }
1819
1859
 
1860
+ // src/server/runtime/createServerFileSearch.ts
1861
+ var DEFAULT_LIMIT = 500;
1862
+ var MAX_LIMIT = 5e3;
1863
+ var SEARCH_TIMEOUT_MS = 5e3;
1864
+ var SEARCH_MAX_OUTPUT_BYTES = 256e3;
1865
+ function shellQuote2(value) {
1866
+ return `'${value.replaceAll("'", "'\\''")}'`;
1867
+ }
1868
+ function normalizeLimit(limit) {
1869
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
1870
+ return DEFAULT_LIMIT;
1871
+ }
1872
+ const normalized = Math.trunc(limit);
1873
+ if (normalized <= 0) return DEFAULT_LIMIT;
1874
+ return Math.min(normalized, MAX_LIMIT);
1875
+ }
1876
+ function buildFindArgs(glob) {
1877
+ const isPathShaped = glob.includes("/") || glob.includes("**");
1878
+ if (!isPathShaped) {
1879
+ return `-iname ${shellQuote2(glob)}`;
1880
+ }
1881
+ let translated = glob.replaceAll("**", "*");
1882
+ translated = translated.replace(/^\/+/, "");
1883
+ if (!translated.startsWith("*")) {
1884
+ translated = `*${translated}`;
1885
+ }
1886
+ return `-ipath ${shellQuote2(translated)}`;
1887
+ }
1888
+ function createServerFileSearch(workspace, sandbox) {
1889
+ return {
1890
+ async search(glob, limit = DEFAULT_LIMIT) {
1891
+ const safeLimit = normalizeLimit(limit);
1892
+ const command = [
1893
+ "find .",
1894
+ "-maxdepth 10",
1895
+ "\\( -path './.boring-agent' -o -path './.git' -o -path './node_modules' \\) -prune -o",
1896
+ buildFindArgs(glob),
1897
+ "-type f",
1898
+ "-print",
1899
+ `| head -n ${safeLimit}`
1900
+ ].join(" ");
1901
+ const { stdout, exitCode } = await sandbox.exec(command, {
1902
+ cwd: workspace.root,
1903
+ timeoutMs: SEARCH_TIMEOUT_MS,
1904
+ maxOutputBytes: SEARCH_MAX_OUTPUT_BYTES
1905
+ });
1906
+ if (exitCode !== 0) {
1907
+ throw new Error(`file-search failed: exit ${exitCode}`);
1908
+ }
1909
+ const decoded = new TextDecoder().decode(stdout);
1910
+ return decoded.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.length > 0).map((line) => line.replace(/^\.\//, ""));
1911
+ }
1912
+ };
1913
+ }
1914
+
1915
+ // src/server/sandbox/remote-worker/protocol.ts
1916
+ var REMOTE_WORKER_RUNTIME_CWD = "/workspace";
1917
+ var REMOTE_WORKER_PROVIDER = "remote-worker";
1918
+ var WORKER_INTERNAL_TOKEN_HEADER = "x-boring-internal-token";
1919
+ var WORKER_WORKSPACE_ID_HEADER = "x-boring-workspace-id";
1920
+ var WORKER_REQUEST_ID_HEADER = "x-boring-request-id";
1921
+
1922
+ // src/server/sandbox/remote-worker/createRemoteWorkerSandbox.ts
1923
+ function filteredEnv(env) {
1924
+ if (!env) return void 0;
1925
+ return Object.fromEntries(
1926
+ Object.entries(env).filter((entry) => typeof entry[1] === "string")
1927
+ );
1928
+ }
1929
+ function createRemoteWorkerSandbox(client) {
1930
+ return {
1931
+ id: REMOTE_WORKER_PROVIDER,
1932
+ placement: "remote",
1933
+ provider: REMOTE_WORKER_PROVIDER,
1934
+ capabilities: ["exec"],
1935
+ runtimeContext: { runtimeCwd: REMOTE_WORKER_RUNTIME_CWD },
1936
+ async init() {
1937
+ await client.health();
1938
+ },
1939
+ async exec(cmd, opts = {}) {
1940
+ const request = {
1941
+ cmd,
1942
+ cwd: opts.cwd,
1943
+ env: filteredEnv(opts.env),
1944
+ timeoutMs: opts.timeoutMs,
1945
+ maxOutputBytes: opts.maxOutputBytes
1946
+ };
1947
+ const startedAt = Date.now();
1948
+ const heartbeat = opts.onHeartbeat ? setInterval(() => opts.onHeartbeat?.(Date.now() - startedAt), 1e3) : null;
1949
+ try {
1950
+ const result = await client.exec(request, { signal: opts.signal });
1951
+ opts.onStdout?.(result.stdout);
1952
+ opts.onStderr?.(result.stderr);
1953
+ return result;
1954
+ } finally {
1955
+ if (heartbeat) clearInterval(heartbeat);
1956
+ }
1957
+ }
1958
+ };
1959
+ }
1960
+
1961
+ // src/server/sandbox/remote-worker/workerClient.ts
1962
+ import { timingSafeEqual } from "crypto";
1963
+ var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
1964
+ var DEFAULT_EXEC_TIMEOUT_MS = 3e4;
1965
+ var DEFAULT_EXEC_REQUEST_GRACE_MS = 1e4;
1966
+ var RemoteWorkerClientError = class extends Error {
1967
+ statusCode;
1968
+ code;
1969
+ details;
1970
+ constructor(message, opts) {
1971
+ super(message);
1972
+ this.name = "RemoteWorkerClientError";
1973
+ this.statusCode = opts.statusCode;
1974
+ this.code = opts.code ?? "remote_worker_error";
1975
+ this.details = opts.details;
1976
+ }
1977
+ };
1978
+ function requireNonEmpty(value, label) {
1979
+ const trimmed = value.trim();
1980
+ if (!trimmed) throw new Error(`${label} is required for ${REMOTE_WORKER_PROVIDER} mode`);
1981
+ return trimmed;
1982
+ }
1983
+ function normalizeBaseUrl(value) {
1984
+ return requireNonEmpty(value, "BORING_WORKER_BASE_URL").replace(/\/+$/, "");
1985
+ }
1986
+ function positiveInteger2(value, fallback, label) {
1987
+ if (value === void 0) return fallback;
1988
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be a positive integer`);
1989
+ return Math.trunc(value);
1990
+ }
1991
+ function encodeWorkspaceId(workspaceId) {
1992
+ return encodeURIComponent(requireNonEmpty(workspaceId, "workspaceId"));
1993
+ }
1994
+ function bytesToBase64(bytes) {
1995
+ return Buffer.from(bytes).toString("base64");
1996
+ }
1997
+ function base64ToBytes(value) {
1998
+ return new Uint8Array(Buffer.from(value, "base64"));
1999
+ }
2000
+ function makeHeaders(opts) {
2001
+ const headers = new Headers();
2002
+ headers.set(WORKER_INTERNAL_TOKEN_HEADER, requireNonEmpty(opts.token, "BORING_WORKER_INTERNAL_TOKEN"));
2003
+ headers.set(WORKER_WORKSPACE_ID_HEADER, requireNonEmpty(opts.workspaceId, "workspaceId"));
2004
+ if (opts.requestId) headers.set(WORKER_REQUEST_ID_HEADER, opts.requestId);
2005
+ if (opts.contentType) headers.set("content-type", opts.contentType);
2006
+ return headers;
2007
+ }
2008
+ async function parseError(response) {
2009
+ let payload;
2010
+ try {
2011
+ payload = await response.json();
2012
+ } catch {
2013
+ }
2014
+ return new RemoteWorkerClientError(
2015
+ payload?.error?.message ?? `remote worker request failed (${response.status})`,
2016
+ {
2017
+ statusCode: payload?.error?.statusCode ?? response.status,
2018
+ code: payload?.error?.code,
2019
+ details: payload?.error?.details
2020
+ }
2021
+ );
2022
+ }
2023
+ var RemoteWorkerClient = class {
2024
+ baseUrl;
2025
+ token;
2026
+ workspaceId;
2027
+ requestId;
2028
+ fetchImpl;
2029
+ requestTimeoutMs;
2030
+ execTimeoutMs;
2031
+ execRequestGraceMs;
2032
+ constructor(opts) {
2033
+ this.baseUrl = normalizeBaseUrl(opts.baseUrl);
2034
+ this.token = requireNonEmpty(opts.token, "BORING_WORKER_INTERNAL_TOKEN");
2035
+ this.workspaceId = requireNonEmpty(opts.workspaceId, "workspaceId");
2036
+ this.requestId = opts.requestId;
2037
+ this.fetchImpl = opts.fetchImpl ?? fetch;
2038
+ this.requestTimeoutMs = positiveInteger2(opts.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS, "requestTimeoutMs");
2039
+ this.execTimeoutMs = positiveInteger2(opts.execTimeoutMs, DEFAULT_EXEC_TIMEOUT_MS, "execTimeoutMs");
2040
+ this.execRequestGraceMs = positiveInteger2(opts.execRequestGraceMs, DEFAULT_EXEC_REQUEST_GRACE_MS, "execRequestGraceMs");
2041
+ }
2042
+ timeoutError(timeoutMs) {
2043
+ return new RemoteWorkerClientError("remote worker request timed out", {
2044
+ statusCode: 504,
2045
+ code: ErrorCode.enum.REMOTE_WORKER_TIMEOUT,
2046
+ details: { timeoutMs, retryable: true }
2047
+ });
2048
+ }
2049
+ abortedError() {
2050
+ return new RemoteWorkerClientError("remote worker request aborted", {
2051
+ statusCode: 499,
2052
+ code: ErrorCode.enum.ABORTED
2053
+ });
2054
+ }
2055
+ async fetchWithTimeout(url, init, timeoutMs) {
2056
+ const controller = new AbortController();
2057
+ let timedOut = false;
2058
+ const timer = setTimeout(() => {
2059
+ timedOut = true;
2060
+ controller.abort();
2061
+ }, timeoutMs);
2062
+ const upstreamSignal = init.signal;
2063
+ const abortFromUpstream = () => controller.abort();
2064
+ if (upstreamSignal?.aborted) {
2065
+ clearTimeout(timer);
2066
+ throw this.abortedError();
2067
+ }
2068
+ upstreamSignal?.addEventListener("abort", abortFromUpstream, { once: true });
2069
+ try {
2070
+ return await this.fetchImpl(url, { ...init, signal: controller.signal });
2071
+ } catch (error) {
2072
+ if (timedOut) throw this.timeoutError(timeoutMs);
2073
+ if (upstreamSignal?.aborted) throw this.abortedError();
2074
+ throw error;
2075
+ } finally {
2076
+ clearTimeout(timer);
2077
+ upstreamSignal?.removeEventListener("abort", abortFromUpstream);
2078
+ }
2079
+ }
2080
+ async health() {
2081
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/internal/health`, {
2082
+ headers: makeHeaders({ token: this.token, workspaceId: this.workspaceId, requestId: this.requestId })
2083
+ }, this.requestTimeoutMs);
2084
+ if (!response.ok) throw await parseError(response);
2085
+ }
2086
+ async workspace(op) {
2087
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/internal/workspaces/${encodeWorkspaceId(this.workspaceId)}/fs`, {
2088
+ method: "POST",
2089
+ headers: makeHeaders({ token: this.token, workspaceId: this.workspaceId, requestId: this.requestId, contentType: "application/json" }),
2090
+ body: JSON.stringify(op)
2091
+ }, this.requestTimeoutMs);
2092
+ if (!response.ok) throw await parseError(response);
2093
+ return await response.json();
2094
+ }
2095
+ async exec(input, opts = {}) {
2096
+ const timeoutMs = (input.timeoutMs ?? this.execTimeoutMs) + this.execRequestGraceMs;
2097
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/internal/workspaces/${encodeWorkspaceId(this.workspaceId)}/exec`, {
2098
+ method: "POST",
2099
+ headers: makeHeaders({ token: this.token, workspaceId: this.workspaceId, requestId: this.requestId, contentType: "application/json" }),
2100
+ body: JSON.stringify(input),
2101
+ signal: opts.signal
2102
+ }, timeoutMs);
2103
+ if (!response.ok) throw await parseError(response);
2104
+ const body = await response.json();
2105
+ return {
2106
+ stdout: base64ToBytes(body.stdoutBase64),
2107
+ stderr: base64ToBytes(body.stderrBase64),
2108
+ exitCode: body.exitCode,
2109
+ durationMs: body.durationMs,
2110
+ truncated: body.truncated,
2111
+ stdoutEncoding: body.stdoutEncoding,
2112
+ stderrEncoding: body.stderrEncoding
2113
+ };
2114
+ }
2115
+ watch(onEvent, onError) {
2116
+ const controller = new AbortController();
2117
+ void this.consumeEvents(controller.signal, onEvent).catch((error) => {
2118
+ if (!controller.signal.aborted) onError?.(error instanceof Error ? error : new Error(String(error)));
2119
+ });
2120
+ return { close: () => controller.abort() };
2121
+ }
2122
+ async consumeEvents(signal, onEvent) {
2123
+ const response = await this.fetchImpl(`${this.baseUrl}/internal/workspaces/${encodeWorkspaceId(this.workspaceId)}/fs/events`, {
2124
+ headers: makeHeaders({ token: this.token, workspaceId: this.workspaceId, requestId: this.requestId }),
2125
+ signal
2126
+ });
2127
+ if (!response.ok) throw await parseError(response);
2128
+ if (!response.body) throw new RemoteWorkerClientError("remote worker event stream missing body", { statusCode: 502 });
2129
+ const reader = response.body.getReader();
2130
+ const decoder2 = new TextDecoder();
2131
+ let buffer = "";
2132
+ try {
2133
+ for (; ; ) {
2134
+ const { done, value } = await reader.read();
2135
+ if (done) {
2136
+ if (signal.aborted) return;
2137
+ throw new RemoteWorkerClientError("remote worker event stream closed", {
2138
+ statusCode: 502,
2139
+ code: ErrorCode.enum.REMOTE_WORKER_STREAM_CLOSED
2140
+ });
2141
+ }
2142
+ buffer += decoder2.decode(value, { stream: true });
2143
+ let boundary = buffer.indexOf("\n\n");
2144
+ while (boundary >= 0) {
2145
+ const frame = buffer.slice(0, boundary);
2146
+ buffer = buffer.slice(boundary + 2);
2147
+ this.handleSseFrame(frame, onEvent);
2148
+ boundary = buffer.indexOf("\n\n");
2149
+ }
2150
+ }
2151
+ } finally {
2152
+ reader.releaseLock();
2153
+ }
2154
+ }
2155
+ handleSseFrame(frame, onEvent) {
2156
+ let eventName = "message";
2157
+ const dataLines = [];
2158
+ for (const line of frame.split("\n")) {
2159
+ if (line.startsWith(":")) continue;
2160
+ if (line.startsWith("event:")) eventName = line.slice("event:".length).trim();
2161
+ if (line.startsWith("data:")) dataLines.push(line.slice("data:".length).trimStart());
2162
+ }
2163
+ if (eventName !== "change" || dataLines.length === 0) return;
2164
+ const payload = JSON.parse(dataLines.join("\n"));
2165
+ onEvent(payload.event);
2166
+ }
2167
+ };
2168
+ function encodeBytesForWorker(bytes) {
2169
+ return bytesToBase64(bytes);
2170
+ }
2171
+ function decodeBytesFromWorker(value) {
2172
+ return base64ToBytes(value);
2173
+ }
2174
+ function constantTimeTokenEqual(a, b) {
2175
+ if (!a || !b) return false;
2176
+ const aBytes = Buffer.from(a);
2177
+ const bBytes = Buffer.from(b);
2178
+ if (aBytes.length !== bBytes.length) return false;
2179
+ return timingSafeEqual(aBytes, bBytes);
2180
+ }
2181
+
2182
+ // src/server/workspace/createRemoteWorkerWorkspace.ts
2183
+ function expectContent(result) {
2184
+ if ("content" in result && typeof result.content === "string") return result.content;
2185
+ throw new Error("remote worker returned invalid file content response");
2186
+ }
2187
+ function expectData(result) {
2188
+ if ("dataBase64" in result && typeof result.dataBase64 === "string") return decodeBytesFromWorker(result.dataBase64);
2189
+ throw new Error("remote worker returned invalid binary response");
2190
+ }
2191
+ function expectStat(result) {
2192
+ if ("stat" in result && result.stat) return result.stat;
2193
+ throw new Error("remote worker returned invalid stat response");
2194
+ }
2195
+ function expectEntries(result) {
2196
+ if ("entries" in result && Array.isArray(result.entries)) return result.entries;
2197
+ throw new Error("remote worker returned invalid readdir response");
2198
+ }
2199
+ function createRemoteWatcher(client) {
2200
+ const listeners = /* @__PURE__ */ new Map();
2201
+ let stream = null;
2202
+ let reconnectTimer = null;
2203
+ let closed = false;
2204
+ const clearReconnectTimer = () => {
2205
+ if (!reconnectTimer) return;
2206
+ clearTimeout(reconnectTimer);
2207
+ reconnectTimer = null;
2208
+ };
2209
+ const scheduleReconnect = () => {
2210
+ if (closed || listeners.size === 0 || reconnectTimer) return;
2211
+ reconnectTimer = setTimeout(() => {
2212
+ reconnectTimer = null;
2213
+ ensureStream();
2214
+ }, 1e3);
2215
+ };
2216
+ const handleStreamEnd = () => {
2217
+ stream = null;
2218
+ for (const options of [...listeners.values()]) {
2219
+ try {
2220
+ options?.onControlEvent?.({ type: "resync-required", reason: "remote_worker_stream_closed" });
2221
+ } catch {
2222
+ }
2223
+ }
2224
+ scheduleReconnect();
2225
+ };
2226
+ const ensureStream = () => {
2227
+ if (stream || closed || listeners.size === 0) return;
2228
+ clearReconnectTimer();
2229
+ stream = client.watch((event) => {
2230
+ for (const listener of [...listeners.keys()]) {
2231
+ try {
2232
+ listener(event);
2233
+ } catch {
2234
+ }
2235
+ }
2236
+ }, handleStreamEnd);
2237
+ };
2238
+ return {
2239
+ subscribe(listener, options) {
2240
+ if (closed) return () => {
2241
+ };
2242
+ listeners.set(listener, options);
2243
+ ensureStream();
2244
+ return () => {
2245
+ listeners.delete(listener);
2246
+ if (listeners.size === 0) {
2247
+ clearReconnectTimer();
2248
+ stream?.close();
2249
+ stream = null;
2250
+ }
2251
+ };
2252
+ },
2253
+ close() {
2254
+ if (closed) return;
2255
+ closed = true;
2256
+ listeners.clear();
2257
+ clearReconnectTimer();
2258
+ stream?.close();
2259
+ stream = null;
2260
+ }
2261
+ };
2262
+ }
2263
+ function createRemoteWorkerWorkspace(client) {
2264
+ let watcher = null;
2265
+ return {
2266
+ root: REMOTE_WORKER_RUNTIME_CWD,
2267
+ runtimeContext: { runtimeCwd: REMOTE_WORKER_RUNTIME_CWD },
2268
+ fsCapability: "best-effort",
2269
+ watch() {
2270
+ watcher ??= createRemoteWatcher(client);
2271
+ return watcher;
2272
+ },
2273
+ async readFile(path4) {
2274
+ return expectContent(await client.workspace({ op: "readFile", path: path4 }));
2275
+ },
2276
+ async readBinaryFile(path4) {
2277
+ return expectData(await client.workspace({ op: "readBinaryFile", path: path4 }));
2278
+ },
2279
+ async writeFile(path4, data) {
2280
+ await client.workspace({ op: "writeFile", path: path4, data });
2281
+ },
2282
+ async writeBinaryFile(path4, data) {
2283
+ await client.workspace({ op: "writeBinaryFile", path: path4, dataBase64: encodeBytesForWorker(data) });
2284
+ },
2285
+ async readFileWithStat(path4) {
2286
+ const result = await client.workspace({ op: "readFileWithStat", path: path4 });
2287
+ if ("content" in result && "stat" in result) return { content: result.content, stat: result.stat };
2288
+ throw new Error("remote worker returned invalid readFileWithStat response");
2289
+ },
2290
+ async writeFileWithStat(path4, data) {
2291
+ return expectStat(await client.workspace({ op: "writeFileWithStat", path: path4, data }));
2292
+ },
2293
+ async writeBinaryFileWithStat(path4, data) {
2294
+ return expectStat(await client.workspace({ op: "writeBinaryFileWithStat", path: path4, dataBase64: encodeBytesForWorker(data) }));
2295
+ },
2296
+ async unlink(path4) {
2297
+ await client.workspace({ op: "unlink", path: path4 });
2298
+ },
2299
+ async readdir(path4) {
2300
+ return expectEntries(await client.workspace({ op: "readdir", path: path4 }));
2301
+ },
2302
+ async stat(path4) {
2303
+ return expectStat(await client.workspace({ op: "stat", path: path4 }));
2304
+ },
2305
+ async mkdir(path4, opts) {
2306
+ await client.workspace({ op: "mkdir", path: path4, recursive: opts?.recursive });
2307
+ },
2308
+ async rename(from, to) {
2309
+ await client.workspace({ op: "rename", from, to });
2310
+ }
2311
+ };
2312
+ }
2313
+
2314
+ // src/server/runtime/modes/remote-worker.ts
2315
+ function requireOption(value, name) {
2316
+ const trimmed = value?.trim();
2317
+ if (!trimmed) throw new Error(`${name} is required for remote-worker mode`);
2318
+ return trimmed;
2319
+ }
2320
+ function createRemoteWorkerModeAdapter(opts = {}) {
2321
+ return {
2322
+ id: REMOTE_WORKER_PROVIDER,
2323
+ workspaceFsCapability: "best-effort",
2324
+ async create(ctx) {
2325
+ const workspaceId = requireOption(ctx.workspaceId ?? ctx.sessionId, "workspaceId");
2326
+ const client = new RemoteWorkerClient({
2327
+ baseUrl: requireOption(opts.baseUrl ?? getEnv("BORING_WORKER_BASE_URL"), "BORING_WORKER_BASE_URL"),
2328
+ token: requireOption(opts.token ?? getEnv("BORING_WORKER_INTERNAL_TOKEN"), "BORING_WORKER_INTERNAL_TOKEN"),
2329
+ workspaceId,
2330
+ requestId: ctx.requestId,
2331
+ fetchImpl: opts.fetchImpl
2332
+ });
2333
+ const workspace = createRemoteWorkerWorkspace(client);
2334
+ const sandbox = createRemoteWorkerSandbox(client);
2335
+ await sandbox.init?.({ workspace, sessionId: ctx.sessionId });
2336
+ return {
2337
+ runtimeContext: { runtimeCwd: REMOTE_WORKER_RUNTIME_CWD },
2338
+ workspace,
2339
+ sandbox,
2340
+ fileSearch: createServerFileSearch(workspace, sandbox)
2341
+ };
2342
+ }
2343
+ };
2344
+ }
2345
+
1820
2346
  // src/server/http/routes/file.ts
1821
2347
  import { dirname as dirname5, extname as extname2, relative as relative4 } from "path/posix";
1822
2348
 
@@ -2851,7 +3377,7 @@ function cloneStat(stat11) {
2851
3377
  function cloneEntries(entries) {
2852
3378
  return entries.map((entry) => ({ name: entry.name, kind: entry.kind }));
2853
3379
  }
2854
- function shellQuote2(value) {
3380
+ function shellQuote3(value) {
2855
3381
  return `'${value.replace(/'/g, `'\\''`)}'`;
2856
3382
  }
2857
3383
  async function runJson(sandbox, script) {
@@ -2931,7 +3457,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2931
3457
  async function assertRealPathWithinSandboxRoot(sandboxPath) {
2932
3458
  const isWithinRoot = await runJson(
2933
3459
  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)}`
3460
+ `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
3461
  );
2936
3462
  if (!isWithinRoot) {
2937
3463
  throw Object.assign(new Error("resolved path escapes workspace root"), { code: EPERM_CODE2 });
@@ -2940,7 +3466,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2940
3466
  async function isSandboxSymlink(sandboxPath) {
2941
3467
  return await runJson(
2942
3468
  remote,
2943
- `node -e ${shellQuote2(`const fs=require('fs'); process.stdout.write(JSON.stringify(fs.lstatSync(process.argv[1]).isSymbolicLink()))`)} ${shellQuote2(sandboxPath)}`
3469
+ `node -e ${shellQuote3(`const fs=require('fs'); process.stdout.write(JSON.stringify(fs.lstatSync(process.argv[1]).isSymbolicLink()))`)} ${shellQuote3(sandboxPath)}`
2944
3470
  );
2945
3471
  }
2946
3472
  async function listDescendantPaths(relPath, sandboxPath) {
@@ -2960,7 +3486,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2960
3486
  }
2961
3487
  return await runJson(
2962
3488
  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)}`
3489
+ `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
3490
  );
2965
3491
  }
2966
3492
  return {
@@ -3053,7 +3579,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3053
3579
  const version = metadataVersion;
3054
3580
  const result = await runJson(
3055
3581
  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)}`
3582
+ `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
3583
  );
3058
3584
  if (metadataVersion === version) {
3059
3585
  statCache.set(sandboxPath, result.stat);
@@ -3081,7 +3607,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3081
3607
  };
3082
3608
  })() : await runJson(
3083
3609
  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)}`
3610
+ `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
3611
  );
3086
3612
  statCache.set(sandboxPath, writtenStat2);
3087
3613
  emitChange({ op: "write", path: relPath, mtimeMs: writtenStat2.mtimeMs });
@@ -3090,7 +3616,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3090
3616
  const encoded = payload.toString("base64");
3091
3617
  const writtenStat = await runJson(
3092
3618
  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)}`
3619
+ `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
3620
  );
3095
3621
  invalidateMetadataCache();
3096
3622
  statCache.set(sandboxPath, writtenStat);
@@ -3119,7 +3645,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3119
3645
  };
3120
3646
  })() : await runJson(
3121
3647
  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)}`
3648
+ `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
3649
  );
3124
3650
  statCache.set(sandboxPath, writtenStat2);
3125
3651
  emitChange({ op: "write", path: relPath, mtimeMs: writtenStat2.mtimeMs });
@@ -3128,7 +3654,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3128
3654
  const encoded = payload.toString("base64");
3129
3655
  const writtenStat = await runJson(
3130
3656
  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)}`
3657
+ `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
3658
  );
3133
3659
  invalidateMetadataCache();
3134
3660
  statCache.set(sandboxPath, writtenStat);
@@ -3144,7 +3670,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3144
3670
  await assertRealPathWithinSandboxRoot(sandboxPath);
3145
3671
  const descendantPaths = await isSandboxSymlink(sandboxPath) ? [] : await listDescendantPaths(relPath, sandboxPath);
3146
3672
  if (remote.fs?.rm) await remote.fs.rm(sandboxPath, { recursive: true, force: false });
3147
- else await runShell(remote, `rm -r -- ${shellQuote2(sandboxPath)}`);
3673
+ else await runShell(remote, `rm -r -- ${shellQuote3(sandboxPath)}`);
3148
3674
  invalidateMetadataCache();
3149
3675
  workspaceOpts.onMutation?.();
3150
3676
  emitChange({ op: "unlink", path: relPath });
@@ -3162,7 +3688,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3162
3688
  kind: entry.isDirectory() ? "dir" : "file"
3163
3689
  })) : await runJson(
3164
3690
  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)}`
3691
+ `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
3692
  );
3167
3693
  if (metadataVersion === version) {
3168
3694
  readdirCache.set(sandboxPath, mappedEntries);
@@ -3185,7 +3711,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3185
3711
  } else {
3186
3712
  mappedStat = await runJson(
3187
3713
  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)}`
3714
+ `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
3715
  );
3190
3716
  }
3191
3717
  if (metadataVersion === version) {
@@ -3196,9 +3722,9 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3196
3722
  async mkdir(relPath, opts) {
3197
3723
  const sandboxPath = toSandboxPath(relPath);
3198
3724
  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)}`);
3725
+ else if (opts?.recursive) await runShell(remote, `mkdir -p -- ${shellQuote3(sandboxPath)}`);
3200
3726
  else if (remote.mkDir) await remote.mkDir(sandboxPath);
3201
- else await runShell(remote, `mkdir -- ${shellQuote2(sandboxPath)}`);
3727
+ else await runShell(remote, `mkdir -- ${shellQuote3(sandboxPath)}`);
3202
3728
  invalidateMetadataCache();
3203
3729
  workspaceOpts.onMutation?.();
3204
3730
  emitChange({ op: "mkdir", path: relPath });
@@ -3207,7 +3733,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3207
3733
  const fromSandboxPath = toSandboxPath(fromRelPath);
3208
3734
  const toSandboxAbsolutePath = toSandboxPath(toRelPath2);
3209
3735
  if (remote.fs?.rename) await remote.fs.rename(fromSandboxPath, toSandboxAbsolutePath);
3210
- else await runShell(remote, `mv -- ${shellQuote2(fromSandboxPath)} ${shellQuote2(toSandboxAbsolutePath)}`);
3736
+ else await runShell(remote, `mv -- ${shellQuote3(fromSandboxPath)} ${shellQuote3(toSandboxAbsolutePath)}`);
3211
3737
  invalidateMetadataCache();
3212
3738
  workspaceOpts.onMutation?.();
3213
3739
  emitChange({ op: "rename", path: toRelPath2, oldPath: fromRelPath });
@@ -4061,61 +4587,6 @@ import { spawnSync } from "child_process";
4061
4587
  // src/server/runtime/modes/direct.ts
4062
4588
  import { mkdir as mkdir8 } from "fs/promises";
4063
4589
 
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
4590
  // src/server/workspace/provision.ts
4120
4591
  import { cp as cp2 } from "fs/promises";
4121
4592
  async function copyTemplate(templatePath, workspaceRoot) {
@@ -6190,6 +6661,11 @@ var INFOMANIAK_PROVIDER = "infomaniak";
6190
6661
  var INFOMANIAK_API_BASE = "https://api.infomaniak.com";
6191
6662
  var DEFAULT_CUSTOM_MODEL_MAX_TOKENS = 16384;
6192
6663
  var DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW = 2e5;
6664
+ var DEFAULT_INFOMANIAK_MODELS = [
6665
+ "moonshotai/Kimi-K2.6",
6666
+ "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8",
6667
+ "Qwen/Qwen3.5-122B-A10B-FP8"
6668
+ ];
6193
6669
  function clean(value) {
6194
6670
  const trimmed = value?.trim();
6195
6671
  return trimmed && trimmed.length > 0 ? trimmed : void 0;
@@ -6213,6 +6689,19 @@ function readModelInput(name) {
6213
6689
  const parsed = raw.split(",").map((part) => part.trim()).filter((part) => part === "text" || part === "image");
6214
6690
  return parsed.length > 0 ? parsed : ["text"];
6215
6691
  }
6692
+ function readMaxTokensField(name, fallback = "max_completion_tokens") {
6693
+ const raw = clean(getEnv(name));
6694
+ return raw === "max_tokens" || raw === "max_completion_tokens" ? raw : fallback;
6695
+ }
6696
+ function buildOpenAICompletionsCompat(envPrefix, defaults = {}) {
6697
+ return {
6698
+ supportsStore: readBoolean(`${envPrefix}_SUPPORTS_STORE`, defaults.supportsStore ?? false),
6699
+ supportsDeveloperRole: readBoolean(`${envPrefix}_SUPPORTS_DEVELOPER_ROLE`, defaults.supportsDeveloperRole ?? true),
6700
+ supportsReasoningEffort: readBoolean(`${envPrefix}_SUPPORTS_REASONING_EFFORT`, defaults.supportsReasoningEffort ?? true),
6701
+ supportsUsageInStreaming: readBoolean(`${envPrefix}_SUPPORTS_USAGE_IN_STREAMING`, defaults.supportsUsageInStreaming ?? true),
6702
+ maxTokensField: readMaxTokensField(`${envPrefix}_MAX_TOKENS_FIELD`, defaults.maxTokensField)
6703
+ };
6704
+ }
6216
6705
  function readApiKeyEnv(candidates) {
6217
6706
  for (const candidate of candidates) {
6218
6707
  const envName = clean(candidate);
@@ -6225,31 +6714,23 @@ function buildOpenAICompatibleProviderConfig(opts) {
6225
6714
  baseUrl: opts.baseUrl,
6226
6715
  apiKey: opts.apiKeyEnv,
6227
6716
  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
- ]
6717
+ models: opts.models.map((model) => ({
6718
+ id: model.id,
6719
+ name: model.name ?? model.id,
6720
+ api: "openai-completions",
6721
+ reasoning: readBoolean(`${opts.envPrefix}_REASONING`, true),
6722
+ input: readModelInput(`${opts.envPrefix}_INPUT`),
6723
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
6724
+ contextWindow: readPositiveInt(
6725
+ `${opts.envPrefix}_CONTEXT_WINDOW`,
6726
+ DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW
6727
+ ),
6728
+ maxTokens: readPositiveInt(
6729
+ `${opts.envPrefix}_MAX_TOKENS`,
6730
+ DEFAULT_CUSTOM_MODEL_MAX_TOKENS
6731
+ ),
6732
+ compat: buildOpenAICompletionsCompat(opts.envPrefix, opts.compatDefaults)
6733
+ }))
6253
6734
  };
6254
6735
  }
6255
6736
  function readInfomaniakBaseUrl() {
@@ -6259,25 +6740,46 @@ function readInfomaniakBaseUrl() {
6259
6740
  if (!productId) return void 0;
6260
6741
  return `${INFOMANIAK_API_BASE}/2/ai/${productId}/openai/v1`;
6261
6742
  }
6743
+ function readInfomaniakModelIds() {
6744
+ const configured = clean(getEnv("BORING_AGENT_INFOMANIAK_MODELS"))?.split(",").map((part) => part.trim()).filter(Boolean);
6745
+ const ids = configured?.length ? configured : [...DEFAULT_INFOMANIAK_MODELS];
6746
+ const legacyModel = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL"));
6747
+ if (legacyModel && !ids.includes(legacyModel)) ids.push(legacyModel);
6748
+ return Array.from(new Set(ids));
6749
+ }
6262
6750
  function readInfomaniakProvider() {
6263
- const modelId = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL")) ?? clean(getEnv("BORING_AGENT_DEFAULT_MODEL_ID"));
6751
+ const modelIds = readInfomaniakModelIds();
6752
+ const defaultModelId = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL")) ?? clean(getEnv("BORING_AGENT_DEFAULT_MODEL_ID")) ?? modelIds[0];
6264
6753
  const baseUrl = readInfomaniakBaseUrl();
6265
6754
  const apiKeyEnv = readApiKeyEnv([
6266
6755
  clean(getEnv("BORING_AGENT_INFOMANIAK_API_KEY_ENV")) ?? "",
6267
6756
  "INFOMANIAK_API_TOKEN",
6268
6757
  "BORING_AGENT_INFOMANIAK_API_KEY"
6269
6758
  ]);
6270
- if (!modelId || !baseUrl || !apiKeyEnv) return void 0;
6759
+ if (!defaultModelId || !baseUrl || !apiKeyEnv) return void 0;
6271
6760
  const provider = clean(getEnv("BORING_AGENT_INFOMANIAK_PROVIDER")) ?? INFOMANIAK_PROVIDER;
6761
+ const modelName = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL_NAME"));
6762
+ const models = modelIds.map((id) => {
6763
+ const model = { id };
6764
+ if (id === defaultModelId && modelName) model.name = modelName;
6765
+ return model;
6766
+ });
6767
+ if (!models.some((model) => model.id === defaultModelId)) {
6768
+ models.push({ id: defaultModelId, name: modelName });
6769
+ }
6272
6770
  return {
6273
6771
  provider,
6274
- model: { provider, id: modelId },
6772
+ models: models.map((model) => ({ provider, id: model.id })),
6773
+ defaultModel: { provider, id: defaultModelId },
6275
6774
  config: buildOpenAICompatibleProviderConfig({
6276
- modelId,
6277
- modelName: clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL_NAME")),
6775
+ models,
6278
6776
  apiKeyEnv,
6279
6777
  baseUrl,
6280
- envPrefix: "BORING_AGENT_INFOMANIAK"
6778
+ envPrefix: "BORING_AGENT_INFOMANIAK",
6779
+ // Infomaniak's OpenAI-compatible chat endpoint rejects OpenAI's newer
6780
+ // `developer` role (surfacing as "400 Unexpected message role"). Hosts
6781
+ // can opt back in with BORING_AGENT_INFOMANIAK_SUPPORTS_DEVELOPER_ROLE=1.
6782
+ compatDefaults: { supportsDeveloperRole: false, supportsReasoningEffort: false }
6281
6783
  })
6282
6784
  };
6283
6785
  }
@@ -6290,12 +6792,15 @@ function readCustomProvider() {
6290
6792
  "BORING_AGENT_CUSTOM_MODEL_API_KEY"
6291
6793
  ]);
6292
6794
  if (!provider || !modelId || !baseUrl || !apiKeyEnv) return void 0;
6795
+ const modelName = clean(getEnv("BORING_AGENT_CUSTOM_MODEL_NAME"));
6796
+ const model = { id: modelId };
6797
+ if (modelName) model.name = modelName;
6293
6798
  return {
6294
6799
  provider,
6295
- model: { provider, id: modelId },
6800
+ models: [{ provider, id: modelId }],
6801
+ defaultModel: { provider, id: modelId },
6296
6802
  config: buildOpenAICompatibleProviderConfig({
6297
- modelId,
6298
- modelName: clean(getEnv("BORING_AGENT_CUSTOM_MODEL_NAME")),
6803
+ models: [model],
6299
6804
  apiKeyEnv,
6300
6805
  baseUrl,
6301
6806
  envPrefix: "BORING_AGENT_CUSTOM_MODEL"
@@ -6309,7 +6814,7 @@ function registerConfiguredModelProviders(registry) {
6309
6814
  if (!provider) continue;
6310
6815
  if (seen.has(provider.provider)) continue;
6311
6816
  registry.registerProvider(provider.provider, provider.config);
6312
- registered.push(provider.model);
6817
+ registered.push(...provider.models);
6313
6818
  seen.add(provider.provider);
6314
6819
  }
6315
6820
  return registered;
@@ -6337,7 +6842,7 @@ function readConfiguredDefaultModel() {
6337
6842
  if (explicitProvider && explicitId) {
6338
6843
  return { provider: explicitProvider, id: explicitId };
6339
6844
  }
6340
- return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.model ?? readCustomProvider()?.model;
6845
+ return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.defaultModel ?? readCustomProvider()?.defaultModel;
6341
6846
  }
6342
6847
 
6343
6848
  // src/server/piPackages.ts
@@ -7191,10 +7696,10 @@ function vercelBashOps(sandbox, opts = {}) {
7191
7696
  return {
7192
7697
  exec(command, cwd, { onData, signal, timeout, env }) {
7193
7698
  const effectiveEnv = opts.mergeEnv ? opts.mergeEnv(env) : env;
7194
- const filteredEnv = effectiveEnv ? Object.fromEntries(Object.entries(effectiveEnv).filter((e) => e[1] != null)) : void 0;
7699
+ const filteredEnv2 = effectiveEnv ? Object.fromEntries(Object.entries(effectiveEnv).filter((e) => e[1] != null)) : void 0;
7195
7700
  return sandbox.exec(command, {
7196
7701
  cwd,
7197
- env: filteredEnv,
7702
+ env: filteredEnv2,
7198
7703
  signal,
7199
7704
  timeoutMs: timeout ? timeout * 1e3 : void 0,
7200
7705
  onStdout: (chunk) => onData(Buffer.from(chunk)),
@@ -7578,8 +8083,7 @@ function adaptPiTool(piTool) {
7578
8083
  }
7579
8084
  function buildFilesystemAgentTools(bundle) {
7580
8085
  const cwd = bundle.workspace.root;
7581
- const storageRoot = getRuntimeBundleStorageRoot(bundle);
7582
- if (bundle.sandbox.provider === "vercel-sandbox") {
8086
+ if (bundle.sandbox.provider === "vercel-sandbox" || bundle.sandbox.provider === "remote-worker") {
7583
8087
  return [
7584
8088
  adaptPiTool(createReadToolDefinition(cwd, { operations: vercelReadOps(bundle.workspace) })),
7585
8089
  adaptPiTool(createWriteToolDefinition(cwd, { operations: vercelWriteOps(bundle.workspace) })),
@@ -7589,6 +8093,7 @@ function buildFilesystemAgentTools(bundle) {
7589
8093
  adaptPiTool(createLsToolDefinition(cwd, { operations: vercelLsOps(bundle.workspace) }))
7590
8094
  ];
7591
8095
  }
8096
+ const storageRoot = getRuntimeBundleStorageRoot(bundle);
7592
8097
  const ops = boundFs(storageRoot, { runtimeRoot: cwd });
7593
8098
  return [
7594
8099
  adaptPiTool(createReadToolDefinition(cwd, { operations: ops.read })),
@@ -7724,9 +8229,9 @@ function directSpawnHook(workspaceRoot, runtime) {
7724
8229
  }
7725
8230
  var VERCEL_SAFE_DEFAULT_PATH = "/vercel/runtimes/node24/bin:/vercel/runtimes/node22/bin:/usr/local/bin:/usr/bin:/bin";
7726
8231
  function bashOptionsForMode(bundle, runtime) {
7727
- const storageRoot = getRuntimeBundleStorageRoot(bundle);
7728
8232
  switch (bundle.sandbox.provider) {
7729
8233
  case "vercel-sandbox":
8234
+ case "remote-worker":
7730
8235
  return {
7731
8236
  operations: vercelBashOps(bundle.sandbox, {
7732
8237
  // The pi bash tool's env may include the host process env. Never
@@ -7735,16 +8240,20 @@ function bashOptionsForMode(bundle, runtime) {
7735
8240
  mergeEnv: () => mergeRuntimeEnv(runtime, { PATH: VERCEL_SAFE_DEFAULT_PATH })
7736
8241
  })
7737
8242
  };
7738
- case "bwrap":
8243
+ case "bwrap": {
8244
+ const storageRoot = getRuntimeBundleStorageRoot(bundle);
7739
8245
  return {
7740
8246
  operations: createLocalBashOperations(),
7741
8247
  spawnHook: bwrapSpawnHook(storageRoot, runtime)
7742
8248
  };
7743
- default:
8249
+ }
8250
+ default: {
8251
+ const storageRoot = getRuntimeBundleStorageRoot(bundle);
7744
8252
  return {
7745
8253
  operations: createLocalBashOperations(),
7746
8254
  spawnHook: directSpawnHook(storageRoot, runtime)
7747
8255
  };
8256
+ }
7748
8257
  }
7749
8258
  }
7750
8259
  function isRuntimeReady(readiness) {
@@ -7921,9 +8430,21 @@ var DEFAULT_BUFFER_SIZE = 1e3;
7921
8430
  function createFsEventBroadcaster(watcher, opts = {}) {
7922
8431
  const bufferSize = opts.bufferSize ?? DEFAULT_BUFFER_SIZE;
7923
8432
  const buffer = [];
7924
- const listeners = /* @__PURE__ */ new Set();
8433
+ const listeners = /* @__PURE__ */ new Map();
7925
8434
  let nextSeq = 1;
7926
8435
  let closed = false;
8436
+ let gapAfterSeq = null;
8437
+ const handleControlEvent = (event) => {
8438
+ if (closed || event.type !== "resync-required") return;
8439
+ gapAfterSeq = nextSeq - 1;
8440
+ buffer.length = 0;
8441
+ for (const { onResyncRequired } of [...listeners.values()]) {
8442
+ try {
8443
+ onResyncRequired?.();
8444
+ } catch {
8445
+ }
8446
+ }
8447
+ };
7927
8448
  const watcherUnsub = watcher.subscribe((change) => {
7928
8449
  if (closed) return;
7929
8450
  const env = {
@@ -7934,13 +8455,13 @@ function createFsEventBroadcaster(watcher, opts = {}) {
7934
8455
  };
7935
8456
  buffer.push(env);
7936
8457
  if (buffer.length > bufferSize) buffer.shift();
7937
- for (const l of [...listeners]) {
8458
+ for (const l of [...listeners.keys()]) {
7938
8459
  try {
7939
8460
  l(env);
7940
8461
  } catch {
7941
8462
  }
7942
8463
  }
7943
- });
8464
+ }, { onControlEvent: handleControlEvent });
7944
8465
  return {
7945
8466
  subscribe(listener, sopts) {
7946
8467
  if (closed) {
@@ -7951,13 +8472,13 @@ function createFsEventBroadcaster(watcher, opts = {}) {
7951
8472
  let resyncRequired = false;
7952
8473
  if (typeof sopts?.lastSeenSeq === "number" && sopts.lastSeenSeq > 0) {
7953
8474
  const head = buffer.length > 0 ? buffer[0].seq : nextSeq;
7954
- if (sopts.lastSeenSeq < head - 1) {
8475
+ if (gapAfterSeq != null && sopts.lastSeenSeq <= gapAfterSeq || sopts.lastSeenSeq < head - 1) {
7955
8476
  resyncRequired = true;
7956
8477
  } else {
7957
8478
  replay = buffer.filter((e) => e.seq > sopts.lastSeenSeq);
7958
8479
  }
7959
8480
  }
7960
- listeners.add(listener);
8481
+ listeners.set(listener, { onResyncRequired: sopts?.onResyncRequired });
7961
8482
  return {
7962
8483
  replay,
7963
8484
  resyncRequired,
@@ -8030,7 +8551,10 @@ function fsEventsRoutes(app, opts, done) {
8030
8551
  try {
8031
8552
  sub = entry.broadcaster.subscribe(
8032
8553
  (env) => writeChange(reply.raw, env),
8033
- lastSeenSeq != null ? { lastSeenSeq } : void 0
8554
+ {
8555
+ ...lastSeenSeq != null ? { lastSeenSeq } : {},
8556
+ onResyncRequired: () => writeSse(reply.raw, "resync-required", {})
8557
+ }
8034
8558
  );
8035
8559
  } catch (error) {
8036
8560
  releaseBroadcaster(workspaceId, entry);
@@ -8041,10 +8565,6 @@ function fsEventsRoutes(app, opts, done) {
8041
8565
  }
8042
8566
  if (sub.resyncRequired) {
8043
8567
  writeSse(reply.raw, "resync-required", {});
8044
- sub.unsubscribe();
8045
- releaseBroadcaster(workspaceId, entry);
8046
- reply.raw.end();
8047
- return;
8048
8568
  }
8049
8569
  for (const env of sub.replay) writeChange(reply.raw, env);
8050
8570
  const heartbeat = setInterval(() => {
@@ -8684,6 +9204,7 @@ function statusCodeFromError(err) {
8684
9204
  }
8685
9205
  const parsedCode = ErrorCode.safeParse(err?.code);
8686
9206
  if (parsedCode.success && parsedCode.data === ErrorCode.enum.SESSION_NOT_FOUND) return 404;
9207
+ if (parsedCode.success && parsedCode.data === ErrorCode.enum.PAYMENT_REQUIRED) return 402;
8687
9208
  return 500;
8688
9209
  }
8689
9210
 
@@ -9129,9 +9650,10 @@ function gitRoutes(app, opts, done) {
9129
9650
  if (path4 === null) return;
9130
9651
  const workspaceRoot = await resolveWorkspaceRoot(request);
9131
9652
  if (!workspaceRoot) {
9132
- return reply.code(500).send({
9133
- error: { code: ERROR_CODE_INTERNAL, message: "workspace root unavailable" }
9134
- });
9653
+ return {
9654
+ enabled: false,
9655
+ reason: "Git file URLs are unavailable for this runtime."
9656
+ };
9135
9657
  }
9136
9658
  try {
9137
9659
  return await resolveGitFileUrl(workspaceRoot, path4);
@@ -9523,7 +10045,9 @@ var PiChatEventMapper = class {
9523
10045
  const mapped = [
9524
10046
  ...this.mapAgentEndFinalAssistant(event, turnId),
9525
10047
  ...this.mapAgentEndError(event, turnId, status),
9526
- this.event({ type: "agent-end", turnId, status })
10048
+ // willRetry marks a non-terminal end (auto-retry coming) so once-per-settle
10049
+ // consumers can ignore it; mirrors mapAgentEndError's own willRetry gate.
10050
+ this.event({ type: "agent-end", turnId, status, ...event.willRetry === true ? { willRetry: true } : {} })
9527
10051
  ];
9528
10052
  this.activeAssistantMessageId = void 0;
9529
10053
  this.toolCallMessageIds.clear();
@@ -10222,6 +10746,454 @@ function messageCreatedAtMs(message) {
10222
10746
  return Number.isFinite(timestamp) ? timestamp : void 0;
10223
10747
  }
10224
10748
 
10749
+ // src/server/pi-chat/metering.ts
10750
+ var meteringLogger = createLogger("pi-chat-metering");
10751
+ var defaultMeteringErrorLogger = (message, error) => {
10752
+ meteringLogger.warn(message, { error });
10753
+ };
10754
+ function promptRunId(sessionId, clientNonce) {
10755
+ return `pi-run:${sessionId}:prompt:${clientNonce}`;
10756
+ }
10757
+ function followUpRunId(sessionId, clientNonce, clientSeq) {
10758
+ return `pi-run:${sessionId}:followup:${clientNonce}:${clientSeq}`;
10759
+ }
10760
+ function followUpMatches(run2, selector) {
10761
+ const fu = run2.followUp;
10762
+ if (!fu) return false;
10763
+ if (selector.clientNonce !== void 0) return fu.clientNonce === selector.clientNonce;
10764
+ if (selector.clientSeq !== void 0) return fu.clientSeq === selector.clientSeq;
10765
+ return false;
10766
+ }
10767
+ function takeQueuedFollowUp(state, selector) {
10768
+ const index = state.queued.findIndex((run2) => followUpMatches(run2, selector));
10769
+ if (index < 0) return void 0;
10770
+ return state.queued.splice(index, 1)[0];
10771
+ }
10772
+ function isRecord3(value) {
10773
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10774
+ }
10775
+ function readTokenCount(value) {
10776
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
10777
+ }
10778
+ function normalizeMeteringUsage(value) {
10779
+ if (!isRecord3(value)) return void 0;
10780
+ const cost = isRecord3(value.cost) ? value.cost : {};
10781
+ return {
10782
+ input: readTokenCount(value.input),
10783
+ output: readTokenCount(value.output),
10784
+ cacheRead: readTokenCount(value.cacheRead),
10785
+ cacheWrite: readTokenCount(value.cacheWrite),
10786
+ cost: {
10787
+ input: readTokenCount(cost.input),
10788
+ output: readTokenCount(cost.output),
10789
+ cacheRead: readTokenCount(cost.cacheRead),
10790
+ cacheWrite: readTokenCount(cost.cacheWrite),
10791
+ total: readTokenCount(cost.total)
10792
+ }
10793
+ };
10794
+ }
10795
+ var PiChatMeteringCoordinator = class {
10796
+ sessions = /* @__PURE__ */ new Map();
10797
+ inflightOps = /* @__PURE__ */ new Set();
10798
+ runInstances = 0;
10799
+ sink;
10800
+ logError;
10801
+ constructor(sink, logError) {
10802
+ this.sink = sink;
10803
+ this.logError = logError ?? defaultMeteringErrorLogger;
10804
+ }
10805
+ /** Reserve a prompt run. Throws (fail closed) when the sink rejects. */
10806
+ async reservePrompt(input) {
10807
+ const state = this.sessionState(input.sessionId);
10808
+ const runId = promptRunId(input.sessionId, input.clientNonce);
10809
+ const existing = this.findRun(state, runId);
10810
+ if (existing) {
10811
+ await existing.reservation;
10812
+ return "duplicate";
10813
+ }
10814
+ const run2 = this.createRun(input, "prompt", runId);
10815
+ state.pendingPrompts.push(run2);
10816
+ return this.materializeReservation(state, run2, input);
10817
+ }
10818
+ /** Reserve a follow-up run. Throws (fail closed) when the sink rejects.
10819
+ * Returns 'duplicate' when this selector already has a tracked/consumed run. */
10820
+ async reserveFollowUp(input) {
10821
+ const state = this.sessionState(input.sessionId);
10822
+ const runId = followUpRunId(input.sessionId, input.clientNonce, input.clientSeq);
10823
+ const existing = this.findRun(state, runId);
10824
+ if (existing) {
10825
+ await existing.reservation;
10826
+ return "duplicate";
10827
+ }
10828
+ if (state.consumedFollowUpNonces.has(input.clientNonce)) return "duplicate";
10829
+ const run2 = this.createRun(input, "followup", runId);
10830
+ run2.followUp = { clientNonce: input.clientNonce, clientSeq: input.clientSeq };
10831
+ state.queued.push(run2);
10832
+ return this.materializeReservation(state, run2, input);
10833
+ }
10834
+ /**
10835
+ * Acquire the reservation for a freshly-registered run. The reserve is put on
10836
+ * the run's ops chain so a concurrent release/settle is ordered strictly
10837
+ * after the reservation row exists. Returns 'cancelled' (skip execution) when
10838
+ * a concurrent stop/interrupt/delete terminated the run while the reserve was
10839
+ * in flight; throws (fail closed) when the sink rejects.
10840
+ */
10841
+ async materializeReservation(state, run2, input) {
10842
+ run2.reservation = this.applyReservation(run2, input);
10843
+ const reserveOp = run2.reservation.catch(() => {
10844
+ });
10845
+ run2.ops = reserveOp;
10846
+ this.inflightOps.add(reserveOp);
10847
+ void reserveOp.finally(() => this.inflightOps.delete(reserveOp));
10848
+ try {
10849
+ await run2.reservation;
10850
+ } catch (err) {
10851
+ this.removeReservingRun(state, run2, input.sessionId);
10852
+ throw err;
10853
+ }
10854
+ return run2.terminal ? "cancelled" : "created";
10855
+ }
10856
+ /** True while a non-terminal prompt run exists for this nonce (accept →
10857
+ * agent-end). Used to suppress duplicate-nonce execution for the full run
10858
+ * lifetime, not just until the user message-start is consumed. */
10859
+ hasPromptRun(sessionId, clientNonce) {
10860
+ const state = this.sessions.get(sessionId);
10861
+ if (!state) return false;
10862
+ const run2 = this.findRun(state, promptRunId(sessionId, clientNonce));
10863
+ return run2 !== void 0 && !run2.terminal;
10864
+ }
10865
+ /** True while a non-terminal follow-up run exists for this selector (queued
10866
+ * or consumed-and-active). Used to suppress duplicate follow-up enqueues. */
10867
+ hasFollowUpRun(sessionId, selector) {
10868
+ const state = this.sessions.get(sessionId);
10869
+ if (!state) return false;
10870
+ if (selector.clientNonce !== void 0 && state.consumedFollowUpNonces.has(selector.clientNonce)) return true;
10871
+ if (state.queued.some((run2) => followUpMatches(run2, selector))) return true;
10872
+ const active = state.active;
10873
+ return active !== void 0 && !active.terminal && active.followUp !== void 0 && followUpMatches(active, selector);
10874
+ }
10875
+ /** The accepted prompt failed before/without running (sync throw or run rejection). */
10876
+ failPromptRun(sessionId, clientNonce) {
10877
+ const state = this.sessions.get(sessionId);
10878
+ if (!state) return;
10879
+ const runId = promptRunId(sessionId, clientNonce);
10880
+ const pendingIndex = state.pendingPrompts.findIndex((run2) => run2.scope.runId === runId);
10881
+ if (pendingIndex >= 0) {
10882
+ const [run2] = state.pendingPrompts.splice(pendingIndex, 1);
10883
+ if (run2) this.finishRun(run2, "error");
10884
+ } else if (state.active?.scope.runId === runId) {
10885
+ this.finishRun(state.active, "error");
10886
+ state.active = void 0;
10887
+ }
10888
+ this.pruneSession(sessionId, state);
10889
+ }
10890
+ /** A queued follow-up was rejected by the adapter before being queued. */
10891
+ failFollowUpRun(sessionId, selector) {
10892
+ const state = this.sessions.get(sessionId);
10893
+ if (!state) return;
10894
+ const run2 = takeQueuedFollowUp(state, selector);
10895
+ if (!run2) return;
10896
+ this.release(run2, "run-rejected");
10897
+ this.pruneSession(sessionId, state);
10898
+ }
10899
+ /**
10900
+ * A queued follow-up is being re-posted as a plain prompt (interrupt
10901
+ * fallback for runtimes without continueQueuedFollowUp). No
10902
+ * `followup-consumed` event will arrive, so bind its reservation to the
10903
+ * next agent-start instead.
10904
+ */
10905
+ promoteQueuedToPrompt(sessionId, selector) {
10906
+ const state = this.sessions.get(sessionId);
10907
+ if (!state) return;
10908
+ const run2 = takeQueuedFollowUp(state, selector);
10909
+ if (!run2) return;
10910
+ state.pendingPrompts.push(run2);
10911
+ }
10912
+ /**
10913
+ * A promoted-to-prompt follow-up failed before agent-start (the fallback
10914
+ * repost rejected). Release its reservation instead of stranding it in
10915
+ * pendingPrompts, where a later agent-start would otherwise misattribute
10916
+ * usage to it.
10917
+ */
10918
+ failPromotedFollowUp(sessionId, selector) {
10919
+ const state = this.sessions.get(sessionId);
10920
+ if (!state || selector.clientNonce === void 0 || selector.clientSeq === void 0) return;
10921
+ const runId = followUpRunId(sessionId, selector.clientNonce, selector.clientSeq);
10922
+ const index = state.pendingPrompts.findIndex((run3) => run3.scope.runId === runId);
10923
+ if (index < 0) return;
10924
+ const [run2] = state.pendingPrompts.splice(index, 1);
10925
+ if (run2) this.release(run2, "run-rejected");
10926
+ this.pruneSession(sessionId, state);
10927
+ }
10928
+ /**
10929
+ * Release prompt runs reserved but not yet bound to an agent-start —
10930
+ * e.g. a stop/interrupt landing in the window between acceptance and the
10931
+ * native agent_start. Without this they would sit `active` in the store
10932
+ * until their TTL, holding the user's balance. No charge.
10933
+ */
10934
+ releasePending(sessionId) {
10935
+ const state = this.sessions.get(sessionId);
10936
+ if (!state) return;
10937
+ for (const run2 of state.pendingPrompts) this.release(run2, "cancelled");
10938
+ state.pendingPrompts = [];
10939
+ this.pruneSession(sessionId, state);
10940
+ }
10941
+ /** Queue cleared via selector or entirely; release affected reservations. */
10942
+ releaseQueued(sessionId, selector) {
10943
+ const state = this.sessions.get(sessionId);
10944
+ if (!state) return;
10945
+ if (selector) {
10946
+ const run2 = takeQueuedFollowUp(state, selector);
10947
+ if (run2) this.release(run2, "queue-cleared");
10948
+ } else {
10949
+ for (const run2 of state.queued) this.release(run2, "queue-cleared");
10950
+ state.queued = [];
10951
+ }
10952
+ this.pruneSession(sessionId, state);
10953
+ }
10954
+ /** Session deleted: tear down every non-terminal run without charging. */
10955
+ releaseSession(sessionId) {
10956
+ const state = this.sessions.get(sessionId);
10957
+ if (!state) return;
10958
+ for (const run2 of state.queued) this.release(run2, "cancelled");
10959
+ for (const run2 of state.pendingPrompts) this.release(run2, "cancelled");
10960
+ if (state.active) this.finishRun(state.active, "aborted");
10961
+ this.sessions.delete(sessionId);
10962
+ }
10963
+ /**
10964
+ * Feed one native adapter event and its mapped PiChatEvents through the
10965
+ * correlation state machine. Must be called after the mapped events were
10966
+ * published (the mapper assigns turn ids during mapping).
10967
+ */
10968
+ observe(sessionId, nativeEvent, mappedEvents) {
10969
+ const state = this.sessions.get(sessionId);
10970
+ if (!state) return;
10971
+ for (const event of mappedEvents) {
10972
+ switch (event.type) {
10973
+ case "agent-start": {
10974
+ let next = state.pendingPrompts.shift();
10975
+ while (next && next.terminal) next = state.pendingPrompts.shift();
10976
+ if (next) {
10977
+ if (state.active && !state.active.terminal) this.finishRun(state.active, "ok");
10978
+ state.active = next;
10979
+ }
10980
+ break;
10981
+ }
10982
+ case "message-start": {
10983
+ if (event.role === "user" && event.clientSeq !== void 0) {
10984
+ this.consumeFollowUp(state, event);
10985
+ }
10986
+ break;
10987
+ }
10988
+ case "followup-consumed": {
10989
+ this.consumeFollowUp(state, event);
10990
+ break;
10991
+ }
10992
+ case "auto-retry-start": {
10993
+ if (state.active) {
10994
+ state.active.recordedMessageIds.clear();
10995
+ state.active.lastIdlessUsageKey = void 0;
10996
+ }
10997
+ break;
10998
+ }
10999
+ case "agent-end": {
11000
+ this.harvestAgentEndUsage(state, nativeEvent);
11001
+ if (isRecord3(nativeEvent) && nativeEvent.willRetry === true) break;
11002
+ if (state.active && !state.active.terminal) this.finishRun(state.active, event.status);
11003
+ state.active = void 0;
11004
+ break;
11005
+ }
11006
+ default:
11007
+ break;
11008
+ }
11009
+ }
11010
+ this.observeMessageEndUsage(state, nativeEvent);
11011
+ this.pruneSession(sessionId, state);
11012
+ }
11013
+ /** Promote a queued follow-up to the active run, settling the previous one. */
11014
+ consumeFollowUp(state, selector) {
11015
+ const run2 = takeQueuedFollowUp(state, selector);
11016
+ if (!run2) return;
11017
+ if (run2.followUp?.clientNonce) state.consumedFollowUpNonces.add(run2.followUp.clientNonce);
11018
+ if (state.active && !state.active.terminal) this.finishRun(state.active, "ok");
11019
+ state.active = run2;
11020
+ }
11021
+ /** Test/diagnostic hook: resolves after every queued sink call settles. */
11022
+ async flush() {
11023
+ while (this.inflightOps.size > 0) {
11024
+ await Promise.all([...this.inflightOps]);
11025
+ }
11026
+ }
11027
+ observeMessageEndUsage(state, nativeEvent) {
11028
+ if (!isRecord3(nativeEvent) || nativeEvent.type !== "message_end") return;
11029
+ this.recordAssistantUsage(state, nativeEvent.message);
11030
+ }
11031
+ /**
11032
+ * Some failure/abort paths never emit message_end; the final assistant
11033
+ * message (and its usage) only rides on agent_end's messages array. Runs
11034
+ * with already-recorded usage dedupe via recordedMessageIds.
11035
+ */
11036
+ harvestAgentEndUsage(state, nativeEvent) {
11037
+ if (!isRecord3(nativeEvent) || !Array.isArray(nativeEvent.messages)) return;
11038
+ for (let index = nativeEvent.messages.length - 1; index >= 0; index -= 1) {
11039
+ const message = nativeEvent.messages[index];
11040
+ if (!isRecord3(message) || message.role !== "assistant") continue;
11041
+ this.recordAssistantUsage(state, message, { isAgentEndFinal: true });
11042
+ return;
11043
+ }
11044
+ }
11045
+ recordAssistantUsage(state, message, opts = {}) {
11046
+ if (!isRecord3(message) || message.role !== "assistant") return;
11047
+ const usage = normalizeMeteringUsage(message.usage);
11048
+ if (!usage) return;
11049
+ const run2 = state.active ?? state.pendingPrompts[0];
11050
+ if (!run2 || run2.terminal) {
11051
+ this.logError("assistant usage arrived with no reserved run; usage not metered", { messageRole: "assistant" });
11052
+ return;
11053
+ }
11054
+ const messageId = typeof message.id === "string" && message.id.length > 0 ? message.id : void 0;
11055
+ if (messageId) {
11056
+ if (run2.recordedMessageIds.has(messageId)) return;
11057
+ run2.recordedMessageIds.add(messageId);
11058
+ } else {
11059
+ const stopReason2 = typeof message.stopReason === "string" ? message.stopReason : "";
11060
+ const signature = `sig:${usage.input}:${usage.output}:${usage.cacheRead}:${usage.cacheWrite}:${usage.cost.total}:${stopReason2}`;
11061
+ if (opts.isAgentEndFinal && signature === run2.lastIdlessUsageKey) return;
11062
+ run2.lastIdlessUsageKey = signature;
11063
+ }
11064
+ run2.usageCount += 1;
11065
+ const model = typeof message.model === "string" && message.model.length > 0 ? message.model : void 0;
11066
+ const provider = typeof message.provider === "string" && message.provider.length > 0 ? message.provider : void 0;
11067
+ const stopReason = typeof message.stopReason === "string" ? message.stopReason : void 0;
11068
+ 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}`;
11069
+ this.enqueue(
11070
+ run2,
11071
+ async () => {
11072
+ try {
11073
+ const result = await this.sink.recordUsage({
11074
+ ...run2.scope,
11075
+ reservationId: run2.reservationId,
11076
+ usageId,
11077
+ messageId,
11078
+ model: model || provider ? { provider, id: model } : void 0,
11079
+ usage,
11080
+ stopReason
11081
+ });
11082
+ if (result.billedMicros > 0) run2.billableUsageCount += 1;
11083
+ } catch (error) {
11084
+ const couldBill = usage.input + usage.output + usage.cacheRead + usage.cacheWrite > 0 || usage.cost.total > 0;
11085
+ if (couldBill) run2.usageWriteFailed = true;
11086
+ throw error;
11087
+ }
11088
+ },
11089
+ "recordUsage failed"
11090
+ );
11091
+ }
11092
+ /** Build a run synchronously (no await) so it can be registered before the
11093
+ * reserve sink call, closing the concurrent-duplicate race. */
11094
+ createRun(input, kind, runId) {
11095
+ return {
11096
+ scope: {
11097
+ workspaceId: input.workspaceId,
11098
+ userId: input.userId,
11099
+ sessionId: input.sessionId,
11100
+ runId,
11101
+ source: "pi-chat"
11102
+ },
11103
+ kind,
11104
+ reservationId: void 0,
11105
+ instanceId: this.runInstances += 1,
11106
+ usageCount: 0,
11107
+ billableUsageCount: 0,
11108
+ recordedMessageIds: /* @__PURE__ */ new Set(),
11109
+ lastIdlessUsageKey: void 0,
11110
+ usageWriteFailed: false,
11111
+ reservation: Promise.resolve(),
11112
+ terminal: false,
11113
+ ops: Promise.resolve()
11114
+ };
11115
+ }
11116
+ /** Acquire the host reservation; throws (fail closed) on a rejecting sink. */
11117
+ async applyReservation(run2, input) {
11118
+ const result = await this.sink.reserveRun({
11119
+ ...run2.scope,
11120
+ kind: run2.kind,
11121
+ message: input.message,
11122
+ model: input.model
11123
+ });
11124
+ run2.reservationId = result?.reservationId;
11125
+ }
11126
+ /** Remove a still-reserving run whose reservation failed, from either list. */
11127
+ removeReservingRun(state, run2, sessionId) {
11128
+ const pendingIndex = state.pendingPrompts.indexOf(run2);
11129
+ if (pendingIndex >= 0) state.pendingPrompts.splice(pendingIndex, 1);
11130
+ const queuedIndex = state.queued.indexOf(run2);
11131
+ if (queuedIndex >= 0) state.queued.splice(queuedIndex, 1);
11132
+ this.pruneSession(sessionId, state);
11133
+ }
11134
+ findRun(state, runId) {
11135
+ if (state.active && !state.active.terminal && state.active.scope.runId === runId) return state.active;
11136
+ const pending = state.pendingPrompts.find((run2) => run2.scope.runId === runId);
11137
+ if (pending) return pending;
11138
+ return state.queued.find((run2) => run2.scope.runId === runId);
11139
+ }
11140
+ finishRun(run2, status) {
11141
+ if (run2.terminal) return;
11142
+ run2.terminal = true;
11143
+ this.enqueue(
11144
+ run2,
11145
+ () => {
11146
+ if (run2.usageWriteFailed) {
11147
+ return this.sink.releaseRun({ ...run2.scope, reservationId: run2.reservationId, reason: "usage-write-failed" });
11148
+ }
11149
+ if (run2.billableUsageCount > 0) {
11150
+ return this.sink.settleRun({ ...run2.scope, reservationId: run2.reservationId, status });
11151
+ }
11152
+ const didPaidWork = status === "ok";
11153
+ if (didPaidWork) {
11154
+ return this.sink.releaseRun({ ...run2.scope, reservationId: run2.reservationId, reason: "fallback-hold-charge" });
11155
+ }
11156
+ return this.sink.releaseRun({
11157
+ ...run2.scope,
11158
+ reservationId: run2.reservationId,
11159
+ reason: status === "error" ? "error-before-usage" : "cancelled"
11160
+ });
11161
+ },
11162
+ "finishRun failed"
11163
+ );
11164
+ }
11165
+ release(run2, reason) {
11166
+ if (run2.terminal) return;
11167
+ run2.terminal = true;
11168
+ this.enqueue(
11169
+ run2,
11170
+ () => this.sink.releaseRun({ ...run2.scope, reservationId: run2.reservationId, reason }),
11171
+ "releaseRun failed"
11172
+ );
11173
+ }
11174
+ enqueue(run2, op, failureMessage) {
11175
+ const chained = run2.ops.then(op).catch((error) => {
11176
+ this.logError(`${failureMessage} (run ${run2.scope.runId})`, error);
11177
+ });
11178
+ run2.ops = chained;
11179
+ this.inflightOps.add(chained);
11180
+ void chained.finally(() => this.inflightOps.delete(chained));
11181
+ }
11182
+ sessionState(sessionId) {
11183
+ let state = this.sessions.get(sessionId);
11184
+ if (!state) {
11185
+ state = { pendingPrompts: [], queued: [], consumedFollowUpNonces: /* @__PURE__ */ new Set() };
11186
+ this.sessions.set(sessionId, state);
11187
+ }
11188
+ return state;
11189
+ }
11190
+ pruneSession(sessionId, state) {
11191
+ if (!state.active && state.pendingPrompts.length === 0 && state.queued.length === 0 && state.consumedFollowUpNonces.size === 0) {
11192
+ this.sessions.delete(sessionId);
11193
+ }
11194
+ }
11195
+ };
11196
+
10225
11197
  // src/server/pi-chat/harnessPiChatService.ts
10226
11198
  var HarnessPiChatService = class {
10227
11199
  harness;
@@ -10237,10 +11209,16 @@ var HarnessPiChatService = class {
10237
11209
  activePromptRuns = /* @__PURE__ */ new Map();
10238
11210
  syntheticPromptFailures = /* @__PURE__ */ new Map();
10239
11211
  activeSyntheticPromptErrors = /* @__PURE__ */ new Map();
11212
+ metering;
10240
11213
  constructor(options) {
10241
11214
  this.harness = options.harness;
10242
11215
  this.sessionStore = options.sessionStore;
10243
11216
  this.workdir = options.workdir;
11217
+ this.metering = options.metering ? new PiChatMeteringCoordinator(options.metering, options.meteringLogger) : void 0;
11218
+ }
11219
+ /** Test/diagnostic hook: resolves once queued metering sink calls settle. */
11220
+ async flushMetering() {
11221
+ await this.metering?.flush();
10244
11222
  }
10245
11223
  async listSessions(ctx, options) {
10246
11224
  return this.sessionStore.list(toSessionCtx(ctx), options);
@@ -10249,8 +11227,16 @@ var HarnessPiChatService = class {
10249
11227
  return this.sessionStore.create(toSessionCtx(ctx), init);
10250
11228
  }
10251
11229
  async deleteSession(ctx, sessionId) {
10252
- this.channels.get(sessionId)?.unsubscribe();
11230
+ const channel = this.channels.get(sessionId);
11231
+ if (channel) {
11232
+ const activeRun = this.activePromptRuns.get(sessionId);
11233
+ await channel.adapter.abort();
11234
+ await activeRun?.catch(() => {
11235
+ });
11236
+ }
11237
+ channel?.unsubscribe();
10253
11238
  this.channels.delete(sessionId);
11239
+ this.metering?.releaseSession(sessionId);
10254
11240
  this.messageMetadata.clearSession(sessionId);
10255
11241
  this.syntheticPromptFailures.delete(sessionId);
10256
11242
  this.activeSyntheticPromptErrors.delete(sessionId);
@@ -10300,16 +11286,35 @@ var HarnessPiChatService = class {
10300
11286
  async prompt(ctx, sessionId, payload) {
10301
11287
  const adapter = await this.getAdapter(ctx, sessionId, payload);
10302
11288
  await this.ensureChannel(ctx, sessionId, adapter);
11289
+ const outcome = await this.metering?.reservePrompt({
11290
+ workspaceId: ctx.workspaceId,
11291
+ userId: ctx.authSubject,
11292
+ sessionId,
11293
+ clientNonce: payload.clientNonce,
11294
+ message: payload.message,
11295
+ model: payload.model
11296
+ }) ?? "created";
11297
+ if (outcome === "duplicate") {
11298
+ return {
11299
+ accepted: true,
11300
+ cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0,
11301
+ clientNonce: payload.clientNonce,
11302
+ duplicate: true
11303
+ };
11304
+ }
11305
+ if (outcome === "cancelled") throw promptCancelledError();
10303
11306
  this.messageMetadata.recordPrompt(sessionId, payload);
10304
11307
  const channel = this.channels.get(sessionId);
10305
11308
  const receiptCursor = nextPromptReceiptCursor(channel);
10306
11309
  try {
10307
11310
  const run2 = this.trackActiveRun(sessionId, adapter.prompt(toPiPromptInput(payload)));
10308
11311
  run2.catch((error) => {
11312
+ this.metering?.failPromptRun(sessionId, payload.clientNonce);
10309
11313
  if (!this.messageMetadata.hasPrompt(sessionId, { clientNonce: payload.clientNonce, displayText: payload.displayMessage ?? payload.message })) return;
10310
11314
  this.publishPromptRunError(sessionId, channel, payload, error);
10311
11315
  });
10312
11316
  } catch (err) {
11317
+ this.metering?.failPromptRun(sessionId, payload.clientNonce);
10313
11318
  this.messageMetadata.removePrompt(sessionId, { clientNonce: payload.clientNonce });
10314
11319
  throw err;
10315
11320
  }
@@ -10318,6 +11323,25 @@ var HarnessPiChatService = class {
10318
11323
  async followUp(ctx, sessionId, payload) {
10319
11324
  const adapter = await this.getAdapter(ctx, sessionId, payload.message);
10320
11325
  await this.ensureChannel(ctx, sessionId, adapter);
11326
+ const outcome = await this.metering?.reserveFollowUp({
11327
+ workspaceId: ctx.workspaceId,
11328
+ userId: ctx.authSubject,
11329
+ sessionId,
11330
+ clientNonce: payload.clientNonce,
11331
+ clientSeq: payload.clientSeq,
11332
+ message: payload.message
11333
+ }) ?? "created";
11334
+ if (outcome === "duplicate") {
11335
+ return {
11336
+ accepted: true,
11337
+ queued: true,
11338
+ cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0,
11339
+ clientNonce: payload.clientNonce,
11340
+ clientSeq: payload.clientSeq,
11341
+ duplicate: true
11342
+ };
11343
+ }
11344
+ if (outcome === "cancelled") throw promptCancelledError();
10321
11345
  this.messageMetadata.recordFollowUp(sessionId, payload);
10322
11346
  try {
10323
11347
  await adapter.followUp(payload.message, {
@@ -10326,6 +11350,7 @@ var HarnessPiChatService = class {
10326
11350
  clientSeq: payload.clientSeq
10327
11351
  });
10328
11352
  } catch (err) {
11353
+ this.metering?.failFollowUpRun(sessionId, payload);
10329
11354
  this.messageMetadata.removeFollowUp(sessionId, payload);
10330
11355
  throw err;
10331
11356
  }
@@ -10337,10 +11362,14 @@ var HarnessPiChatService = class {
10337
11362
  const before = adapter.readSnapshot().followUpMessages.length;
10338
11363
  adapter.clearFollowUp(payload);
10339
11364
  const after = adapter.readSnapshot().followUpMessages.length;
10340
- if (after < before) this.messageMetadata.removeFollowUp(sessionId, payload);
11365
+ if (after < before) {
11366
+ this.messageMetadata.removeFollowUp(sessionId, payload);
11367
+ this.metering?.releaseQueued(sessionId, payload);
11368
+ }
10341
11369
  return { accepted: true, cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0, cleared: Math.max(0, before - after) };
10342
11370
  }
10343
11371
  const clearedQueue = this.clearAllFollowUps(adapter, sessionId);
11372
+ this.metering?.releaseQueued(sessionId);
10344
11373
  return { accepted: true, cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0, cleared: clearedQueue.length };
10345
11374
  }
10346
11375
  async interrupt(ctx, sessionId, _payload) {
@@ -10353,12 +11382,15 @@ var HarnessPiChatService = class {
10353
11382
  if (wasActive) await adapter.abort();
10354
11383
  await activeRun?.catch(() => {
10355
11384
  });
11385
+ this.metering?.releasePending(sessionId);
10356
11386
  if (nextFollowUp) await this.autoPostInterruptedFollowUp(sessionId, adapter, nextFollowUp);
10357
11387
  return { accepted: true, cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0 };
10358
11388
  }
10359
11389
  async stop(ctx, sessionId, _payload) {
10360
11390
  const adapter = await this.getAdapter(ctx, sessionId, "");
10361
11391
  const clearedQueue = this.clearAllFollowUps(adapter, sessionId);
11392
+ this.metering?.releaseQueued(sessionId);
11393
+ this.metering?.releasePending(sessionId);
10362
11394
  await adapter.abort();
10363
11395
  return { accepted: true, stopped: true, cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0, clearedQueue: buildPiChatQueuedFollowUps(sessionId, clearedQueue) };
10364
11396
  }
@@ -10380,13 +11412,24 @@ var HarnessPiChatService = class {
10380
11412
  const metadata = this.messageMetadata.findFollowUpForQueueItem(sessionId, followUp);
10381
11413
  this.messageMetadata.recordConsumingFollowUp(sessionId, followUp, metadata?.serverText);
10382
11414
  if (adapter.continueQueuedFollowUp) {
10383
- await this.trackActiveRun(sessionId, adapter.continueQueuedFollowUp());
11415
+ try {
11416
+ await this.trackActiveRun(sessionId, adapter.continueQueuedFollowUp());
11417
+ } catch (err) {
11418
+ this.metering?.failFollowUpRun(sessionId, followUp);
11419
+ throw err;
11420
+ }
10384
11421
  return;
10385
11422
  }
10386
11423
  if (!this.canClearAutoPostedFollowUpForFallback(adapter, followUp)) {
10387
11424
  throw new AutoPostFollowUpError("Cannot auto-post queued follow-up because this runtime cannot safely remove only the consumed queued item.");
10388
11425
  }
10389
- await this.runPrompt(sessionId, adapter, metadata?.serverText ?? followUp.displayText);
11426
+ this.metering?.promoteQueuedToPrompt(sessionId, followUp);
11427
+ try {
11428
+ await this.runPrompt(sessionId, adapter, metadata?.serverText ?? followUp.displayText);
11429
+ } catch (err) {
11430
+ this.metering?.failPromotedFollowUp(sessionId, followUp);
11431
+ throw err;
11432
+ }
10390
11433
  this.clearAutoPostedFollowUpForFallback(sessionId, adapter, followUp);
10391
11434
  }
10392
11435
  async runPrompt(sessionId, adapter, input) {
@@ -10531,10 +11574,14 @@ var HarnessPiChatService = class {
10531
11574
  const channel = { buffer, adapter, unsubscribe: () => {
10532
11575
  }, mapper, messageTurnIds: /* @__PURE__ */ new Map() };
10533
11576
  const unsubscribe = adapter.subscribe((event) => {
10534
- for (const mapped of mapper.map(event)) {
11577
+ const mappedEvents = mapper.map(event);
11578
+ const enrichedEvents = [];
11579
+ for (const mapped of mappedEvents) {
10535
11580
  const enriched = this.messageMetadata.enrichEvent(sessionId, mapped);
11581
+ enrichedEvents.push(enriched);
10536
11582
  this.publishChannelEvent(sessionId, channel, enriched);
10537
11583
  }
11584
+ this.metering?.observe(sessionId, event, enrichedEvents);
10538
11585
  });
10539
11586
  channel.unsubscribe = unsubscribe;
10540
11587
  this.channels.set(sessionId, channel);
@@ -10543,6 +11590,13 @@ var HarnessPiChatService = class {
10543
11590
  };
10544
11591
  var AutoPostFollowUpError = class extends Error {
10545
11592
  };
11593
+ function promptCancelledError() {
11594
+ return Object.assign(new Error("request cancelled before execution"), {
11595
+ statusCode: 409,
11596
+ code: ErrorCode.enum.ABORTED,
11597
+ retryable: true
11598
+ });
11599
+ }
10546
11600
  function nextPromptReceiptCursor(channel) {
10547
11601
  return (channel?.buffer.latestSeq ?? 0) + 1;
10548
11602
  }
@@ -10675,7 +11729,8 @@ async function createAgentApp(opts = {}) {
10675
11729
  templatePath
10676
11730
  });
10677
11731
  const pluginTools = [];
10678
- if (modeAdapter.workspaceFsCapability === "strong") {
11732
+ const externalPluginsEnabled = opts.externalPlugins !== false;
11733
+ if (externalPluginsEnabled && modeAdapter.workspaceFsCapability === "strong") {
10679
11734
  const pluginResult = await loadPlugins({ cwd: workspaceRoot });
10680
11735
  if (pluginResult.errors.length > 0) {
10681
11736
  for (const e of pluginResult.errors) {
@@ -10706,13 +11761,13 @@ async function createAgentApp(opts = {}) {
10706
11761
  ...opts.disableDefaultFileTools ? [] : buildFilesystemAgentTools(runtimeBundle),
10707
11762
  ...opts.extraTools ?? [],
10708
11763
  ...pluginTools,
10709
- createPluginDiagnosticsTool({
11764
+ ...externalPluginsEnabled ? [createPluginDiagnosticsTool({
10710
11765
  getLastReloadDiagnostics: () => lastReloadDiagnostics,
10711
11766
  getHarness: () => harnessRef,
10712
11767
  ...opts.getPluginDiagnostics ? {
10713
11768
  getPluginErrors: () => opts.getPluginDiagnostics({ workspaceId: sessionId, workspaceRoot })
10714
11769
  } : {}
10715
- })
11770
+ })] : []
10716
11771
  ];
10717
11772
  const harnessFactory = opts.harnessFactory ?? ((input) => createPiCodingAgentHarness({
10718
11773
  ...input,
@@ -10751,11 +11806,17 @@ async function createAgentApp(opts = {}) {
10751
11806
  await app.register(fsEventsRoutes, { workspace: runtimeBundle.workspace });
10752
11807
  await app.register(treeRoutes, { workspace: runtimeBundle.workspace });
10753
11808
  await app.register(searchRoutes, { fileSearch: runtimeBundle.fileSearch });
10754
- await app.register(gitRoutes, { getWorkspaceRoot: () => getRuntimeBundleStorageRoot(runtimeBundle) });
11809
+ await app.register(gitRoutes, {
11810
+ getWorkspaceRoot: () => {
11811
+ if (runtimeBundle.sandbox.provider === "remote-worker") return void 0;
11812
+ return getRuntimeBundleStorageRoot(runtimeBundle);
11813
+ }
11814
+ });
10755
11815
  const piChatService = new HarnessPiChatService({
10756
11816
  harness,
10757
11817
  sessionStore: harness.sessions,
10758
- workdir: runtimeBundle.workspace.root
11818
+ workdir: runtimeBundle.workspace.root,
11819
+ metering: opts.metering
10759
11820
  });
10760
11821
  await app.register(piChatRoutes, { service: piChatService });
10761
11822
  await app.register(systemPromptRoutes, { harness });
@@ -10892,7 +11953,6 @@ function basenameForUpload2(filename) {
10892
11953
  }
10893
11954
  function buildUploadAgentTools(bundle) {
10894
11955
  const { workspace } = bundle;
10895
- const storageRoot = getRuntimeBundleStorageRoot(bundle);
10896
11956
  return [
10897
11957
  {
10898
11958
  name: "upload_file",
@@ -10921,7 +11981,7 @@ function buildUploadAgentTools(bundle) {
10921
11981
  const rawDir = typeof input.directory === "string" ? input.directory.trim() : "";
10922
11982
  const dir = rawDir ? rawDir.replace(/^\.\/+/, "").replace(/\/+$/, "") : DEFAULT_UPLOAD_DIR;
10923
11983
  try {
10924
- const bytes = workspace.readBinaryFile ? Buffer.from(await workspace.readBinaryFile(filePath)) : await readFile12(join14(storageRoot, filePath));
11984
+ const bytes = workspace.readBinaryFile ? Buffer.from(await workspace.readBinaryFile(filePath)) : await readFile12(join14(getRuntimeBundleStorageRoot(bundle), filePath));
10925
11985
  if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES2) {
10926
11986
  return {
10927
11987
  content: [{ type: "text", text: `file must be between 1 byte and ${MAX_UPLOAD_BYTES2} bytes` }],
@@ -11075,6 +12135,7 @@ var registerAgentRoutes = async (app, opts) => {
11075
12135
  });
11076
12136
  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
12137
  const sessionChangesTracker = new InMemorySessionChangesTracker();
12138
+ const externalPluginsEnabled = opts.externalPlugins !== false;
11078
12139
  const runtimeBindings = /* @__PURE__ */ new Map();
11079
12140
  const MAX_RUNTIME_BINDINGS = 256;
11080
12141
  function evictRuntimeBindings() {
@@ -11258,17 +12319,17 @@ var registerAgentRoutes = async (app, opts) => {
11258
12319
  }),
11259
12320
  ...buildFilesystemAgentTools(runtimeBundle),
11260
12321
  ...buildUploadAgentTools(runtimeBundle),
11261
- createPluginDiagnosticsTool({
12322
+ ...externalPluginsEnabled ? [createPluginDiagnosticsTool({
11262
12323
  // `binding` is assigned later in this function; read through thunks.
11263
12324
  getLastReloadDiagnostics: () => binding?.lastReloadDiagnostics ?? [],
11264
12325
  getHarness: () => binding?.harness,
11265
12326
  ...opts.getPluginDiagnostics ? {
11266
12327
  getPluginErrors: () => opts.getPluginDiagnostics({ workspaceId, workspaceRoot: root })
11267
12328
  } : {}
11268
- })
12329
+ })] : []
11269
12330
  ];
11270
12331
  const pluginTools = [];
11271
- if (modeAdapter.workspaceFsCapability === "strong") {
12332
+ if (externalPluginsEnabled && modeAdapter.workspaceFsCapability === "strong") {
11272
12333
  const pluginResult = await loadPlugins({ cwd: root });
11273
12334
  if (pluginResult.errors.length > 0) {
11274
12335
  for (const e of pluginResult.errors) {
@@ -11527,7 +12588,11 @@ var registerAgentRoutes = async (app, opts) => {
11527
12588
  getFileSearch: async (request) => (await getBindingForRequest(request)).runtimeBundle.fileSearch
11528
12589
  });
11529
12590
  await app.register(gitRoutes, {
11530
- getWorkspaceRoot: async (request) => getRuntimeBundleStorageRoot((await getBindingForRequest(request)).runtimeBundle)
12591
+ getWorkspaceRoot: async (request) => {
12592
+ const bundle = (await getBindingForRequest(request)).runtimeBundle;
12593
+ if (bundle.sandbox.provider === "remote-worker") return void 0;
12594
+ return getRuntimeBundleStorageRoot(bundle);
12595
+ }
11531
12596
  });
11532
12597
  await app.register(piChatRoutes, {
11533
12598
  getService: async (request) => {
@@ -11535,7 +12600,8 @@ var registerAgentRoutes = async (app, opts) => {
11535
12600
  binding.piChatService ??= new HarnessPiChatService({
11536
12601
  harness: binding.harness,
11537
12602
  sessionStore: binding.harness.sessions,
11538
- workdir: binding.runtimeBundle.workspace.root
12603
+ workdir: binding.runtimeBundle.workspace.root,
12604
+ metering: opts.metering
11539
12605
  });
11540
12606
  return binding.piChatService;
11541
12607
  }
@@ -11624,10 +12690,17 @@ var registerAgentRoutes = async (app, opts) => {
11624
12690
  export {
11625
12691
  FileHandleStore,
11626
12692
  PI_PACKAGE_RESOURCE_FILTERS,
12693
+ REMOTE_WORKER_PROVIDER,
12694
+ REMOTE_WORKER_RUNTIME_CWD,
12695
+ RemoteWorkerClient,
12696
+ RemoteWorkerClientError,
11627
12697
  UV_SETUP_COMMANDS,
11628
12698
  VERCEL_PROVISIONING_CACHE_ROOT,
11629
12699
  VERCEL_SANDBOX_WORKSPACE_ROOT,
11630
12700
  UV_SETUP_COMMANDS as VERCEL_UV_SETUP_COMMANDS,
12701
+ WORKER_INTERNAL_TOKEN_HEADER,
12702
+ WORKER_REQUEST_ID_HEADER,
12703
+ WORKER_WORKSPACE_ID_HEADER,
11631
12704
  applyCspHeaders,
11632
12705
  autoDetectMode,
11633
12706
  bakeSnapshotIfNeeded,
@@ -11635,21 +12708,28 @@ export {
11635
12708
  buildPackageHash,
11636
12709
  buildSnapshotRecipeHash,
11637
12710
  compactPiPackages,
12711
+ constantTimeTokenEqual,
11638
12712
  createAgentApp,
11639
12713
  createBwrapSandbox,
11640
12714
  createDirectSandbox,
11641
12715
  createLogger,
11642
12716
  createNodeWorkspace,
12717
+ createRemoteWorkerModeAdapter,
12718
+ createRemoteWorkerSandbox,
12719
+ createRemoteWorkerWorkspace,
11643
12720
  createResourceSettingsManager,
11644
12721
  createVercelDeploymentSnapshotProvider,
11645
12722
  createVercelProvisioningAdapter,
11646
12723
  createVercelSandboxWorkspace,
12724
+ decodeBytesFromWorker,
12725
+ encodeBytesForWorker,
11647
12726
  fileRoutes,
11648
12727
  getBoringAgentPathEntries,
11649
12728
  getBoringAgentRuntimeEnv,
11650
12729
  getBoringAgentRuntimePaths,
11651
12730
  hasBwrap,
11652
12731
  mergePiPackageSources,
12732
+ normalizeMeteringUsage,
11653
12733
  piPackageSourceKey,
11654
12734
  prepareDeploymentSnapshot,
11655
12735
  prepareVercelDeploymentSnapshot,