@axiom-lattice/gateway 4.0.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -195,9 +195,9 @@ async function saveProjectFile(params) {
195
195
  }
196
196
  const targetPath = path3.join(resolvedTargetDir, filename);
197
197
  await fs3.writeFile(targetPath, buffer);
198
- const stat2 = await fs3.stat(targetPath);
198
+ const stat3 = await fs3.stat(targetPath);
199
199
  const uri = path3.posix.join("/", relativeDirectory, filename);
200
- return { uri, name: filename, size: stat2.size };
200
+ return { uri, name: filename, size: stat3.size };
201
201
  }
202
202
  var fs3, path3, import_core23, BINARY_EXTENSIONS, ProjectFileValidationError;
203
203
  var init_workspace_file_service = __esm({
@@ -1212,7 +1212,7 @@ var init_WechatChannelAdapter = __esm({
1212
1212
  installationId,
1213
1213
  error: err instanceof Error ? err.message : String(err)
1214
1214
  });
1215
- await new Promise((resolve3) => setTimeout(resolve3, reconnectDelay));
1215
+ await new Promise((resolve4) => setTimeout(resolve4, reconnectDelay));
1216
1216
  reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
1217
1217
  }
1218
1218
  scheduleNextPoll();
@@ -3350,8 +3350,8 @@ function resolveLocalA2AWorkingDirectory(options) {
3350
3350
  }
3351
3351
  function validateWorkingDirectory(cwd) {
3352
3352
  try {
3353
- const stat2 = (0, import_node_fs.statSync)(cwd);
3354
- if (!stat2.isDirectory()) {
3353
+ const stat3 = (0, import_node_fs.statSync)(cwd);
3354
+ if (!stat3.isDirectory()) {
3355
3355
  return `Working directory is not a directory: ${cwd}`;
3356
3356
  }
3357
3357
  return null;
@@ -4125,8 +4125,8 @@ async function pipeAsyncIterableToSse(raw, stream, options = {}) {
4125
4125
  let closed = raw.destroyed || raw.writableEnded;
4126
4126
  let returned = false;
4127
4127
  let resolveClosed;
4128
- const closedPromise = new Promise((resolve3) => {
4129
- resolveClosed = resolve3;
4128
+ const closedPromise = new Promise((resolve4) => {
4129
+ resolveClosed = resolve4;
4130
4130
  });
4131
4131
  const returnConsumer = () => {
4132
4132
  if (returned) return;
@@ -4176,14 +4176,14 @@ async function pipeAsyncIterableToSse(raw, stream, options = {}) {
4176
4176
  async function writeSse(raw, value, closed) {
4177
4177
  if (raw.destroyed || raw.writableEnded) return false;
4178
4178
  if (raw.write(value)) return true;
4179
- return new Promise((resolve3) => {
4179
+ return new Promise((resolve4) => {
4180
4180
  let settled = false;
4181
4181
  const onDrain = () => finish(!raw.destroyed && !raw.writableEnded);
4182
4182
  const finish = (writable) => {
4183
4183
  if (settled) return;
4184
4184
  settled = true;
4185
4185
  raw.removeListener("drain", onDrain);
4186
- resolve3(writable);
4186
+ resolve4(writable);
4187
4187
  };
4188
4188
  raw.once("drain", onDrain);
4189
4189
  void closed.then(() => finish(false));
@@ -6977,7 +6977,7 @@ var ExportImportService = class {
6977
6977
  const files = /* @__PURE__ */ new Map();
6978
6978
  let bundle;
6979
6979
  const gunzip = zlib.createGunzip();
6980
- await new Promise((resolve3, reject) => {
6980
+ await new Promise((resolve4, reject) => {
6981
6981
  let settled = false;
6982
6982
  let entryCount = 0;
6983
6983
  let declaredTotalBytes = 0;
@@ -7097,7 +7097,7 @@ var ExportImportService = class {
7097
7097
  parser.on("end", () => {
7098
7098
  if (settled) return;
7099
7099
  settled = true;
7100
- resolve3();
7100
+ resolve4();
7101
7101
  });
7102
7102
  gunzip.pipe(parser);
7103
7103
  gunzip.end(buffer);
@@ -7782,6 +7782,20 @@ var dataQuerySchema = {
7782
7782
  };
7783
7783
 
7784
7784
  // src/schemas/index.ts
7785
+ var downloadFolderSchema = {
7786
+ description: "Download a workspace project folder as a ZIP archive",
7787
+ tags: ["Workspace"],
7788
+ querystring: {
7789
+ type: "object",
7790
+ properties: {
7791
+ path: { type: "string", minLength: 1 },
7792
+ assistantId: { type: "string", minLength: 1 },
7793
+ tenantId: { type: "string", minLength: 1 }
7794
+ },
7795
+ required: ["path"],
7796
+ additionalProperties: false
7797
+ }
7798
+ };
7785
7799
  var getAllMemoryItemsSchema = {
7786
7800
  description: "Get all memory items for an assistant thread",
7787
7801
  tags: ["Memory"],
@@ -8380,6 +8394,7 @@ function registerSandboxProxyRoutes(app2) {
8380
8394
  // src/controllers/workspace.ts
8381
8395
  var fs4 = __toESM(require("fs/promises"));
8382
8396
  var path4 = __toESM(require("path"));
8397
+ var import_fs = require("fs");
8383
8398
  var os2 = __toESM(require("os"));
8384
8399
  var import_core24 = require("@axiom-lattice/core");
8385
8400
  var import_core25 = require("@axiom-lattice/core");
@@ -8388,6 +8403,83 @@ var import_uuid3 = require("uuid");
8388
8403
  init_mime();
8389
8404
  init_workspace_file_service();
8390
8405
  init_path_containment();
8406
+ var MAX_ZIP_ENTRIES = 1e4;
8407
+ var MAX_ZIP_FILE_SIZE = 100 * 1024 * 1024;
8408
+ var MAX_ZIP_TOTAL_SIZE = 512 * 1024 * 1024;
8409
+ var FolderDownloadError = class extends Error {
8410
+ constructor(statusCode, code, message) {
8411
+ super(message);
8412
+ this.statusCode = statusCode;
8413
+ this.code = code;
8414
+ this.name = "FolderDownloadError";
8415
+ }
8416
+ };
8417
+ function zipCrc32(data) {
8418
+ let crc = 4294967295;
8419
+ for (const byte of data) {
8420
+ crc ^= byte;
8421
+ for (let bit = 0; bit < 8; bit += 1) {
8422
+ crc = crc >>> 1 ^ 3988292384 & -(crc & 1);
8423
+ }
8424
+ }
8425
+ return (crc ^ 4294967295) >>> 0;
8426
+ }
8427
+ function createZip(entries) {
8428
+ const localParts = [];
8429
+ const centralParts = [];
8430
+ let offset = 0;
8431
+ for (const entry of entries) {
8432
+ const name = Buffer.from(entry.name, "utf8");
8433
+ const data = entry.data;
8434
+ const crc = zipCrc32(data);
8435
+ const local = Buffer.alloc(30 + name.length + data.length);
8436
+ local.writeUInt32LE(67324752, 0);
8437
+ local.writeUInt16LE(20, 4);
8438
+ local.writeUInt16LE(0, 6);
8439
+ local.writeUInt16LE(2048, 8);
8440
+ local.writeUInt16LE(0, 10);
8441
+ local.writeUInt16LE(0, 12);
8442
+ local.writeUInt32LE(crc, 14);
8443
+ local.writeUInt32LE(data.length, 18);
8444
+ local.writeUInt32LE(data.length, 22);
8445
+ local.writeUInt16LE(name.length, 26);
8446
+ local.writeUInt16LE(0, 28);
8447
+ name.copy(local, 30);
8448
+ data.copy(local, 30 + name.length);
8449
+ localParts.push(local);
8450
+ const central = Buffer.alloc(46 + name.length);
8451
+ central.writeUInt32LE(33639248, 0);
8452
+ central.writeUInt16LE(20, 4);
8453
+ central.writeUInt16LE(20, 6);
8454
+ central.writeUInt16LE(0, 8);
8455
+ central.writeUInt16LE(2048, 10);
8456
+ central.writeUInt16LE(0, 12);
8457
+ central.writeUInt16LE(0, 14);
8458
+ central.writeUInt32LE(crc, 16);
8459
+ central.writeUInt32LE(data.length, 20);
8460
+ central.writeUInt32LE(data.length, 24);
8461
+ central.writeUInt16LE(name.length, 28);
8462
+ central.writeUInt16LE(0, 30);
8463
+ central.writeUInt16LE(0, 32);
8464
+ central.writeUInt16LE(0, 34);
8465
+ central.writeUInt16LE(entry.directory ? 16 : 0, 36);
8466
+ central.writeUInt32LE(entry.directory ? 16 : 0, 38);
8467
+ central.writeUInt32LE(offset, 42);
8468
+ name.copy(central, 46);
8469
+ centralParts.push(central);
8470
+ offset += local.length;
8471
+ }
8472
+ const centralDirectory = Buffer.concat(centralParts);
8473
+ const end = Buffer.alloc(22);
8474
+ end.writeUInt32LE(101010256, 0);
8475
+ end.writeUInt16LE(0, 4);
8476
+ end.writeUInt16LE(0, 6);
8477
+ end.writeUInt16LE(entries.length, 8);
8478
+ end.writeUInt16LE(entries.length, 10);
8479
+ end.writeUInt32LE(centralDirectory.length, 12);
8480
+ end.writeUInt32LE(offset, 16);
8481
+ return Buffer.concat([...localParts, centralDirectory, end]);
8482
+ }
8391
8483
  var WorkspaceController = class {
8392
8484
  constructor() {
8393
8485
  this.workspaceStore = (0, import_core24.getStoreLattice)("default", "workspace").store;
@@ -8653,6 +8745,259 @@ var WorkspaceController = class {
8653
8745
  return reply.status(502).send({ success: false, error: `Download proxy error: ${message}` });
8654
8746
  }
8655
8747
  }
8748
+ async downloadFolder(request, reply) {
8749
+ const tenantId = this.getTenantId(request);
8750
+ const { workspaceId, projectId } = request.params;
8751
+ const folderPath = request.query.path;
8752
+ const assistantId = request.query.assistantId;
8753
+ if (!folderPath || folderPath.includes("\\") || folderPath.split("/").includes("..")) {
8754
+ return reply.status(400).send({
8755
+ success: false,
8756
+ error: "Invalid folder path",
8757
+ code: "INVALID_PATH"
8758
+ });
8759
+ }
8760
+ try {
8761
+ const storedWorkspace = await this.workspaceStore.getWorkspaceById(tenantId, workspaceId);
8762
+ if (!storedWorkspace) {
8763
+ throw new FolderDownloadError(404, "WORKSPACE_NOT_FOUND", "Workspace not found");
8764
+ }
8765
+ const project = await this.projectStore.getProjectById(tenantId, projectId);
8766
+ if (!project || project.workspaceId !== workspaceId) {
8767
+ throw new FolderDownloadError(404, "PROJECT_NOT_FOUND", "Project not found");
8768
+ }
8769
+ const { backend, workspace } = await this.getBackend(
8770
+ tenantId,
8771
+ workspaceId,
8772
+ projectId,
8773
+ assistantId,
8774
+ folderPath
8775
+ );
8776
+ if (!backend.lsInfo || workspace.storageType === "sandbox" && !backend.readBinary) {
8777
+ throw new FolderDownloadError(502, "BACKEND_UNSUPPORTED", "Backend does not support folder downloads");
8778
+ }
8779
+ let listEntries2;
8780
+ if (workspace.storageType !== "sandbox") {
8781
+ const root = filesystemRootDirectory(tenantId, workspaceId, projectId);
8782
+ const target = path4.resolve(root, `.${folderPath.startsWith("/") ? folderPath : `/${folderPath}`}`);
8783
+ const contained = await resolvePathWithinRoot(root, target).catch((error) => {
8784
+ if (error instanceof Error && error.code === "ENOENT") {
8785
+ throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
8786
+ }
8787
+ throw error;
8788
+ });
8789
+ let stat3;
8790
+ try {
8791
+ stat3 = await fs4.stat(contained.targetPath);
8792
+ } catch (error) {
8793
+ if (error instanceof Error && error.code === "ENOENT") {
8794
+ throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
8795
+ }
8796
+ throw error;
8797
+ }
8798
+ if (!stat3.isDirectory()) {
8799
+ throw new FolderDownloadError(400, "NOT_A_FOLDER", "Path is not a folder");
8800
+ }
8801
+ } else if (folderPath) {
8802
+ const sandboxManager = (0, import_core26.getSandBoxManager)();
8803
+ const volumeConfig = {
8804
+ assistant_id: assistantId || "",
8805
+ thread_id: "",
8806
+ tenantId,
8807
+ workspaceId,
8808
+ projectId
8809
+ };
8810
+ const volume = await sandboxManager.getVolumeBackendForPath(volumeConfig, folderPath);
8811
+ if (!volume) {
8812
+ const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
8813
+ try {
8814
+ await sandbox.file.listPath(folderPath, { recursive: false });
8815
+ } catch (error) {
8816
+ const code = error instanceof Error ? error.code : void 0;
8817
+ const message = error instanceof Error ? error.message : String(error);
8818
+ if (code === "ENOENT" || /not found|no such file|enoent/i.test(message)) {
8819
+ throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
8820
+ }
8821
+ throw error;
8822
+ }
8823
+ listEntries2 = async (directoryPath) => {
8824
+ const result = await sandbox.file.listPath(directoryPath, { recursive: false });
8825
+ return result.files;
8826
+ };
8827
+ } else {
8828
+ listEntries2 = async (directoryPath) => backend.lsInfo(directoryPath);
8829
+ }
8830
+ }
8831
+ if (!listEntries2) {
8832
+ listEntries2 = async (directoryPath) => backend.lsInfo(directoryPath);
8833
+ }
8834
+ const folderName = path4.basename(folderPath.replace(/\/$/, "")) || "folder";
8835
+ const prefix = `${folderName}/`;
8836
+ const entries = [{ name: prefix, data: Buffer.alloc(0), directory: true }];
8837
+ let totalSize = 0;
8838
+ const filesystemRoot = workspace.storageType === "sandbox" ? void 0 : filesystemRootDirectory(tenantId, workspaceId, projectId);
8839
+ const addFile = (archivePath, data) => {
8840
+ if (data.length > MAX_ZIP_FILE_SIZE || totalSize + data.length > MAX_ZIP_TOTAL_SIZE) {
8841
+ throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
8842
+ }
8843
+ totalSize += data.length;
8844
+ entries.push({ name: archivePath, data, directory: false });
8845
+ };
8846
+ const collectFilesystem = async (directoryPath, archivePath) => {
8847
+ let children;
8848
+ try {
8849
+ children = await fs4.readdir(directoryPath, { withFileTypes: true });
8850
+ } catch (error) {
8851
+ if (error instanceof Error && error.code === "ENOENT") {
8852
+ throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
8853
+ }
8854
+ throw error;
8855
+ }
8856
+ for (const child of children) {
8857
+ if (entries.length >= MAX_ZIP_ENTRIES) {
8858
+ throw new FolderDownloadError(400, "ENTRY_LIMIT", "Folder contains too many entries");
8859
+ }
8860
+ const childPath = path4.join(directoryPath, child.name);
8861
+ const archiveChildPath = `${archivePath}${child.name}`;
8862
+ let resolvedChild;
8863
+ try {
8864
+ resolvedChild = await resolvePathWithinRoot(filesystemRoot, childPath);
8865
+ } catch (error) {
8866
+ if (error instanceof Error && error.code === "ENOENT") {
8867
+ throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
8868
+ }
8869
+ throw new FolderDownloadError(400, "INVALID_PATH", "Folder contains a path outside the project");
8870
+ }
8871
+ let childStat;
8872
+ try {
8873
+ childStat = await fs4.lstat(resolvedChild.targetPath);
8874
+ } catch (error) {
8875
+ if (error instanceof Error && error.code === "ENOENT") {
8876
+ throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
8877
+ }
8878
+ throw error;
8879
+ }
8880
+ if (childStat.isSymbolicLink() || child.isSymbolicLink()) {
8881
+ throw new FolderDownloadError(400, "INVALID_PATH", "Symbolic links are not allowed in folder downloads");
8882
+ }
8883
+ if (child.isDirectory()) {
8884
+ entries.push({ name: `${archiveChildPath}/`, data: Buffer.alloc(0), directory: true });
8885
+ await collectFilesystem(resolvedChild.targetPath, `${archiveChildPath}/`);
8886
+ } else if (child.isFile()) {
8887
+ if (childStat.size > MAX_ZIP_FILE_SIZE || totalSize + childStat.size > MAX_ZIP_TOTAL_SIZE) {
8888
+ throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
8889
+ }
8890
+ const flags = import_fs.constants.O_NOFOLLOW === void 0 ? import_fs.constants.O_RDONLY : import_fs.constants.O_RDONLY | import_fs.constants.O_NOFOLLOW;
8891
+ let handle3;
8892
+ try {
8893
+ handle3 = await fs4.open(resolvedChild.targetPath, flags);
8894
+ } catch (error) {
8895
+ if (error instanceof Error && error.code === "ENOENT") {
8896
+ throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
8897
+ }
8898
+ throw error;
8899
+ }
8900
+ try {
8901
+ const openedStat = await handle3.stat();
8902
+ if (!openedStat.isFile() || openedStat.size > MAX_ZIP_FILE_SIZE || totalSize + openedStat.size > MAX_ZIP_TOTAL_SIZE) {
8903
+ throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
8904
+ }
8905
+ const readLimit = MAX_ZIP_FILE_SIZE + 1;
8906
+ const dataBuffer = Buffer.alloc(Math.min(openedStat.size + 1, readLimit));
8907
+ let bytesRead = 0;
8908
+ while (bytesRead < dataBuffer.length) {
8909
+ const result = await handle3.read(
8910
+ dataBuffer,
8911
+ bytesRead,
8912
+ dataBuffer.length - bytesRead,
8913
+ bytesRead
8914
+ );
8915
+ if (result.bytesRead === 0) break;
8916
+ bytesRead += result.bytesRead;
8917
+ }
8918
+ const data = dataBuffer.subarray(0, bytesRead);
8919
+ const finalStat = await handle3.stat();
8920
+ if (finalStat.size > MAX_ZIP_FILE_SIZE || totalSize + finalStat.size > MAX_ZIP_TOTAL_SIZE || finalStat.size > data.length) {
8921
+ throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
8922
+ }
8923
+ addFile(archiveChildPath, data);
8924
+ } finally {
8925
+ await handle3.close();
8926
+ }
8927
+ }
8928
+ }
8929
+ };
8930
+ const collect = async (currentPath, archivePath) => {
8931
+ let children;
8932
+ try {
8933
+ children = await listEntries2(currentPath);
8934
+ } catch (error) {
8935
+ const message = error instanceof Error ? error.message : String(error);
8936
+ if (/not found|enoent|no such file/i.test(message)) {
8937
+ throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
8938
+ }
8939
+ throw error;
8940
+ }
8941
+ for (const child of children) {
8942
+ if (entries.length >= MAX_ZIP_ENTRIES) {
8943
+ throw new FolderDownloadError(400, "ENTRY_LIMIT", "Folder contains too many entries");
8944
+ }
8945
+ const childPath = child.path;
8946
+ if (childPath.includes("\\") || childPath.split("/").includes("..") || !childPath.startsWith(folderPath.endsWith("/") ? folderPath : `${folderPath}/`)) {
8947
+ throw new FolderDownloadError(400, "INVALID_PATH", "Folder contains an unsafe child path");
8948
+ }
8949
+ if (workspace.storageType !== "sandbox") {
8950
+ const root = filesystemRootDirectory(tenantId, workspaceId, projectId);
8951
+ const virtualChildPath = childPath.startsWith("/") ? childPath : `/${childPath}`;
8952
+ const target = path4.resolve(root, `.${virtualChildPath}`);
8953
+ try {
8954
+ await resolvePathWithinRoot(root, target);
8955
+ const childStat = await fs4.lstat(target);
8956
+ if (childStat.isSymbolicLink()) {
8957
+ throw new FolderDownloadError(400, "INVALID_PATH", "Symbolic links are not allowed in folder downloads");
8958
+ }
8959
+ } catch (error) {
8960
+ if (error instanceof FolderDownloadError) throw error;
8961
+ if (error instanceof Error && error.code === "ENOENT") {
8962
+ throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
8963
+ }
8964
+ throw new FolderDownloadError(400, "INVALID_PATH", "Folder contains a path outside the project");
8965
+ }
8966
+ }
8967
+ const childName = path4.basename(childPath.replace(/\/$/, ""));
8968
+ const childArchivePath = `${archivePath}${childName}`;
8969
+ if (child.is_dir) {
8970
+ entries.push({ name: `${childArchivePath}/`, data: Buffer.alloc(0), directory: true });
8971
+ await collect(childPath, `${childArchivePath}/`);
8972
+ } else {
8973
+ const size = child.size ?? 0;
8974
+ if (size > MAX_ZIP_FILE_SIZE || totalSize + size > MAX_ZIP_TOTAL_SIZE) {
8975
+ throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
8976
+ }
8977
+ const data = await backend.readBinary(childPath);
8978
+ addFile(childArchivePath, data);
8979
+ }
8980
+ }
8981
+ };
8982
+ if (workspace.storageType === "sandbox") {
8983
+ await collect(folderPath, prefix);
8984
+ } else {
8985
+ const root = filesystemRoot;
8986
+ const target = path4.resolve(root, `.${folderPath.startsWith("/") ? folderPath : `/${folderPath}`}`);
8987
+ await collectFilesystem(target, prefix);
8988
+ }
8989
+ const zip = createZip(entries);
8990
+ const downloadName = `${folderName}.zip`;
8991
+ return reply.status(200).type("application/zip").header("Content-Disposition", `attachment; filename*=UTF-8''${encodeURIComponent(downloadName)}`).send(zip);
8992
+ } catch (error) {
8993
+ const folderError = error instanceof FolderDownloadError ? error : error instanceof PathOutsideRootError || error instanceof Error && ["ELOOP", "ENOTDIR", "EACCES"].includes(error.code || "") ? new FolderDownloadError(400, "INVALID_PATH", "Folder contains a path outside the project") : new FolderDownloadError(502, "DOWNLOAD_FAILED", error instanceof Error ? error.message : String(error));
8994
+ return reply.status(folderError.statusCode).send({
8995
+ success: false,
8996
+ error: folderError.message,
8997
+ code: folderError.code
8998
+ });
8999
+ }
9000
+ }
8656
9001
  /**
8657
9002
  * View a file inline in the browser (not download).
8658
9003
  * Returns file content with inline Content-Disposition for preview.
@@ -8945,6 +9290,11 @@ function registerWorkspaceRoutes(app2) {
8945
9290
  "/api/workspaces/:workspaceId/projects/:projectId/downloadfile",
8946
9291
  controller.downloadFile.bind(controller)
8947
9292
  );
9293
+ app2.get(
9294
+ "/api/workspaces/:workspaceId/projects/:projectId/downloadfolder",
9295
+ { schema: downloadFolderSchema },
9296
+ controller.downloadFolder.bind(controller)
9297
+ );
8948
9298
  app2.get(
8949
9299
  "/api/workspaces/:workspaceId/projects/:projectId/viewfile",
8950
9300
  controller.viewFile.bind(controller)
@@ -9169,7 +9519,7 @@ async function testDatabaseConnection(request, reply) {
9169
9519
  await db.connect();
9170
9520
  await db.listTables();
9171
9521
  const latency = Date.now() - startTime;
9172
- await new Promise((resolve3) => setTimeout(resolve3, 100));
9522
+ await new Promise((resolve4) => setTimeout(resolve4, 100));
9173
9523
  await db.disconnect();
9174
9524
  await import_core27.sqlDatabaseManager.removeDatabase(tenantId, testKey);
9175
9525
  return {
@@ -9986,6 +10336,7 @@ var import_uuid5 = require("uuid");
9986
10336
  var import_events = require("events");
9987
10337
  var import_core31 = require("@axiom-lattice/core");
9988
10338
  var import_uuid4 = require("uuid");
10339
+ var TERMINAL_PERSIST_ATTEMPTS = 2;
9989
10340
  function mapLogs(logs) {
9990
10341
  return logs.map((l) => ({
9991
10342
  timestamp: l.ts,
@@ -9997,18 +10348,42 @@ function mapLogs(logs) {
9997
10348
  var EvalRunner = class {
9998
10349
  constructor() {
9999
10350
  this.runs = /* @__PURE__ */ new Map();
10351
+ this.projectLeases = /* @__PURE__ */ new Map();
10000
10352
  this.eventEmitter = new import_events.EventEmitter();
10001
10353
  this.processStartedAt = /* @__PURE__ */ new Date();
10002
10354
  }
10003
10355
  getEventEmitter() {
10004
10356
  return this.eventEmitter;
10005
10357
  }
10358
+ reserveProjectDeletion(tenantId, projectId) {
10359
+ const leaseKey = `${tenantId}:${projectId}`;
10360
+ if (this.projectLeases.has(leaseKey) || [...this.runs.values()].some(
10361
+ (ctx) => ctx.tenantId === tenantId && ctx.projectId === projectId && !ctx.terminal.workerSettled
10362
+ )) return null;
10363
+ const leaseToken = Symbol(`delete:${leaseKey}`);
10364
+ this.projectLeases.set(leaseKey, leaseToken);
10365
+ return () => this.releaseProjectLease(leaseKey, leaseToken);
10366
+ }
10006
10367
  async startRun(tenantId, projectId, suiteIds, caseIds, runConfig, taskId) {
10368
+ const leaseKey = `${tenantId}:${projectId}`;
10369
+ if (this.projectLeases.has(leaseKey)) {
10370
+ throw new Error("A run is already in progress for this project");
10371
+ }
10372
+ const leaseToken = Symbol(leaseKey);
10373
+ this.projectLeases.set(leaseKey, leaseToken);
10374
+ try {
10375
+ return await this.startLeasedRun(tenantId, projectId, suiteIds, caseIds, runConfig, taskId, leaseKey, leaseToken);
10376
+ } catch (error) {
10377
+ this.releaseProjectLease(leaseKey, leaseToken);
10378
+ throw error;
10379
+ }
10380
+ }
10381
+ async startLeasedRun(tenantId, projectId, suiteIds, caseIds, runConfig, taskId, leaseKey, leaseToken) {
10007
10382
  const store2 = this.getEvalStore();
10008
10383
  const project = await store2.getProjectById(tenantId, projectId);
10009
10384
  if (!project) throw new Error("Project not found");
10010
10385
  for (const ctx of this.runs.values()) {
10011
- if (ctx.projectId === projectId) {
10386
+ if (ctx.projectId === projectId && !ctx.terminal.persisted) {
10012
10387
  throw new Error("A run is already in progress for this project");
10013
10388
  }
10014
10389
  }
@@ -10112,14 +10487,35 @@ var EvalRunner = class {
10112
10487
  };
10113
10488
  const abortController = new AbortController();
10114
10489
  const stats = { passed: 0, failed: 0, interrupted: 0, totalScore: 0 };
10490
+ const terminal = { claiming: false, persisted: false, workerSettled: false };
10491
+ const terminalUnavailable = () => terminal.claiming || terminal.persisted;
10492
+ const persistTerminal = async (update, event) => {
10493
+ if (terminalUnavailable()) return false;
10494
+ terminal.claiming = true;
10495
+ try {
10496
+ let lastError;
10497
+ for (let attempt = 0; attempt < TERMINAL_PERSIST_ATTEMPTS; attempt++) {
10498
+ try {
10499
+ const persistedRun = await store2.updateRunStatus(tenantId, runId, update);
10500
+ if (!persistedRun) throw new Error(`Evaluation run ${runId} no longer exists`);
10501
+ lastError = void 0;
10502
+ break;
10503
+ } catch (error) {
10504
+ lastError = error;
10505
+ }
10506
+ }
10507
+ if (lastError) throw lastError;
10508
+ terminal.persisted = true;
10509
+ this.eventEmitter.emit(`run:${runId}`, event);
10510
+ return true;
10511
+ } finally {
10512
+ terminal.claiming = false;
10513
+ }
10514
+ };
10115
10515
  const onCaseComplete = async (result, suiteName) => {
10516
+ if (terminalUnavailable()) return;
10116
10517
  const passed = result.result?.pass ?? false;
10117
10518
  const score = result.result?.final_score ?? 0;
10118
- if (passed) stats.passed++;
10119
- else stats.failed++;
10120
- stats.totalScore += score;
10121
- if (result.interrupted) stats.interrupted++;
10122
- const completedCount = stats.passed + stats.failed;
10123
10519
  await store2.createRunResult(tenantId, runId, (0, import_uuid4.v4)(), {
10124
10520
  suiteName,
10125
10521
  caseId: result.caseId,
@@ -10137,13 +10533,22 @@ var EvalRunner = class {
10137
10533
  error: result.error,
10138
10534
  interrupted: result.interrupted
10139
10535
  });
10536
+ if (terminalUnavailable()) return;
10537
+ if (passed) stats.passed++;
10538
+ else stats.failed++;
10539
+ stats.totalScore += score;
10540
+ if (result.interrupted) stats.interrupted++;
10541
+ const completedCount = stats.passed + stats.failed;
10542
+ if (terminalUnavailable()) return;
10140
10543
  const avgScore = completedCount > 0 ? stats.totalScore / completedCount : 0;
10141
- await store2.updateRunStatus(tenantId, runId, {
10544
+ const persistedRun = await store2.updateRunStatus(tenantId, runId, {
10142
10545
  passedCases: stats.passed,
10143
10546
  failedCases: stats.failed,
10144
10547
  interruptedCases: stats.interrupted,
10145
10548
  avgScore
10146
10549
  });
10550
+ if (!persistedRun) throw new Error(`Evaluation run ${runId} no longer exists`);
10551
+ if (terminalUnavailable()) return;
10147
10552
  this.eventEmitter.emit(`run:${runId}`, {
10148
10553
  type: "progress",
10149
10554
  runId,
@@ -10155,65 +10560,86 @@ var EvalRunner = class {
10155
10560
  const evalProject = new import_core31.LatticeEvalProject(projectConfig, onCaseComplete);
10156
10561
  const calibration = await evalProject.calibrateJudge();
10157
10562
  if (!calibration.ok && !calibration.bypassed) {
10158
- await store2.updateRunStatus(tenantId, runId, {
10159
- status: "failed",
10160
- error: `Judge calibration failed: ${calibration.reason}`,
10161
- completedAt: /* @__PURE__ */ new Date()
10162
- });
10163
10563
  throw new Error(`Judge calibration failed: ${calibration.reason}`);
10164
10564
  }
10165
10565
  if (calibration.bypassed) {
10166
10566
  console.warn(`[eval-runner] Judge calibration bypassed \u2014 judge may be unreliable: ${calibration.reason}`);
10167
10567
  }
10168
10568
  const { report } = await evalProject.runAllSuitesBatch(concurrency, abortController.signal);
10169
- if (!abortController.signal.aborted) {
10170
- const completedCount = stats.passed + stats.failed;
10171
- await store2.updateRunStatus(tenantId, runId, {
10172
- status: "completed",
10173
- completedAt: /* @__PURE__ */ new Date(),
10174
- passedCases: stats.passed,
10175
- failedCases: stats.failed,
10176
- interruptedCases: stats.interrupted,
10177
- avgScore: completedCount > 0 ? stats.totalScore / completedCount : 0
10178
- });
10179
- this.eventEmitter.emit(`run:${runId}`, {
10180
- type: "completed",
10181
- runId,
10182
- data: { passed: stats.passed, failed: stats.failed, avgScore: completedCount > 0 ? stats.totalScore / completedCount : 0 }
10183
- });
10184
- }
10569
+ const completedCount = stats.passed + stats.failed;
10570
+ await persistTerminal({
10571
+ status: "completed",
10572
+ completedAt: /* @__PURE__ */ new Date(),
10573
+ passedCases: stats.passed,
10574
+ failedCases: stats.failed,
10575
+ interruptedCases: stats.interrupted,
10576
+ avgScore: completedCount > 0 ? stats.totalScore / completedCount : 0
10577
+ }, {
10578
+ type: "completed",
10579
+ runId,
10580
+ data: { passed: stats.passed, failed: stats.failed, avgScore: completedCount > 0 ? stats.totalScore / completedCount : 0 }
10581
+ });
10185
10582
  return { report };
10186
10583
  } catch (err) {
10187
10584
  const errorMsg = err.message;
10188
- if (!abortController.signal.aborted) {
10189
- await store2.updateRunStatus(tenantId, runId, {
10585
+ try {
10586
+ await persistTerminal({
10190
10587
  status: "failed",
10191
10588
  error: errorMsg,
10192
10589
  completedAt: /* @__PURE__ */ new Date()
10193
- });
10194
- this.eventEmitter.emit(`run:${runId}`, {
10590
+ }, {
10195
10591
  type: "error",
10196
10592
  runId,
10197
10593
  data: { message: errorMsg }
10198
10594
  });
10595
+ } catch {
10199
10596
  }
10200
10597
  throw err;
10201
10598
  } finally {
10202
- this.runs.delete(runId);
10599
+ terminal.workerSettled = true;
10600
+ if (terminal.persisted) {
10601
+ this.runs.delete(runId);
10602
+ this.releaseProjectLease(leaseKey, leaseToken);
10603
+ }
10203
10604
  }
10204
10605
  })();
10205
- this.runs.set(runId, { runId, projectId, tenantId, abortController, promise: runPromise, taskId });
10606
+ this.runs.set(runId, { runId, projectId, tenantId, abortController, promise: runPromise, terminal, leaseKey, leaseToken, taskId });
10206
10607
  runPromise.catch(() => {
10207
10608
  });
10208
10609
  return runId;
10209
10610
  }
10210
- async abortRun(runId) {
10611
+ async abortRun(runId, tenantId) {
10211
10612
  const ctx = this.runs.get(runId);
10212
- if (!ctx) return false;
10613
+ if (!ctx || tenantId !== void 0 && ctx.tenantId !== tenantId) return false;
10614
+ if (ctx.terminal.claiming || ctx.terminal.persisted) return false;
10213
10615
  ctx.abortController.abort();
10214
10616
  const store2 = this.getEvalStore();
10215
- await store2.updateRunStatus(ctx.tenantId, runId, { status: "aborted", completedAt: /* @__PURE__ */ new Date() });
10216
- return true;
10617
+ ctx.terminal.claiming = true;
10618
+ try {
10619
+ let lastError;
10620
+ for (let attempt = 0; attempt < TERMINAL_PERSIST_ATTEMPTS; attempt++) {
10621
+ try {
10622
+ const persistedRun = await store2.updateRunStatus(ctx.tenantId, runId, { status: "aborted", completedAt: /* @__PURE__ */ new Date() });
10623
+ if (!persistedRun) throw new Error(`Evaluation run ${runId} no longer exists`);
10624
+ lastError = void 0;
10625
+ break;
10626
+ } catch (error) {
10627
+ lastError = error;
10628
+ }
10629
+ }
10630
+ if (lastError) throw lastError;
10631
+ ctx.terminal.persisted = true;
10632
+ this.eventEmitter.emit(`run:${runId}`, {
10633
+ type: "error",
10634
+ runId,
10635
+ data: { message: "Run aborted" }
10636
+ });
10637
+ this.releaseProjectLease(ctx.leaseKey, ctx.leaseToken);
10638
+ if (ctx.terminal.workerSettled) this.runs.delete(runId);
10639
+ return true;
10640
+ } finally {
10641
+ ctx.terminal.claiming = false;
10642
+ }
10217
10643
  }
10218
10644
  async waitForRun(runId) {
10219
10645
  const ctx = this.runs.get(runId);
@@ -10221,12 +10647,25 @@ var EvalRunner = class {
10221
10647
  const { report } = await ctx.promise;
10222
10648
  return report;
10223
10649
  }
10224
- isRunning(runId) {
10225
- return this.runs.has(runId);
10650
+ /**
10651
+ * Whether the run can still be controlled or observed as a non-terminal run.
10652
+ * Acknowledged aborts return false even while their worker is unwinding.
10653
+ */
10654
+ isRunning(runId, tenantId) {
10655
+ const ctx = this.runs.get(runId);
10656
+ return ctx !== void 0 && !ctx.terminal.persisted && (tenantId === void 0 || ctx.tenantId === tenantId);
10657
+ }
10658
+ /** Whether the run's worker promise has not yet reached its finally block. */
10659
+ hasActiveExecution(runId, tenantId) {
10660
+ const ctx = this.runs.get(runId);
10661
+ return ctx !== void 0 && !ctx.terminal.workerSettled && (tenantId === void 0 || ctx.tenantId === tenantId);
10226
10662
  }
10227
10663
  getEvalStore() {
10228
10664
  return (0, import_core31.getStoreLattice)("default", "eval").store;
10229
10665
  }
10666
+ releaseProjectLease(leaseKey, leaseToken) {
10667
+ if (this.projectLeases.get(leaseKey) === leaseToken) this.projectLeases.delete(leaseKey);
10668
+ }
10230
10669
  };
10231
10670
  var evalRunner = new EvalRunner();
10232
10671
 
@@ -10241,6 +10680,18 @@ function getTenantId12(request) {
10241
10680
  function getEvalStore() {
10242
10681
  return (0, import_core32.getStoreLattice)("default", "eval").store;
10243
10682
  }
10683
+ async function getOwnedSuite(store2, tenantId, projectId, suiteId) {
10684
+ const project = await store2.getProjectById(tenantId, projectId);
10685
+ if (!project) return null;
10686
+ const suite = await store2.getSuiteById(tenantId, suiteId);
10687
+ return suite?.projectId === projectId ? suite : null;
10688
+ }
10689
+ async function getOwnedCase(store2, tenantId, projectId, suiteId, caseId) {
10690
+ const suite = await getOwnedSuite(store2, tenantId, projectId, suiteId);
10691
+ if (!suite) return null;
10692
+ const evalCase = await store2.getCaseById(tenantId, caseId);
10693
+ return evalCase?.suiteId === suiteId ? evalCase : null;
10694
+ }
10244
10695
  async function createProject(request, reply) {
10245
10696
  try {
10246
10697
  const tenantId = getTenantId12(request);
@@ -10396,6 +10847,7 @@ async function updateProject(request, reply) {
10396
10847
  }
10397
10848
  }
10398
10849
  async function deleteProject(request, reply) {
10850
+ let releaseDeletion;
10399
10851
  try {
10400
10852
  const tenantId = getTenantId12(request);
10401
10853
  const store2 = getEvalStore();
@@ -10404,6 +10856,23 @@ async function deleteProject(request, reply) {
10404
10856
  if (!existing) {
10405
10857
  return reply.status(404).send({ success: false, message: "Project not found" });
10406
10858
  }
10859
+ releaseDeletion = evalRunner.reserveProjectDeletion(tenantId, pid) ?? void 0;
10860
+ if (!releaseDeletion) {
10861
+ return reply.status(409).send({
10862
+ success: false,
10863
+ message: "Cannot delete an evaluation project with an active run"
10864
+ });
10865
+ }
10866
+ const runs = await store2.getRunsByTenant(tenantId, { projectId: pid });
10867
+ const hasActiveRun = runs.some(
10868
+ (run) => run.status === "running" || evalRunner.hasActiveExecution(run.id, tenantId)
10869
+ );
10870
+ if (hasActiveRun) {
10871
+ return reply.status(409).send({
10872
+ success: false,
10873
+ message: "Cannot delete an evaluation project with an active run"
10874
+ });
10875
+ }
10407
10876
  const deleted = await store2.deleteProject(tenantId, pid);
10408
10877
  if (!deleted) {
10409
10878
  return reply.status(500).send({ success: false, message: "Failed to delete project" });
@@ -10412,6 +10881,8 @@ async function deleteProject(request, reply) {
10412
10881
  } catch (err) {
10413
10882
  const message = err instanceof Error ? err.message : "Failed to delete project";
10414
10883
  return reply.status(500).send({ success: false, message });
10884
+ } finally {
10885
+ releaseDeletion?.();
10415
10886
  }
10416
10887
  }
10417
10888
  async function createSuite(request, reply) {
@@ -10440,8 +10911,8 @@ async function updateSuite(request, reply) {
10440
10911
  try {
10441
10912
  const tenantId = getTenantId12(request);
10442
10913
  const store2 = getEvalStore();
10443
- const { sid } = request.params;
10444
- const existing = await store2.getSuiteById(tenantId, sid);
10914
+ const { pid, sid } = request.params;
10915
+ const existing = await getOwnedSuite(store2, tenantId, pid, sid);
10445
10916
  if (!existing) {
10446
10917
  return reply.status(404).send({ success: false, message: "Suite not found" });
10447
10918
  }
@@ -10460,8 +10931,8 @@ async function deleteSuite(request, reply) {
10460
10931
  try {
10461
10932
  const tenantId = getTenantId12(request);
10462
10933
  const store2 = getEvalStore();
10463
- const { sid } = request.params;
10464
- const existing = await store2.getSuiteById(tenantId, sid);
10934
+ const { pid, sid } = request.params;
10935
+ const existing = await getOwnedSuite(store2, tenantId, pid, sid);
10465
10936
  if (!existing) {
10466
10937
  return reply.status(404).send({ success: false, message: "Suite not found" });
10467
10938
  }
@@ -10479,8 +10950,8 @@ async function createCase(request, reply) {
10479
10950
  try {
10480
10951
  const tenantId = getTenantId12(request);
10481
10952
  const store2 = getEvalStore();
10482
- const { sid } = request.params;
10483
- const suite = await store2.getSuiteById(tenantId, sid);
10953
+ const { pid, sid } = request.params;
10954
+ const suite = await getOwnedSuite(store2, tenantId, pid, sid);
10484
10955
  if (!suite) {
10485
10956
  return reply.status(404).send({ success: false, message: "Suite not found" });
10486
10957
  }
@@ -10509,7 +10980,11 @@ async function listCasesForSuite(request, reply) {
10509
10980
  try {
10510
10981
  const tenantId = getTenantId12(request);
10511
10982
  const store2 = getEvalStore();
10512
- const { sid } = request.params;
10983
+ const { pid, sid } = request.params;
10984
+ const suite = await getOwnedSuite(store2, tenantId, pid, sid);
10985
+ if (!suite) {
10986
+ return reply.status(404).send({ success: false, message: "Suite not found" });
10987
+ }
10513
10988
  const cases = await store2.getCasesBySuite(tenantId, sid);
10514
10989
  return {
10515
10990
  success: true,
@@ -10525,8 +11000,8 @@ async function updateCase(request, reply) {
10525
11000
  try {
10526
11001
  const tenantId = getTenantId12(request);
10527
11002
  const store2 = getEvalStore();
10528
- const { cid } = request.params;
10529
- const existing = await store2.getCaseById(tenantId, cid);
11003
+ const { pid, sid, cid } = request.params;
11004
+ const existing = await getOwnedCase(store2, tenantId, pid, sid, cid);
10530
11005
  if (!existing) {
10531
11006
  return reply.status(404).send({ success: false, message: "Case not found" });
10532
11007
  }
@@ -10545,8 +11020,8 @@ async function deleteCase(request, reply) {
10545
11020
  try {
10546
11021
  const tenantId = getTenantId12(request);
10547
11022
  const store2 = getEvalStore();
10548
- const { cid } = request.params;
10549
- const existing = await store2.getCaseById(tenantId, cid);
11023
+ const { pid, sid, cid } = request.params;
11024
+ const existing = await getOwnedCase(store2, tenantId, pid, sid, cid);
10550
11025
  if (!existing) {
10551
11026
  return reply.status(404).send({ success: false, message: "Case not found" });
10552
11027
  }
@@ -10615,6 +11090,11 @@ function registerEvalRoutes(app2) {
10615
11090
  }
10616
11091
  });
10617
11092
  app2.get("/api/eval/runs/:rid/stream", async (request, reply) => {
11093
+ const tenantId = getTenantId12(request);
11094
+ const { rid } = request.params;
11095
+ const store2 = getEvalStore();
11096
+ const ownedRun = await store2.getRunById(tenantId, rid);
11097
+ if (!ownedRun) return reply.status(404).send({ success: false, message: "Run not found" });
10618
11098
  reply.hijack();
10619
11099
  reply.raw.writeHead(200, {
10620
11100
  "Content-Type": "text/event-stream",
@@ -10622,41 +11102,75 @@ function registerEvalRoutes(app2) {
10622
11102
  Connection: "keep-alive",
10623
11103
  "Access-Control-Allow-Origin": "*"
10624
11104
  });
10625
- const { rid } = request.params;
10626
11105
  const emitter = evalRunner.getEventEmitter();
10627
11106
  const eventKey = `run:${rid}`;
10628
- if (!evalRunner.isRunning(rid)) {
10629
- const tenantId = getTenantId12(request);
10630
- const run = await getEvalStore().getRunById(tenantId, rid);
10631
- if (run) {
10632
- const eventType = run.status === "completed" ? "completed" : "error";
10633
- reply.raw.write(`event: ${eventType}
10634
- data: ${JSON.stringify({ passed: run.passedCases, failed: run.failedCases, avgScore: run.avgScore })}
11107
+ let settled = false;
11108
+ const cleanup = () => {
11109
+ emitter.off(eventKey, handler);
11110
+ request.raw.off("close", disconnect);
11111
+ };
11112
+ const disconnect = () => {
11113
+ if (settled) return;
11114
+ settled = true;
11115
+ cleanup();
11116
+ };
11117
+ const finish = (type, data) => {
11118
+ if (settled) return;
11119
+ settled = true;
11120
+ cleanup();
11121
+ reply.raw.write(`event: ${type}
11122
+ data: ${JSON.stringify(data)}
10635
11123
 
10636
11124
  `);
10637
- }
10638
11125
  reply.raw.end();
10639
- return;
10640
- }
11126
+ };
10641
11127
  const handler = (event) => {
10642
- reply.raw.write(`event: ${event.type}
11128
+ if (event.type === "completed" || event.type === "error") {
11129
+ finish(event.type, event.data);
11130
+ return;
11131
+ }
11132
+ if (!settled) reply.raw.write(`event: ${event.type}
10643
11133
  data: ${JSON.stringify(event.data)}
10644
11134
 
10645
11135
  `);
10646
- if (event.type === "completed" || event.type === "error") {
10647
- emitter.off(eventKey, handler);
10648
- reply.raw.end();
10649
- }
10650
11136
  };
10651
11137
  emitter.on(eventKey, handler);
10652
- request.raw.on("close", () => {
10653
- emitter.off(eventKey, handler);
10654
- });
11138
+ request.raw.on("close", disconnect);
11139
+ let reconciledRun;
11140
+ try {
11141
+ reconciledRun = await store2.getRunById(tenantId, rid);
11142
+ } catch (error) {
11143
+ finish("error", { message: error.message });
11144
+ return;
11145
+ }
11146
+ if (settled) return;
11147
+ if (!reconciledRun) {
11148
+ cleanup();
11149
+ reply.raw.end();
11150
+ return;
11151
+ }
11152
+ if (reconciledRun.status === "completed") {
11153
+ finish("completed", {
11154
+ passed: reconciledRun.passedCases,
11155
+ failed: reconciledRun.failedCases,
11156
+ avgScore: reconciledRun.avgScore
11157
+ });
11158
+ } else if (reconciledRun.status === "failed") {
11159
+ finish("error", { message: reconciledRun.error ?? "Evaluation failed" });
11160
+ } else if (reconciledRun.status === "aborted") {
11161
+ finish("error", { message: "Run aborted" });
11162
+ } else if (!evalRunner.isRunning(rid, tenantId)) {
11163
+ cleanup();
11164
+ reply.raw.end();
11165
+ }
10655
11166
  });
10656
11167
  app2.post("/api/eval/runs/:rid/abort", async (request, reply) => {
10657
11168
  try {
11169
+ const tenantId = getTenantId12(request);
10658
11170
  const { rid } = request.params;
10659
- const aborted = await evalRunner.abortRun(rid);
11171
+ const run = await getEvalStore().getRunById(tenantId, rid);
11172
+ if (!run) return reply.status(404).send({ success: false, message: "Run not found" });
11173
+ const aborted = await evalRunner.abortRun(rid, tenantId);
10660
11174
  if (!aborted) return reply.status(404).send({ success: false, message: "Run not found or not running" });
10661
11175
  reply.send({ success: true, message: "Run aborted" });
10662
11176
  } catch (err) {
@@ -10667,7 +11181,13 @@ data: ${JSON.stringify(event.data)}
10667
11181
  try {
10668
11182
  const tenantId = getTenantId12(request);
10669
11183
  const { rid } = request.params;
10670
- const deleted = await getEvalStore().deleteRun(tenantId, rid);
11184
+ const store2 = getEvalStore();
11185
+ const run = await store2.getRunById(tenantId, rid);
11186
+ if (!run) return reply.status(404).send({ success: false, message: "Run not found" });
11187
+ if (run.status === "running" || evalRunner.hasActiveExecution(rid, tenantId)) {
11188
+ return reply.status(409).send({ success: false, message: "Cannot delete an active evaluation run" });
11189
+ }
11190
+ const deleted = await store2.deleteRun(tenantId, rid);
10671
11191
  if (!deleted) return reply.status(404).send({ success: false, message: "Run not found" });
10672
11192
  reply.send({ success: true, message: "Run deleted" });
10673
11193
  } catch (err) {
@@ -11263,7 +11783,7 @@ function registerConsoleAuthHook(app2, options) {
11263
11783
  if (PUBLIC_ROUTES.some((route) => request.url === route)) return;
11264
11784
  if (request.url.startsWith("/s/")) return;
11265
11785
  const path6 = request.url.split("?")[0];
11266
- if (path6.includes("/viewfile") || path6.includes("/downloadfile") || path6.includes("/uploadfile") || path6.startsWith("/f/") || path6 === "/f") {
11786
+ if (request.method === "GET" && /^\/api\/workspaces\/[^/]+\/projects\/[^/]+\/viewfile$/.test(path6) || request.method === "GET" && /^\/api\/workspaces\/[^/]+\/projects\/[^/]+\/downloadfile$/.test(path6) || request.method === "POST" && /^\/api\/workspaces\/[^/]+\/projects\/[^/]+\/uploadfile$/.test(path6) || path6.startsWith("/f/") || path6 === "/f") {
11267
11787
  return;
11268
11788
  }
11269
11789
  await reply.status(401).send({
@@ -12266,12 +12786,12 @@ async function handleBridgeProxy(request, reply) {
12266
12786
  };
12267
12787
  const timeout = 3e5;
12268
12788
  try {
12269
- const response = await new Promise((resolve3, reject) => {
12789
+ const response = await new Promise((resolve4, reject) => {
12270
12790
  const timer = setTimeout(() => {
12271
12791
  pending.delete(requestId);
12272
12792
  reject(new Error(`Bridge request timeout after ${timeout}ms`));
12273
12793
  }, timeout);
12274
- pending.set(requestId, { resolve: resolve3, reject, timer, agentId });
12794
+ pending.set(requestId, { resolve: resolve4, reject, timer, agentId });
12275
12795
  try {
12276
12796
  if (conn.ws.readyState !== conn.ws.OPEN) {
12277
12797
  pending.delete(requestId);
@@ -16049,7 +16569,7 @@ ${summary}`
16049
16569
  console.log(
16050
16570
  `\u5C06\u5728 ${delayMs}ms \u540E\u91CD\u8BD5\u4EFB\u52A1 (${nextRetryCount}/${maxRetries})`
16051
16571
  );
16052
- await new Promise((resolve3) => setTimeout(resolve3, delayMs));
16572
+ await new Promise((resolve4) => setTimeout(resolve4, delayMs));
16053
16573
  return handleAgentTask(taskRequest, nextRetryCount);
16054
16574
  }
16055
16575
  if (callback_event) {