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