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