@hachej/boring-agent 0.1.31 → 0.1.32

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.
@@ -3,6 +3,10 @@ import {
3
3
  safeCapture,
4
4
  validateTool
5
5
  } from "../chunk-HDOSWEXG.js";
6
+ import {
7
+ dropEmptyAssistantUiMessages,
8
+ sanitizeUiMessages
9
+ } from "../chunk-IDRC4SFU.js";
6
10
  import {
7
11
  ErrorCode
8
12
  } from "../chunk-UBWQ5LT4.js";
@@ -100,23 +104,23 @@ function withWorkspacePythonEnv(opts) {
100
104
  var DEFAULT_TIMEOUT_MS = 3e4;
101
105
  var DEFAULT_MAX_OUTPUT_BYTES = 1048576;
102
106
  var TERMINATION_GRACE_MS = 5e3;
103
- function appendOutput(chunks, chunk2, state, onChunk) {
107
+ function appendOutput(chunks, chunk3, state, onChunk) {
104
108
  const remaining = state.maxBytes - state.capturedBytes;
105
109
  if (remaining <= 0) {
106
110
  state.truncated = true;
107
111
  return;
108
112
  }
109
- if (chunk2.length > remaining) {
110
- const partial = chunk2.subarray(0, remaining);
113
+ if (chunk3.length > remaining) {
114
+ const partial = chunk3.subarray(0, remaining);
111
115
  chunks.push(partial);
112
116
  state.capturedBytes += remaining;
113
117
  state.truncated = true;
114
118
  onChunk?.(new Uint8Array(partial));
115
119
  return;
116
120
  }
117
- chunks.push(chunk2);
118
- state.capturedBytes += chunk2.length;
119
- onChunk?.(new Uint8Array(chunk2));
121
+ chunks.push(chunk3);
122
+ state.capturedBytes += chunk3.length;
123
+ onChunk?.(new Uint8Array(chunk3));
120
124
  }
121
125
  function terminateProcess(child, signal) {
122
126
  const pid = child.pid;
@@ -157,7 +161,7 @@ function createDirectSandbox(opts = {}) {
157
161
  const maxOutputBytes = opts2?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
158
162
  const workspaceRoot = runtimeContext.runtimeCwd;
159
163
  const cwd = opts2?.cwd ?? workspaceRoot;
160
- return await new Promise((resolve10, reject) => {
164
+ return await new Promise((resolve11, reject) => {
161
165
  const child = spawn(cmd, {
162
166
  cwd,
163
167
  env: withWorkspacePythonEnv({ workspaceRoot, env: opts2?.env }),
@@ -186,7 +190,7 @@ function createDirectSandbox(opts = {}) {
186
190
  if (settled) return;
187
191
  settled = true;
188
192
  cleanup();
189
- resolve10({
193
+ resolve11({
190
194
  stdout: new Uint8Array(Buffer.concat(stdoutChunks)),
191
195
  stderr: new Uint8Array(Buffer.concat(stderrChunks)),
192
196
  exitCode: typeof exitCode === "number" ? exitCode : timedOut ? 124 : 1,
@@ -196,11 +200,11 @@ function createDirectSandbox(opts = {}) {
196
200
  stderrEncoding: "utf-8"
197
201
  });
198
202
  };
199
- child.stdout?.on("data", (chunk2) => {
200
- appendOutput(stdoutChunks, chunk2, captureState, opts2?.onStdout);
203
+ child.stdout?.on("data", (chunk3) => {
204
+ appendOutput(stdoutChunks, chunk3, captureState, opts2?.onStdout);
201
205
  });
202
- child.stderr?.on("data", (chunk2) => {
203
- appendOutput(stderrChunks, chunk2, captureState, opts2?.onStderr);
206
+ child.stderr?.on("data", (chunk3) => {
207
+ appendOutput(stderrChunks, chunk3, captureState, opts2?.onStderr);
204
208
  });
205
209
  child.on("error", (error) => {
206
210
  if (settled) return;
@@ -625,23 +629,23 @@ function createNodeWorkspace(root, opts = {}) {
625
629
  var DEFAULT_TIMEOUT_MS2 = BWRAP_TIMEOUT_SECONDS * 1e3;
626
630
  var DEFAULT_MAX_OUTPUT_BYTES2 = 1048576;
627
631
  var SANDBOX_HOME2 = "/workspace";
628
- function appendOutput2(chunks, chunk2, state, onChunk) {
632
+ function appendOutput2(chunks, chunk3, state, onChunk) {
629
633
  const remaining = state.maxBytes - state.capturedBytes;
630
634
  if (remaining <= 0) {
631
635
  state.truncated = true;
632
636
  return;
633
637
  }
634
- if (chunk2.length > remaining) {
635
- const partial = chunk2.subarray(0, remaining);
638
+ if (chunk3.length > remaining) {
639
+ const partial = chunk3.subarray(0, remaining);
636
640
  chunks.push(partial);
637
641
  state.capturedBytes += remaining;
638
642
  state.truncated = true;
639
643
  onChunk?.(new Uint8Array(partial));
640
644
  return;
641
645
  }
642
- chunks.push(chunk2);
643
- state.capturedBytes += chunk2.length;
644
- onChunk?.(new Uint8Array(chunk2));
646
+ chunks.push(chunk3);
647
+ state.capturedBytes += chunk3.length;
648
+ onChunk?.(new Uint8Array(chunk3));
645
649
  }
646
650
  function terminateProcess2(child, signal) {
647
651
  const pid = child.pid;
@@ -788,7 +792,7 @@ function createBwrapSandbox(opts = {}) {
788
792
  "-c",
789
793
  cmd
790
794
  ];
791
- return await new Promise((resolve10, reject) => {
795
+ return await new Promise((resolve11, reject) => {
792
796
  const child = spawn2("bwrap", args, {
793
797
  env: {
794
798
  ...withWorkspacePythonEnv({ workspaceRoot, env: opts2?.env, sandboxRoot: SANDBOX_HOME2 }),
@@ -818,7 +822,7 @@ function createBwrapSandbox(opts = {}) {
818
822
  if (settled) return;
819
823
  settled = true;
820
824
  cleanup();
821
- resolve10({
825
+ resolve11({
822
826
  stdout: new Uint8Array(Buffer.concat(stdoutChunks)),
823
827
  stderr: new Uint8Array(Buffer.concat(stderrChunks)),
824
828
  exitCode: typeof exitCode === "number" ? exitCode : timedOut ? 124 : 1,
@@ -828,11 +832,11 @@ function createBwrapSandbox(opts = {}) {
828
832
  stderrEncoding: "utf-8"
829
833
  });
830
834
  };
831
- child.stdout?.on("data", (chunk2) => {
832
- appendOutput2(stdoutChunks, chunk2, captureState, opts2?.onStdout);
835
+ child.stdout?.on("data", (chunk3) => {
836
+ appendOutput2(stdoutChunks, chunk3, captureState, opts2?.onStdout);
833
837
  });
834
- child.stderr?.on("data", (chunk2) => {
835
- appendOutput2(stderrChunks, chunk2, captureState, opts2?.onStderr);
838
+ child.stderr?.on("data", (chunk3) => {
839
+ appendOutput2(stderrChunks, chunk3, captureState, opts2?.onStderr);
836
840
  });
837
841
  child.on("error", (error) => {
838
842
  if (settled) return;
@@ -1618,7 +1622,7 @@ async function prepareVercelDeploymentSnapshot(opts) {
1618
1622
  }
1619
1623
 
1620
1624
  // src/server/http/routes/file.ts
1621
- import { dirname as dirname5, extname, relative as relative4 } from "path/posix";
1625
+ import { dirname as dirname5, extname as extname2, relative as relative4 } from "path/posix";
1622
1626
 
1623
1627
  // src/server/http/middleware.ts
1624
1628
  var ERROR_CODE_AUTH_REQUIRED = "auth_required";
@@ -1783,6 +1787,211 @@ function createLogger(prefix) {
1783
1787
  };
1784
1788
  }
1785
1789
 
1790
+ // src/server/http/routes/fileRecords.ts
1791
+ import { extname } from "path/posix";
1792
+ var MAX_RECORD_FILE_BYTES = 2 * 1024 * 1024;
1793
+ var MAX_RECORD_ROWS_SCANNED = 1e4;
1794
+ var MAX_RECORD_ROWS_RETURNED = 100;
1795
+ var DEFAULT_RECORD_ROWS_RETURNED = 50;
1796
+ var MAX_RECORD_OUTPUT_BYTES = 512 * 1024;
1797
+ var MAX_RECORD_QUERY_LENGTH = 256;
1798
+ var MAX_RECORD_COLUMNS = 100;
1799
+ var MAX_RECORD_COLUMN_SAMPLE_ROWS = 100;
1800
+ var FileRecordsValidationError = class extends Error {
1801
+ field;
1802
+ constructor(message, field) {
1803
+ super(message);
1804
+ this.name = "FileRecordsValidationError";
1805
+ this.field = field;
1806
+ }
1807
+ };
1808
+ function firstQueryValue(value) {
1809
+ return Array.isArray(value) ? value[0] : value;
1810
+ }
1811
+ function requireStringQueryParam(value, field) {
1812
+ if (typeof value !== "string" || value.length === 0) {
1813
+ throw new FileRecordsValidationError(`${field} is required`, field);
1814
+ }
1815
+ if (value.includes("\0")) {
1816
+ throw new FileRecordsValidationError("null bytes not allowed", field);
1817
+ }
1818
+ return value;
1819
+ }
1820
+ function parseNonNegativeInteger(value, field, fallback, max = MAX_RECORD_ROWS_SCANNED) {
1821
+ const raw = firstQueryValue(value);
1822
+ if (raw === void 0) return fallback;
1823
+ const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw.trim() : "";
1824
+ if (!/^\d+$/.test(text)) {
1825
+ throw new FileRecordsValidationError(`${field} must be a non-negative integer`, field);
1826
+ }
1827
+ const parsed = Number(text);
1828
+ if (!Number.isSafeInteger(parsed) || parsed > max) {
1829
+ throw new FileRecordsValidationError(`${field} is too large`, field);
1830
+ }
1831
+ return parsed;
1832
+ }
1833
+ function parseLimit(value) {
1834
+ const raw = firstQueryValue(value);
1835
+ if (raw === void 0) return DEFAULT_RECORD_ROWS_RETURNED;
1836
+ const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw.trim() : "";
1837
+ if (!/^\d+$/.test(text) || Number(text) < 1) {
1838
+ throw new FileRecordsValidationError("limit must be a positive integer", "limit");
1839
+ }
1840
+ return Math.min(Number(text), MAX_RECORD_ROWS_RETURNED);
1841
+ }
1842
+ function parseFileRecordsRequest(query) {
1843
+ const path4 = requireStringQueryParam(firstQueryValue(query.path), "path");
1844
+ const recordSet = firstQueryValue(query.recordSet);
1845
+ if (recordSet !== void 0 && String(recordSet).trim() !== "") {
1846
+ throw new FileRecordsValidationError("recordSet is not supported for file records in v1", "recordSet");
1847
+ }
1848
+ const offset = parseNonNegativeInteger(query.offset, "offset", 0);
1849
+ const limit = parseLimit(query.limit);
1850
+ const rawQ = firstQueryValue(query.q);
1851
+ const q = typeof rawQ === "string" ? rawQ.trim() : rawQ === void 0 ? "" : String(rawQ).trim();
1852
+ if (q.length > MAX_RECORD_QUERY_LENGTH) {
1853
+ throw new FileRecordsValidationError("q is too long", "q");
1854
+ }
1855
+ return { path: path4, offset, limit, q: q ? q.toLowerCase() : null };
1856
+ }
1857
+ function detectRecordsFormat(path4, content) {
1858
+ const ext = extname(path4).toLowerCase();
1859
+ if (ext === ".ndjson" || ext === ".jsonl") return "ndjson";
1860
+ if (ext === ".csv") return "csv";
1861
+ if (ext === ".json") return "json-array";
1862
+ const trimmed = content.trimStart();
1863
+ if (trimmed.startsWith("[")) return "json-array";
1864
+ if (trimmed.startsWith("{")) return "ndjson";
1865
+ return null;
1866
+ }
1867
+ function normalizeRecord(row) {
1868
+ if (row && typeof row === "object" && !Array.isArray(row)) return row;
1869
+ return { value: row };
1870
+ }
1871
+ function scalarMatches(value, q) {
1872
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return false;
1873
+ return String(value).toLowerCase().includes(q);
1874
+ }
1875
+ function recordMatches(record, q) {
1876
+ if (!q) return true;
1877
+ return Object.values(record).some((value) => scalarMatches(value, q));
1878
+ }
1879
+ function inferValueType(value) {
1880
+ if (value === null || value === void 0) return "null";
1881
+ if (Array.isArray(value)) return "array";
1882
+ return typeof value;
1883
+ }
1884
+ function inferColumns(rows) {
1885
+ const typesByName = /* @__PURE__ */ new Map();
1886
+ for (const row of rows.slice(0, MAX_RECORD_COLUMN_SAMPLE_ROWS)) {
1887
+ for (const [name, value] of Object.entries(row)) {
1888
+ if (!typesByName.has(name)) {
1889
+ if (typesByName.size >= MAX_RECORD_COLUMNS) continue;
1890
+ typesByName.set(name, /* @__PURE__ */ new Set());
1891
+ }
1892
+ typesByName.get(name)?.add(inferValueType(value));
1893
+ }
1894
+ }
1895
+ return [...typesByName.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, types]) => ({ name, type: types.size === 1 ? [...types][0] ?? "unknown" : "mixed" }));
1896
+ }
1897
+ function parseJsonArrayRecords(content) {
1898
+ let parsed;
1899
+ try {
1900
+ parsed = JSON.parse(content);
1901
+ } catch {
1902
+ throw new FileRecordsValidationError("malformed JSON records file");
1903
+ }
1904
+ if (!Array.isArray(parsed)) throw new FileRecordsValidationError("JSON records file must contain an array");
1905
+ if (parsed.length > MAX_RECORD_ROWS_SCANNED) throw new FileRecordsValidationError("record scan limit exceeded");
1906
+ return parsed.map(normalizeRecord);
1907
+ }
1908
+ function parseNdjsonRecords(content) {
1909
+ const rows = [];
1910
+ for (const line of content.split(/\r?\n/)) {
1911
+ if (!line.trim()) continue;
1912
+ if (rows.length >= MAX_RECORD_ROWS_SCANNED) throw new FileRecordsValidationError("record scan limit exceeded");
1913
+ try {
1914
+ rows.push(normalizeRecord(JSON.parse(line)));
1915
+ } catch {
1916
+ throw new FileRecordsValidationError("malformed NDJSON records file");
1917
+ }
1918
+ }
1919
+ return rows;
1920
+ }
1921
+ function parseCsvRows(content) {
1922
+ const rows = [];
1923
+ let row = [];
1924
+ let current = "";
1925
+ let quoted = false;
1926
+ for (let i = 0; i < content.length; i += 1) {
1927
+ const char = content[i];
1928
+ if (char === '"') {
1929
+ if (quoted && content[i + 1] === '"') {
1930
+ current += '"';
1931
+ i += 1;
1932
+ } else {
1933
+ quoted = !quoted;
1934
+ }
1935
+ continue;
1936
+ }
1937
+ if (char === "," && !quoted) {
1938
+ row.push(current);
1939
+ current = "";
1940
+ continue;
1941
+ }
1942
+ if ((char === "\n" || char === "\r") && !quoted) {
1943
+ if (char === "\r" && content[i + 1] === "\n") i += 1;
1944
+ row.push(current);
1945
+ if (row.some((value) => value.length > 0)) rows.push(row);
1946
+ row = [];
1947
+ current = "";
1948
+ continue;
1949
+ }
1950
+ current += char;
1951
+ }
1952
+ if (quoted) throw new FileRecordsValidationError("malformed CSV records file");
1953
+ row.push(current);
1954
+ if (row.some((value) => value.length > 0)) rows.push(row);
1955
+ return rows;
1956
+ }
1957
+ function parseCsvRecords(content) {
1958
+ const parsedRows = parseCsvRows(content);
1959
+ if (parsedRows.length === 0) return [];
1960
+ const headers = (parsedRows[0] ?? []).map((header) => header.trim());
1961
+ if (headers.some((header) => !header)) throw new FileRecordsValidationError("CSV records file must have a non-empty header row");
1962
+ const rows = [];
1963
+ for (const values of parsedRows.slice(1)) {
1964
+ if (rows.length >= MAX_RECORD_ROWS_SCANNED) throw new FileRecordsValidationError("record scan limit exceeded");
1965
+ const row = {};
1966
+ for (const [index, header] of headers.entries()) row[header] = values[index] ?? "";
1967
+ rows.push(row);
1968
+ }
1969
+ return rows;
1970
+ }
1971
+ function buildFileRecordsResult(args) {
1972
+ const format = detectRecordsFormat(args.path, args.content);
1973
+ if (!format) throw new FileRecordsValidationError("unsupported records file format");
1974
+ const allRows = format === "json-array" ? parseJsonArrayRecords(args.content) : format === "ndjson" ? parseNdjsonRecords(args.content) : parseCsvRecords(args.content);
1975
+ const matching = allRows.filter((row) => recordMatches(row, args.q));
1976
+ const rows = matching.slice(args.offset, args.offset + args.limit);
1977
+ const result = {
1978
+ source: { kind: "file", path: args.path, format },
1979
+ path: args.path,
1980
+ format,
1981
+ columns: inferColumns(matching),
1982
+ rows,
1983
+ total: matching.length,
1984
+ hasMore: args.offset + rows.length < matching.length,
1985
+ offset: args.offset,
1986
+ limit: args.limit,
1987
+ mtimeMs: args.mtimeMs
1988
+ };
1989
+ if (Buffer.byteLength(JSON.stringify(result), "utf8") > MAX_RECORD_OUTPUT_BYTES) {
1990
+ throw new FileRecordsValidationError("record output limit exceeded");
1991
+ }
1992
+ return result;
1993
+ }
1994
+
1786
1995
  // src/server/http/routes/file.ts
1787
1996
  var log = createLogger("boring/workspace-settings");
1788
1997
  var BORING_SETTINGS_PATH = ".boring/settings";
@@ -1881,7 +2090,7 @@ function normalizeUploadDir(value) {
1881
2090
  return dir.replace(/\/+$/, "");
1882
2091
  }
1883
2092
  function extForUpload(filename, contentType) {
1884
- const fromName = extname(filename).toLowerCase().replace(/^\./, "");
2093
+ const fromName = extname2(filename).toLowerCase().replace(/^\./, "");
1885
2094
  if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
1886
2095
  if (contentType === "image/jpeg") return "jpg";
1887
2096
  if (contentType === "image/png") return "png";
@@ -1903,7 +2112,7 @@ function markdownUrlFor(sourcePath, assetPath) {
1903
2112
  return rel && !rel.startsWith(".") ? rel : rel || assetPath;
1904
2113
  }
1905
2114
  function contentTypeForPath(path4) {
1906
- switch (extname(path4).toLowerCase()) {
2115
+ switch (extname2(path4).toLowerCase()) {
1907
2116
  case ".avif":
1908
2117
  return "image/avif";
1909
2118
  case ".gif":
@@ -1932,6 +2141,11 @@ function contentTypeForPath(path4) {
1932
2141
  return "application/octet-stream";
1933
2142
  }
1934
2143
  }
2144
+ function sendValidationError(reply, message, field) {
2145
+ return reply.code(400).send({
2146
+ error: { code: ERROR_CODE_VALIDATION_ERROR, message, ...field ? { field } : {} }
2147
+ });
2148
+ }
1935
2149
  function fileRoutes(app, opts, done) {
1936
2150
  async function resolveWorkspace(request) {
1937
2151
  if (opts.getWorkspace) return await opts.getWorkspace(request);
@@ -1961,6 +2175,36 @@ function fileRoutes(app, opts, done) {
1961
2175
  return classifyError(err, reply, "file");
1962
2176
  }
1963
2177
  });
2178
+ app.get("/api/v1/files/records", async (request, reply) => {
2179
+ try {
2180
+ const parsed = parseFileRecordsRequest(request.query);
2181
+ const workspace = await resolveWorkspace(request);
2182
+ const stat11 = await workspace.stat(parsed.path);
2183
+ if (stat11.kind !== "file") {
2184
+ return sendValidationError(reply, "path is not a file", "path");
2185
+ }
2186
+ if (stat11.size > MAX_RECORD_FILE_BYTES) {
2187
+ return sendValidationError(reply, "records file is too large", "path");
2188
+ }
2189
+ const content = await workspace.readFile(parsed.path);
2190
+ if (Buffer.byteLength(content, "utf8") > MAX_RECORD_FILE_BYTES) {
2191
+ return sendValidationError(reply, "records file is too large", "path");
2192
+ }
2193
+ return buildFileRecordsResult({
2194
+ path: parsed.path,
2195
+ content,
2196
+ mtimeMs: stat11.mtimeMs,
2197
+ offset: parsed.offset,
2198
+ limit: parsed.limit,
2199
+ q: parsed.q
2200
+ });
2201
+ } catch (err) {
2202
+ if (err instanceof FileRecordsValidationError) {
2203
+ return sendValidationError(reply, err.message, err.field);
2204
+ }
2205
+ return classifyError(err, reply, "file");
2206
+ }
2207
+ });
1964
2208
  app.get("/api/v1/files", async (request, reply) => {
1965
2209
  const query = request.query;
1966
2210
  const path4 = requireStringParam(query.path, "path", reply);
@@ -2874,10 +3118,13 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2874
3118
  };
2875
3119
  }
2876
3120
 
2877
- // src/server/sandbox/vercel-sandbox/provisioningAdapter.ts
2878
- import { mkdtemp } from "fs/promises";
2879
- import { join as join4 } from "path";
3121
+ // src/server/workspace/provisioning/packArtifact.ts
3122
+ import { execFile as execFile2 } from "child_process";
3123
+ import { mkdir as mkdir5, mkdtemp, rename as rename4 } from "fs/promises";
3124
+ import { dirname as dirname7, isAbsolute as isAbsolute5, join as join4 } from "path";
2880
3125
  import { tmpdir } from "os";
3126
+ import { fileURLToPath as fileURLToPath2 } from "url";
3127
+ import { promisify as promisify2 } from "util";
2881
3128
 
2882
3129
  // src/server/workspace/provisioning/errors.ts
2883
3130
  var ProvisioningError = class extends Error {
@@ -2904,17 +3151,62 @@ function logProvisioning(logger, level, message, fields = {}) {
2904
3151
  logger?.[level]?.(message, fields);
2905
3152
  }
2906
3153
 
2907
- // src/server/sandbox/vercel-sandbox/provisioningAdapter.ts
2908
- var VERCEL_PROVISIONING_CACHE_ROOT = "/tmp/boring-agent-cache";
3154
+ // src/server/workspace/provisioning/packArtifact.ts
3155
+ var execFileAsync2 = promisify2(execFile2);
3156
+ async function packProvisioningArtifact(request) {
3157
+ const sourcePath = request.source instanceof URL ? fileURLToPath2(request.source) : request.source;
3158
+ await mkdir5(dirname7(request.outputPath), { recursive: true });
3159
+ if (request.kind === "node") {
3160
+ const { stdout } = await execFileAsync2("pnpm", [
3161
+ "--dir",
3162
+ sourcePath,
3163
+ "pack",
3164
+ "--pack-destination",
3165
+ dirname7(request.outputPath)
3166
+ ], { maxBuffer: 1024 * 1024 * 20 });
3167
+ const packedName = stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
3168
+ if (!packedName) throw new Error(`pnpm pack produced no artifact for ${sourcePath}`);
3169
+ const packedPath = isAbsolute5(packedName) ? packedName : join4(dirname7(request.outputPath), packedName);
3170
+ await rename4(packedPath, request.outputPath);
3171
+ return;
3172
+ }
3173
+ await execFileAsync2("tar", ["-czf", request.outputPath, "-C", sourcePath, "."], {
3174
+ maxBuffer: 1024 * 1024 * 20
3175
+ });
3176
+ }
2909
3177
  function artifactExtension(kind) {
2910
3178
  return kind === "node" ? ".tgz" : ".tar.gz";
2911
3179
  }
2912
- function artifactName(kind, id, fingerprint2) {
3180
+ function provisioningArtifactName(kind, id, fingerprint2) {
2913
3181
  const safeId = id.replace(/[^A-Za-z0-9._-]/g, "-");
2914
3182
  const safeFingerprint = fingerprint2.replace(/^sha256:/, "");
2915
3183
  const formatVersion = kind === "node" ? "pnpm-pack-v2" : "v1";
2916
3184
  return `${safeId}-${formatVersion}-${safeFingerprint}${artifactExtension(kind)}`;
2917
3185
  }
3186
+ async function resolveArtifactInstallSource(args) {
3187
+ const { kind, id, fingerprint: fingerprint2 } = args.opts;
3188
+ const name = provisioningArtifactName(kind, id, fingerprint2);
3189
+ const workspaceRel = `.boring-agent/tmp/${name}`;
3190
+ if (!await args.workspaceFs.exists(workspaceRel)) {
3191
+ const artifactDir = await mkdtemp(join4(tmpdir(), "boring-agent-artifact-"));
3192
+ const outputPath = join4(artifactDir, name);
3193
+ try {
3194
+ await args.prepareArtifact({ kind, id, fingerprint: fingerprint2, source: args.source, outputPath });
3195
+ await args.workspaceFs.copyFromHost(outputPath, workspaceRel);
3196
+ } catch (error) {
3197
+ throw toProvisioningError(
3198
+ ErrorCode.enum.PROVISIONING_ARTIFACT_FAILED,
3199
+ "adapter-artifact",
3200
+ error,
3201
+ { runtime: kind, id, artifact: workspaceRel }
3202
+ );
3203
+ }
3204
+ }
3205
+ return `${args.runtimeTmpDir}/${name}`;
3206
+ }
3207
+
3208
+ // src/server/sandbox/vercel-sandbox/provisioningAdapter.ts
3209
+ var VERCEL_PROVISIONING_CACHE_ROOT = "/tmp/boring-agent-cache";
2918
3210
  function createVercelProvisioningAdapter(options) {
2919
3211
  return {
2920
3212
  mode: "vercel-sandbox",
@@ -2926,31 +3218,13 @@ function createVercelProvisioningAdapter(options) {
2926
3218
  });
2927
3219
  },
2928
3220
  async resolveInstallSource(source, opts) {
2929
- const name = artifactName(opts.kind, opts.id, opts.fingerprint);
2930
- const workspaceRel = `.boring-agent/tmp/${name}`;
2931
- const runtimePath = `${options.runtimeLayout.tmp}/${name}`;
2932
- if (!await options.workspaceFs.exists(workspaceRel)) {
2933
- const artifactDir = await mkdtemp(join4(tmpdir(), "boring-agent-vercel-artifact-"));
2934
- const outputPath = join4(artifactDir, name);
2935
- try {
2936
- await options.prepareArtifact({
2937
- kind: opts.kind,
2938
- id: opts.id,
2939
- fingerprint: opts.fingerprint,
2940
- source,
2941
- outputPath
2942
- });
2943
- await options.workspaceFs.copyFromHost(outputPath, workspaceRel);
2944
- } catch (error) {
2945
- throw toProvisioningError(
2946
- ErrorCode.enum.PROVISIONING_ARTIFACT_FAILED,
2947
- "adapter-artifact",
2948
- error,
2949
- { runtime: opts.kind, id: opts.id, artifact: workspaceRel }
2950
- );
2951
- }
2952
- }
2953
- return runtimePath;
3221
+ return await resolveArtifactInstallSource({
3222
+ workspaceFs: options.workspaceFs,
3223
+ prepareArtifact: options.prepareArtifact,
3224
+ runtimeTmpDir: options.runtimeLayout.tmp,
3225
+ source,
3226
+ opts
3227
+ });
2954
3228
  },
2955
3229
  workspaceFs: options.workspaceFs,
2956
3230
  getRuntimeCacheRoot() {
@@ -2962,8 +3236,8 @@ function createVercelProvisioningAdapter(options) {
2962
3236
  // src/server/workspace/provisioning/fingerprint.ts
2963
3237
  import { createHash as createHash4 } from "crypto";
2964
3238
  import { constants as constants3 } from "fs";
2965
- import { access as access3, mkdir as mkdir5, open, readFile as readFile5, rename as rename4, rm as rm2 } from "fs/promises";
2966
- import { dirname as dirname7 } from "path";
3239
+ import { access as access3, mkdir as mkdir6, open, readFile as readFile5, rename as rename5, rm as rm2 } from "fs/promises";
3240
+ import { dirname as dirname8 } from "path";
2967
3241
  var FINGERPRINT_RE = /^sha256:[a-f0-9]{64}$/;
2968
3242
  function isValidFingerprint(value) {
2969
3243
  return FINGERPRINT_RE.test(value.trim());
@@ -3153,13 +3427,13 @@ async function ensureNodeRuntime(options) {
3153
3427
  }
3154
3428
 
3155
3429
  // src/server/workspace/provisioning/python.ts
3156
- import { dirname as dirname8, join as join6, relative as relative7, sep as sep4 } from "path";
3157
- import { fileURLToPath as fileURLToPath2 } from "url";
3430
+ import { dirname as dirname9, join as join6, relative as relative7, sep as sep4 } from "path";
3431
+ import { fileURLToPath as fileURLToPath3 } from "url";
3158
3432
  var VENV_REL = ".boring-agent/venv";
3159
3433
  var VENV_FINGERPRINT_REL = `${VENV_REL}/.fingerprint`;
3160
3434
  var UV_BIN_REL = ".boring-agent/sdk/uv/bin/uv";
3161
3435
  function sourceToPath(source) {
3162
- return source instanceof URL ? fileURLToPath2(source) : source;
3436
+ return source instanceof URL ? fileURLToPath3(source) : source;
3163
3437
  }
3164
3438
  async function commandOutput2(adapter, command, args) {
3165
3439
  const result = await adapter.exec(command, args);
@@ -3225,7 +3499,7 @@ function pythonInstallSource(spec) {
3225
3499
  }
3226
3500
  function sourceRootForPythonSpec(spec) {
3227
3501
  if (spec.packageRoot) return spec.packageRoot;
3228
- if (spec.projectFile) return dirname8(sourceToPath(spec.projectFile));
3502
+ if (spec.projectFile) return dirname9(sourceToPath(spec.projectFile));
3229
3503
  return null;
3230
3504
  }
3231
3505
  function expectedPythonOutputs(paths, packages) {
@@ -3346,11 +3620,11 @@ async function ensurePythonRuntime(options) {
3346
3620
  // src/server/workspace/provisioning/skills.ts
3347
3621
  import { stat as stat5 } from "fs/promises";
3348
3622
  import { join as join7 } from "path";
3349
- import { fileURLToPath as fileURLToPath3 } from "url";
3623
+ import { fileURLToPath as fileURLToPath4 } from "url";
3350
3624
  var GENERATED_SKILLS_REL = ".boring-agent/skills";
3351
3625
  var USER_SKILLS_REL = ".agents/skills";
3352
3626
  function sourceToPath2(source) {
3353
- return source instanceof URL ? fileURLToPath3(source) : source;
3627
+ return source instanceof URL ? fileURLToPath4(source) : source;
3354
3628
  }
3355
3629
  function assertSafeSegment(kind, value) {
3356
3630
  if (value.length === 0 || value.includes("\0") || value.includes("/") || value.includes("\\") || value === "." || value === "..") {
@@ -3392,9 +3666,9 @@ async function mirrorPluginSkills(options) {
3392
3666
  import { basename, join as joinPath } from "path";
3393
3667
  import { posix as posix2 } from "path";
3394
3668
  import { readdir as readdir3, stat as stat6 } from "fs/promises";
3395
- import { fileURLToPath as fileURLToPath4, pathToFileURL } from "url";
3669
+ import { fileURLToPath as fileURLToPath5, pathToFileURL } from "url";
3396
3670
  function sourceToPath3(source) {
3397
- return source instanceof URL ? fileURLToPath4(source) : source;
3671
+ return source instanceof URL ? fileURLToPath5(source) : source;
3398
3672
  }
3399
3673
  function toWorkspaceRel3(...parts) {
3400
3674
  return posix2.normalize(parts.filter(Boolean).join("/"));
@@ -3688,7 +3962,7 @@ async function provisionWorkspaceRuntime(opts) {
3688
3962
  import { spawnSync } from "child_process";
3689
3963
 
3690
3964
  // src/server/runtime/modes/direct.ts
3691
- import { mkdir as mkdir7 } from "fs/promises";
3965
+ import { mkdir as mkdir8 } from "fs/promises";
3692
3966
 
3693
3967
  // src/server/runtime/createServerFileSearch.ts
3694
3968
  var DEFAULT_LIMIT = 500;
@@ -3766,12 +4040,12 @@ async function copyTemplate(templatePath, workspaceRoot) {
3766
4040
 
3767
4041
  // src/server/runtime/modes/provisioningAdapter.ts
3768
4042
  import { spawn as spawn3 } from "child_process";
3769
- import { cp as cp3, lstat as lstat3, mkdir as mkdir6, readFile as readFile6, realpath as realpath3, rm as rm3, stat as stat7, writeFile as writeFile5 } from "fs/promises";
3770
- import { dirname as dirname9, isAbsolute as isAbsolute5, relative as relative8, resolve as resolve6, sep as sep5 } from "path";
3771
- import { fileURLToPath as fileURLToPath5 } from "url";
4043
+ import { cp as cp3, lstat as lstat3, mkdir as mkdir7, readFile as readFile6, realpath as realpath3, rm as rm3, stat as stat7, writeFile as writeFile5 } from "fs/promises";
4044
+ import { dirname as dirname10, isAbsolute as isAbsolute6, relative as relative8, resolve as resolve6, sep as sep5 } from "path";
4045
+ import { fileURLToPath as fileURLToPath6 } from "url";
3772
4046
  var LOCAL_SANDBOX_WORKSPACE_ROOT = "/workspace";
3773
4047
  function sourceToPath4(source) {
3774
- return source instanceof URL ? fileURLToPath5(source) : source;
4048
+ return source instanceof URL ? fileURLToPath6(source) : source;
3775
4049
  }
3776
4050
  async function assertExistingInsideWorkspace(root, relPath, enforceSymlinkBoundary) {
3777
4051
  const absPath = validatePath(root, relPath);
@@ -3789,9 +4063,9 @@ async function assertExistingInsideWorkspace(root, relPath, enforceSymlinkBounda
3789
4063
  }
3790
4064
  async function prepareWritablePath(root, relPath, enforceSymlinkBoundary) {
3791
4065
  const absPath = validatePath(root, relPath);
3792
- await mkdir6(dirname9(absPath), { recursive: true });
4066
+ await mkdir7(dirname10(absPath), { recursive: true });
3793
4067
  if (enforceSymlinkBoundary) {
3794
- await assertRealPathWithinWorkspace(root, dirname9(absPath));
4068
+ await assertRealPathWithinWorkspace(root, dirname10(absPath));
3795
4069
  }
3796
4070
  try {
3797
4071
  const targetStat = await lstat3(absPath);
@@ -3830,8 +4104,8 @@ async function spawnCommand(command, args, opts) {
3830
4104
  stderr: Buffer.concat(stderr).toString("utf8")
3831
4105
  });
3832
4106
  };
3833
- child.stdout?.on("data", (chunk2) => stdout.push(chunk2));
3834
- child.stderr?.on("data", (chunk2) => stderr.push(chunk2));
4107
+ child.stdout?.on("data", (chunk3) => stdout.push(chunk3));
4108
+ child.stderr?.on("data", (chunk3) => stderr.push(chunk3));
3835
4109
  child.on("error", settle);
3836
4110
  child.on("close", (code) => {
3837
4111
  if (code === 0) {
@@ -3855,7 +4129,7 @@ function defaultExecOptions(paths, opts) {
3855
4129
  };
3856
4130
  }
3857
4131
  function mapWorkspacePathToLocalSandbox(paths, value) {
3858
- const absolute = isAbsolute5(value) ? value : resolve6(paths.workspaceRoot, value);
4132
+ const absolute = isAbsolute6(value) ? value : resolve6(paths.workspaceRoot, value);
3859
4133
  const relPath = relative8(paths.workspaceRoot, absolute);
3860
4134
  if (relPath === "") return LOCAL_SANDBOX_WORKSPACE_ROOT;
3861
4135
  if (relPath === ".." || relPath.startsWith(`..${sep5}`)) return value;
@@ -3869,33 +4143,18 @@ function mapEnvToLocalSandbox(paths, env) {
3869
4143
  Object.entries(env).map(([key, value]) => [key, mapValueToLocalSandbox(paths, value)])
3870
4144
  );
3871
4145
  }
3872
- function sanitizeInstallSourcePart(value) {
3873
- const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
3874
- return sanitized.length > 0 ? sanitized : "source";
3875
- }
3876
- async function copyExternalSourceIntoWorkspace(paths, sourcePath, opts) {
3877
- const fingerprint2 = opts.fingerprint.replace(/^sha256:/, "");
3878
- const relTarget = `.boring-agent/tmp/${sanitizeInstallSourcePart(opts.kind)}-${sanitizeInstallSourcePart(opts.id)}-${sanitizeInstallSourcePart(fingerprint2)}-source`;
3879
- const absTarget = validatePath(paths.workspaceRoot, relTarget);
3880
- await rm3(absTarget, { recursive: true, force: true });
3881
- await mkdir6(dirname9(absTarget), { recursive: true });
3882
- await assertRealPathWithinWorkspace(paths.workspaceRoot, dirname9(absTarget));
3883
- const sourceStat = await stat7(sourcePath);
3884
- await cp3(sourcePath, absTarget, {
3885
- recursive: sourceStat.isDirectory(),
3886
- force: false,
3887
- errorOnExist: true
3888
- });
3889
- return `${LOCAL_SANDBOX_WORKSPACE_ROOT}/${relTarget}`;
3890
- }
3891
4146
  function createWorkspaceFs(workspaceRoot, opts) {
3892
4147
  const { enforceSymlinkBoundary } = opts;
3893
4148
  return {
3894
4149
  async exists(workspaceRelativePath) {
3895
- const absPath = await assertExistingInsideWorkspace(workspaceRoot, workspaceRelativePath, enforceSymlinkBoundary);
3896
- if (!absPath) return false;
3897
- await lstat3(absPath);
3898
- return true;
4150
+ const absPath = validatePath(workspaceRoot, workspaceRelativePath);
4151
+ try {
4152
+ await stat7(absPath);
4153
+ return true;
4154
+ } catch (error) {
4155
+ if (error.code === "ENOENT") return false;
4156
+ throw error;
4157
+ }
3899
4158
  },
3900
4159
  async rm(workspaceRelativePath) {
3901
4160
  const absPath = await assertExistingInsideWorkspace(workspaceRoot, workspaceRelativePath, enforceSymlinkBoundary);
@@ -3904,7 +4163,7 @@ function createWorkspaceFs(workspaceRoot, opts) {
3904
4163
  },
3905
4164
  async mkdir(workspaceRelativePath) {
3906
4165
  const absPath = validatePath(workspaceRoot, workspaceRelativePath);
3907
- await mkdir6(absPath, { recursive: true });
4166
+ await mkdir7(absPath, { recursive: true });
3908
4167
  if (enforceSymlinkBoundary) {
3909
4168
  await assertRealPathWithinWorkspace(workspaceRoot, absPath);
3910
4169
  }
@@ -3947,6 +4206,7 @@ function createDirectProvisioningAdapter(paths, runner = spawnCommand) {
3947
4206
  }
3948
4207
  function createLocalProvisioningAdapter(paths, runner = spawnCommand) {
3949
4208
  const sourceMounts = /* @__PURE__ */ new Map();
4209
+ const workspaceFs = createWorkspaceFs(paths.workspaceRoot, { enforceSymlinkBoundary: true });
3950
4210
  return {
3951
4211
  mode: "local",
3952
4212
  async exec(command, args, opts) {
@@ -3976,12 +4236,18 @@ function createLocalProvisioningAdapter(paths, runner = spawnCommand) {
3976
4236
  const realSource = await realpath3(hostPath);
3977
4237
  const relPath = relative8(realWorkspaceRoot, realSource);
3978
4238
  if (relPath === "") return LOCAL_SANDBOX_WORKSPACE_ROOT;
3979
- if (!relPath.startsWith("..") && !isAbsolute5(relPath)) {
4239
+ if (!relPath.startsWith("..") && !isAbsolute6(relPath)) {
3980
4240
  return `${LOCAL_SANDBOX_WORKSPACE_ROOT}/${relPath.split(sep5).join("/")}`;
3981
4241
  }
3982
- return await copyExternalSourceIntoWorkspace(paths, realSource, opts);
4242
+ return await resolveArtifactInstallSource({
4243
+ workspaceFs,
4244
+ prepareArtifact: packProvisioningArtifact,
4245
+ runtimeTmpDir: `${LOCAL_SANDBOX_WORKSPACE_ROOT}/.boring-agent/tmp`,
4246
+ source: realSource,
4247
+ opts
4248
+ });
3983
4249
  },
3984
- workspaceFs: createWorkspaceFs(paths.workspaceRoot, { enforceSymlinkBoundary: true }),
4250
+ workspaceFs,
3985
4251
  getRuntimeCacheRoot() {
3986
4252
  return paths.cache;
3987
4253
  }
@@ -3994,7 +4260,7 @@ var directModeAdapter = {
3994
4260
  workspaceFsCapability: "strong",
3995
4261
  createProvisioningAdapter: (runtimeLayout) => createDirectProvisioningAdapter(runtimeLayout),
3996
4262
  async create(ctx) {
3997
- await mkdir7(ctx.workspaceRoot, { recursive: true });
4263
+ await mkdir8(ctx.workspaceRoot, { recursive: true });
3998
4264
  await copyTemplate(ctx.templatePath, ctx.workspaceRoot);
3999
4265
  const runtimeContext = { runtimeCwd: ctx.workspaceRoot };
4000
4266
  const workspace = createNodeWorkspace(ctx.workspaceRoot, { runtimeContext });
@@ -4011,7 +4277,7 @@ var directModeAdapter = {
4011
4277
  };
4012
4278
 
4013
4279
  // src/server/runtime/modes/local.ts
4014
- import { mkdir as mkdir8 } from "fs/promises";
4280
+ import { mkdir as mkdir9 } from "fs/promises";
4015
4281
  var localModeAdapter = {
4016
4282
  id: "local",
4017
4283
  workspaceFsCapability: "strong",
@@ -4020,7 +4286,7 @@ var localModeAdapter = {
4020
4286
  if (process.platform !== "linux") {
4021
4287
  throw new Error("local mode requires Linux with bubblewrap");
4022
4288
  }
4023
- await mkdir8(ctx.workspaceRoot, { recursive: true });
4289
+ await mkdir9(ctx.workspaceRoot, { recursive: true });
4024
4290
  await copyTemplate(ctx.templatePath, ctx.workspaceRoot);
4025
4291
  const runtimeContext = { runtimeCwd: "/workspace" };
4026
4292
  const workspace = createNodeWorkspace(ctx.workspaceRoot, { runtimeContext });
@@ -4040,11 +4306,9 @@ var localModeAdapter = {
4040
4306
  };
4041
4307
 
4042
4308
  // src/server/runtime/modes/vercel-sandbox.ts
4043
- import { execFile as execFile2 } from "child_process";
4044
- import { mkdir as mkdir9, readFile as readFile8, readdir as readdir5, rename as rename5, stat as stat8 } from "fs/promises";
4045
- import { dirname as dirname10, isAbsolute as isAbsolute6, join as join8 } from "path";
4046
- import { fileURLToPath as fileURLToPath6 } from "url";
4047
- import { promisify as promisify2 } from "util";
4309
+ import { readFile as readFile8, readdir as readdir5, stat as stat8 } from "fs/promises";
4310
+ import { join as join8 } from "path";
4311
+ import { fileURLToPath as fileURLToPath7 } from "url";
4048
4312
  import { Sandbox as VercelSandbox } from "@vercel/sandbox";
4049
4313
 
4050
4314
  // src/server/sandbox/vercel-sandbox/createVercelSandboxExec.ts
@@ -4055,15 +4319,15 @@ var DEFAULT_MAX_OUTPUT_BYTES3 = 1048576;
4055
4319
  var VERCEL_SANDBOX_DEFAULT_PATH = "/vercel/runtimes/node24/bin:/vercel/runtimes/node22/bin:/vercel/runtimes/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
4056
4320
  function createStreamWritable(collector, shared, onChunk) {
4057
4321
  return new Writable({
4058
- write(chunk2, _enc, cb) {
4322
+ write(chunk3, _enc, cb) {
4059
4323
  const remaining = shared.maxBytes - shared.totalBytes;
4060
4324
  if (remaining <= 0) {
4061
4325
  shared.truncated = true;
4062
4326
  cb();
4063
4327
  return;
4064
4328
  }
4065
- if (chunk2.length > remaining) {
4066
- const partial = chunk2.subarray(0, remaining);
4329
+ if (chunk3.length > remaining) {
4330
+ const partial = chunk3.subarray(0, remaining);
4067
4331
  collector.chunks.push(partial);
4068
4332
  shared.totalBytes += remaining;
4069
4333
  shared.truncated = true;
@@ -4071,9 +4335,9 @@ function createStreamWritable(collector, shared, onChunk) {
4071
4335
  cb();
4072
4336
  return;
4073
4337
  }
4074
- collector.chunks.push(chunk2);
4075
- shared.totalBytes += chunk2.length;
4076
- onChunk?.(new Uint8Array(chunk2));
4338
+ collector.chunks.push(chunk3);
4339
+ shared.totalBytes += chunk3.length;
4340
+ onChunk?.(new Uint8Array(chunk3));
4077
4341
  cb();
4078
4342
  }
4079
4343
  });
@@ -4257,11 +4521,11 @@ async function buildTarGz(files) {
4257
4521
  }
4258
4522
  chunks.push(Buffer.alloc(1024));
4259
4523
  const tarBuffer = Buffer.concat(chunks);
4260
- return new Promise((resolve10, reject) => {
4524
+ return new Promise((resolve11, reject) => {
4261
4525
  const gzip = createGzip({ level: 6 });
4262
4526
  const gzChunks = [];
4263
- gzip.on("data", (chunk2) => gzChunks.push(chunk2));
4264
- gzip.on("end", () => resolve10(Buffer.concat(gzChunks)));
4527
+ gzip.on("data", (chunk3) => gzChunks.push(chunk3));
4528
+ gzip.on("end", () => resolve11(Buffer.concat(gzChunks)));
4265
4529
  gzip.on("error", reject);
4266
4530
  Readable.from(tarBuffer).pipe(gzip);
4267
4531
  });
@@ -4301,7 +4565,6 @@ var ORPHAN_GUARD_MAX_IDLE_MS = 24 * 60 * 60 * 1e3;
4301
4565
  var VERCEL_SANDBOX_TIMEOUT_MS_ENV = "BORING_AGENT_VERCEL_SANDBOX_TIMEOUT_MS";
4302
4566
  var VERCEL_SANDBOX_RUNTIME_ENV = "BORING_AGENT_VERCEL_SANDBOX_RUNTIME";
4303
4567
  var DEFAULT_VERCEL_SANDBOX_RUNTIME = "node24";
4304
- var execFileAsync2 = promisify2(execFile2);
4305
4568
  function sandboxTelemetryProperties(ctx, extra = {}) {
4306
4569
  return {
4307
4570
  runtimeMode: "vercel-sandbox",
@@ -4449,28 +4712,7 @@ async function seedTemplateIntoVercelWorkspace(workspace, templatePath, logger)
4449
4712
  });
4450
4713
  }
4451
4714
  function provisioningSourceToPath(source) {
4452
- return source instanceof URL ? fileURLToPath6(source) : source;
4453
- }
4454
- async function prepareVercelProvisioningArtifact(request) {
4455
- const sourcePath = provisioningSourceToPath(request.source);
4456
- await mkdir9(dirname10(request.outputPath), { recursive: true });
4457
- if (request.kind === "node") {
4458
- const { stdout } = await execFileAsync2("pnpm", [
4459
- "--dir",
4460
- sourcePath,
4461
- "pack",
4462
- "--pack-destination",
4463
- dirname10(request.outputPath)
4464
- ], { maxBuffer: 1024 * 1024 * 20 });
4465
- const packedName = stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
4466
- if (!packedName) throw new Error(`pnpm pack produced no artifact for ${sourcePath}`);
4467
- const packedPath = isAbsolute6(packedName) ? packedName : join8(dirname10(request.outputPath), packedName);
4468
- await rename5(packedPath, request.outputPath);
4469
- return;
4470
- }
4471
- await execFileAsync2("tar", ["-czf", request.outputPath, "-C", sourcePath, "."], {
4472
- maxBuffer: 1024 * 1024 * 20
4473
- });
4715
+ return source instanceof URL ? fileURLToPath7(source) : source;
4474
4716
  }
4475
4717
  function shellSingleQuote(value) {
4476
4718
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -4720,7 +4962,7 @@ function createVercelSandboxModeAdapter(opts = {}) {
4720
4962
  }
4721
4963
  return { stdout, stderr };
4722
4964
  },
4723
- prepareArtifact: prepareVercelProvisioningArtifact
4965
+ prepareArtifact: packProvisioningArtifact
4724
4966
  });
4725
4967
  },
4726
4968
  async create(ctx) {
@@ -4901,8 +5143,8 @@ import Fastify from "fastify";
4901
5143
 
4902
5144
  // src/server/harness/pi-coding-agent/createHarness.ts
4903
5145
  import { existsSync, readFileSync as readFileSync2 } from "fs";
4904
- import { mkdir as mkdir11, writeFile as writeFile7 } from "fs/promises";
4905
- import { extname as extname2, join as join10 } from "path";
5146
+ import { mkdir as mkdir12, writeFile as writeFile7 } from "fs/promises";
5147
+ import { extname as extname3, join as join10 } from "path";
4906
5148
  import {
4907
5149
  createAgentSession,
4908
5150
  SessionManager,
@@ -5183,17 +5425,16 @@ import {
5183
5425
  readFile as readFile9,
5184
5426
  stat as fsStat,
5185
5427
  rm as rm4,
5186
- mkdir as mkdir10,
5428
+ mkdir as mkdir11,
5187
5429
  writeFile as writeFile6,
5188
5430
  appendFile,
5189
5431
  utimes
5190
5432
  } from "fs/promises";
5191
5433
  import { readFileSync, readdirSync } from "fs";
5192
- import { join as join9 } from "path";
5434
+ import { join as join9, resolve as resolve7 } from "path";
5193
5435
  import { homedir as homedir3 } from "os";
5194
5436
  import {
5195
5437
  parseSessionEntries,
5196
- buildSessionContext,
5197
5438
  CURRENT_SESSION_VERSION
5198
5439
  } from "@mariozechner/pi-coding-agent";
5199
5440
  function defaultSessionDir(cwd) {
@@ -5226,13 +5467,16 @@ var PiSessionStore = class {
5226
5467
  async list(ctx) {
5227
5468
  const files = await readdir6(this.sessionDir).catch(() => []);
5228
5469
  const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
5470
+ const filepaths = jsonlFiles.map((f) => join9(this.sessionDir, f));
5471
+ const referencedPiFiles = await this.referencedPiFiles(filepaths);
5472
+ const visibleFiles = filepaths.filter((filepath) => !referencedPiFiles.has(resolve7(filepath)));
5229
5473
  const summaries = await Promise.all(
5230
- jsonlFiles.map((f) => this.summarizeFile(join9(this.sessionDir, f)))
5474
+ visibleFiles.map((filepath) => this.summarizeFile(filepath))
5231
5475
  );
5232
5476
  return summaries.filter((s) => s !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
5233
5477
  }
5234
5478
  async create(ctx, init) {
5235
- await mkdir10(this.sessionDir, { recursive: true });
5479
+ await mkdir11(this.sessionDir, { recursive: true });
5236
5480
  const id = randomUUID();
5237
5481
  const now = (/* @__PURE__ */ new Date()).toISOString();
5238
5482
  const header = {
@@ -5279,17 +5523,25 @@ var PiSessionStore = class {
5279
5523
  (e) => e.type !== "session"
5280
5524
  );
5281
5525
  const fileStat = await fsStat(filepath);
5526
+ const linkedPiFile = extractPiSessionFilePath(fileEntries);
5527
+ const linked = linkedPiFile && resolve7(linkedPiFile) !== resolve7(filepath) ? await this.readLinkedPiSession(linkedPiFile) : null;
5528
+ const linkedEntries = linked?.entries.filter(
5529
+ (e) => e.type !== "session"
5530
+ ) ?? [];
5282
5531
  const uiSnapshot = extractLatestUiSnapshot(fileEntries);
5283
- const messages = uiSnapshot ?? piMessagesToUIMessages(
5284
- buildSessionContext(sessionEntries).messages
5285
- );
5286
- const title = extractTitle(sessionEntries) ?? "New session";
5532
+ const transcriptEntries = linkedEntries.length > 0 ? linkedEntries : sessionEntries;
5533
+ const messages = uiSnapshot ?? dropEmptyAssistantUiMessages(piMessagesToUIMessages(
5534
+ transcriptEntries.filter((entry) => entry.type === "message").map((entry) => entry.message),
5535
+ sessionId
5536
+ ));
5537
+ const title = extractTitle(sessionEntries) ?? extractTitle(linkedEntries) ?? "New session";
5287
5538
  const turnCount = messages.filter((m) => m.role === "user").length;
5539
+ const updatedAtMs = Math.max(fileStat.mtime.getTime(), linked?.mtime.getTime() ?? 0);
5288
5540
  return {
5289
5541
  id: sessionId,
5290
5542
  title,
5291
5543
  createdAt: header?.timestamp ?? fileStat.birthtime.toISOString(),
5292
- updatedAt: fileStat.mtime.toISOString(),
5544
+ updatedAt: new Date(updatedAtMs).toISOString(),
5293
5545
  turnCount,
5294
5546
  messages
5295
5547
  };
@@ -5300,7 +5552,7 @@ var PiSessionStore = class {
5300
5552
  type: "ui_snapshot",
5301
5553
  id: randomUUID(),
5302
5554
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5303
- messages
5555
+ messages: sanitizeUiMessages(messages, { dropEmptyAssistantMessages: true })
5304
5556
  });
5305
5557
  await appendFile(filepath, entry + "\n");
5306
5558
  }
@@ -5364,7 +5616,12 @@ var PiSessionStore = class {
5364
5616
  const filepath = await this.resolveSessionFile(sessionId).catch(
5365
5617
  () => null
5366
5618
  );
5367
- if (filepath) await rm4(filepath, { force: true });
5619
+ if (!filepath) return;
5620
+ const linkedPiFile = await this.linkedPiFileFor(filepath);
5621
+ await rm4(filepath, { force: true });
5622
+ if (linkedPiFile && resolve7(linkedPiFile) !== resolve7(filepath)) {
5623
+ await rm4(linkedPiFile, { force: true });
5624
+ }
5368
5625
  }
5369
5626
  touchSession(sessionId, title) {
5370
5627
  this.resolveSessionFile(sessionId).then((filepath) => {
@@ -5400,6 +5657,28 @@ var PiSessionStore = class {
5400
5657
  if (!match) throw new Error(`Session not found: ${sessionId}`);
5401
5658
  return join9(this.sessionDir, match);
5402
5659
  }
5660
+ async linkedPiFileFor(filepath) {
5661
+ try {
5662
+ const content = await readFile9(filepath, "utf-8");
5663
+ return extractPiSessionFilePath(safeParseEntries(content));
5664
+ } catch {
5665
+ return null;
5666
+ }
5667
+ }
5668
+ async referencedPiFiles(filepaths) {
5669
+ const referenced = /* @__PURE__ */ new Set();
5670
+ await Promise.all(filepaths.map(async (filepath) => {
5671
+ try {
5672
+ const content = await readFile9(filepath, "utf-8");
5673
+ const piFilePath = extractPiSessionFilePath(safeParseEntries(content));
5674
+ if (piFilePath && resolve7(piFilePath) !== resolve7(filepath)) {
5675
+ referenced.add(resolve7(piFilePath));
5676
+ }
5677
+ } catch {
5678
+ }
5679
+ }));
5680
+ return referenced;
5681
+ }
5403
5682
  async summarizeFile(filepath) {
5404
5683
  try {
5405
5684
  const [fileStat, content] = await Promise.all([
@@ -5416,28 +5695,56 @@ var PiSessionStore = class {
5416
5695
  const sessionEntries = entries.filter(
5417
5696
  (e) => e.type !== "session"
5418
5697
  );
5419
- const title = extractTitle(sessionEntries) ?? firstUserMessage(sessionEntries) ?? "New session";
5420
- const turnCount = sessionEntries.filter(
5698
+ const linkedPiFile = extractPiSessionFilePath(entries);
5699
+ const linked = linkedPiFile && resolve7(linkedPiFile) !== resolve7(filepath) ? await this.readLinkedPiSession(linkedPiFile) : null;
5700
+ const linkedEntries = linked?.entries.filter(
5701
+ (e) => e.type !== "session"
5702
+ ) ?? [];
5703
+ const uiSnapshot = extractLatestUiSnapshot(entries);
5704
+ const title = extractTitle(sessionEntries) ?? extractTitle(linkedEntries) ?? firstUserMessage(linkedEntries) ?? firstUserMessage(sessionEntries) ?? "New session";
5705
+ const turnCount = uiSnapshot ? uiSnapshot.filter((m) => m.role === "user").length : [...sessionEntries, ...linkedEntries].filter(
5421
5706
  (e) => e.type === "message" && e.message?.role === "user"
5422
5707
  ).length;
5708
+ const updatedAtMs = Math.max(fileStat.mtime.getTime(), linked?.mtime.getTime() ?? 0);
5423
5709
  return {
5424
5710
  id: header.id,
5425
5711
  title,
5426
5712
  createdAt: header.timestamp,
5427
- updatedAt: fileStat.mtime.toISOString(),
5713
+ updatedAt: new Date(updatedAtMs).toISOString(),
5428
5714
  turnCount
5429
5715
  };
5430
5716
  } catch {
5431
5717
  return null;
5432
5718
  }
5433
5719
  }
5720
+ async readLinkedPiSession(filepath) {
5721
+ try {
5722
+ const [fileStat, content] = await Promise.all([
5723
+ fsStat(filepath),
5724
+ readFile9(filepath, "utf-8")
5725
+ ]);
5726
+ return { entries: safeParseEntries(content), mtime: fileStat.mtime };
5727
+ } catch {
5728
+ return null;
5729
+ }
5730
+ }
5434
5731
  };
5732
+ function extractPiSessionFilePath(entries) {
5733
+ let piFilePath = null;
5734
+ for (const e of entries) {
5735
+ const rec = e;
5736
+ if (rec.type === "pi_session_file" && typeof rec.path === "string") {
5737
+ piFilePath = rec.path;
5738
+ }
5739
+ }
5740
+ return piFilePath;
5741
+ }
5435
5742
  function extractLatestUiSnapshot(entries) {
5436
5743
  let latest = null;
5437
5744
  for (const e of entries) {
5438
5745
  const rec = e;
5439
5746
  if (rec.type === "ui_snapshot" && Array.isArray(rec.messages)) {
5440
- latest = rec.messages;
5747
+ latest = sanitizeUiMessages(rec.messages, { dropEmptyAssistantMessages: true });
5441
5748
  }
5442
5749
  }
5443
5750
  return latest;
@@ -5470,7 +5777,10 @@ function safeParseEntries(content) {
5470
5777
  return results;
5471
5778
  }
5472
5779
  }
5473
- function piMessagesToUIMessages(messages) {
5780
+ function reconstructedMessageId(sessionId, role, index) {
5781
+ return sessionId ? `${sessionId}-${role}-${index}` : `${role}-${index}`;
5782
+ }
5783
+ function piMessagesToUIMessages(messages, sessionId) {
5474
5784
  const result = [];
5475
5785
  let currentAssistant = null;
5476
5786
  for (const raw of messages) {
@@ -5482,7 +5792,7 @@ function piMessagesToUIMessages(messages) {
5482
5792
  const content = msg.content;
5483
5793
  const text = typeof content === "string" ? content : Array.isArray(content) ? content.filter((c) => c.type === "text").map((c) => c.text).join("") : "";
5484
5794
  result.push({
5485
- id: randomUUID(),
5795
+ id: reconstructedMessageId(sessionId, "user", result.length),
5486
5796
  role: "user",
5487
5797
  parts: [{ type: "text", text }]
5488
5798
  });
@@ -5516,7 +5826,7 @@ function piMessagesToUIMessages(messages) {
5516
5826
  }
5517
5827
  }
5518
5828
  const uiMsg = {
5519
- id: randomUUID(),
5829
+ id: reconstructedMessageId(sessionId, "assistant", result.length),
5520
5830
  role: "assistant",
5521
5831
  parts
5522
5832
  };
@@ -5554,7 +5864,7 @@ var DEFAULT_POLL_MS = 250;
5554
5864
  var MAX_PROMPT_CHARS = 1200;
5555
5865
  var MAX_TITLE_CHARS = 80;
5556
5866
  function sleep(ms) {
5557
- return new Promise((resolve10) => setTimeout(resolve10, ms));
5867
+ return new Promise((resolve11) => setTimeout(resolve11, ms));
5558
5868
  }
5559
5869
  function truncateForPrompt(text) {
5560
5870
  const trimmed = text.trim();
@@ -6029,7 +6339,7 @@ async function applyRequestedSessionOptions(handle, input) {
6029
6339
  var log3 = createLogger("pi-harness");
6030
6340
  var DEFAULT_ATTACHMENT_DIR = "assets/images";
6031
6341
  function extForAttachment(filename, contentType) {
6032
- const fromName = extname2(filename).toLowerCase().replace(/^\./, "");
6342
+ const fromName = extname3(filename).toLowerCase().replace(/^\./, "");
6033
6343
  if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
6034
6344
  if (contentType === "image/jpeg") return "jpg";
6035
6345
  if (contentType === "image/png") return "png";
@@ -6189,6 +6499,7 @@ function createPiCodingAgentHarness(opts) {
6189
6499
  handle.piSession.dispose();
6190
6500
  piSessions.delete(sessionId);
6191
6501
  clearNativeFollowUpWork(sessionId);
6502
+ consumedNativeFollowUpKeys.delete(sessionId);
6192
6503
  }
6193
6504
  const originalDelete = sessionStore.delete.bind(sessionStore);
6194
6505
  sessionStore.delete = async (ctx, sessionId) => {
@@ -6197,6 +6508,7 @@ function createPiCodingAgentHarness(opts) {
6197
6508
  };
6198
6509
  const nativeFollowUpPending = /* @__PURE__ */ new Set();
6199
6510
  const nativeFollowUpQueues = /* @__PURE__ */ new Map();
6511
+ const consumedNativeFollowUpKeys = /* @__PURE__ */ new Map();
6200
6512
  function clearNativeFollowUpWork(sessionId) {
6201
6513
  nativeFollowUpPending.delete(sessionId);
6202
6514
  nativeFollowUpQueues.delete(sessionId);
@@ -6232,12 +6544,38 @@ function createPiCodingAgentHarness(opts) {
6232
6544
  }
6233
6545
  session._emitQueueUpdate?.();
6234
6546
  }
6547
+ function followUpDedupeKeys(options) {
6548
+ const keys = [];
6549
+ if (options?.clientNonce) keys.push(`nonce:${options.clientNonce}`);
6550
+ if (options?.clientSeq !== void 0) keys.push(`seq:${options.clientSeq}`);
6551
+ return keys;
6552
+ }
6235
6553
  function hasFollowUpSelector(options) {
6236
- return Boolean(options?.clientNonce) || options?.clientSeq !== void 0;
6554
+ return followUpDedupeKeys(options).length > 0;
6237
6555
  }
6238
6556
  function matchesFollowUpSelector(item, options) {
6239
6557
  if (!hasFollowUpSelector(options)) return true;
6240
- return Boolean(options?.clientNonce && item.clientNonce === options.clientNonce) || options?.clientSeq !== void 0 && item.clientSeq === options.clientSeq;
6558
+ const optionKeys = new Set(followUpDedupeKeys(options));
6559
+ return followUpDedupeKeys(item).some((key) => optionKeys.has(key));
6560
+ }
6561
+ function rememberConsumedNativeFollowUp(sessionId, request) {
6562
+ const keys = followUpDedupeKeys(request);
6563
+ if (keys.length === 0) return;
6564
+ const consumed = consumedNativeFollowUpKeys.get(sessionId) ?? /* @__PURE__ */ new Set();
6565
+ for (const key of keys) consumed.add(key);
6566
+ consumedNativeFollowUpKeys.set(sessionId, consumed);
6567
+ }
6568
+ function followUpPostDedupeKeys(options) {
6569
+ if (options?.clientNonce) return [`nonce:${options.clientNonce}`];
6570
+ if (options?.clientSeq !== void 0) return [`seq:${options.clientSeq}`];
6571
+ return [];
6572
+ }
6573
+ function hasQueuedOrConsumedNativeFollowUp(sessionId, options) {
6574
+ const optionKeys = new Set(followUpPostDedupeKeys(options));
6575
+ if (optionKeys.size === 0) return false;
6576
+ const consumed = consumedNativeFollowUpKeys.get(sessionId);
6577
+ if (consumed && [...optionKeys].some((key) => consumed.has(key))) return true;
6578
+ return (nativeFollowUpQueues.get(sessionId) ?? []).some((request) => followUpPostDedupeKeys(request).some((key) => optionKeys.has(key)));
6241
6579
  }
6242
6580
  function removeNativeFollowUp(sessionId, options) {
6243
6581
  const queue = nativeFollowUpQueues.get(sessionId);
@@ -6265,6 +6603,7 @@ function createPiCodingAgentHarness(opts) {
6265
6603
  async followUp(sessionId, text, _attachments, displayText = text, options) {
6266
6604
  const handle = piSessions.get(sessionId);
6267
6605
  if (!handle) throw new Error("followup_session_not_ready");
6606
+ if (hasQueuedOrConsumedNativeFollowUp(sessionId, options)) return;
6268
6607
  const queue = nativeFollowUpQueues.get(sessionId) ?? [];
6269
6608
  queue.push({
6270
6609
  text,
@@ -6369,34 +6708,34 @@ function createPiCodingAgentHarness(opts) {
6369
6708
  "tool-output-error"
6370
6709
  ]);
6371
6710
  const standardToolInputsSeen = /* @__PURE__ */ new Set();
6372
- function isStandardVisibleChunk(chunk2) {
6373
- const type = chunk2.type;
6711
+ function isStandardVisibleChunk(chunk3) {
6712
+ const type = chunk3.type;
6374
6713
  return typeof type === "string" && STANDARD_VISIBLE_CHUNK_TYPES.has(type);
6375
6714
  }
6376
6715
  function namespaceInlinePartIds(input2) {
6377
6716
  if (inlineTurnIndex === 0) return input2;
6378
- return input2.map((chunk2) => {
6379
- const rec = chunk2;
6717
+ return input2.map((chunk3) => {
6718
+ const rec = chunk3;
6380
6719
  if ((rec.type === "text-start" || rec.type === "text-delta" || rec.type === "text-end" || rec.type === "reasoning-start" || rec.type === "reasoning-delta" || rec.type === "reasoning-end") && typeof rec.id === "string") {
6381
6720
  return { ...rec, id: `turn-${inlineTurnIndex}:${rec.id}` };
6382
6721
  }
6383
- return chunk2;
6722
+ return chunk3;
6384
6723
  });
6385
6724
  }
6386
6725
  function filterSdkChunksForCurrentSegment(input2) {
6387
6726
  const out = [];
6388
- for (const chunk2 of input2) {
6389
- const rec = chunk2;
6390
- if (inlineTurnIndex > 0 && isStandardVisibleChunk(chunk2)) continue;
6727
+ for (const chunk3 of input2) {
6728
+ const rec = chunk3;
6729
+ if (inlineTurnIndex > 0 && isStandardVisibleChunk(chunk3)) continue;
6391
6730
  if (rec.type === "tool-input-available" && rec.toolCallId) {
6392
6731
  standardToolInputsSeen.add(rec.toolCallId);
6393
- out.push(chunk2);
6732
+ out.push(chunk3);
6394
6733
  continue;
6395
6734
  }
6396
6735
  if ((rec.type === "tool-output-available" || rec.type === "tool-output-error" || rec.type === "tool-output-denied") && rec.toolCallId && !standardToolInputsSeen.has(rec.toolCallId)) {
6397
6736
  continue;
6398
6737
  }
6399
- out.push(chunk2);
6738
+ out.push(chunk3);
6400
6739
  }
6401
6740
  return out;
6402
6741
  }
@@ -6528,8 +6867,8 @@ function createPiCodingAgentHarness(opts) {
6528
6867
  const queue = nativeFollowUpQueues.get(input.sessionId);
6529
6868
  if (queue?.length) {
6530
6869
  const index = queue.findIndex((item) => item.text === text || item.displayText === text);
6531
- if (index >= 0) queue.splice(index, 1);
6532
- else queue.shift();
6870
+ const consumed = index >= 0 ? queue.splice(index, 1)[0] : queue.shift();
6871
+ rememberConsumedNativeFollowUp(input.sessionId, consumed);
6533
6872
  if (queue.length > 0) nativeFollowUpQueues.set(input.sessionId, queue);
6534
6873
  else clearNativeFollowUpWork(input.sessionId);
6535
6874
  } else {
@@ -6541,10 +6880,10 @@ function createPiCodingAgentHarness(opts) {
6541
6880
  } else {
6542
6881
  const sdkChunks = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
6543
6882
  const shouldBufferTerminalError = event.type === "message_update" && event.assistantMessageEvent?.type === "error";
6544
- const visibleSdkChunks = shouldBufferTerminalError ? sdkChunks.filter((chunk2) => {
6545
- const type = chunk2.type;
6883
+ const visibleSdkChunks = shouldBufferTerminalError ? sdkChunks.filter((chunk3) => {
6884
+ const type = chunk3.type;
6546
6885
  if (type === "error" || type === "finish") {
6547
- pendingTerminalErrorChunks.push(chunk2);
6886
+ pendingTerminalErrorChunks.push(chunk3);
6548
6887
  return false;
6549
6888
  }
6550
6889
  return true;
@@ -6552,12 +6891,12 @@ function createPiCodingAgentHarness(opts) {
6552
6891
  const sdkChunksForTurn = filterSdkChunksForCurrentSegment(visibleSdkChunks);
6553
6892
  converted = [...piHistoryChunks, ...sdkChunksForTurn];
6554
6893
  }
6555
- for (const chunk2 of converted) {
6556
- const t = chunk2.type;
6894
+ for (const chunk3 of converted) {
6895
+ const t = chunk3.type;
6557
6896
  if (t === "text-delta" || t === "data-pi-text-delta" || t === "data-pi-text-end") {
6558
6897
  sawTextChunk = true;
6559
6898
  }
6560
- const piEndData = chunk2.data;
6899
+ const piEndData = chunk3.data;
6561
6900
  if (t === "data-pi-message-end" && piEndData?.role === "assistant" && typeof piEndData.text === "string" && piEndData.text.length > 0) {
6562
6901
  sawTextChunk = true;
6563
6902
  }
@@ -6626,7 +6965,7 @@ function createPiCodingAgentHarness(opts) {
6626
6965
  const base = basenameForAttachment(a.filename ?? "image");
6627
6966
  const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
6628
6967
  const relPath = `${DEFAULT_ATTACHMENT_DIR}/${base}-${unique}.${ext}`;
6629
- await mkdir11(join10(opts.cwd, DEFAULT_ATTACHMENT_DIR), { recursive: true });
6968
+ await mkdir12(join10(opts.cwd, DEFAULT_ATTACHMENT_DIR), { recursive: true });
6630
6969
  await writeFile7(join10(opts.cwd, relPath), bytes);
6631
6970
  savedPaths.push(relPath);
6632
6971
  } catch (err) {
@@ -6717,7 +7056,7 @@ ${attachmentNotes.join("\n")}` : message,
6717
7056
 
6718
7057
  // src/server/harness/pi-coding-agent/pluginLoader.ts
6719
7058
  import { readdir as readdir7, stat as stat9, readFile as readFile10 } from "fs/promises";
6720
- import { join as join11, extname as extname3, resolve as resolve7, sep as sep6, relative as relative9 } from "path";
7059
+ import { join as join11, extname as extname4, resolve as resolve8, sep as sep6, relative as relative9 } from "path";
6721
7060
  import { homedir as homedir4 } from "os";
6722
7061
  import { pathToFileURL as pathToFileURL2 } from "url";
6723
7062
  var VALID_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs"]);
@@ -6743,7 +7082,7 @@ async function fileExists(path4) {
6743
7082
  async function discoverFromDir(dir, source) {
6744
7083
  if (!await dirExists2(dir)) return [];
6745
7084
  const entries = await readdir7(dir);
6746
- return entries.filter((e) => VALID_EXTENSIONS.has(extname3(e))).map((e) => ({ path: join11(dir, e), source }));
7085
+ return entries.filter((e) => VALID_EXTENSIONS.has(extname4(e))).map((e) => ({ path: join11(dir, e), source }));
6747
7086
  }
6748
7087
  function extractTools(mod) {
6749
7088
  const tools = [];
@@ -6787,8 +7126,8 @@ async function loadNpmPlugin(pkgDir, importFn) {
6787
7126
  if (!await fileExists(pkgJsonPath)) return [];
6788
7127
  const pkgJson = JSON.parse(await readFile10(pkgJsonPath, "utf-8"));
6789
7128
  const main = pkgJson.main ?? "index.js";
6790
- const resolvedMain = resolve7(pkgDir, main);
6791
- const resolvedPkgDir = resolve7(pkgDir);
7129
+ const resolvedMain = resolve8(pkgDir, main);
7130
+ const resolvedPkgDir = resolve8(pkgDir);
6792
7131
  if (resolvedMain !== resolvedPkgDir && !resolvedMain.startsWith(resolvedPkgDir + sep6)) {
6793
7132
  return [];
6794
7133
  }
@@ -6886,8 +7225,8 @@ function getRuntimeBundleStorageRoot(bundle) {
6886
7225
 
6887
7226
  // src/server/tools/operations/bound.ts
6888
7227
  import { constants as constants4 } from "fs";
6889
- import { access as access4, lstat as lstat4, mkdir as mkdir12, readFile as readFile11, readdir as readdir8, readlink, realpath as realpath4, stat as stat10, writeFile as writeFile8 } from "fs/promises";
6890
- import { dirname as dirname11, isAbsolute as isAbsolute7, join as join12, relative as relative10, resolve as resolve8 } from "path";
7228
+ import { access as access4, lstat as lstat4, mkdir as mkdir13, readFile as readFile11, readdir as readdir8, readlink, realpath as realpath4, stat as stat10, writeFile as writeFile8 } from "fs/promises";
7229
+ import { dirname as dirname11, isAbsolute as isAbsolute7, join as join12, relative as relative10, resolve as resolve9 } from "path";
6891
7230
  function toPosixPath(value) {
6892
7231
  return value.split("\\").join("/");
6893
7232
  }
@@ -6943,7 +7282,7 @@ async function walkMatches(root, current, pattern, ignore, limit, out) {
6943
7282
  const entries = await readdir8(current, { withFileTypes: true });
6944
7283
  for (const entry of entries) {
6945
7284
  if (out.length >= limit) return;
6946
- const absolutePath = resolve8(current, entry.name);
7285
+ const absolutePath = resolve9(current, entry.name);
6947
7286
  const relativePath = toPosixPath(relative10(root, absolutePath));
6948
7287
  if (entry.isDirectory() && shouldSkipDir(relativePath, ignore)) continue;
6949
7288
  if (matchesGlob(relativePath, pattern)) {
@@ -6968,12 +7307,12 @@ async function findNearestExistingAncestor(absPath) {
6968
7307
  }
6969
7308
  }
6970
7309
  async function assertWithinWorkspace(workspaceRoot, absPath) {
6971
- const realRoot = await realpath4(resolve8(workspaceRoot));
7310
+ const realRoot = await realpath4(resolve9(workspaceRoot));
6972
7311
  try {
6973
7312
  const s = await lstat4(absPath);
6974
7313
  if (s.isSymbolicLink()) {
6975
7314
  const target = await readlink(absPath);
6976
- const resolvedTarget = resolve8(dirname11(absPath), target);
7315
+ const resolvedTarget = resolve9(dirname11(absPath), target);
6977
7316
  const nearestAncestor = await findNearestExistingAncestor(resolvedTarget);
6978
7317
  const realAncestor = await realpath4(nearestAncestor);
6979
7318
  const rel2 = relative10(realRoot, realAncestor);
@@ -7045,13 +7384,13 @@ function boundFs(workspaceRoot, opts = {}) {
7045
7384
  async writeFile(absolutePath, content) {
7046
7385
  const storagePath = toStoragePath(absolutePath);
7047
7386
  await assertWithinWorkspace(workspaceRoot, storagePath);
7048
- await mkdir12(dirname11(storagePath), { recursive: true });
7387
+ await mkdir13(dirname11(storagePath), { recursive: true });
7049
7388
  await writeFile8(storagePath, content);
7050
7389
  },
7051
7390
  async mkdir(dir) {
7052
7391
  const storagePath = toStoragePath(dir);
7053
7392
  await assertWithinWorkspace(workspaceRoot, storagePath);
7054
- await mkdir12(storagePath, { recursive: true });
7393
+ await mkdir13(storagePath, { recursive: true });
7055
7394
  }
7056
7395
  };
7057
7396
  const edit = {
@@ -7168,8 +7507,8 @@ function vercelBashOps(sandbox, opts = {}) {
7168
7507
  env: filteredEnv,
7169
7508
  signal,
7170
7509
  timeoutMs: timeout ? timeout * 1e3 : void 0,
7171
- onStdout: (chunk2) => onData(Buffer.from(chunk2)),
7172
- onStderr: (chunk2) => onData(Buffer.from(chunk2))
7510
+ onStdout: (chunk3) => onData(Buffer.from(chunk3)),
7511
+ onStderr: (chunk3) => onData(Buffer.from(chunk3))
7173
7512
  }).then((result) => ({ exitCode: result.exitCode }));
7174
7513
  }
7175
7514
  };
@@ -7336,7 +7675,7 @@ import {
7336
7675
  truncateHead,
7337
7676
  truncateLine
7338
7677
  } from "@mariozechner/pi-coding-agent";
7339
- import { resolve as resolve9, relative as relative12 } from "path";
7678
+ import { resolve as resolve10, relative as relative12 } from "path";
7340
7679
 
7341
7680
  // src/server/catalog/tools/_shared.ts
7342
7681
  function makeError(message) {
@@ -7488,7 +7827,7 @@ function vercelGrepTool(sandbox, workspaceRoot) {
7488
7827
  return makeError("pattern is required");
7489
7828
  }
7490
7829
  if (workspaceRoot && typeof params.path === "string" && params.path.length > 0) {
7491
- const resolved = resolve9(workspaceRoot, params.path);
7830
+ const resolved = resolve10(workspaceRoot, params.path);
7492
7831
  const rel = relative12(workspaceRoot, resolved);
7493
7832
  if (rel.startsWith("..") || rel.startsWith("/")) {
7494
7833
  return makeError(`path "${params.path}" is outside workspace`);
@@ -7576,6 +7915,84 @@ import {
7576
7915
  createBashToolDefinition,
7577
7916
  createLocalBashOperations
7578
7917
  } from "@mariozechner/pi-coding-agent";
7918
+
7919
+ // src/server/catalog/toolReadiness.ts
7920
+ var WORKSPACE_PREPARING_MESSAGE = "Workspace is still preparing. Try again in a moment.";
7921
+ function isRuntimeRequirement(requirement) {
7922
+ return requirement === "runtime-dependencies" || requirement.startsWith("runtime:");
7923
+ }
7924
+ function isReadyState(state) {
7925
+ return state === true || typeof state === "object" && state !== null && state.ready === true;
7926
+ }
7927
+ function blockedState(state) {
7928
+ if (state && typeof state === "object" && state.ready === false) return state;
7929
+ return { ready: false, state: "preparing", retryable: true };
7930
+ }
7931
+ function runtimeRequirementMessage(requirement, state) {
7932
+ if (state === "failed") {
7933
+ return "Runtime setup failed. Retry provisioning or reload the workspace.";
7934
+ }
7935
+ switch (requirement) {
7936
+ case "runtime:python":
7937
+ return "Python runtime dependencies are still installing. This usually takes a few seconds.";
7938
+ case "runtime:node":
7939
+ return "Node runtime dependencies are still installing. This usually takes a few seconds.";
7940
+ default:
7941
+ return "Runtime dependencies are still installing. This usually takes a few seconds.";
7942
+ }
7943
+ }
7944
+ function workspaceNotReadyToolResult(requirement) {
7945
+ return {
7946
+ content: [{ type: "text", text: WORKSPACE_PREPARING_MESSAGE }],
7947
+ isError: true,
7948
+ details: {
7949
+ code: ErrorCode.enum.WORKSPACE_NOT_READY,
7950
+ retryable: true,
7951
+ requirement
7952
+ }
7953
+ };
7954
+ }
7955
+ function runtimeNotReadyToolResult(requirement, state = { ready: false, state: "preparing", retryable: true }) {
7956
+ const readinessState = state.state ?? "preparing";
7957
+ const code = readinessState === "failed" ? ErrorCode.enum.RUNTIME_PROVISIONING_FAILED : ErrorCode.enum.AGENT_RUNTIME_NOT_READY;
7958
+ const message = state.message ?? runtimeRequirementMessage(requirement, readinessState);
7959
+ return {
7960
+ content: [{ type: "text", text: message }],
7961
+ isError: true,
7962
+ details: {
7963
+ code,
7964
+ retryable: state.retryable ?? true,
7965
+ requirement,
7966
+ state: readinessState,
7967
+ ...state.workspaceId ? { workspaceId: state.workspaceId } : {},
7968
+ ...state.errorCode ? { errorCode: state.errorCode } : {},
7969
+ ...state.causeCode ? { causeCode: state.causeCode } : {}
7970
+ }
7971
+ };
7972
+ }
7973
+ function readinessToolResult(requirement, state) {
7974
+ if (isRuntimeRequirement(requirement)) return runtimeNotReadyToolResult(requirement, blockedState(state));
7975
+ return workspaceNotReadyToolResult(requirement);
7976
+ }
7977
+ function withReadinessRequirements(tool, readinessRequirements) {
7978
+ if (tool.readinessRequirements === readinessRequirements) return tool;
7979
+ return { ...tool, readinessRequirements };
7980
+ }
7981
+ function wrapToolForReadiness(tool, checkReadiness) {
7982
+ if (!checkReadiness || !tool.readinessRequirements || tool.readinessRequirements.length === 0) return tool;
7983
+ return {
7984
+ ...tool,
7985
+ async execute(params, ctx) {
7986
+ for (const requirement of tool.readinessRequirements ?? []) {
7987
+ const readiness = checkReadiness(requirement, tool);
7988
+ if (!isReadyState(readiness)) return readinessToolResult(requirement, readiness);
7989
+ }
7990
+ return await tool.execute(params, ctx);
7991
+ }
7992
+ };
7993
+ }
7994
+
7995
+ // src/server/tools/harness/index.ts
7579
7996
  function shellEscape2(s) {
7580
7997
  return `'${s.replace(/'/g, "'\\''")}'`;
7581
7998
  }
@@ -7639,27 +8056,77 @@ function bashOptionsForMode(bundle, runtime) {
7639
8056
  };
7640
8057
  }
7641
8058
  }
7642
- function adaptPiTool2(piTool) {
8059
+ function isRuntimeReady(readiness) {
8060
+ return readiness === void 0 || readiness === true || typeof readiness === "object" && readiness !== null && readiness.ready === true;
8061
+ }
8062
+ function runtimeRequirementForCommand(command) {
8063
+ if (command.includes(".boring-agent/venv/bin/")) return "runtime:python";
8064
+ if (/python(?:3)?\s+-c\s+['\"][^'\"]*(?:from\s+\S+\s+)?import\s+/.test(command)) return "runtime:python";
8065
+ if (command.includes(".boring-agent/node/")) return "runtime:node";
8066
+ if (command.includes(".boring-agent/")) return "runtime-dependencies";
8067
+ return void 0;
8068
+ }
8069
+ function runtimeRequirementForFailure(text) {
8070
+ if (/\.boring-agent\/venv\/bin\/[^\s:]+: (?:No such file or directory|not found)/i.test(text)) return "runtime:python";
8071
+ if (/ModuleNotFoundError: No module named ['\"][^'\"]+['\"]/i.test(text)) return "runtime:python";
8072
+ if (/\.boring-agent\/node\/[^\s:]+: (?:No such file or directory|not found)/i.test(text)) return "runtime:node";
8073
+ if (/(?:^|\n)(?:[^\n:]+:\s*)?(?:line \d+:\s*)?[A-Za-z0-9_.-]+: command not found/i.test(text)) return "runtime-dependencies";
8074
+ return void 0;
8075
+ }
8076
+ function adaptPiTool2(piTool, runtime) {
7643
8077
  return {
7644
8078
  name: piTool.name,
7645
8079
  description: piTool.description,
7646
8080
  promptSnippet: piTool.promptSnippet,
7647
8081
  parameters: piTool.parameters,
8082
+ readinessRequirements: ["sandbox-exec"],
7648
8083
  async execute(params, ctx) {
7649
- const result = await piTool.execute(
7650
- ctx.toolCallId,
7651
- params,
7652
- ctx.abortSignal,
7653
- ctx.onUpdate ? (update) => {
7654
- const text = update.content.filter((c) => c.type === "text").map((c) => c.text).join("");
7655
- ctx.onUpdate(text);
7656
- } : void 0,
7657
- {}
7658
- );
8084
+ const command = typeof params.command === "string" ? params.command : "";
8085
+ const readiness = runtime?.getReadiness?.();
8086
+ const commandRuntimeRequirement = command ? runtimeRequirementForCommand(command) : void 0;
8087
+ if (commandRuntimeRequirement && !isRuntimeReady(readiness)) {
8088
+ return runtimeNotReadyToolResult(
8089
+ commandRuntimeRequirement,
8090
+ readiness && typeof readiness === "object" && readiness.ready === false ? readiness : { ready: false, state: "preparing", retryable: true }
8091
+ );
8092
+ }
8093
+ let result;
8094
+ try {
8095
+ result = await piTool.execute(
8096
+ ctx.toolCallId,
8097
+ params,
8098
+ ctx.abortSignal,
8099
+ ctx.onUpdate ? (update) => {
8100
+ const text2 = update.content.filter((c) => c.type === "text").map((c) => c.text).join("");
8101
+ ctx.onUpdate(text2);
8102
+ } : void 0,
8103
+ {}
8104
+ );
8105
+ } catch (error) {
8106
+ const message = error instanceof Error ? error.message : String(error);
8107
+ const latestReadiness2 = runtime?.getReadiness?.();
8108
+ const failureRuntimeRequirement2 = runtimeRequirementForFailure(message);
8109
+ if (command && failureRuntimeRequirement2 && !isRuntimeReady(latestReadiness2)) {
8110
+ return runtimeNotReadyToolResult(
8111
+ failureRuntimeRequirement2,
8112
+ latestReadiness2 && typeof latestReadiness2 === "object" && latestReadiness2.ready === false ? latestReadiness2 : { ready: false, state: "preparing", retryable: true }
8113
+ );
8114
+ }
8115
+ throw error;
8116
+ }
7659
8117
  const textContent = (result.content ?? []).filter((c) => c.type === "text").map((c) => ({ type: "text", text: c.text }));
8118
+ const text = textContent.map((part) => part.text).join("\n");
8119
+ const latestReadiness = runtime?.getReadiness?.();
8120
+ const failureRuntimeRequirement = runtimeRequirementForFailure(text);
8121
+ if (command && failureRuntimeRequirement && !isRuntimeReady(latestReadiness)) {
8122
+ return runtimeNotReadyToolResult(
8123
+ failureRuntimeRequirement,
8124
+ latestReadiness && typeof latestReadiness === "object" && latestReadiness.ready === false ? latestReadiness : { ready: false, state: "preparing", retryable: true }
8125
+ );
8126
+ }
7660
8127
  return {
7661
8128
  content: textContent.length > 0 ? textContent : [{ type: "text", text: "" }],
7662
- isError: false,
8129
+ isError: Boolean(result.isError),
7663
8130
  details: result.details
7664
8131
  };
7665
8132
  }
@@ -7720,7 +8187,7 @@ function createExecuteIsolatedCodeTool(sandbox) {
7720
8187
  }
7721
8188
  function buildHarnessAgentTools(bundle, runtime) {
7722
8189
  const tools = [
7723
- adaptPiTool2(createBashToolDefinition(bundle.workspace.root, bashOptionsForMode(bundle, runtime)))
8190
+ adaptPiTool2(createBashToolDefinition(bundle.workspace.root, bashOptionsForMode(bundle, runtime)), runtime)
7724
8191
  ];
7725
8192
  if (bundle.sandbox.capabilities.includes("isolated-code")) {
7726
8193
  tools.push(createExecuteIsolatedCodeTool(bundle.sandbox));
@@ -8048,9 +8515,9 @@ var TurnBuffer = class {
8048
8515
  onChunk = /* @__PURE__ */ new Set();
8049
8516
  onDone = /* @__PURE__ */ new Set();
8050
8517
  gc = null;
8051
- append(chunk2) {
8518
+ append(chunk3) {
8052
8519
  const idx = this.nextIdx++;
8053
- const entry = { idx, chunk: chunk2 };
8520
+ const entry = { idx, chunk: chunk3 };
8054
8521
  this.ring.push(entry);
8055
8522
  if (this.ring.length > MAX_CHUNKS) this.ring.shift();
8056
8523
  for (const h of this.onChunk) h(entry);
@@ -8165,11 +8632,11 @@ var VALID_OPS = /* @__PURE__ */ new Set([
8165
8632
  function isRecord2(value) {
8166
8633
  return typeof value === "object" && value !== null;
8167
8634
  }
8168
- function parseFileChangeChunk(chunk2) {
8169
- if (!isRecord2(chunk2) || chunk2.type !== "data-file-changed") {
8635
+ function parseFileChangeChunk(chunk3) {
8636
+ if (!isRecord2(chunk3) || chunk3.type !== "data-file-changed") {
8170
8637
  return null;
8171
8638
  }
8172
- const data = chunk2.data;
8639
+ const data = chunk3.data;
8173
8640
  if (!isRecord2(data)) {
8174
8641
  return null;
8175
8642
  }
@@ -8195,6 +8662,115 @@ function parseFileChangeChunk(chunk2) {
8195
8662
  return change;
8196
8663
  }
8197
8664
 
8665
+ // src/server/http/turnManager.ts
8666
+ function chunk2(data) {
8667
+ return data;
8668
+ }
8669
+ var TurnManager = class {
8670
+ buffers = new StreamBufferStore();
8671
+ activeAbortControllersBySession = /* @__PURE__ */ new Map();
8672
+ activeTurnBySession = /* @__PURE__ */ new Map();
8673
+ async startTurn(options) {
8674
+ const reserved = this.reserve(options.sessionId, options.turnId);
8675
+ if (!reserved) return { active: true };
8676
+ try {
8677
+ const runtime = await options.resolveRuntime();
8678
+ options.onSubmitted?.();
8679
+ const chunks = runtime.harness.sendMessage(options.input, {
8680
+ abortSignal: reserved.abortController.signal,
8681
+ workdir: runtime.workdir
8682
+ });
8683
+ this.pump(reserved, chunks, options);
8684
+ return { turnId: reserved.turnId, buffer: reserved.buffer };
8685
+ } catch (err) {
8686
+ this.cleanup(reserved);
8687
+ reserved.buffer.markComplete(() => this.buffers.evict(reserved.sessionId, reserved.turnId));
8688
+ throw err;
8689
+ }
8690
+ }
8691
+ abortTurn(sessionId, turnId) {
8692
+ const requestedTurnId = turnId ?? this.activeTurnBySession.get(sessionId);
8693
+ if (!requestedTurnId) return;
8694
+ this.activeAbortControllersBySession.get(sessionId)?.get(requestedTurnId)?.abort();
8695
+ }
8696
+ getActive(sessionId) {
8697
+ return this.buffers.getActive(sessionId);
8698
+ }
8699
+ reserve(sessionId, turnId) {
8700
+ if (this.activeTurnBySession.has(sessionId)) return null;
8701
+ const abortController = new AbortController();
8702
+ const sessionAbortControllers = this.activeAbortControllersBySession.get(sessionId) ?? /* @__PURE__ */ new Map();
8703
+ sessionAbortControllers.set(turnId, abortController);
8704
+ this.activeAbortControllersBySession.set(sessionId, sessionAbortControllers);
8705
+ this.activeTurnBySession.set(sessionId, turnId);
8706
+ return {
8707
+ sessionId,
8708
+ turnId,
8709
+ abortController,
8710
+ buffer: this.buffers.create(sessionId, turnId)
8711
+ };
8712
+ }
8713
+ pump(reserved, chunks, options) {
8714
+ void (async () => {
8715
+ let streamFailed = false;
8716
+ try {
8717
+ reserved.buffer.append(chunk2({ type: "data-turn-start", data: { turnId: reserved.turnId } }));
8718
+ for await (const rawChunk of chunks) {
8719
+ const nextChunk = rawChunk;
8720
+ const fileChange = parseFileChangeChunk(nextChunk);
8721
+ if (fileChange) {
8722
+ options.sessionChangesTracker?.record(reserved.sessionId, fileChange);
8723
+ }
8724
+ reserved.buffer.append(nextChunk);
8725
+ }
8726
+ } catch (err) {
8727
+ streamFailed = true;
8728
+ options.onStreamError?.(err);
8729
+ reserved.buffer.append({
8730
+ type: "error",
8731
+ errorText: "internal error"
8732
+ });
8733
+ } finally {
8734
+ this.cleanup(reserved);
8735
+ if (!streamFailed) options.onStreamComplete?.();
8736
+ reserved.buffer.markComplete(() => this.buffers.evict(reserved.sessionId, reserved.turnId));
8737
+ }
8738
+ })();
8739
+ }
8740
+ cleanup(reserved) {
8741
+ const sessionAbortControllers = this.activeAbortControllersBySession.get(reserved.sessionId);
8742
+ if (sessionAbortControllers?.get(reserved.turnId) === reserved.abortController) {
8743
+ sessionAbortControllers.delete(reserved.turnId);
8744
+ if (sessionAbortControllers.size === 0) this.activeAbortControllersBySession.delete(reserved.sessionId);
8745
+ }
8746
+ if (this.activeTurnBySession.get(reserved.sessionId) === reserved.turnId) {
8747
+ this.activeTurnBySession.delete(reserved.sessionId);
8748
+ }
8749
+ }
8750
+ };
8751
+ function createBufferedUiMessageStream(buffer, closeEmitter, cursor = -1) {
8752
+ return createUIMessageStream({
8753
+ async execute({ writer }) {
8754
+ const replayed = buffer.replay(cursor);
8755
+ for (const e of replayed) writer.write(e.chunk);
8756
+ if (buffer.complete) return;
8757
+ await new Promise((resolve11) => {
8758
+ const unsub = buffer.subscribe(
8759
+ (e) => writer.write(e.chunk),
8760
+ () => {
8761
+ unsub();
8762
+ resolve11();
8763
+ }
8764
+ );
8765
+ closeEmitter.on("close", () => {
8766
+ unsub();
8767
+ resolve11();
8768
+ });
8769
+ });
8770
+ }
8771
+ });
8772
+ }
8773
+
8198
8774
  // src/server/harness/pi-coding-agent/projectPiDataMessages.ts
8199
8775
  function projectPiDataMessages(messages) {
8200
8776
  const dataParts = messages.flatMap((message) => message.parts ?? []).filter((part) => {
@@ -8268,7 +8844,8 @@ var chatBodySchema = z.object({
8268
8844
  mediaType: z.string().optional(),
8269
8845
  url: z.string()
8270
8846
  })
8271
- ).max(20).optional()
8847
+ ).max(20).optional(),
8848
+ clientTurnId: z.string().min(1).max(128).optional()
8272
8849
  });
8273
8850
  function addTelemetryProperty(properties, key, value) {
8274
8851
  if (typeof value === "string" && value) properties[key] = value;
@@ -8282,7 +8859,7 @@ function chatRoutes(app, opts, done) {
8282
8859
  const { sessionChangesTracker } = opts;
8283
8860
  const telemetry = opts.telemetry ?? noopTelemetry;
8284
8861
  const validateBody = createBodyValidator(chatBodySchema);
8285
- const buffers = new StreamBufferStore();
8862
+ const turnManager = new TurnManager();
8286
8863
  const lastFollowUpBySession = /* @__PURE__ */ new Map();
8287
8864
  const cancelledFollowUpsBySession = /* @__PURE__ */ new Map();
8288
8865
  const MAX_FOLLOWUP_CACHE = 5e3;
@@ -8319,12 +8896,18 @@ function chatRoutes(app, opts, done) {
8319
8896
  }
8320
8897
  throw new Error("chat route requires harness/workdir or getRuntime");
8321
8898
  }
8899
+ async function resolveSessionStore(request) {
8900
+ if (opts.getSessionStore) return await opts.getSessionStore(request);
8901
+ if (opts.sessionStore) return opts.sessionStore;
8902
+ const runtime = await resolveRuntime(request);
8903
+ return runtime.harness.sessions;
8904
+ }
8322
8905
  app.post(
8323
8906
  "/api/v1/agent/chat",
8324
8907
  { preHandler: validateBody },
8325
8908
  async (request, reply) => {
8326
- const { sessionId, message, model, thinkingLevel, attachments } = request.body;
8327
- const turnId = randomUUID3();
8909
+ const { sessionId, message, model, thinkingLevel, attachments, clientTurnId } = request.body;
8910
+ const turnId = clientTurnId ?? randomUUID3();
8328
8911
  const startedAt = Date.now();
8329
8912
  const telemetryProperties2 = {
8330
8913
  sessionId,
@@ -8337,83 +8920,57 @@ function chatRoutes(app, opts, done) {
8337
8920
  name: "agent.chat.started",
8338
8921
  properties: telemetryProperties2
8339
8922
  });
8340
- const abortController = new AbortController();
8341
- let streamStarted = false;
8342
- let streamCompleted = false;
8343
- reply.raw.on("close", () => {
8344
- if (streamStarted && !streamCompleted && !abortController.signal.aborted) {
8345
- abortController.abort();
8346
- }
8347
- });
8348
- const buf = buffers.create(sessionId, turnId);
8349
8923
  try {
8350
- const runtime = await resolveRuntime(request);
8351
- const ctx = {
8352
- abortSignal: abortController.signal,
8353
- workdir: runtime.workdir
8354
- };
8355
- safeCapture(telemetry, {
8356
- name: "agent.chat.message.submitted",
8357
- properties: telemetryProperties2
8358
- });
8359
- const chunks = runtime.harness.sendMessage(
8360
- { sessionId, message, model, thinkingLevel, attachments },
8361
- ctx
8362
- );
8363
- const stream = createUIMessageStream({
8364
- async execute({ writer }) {
8365
- let streamFailed = false;
8366
- try {
8367
- for await (const chunk2 of chunks) {
8368
- const c = chunk2;
8369
- const fileChange = parseFileChangeChunk(c);
8370
- if (fileChange) {
8371
- sessionChangesTracker?.record(sessionId, fileChange);
8372
- }
8373
- buf.append(c);
8374
- writer.write(c);
8924
+ const startedTurn = await turnManager.startTurn({
8925
+ sessionId,
8926
+ turnId,
8927
+ input: { sessionId, message, model, thinkingLevel, attachments },
8928
+ resolveRuntime: () => resolveRuntime(request),
8929
+ sessionChangesTracker,
8930
+ onSubmitted: () => {
8931
+ safeCapture(telemetry, {
8932
+ name: "agent.chat.message.submitted",
8933
+ properties: telemetryProperties2
8934
+ });
8935
+ },
8936
+ onStreamError: (err) => {
8937
+ request.log.error({ err, sessionId }, "[chat] stream error");
8938
+ safeCapture(telemetry, {
8939
+ name: "agent.chat.failed",
8940
+ properties: {
8941
+ ...telemetryProperties2,
8942
+ status: "error",
8943
+ durationMs: Date.now() - startedAt,
8944
+ errorCode: ErrorCode.enum.INTERNAL_ERROR
8375
8945
  }
8376
- } catch (err) {
8377
- streamFailed = true;
8378
- request.log.error({ err, sessionId }, "[chat] stream error");
8379
- safeCapture(telemetry, {
8380
- name: "agent.chat.failed",
8381
- properties: {
8382
- ...telemetryProperties2,
8383
- status: "error",
8384
- durationMs: Date.now() - startedAt,
8385
- errorCode: ErrorCode.enum.INTERNAL_ERROR
8386
- }
8387
- });
8388
- const errChunk = {
8389
- type: "error",
8390
- errorText: "internal error"
8391
- };
8392
- buf.append(errChunk);
8393
- writer.write(errChunk);
8394
- } finally {
8395
- streamCompleted = true;
8396
- if (!streamFailed) {
8397
- safeCapture(telemetry, {
8398
- name: "agent.chat.completed",
8399
- properties: {
8400
- ...telemetryProperties2,
8401
- status: "ok",
8402
- durationMs: Date.now() - startedAt
8403
- }
8404
- });
8946
+ });
8947
+ },
8948
+ onStreamComplete: () => {
8949
+ safeCapture(telemetry, {
8950
+ name: "agent.chat.completed",
8951
+ properties: {
8952
+ ...telemetryProperties2,
8953
+ status: "ok",
8954
+ durationMs: Date.now() - startedAt
8405
8955
  }
8406
- buf.markComplete(() => buffers.evict(sessionId, turnId));
8407
- }
8956
+ });
8408
8957
  }
8409
8958
  });
8410
- streamStarted = true;
8959
+ if ("active" in startedTurn) {
8960
+ return reply.code(409).send({
8961
+ error: {
8962
+ code: ERROR_CODE_CONFLICT,
8963
+ message: "turn_already_active"
8964
+ }
8965
+ });
8966
+ }
8967
+ const stream = createBufferedUiMessageStream(startedTurn.buffer, request.raw);
8411
8968
  reply.hijack();
8412
8969
  pipeUIMessageStreamToResponse({
8413
8970
  response: reply.raw,
8414
8971
  stream,
8415
8972
  headers: {
8416
- "X-Turn-Id": turnId,
8973
+ "X-Turn-Id": startedTurn.turnId,
8417
8974
  "X-Accel-Buffering": "no",
8418
8975
  "Cache-Control": "no-cache, no-transform"
8419
8976
  }
@@ -8431,8 +8988,6 @@ function chatRoutes(app, opts, done) {
8431
8988
  errorCode: safeTelemetryErrorCode(err?.code)
8432
8989
  }
8433
8990
  });
8434
- buf.markComplete(() => buffers.evict(sessionId, turnId));
8435
- if (streamStarted) return;
8436
8991
  const statusCode = err?.statusCode;
8437
8992
  const stableCode = err?.code;
8438
8993
  if (typeof statusCode === "number" && statusCode >= 400 && statusCode < 600) {
@@ -8464,7 +9019,7 @@ function chatRoutes(app, opts, done) {
8464
9019
  }
8465
9020
  });
8466
9021
  }
8467
- const active = buffers.getActive(sessionId);
9022
+ const active = turnManager.getActive(sessionId);
8468
9023
  if (!active) {
8469
9024
  return reply.code(204).send();
8470
9025
  }
@@ -8481,26 +9036,7 @@ function chatRoutes(app, opts, done) {
8481
9036
  { sessionId, cursor, bufHigh: buf.highIdx },
8482
9037
  "[resume] replaying"
8483
9038
  );
8484
- const stream = createUIMessageStream({
8485
- async execute({ writer }) {
8486
- const replayed = buf.replay(cursor);
8487
- for (const e of replayed) writer.write(e.chunk);
8488
- if (buf.complete) return;
8489
- await new Promise((resolve10) => {
8490
- const unsub = buf.subscribe(
8491
- (e) => writer.write(e.chunk),
8492
- () => {
8493
- unsub();
8494
- resolve10();
8495
- }
8496
- );
8497
- request.raw.on("close", () => {
8498
- unsub();
8499
- resolve10();
8500
- });
8501
- });
8502
- }
8503
- });
9039
+ const stream = createBufferedUiMessageStream(buf, request.raw, cursor);
8504
9040
  reply.hijack();
8505
9041
  pipeUIMessageStreamToResponse({
8506
9042
  response: reply.raw,
@@ -8521,8 +9057,8 @@ function chatRoutes(app, opts, done) {
8521
9057
  workspaceId: request.workspaceContext?.workspaceId ?? "default"
8522
9058
  };
8523
9059
  try {
8524
- const runtime = await resolveRuntime(request);
8525
- const detail = await runtime.harness.sessions.load(ctx, sessionId);
9060
+ const store = await resolveSessionStore(request);
9061
+ const detail = await store.load(ctx, sessionId);
8526
9062
  const messages = Array.isArray(detail?.messages) ? detail.messages : [];
8527
9063
  return reply.code(200).send({ messages });
8528
9064
  } catch {
@@ -8530,6 +9066,16 @@ function chatRoutes(app, opts, done) {
8530
9066
  }
8531
9067
  }
8532
9068
  );
9069
+ app.delete(
9070
+ "/api/v1/agent/chat/:sessionId/turn",
9071
+ async (request, reply) => {
9072
+ const { sessionId } = request.params;
9073
+ const query = request.query;
9074
+ const requestedTurnId = typeof query.turnId === "string" && query.turnId.length > 0 ? query.turnId : void 0;
9075
+ turnManager.abortTurn(sessionId, requestedTurnId);
9076
+ return reply.code(204).send();
9077
+ }
9078
+ );
8533
9079
  app.post(
8534
9080
  "/api/v1/agent/chat/:sessionId/followup",
8535
9081
  async (request, reply) => {
@@ -8619,9 +9165,9 @@ function chatRoutes(app, opts, done) {
8619
9165
  workspaceId: request.workspaceContext?.workspaceId ?? "default"
8620
9166
  };
8621
9167
  try {
8622
- const runtime = await resolveRuntime(request);
8623
- if (runtime.harness.sessions.saveMessages) {
8624
- await runtime.harness.sessions.saveMessages(ctx, sessionId, projectPiDataMessages(body.messages));
9168
+ const store = await resolveSessionStore(request);
9169
+ if (store.saveMessages) {
9170
+ await store.saveMessages(ctx, sessionId, projectPiDataMessages(body.messages));
8625
9171
  }
8626
9172
  return reply.code(204).send();
8627
9173
  } catch {
@@ -8634,41 +9180,23 @@ function chatRoutes(app, opts, done) {
8634
9180
 
8635
9181
  // src/server/http/routes/models.ts
8636
9182
  import { AuthStorage as AuthStorage2, ModelRegistry as ModelRegistry2 } from "@mariozechner/pi-coding-agent";
8637
- var AUTH_CHECK_TTL_MS = 6e4;
8638
9183
  function modelsRoutes(app, _opts, done) {
8639
9184
  const authStorage = AuthStorage2.create();
8640
9185
  const registry = ModelRegistry2.create(authStorage);
8641
9186
  registerConfiguredModelProviders(registry);
8642
- const providerAuthCache = /* @__PURE__ */ new Map();
8643
- async function providerHasResolvableAuth(provider) {
8644
- const now = Date.now();
8645
- const cached = providerAuthCache.get(provider);
8646
- if (cached && cached.expiresAt > now) return cached.available;
8647
- let available = false;
8648
- try {
8649
- const model = registry.getAvailable().find((candidate) => candidate.provider === provider);
8650
- const auth = model ? await registry.getApiKeyAndHeaders(model) : void 0;
8651
- available = auth?.ok === true && (typeof auth.apiKey === "string" && auth.apiKey.trim().length > 0 || auth.headers !== void 0);
8652
- } catch {
8653
- available = false;
8654
- }
8655
- providerAuthCache.set(provider, { available, expiresAt: now + AUTH_CHECK_TTL_MS });
8656
- return available;
8657
- }
8658
9187
  app.get("/api/v1/agent/models", async (_request, reply) => {
8659
9188
  const availableModels = registry.getAvailable();
8660
9189
  const availableSet = new Set(
8661
9190
  availableModels.map((m) => `${m.provider}:${m.id}`)
8662
9191
  );
8663
- const providerAvailability = /* @__PURE__ */ new Map();
8664
- await Promise.all([...new Set(availableModels.map((m) => m.provider))].map(async (provider) => {
8665
- providerAvailability.set(provider, await providerHasResolvableAuth(provider));
8666
- }));
8667
9192
  const models = registry.getAll().map((m) => ({
8668
9193
  provider: m.provider,
8669
9194
  id: m.id,
8670
9195
  label: m.label ?? m.id,
8671
- available: availableSet.has(`${m.provider}:${m.id}`) && providerAvailability.get(m.provider) === true
9196
+ // Keep this endpoint cheap: it is fetched on chat mount, so it must never
9197
+ // block workspace load on deep provider auth resolution. ModelRegistry's
9198
+ // available set is already derived from configured auth sources.
9199
+ available: availableSet.has(`${m.provider}:${m.id}`)
8672
9200
  }));
8673
9201
  models.sort((a, b) => {
8674
9202
  if (a.available !== b.available) return a.available ? -1 : 1;
@@ -8981,8 +9509,8 @@ function buildAnalysisPrompt(session, transcript, instructions) {
8981
9509
  ].filter(Boolean).join("\n");
8982
9510
  }
8983
9511
  function analysisTextFromChunks(chunks) {
8984
- return chunks.map((chunk2) => {
8985
- const record = chunk2;
9512
+ return chunks.map((chunk3) => {
9513
+ const record = chunk3;
8986
9514
  return record.type === "text-delta" && typeof record.delta === "string" ? record.delta : "";
8987
9515
  }).join("").trim();
8988
9516
  }
@@ -9112,14 +9640,14 @@ function sessionRoutes(app, opts, done) {
9112
9640
  }
9113
9641
  const abortController = new AbortController();
9114
9642
  const chunks = [];
9115
- for await (const chunk2 of runtime.harness.sendMessage(
9643
+ for await (const chunk3 of runtime.harness.sendMessage(
9116
9644
  { sessionId: analysisSession.id, message: prompt },
9117
9645
  {
9118
9646
  abortSignal: abortController.signal,
9119
9647
  workdir: runtime.workdir
9120
9648
  }
9121
9649
  )) {
9122
- chunks.push(chunk2);
9650
+ chunks.push(chunk3);
9123
9651
  }
9124
9652
  return {
9125
9653
  sourceSession: {
@@ -9249,12 +9777,35 @@ function readyStatusRoutes(app, opts, done) {
9249
9777
  app.get("/api/v1/ready-status", async (request, reply) => {
9250
9778
  const tracker = opts.getTracker ? await opts.getTracker(request) : opts.tracker;
9251
9779
  if (!tracker) throw new Error("ready-status route requires tracker or getTracker");
9780
+ const initial = tracker.getReadiness();
9781
+ const initialEvent = {
9782
+ state: tracker.state,
9783
+ sandboxReady: initial.sandboxReady,
9784
+ harnessReady: initial.harnessReady,
9785
+ capabilities: initial.capabilities,
9786
+ message: initial.degradedReason,
9787
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9788
+ };
9789
+ const initialRuntimePending = initialEvent.capabilities.runtimeDependencies.state === "preparing";
9790
+ if (initialEvent.state === "degraded" || initialEvent.state === "ready" && !initialRuntimePending) {
9791
+ return reply.type("text/event-stream").headers({ "Cache-Control": "no-cache" }).send(`:
9792
+
9793
+ event: status
9794
+ data: ${JSON.stringify(initialEvent)}
9795
+
9796
+ `);
9797
+ }
9252
9798
  reply.raw.writeHead(200, {
9253
9799
  "Content-Type": "text/event-stream",
9254
9800
  "Cache-Control": "no-cache",
9255
9801
  Connection: "keep-alive"
9256
9802
  });
9257
- reply.raw.write(":\n\n");
9803
+ reply.raw.write(`:
9804
+
9805
+ event: status
9806
+ data: ${JSON.stringify(initialEvent)}
9807
+
9808
+ `);
9258
9809
  let closed = false;
9259
9810
  let unsubscribe = null;
9260
9811
  const closeStream = () => {
@@ -9263,16 +9814,19 @@ function readyStatusRoutes(app, opts, done) {
9263
9814
  unsubscribe?.();
9264
9815
  reply.raw.end();
9265
9816
  };
9817
+ request.raw.on("close", closeStream);
9818
+ reply.hijack();
9266
9819
  unsubscribe = tracker.subscribe((event) => {
9267
9820
  if (closed) return;
9268
9821
  reply.raw.write(`event: status
9269
9822
  data: ${JSON.stringify(event)}
9270
9823
 
9271
9824
  `);
9272
- if (event.state === "ready" || event.state === "degraded") closeStream();
9825
+ const runtimePending = event.capabilities.runtimeDependencies.state === "preparing";
9826
+ if (event.state === "degraded" || event.state === "ready" && !runtimePending) {
9827
+ queueMicrotask(closeStream);
9828
+ }
9273
9829
  });
9274
- request.raw.on("close", closeStream);
9275
- reply.hijack();
9276
9830
  return reply;
9277
9831
  });
9278
9832
  done();
@@ -9370,14 +9924,26 @@ function searchRoutes(app, opts, done) {
9370
9924
  }
9371
9925
 
9372
9926
  // src/server/sandbox/vercel-sandbox/readyStatus.ts
9927
+ function defaultCapabilities(sandboxReady, harnessReady) {
9928
+ return {
9929
+ chat: { state: harnessReady ? "ready" : "preparing" },
9930
+ workspace: { state: sandboxReady ? "ready" : "preparing" },
9931
+ runtimeDependencies: { state: "ready" }
9932
+ };
9933
+ }
9373
9934
  var ReadyStatusTracker = class {
9374
9935
  _sandboxReady;
9375
9936
  _harnessReady;
9376
9937
  _degradedReason;
9938
+ _capabilities;
9377
9939
  subscribers = /* @__PURE__ */ new Set();
9378
9940
  constructor(opts) {
9379
9941
  this._sandboxReady = opts?.sandboxReady ?? false;
9380
9942
  this._harnessReady = opts?.harnessReady ?? false;
9943
+ this._capabilities = {
9944
+ ...defaultCapabilities(this._sandboxReady, this._harnessReady),
9945
+ ...opts?.capabilities ?? {}
9946
+ };
9381
9947
  }
9382
9948
  get state() {
9383
9949
  if (this._degradedReason) return "degraded";
@@ -9390,23 +9956,52 @@ var ReadyStatusTracker = class {
9390
9956
  return {
9391
9957
  sandboxReady: this._sandboxReady,
9392
9958
  harnessReady: this._harnessReady,
9959
+ capabilities: this.getCapabilities(),
9393
9960
  degradedReason: this._degradedReason
9394
9961
  };
9395
9962
  }
9963
+ getCapabilities() {
9964
+ return {
9965
+ chat: { ...this._capabilities.chat },
9966
+ workspace: { ...this._capabilities.workspace },
9967
+ runtimeDependencies: { ...this._capabilities.runtimeDependencies }
9968
+ };
9969
+ }
9970
+ updateCapability(name, detail) {
9971
+ this._capabilities = {
9972
+ ...this._capabilities,
9973
+ [name]: { ...detail }
9974
+ };
9975
+ this.emit();
9976
+ }
9977
+ updateRuntimeDependencies(detail) {
9978
+ this.updateCapability("runtimeDependencies", detail);
9979
+ }
9396
9980
  markSandboxReady() {
9397
9981
  if (this._sandboxReady) return;
9398
9982
  this._sandboxReady = true;
9983
+ if (this._capabilities.workspace.state === "preparing") {
9984
+ this._capabilities.workspace = { state: "ready" };
9985
+ }
9399
9986
  this.emit();
9400
9987
  }
9401
9988
  markHarnessReady() {
9402
9989
  if (this._harnessReady) return;
9403
9990
  this._harnessReady = true;
9991
+ if (this._capabilities.chat.state === "preparing") {
9992
+ this._capabilities.chat = { state: "ready" };
9993
+ }
9404
9994
  this.emit();
9405
9995
  }
9406
9996
  markDegraded(reason) {
9407
9997
  this._degradedReason = reason;
9408
9998
  this.emit();
9409
9999
  }
10000
+ clearDegraded() {
10001
+ if (!this._degradedReason) return;
10002
+ this._degradedReason = void 0;
10003
+ this.emit();
10004
+ }
9410
10005
  subscribe(handler) {
9411
10006
  this.subscribers.add(handler);
9412
10007
  handler(this.snapshot());
@@ -9423,6 +10018,7 @@ var ReadyStatusTracker = class {
9423
10018
  state: this.state,
9424
10019
  sandboxReady: this._sandboxReady,
9425
10020
  harnessReady: this._harnessReady,
10021
+ capabilities: this.getCapabilities(),
9426
10022
  message: this._degradedReason,
9427
10023
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
9428
10024
  };
@@ -9518,6 +10114,7 @@ async function createAgentApp(opts = {}) {
9518
10114
  await app.register(chatRoutes, {
9519
10115
  harness,
9520
10116
  workdir: runtimeBundle.workspace.root,
10117
+ sessionStore: harness.sessions,
9521
10118
  sessionChangesTracker,
9522
10119
  telemetry: opts.telemetry
9523
10120
  });
@@ -9578,36 +10175,6 @@ function applyCspHeaders(response, opts = {}) {
9578
10175
  import { basename as basename3 } from "path";
9579
10176
  import { AuthStorage as AuthStorage3, ModelRegistry as ModelRegistry3 } from "@mariozechner/pi-coding-agent";
9580
10177
 
9581
- // src/server/catalog/toolReadiness.ts
9582
- var WORKSPACE_PREPARING_MESSAGE = "Workspace is still preparing. Try again in a moment.";
9583
- function workspaceNotReadyToolResult(requirement) {
9584
- return {
9585
- content: [{ type: "text", text: WORKSPACE_PREPARING_MESSAGE }],
9586
- isError: true,
9587
- details: {
9588
- code: ErrorCode.enum.WORKSPACE_NOT_READY,
9589
- retryable: true,
9590
- requirement
9591
- }
9592
- };
9593
- }
9594
- function withReadinessRequirements(tool, readinessRequirements) {
9595
- if (tool.readinessRequirements === readinessRequirements) return tool;
9596
- return { ...tool, readinessRequirements };
9597
- }
9598
- function wrapToolForReadiness(tool, checkReadiness) {
9599
- if (!checkReadiness || !tool.readinessRequirements || tool.readinessRequirements.length === 0) return tool;
9600
- return {
9601
- ...tool,
9602
- async execute(params, ctx) {
9603
- for (const requirement of tool.readinessRequirements ?? []) {
9604
- if (!checkReadiness(requirement, tool)) return workspaceNotReadyToolResult(requirement);
9605
- }
9606
- return await tool.execute(params, ctx);
9607
- }
9608
- };
9609
- }
9610
-
9611
10178
  // src/server/catalog/mergeTools.ts
9612
10179
  function setLastRegistered(merged, tool) {
9613
10180
  merged.delete(tool.name);
@@ -9642,11 +10209,11 @@ function mergeTools(options) {
9642
10209
 
9643
10210
  // src/server/tools/upload/index.ts
9644
10211
  import { readFile as readFile12 } from "fs/promises";
9645
- import { extname as extname4, join as join13 } from "path";
10212
+ import { extname as extname5, join as join13 } from "path";
9646
10213
  var DEFAULT_UPLOAD_DIR = "assets/images";
9647
10214
  var MAX_UPLOAD_BYTES2 = 10 * 1024 * 1024;
9648
10215
  function contentTypeFromExt(path4) {
9649
- switch (extname4(path4).toLowerCase()) {
10216
+ switch (extname5(path4).toLowerCase()) {
9650
10217
  case ".jpg":
9651
10218
  case ".jpeg":
9652
10219
  return "image/jpeg";
@@ -9665,7 +10232,7 @@ function contentTypeFromExt(path4) {
9665
10232
  }
9666
10233
  }
9667
10234
  function extForUpload2(filename, contentType) {
9668
- const fromName = extname4(filename).toLowerCase().replace(/^\./, "");
10235
+ const fromName = extname5(filename).toLowerCase().replace(/^\./, "");
9669
10236
  if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
9670
10237
  if (contentType === "image/jpeg") return "jpg";
9671
10238
  if (contentType === "image/png") return "png";
@@ -9831,6 +10398,29 @@ function createRuntimeProvisioningFailedError(workspaceId, cause) {
9831
10398
  }
9832
10399
  );
9833
10400
  }
10401
+ function isRuntimeReadinessRequirement(requirement) {
10402
+ return requirement === "runtime-dependencies" || requirement.startsWith("runtime:");
10403
+ }
10404
+ function causeCodeFrom(error) {
10405
+ const code = error?.code;
10406
+ return typeof code === "string" ? code : void 0;
10407
+ }
10408
+ function createRuntimeReadinessCheck(workspaceId, getRuntimeDependencies) {
10409
+ return (requirement) => {
10410
+ if (!isRuntimeReadinessRequirement(requirement)) return true;
10411
+ const runtimeDependencies = getRuntimeDependencies();
10412
+ if (runtimeDependencies.state === "ready" || runtimeDependencies.state === "not-started") return true;
10413
+ return {
10414
+ ready: false,
10415
+ state: runtimeDependencies.state,
10416
+ errorCode: runtimeDependencies.errorCode,
10417
+ causeCode: runtimeDependencies.causeCode,
10418
+ message: runtimeDependencies.message,
10419
+ workspaceId,
10420
+ retryable: runtimeDependencies.retryable ?? true
10421
+ };
10422
+ };
10423
+ }
9834
10424
  var registerAgentRoutes = async (app, opts) => {
9835
10425
  const workspaceRoot = opts.workspaceRoot ?? process.cwd();
9836
10426
  const sessionId = opts.sessionId ?? DEFAULT_WORKSPACE_ID3;
@@ -9926,14 +10516,99 @@ var registerAgentRoutes = async (app, opts) => {
9926
10516
  requestId: request?.id,
9927
10517
  telemetry: opts.telemetry
9928
10518
  };
9929
- let runtimeProvisioning = await runRuntimeProvisioning(workspaceId, scope, request);
10519
+ let runtimeProvisioning;
10520
+ let runtimeDependencies = hasRuntimeProvisioningInput ? {
10521
+ state: "preparing",
10522
+ requirement: "runtime-dependencies",
10523
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
10524
+ retryable: true
10525
+ } : { state: "ready" };
10526
+ let provisioningGeneration = 0;
9930
10527
  const runtimeBundle = await modeAdapter.create(modeCtx);
10528
+ const readyTracker = new ReadyStatusTracker({
10529
+ sandboxReady: resolvedMode !== "vercel-sandbox",
10530
+ harnessReady: false,
10531
+ capabilities: {
10532
+ chat: { state: "preparing" },
10533
+ workspace: { state: resolvedMode !== "vercel-sandbox" ? "ready" : "preparing" },
10534
+ runtimeDependencies
10535
+ }
10536
+ });
10537
+ if (resolvedMode === "vercel-sandbox") {
10538
+ queueMicrotask(() => readyTracker.markSandboxReady());
10539
+ }
10540
+ let binding;
10541
+ const updateRuntimeDependencies = (next) => {
10542
+ runtimeDependencies = next;
10543
+ if (binding) binding.runtimeDependencies = next;
10544
+ readyTracker.updateRuntimeDependencies(next);
10545
+ };
10546
+ const startRuntimeProvisioning = (provisionRequest) => {
10547
+ if (!hasRuntimeProvisioningInput) return void 0;
10548
+ if (binding?.runtimeProvisioningTask && runtimeDependencies.state === "preparing") {
10549
+ return binding.runtimeProvisioningTask;
10550
+ }
10551
+ const generation = ++provisioningGeneration;
10552
+ readyTracker.clearDegraded();
10553
+ updateRuntimeDependencies({
10554
+ state: "preparing",
10555
+ requirement: "runtime-dependencies",
10556
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
10557
+ retryable: true
10558
+ });
10559
+ const task = runRuntimeProvisioning(workspaceId, scope, provisionRequest).then(
10560
+ async (result) => {
10561
+ if (generation !== provisioningGeneration) return result;
10562
+ runtimeProvisioning = result;
10563
+ if (binding) binding.runtimeProvisioning = result;
10564
+ if (binding?.harness.reloadSession) {
10565
+ try {
10566
+ const sessions = await binding.harness.sessions.list({ workspaceId });
10567
+ await Promise.allSettled(
10568
+ sessions.map((session) => binding?.harness.reloadSession?.(session.id))
10569
+ );
10570
+ } catch (error) {
10571
+ app.log.warn({ err: error, workspaceId }, "[agent] failed to refresh harness sessions after runtime provisioning");
10572
+ }
10573
+ }
10574
+ updateRuntimeDependencies({
10575
+ state: "ready",
10576
+ requirement: "runtime-dependencies",
10577
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
10578
+ retryable: true
10579
+ });
10580
+ return result;
10581
+ },
10582
+ (error) => {
10583
+ if (generation !== provisioningGeneration) throw error;
10584
+ const causeCode = causeCodeFrom(error);
10585
+ updateRuntimeDependencies({
10586
+ state: "failed",
10587
+ requirement: "runtime-dependencies",
10588
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
10589
+ errorCode: ErrorCode.enum.RUNTIME_PROVISIONING_FAILED,
10590
+ ...causeCode ? { causeCode } : {},
10591
+ retryable: true,
10592
+ message: "Agent runtime provisioning failed. Reload the workspace and try again."
10593
+ });
10594
+ readyTracker.markDegraded("runtime dependency provisioning failed");
10595
+ app.log.warn({ err: error, workspaceId }, "[agent] background runtime provisioning failed");
10596
+ throw error;
10597
+ }
10598
+ );
10599
+ task.catch(() => {
10600
+ });
10601
+ if (binding) binding.runtimeProvisioningTask = task;
10602
+ return task;
10603
+ };
10604
+ const checkReadiness = createRuntimeReadinessCheck(workspaceId, () => runtimeDependencies);
9931
10605
  const standardTools = [
9932
10606
  ...buildHarnessAgentTools(runtimeBundle, {
9933
10607
  getCurrent: () => runtimeProvisioning ? {
9934
10608
  env: runtimeProvisioning.env,
9935
10609
  pathEntries: runtimeProvisioning.pathEntries
9936
- } : void 0
10610
+ } : void 0,
10611
+ getReadiness: () => checkReadiness("runtime:python", {})
9937
10612
  }),
9938
10613
  ...buildFilesystemAgentTools(runtimeBundle),
9939
10614
  ...buildUploadAgentTools(runtimeBundle)
@@ -9966,7 +10641,8 @@ var registerAgentRoutes = async (app, opts) => {
9966
10641
  ...scopedExtraTools
9967
10642
  ],
9968
10643
  pluginTools,
9969
- logger: app.log
10644
+ logger: app.log,
10645
+ checkReadiness
9970
10646
  });
9971
10647
  const harnessFactory = opts.harnessFactory ?? ((input) => createPiCodingAgentHarness({
9972
10648
  ...input,
@@ -9975,9 +10651,18 @@ var registerAgentRoutes = async (app, opts) => {
9975
10651
  noSkills: true,
9976
10652
  ...scope.pi,
9977
10653
  additionalSkillPaths: [
9978
- ...runtimeProvisioning?.skillPaths ?? [],
9979
10654
  ...scope.pi?.additionalSkillPaths ?? []
9980
- ]
10655
+ ],
10656
+ getHotReloadableResources: () => {
10657
+ const hot = scope.pi?.getHotReloadableResources?.() ?? {};
10658
+ return {
10659
+ ...hot,
10660
+ additionalSkillPaths: [
10661
+ ...runtimeProvisioning?.skillPaths ?? [],
10662
+ ...hot.additionalSkillPaths ?? []
10663
+ ]
10664
+ };
10665
+ }
9981
10666
  }
9982
10667
  }));
9983
10668
  const harness = await harnessFactory({
@@ -9988,24 +10673,22 @@ var registerAgentRoutes = async (app, opts) => {
9988
10673
  systemPromptDynamic: opts.getSystemPromptDynamic ? () => opts.getSystemPromptDynamic?.({ workspaceId, workspaceRoot: root }) : opts.systemPromptDynamic,
9989
10674
  telemetry: opts.telemetry
9990
10675
  });
9991
- const readyTracker = new ReadyStatusTracker({
9992
- sandboxReady: resolvedMode !== "vercel-sandbox",
9993
- harnessReady: true
9994
- });
9995
- if (resolvedMode === "vercel-sandbox") {
9996
- queueMicrotask(() => readyTracker.markSandboxReady());
9997
- }
9998
- return {
10676
+ readyTracker.markHarnessReady();
10677
+ binding = {
9999
10678
  runtimeBundle,
10000
10679
  runtimeProvisioning,
10680
+ runtimeDependencies,
10681
+ runtimeProvisioningTask: void 0,
10001
10682
  reprovision: async (reloadRequest) => {
10002
- runtimeProvisioning = await runRuntimeProvisioning(workspaceId, scope, reloadRequest);
10003
- return runtimeProvisioning;
10683
+ const result = await startRuntimeProvisioning(reloadRequest);
10684
+ return await result;
10004
10685
  },
10005
10686
  harness,
10006
10687
  tools,
10007
10688
  readyTracker
10008
10689
  };
10690
+ startRuntimeProvisioning(request);
10691
+ return binding;
10009
10692
  }
10010
10693
  function createRuntimeBindingEntry(workspaceId, scope, request) {
10011
10694
  const entry = {
@@ -10099,6 +10782,7 @@ var registerAgentRoutes = async (app, opts) => {
10099
10782
  const hasRuntimeProvisioningInput = opts.provisionWorkspace !== false && Boolean(opts.provisionRuntime);
10100
10783
  const staticBinding = requestScopedRuntime ? null : await getOrCreateRuntimeBinding(sessionId);
10101
10784
  const skillsScopeByRequest = /* @__PURE__ */ new WeakMap();
10785
+ const earlySessionStores = /* @__PURE__ */ new Map();
10102
10786
  function getSkillsScopeForRequest(request) {
10103
10787
  let promise = skillsScopeByRequest.get(request);
10104
10788
  if (!promise) {
@@ -10111,6 +10795,18 @@ var registerAgentRoutes = async (app, opts) => {
10111
10795
  if (staticBinding) return staticBinding;
10112
10796
  return await getOrCreateRuntimeBinding(getRequestWorkspaceId(request), request, options);
10113
10797
  }
10798
+ async function getSessionStoreForRequest(request) {
10799
+ if (staticBinding) return staticBinding.harness.sessions;
10800
+ const scope = await resolveRuntimeScope(getRequestWorkspaceId(request), request);
10801
+ const cached = earlySessionStores.get(scope.key);
10802
+ if (cached) return cached;
10803
+ const store = new PiSessionStore(scope.root, {
10804
+ sessionNamespace: scope.sessionNamespace,
10805
+ storageCwd: scope.root
10806
+ });
10807
+ earlySessionStores.set(scope.key, store);
10808
+ return store;
10809
+ }
10114
10810
  const agentToolNames = staticBinding ? staticBinding.tools.map((tool) => tool.name) : [
10115
10811
  ...STANDARD_AGENT_TOOL_NAMES,
10116
10812
  ...(opts.extraTools ?? []).map((tool) => tool.name)
@@ -10179,20 +10875,18 @@ var registerAgentRoutes = async (app, opts) => {
10179
10875
  });
10180
10876
  await app.register(chatRoutes, {
10181
10877
  getRuntime: async (request) => {
10182
- const binding = await getBindingForRequest(request, { failIfPending: hasRuntimeProvisioningInput });
10878
+ const binding = await getBindingForRequest(request);
10183
10879
  return {
10184
10880
  harness: binding.harness,
10185
10881
  workdir: binding.runtimeBundle.workspace.root
10186
10882
  };
10187
10883
  },
10884
+ getSessionStore: getSessionStoreForRequest,
10188
10885
  sessionChangesTracker,
10189
10886
  telemetry: opts.telemetry
10190
10887
  });
10191
10888
  await app.register(sessionRoutes, {
10192
- getSessionStore: async (request) => {
10193
- const binding = await getBindingForRequest(request);
10194
- return binding.harness.sessions;
10195
- },
10889
+ getSessionStore: getSessionStoreForRequest,
10196
10890
  getRuntime: async (request) => {
10197
10891
  const binding = await getBindingForRequest(request);
10198
10892
  return {