@prismshadow/penguin-server 0.1.4 → 0.1.5
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/api/types.d.ts +73 -20
- package/dist/index.js +728 -241
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/web-dist/assets/index-Da8gVBDc.css +1 -0
- package/web-dist/assets/{index-7gbWDtby.js → index-DibOt1-H.js} +1 -1
- package/web-dist/assets/index-nay7sGjx.js +171 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-Bnva9EYm.js +0 -102
- package/web-dist/assets/index-DMcMNwnl.css +0 -1
package/dist/index.js
CHANGED
|
@@ -3,10 +3,11 @@ import { config as loadDotenv } from "dotenv";
|
|
|
3
3
|
import { serve } from "@hono/node-server";
|
|
4
4
|
|
|
5
5
|
// src/app.ts
|
|
6
|
-
import
|
|
6
|
+
import fs17 from "fs";
|
|
7
7
|
import fsp from "fs/promises";
|
|
8
|
-
import
|
|
8
|
+
import path16 from "path";
|
|
9
9
|
import { Hono as Hono21 } from "hono";
|
|
10
|
+
import { bodyLimit } from "hono/body-limit";
|
|
10
11
|
|
|
11
12
|
// src/db/database.ts
|
|
12
13
|
import { mkdirSync } from "fs";
|
|
@@ -299,13 +300,13 @@ var ErrorsRepo = class {
|
|
|
299
300
|
};
|
|
300
301
|
}
|
|
301
302
|
/** The most recent `limit` entries (reverse chronological order). */
|
|
302
|
-
recent(projectId, f = {}, limit = 20) {
|
|
303
|
+
recent(projectId, f = {}, limit = 20, offset = 0) {
|
|
303
304
|
const { where, params } = this.conds(projectId, f);
|
|
304
305
|
const rows = this.db.prepare(
|
|
305
306
|
`SELECT ts, source, code, kind, message
|
|
306
307
|
FROM error_records WHERE ${where}
|
|
307
|
-
ORDER BY id DESC LIMIT :limit`
|
|
308
|
-
).all({ ...params, limit });
|
|
308
|
+
ORDER BY id DESC LIMIT :limit OFFSET :offset`
|
|
309
|
+
).all({ ...params, limit, offset });
|
|
309
310
|
return rows.map((r) => ({
|
|
310
311
|
ts: r.ts,
|
|
311
312
|
source: r.source,
|
|
@@ -1563,6 +1564,14 @@ function projectsRoutes(deps2) {
|
|
|
1563
1564
|
const project = await deps2.projectService.createProject(c.var.user, projectId, name);
|
|
1564
1565
|
return c.json({ project }, 201);
|
|
1565
1566
|
});
|
|
1567
|
+
app2.patch("/:projectId", async (c) => {
|
|
1568
|
+
const projectId = requireValidId(c, "projectId");
|
|
1569
|
+
const body = await readJson(c);
|
|
1570
|
+
const name = requireString(body, "name", { maxLen: 100, label: "name" }).trim();
|
|
1571
|
+
if (name === "") throw badRequest("name must be at least 1 characters.");
|
|
1572
|
+
const project = await deps2.projectService.renameProject(c.var.user.userId, projectId, name);
|
|
1573
|
+
return c.json({ project });
|
|
1574
|
+
});
|
|
1566
1575
|
app2.delete("/:projectId", async (c) => {
|
|
1567
1576
|
await deps2.projectService.deleteProject(c.var.user.userId, requireValidId(c, "projectId"));
|
|
1568
1577
|
return c.body(null, 204);
|
|
@@ -2190,6 +2199,7 @@ async function readItem(deps2, projectId, agentId, name) {
|
|
|
2190
2199
|
|
|
2191
2200
|
// src/http/routes/benchmarks.ts
|
|
2192
2201
|
import { Hono as Hono10 } from "hono";
|
|
2202
|
+
var TEXT_PREVIEW_BYTES = 256 * 1024;
|
|
2193
2203
|
function benchmarksRoutes(deps2) {
|
|
2194
2204
|
const app2 = new Hono10();
|
|
2195
2205
|
app2.get("/", async (c) => {
|
|
@@ -2199,6 +2209,58 @@ function benchmarksRoutes(deps2) {
|
|
|
2199
2209
|
await deps2.agentConfigService.requireExists(projectId, agentId);
|
|
2200
2210
|
return c.json(await deps2.benchmarks.list(projectId, agentId));
|
|
2201
2211
|
});
|
|
2212
|
+
app2.get("/:benchmarkId/cases", async (c) => {
|
|
2213
|
+
const projectId = requireValidId(c, "projectId");
|
|
2214
|
+
const agentId = requireValidId(c, "agentId");
|
|
2215
|
+
const benchmarkId = requireValidId(c, "benchmarkId");
|
|
2216
|
+
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
2217
|
+
await deps2.agentConfigService.requireExists(projectId, agentId);
|
|
2218
|
+
return c.json(await deps2.benchmarks.listCases(projectId, agentId, benchmarkId));
|
|
2219
|
+
});
|
|
2220
|
+
app2.get("/:benchmarkId/cases/:caseId/files", async (c) => {
|
|
2221
|
+
const projectId = requireValidId(c, "projectId");
|
|
2222
|
+
const agentId = requireValidId(c, "agentId");
|
|
2223
|
+
const benchmarkId = requireValidId(c, "benchmarkId");
|
|
2224
|
+
const caseId = requireValidId(c, "caseId");
|
|
2225
|
+
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
2226
|
+
await deps2.agentConfigService.requireExists(projectId, agentId);
|
|
2227
|
+
return c.json(
|
|
2228
|
+
await deps2.benchmarks.listCaseFiles(
|
|
2229
|
+
projectId,
|
|
2230
|
+
agentId,
|
|
2231
|
+
benchmarkId,
|
|
2232
|
+
caseId,
|
|
2233
|
+
c.req.query("path") ?? ""
|
|
2234
|
+
)
|
|
2235
|
+
);
|
|
2236
|
+
});
|
|
2237
|
+
app2.get("/:benchmarkId/cases/:caseId/files/content", async (c) => {
|
|
2238
|
+
const projectId = requireValidId(c, "projectId");
|
|
2239
|
+
const agentId = requireValidId(c, "agentId");
|
|
2240
|
+
const benchmarkId = requireValidId(c, "benchmarkId");
|
|
2241
|
+
const caseId = requireValidId(c, "caseId");
|
|
2242
|
+
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
2243
|
+
await deps2.agentConfigService.requireExists(projectId, agentId);
|
|
2244
|
+
const download = c.req.query("download") === "1";
|
|
2245
|
+
const boundedPreview = !download && c.req.query("preview") === "1";
|
|
2246
|
+
const { data, fileName, contentType, scriptable, truncated } = await deps2.benchmarks.readCaseFile(
|
|
2247
|
+
projectId,
|
|
2248
|
+
agentId,
|
|
2249
|
+
benchmarkId,
|
|
2250
|
+
caseId,
|
|
2251
|
+
c.req.query("path") ?? "",
|
|
2252
|
+
boundedPreview ? { maxBytes: TEXT_PREVIEW_BYTES } : void 0
|
|
2253
|
+
);
|
|
2254
|
+
return new Response(new Uint8Array(data), {
|
|
2255
|
+
status: 200,
|
|
2256
|
+
headers: {
|
|
2257
|
+
"Content-Type": !download && scriptable ? "text/plain; charset=utf-8" : contentType,
|
|
2258
|
+
"Content-Disposition": `${download ? "attachment" : "inline"}; filename*=UTF-8''${encodeURIComponent(fileName)}`,
|
|
2259
|
+
"X-Content-Type-Options": "nosniff",
|
|
2260
|
+
...truncated ? { "X-Content-Truncated": "1" } : {}
|
|
2261
|
+
}
|
|
2262
|
+
});
|
|
2263
|
+
});
|
|
2202
2264
|
return app2;
|
|
2203
2265
|
}
|
|
2204
2266
|
|
|
@@ -2611,12 +2673,32 @@ function usageRoutes(deps2) {
|
|
|
2611
2673
|
})
|
|
2612
2674
|
);
|
|
2613
2675
|
});
|
|
2676
|
+
app2.get("/errors", (c) => {
|
|
2677
|
+
const projectId = requireValidId(c, "projectId");
|
|
2678
|
+
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
2679
|
+
const { offset, limit } = paginationQuery(c);
|
|
2680
|
+
const from = optionalDateParam(c.req.query("from"), "from");
|
|
2681
|
+
const to = optionalDateParam(c.req.query("to"), "to");
|
|
2682
|
+
const agentId = c.req.query("agentId");
|
|
2683
|
+
return c.json(
|
|
2684
|
+
deps2.usageService.queryErrors(projectId, {
|
|
2685
|
+
offset,
|
|
2686
|
+
limit,
|
|
2687
|
+
// Same admin-only rule as the dashboard: a regular member seeing another tenant's
|
|
2688
|
+
// unattributed errors would be a cross-tenant leak.
|
|
2689
|
+
includeGlobalErrors: c.var.user.isAdmin,
|
|
2690
|
+
...from !== void 0 ? { from } : {},
|
|
2691
|
+
...to !== void 0 ? { to } : {},
|
|
2692
|
+
...agentId !== void 0 && agentId !== "" ? { agentId } : {}
|
|
2693
|
+
})
|
|
2694
|
+
);
|
|
2695
|
+
});
|
|
2614
2696
|
return app2;
|
|
2615
2697
|
}
|
|
2616
2698
|
|
|
2617
2699
|
// src/http/routes/sessions.ts
|
|
2618
|
-
import
|
|
2619
|
-
import
|
|
2700
|
+
import fs8 from "fs/promises";
|
|
2701
|
+
import path7 from "path";
|
|
2620
2702
|
import { Hono as Hono18 } from "hono";
|
|
2621
2703
|
import { imageUrlMessage, scratchpadDir, userText } from "@prismshadow/penguin-core";
|
|
2622
2704
|
|
|
@@ -2796,9 +2878,14 @@ var WorkspaceFilesService = class {
|
|
|
2796
2878
|
);
|
|
2797
2879
|
return unique.filter((_, i) => exists[i]);
|
|
2798
2880
|
}
|
|
2799
|
-
/**
|
|
2881
|
+
/**
|
|
2882
|
+
* List a directory: dirs come first, each group sorted by name. Entries whose
|
|
2883
|
+
* canonical target leaves the Workspace are omitted, so listing cannot expose
|
|
2884
|
+
* metadata for an out-of-bounds symlink.
|
|
2885
|
+
*/
|
|
2800
2886
|
async list(workspace, rel) {
|
|
2801
2887
|
const dir = await this.resolveRead(workspace, rel);
|
|
2888
|
+
const realBase = await this.realBase(workspace);
|
|
2802
2889
|
let dirents;
|
|
2803
2890
|
try {
|
|
2804
2891
|
dirents = await fs6.readdir(dir, { withFileTypes: true });
|
|
@@ -2811,33 +2898,31 @@ var WorkspaceFilesService = class {
|
|
|
2811
2898
|
}
|
|
2812
2899
|
throw err;
|
|
2813
2900
|
}
|
|
2814
|
-
const
|
|
2901
|
+
const listed = await Promise.all(
|
|
2815
2902
|
dirents.map(async (d) => {
|
|
2816
|
-
let sizeBytes = 0;
|
|
2817
|
-
let mtime = "";
|
|
2818
|
-
let isDir = d.isDirectory();
|
|
2819
2903
|
try {
|
|
2820
|
-
const
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2904
|
+
const canonical = await fs6.realpath(path5.join(dir, d.name));
|
|
2905
|
+
this.assertInside(canonical, realBase);
|
|
2906
|
+
const stat = await fs6.stat(canonical);
|
|
2907
|
+
return {
|
|
2908
|
+
name: d.name,
|
|
2909
|
+
kind: stat.isDirectory() ? "dir" : "file",
|
|
2910
|
+
sizeBytes: stat.size,
|
|
2911
|
+
mtime: stat.mtime.toISOString()
|
|
2912
|
+
};
|
|
2824
2913
|
} catch {
|
|
2914
|
+
return null;
|
|
2825
2915
|
}
|
|
2826
|
-
return {
|
|
2827
|
-
name: d.name,
|
|
2828
|
-
kind: isDir ? "dir" : "file",
|
|
2829
|
-
sizeBytes,
|
|
2830
|
-
mtime
|
|
2831
|
-
};
|
|
2832
2916
|
})
|
|
2833
2917
|
);
|
|
2918
|
+
const entries = listed.filter((entry) => entry !== null);
|
|
2834
2919
|
entries.sort(
|
|
2835
2920
|
(a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "dir" ? -1 : 1
|
|
2836
2921
|
);
|
|
2837
2922
|
return { path: rel, entries };
|
|
2838
2923
|
}
|
|
2839
2924
|
/** Read a file (preview/download): IO on the canonical path (resolveRead has already eliminated symlink escapes). */
|
|
2840
|
-
async read(workspace, rel) {
|
|
2925
|
+
async read(workspace, rel, options) {
|
|
2841
2926
|
const file = await this.resolveRead(workspace, rel);
|
|
2842
2927
|
let stat;
|
|
2843
2928
|
try {
|
|
@@ -2846,16 +2931,35 @@ var WorkspaceFilesService = class {
|
|
|
2846
2931
|
throw new HttpError(404, "path_not_found", "File does not exist.");
|
|
2847
2932
|
}
|
|
2848
2933
|
if (stat.isDirectory()) throw badRequest("path is a directory.");
|
|
2849
|
-
|
|
2934
|
+
const maxBytes = options?.maxBytes;
|
|
2935
|
+
if (maxBytes !== void 0 && (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > MAX_READ_BYTES)) {
|
|
2936
|
+
throw badRequest("maxBytes must be a positive integer within the read limit.");
|
|
2937
|
+
}
|
|
2938
|
+
if (maxBytes === void 0 && stat.size > MAX_READ_BYTES) {
|
|
2850
2939
|
throw new HttpError(413, "file_too_large", "File exceeds the 50MB read limit.");
|
|
2851
2940
|
}
|
|
2852
|
-
|
|
2941
|
+
let data;
|
|
2942
|
+
let truncated = false;
|
|
2943
|
+
if (maxBytes !== void 0 && stat.size > maxBytes) {
|
|
2944
|
+
const handle = await fs6.open(file, "r");
|
|
2945
|
+
try {
|
|
2946
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
2947
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
2948
|
+
data = buffer.subarray(0, bytesRead);
|
|
2949
|
+
truncated = true;
|
|
2950
|
+
} finally {
|
|
2951
|
+
await handle.close();
|
|
2952
|
+
}
|
|
2953
|
+
} else {
|
|
2954
|
+
data = await fs6.readFile(file);
|
|
2955
|
+
}
|
|
2853
2956
|
const ext = path5.extname(file).toLowerCase();
|
|
2854
2957
|
return {
|
|
2855
2958
|
data,
|
|
2856
2959
|
fileName: path5.basename(file),
|
|
2857
2960
|
contentType: CONTENT_TYPES[ext] ?? "application/octet-stream",
|
|
2858
|
-
scriptable: ext === ".html" || ext === ".htm" || ext === ".svg"
|
|
2961
|
+
scriptable: ext === ".html" || ext === ".htm" || ext === ".svg",
|
|
2962
|
+
...truncated ? { truncated: true } : {}
|
|
2859
2963
|
};
|
|
2860
2964
|
}
|
|
2861
2965
|
/**
|
|
@@ -2897,6 +3001,133 @@ var WorkspaceFilesService = class {
|
|
|
2897
3001
|
}
|
|
2898
3002
|
};
|
|
2899
3003
|
|
|
3004
|
+
// src/services/task-attachments.ts
|
|
3005
|
+
import fs7 from "fs/promises";
|
|
3006
|
+
import path6 from "path";
|
|
3007
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
3008
|
+
import { appendAttachmentLines, attachedFileLine } from "@prismshadow/penguin-core";
|
|
3009
|
+
var MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
3010
|
+
var MAX_ATTACHMENT_COUNT = 20;
|
|
3011
|
+
var MAX_TOTAL_ATTACHMENT_BYTES = 12 * 1024 * 1024;
|
|
3012
|
+
var MAX_STEM_BYTES = 80;
|
|
3013
|
+
var WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
3014
|
+
var INVISIBLE_CHAR = new RegExp("\\p{C}", "u");
|
|
3015
|
+
function unsafeNameChar(ch) {
|
|
3016
|
+
if (/[A-Za-z0-9._-]/.test(ch)) return false;
|
|
3017
|
+
return ch.codePointAt(0) < 128 || INVISIBLE_CHAR.test(ch);
|
|
3018
|
+
}
|
|
3019
|
+
function sanitizeSegment(value) {
|
|
3020
|
+
return Array.from(value, (ch) => unsafeNameChar(ch) ? "-" : ch).join("");
|
|
3021
|
+
}
|
|
3022
|
+
function truncateBytes(value, maxBytes) {
|
|
3023
|
+
if (Buffer.byteLength(value) <= maxBytes) return value;
|
|
3024
|
+
let out = "";
|
|
3025
|
+
for (const ch of value) {
|
|
3026
|
+
if (Buffer.byteLength(out) + Buffer.byteLength(ch) > maxBytes) break;
|
|
3027
|
+
out += ch;
|
|
3028
|
+
}
|
|
3029
|
+
return out;
|
|
3030
|
+
}
|
|
3031
|
+
var MAX_NAME_ATTEMPTS = 16;
|
|
3032
|
+
function parseAttachmentPart(part, index) {
|
|
3033
|
+
const fileName = part.fileName;
|
|
3034
|
+
if (typeof fileName !== "string" || fileName.length === 0 || fileName.includes("/") || fileName.includes("\\") || fileName.includes("..") || fileName.includes("\0")) {
|
|
3035
|
+
throw badRequest(
|
|
3036
|
+
`input[${index}].fileName must be a non-empty file name without path separators or "..".`
|
|
3037
|
+
);
|
|
3038
|
+
}
|
|
3039
|
+
const dataUrl = part.dataUrl;
|
|
3040
|
+
const match = typeof dataUrl === "string" ? /^data:[^,]*;base64,([A-Za-z0-9+/=\s]+)$/.exec(dataUrl) : null;
|
|
3041
|
+
if (!match) {
|
|
3042
|
+
throw badRequest(`input[${index}].dataUrl must be a base64 data: URL of the file's bytes.`);
|
|
3043
|
+
}
|
|
3044
|
+
const bytes = Buffer.from(match[1], "base64");
|
|
3045
|
+
if (bytes.length === 0) {
|
|
3046
|
+
throw badRequest(`input[${index}].dataUrl decodes to an empty file.`);
|
|
3047
|
+
}
|
|
3048
|
+
if (bytes.length > MAX_ATTACHMENT_BYTES) {
|
|
3049
|
+
throw new HttpError(
|
|
3050
|
+
413,
|
|
3051
|
+
"file_too_large",
|
|
3052
|
+
`Attached file exceeds the ${MAX_ATTACHMENT_BYTES / (1024 * 1024)}MB limit.`
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
3055
|
+
return { fileName, bytes };
|
|
3056
|
+
}
|
|
3057
|
+
function assertAttachmentBudget(attachments) {
|
|
3058
|
+
if (attachments.length > MAX_ATTACHMENT_COUNT) {
|
|
3059
|
+
throw new HttpError(
|
|
3060
|
+
413,
|
|
3061
|
+
"too_many_files",
|
|
3062
|
+
`A message may carry at most ${MAX_ATTACHMENT_COUNT} attached files.`
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
let total = 0;
|
|
3066
|
+
for (const a of attachments) total += a.bytes.length;
|
|
3067
|
+
if (total > MAX_TOTAL_ATTACHMENT_BYTES) {
|
|
3068
|
+
throw new HttpError(
|
|
3069
|
+
413,
|
|
3070
|
+
"payload_too_large",
|
|
3071
|
+
`Attached files exceed the ${MAX_TOTAL_ATTACHMENT_BYTES / (1024 * 1024)}MB total limit for one message.`
|
|
3072
|
+
);
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
function scratchpadName(fileName) {
|
|
3076
|
+
const ext = sanitizeSegment(path6.extname(fileName));
|
|
3077
|
+
const rawStem = fileName.slice(0, fileName.length - path6.extname(fileName).length);
|
|
3078
|
+
const stem = truncateBytes(sanitizeSegment(rawStem), MAX_STEM_BYTES).replace(/[. ]+$/, "");
|
|
3079
|
+
if (!stem || /^-+$/.test(stem)) return `file${ext}`;
|
|
3080
|
+
return WINDOWS_RESERVED.test(stem) ? `_${stem}${ext}` : `${stem}${ext}`;
|
|
3081
|
+
}
|
|
3082
|
+
async function writeAttachment(dir, attachment) {
|
|
3083
|
+
const base = scratchpadName(attachment.fileName);
|
|
3084
|
+
const ext = path6.extname(base);
|
|
3085
|
+
const stem = base.slice(0, base.length - ext.length);
|
|
3086
|
+
for (let attempt = 0; attempt < MAX_NAME_ATTEMPTS; attempt++) {
|
|
3087
|
+
const name = attempt === 0 ? base : `${stem}-${randomBytes4(3).toString("hex")}${ext}`;
|
|
3088
|
+
const file = path6.join(dir, name);
|
|
3089
|
+
try {
|
|
3090
|
+
await fs7.writeFile(file, attachment.bytes, { flag: "wx" });
|
|
3091
|
+
return file;
|
|
3092
|
+
} catch (err) {
|
|
3093
|
+
if (err.code !== "EEXIST") throw err;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
throw new Error(
|
|
3097
|
+
`failed to allocate a unique attachment file name under ${dir} after ${MAX_NAME_ATTEMPTS} attempts`
|
|
3098
|
+
);
|
|
3099
|
+
}
|
|
3100
|
+
async function openScratchpadDir(root, sessionId) {
|
|
3101
|
+
const dir = path6.join(root, sessionId);
|
|
3102
|
+
await fs7.mkdir(dir, { recursive: true });
|
|
3103
|
+
const canonicalRoot = await fs7.realpath(root);
|
|
3104
|
+
const rel = path6.relative(canonicalRoot, await fs7.realpath(dir));
|
|
3105
|
+
if (rel !== sessionId) {
|
|
3106
|
+
throw new Error(
|
|
3107
|
+
`session scratchpad ${dir} resolves outside the agent scratchpad root; refusing to write attachments`
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
return dir;
|
|
3111
|
+
}
|
|
3112
|
+
async function removeAttachments(files) {
|
|
3113
|
+
await Promise.all(files.map((f) => fs7.rm(f, { force: true }).catch(() => {
|
|
3114
|
+
})));
|
|
3115
|
+
}
|
|
3116
|
+
async function attachFilesToInput(messages, attachments, root, sessionId) {
|
|
3117
|
+
if (attachments.length === 0) return { input: messages, written: [] };
|
|
3118
|
+
const dir = await openScratchpadDir(root, sessionId);
|
|
3119
|
+
const written = [];
|
|
3120
|
+
try {
|
|
3121
|
+
for (const attachment of attachments) {
|
|
3122
|
+
written.push(await writeAttachment(dir, attachment));
|
|
3123
|
+
}
|
|
3124
|
+
} catch (err) {
|
|
3125
|
+
await removeAttachments(written);
|
|
3126
|
+
throw err;
|
|
3127
|
+
}
|
|
3128
|
+
return { input: appendAttachmentLines(messages, written.map(attachedFileLine)), written };
|
|
3129
|
+
}
|
|
3130
|
+
|
|
2900
3131
|
// src/http/routes/sessions.ts
|
|
2901
3132
|
var SESSION_TITLE_MAX = 120;
|
|
2902
3133
|
var STAT_MAX_PATHS = 100;
|
|
@@ -2914,12 +3145,33 @@ var SESSION_CATEGORIES = [
|
|
|
2914
3145
|
"schedule",
|
|
2915
3146
|
"archived"
|
|
2916
3147
|
];
|
|
3148
|
+
var IMAGE_DATA_URL = /^data:[^;,]+;base64,[A-Za-z0-9+/=\s]+$/;
|
|
3149
|
+
function requireImageUrl(url, field) {
|
|
3150
|
+
if (typeof url === "string") {
|
|
3151
|
+
if (url.startsWith("http://") || url.startsWith("https://")) return url;
|
|
3152
|
+
if (IMAGE_DATA_URL.test(url)) return url;
|
|
3153
|
+
}
|
|
3154
|
+
throw badRequest(
|
|
3155
|
+
`${field} must be an http(s) URL or a base64 data: URL (data:<mime>;base64,<bytes>).`
|
|
3156
|
+
);
|
|
3157
|
+
}
|
|
3158
|
+
function resolveScratchpadFile(dir, fileName) {
|
|
3159
|
+
if (!fileName || fileName === "." || fileName === "..") return null;
|
|
3160
|
+
for (const ch of fileName) {
|
|
3161
|
+
const code = ch.codePointAt(0);
|
|
3162
|
+
if (code < 32 || code === 127 || ch === "/" || ch === "\\") return null;
|
|
3163
|
+
}
|
|
3164
|
+
const resolved = path7.resolve(dir, fileName);
|
|
3165
|
+
return path7.dirname(resolved) === path7.resolve(dir) ? resolved : null;
|
|
3166
|
+
}
|
|
2917
3167
|
function parseTaskInput(body) {
|
|
2918
3168
|
const input = body.input;
|
|
2919
3169
|
if (!Array.isArray(input) || input.length === 0) {
|
|
2920
3170
|
throw badRequest("input must be an array with at least one item.");
|
|
2921
3171
|
}
|
|
2922
|
-
|
|
3172
|
+
const messages = [];
|
|
3173
|
+
const attachments = [];
|
|
3174
|
+
input.forEach((item, i) => {
|
|
2923
3175
|
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
2924
3176
|
throw badRequest(`input[${i}] must be an object.`);
|
|
2925
3177
|
}
|
|
@@ -2928,17 +3180,27 @@ function parseTaskInput(body) {
|
|
|
2928
3180
|
if (typeof part.text !== "string" || part.text.length === 0) {
|
|
2929
3181
|
throw badRequest(`input[${i}].text must be a non-empty string.`);
|
|
2930
3182
|
}
|
|
2931
|
-
|
|
3183
|
+
messages.push(userText(part.text));
|
|
3184
|
+
return;
|
|
2932
3185
|
}
|
|
2933
3186
|
if (part.type === "image_url") {
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
throw badRequest(`input[${i}].imageUrl only supports data: or http(s) URLs.`);
|
|
2937
|
-
}
|
|
2938
|
-
return imageUrlMessage(url);
|
|
3187
|
+
messages.push(imageUrlMessage(requireImageUrl(part.imageUrl, `input[${i}].imageUrl`)));
|
|
3188
|
+
return;
|
|
2939
3189
|
}
|
|
2940
|
-
|
|
3190
|
+
if (part.type === "file") {
|
|
3191
|
+
attachments.push(parseAttachmentPart(part, i));
|
|
3192
|
+
assertAttachmentBudget(attachments);
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
throw badRequest(`input[${i}].type must be one of text / image_url / file.`);
|
|
2941
3196
|
});
|
|
3197
|
+
return { messages, attachments };
|
|
3198
|
+
}
|
|
3199
|
+
function parseSteerImages(body) {
|
|
3200
|
+
const images = body.images;
|
|
3201
|
+
if (images === void 0) return [];
|
|
3202
|
+
if (!Array.isArray(images)) throw badRequest("images must be an array.");
|
|
3203
|
+
return images.map((url, i) => requireImageUrl(url, `images[${i}]`));
|
|
2942
3204
|
}
|
|
2943
3205
|
function parseGoalField(body) {
|
|
2944
3206
|
const goal = body.goal;
|
|
@@ -3101,8 +3363,8 @@ function sessionsRoutes(deps2) {
|
|
|
3101
3363
|
]);
|
|
3102
3364
|
}
|
|
3103
3365
|
await deps2.traceService.deleteSessionTraces(row.projectId, row.agentId, row.sessionId);
|
|
3104
|
-
await
|
|
3105
|
-
|
|
3366
|
+
await fs8.rm(
|
|
3367
|
+
path7.join(scratchpadDir(deps2.config.root, row.projectId, row.agentId), row.sessionId),
|
|
3106
3368
|
{ recursive: true, force: true }
|
|
3107
3369
|
);
|
|
3108
3370
|
deps2.sessionsRepo.deleteById(row.sessionId);
|
|
@@ -3116,17 +3378,14 @@ function sessionsRoutes(deps2) {
|
|
|
3116
3378
|
app2.get("/:sessionId/scratchpad/:fileName", async (c) => {
|
|
3117
3379
|
const row = resolveSession(c);
|
|
3118
3380
|
const fileName = c.req.param("fileName") ?? "";
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
}
|
|
3122
|
-
const filePath = path6.join(
|
|
3123
|
-
scratchpadDir(deps2.config.root, row.projectId, row.agentId),
|
|
3124
|
-
row.sessionId,
|
|
3381
|
+
const filePath = resolveScratchpadFile(
|
|
3382
|
+
path7.join(scratchpadDir(deps2.config.root, row.projectId, row.agentId), row.sessionId),
|
|
3125
3383
|
fileName
|
|
3126
3384
|
);
|
|
3385
|
+
if (!filePath) throw new HttpError(404, "file_not_found", "File does not exist.");
|
|
3127
3386
|
let bytes;
|
|
3128
3387
|
try {
|
|
3129
|
-
bytes = await
|
|
3388
|
+
bytes = await fs8.readFile(filePath);
|
|
3130
3389
|
} catch {
|
|
3131
3390
|
throw new HttpError(404, "file_not_found", "File does not exist.");
|
|
3132
3391
|
}
|
|
@@ -3137,9 +3396,21 @@ function sessionsRoutes(deps2) {
|
|
|
3137
3396
|
".gif": "image/gif",
|
|
3138
3397
|
".webp": "image/webp"
|
|
3139
3398
|
};
|
|
3140
|
-
const mime = MIME_BY_EXT[
|
|
3399
|
+
const mime = MIME_BY_EXT[path7.extname(fileName).toLowerCase()];
|
|
3141
3400
|
return c.body(new Uint8Array(bytes), 200, {
|
|
3142
|
-
"content-type": mime,
|
|
3401
|
+
"content-type": mime ?? "application/octet-stream",
|
|
3402
|
+
// nosniff: the composer's file attachments land in this same directory, so the bytes
|
|
3403
|
+
// here are arbitrary user content served from the App's own origin — without it a
|
|
3404
|
+
// browser could sniff an `application/octet-stream` upload back into HTML and run it
|
|
3405
|
+
// same-origin (the same defense workspace file reads apply).
|
|
3406
|
+
"x-content-type-options": "nosniff",
|
|
3407
|
+
// Second, independent layer for everything that fell off the allowlist: the only reason
|
|
3408
|
+
// this endpoint is fetched inline is the conversation's <img> tags, so anything that is
|
|
3409
|
+
// not one of those images is served as a download and never renders as a document —
|
|
3410
|
+
// nosniff alone would be the whole defense otherwise.
|
|
3411
|
+
...mime === void 0 ? {
|
|
3412
|
+
"content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`
|
|
3413
|
+
} : {},
|
|
3143
3414
|
"cache-control": "private, max-age=31536000, immutable"
|
|
3144
3415
|
});
|
|
3145
3416
|
});
|
|
@@ -3182,32 +3453,53 @@ function sessionsRoutes(deps2) {
|
|
|
3182
3453
|
const goal = parseGoalField(body);
|
|
3183
3454
|
const thinkingLevel = optionalEnum(body, "thinkingLevel", THINKING_LEVELS);
|
|
3184
3455
|
if (goal) {
|
|
3185
|
-
const
|
|
3186
|
-
const text =
|
|
3187
|
-
if (!text
|
|
3188
|
-
throw badRequest("goal mode requires
|
|
3456
|
+
const { messages, attachments } = parseTaskInput(body);
|
|
3457
|
+
const text = messages.filter((m) => m.payload.type === "text").map((m) => m.payload.text).join("\n").trim();
|
|
3458
|
+
if (!text) {
|
|
3459
|
+
throw badRequest("goal mode requires a non-empty text objective.");
|
|
3460
|
+
}
|
|
3461
|
+
if (attachments.length > 0) {
|
|
3462
|
+
throw badRequest("goal mode accepts text and images only (no file attachments).");
|
|
3189
3463
|
}
|
|
3190
|
-
const { sessionId
|
|
3191
|
-
input:
|
|
3464
|
+
const { sessionId } = await deps2.manager.startGoal(row.sessionId, {
|
|
3465
|
+
input: messages,
|
|
3192
3466
|
budget: goal.budget,
|
|
3193
3467
|
...thinkingLevel !== void 0 ? { thinkingLevel } : {}
|
|
3194
3468
|
});
|
|
3195
|
-
return c.json({ sessionId
|
|
3469
|
+
return c.json({ sessionId }, 202);
|
|
3196
3470
|
}
|
|
3197
|
-
const
|
|
3471
|
+
const parsed = parseTaskInput(body);
|
|
3198
3472
|
const queueIfBusy = body.queueIfBusy === true;
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3473
|
+
deps2.manager.assertCanAcceptTask(row.sessionId, { queueIfBusy });
|
|
3474
|
+
const { input, written } = await attachFilesToInput(
|
|
3475
|
+
parsed.messages,
|
|
3476
|
+
parsed.attachments,
|
|
3477
|
+
scratchpadDir(deps2.config.root, row.projectId, row.agentId),
|
|
3478
|
+
row.sessionId
|
|
3479
|
+
);
|
|
3480
|
+
try {
|
|
3481
|
+
const { sessionId, queued } = await deps2.manager.startTask(row.sessionId, input, {
|
|
3482
|
+
...thinkingLevel !== void 0 ? { thinkingLevel } : {},
|
|
3483
|
+
queueIfBusy
|
|
3484
|
+
});
|
|
3485
|
+
return c.json({ sessionId, queued }, 202);
|
|
3486
|
+
} catch (err) {
|
|
3487
|
+
await removeAttachments(written);
|
|
3488
|
+
throw err;
|
|
3489
|
+
}
|
|
3204
3490
|
});
|
|
3205
3491
|
app2.post("/:sessionId/steer", async (c) => {
|
|
3206
3492
|
const row = resolveSession(c);
|
|
3207
3493
|
const body = await readJson(c);
|
|
3208
3494
|
const text = typeof body.text === "string" ? body.text.trim() : "";
|
|
3209
|
-
|
|
3210
|
-
|
|
3495
|
+
const images = parseSteerImages(body);
|
|
3496
|
+
if (!text && images.length === 0) {
|
|
3497
|
+
throw badRequest("text or images must carry the steering message.");
|
|
3498
|
+
}
|
|
3499
|
+
deps2.manager.steer(row.sessionId, [
|
|
3500
|
+
...text ? [userText(text)] : [],
|
|
3501
|
+
...images.map((url) => imageUrlMessage(url))
|
|
3502
|
+
]);
|
|
3211
3503
|
return c.body(null, 202);
|
|
3212
3504
|
});
|
|
3213
3505
|
app2.get("/:sessionId/goal", (c) => {
|
|
@@ -3661,8 +3953,8 @@ var ErrorRecorder = class {
|
|
|
3661
3953
|
};
|
|
3662
3954
|
|
|
3663
3955
|
// src/runtime/session-manager.ts
|
|
3664
|
-
import
|
|
3665
|
-
import
|
|
3956
|
+
import fs9 from "fs/promises";
|
|
3957
|
+
import path9 from "path";
|
|
3666
3958
|
import {
|
|
3667
3959
|
createAgent,
|
|
3668
3960
|
findLatestTraceFile,
|
|
@@ -3922,14 +4214,26 @@ var SessionSources = class {
|
|
|
3922
4214
|
|
|
3923
4215
|
// src/runtime/stream-error-watcher.ts
|
|
3924
4216
|
import { isEventMessage, isModelMessage, isSessionMeta } from "@prismshadow/penguin-core";
|
|
3925
|
-
import
|
|
4217
|
+
import path8 from "path";
|
|
3926
4218
|
var TOOL_NAMES_MAX = 1e3;
|
|
3927
4219
|
var ORIGIN_CTX_MAX = 200;
|
|
3928
4220
|
var LLM_FAILURES = {
|
|
3929
|
-
|
|
3930
|
-
//
|
|
3931
|
-
//
|
|
3932
|
-
|
|
4221
|
+
// Reached here only when no further attempt followed: the ladder ran out (or the run ended
|
|
4222
|
+
// on it), so the user lost the turn and someone should look. A `failed` that was retried
|
|
4223
|
+
// takes LLM_FAILED_RETRIED instead.
|
|
4224
|
+
failed: {
|
|
4225
|
+
code: "llm_failed",
|
|
4226
|
+
kind: "unexpected",
|
|
4227
|
+
text: "LLM request failed and the retries did not recover it."
|
|
4228
|
+
},
|
|
4229
|
+
// Its own code, not the failed bucket: dedup is `(source, code, Project)` over a short
|
|
4230
|
+
// window, and `llm_failed_retried` can now fire on any recovered blip — sharing a bucket
|
|
4231
|
+
// would let a genuine credential failure be dropped as a duplicate of one.
|
|
4232
|
+
auth: {
|
|
4233
|
+
code: "llm_auth",
|
|
4234
|
+
kind: "unexpected",
|
|
4235
|
+
text: "LLM request rejected: the provider did not accept the credentials."
|
|
4236
|
+
},
|
|
3933
4237
|
timeout: {
|
|
3934
4238
|
code: "llm_timeout",
|
|
3935
4239
|
kind: "expected",
|
|
@@ -3941,12 +4245,23 @@ var LLM_FAILURES = {
|
|
|
3941
4245
|
text: "LLM response could not be parsed (the engine reconnects and retries)."
|
|
3942
4246
|
}
|
|
3943
4247
|
};
|
|
4248
|
+
var LLM_FAILED_RETRIED = {
|
|
4249
|
+
code: "llm_failed_retried",
|
|
4250
|
+
kind: "expected",
|
|
4251
|
+
text: "LLM request failed (the engine reconnects and retries)."
|
|
4252
|
+
};
|
|
3944
4253
|
function isLlmFailure(s) {
|
|
3945
4254
|
return s === "failed" || s === "timeout" || s === "malformed" || s === "auth";
|
|
3946
4255
|
}
|
|
3947
4256
|
function isToolFailure(s) {
|
|
3948
4257
|
return s === "failed" || s === "timeout";
|
|
3949
4258
|
}
|
|
4259
|
+
var COMMAND_TOOLS = /* @__PURE__ */ new Set(["exec_command", "input_command"]);
|
|
4260
|
+
var ORDINARY_EXIT_NOTE = /\[exit code: [^\]\n]*\]\s*$/;
|
|
4261
|
+
function isOrdinaryCommandExit(name, output) {
|
|
4262
|
+
if (name !== void 0 && !COMMAND_TOOLS.has(name)) return false;
|
|
4263
|
+
return ORDINARY_EXIT_NOTE.test(output);
|
|
4264
|
+
}
|
|
3950
4265
|
function isUserAbortReason(reason) {
|
|
3951
4266
|
return /^aborted\b/i.test(reason);
|
|
3952
4267
|
}
|
|
@@ -4004,7 +4319,7 @@ var StreamErrorWatcher = class {
|
|
|
4004
4319
|
registerOrigin(msg) {
|
|
4005
4320
|
const key = originKey(msg);
|
|
4006
4321
|
if (!key) return;
|
|
4007
|
-
const agentId =
|
|
4322
|
+
const agentId = path8.basename(path8.dirname(msg.payload.agent_state));
|
|
4008
4323
|
if (!agentId || agentId === "." || agentId === "..") return;
|
|
4009
4324
|
this.originCtx.delete(key);
|
|
4010
4325
|
this.originCtx.set(key, { agentId, sessionId: key });
|
|
@@ -4041,7 +4356,7 @@ var StreamErrorWatcher = class {
|
|
|
4041
4356
|
return;
|
|
4042
4357
|
}
|
|
4043
4358
|
if (p.type === "request_begin") {
|
|
4044
|
-
this.flush(key);
|
|
4359
|
+
this.flush(key, null, true);
|
|
4045
4360
|
return;
|
|
4046
4361
|
}
|
|
4047
4362
|
if (p.type === "abort") {
|
|
@@ -4050,15 +4365,21 @@ var StreamErrorWatcher = class {
|
|
|
4050
4365
|
}
|
|
4051
4366
|
/**
|
|
4052
4367
|
* Persist a pending LLM failure (no-op if none is pending); `reason` is the abort
|
|
4053
|
-
* message that arrived afterward
|
|
4368
|
+
* message that arrived afterward, and `retried` says whether another attempt followed
|
|
4369
|
+
* (only a `request_begin` proves that). Pending state is already bucketed by origin, so
|
|
4054
4370
|
* `key` is exactly "the session that produced this failure" — attribution is looked
|
|
4055
4371
|
* up from it (see file header).
|
|
4372
|
+
*
|
|
4373
|
+
* `retried` defaults to false so every other resolution — an abort, the defensive flush at
|
|
4374
|
+
* the next request_end, or close() at the end of the run — is read as "nothing came after
|
|
4375
|
+
* this failure". That is the conservative direction: at worst a recovered blip is escalated,
|
|
4376
|
+
* never a lost turn silently downgraded.
|
|
4056
4377
|
*/
|
|
4057
|
-
flush(key, reason) {
|
|
4378
|
+
flush(key, reason, retried = false) {
|
|
4058
4379
|
const entry = this.pending.get(key);
|
|
4059
4380
|
if (entry === void 0) return;
|
|
4060
4381
|
this.pending.delete(key);
|
|
4061
|
-
const spec = LLM_FAILURES[entry.status];
|
|
4382
|
+
const spec = retried && entry.status === "failed" ? LLM_FAILED_RETRIED : LLM_FAILURES[entry.status];
|
|
4062
4383
|
const trimmed = reason?.trim();
|
|
4063
4384
|
const message = trimmed && !isUserAbortReason(trimmed) ? trimmed : entry.message ?? spec.text;
|
|
4064
4385
|
this.errors.record({
|
|
@@ -4088,9 +4409,11 @@ var StreamErrorWatcher = class {
|
|
|
4088
4409
|
const name = this.toolNames.get(key);
|
|
4089
4410
|
this.toolNames.delete(key);
|
|
4090
4411
|
if (!isToolFailure(p.stop_reason)) return;
|
|
4412
|
+
const output = p.output ?? "";
|
|
4413
|
+
if (isOrdinaryCommandExit(name, output)) return;
|
|
4091
4414
|
this.errors.record({
|
|
4092
4415
|
source: "environment",
|
|
4093
|
-
err: toolFailureText(
|
|
4416
|
+
err: toolFailureText(output),
|
|
4094
4417
|
ctx: this.ctxFor(origin),
|
|
4095
4418
|
// The tool name goes into code: so the stats dashboard's "most common error code" and table can show which tool failed.
|
|
4096
4419
|
code: `tool_${p.stop_reason}:${name ?? "unknown"}`,
|
|
@@ -4129,7 +4452,7 @@ function createCoreSessionLoader(root, sources) {
|
|
|
4129
4452
|
}
|
|
4130
4453
|
}
|
|
4131
4454
|
try {
|
|
4132
|
-
const stat = await
|
|
4455
|
+
const stat = await fs9.stat(row.workspace);
|
|
4133
4456
|
if (!stat.isDirectory()) throw new Error("not a directory");
|
|
4134
4457
|
} catch {
|
|
4135
4458
|
throw new HttpError(
|
|
@@ -4305,6 +4628,23 @@ var SessionManager = class {
|
|
|
4305
4628
|
for (const agentId of agentIds) this.invalidateAgentRuntimes(projectId, agentId);
|
|
4306
4629
|
}
|
|
4307
4630
|
// —— Task / compaction drive ——
|
|
4631
|
+
/**
|
|
4632
|
+
* Cheap, lock-free rehearsal of the 409/503 conditions startTask checks, throwing exactly the
|
|
4633
|
+
* same HttpErrors. **Advisory only**: it neither takes the Session lock nor loads an entry, so
|
|
4634
|
+
* a session that isn't in the active table reads as acceptable and a status change racing this
|
|
4635
|
+
* call is not caught — the authoritative check is still the one inside startTask.
|
|
4636
|
+
*
|
|
4637
|
+
* It exists so a caller that has irreversible work to do first (POST /tasks writes the
|
|
4638
|
+
* message's file attachments to disk) can find out about the ordinary "a Task is already
|
|
4639
|
+
* running" rejection before doing it, instead of undoing it afterwards.
|
|
4640
|
+
*/
|
|
4641
|
+
assertCanAcceptTask(sessionId, opts) {
|
|
4642
|
+
this.assertOpen();
|
|
4643
|
+
this.assertAgentNotDeleting(sessionId);
|
|
4644
|
+
this.assertSessionNotDeleting(sessionId);
|
|
4645
|
+
const entry = this.entries.get(sessionId);
|
|
4646
|
+
if (entry && !opts?.queueIfBusy) this.assertIdle(entry);
|
|
4647
|
+
}
|
|
4308
4648
|
/**
|
|
4309
4649
|
* Start a Task: get-or-load → 409
|
|
4310
4650
|
* mutual-exclusion check → publish the input messages first → drive run in the
|
|
@@ -4543,15 +4883,16 @@ var SessionManager = class {
|
|
|
4543
4883
|
return entry.approvals.decide(toolCallId, decision);
|
|
4544
4884
|
}
|
|
4545
4885
|
/**
|
|
4546
|
-
* Mid-run steering: forward the
|
|
4547
|
-
* turns as a standalone `[user_steering]` user message
|
|
4548
|
-
*
|
|
4549
|
-
* Session isn't running a Task (idle / compacting / not loaded)
|
|
4550
|
-
* race window — the caller falls back to submitting a normal
|
|
4886
|
+
* Mid-run steering: forward the message to the running Session (core delivers it between
|
|
4887
|
+
* turns as a standalone `[user_steering]` user message followed by its images — no SSE
|
|
4888
|
+
* event of its own; the messages arrive through the stream the drive loop already
|
|
4889
|
+
* publishes). 409 when the Session isn't running a Task (idle / compacting / not loaded)
|
|
4890
|
+
* or the run finished in the race window — the caller falls back to submitting a normal
|
|
4891
|
+
* task, which carries the same text and images.
|
|
4551
4892
|
*/
|
|
4552
|
-
steer(sessionId,
|
|
4893
|
+
steer(sessionId, input) {
|
|
4553
4894
|
const entry = this.entries.get(sessionId);
|
|
4554
|
-
if (!entry || entry.status !== "running" || !entry.session.steer(
|
|
4895
|
+
if (!entry || entry.status !== "running" || !entry.session.steer(input)) {
|
|
4555
4896
|
throw new HttpError(
|
|
4556
4897
|
409,
|
|
4557
4898
|
"not_running",
|
|
@@ -4915,7 +5256,7 @@ var SessionManager = class {
|
|
|
4915
5256
|
const childSid = msg.origin[msg.origin.length - 1];
|
|
4916
5257
|
if (children.has(childSid)) return null;
|
|
4917
5258
|
const p = msg.payload;
|
|
4918
|
-
const agentId =
|
|
5259
|
+
const agentId = path9.basename(path9.dirname(p.agent_state));
|
|
4919
5260
|
if (!agentId || agentId === "." || agentId === "..") return null;
|
|
4920
5261
|
const source = asSessionSource(p.source) ?? "subagent";
|
|
4921
5262
|
this.deps.sources.set(childSid, source);
|
|
@@ -5394,7 +5735,7 @@ var UsageRecorder = class {
|
|
|
5394
5735
|
};
|
|
5395
5736
|
|
|
5396
5737
|
// src/services/ids.ts
|
|
5397
|
-
import { randomBytes as
|
|
5738
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
5398
5739
|
var SEMANTIC_ID_PATTERN = /^[a-z][a-z0-9_]{1,63}$/;
|
|
5399
5740
|
var USERNAME_PATTERN = /^[a-z][a-z0-9_]{1,31}$/;
|
|
5400
5741
|
var PROJECT_SUFFIX_PATTERN = /^[a-z0-9_]+$/;
|
|
@@ -5470,7 +5811,7 @@ var AdminService = class {
|
|
|
5470
5811
|
};
|
|
5471
5812
|
|
|
5472
5813
|
// src/services/agent-config-service.ts
|
|
5473
|
-
import
|
|
5814
|
+
import fs11 from "fs/promises";
|
|
5474
5815
|
import { parseDocument, parse as parseYaml } from "yaml";
|
|
5475
5816
|
import {
|
|
5476
5817
|
agentsMdPath,
|
|
@@ -5485,8 +5826,8 @@ import {
|
|
|
5485
5826
|
} from "@prismshadow/penguin-core";
|
|
5486
5827
|
|
|
5487
5828
|
// src/services/project-config-service.ts
|
|
5488
|
-
import
|
|
5489
|
-
import
|
|
5829
|
+
import fs10 from "fs/promises";
|
|
5830
|
+
import path10 from "path";
|
|
5490
5831
|
import { parse as parseToml2 } from "smol-toml";
|
|
5491
5832
|
import {
|
|
5492
5833
|
GenerativeModel,
|
|
@@ -5545,7 +5886,7 @@ var ProjectConfigService = class {
|
|
|
5545
5886
|
*/
|
|
5546
5887
|
async configUpdatedAt(projectId) {
|
|
5547
5888
|
try {
|
|
5548
|
-
const st = await
|
|
5889
|
+
const st = await fs10.stat(this.filePath(projectId));
|
|
5549
5890
|
return st.mtime.toISOString();
|
|
5550
5891
|
} catch {
|
|
5551
5892
|
return void 0;
|
|
@@ -5555,7 +5896,7 @@ var ProjectConfigService = class {
|
|
|
5555
5896
|
async readRaw(projectId) {
|
|
5556
5897
|
let raw;
|
|
5557
5898
|
try {
|
|
5558
|
-
raw = await
|
|
5899
|
+
raw = await fs10.readFile(this.filePath(projectId), "utf8");
|
|
5559
5900
|
} catch (err) {
|
|
5560
5901
|
if (err.code === "ENOENT") return {};
|
|
5561
5902
|
throw err;
|
|
@@ -5570,9 +5911,9 @@ var ProjectConfigService = class {
|
|
|
5570
5911
|
*/
|
|
5571
5912
|
async writeRaw(projectId, data) {
|
|
5572
5913
|
const file = this.filePath(projectId);
|
|
5573
|
-
await
|
|
5574
|
-
await
|
|
5575
|
-
await
|
|
5914
|
+
await fs10.mkdir(path10.dirname(file), { recursive: true });
|
|
5915
|
+
await fs10.writeFile(file, renderProjectConfigToml(data), { encoding: "utf8", mode: 384 });
|
|
5916
|
+
await fs10.chmod(file, 384);
|
|
5576
5917
|
}
|
|
5577
5918
|
/**
|
|
5578
5919
|
* Initial config for a newly created Project: display name + preset built-in
|
|
@@ -5616,6 +5957,15 @@ var ProjectConfigService = class {
|
|
|
5616
5957
|
const raw = await this.readRaw(projectId);
|
|
5617
5958
|
return typeof raw.name === "string" ? raw.name : void 0;
|
|
5618
5959
|
}
|
|
5960
|
+
/**
|
|
5961
|
+
* Rewrites the display name, preserving every other field (models, credentials, default
|
|
5962
|
+
* model): read-modify-write of the same toml, like ensurePresetModels. The id itself is
|
|
5963
|
+
* immutable — only this label changes.
|
|
5964
|
+
*/
|
|
5965
|
+
async setName(projectId, name) {
|
|
5966
|
+
const raw = await this.readRaw(projectId);
|
|
5967
|
+
await this.writeRaw(projectId, { ...raw, name });
|
|
5968
|
+
}
|
|
5619
5969
|
/** Paired reference of the default Model; returns undefined if unconfigured (or in the old string format). */
|
|
5620
5970
|
async getDefaultModelRef(projectId) {
|
|
5621
5971
|
const raw = await this.readRaw(projectId);
|
|
@@ -5936,7 +6286,7 @@ var AgentConfigService = class {
|
|
|
5936
6286
|
/** Whether the Agent exists (determined by the presence of system_config.yaml, matching the CLI's convention). */
|
|
5937
6287
|
async exists(projectId, agentId) {
|
|
5938
6288
|
try {
|
|
5939
|
-
await
|
|
6289
|
+
await fs11.access(systemConfigPath(this.root, projectId, agentId));
|
|
5940
6290
|
return true;
|
|
5941
6291
|
} catch {
|
|
5942
6292
|
return false;
|
|
@@ -5954,7 +6304,7 @@ var AgentConfigService = class {
|
|
|
5954
6304
|
*/
|
|
5955
6305
|
async readCardMeta(projectId, agentId) {
|
|
5956
6306
|
try {
|
|
5957
|
-
const raw = await
|
|
6307
|
+
const raw = await fs11.readFile(systemConfigPath(this.root, projectId, agentId), "utf8");
|
|
5958
6308
|
const parsed = asRecord(parseYaml(raw));
|
|
5959
6309
|
const tools = asRecord(parsed.tools);
|
|
5960
6310
|
const countOf = (v) => Array.isArray(v) ? v.length : 0;
|
|
@@ -5972,14 +6322,14 @@ var AgentConfigService = class {
|
|
|
5972
6322
|
async getConfig(projectId, agentId) {
|
|
5973
6323
|
await this.requireExists(projectId, agentId);
|
|
5974
6324
|
const yamlPath = systemConfigPath(this.root, projectId, agentId);
|
|
5975
|
-
const systemConfigYaml = await
|
|
6325
|
+
const systemConfigYaml = await fs11.readFile(yamlPath, "utf8");
|
|
5976
6326
|
const parsed = asRecord(parseYaml(systemConfigYaml));
|
|
5977
6327
|
const model = asRecord(parsed.model);
|
|
5978
6328
|
const compaction = asRecord(parsed.compaction);
|
|
5979
6329
|
const tools = asRecord(parsed.tools);
|
|
5980
6330
|
let agentsMd = "";
|
|
5981
6331
|
try {
|
|
5982
|
-
agentsMd = await
|
|
6332
|
+
agentsMd = await fs11.readFile(agentsMdPath(this.root, projectId, agentId), "utf8");
|
|
5983
6333
|
} catch {
|
|
5984
6334
|
}
|
|
5985
6335
|
const modelDto = {
|
|
@@ -6022,7 +6372,7 @@ var AgentConfigService = class {
|
|
|
6022
6372
|
await this.applyConfigUpdate(projectId, agentId, req.config);
|
|
6023
6373
|
}
|
|
6024
6374
|
if (req.agentsMd !== void 0) {
|
|
6025
|
-
await
|
|
6375
|
+
await fs11.writeFile(agentsMdPath(this.root, projectId, agentId), req.agentsMd, "utf8");
|
|
6026
6376
|
}
|
|
6027
6377
|
}
|
|
6028
6378
|
/**
|
|
@@ -6040,9 +6390,9 @@ var AgentConfigService = class {
|
|
|
6040
6390
|
async applyConfigUpdate(projectId, agentId, config2) {
|
|
6041
6391
|
const cfg = config2;
|
|
6042
6392
|
const yamlPath = systemConfigPath(this.root, projectId, agentId);
|
|
6043
|
-
const doc = parseDocument(await
|
|
6044
|
-
const setIfProvided = (
|
|
6045
|
-
if (value !== void 0) doc.setIn(
|
|
6393
|
+
const doc = parseDocument(await fs11.readFile(yamlPath, "utf8"));
|
|
6394
|
+
const setIfProvided = (path18, value) => {
|
|
6395
|
+
if (value !== void 0) doc.setIn(path18, value);
|
|
6046
6396
|
};
|
|
6047
6397
|
setIfProvided(["name"], optionalString(cfg, "name", { maxLen: 100, label: "name" }));
|
|
6048
6398
|
setIfProvided(
|
|
@@ -6091,7 +6441,7 @@ var AgentConfigService = class {
|
|
|
6091
6441
|
if (cfg.mcpServers !== void 0) {
|
|
6092
6442
|
doc.setIn(["tools", "mcpServers"], validateMcpServers(cfg.mcpServers));
|
|
6093
6443
|
}
|
|
6094
|
-
await
|
|
6444
|
+
await fs11.writeFile(yamlPath, doc.toString(), "utf8");
|
|
6095
6445
|
}
|
|
6096
6446
|
/** Read the Agent vault (agent_state/.vault.toml): values are always masked, plaintext is never sent to the client. */
|
|
6097
6447
|
async getVault(projectId, agentId) {
|
|
@@ -6188,8 +6538,8 @@ function validateMcpServers(value) {
|
|
|
6188
6538
|
}
|
|
6189
6539
|
|
|
6190
6540
|
// src/services/agent-service.ts
|
|
6191
|
-
import
|
|
6192
|
-
import
|
|
6541
|
+
import fs12 from "fs/promises";
|
|
6542
|
+
import path11 from "path";
|
|
6193
6543
|
import {
|
|
6194
6544
|
agentDir,
|
|
6195
6545
|
agentsDir as agentsDir2,
|
|
@@ -6216,7 +6566,7 @@ var AgentService = class {
|
|
|
6216
6566
|
const known = new Map(this.agents.list(projectId).map((r) => [r.agentId, r]));
|
|
6217
6567
|
let entries = [];
|
|
6218
6568
|
try {
|
|
6219
|
-
const dirents = await
|
|
6569
|
+
const dirents = await fs12.readdir(agentsDir2(this.root, projectId), { withFileTypes: true });
|
|
6220
6570
|
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
6221
6571
|
} catch {
|
|
6222
6572
|
}
|
|
@@ -6225,7 +6575,7 @@ var AgentService = class {
|
|
|
6225
6575
|
const configPath = systemConfigPath2(this.root, projectId, agentId);
|
|
6226
6576
|
let createdAt;
|
|
6227
6577
|
try {
|
|
6228
|
-
const stat = await
|
|
6578
|
+
const stat = await fs12.stat(configPath);
|
|
6229
6579
|
createdAt = (stat.birthtime.getTime() > 0 ? stat.birthtime : stat.mtime).toISOString();
|
|
6230
6580
|
} catch {
|
|
6231
6581
|
continue;
|
|
@@ -6269,7 +6619,7 @@ var AgentService = class {
|
|
|
6269
6619
|
/** Number of scheduled tasks: count of .toml files under schedule/ (0 if the directory doesn't exist). */
|
|
6270
6620
|
async scheduleCount(projectId, agentId) {
|
|
6271
6621
|
try {
|
|
6272
|
-
const names = await
|
|
6622
|
+
const names = await fs12.readdir(scheduleDir2(this.root, projectId, agentId));
|
|
6273
6623
|
return names.filter((n) => n.endsWith(".toml")).length;
|
|
6274
6624
|
} catch {
|
|
6275
6625
|
return 0;
|
|
@@ -6280,14 +6630,14 @@ var AgentService = class {
|
|
|
6280
6630
|
const base = skillsDir2(this.root, projectId, agentId);
|
|
6281
6631
|
let dirents;
|
|
6282
6632
|
try {
|
|
6283
|
-
dirents = await
|
|
6633
|
+
dirents = await fs12.readdir(base, { withFileTypes: true });
|
|
6284
6634
|
} catch {
|
|
6285
6635
|
return 0;
|
|
6286
6636
|
}
|
|
6287
6637
|
const present = await Promise.all(
|
|
6288
6638
|
dirents.filter((d) => d.isDirectory()).map(async (d) => {
|
|
6289
6639
|
try {
|
|
6290
|
-
await
|
|
6640
|
+
await fs12.access(path11.join(base, d.name, "SKILL.md"));
|
|
6291
6641
|
return true;
|
|
6292
6642
|
} catch {
|
|
6293
6643
|
return false;
|
|
@@ -6305,7 +6655,7 @@ var AgentService = class {
|
|
|
6305
6655
|
const times = await Promise.all(
|
|
6306
6656
|
paths.map(async (p) => {
|
|
6307
6657
|
try {
|
|
6308
|
-
return (await
|
|
6658
|
+
return (await fs12.stat(p)).mtime.getTime();
|
|
6309
6659
|
} catch {
|
|
6310
6660
|
return 0;
|
|
6311
6661
|
}
|
|
@@ -6330,7 +6680,7 @@ var AgentService = class {
|
|
|
6330
6680
|
"Built-in Agents (default_agent) are provisioned with the Project and cannot be deleted from the web."
|
|
6331
6681
|
);
|
|
6332
6682
|
}
|
|
6333
|
-
await
|
|
6683
|
+
await fs12.rm(agentDir(this.root, projectId, agentId), { recursive: true, force: true });
|
|
6334
6684
|
this.agents.delete(projectId, agentId);
|
|
6335
6685
|
}
|
|
6336
6686
|
/**
|
|
@@ -6347,7 +6697,7 @@ var AgentService = class {
|
|
|
6347
6697
|
`Agent id must be 2\u201364 characters: ${SEMANTIC_ID_RULE}.`
|
|
6348
6698
|
);
|
|
6349
6699
|
}
|
|
6350
|
-
const taken = this.agents.exists(projectId, agentId) || await
|
|
6700
|
+
const taken = this.agents.exists(projectId, agentId) || await fs12.stat(agentDir(this.root, projectId, agentId)).then(
|
|
6351
6701
|
() => true,
|
|
6352
6702
|
() => false
|
|
6353
6703
|
);
|
|
@@ -6361,7 +6711,7 @@ var AgentService = class {
|
|
|
6361
6711
|
config: { name: displayName, ...description !== void 0 ? { description } : {} }
|
|
6362
6712
|
});
|
|
6363
6713
|
} catch (err) {
|
|
6364
|
-
await
|
|
6714
|
+
await fs12.rm(agentDir(this.root, projectId, agentId), { recursive: true, force: true }).catch(() => {
|
|
6365
6715
|
});
|
|
6366
6716
|
throw err;
|
|
6367
6717
|
}
|
|
@@ -6385,120 +6735,205 @@ var AgentService = class {
|
|
|
6385
6735
|
};
|
|
6386
6736
|
|
|
6387
6737
|
// src/services/benchmark-service.ts
|
|
6388
|
-
import
|
|
6389
|
-
import
|
|
6738
|
+
import fs13 from "fs/promises";
|
|
6739
|
+
import path12 from "path";
|
|
6390
6740
|
import { parse as parseToml3 } from "smol-toml";
|
|
6391
6741
|
import { parse as parseYaml2 } from "yaml";
|
|
6392
6742
|
import { benchmarksDir } from "@prismshadow/penguin-core";
|
|
6743
|
+
var STATEMENT_TITLE_READ_BYTES = 64 * 1024;
|
|
6393
6744
|
function asRecord2(v) {
|
|
6394
6745
|
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
6395
6746
|
}
|
|
6396
6747
|
function numberOr(v) {
|
|
6397
6748
|
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
6398
6749
|
}
|
|
6750
|
+
function scoreOr(v) {
|
|
6751
|
+
const value = numberOr(v);
|
|
6752
|
+
return value !== void 0 && value >= 0 && value <= 100 ? value : void 0;
|
|
6753
|
+
}
|
|
6754
|
+
function nonNegativeOr(v) {
|
|
6755
|
+
const value = numberOr(v);
|
|
6756
|
+
return value !== void 0 && value >= 0 ? value : void 0;
|
|
6757
|
+
}
|
|
6758
|
+
function nonNegativeIntegerOr(v) {
|
|
6759
|
+
const value = nonNegativeOr(v);
|
|
6760
|
+
return value !== void 0 && Number.isInteger(value) ? value : void 0;
|
|
6761
|
+
}
|
|
6762
|
+
function nullableCostOr(v) {
|
|
6763
|
+
if (v === null) return null;
|
|
6764
|
+
return nonNegativeOr(v);
|
|
6765
|
+
}
|
|
6399
6766
|
function stringOr(v) {
|
|
6400
6767
|
return typeof v === "string" && v !== "" ? v : void 0;
|
|
6401
6768
|
}
|
|
6769
|
+
function isWithin(parent, child) {
|
|
6770
|
+
const relative = path12.relative(parent, child);
|
|
6771
|
+
return relative !== "" && !relative.startsWith("..") && !path12.isAbsolute(relative);
|
|
6772
|
+
}
|
|
6773
|
+
function statementTitle(statement, fallback) {
|
|
6774
|
+
const heading = /^#\s+(.+)$/m.exec(statement)?.[1]?.trim();
|
|
6775
|
+
return heading?.replace(/^Case\s+\d+\s*:\s*/i, "") || fallback;
|
|
6776
|
+
}
|
|
6777
|
+
async function readStatementTitle(readme, fallback) {
|
|
6778
|
+
const handle = await fs13.open(readme, "r");
|
|
6779
|
+
try {
|
|
6780
|
+
const buffer = Buffer.alloc(STATEMENT_TITLE_READ_BYTES);
|
|
6781
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
6782
|
+
return statementTitle(buffer.subarray(0, bytesRead).toString("utf8"), fallback);
|
|
6783
|
+
} finally {
|
|
6784
|
+
await handle.close();
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6402
6787
|
function toRun(v) {
|
|
6403
6788
|
const r = asRecord2(v);
|
|
6404
|
-
const score =
|
|
6405
|
-
|
|
6406
|
-
const
|
|
6407
|
-
const durationMs = numberOr(r.duration_ms);
|
|
6789
|
+
const score = scoreOr(r.score);
|
|
6790
|
+
const cost = nullableCostOr(r.cost);
|
|
6791
|
+
const durationMs = nonNegativeIntegerOr(r.duration_ms);
|
|
6408
6792
|
const sessionId = stringOr(r.session_id);
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
...durationMs !== void 0 ? { durationMs } : {},
|
|
6413
|
-
...sessionId !== void 0 ? { sessionId } : {}
|
|
6414
|
-
};
|
|
6415
|
-
}
|
|
6416
|
-
function averageOf(runs, pick) {
|
|
6417
|
-
const values = runs.map(pick).filter((v) => v !== void 0);
|
|
6418
|
-
if (values.length === 0) return void 0;
|
|
6419
|
-
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
6793
|
+
if (score === void 0 || cost === void 0 || durationMs === void 0 || !sessionId)
|
|
6794
|
+
return null;
|
|
6795
|
+
return { score, cost, durationMs, sessionId };
|
|
6420
6796
|
}
|
|
6421
6797
|
function toCase(v) {
|
|
6422
6798
|
const cr = asRecord2(v);
|
|
6423
6799
|
const caseId = stringOr(cr.case);
|
|
6424
|
-
|
|
6425
|
-
const
|
|
6426
|
-
const
|
|
6427
|
-
if (score === void 0)
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
const
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
{
|
|
6434
|
-
score,
|
|
6435
|
-
...cost !== void 0 ? { cost } : {},
|
|
6436
|
-
...durationMs !== void 0 ? { durationMs } : {},
|
|
6437
|
-
...sessionId !== void 0 ? { sessionId } : {}
|
|
6438
|
-
}
|
|
6439
|
-
];
|
|
6800
|
+
const score = scoreOr(cr.score);
|
|
6801
|
+
const cost = nullableCostOr(cr.cost);
|
|
6802
|
+
const durationMs = nonNegativeIntegerOr(cr.duration_ms);
|
|
6803
|
+
if (!caseId || score === void 0 || cost === void 0 || durationMs === void 0 || "max_score" in cr || !Array.isArray(cr.runs) || cr.runs.length === 0) {
|
|
6804
|
+
return null;
|
|
6805
|
+
}
|
|
6806
|
+
const parsedRuns = cr.runs.map(toRun);
|
|
6807
|
+
if (parsedRuns.some((run) => run === null)) return null;
|
|
6808
|
+
const runs = parsedRuns;
|
|
6440
6809
|
return {
|
|
6441
6810
|
case: caseId,
|
|
6442
6811
|
score,
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
...sessionId !== void 0 ? { sessionId } : {},
|
|
6812
|
+
cost,
|
|
6813
|
+
durationMs,
|
|
6446
6814
|
runs
|
|
6447
6815
|
};
|
|
6448
6816
|
}
|
|
6449
6817
|
function toEvaluation(v) {
|
|
6450
6818
|
const r = asRecord2(v);
|
|
6451
6819
|
const time = r.time instanceof Date ? r.time.toISOString() : r.time;
|
|
6452
|
-
const score =
|
|
6453
|
-
|
|
6454
|
-
const
|
|
6820
|
+
const score = scoreOr(r.score);
|
|
6821
|
+
const cost = nullableCostOr(r.cost);
|
|
6822
|
+
const durationMs = nonNegativeIntegerOr(r.duration_ms);
|
|
6455
6823
|
const summary = stringOr(r.summary);
|
|
6456
6824
|
const summaryTitle = stringOr(r.summary_title);
|
|
6457
6825
|
const modelId = stringOr(r.model_id);
|
|
6458
6826
|
const provider = stringOr(r.provider);
|
|
6459
|
-
const
|
|
6460
|
-
const
|
|
6461
|
-
|
|
6827
|
+
const thinkingLevel = stringOr(r.thinking_level);
|
|
6828
|
+
const version = nonNegativeIntegerOr(r.version);
|
|
6829
|
+
if (typeof time !== "string" || time === "" || score === void 0 || cost === void 0 || durationMs === void 0 || !modelId || !provider || !thinkingLevel || version === void 0 || version < 1 || !Array.isArray(r.cases) || r.cases.length === 0) {
|
|
6830
|
+
return null;
|
|
6831
|
+
}
|
|
6832
|
+
const parsedCases = r.cases.map(toCase);
|
|
6833
|
+
if (parsedCases.some((item) => item === null)) return null;
|
|
6834
|
+
const cases = parsedCases;
|
|
6462
6835
|
return {
|
|
6463
6836
|
time,
|
|
6464
6837
|
...summaryTitle !== void 0 ? { summaryTitle } : {},
|
|
6465
6838
|
...summary !== void 0 ? { summary } : {},
|
|
6466
|
-
|
|
6467
|
-
|
|
6839
|
+
modelId,
|
|
6840
|
+
provider,
|
|
6841
|
+
thinkingLevel,
|
|
6468
6842
|
score,
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6843
|
+
version,
|
|
6844
|
+
cost,
|
|
6845
|
+
durationMs,
|
|
6472
6846
|
cases
|
|
6473
6847
|
};
|
|
6474
6848
|
}
|
|
6475
6849
|
var BenchmarkService = class {
|
|
6476
|
-
constructor(root) {
|
|
6850
|
+
constructor(root, workspaceFiles) {
|
|
6477
6851
|
this.root = root;
|
|
6852
|
+
this.workspaceFiles = workspaceFiles;
|
|
6478
6853
|
}
|
|
6479
6854
|
root;
|
|
6855
|
+
workspaceFiles;
|
|
6480
6856
|
async list(projectId, agentId) {
|
|
6481
6857
|
const dir = benchmarksDir(this.root, projectId, agentId);
|
|
6482
6858
|
let items;
|
|
6483
6859
|
try {
|
|
6484
|
-
const entries = await
|
|
6860
|
+
const entries = await fs13.readdir(dir, { withFileTypes: true });
|
|
6485
6861
|
items = entries.map((e) => ({ name: e.name, isDir: e.isDirectory() }));
|
|
6486
6862
|
} catch {
|
|
6487
6863
|
return { benchmarks: [] };
|
|
6488
6864
|
}
|
|
6489
6865
|
const benchmarks = [];
|
|
6490
6866
|
for (const item of items.filter((i) => i.isDir).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
6491
|
-
benchmarks.push(await this.readBenchmark(
|
|
6867
|
+
benchmarks.push(await this.readBenchmark(path12.join(dir, item.name), item.name));
|
|
6492
6868
|
}
|
|
6493
6869
|
return { benchmarks };
|
|
6494
6870
|
}
|
|
6871
|
+
async listCases(projectId, agentId, benchmarkId) {
|
|
6872
|
+
const baseDir = benchmarksDir(this.root, projectId, agentId);
|
|
6873
|
+
const benchDir = path12.join(baseDir, benchmarkId);
|
|
6874
|
+
let entries;
|
|
6875
|
+
let realBaseDir;
|
|
6876
|
+
let realBenchDir;
|
|
6877
|
+
try {
|
|
6878
|
+
[entries, realBaseDir, realBenchDir] = await Promise.all([
|
|
6879
|
+
fs13.readdir(benchDir, { withFileTypes: true }),
|
|
6880
|
+
fs13.realpath(baseDir),
|
|
6881
|
+
fs13.realpath(benchDir)
|
|
6882
|
+
]);
|
|
6883
|
+
} catch {
|
|
6884
|
+
return { cases: [] };
|
|
6885
|
+
}
|
|
6886
|
+
if (!isWithin(realBaseDir, realBenchDir)) return { cases: [] };
|
|
6887
|
+
const cases = [];
|
|
6888
|
+
for (const entry of entries.filter((item) => item.isDirectory() && item.name.startsWith("CASE-")).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
6889
|
+
const fallback = { id: entry.name, title: entry.name };
|
|
6890
|
+
try {
|
|
6891
|
+
const statementDir = await this.statementRoot(projectId, agentId, benchmarkId, entry.name);
|
|
6892
|
+
const realReadme = await fs13.realpath(path12.join(statementDir, "README.md"));
|
|
6893
|
+
if (!isWithin(statementDir, realReadme)) throw new Error("README escapes Statement");
|
|
6894
|
+
cases.push({
|
|
6895
|
+
id: entry.name,
|
|
6896
|
+
title: await readStatementTitle(realReadme, entry.name)
|
|
6897
|
+
});
|
|
6898
|
+
} catch {
|
|
6899
|
+
cases.push(fallback);
|
|
6900
|
+
}
|
|
6901
|
+
}
|
|
6902
|
+
return { cases };
|
|
6903
|
+
}
|
|
6904
|
+
async listCaseFiles(projectId, agentId, benchmarkId, caseId, rel) {
|
|
6905
|
+
const statementDir = await this.statementRoot(projectId, agentId, benchmarkId, caseId);
|
|
6906
|
+
return this.workspaceFiles.list(statementDir, rel);
|
|
6907
|
+
}
|
|
6908
|
+
async readCaseFile(projectId, agentId, benchmarkId, caseId, rel, options) {
|
|
6909
|
+
const statementDir = await this.statementRoot(projectId, agentId, benchmarkId, caseId);
|
|
6910
|
+
return this.workspaceFiles.read(statementDir, rel, options);
|
|
6911
|
+
}
|
|
6912
|
+
async statementRoot(projectId, agentId, benchmarkId, caseId) {
|
|
6913
|
+
const benchDir = path12.join(benchmarksDir(this.root, projectId, agentId), benchmarkId);
|
|
6914
|
+
const caseDir = path12.join(benchDir, caseId);
|
|
6915
|
+
const statementDir = path12.join(caseDir, "statement");
|
|
6916
|
+
try {
|
|
6917
|
+
const [realBenchDir, realCaseDir, realStatementDir] = await Promise.all([
|
|
6918
|
+
fs13.realpath(benchDir),
|
|
6919
|
+
fs13.realpath(caseDir),
|
|
6920
|
+
fs13.realpath(statementDir)
|
|
6921
|
+
]);
|
|
6922
|
+
if (!isWithin(realBenchDir, realCaseDir) || path12.dirname(realStatementDir) !== realCaseDir || path12.basename(realStatementDir) !== "statement") {
|
|
6923
|
+
throw new Error("Statement path is not canonical");
|
|
6924
|
+
}
|
|
6925
|
+
return realStatementDir;
|
|
6926
|
+
} catch {
|
|
6927
|
+
throw new HttpError(404, "not_found", "Public Case Statement does not exist.");
|
|
6928
|
+
}
|
|
6929
|
+
}
|
|
6495
6930
|
async readBenchmark(benchDir, id) {
|
|
6496
6931
|
let title = id;
|
|
6497
6932
|
let description;
|
|
6498
6933
|
let runs;
|
|
6499
6934
|
try {
|
|
6500
6935
|
const config2 = asRecord2(
|
|
6501
|
-
parseToml3(await
|
|
6936
|
+
parseToml3(await fs13.readFile(path12.join(benchDir, "benchmark_config.toml"), "utf8"))
|
|
6502
6937
|
);
|
|
6503
6938
|
if (typeof config2.title === "string" && config2.title !== "") title = config2.title;
|
|
6504
6939
|
if (typeof config2.description === "string" && config2.description !== "") {
|
|
@@ -6513,7 +6948,7 @@ var BenchmarkService = class {
|
|
|
6513
6948
|
let evaluations = [];
|
|
6514
6949
|
try {
|
|
6515
6950
|
const scoreboard = asRecord2(
|
|
6516
|
-
parseYaml2(await
|
|
6951
|
+
parseYaml2(await fs13.readFile(path12.join(benchDir, "scoreboard.yaml"), "utf8"))
|
|
6517
6952
|
);
|
|
6518
6953
|
if (Array.isArray(scoreboard.evaluations)) {
|
|
6519
6954
|
evaluations = scoreboard.evaluations.map(toEvaluation).filter((e) => e !== null);
|
|
@@ -6522,8 +6957,8 @@ var BenchmarkService = class {
|
|
|
6522
6957
|
}
|
|
6523
6958
|
let caseCount = 0;
|
|
6524
6959
|
try {
|
|
6525
|
-
const entries = await
|
|
6526
|
-
caseCount = entries.filter((e) => e.isDirectory()).length;
|
|
6960
|
+
const entries = await fs13.readdir(benchDir, { withFileTypes: true });
|
|
6961
|
+
caseCount = entries.filter((e) => e.isDirectory() && e.name.startsWith("CASE-")).length;
|
|
6527
6962
|
} catch {
|
|
6528
6963
|
}
|
|
6529
6964
|
return {
|
|
@@ -6538,9 +6973,9 @@ var BenchmarkService = class {
|
|
|
6538
6973
|
};
|
|
6539
6974
|
|
|
6540
6975
|
// src/services/snapshot-service.ts
|
|
6541
|
-
import { randomBytes as
|
|
6542
|
-
import
|
|
6543
|
-
import
|
|
6976
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
6977
|
+
import fs14 from "fs/promises";
|
|
6978
|
+
import path13 from "path";
|
|
6544
6979
|
import * as tar from "tar";
|
|
6545
6980
|
import { parse as parseYaml3 } from "yaml";
|
|
6546
6981
|
import {
|
|
@@ -6553,7 +6988,7 @@ import {
|
|
|
6553
6988
|
} from "@prismshadow/penguin-core";
|
|
6554
6989
|
var VAULT_BASENAME = ".vault.toml";
|
|
6555
6990
|
function isVaultEntry(entryPath) {
|
|
6556
|
-
return
|
|
6991
|
+
return path13.posix.basename(entryPath.replaceAll("\\", "/")) === VAULT_BASENAME;
|
|
6557
6992
|
}
|
|
6558
6993
|
var SnapshotService = class {
|
|
6559
6994
|
constructor(root) {
|
|
@@ -6564,7 +6999,7 @@ var SnapshotService = class {
|
|
|
6564
6999
|
async currentVersion(projectId, agentId) {
|
|
6565
7000
|
let raw;
|
|
6566
7001
|
try {
|
|
6567
|
-
raw = await
|
|
7002
|
+
raw = await fs14.readFile(systemConfigPath3(this.root, projectId, agentId), "utf8");
|
|
6568
7003
|
} catch {
|
|
6569
7004
|
throw new HttpError(404, "agent_not_found", "Agent does not exist.");
|
|
6570
7005
|
}
|
|
@@ -6577,14 +7012,14 @@ var SnapshotService = class {
|
|
|
6577
7012
|
async ensureSnapshot(projectId, agentId) {
|
|
6578
7013
|
const version = await this.currentVersion(projectId, agentId);
|
|
6579
7014
|
const dir = snapshotsDir(this.root, projectId, agentId);
|
|
6580
|
-
const file =
|
|
7015
|
+
const file = path13.join(dir, `v${version}.tar.gz`);
|
|
6581
7016
|
try {
|
|
6582
|
-
await
|
|
7017
|
+
await fs14.access(file);
|
|
6583
7018
|
return { version, file };
|
|
6584
7019
|
} catch {
|
|
6585
7020
|
}
|
|
6586
|
-
await
|
|
6587
|
-
const tmp = `${file}.tmp-${
|
|
7021
|
+
await fs14.mkdir(dir, { recursive: true });
|
|
7022
|
+
const tmp = `${file}.tmp-${randomBytes6(4).toString("hex")}`;
|
|
6588
7023
|
await tar.create(
|
|
6589
7024
|
{
|
|
6590
7025
|
gzip: true,
|
|
@@ -6595,7 +7030,7 @@ var SnapshotService = class {
|
|
|
6595
7030
|
},
|
|
6596
7031
|
["agent_state"]
|
|
6597
7032
|
);
|
|
6598
|
-
await
|
|
7033
|
+
await fs14.rename(tmp, file);
|
|
6599
7034
|
return { version, file };
|
|
6600
7035
|
}
|
|
6601
7036
|
/** Export: automatically packs a snapshot first if none exists, returns the info needed for download. */
|
|
@@ -6612,13 +7047,13 @@ var SnapshotService = class {
|
|
|
6612
7047
|
async importArchive(projectId, agentId, archive, confirm) {
|
|
6613
7048
|
const current = await this.currentVersion(projectId, agentId);
|
|
6614
7049
|
const base = agentDir2(this.root, projectId, agentId);
|
|
6615
|
-
const staging =
|
|
6616
|
-
await
|
|
7050
|
+
const staging = path13.join(base, `.import-${randomBytes6(6).toString("hex")}`);
|
|
7051
|
+
await fs14.mkdir(staging, { recursive: true });
|
|
6617
7052
|
try {
|
|
6618
|
-
const archiveFile =
|
|
6619
|
-
await
|
|
6620
|
-
const extractDir =
|
|
6621
|
-
await
|
|
7053
|
+
const archiveFile = path13.join(staging, "archive.tar.gz");
|
|
7054
|
+
await fs14.writeFile(archiveFile, archive);
|
|
7055
|
+
const extractDir = path13.join(staging, "extracted");
|
|
7056
|
+
await fs14.mkdir(extractDir, { recursive: true });
|
|
6622
7057
|
try {
|
|
6623
7058
|
await tar.extract({
|
|
6624
7059
|
file: archiveFile,
|
|
@@ -6628,10 +7063,10 @@ var SnapshotService = class {
|
|
|
6628
7063
|
} catch {
|
|
6629
7064
|
throw badRequest("Import failed: not a valid tar.gz snapshot package.");
|
|
6630
7065
|
}
|
|
6631
|
-
const configPath =
|
|
7066
|
+
const configPath = path13.join(extractDir, "agent_state", "system_config.yaml");
|
|
6632
7067
|
let parsed;
|
|
6633
7068
|
try {
|
|
6634
|
-
parsed = parseYaml3(await
|
|
7069
|
+
parsed = parseYaml3(await fs14.readFile(configPath, "utf8"));
|
|
6635
7070
|
} catch {
|
|
6636
7071
|
throw badRequest("Import failed: the package is missing agent_state/system_config.yaml.");
|
|
6637
7072
|
}
|
|
@@ -6654,37 +7089,37 @@ var SnapshotService = class {
|
|
|
6654
7089
|
}
|
|
6655
7090
|
await this.ensureSnapshot(projectId, agentId);
|
|
6656
7091
|
const stateDir = agentStateDir2(this.root, projectId, agentId);
|
|
6657
|
-
const incomingState =
|
|
7092
|
+
const incomingState = path13.join(extractDir, "agent_state");
|
|
6658
7093
|
let vault = null;
|
|
6659
7094
|
try {
|
|
6660
|
-
vault = await
|
|
7095
|
+
vault = await fs14.readFile(agentVaultPath(this.root, projectId, agentId));
|
|
6661
7096
|
} catch {
|
|
6662
7097
|
}
|
|
6663
7098
|
if (vault !== null) {
|
|
6664
|
-
await
|
|
7099
|
+
await fs14.writeFile(path13.join(incomingState, VAULT_BASENAME), vault, { mode: 384 });
|
|
6665
7100
|
}
|
|
6666
|
-
const trash =
|
|
6667
|
-
await
|
|
7101
|
+
const trash = path13.join(staging, "replaced-agent_state");
|
|
7102
|
+
await fs14.rename(stateDir, trash);
|
|
6668
7103
|
try {
|
|
6669
|
-
await
|
|
7104
|
+
await fs14.rename(incomingState, stateDir);
|
|
6670
7105
|
} catch (err) {
|
|
6671
|
-
await
|
|
7106
|
+
await fs14.rename(trash, stateDir);
|
|
6672
7107
|
throw err;
|
|
6673
7108
|
}
|
|
6674
7109
|
return { version: incoming };
|
|
6675
7110
|
} finally {
|
|
6676
|
-
await
|
|
7111
|
+
await fs14.rm(staging, { recursive: true, force: true });
|
|
6677
7112
|
}
|
|
6678
7113
|
}
|
|
6679
7114
|
};
|
|
6680
7115
|
|
|
6681
7116
|
// src/services/project-service.ts
|
|
6682
|
-
import
|
|
7117
|
+
import fs15 from "fs/promises";
|
|
6683
7118
|
import { DEFAULT_PROJECT_ID, projectDir, provisionProjectAgents } from "@prismshadow/penguin-core";
|
|
6684
7119
|
var ABORT_SETTLE_TIMEOUT_MS = 5e3;
|
|
6685
|
-
async function dirExists(
|
|
7120
|
+
async function dirExists(path18) {
|
|
6686
7121
|
try {
|
|
6687
|
-
const stat = await
|
|
7122
|
+
const stat = await fs15.stat(path18);
|
|
6688
7123
|
return stat.isDirectory();
|
|
6689
7124
|
} catch {
|
|
6690
7125
|
return false;
|
|
@@ -6809,12 +7244,12 @@ var ProjectService = class {
|
|
|
6809
7244
|
throw err;
|
|
6810
7245
|
}
|
|
6811
7246
|
try {
|
|
6812
|
-
await
|
|
7247
|
+
await fs15.mkdir(projectDir(this.deps.root, projectId), { recursive: true });
|
|
6813
7248
|
await this.deps.projectConfig.writeInitialConfig(projectId, displayName);
|
|
6814
7249
|
await this.provisionBuiltinAgents(projectId);
|
|
6815
7250
|
} catch (err) {
|
|
6816
7251
|
this.deps.projects.delete(projectId);
|
|
6817
|
-
await
|
|
7252
|
+
await fs15.rm(projectDir(this.deps.root, projectId), { recursive: true, force: true }).catch(() => {
|
|
6818
7253
|
});
|
|
6819
7254
|
throw err;
|
|
6820
7255
|
}
|
|
@@ -6847,6 +7282,23 @@ var ProjectService = class {
|
|
|
6847
7282
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6848
7283
|
});
|
|
6849
7284
|
}
|
|
7285
|
+
/**
|
|
7286
|
+
* Rename a Project's display name (owner): the id stays immutable — it names the directory,
|
|
7287
|
+
* the Workspace paths and every stored reference — so only the label in project_config.toml
|
|
7288
|
+
* changes. Returns the refreshed summary so the caller can swap it into its list without a
|
|
7289
|
+
* reload.
|
|
7290
|
+
*/
|
|
7291
|
+
async renameProject(userId, projectId, name) {
|
|
7292
|
+
const row = this.requireProjectOwner(userId, projectId);
|
|
7293
|
+
await this.deps.projectConfig.setName(projectId, name);
|
|
7294
|
+
return {
|
|
7295
|
+
projectId,
|
|
7296
|
+
name,
|
|
7297
|
+
role: "owner",
|
|
7298
|
+
ownerUserId: row.ownerUserId,
|
|
7299
|
+
createdAt: row.createdAt
|
|
7300
|
+
};
|
|
7301
|
+
}
|
|
6850
7302
|
/**
|
|
6851
7303
|
* Delete a Project (owner): default_project is refused; deleting the user's
|
|
6852
7304
|
* **last accessible Project** is refused too (deleting it would leave the list
|
|
@@ -6895,7 +7347,7 @@ var ProjectService = class {
|
|
|
6895
7347
|
this.deps.errors.deleteByProject(projectId);
|
|
6896
7348
|
this.deps.schedules.deleteByProject(projectId);
|
|
6897
7349
|
this.deps.goals.deleteByProject(projectId);
|
|
6898
|
-
await
|
|
7350
|
+
await fs15.rm(projectDir(this.deps.root, projectId), { recursive: true, force: true });
|
|
6899
7351
|
}
|
|
6900
7352
|
// —— Member authorization ——
|
|
6901
7353
|
/** Member list: owner (role=owner) + members. */
|
|
@@ -6966,7 +7418,7 @@ var ProjectService = class {
|
|
|
6966
7418
|
};
|
|
6967
7419
|
|
|
6968
7420
|
// src/services/session-service.ts
|
|
6969
|
-
import
|
|
7421
|
+
import path14 from "path";
|
|
6970
7422
|
import { open, readdir as readdir2 } from "fs/promises";
|
|
6971
7423
|
import {
|
|
6972
7424
|
createAgent as createAgent2,
|
|
@@ -7148,7 +7600,7 @@ var SessionService = class {
|
|
|
7148
7600
|
};
|
|
7149
7601
|
const dir = tracesDir2(this.deps.root, projectId, agentId);
|
|
7150
7602
|
for (const dateDir of await listDirsSafe(dir)) {
|
|
7151
|
-
for (const file of await listFilesSafe(
|
|
7603
|
+
for (const file of await listFilesSafe(path14.join(dir, dateDir))) {
|
|
7152
7604
|
const match = TRACE_FILE_RE.exec(file);
|
|
7153
7605
|
if (match) mark(dateDir, match[1]);
|
|
7154
7606
|
}
|
|
@@ -7264,14 +7716,14 @@ var SessionService = class {
|
|
|
7264
7716
|
const dir = tracesDir2(this.deps.root, projectId, agentId);
|
|
7265
7717
|
const out = /* @__PURE__ */ new Map();
|
|
7266
7718
|
for (const dateDir of await listDirsSafe(dir)) {
|
|
7267
|
-
for (const file of await listFilesSafe(
|
|
7719
|
+
for (const file of await listFilesSafe(path14.join(dir, dateDir))) {
|
|
7268
7720
|
const match = TRACE_FILE_RE.exec(file);
|
|
7269
7721
|
if (!match) continue;
|
|
7270
7722
|
const sessionId = match[1];
|
|
7271
7723
|
const index = Number(match[2]);
|
|
7272
7724
|
const cur = out.get(sessionId);
|
|
7273
7725
|
if (!cur || index < cur.index) {
|
|
7274
|
-
out.set(sessionId, { path:
|
|
7726
|
+
out.set(sessionId, { path: path14.join(dir, dateDir, file), index });
|
|
7275
7727
|
}
|
|
7276
7728
|
}
|
|
7277
7729
|
}
|
|
@@ -7334,8 +7786,8 @@ async function listFilesSafe(dir) {
|
|
|
7334
7786
|
}
|
|
7335
7787
|
|
|
7336
7788
|
// src/services/trace-service.ts
|
|
7337
|
-
import
|
|
7338
|
-
import
|
|
7789
|
+
import fs16 from "fs/promises";
|
|
7790
|
+
import path15 from "path";
|
|
7339
7791
|
import {
|
|
7340
7792
|
agentsDir as agentsDir3,
|
|
7341
7793
|
isSessionMeta as isSessionMeta5,
|
|
@@ -7357,7 +7809,7 @@ function subagentPointer(msg) {
|
|
|
7357
7809
|
}
|
|
7358
7810
|
async function listDirs(dir) {
|
|
7359
7811
|
try {
|
|
7360
|
-
const entries = await
|
|
7812
|
+
const entries = await fs16.readdir(dir, { withFileTypes: true });
|
|
7361
7813
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
7362
7814
|
} catch {
|
|
7363
7815
|
return [];
|
|
@@ -7365,7 +7817,7 @@ async function listDirs(dir) {
|
|
|
7365
7817
|
}
|
|
7366
7818
|
async function listFiles(dir) {
|
|
7367
7819
|
try {
|
|
7368
|
-
const entries = await
|
|
7820
|
+
const entries = await fs16.readdir(dir, { withFileTypes: true });
|
|
7369
7821
|
return entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
7370
7822
|
} catch {
|
|
7371
7823
|
return [];
|
|
@@ -7381,10 +7833,10 @@ var TraceService = class {
|
|
|
7381
7833
|
const dir = tracesDir3(this.root, projectId, agentId);
|
|
7382
7834
|
const out = [];
|
|
7383
7835
|
for (const dateDir of await listDirs(dir)) {
|
|
7384
|
-
for (const file of await listFiles(
|
|
7836
|
+
for (const file of await listFiles(path15.join(dir, dateDir))) {
|
|
7385
7837
|
const match = TRACE_FILE_RE2.exec(file);
|
|
7386
7838
|
if (!match || match[1] !== sessionId) continue;
|
|
7387
|
-
out.push({ path:
|
|
7839
|
+
out.push({ path: path15.join(dir, dateDir, file), date: dateDir, index: Number(match[2]) });
|
|
7388
7840
|
}
|
|
7389
7841
|
}
|
|
7390
7842
|
return out.sort((a, b) => a.index - b.index);
|
|
@@ -7393,7 +7845,7 @@ var TraceService = class {
|
|
|
7393
7845
|
async deleteSessionTraces(projectId, agentId, sessionId) {
|
|
7394
7846
|
const files = await this.locateAll(projectId, agentId, sessionId);
|
|
7395
7847
|
for (const file of files) {
|
|
7396
|
-
await
|
|
7848
|
+
await fs16.rm(file.path, { force: true });
|
|
7397
7849
|
}
|
|
7398
7850
|
}
|
|
7399
7851
|
/**
|
|
@@ -7431,7 +7883,7 @@ var TraceService = class {
|
|
|
7431
7883
|
for (const agentId of await listDirs(agentsDir3(this.root, projectId))) {
|
|
7432
7884
|
const dir = tracesDir3(this.root, projectId, agentId);
|
|
7433
7885
|
for (const dateDir of await listDirs(dir)) {
|
|
7434
|
-
for (const file of await listFiles(
|
|
7886
|
+
for (const file of await listFiles(path15.join(dir, dateDir))) {
|
|
7435
7887
|
const match = TRACE_FILE_RE2.exec(file);
|
|
7436
7888
|
if (match && !index.has(match[1])) index.set(match[1], agentId);
|
|
7437
7889
|
}
|
|
@@ -7474,7 +7926,7 @@ var TraceService = class {
|
|
|
7474
7926
|
const files = await this.locateAll(projectId, agentId, sessionId);
|
|
7475
7927
|
const out = [];
|
|
7476
7928
|
for (const file of files) {
|
|
7477
|
-
const stat = await
|
|
7929
|
+
const stat = await fs16.stat(file.path);
|
|
7478
7930
|
out.push({
|
|
7479
7931
|
index: file.index,
|
|
7480
7932
|
date: file.date,
|
|
@@ -7510,6 +7962,7 @@ var TraceService = class {
|
|
|
7510
7962
|
let prevSerialTs = null;
|
|
7511
7963
|
let taskIndex = -1;
|
|
7512
7964
|
let continuation = false;
|
|
7965
|
+
let steeringImages = false;
|
|
7513
7966
|
let sawToolCallThisRequest = false;
|
|
7514
7967
|
let compactionActive = false;
|
|
7515
7968
|
const taskStats = /* @__PURE__ */ new Map();
|
|
@@ -7537,7 +7990,10 @@ var TraceService = class {
|
|
|
7537
7990
|
const hasOrigin = msg.origin !== void 0 && msg.origin.length > 0;
|
|
7538
7991
|
const isSteeringText = !hasOrigin && msg.type === "model_msg" && p.type === "text" && p.role === "user" && typeof p.text === "string" && parseUserSteeringText(p.text) !== null;
|
|
7539
7992
|
if (isSteeringText) continuation = true;
|
|
7540
|
-
const
|
|
7993
|
+
const isImage = !hasOrigin && msg.type === "model_msg" && p.type === "image_url";
|
|
7994
|
+
if (!hasOrigin && !isSteeringText && !(isImage && steeringImages)) steeringImages = false;
|
|
7995
|
+
if (isSteeringText) steeringImages = true;
|
|
7996
|
+
const startsUserTurn = !hasOrigin && !compactionActive && !isSteeringText && !steeringImages && msg.type === "model_msg" && (p.type === "text" && p.role === "user" || p.type === "image_url");
|
|
7541
7997
|
const startsCompactionTurn = !hasOrigin && msg.type === "event_msg" && p.type === "compaction_begin";
|
|
7542
7998
|
if (startsUserTurn || startsCompactionTurn) {
|
|
7543
7999
|
if (pendingFrom === null) pendingFrom = mi;
|
|
@@ -7582,7 +8038,7 @@ var TraceService = class {
|
|
|
7582
8038
|
}
|
|
7583
8039
|
} else if (p.type === "request_end") {
|
|
7584
8040
|
const status = typeof p.status === "string" ? p.status : void 0;
|
|
7585
|
-
const retryable = status === "timeout" || status === "malformed";
|
|
8041
|
+
const retryable = status === "timeout" || status === "malformed" || status === "failed" && !compactionActive;
|
|
7586
8042
|
if (!hasOrigin) {
|
|
7587
8043
|
prevSerialTs = null;
|
|
7588
8044
|
continuation = sawToolCallThisRequest || retryable;
|
|
@@ -7733,11 +8189,11 @@ var TraceService = class {
|
|
|
7733
8189
|
const out = { dates: [] };
|
|
7734
8190
|
for (const date of dates) {
|
|
7735
8191
|
const bySession = /* @__PURE__ */ new Map();
|
|
7736
|
-
for (const file of await listFiles(
|
|
8192
|
+
for (const file of await listFiles(path15.join(dir, date))) {
|
|
7737
8193
|
const match = TRACE_FILE_RE2.exec(file);
|
|
7738
8194
|
if (!match) continue;
|
|
7739
8195
|
const sessionId = match[1];
|
|
7740
|
-
const stat = await
|
|
8196
|
+
const stat = await fs16.stat(path15.join(dir, date, file));
|
|
7741
8197
|
const files = bySession.get(sessionId) ?? [];
|
|
7742
8198
|
files.push({ index: Number(match[2]), sizeBytes: stat.size });
|
|
7743
8199
|
bySession.set(sessionId, files);
|
|
@@ -7773,7 +8229,7 @@ var TraceService = class {
|
|
|
7773
8229
|
/** Raw bytes of the Trace file at the given index (export/download: the file is served verbatim). */
|
|
7774
8230
|
async readFileRaw(projectId, agentId, sessionId, index) {
|
|
7775
8231
|
const file = await this.locateByIndex(projectId, agentId, sessionId, index);
|
|
7776
|
-
return
|
|
8232
|
+
return fs16.readFile(file.path);
|
|
7777
8233
|
}
|
|
7778
8234
|
/**
|
|
7779
8235
|
* Imports an uploaded Trace file (raw JSONL content). Validates the content itself —
|
|
@@ -7812,11 +8268,11 @@ var TraceService = class {
|
|
|
7812
8268
|
const ts = Date.parse(first.timestamp);
|
|
7813
8269
|
const date = formatLocalDate(Number.isNaN(ts) ? /* @__PURE__ */ new Date() : new Date(ts));
|
|
7814
8270
|
const index = 1;
|
|
7815
|
-
const dir =
|
|
7816
|
-
await
|
|
7817
|
-
const file =
|
|
8271
|
+
const dir = path15.join(tracesDir3(this.root, projectId, agentId), date);
|
|
8272
|
+
await fs16.mkdir(dir, { recursive: true });
|
|
8273
|
+
const file = path15.join(dir, `${sessionId}_${String(index).padStart(3, "0")}.jsonl`);
|
|
7818
8274
|
try {
|
|
7819
|
-
await
|
|
8275
|
+
await fs16.writeFile(file, content.replace(/\n+$/, "") + "\n", {
|
|
7820
8276
|
encoding: "utf8",
|
|
7821
8277
|
flag: "wx"
|
|
7822
8278
|
});
|
|
@@ -8015,6 +8471,33 @@ var UsageService = class {
|
|
|
8015
8471
|
models: this.usage.distinctModels(projectId)
|
|
8016
8472
|
};
|
|
8017
8473
|
}
|
|
8474
|
+
/**
|
|
8475
|
+
* One page of the error detail table, newest first. The dashboard's own response already
|
|
8476
|
+
* carries the first page (`errors.recent`); this serves the "show me earlier ones" paging,
|
|
8477
|
+
* where refetching the whole aggregate to move one page would be wasteful. `total` is the
|
|
8478
|
+
* filtered row count, so the caller knows when it has reached the end.
|
|
8479
|
+
*
|
|
8480
|
+
* Takes the same filter the dashboard applies — date + agent, and admin-only visibility of
|
|
8481
|
+
* unattributed errors — so a page never widens what the summary above it counted.
|
|
8482
|
+
*
|
|
8483
|
+
* Paging is by offset into a newest-first table that grows at its head, so errors recorded
|
|
8484
|
+
* between two page requests shift every row down and a page can re-show rows already seen.
|
|
8485
|
+
* Accepted deliberately: this is a diagnostic table read at a moment in time, not a feed to
|
|
8486
|
+
* be walked exhaustively, and keying off the newest row's id would cost a cursor the caller
|
|
8487
|
+
* has no other use for.
|
|
8488
|
+
*/
|
|
8489
|
+
queryErrors(projectId, q) {
|
|
8490
|
+
const f = {
|
|
8491
|
+
...q.agentId !== void 0 ? { agentId: q.agentId } : {},
|
|
8492
|
+
...q.from !== void 0 ? { from: q.from } : {},
|
|
8493
|
+
...q.to !== void 0 ? { to: q.to } : {},
|
|
8494
|
+
...q.includeGlobalErrors === true ? { includeGlobal: true } : {}
|
|
8495
|
+
};
|
|
8496
|
+
return {
|
|
8497
|
+
items: this.errors.recent(projectId, f, q.limit, q.offset),
|
|
8498
|
+
total: this.errors.summary(projectId, f).total
|
|
8499
|
+
};
|
|
8500
|
+
}
|
|
8018
8501
|
/** Error statistics: summary info (total / unexpected / most common error code) + the last N entries, all filtered by the selected range. */
|
|
8019
8502
|
foldErrors(projectId, f) {
|
|
8020
8503
|
const { total, unexpected } = this.errors.summary(projectId, f);
|
|
@@ -8167,7 +8650,7 @@ function buildAppDeps(config2, overrides = {}) {
|
|
|
8167
8650
|
const traceService = new TraceService(config2.root);
|
|
8168
8651
|
const workspaceFiles = new WorkspaceFilesService();
|
|
8169
8652
|
const previewTokens = createPreviewTokenSigner();
|
|
8170
|
-
const benchmarks = new BenchmarkService(config2.root);
|
|
8653
|
+
const benchmarks = new BenchmarkService(config2.root, workspaceFiles);
|
|
8171
8654
|
const snapshots = new SnapshotService(config2.root);
|
|
8172
8655
|
const usageService = new UsageService(
|
|
8173
8656
|
usageRepo,
|
|
@@ -8308,13 +8791,17 @@ function createApp(deps2) {
|
|
|
8308
8791
|
await next();
|
|
8309
8792
|
});
|
|
8310
8793
|
}
|
|
8311
|
-
app2.use(
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8794
|
+
app2.use(
|
|
8795
|
+
"/api/*",
|
|
8796
|
+
bodyLimit({
|
|
8797
|
+
maxSize: MAX_BODY_BYTES,
|
|
8798
|
+
// Its default is a bare text/plain 413; throw the App's own error instead so the response
|
|
8799
|
+
// stays the documented `payload_too_large` body that every client already handles.
|
|
8800
|
+
onError: () => {
|
|
8801
|
+
throw new HttpError(413, "payload_too_large", "Request body exceeds the 20MB limit.");
|
|
8802
|
+
}
|
|
8803
|
+
})
|
|
8804
|
+
);
|
|
8318
8805
|
app2.use("/api/*", jsonOnlyWrites);
|
|
8319
8806
|
app2.route("/api/auth", authRoutes(deps2));
|
|
8320
8807
|
const auth = authMiddleware(deps2.authService);
|
|
@@ -8340,7 +8827,7 @@ function createApp(deps2) {
|
|
|
8340
8827
|
app2.route("/api/projects/:projectId/usage", usageRoutes(deps2));
|
|
8341
8828
|
app2.route("/api/sessions", sessionsRoutes(deps2));
|
|
8342
8829
|
app2.route("/preview", previewRoutes(deps2));
|
|
8343
|
-
if (
|
|
8830
|
+
if (fs17.existsSync(deps2.config.webDist)) {
|
|
8344
8831
|
registerStaticRoutes(app2, deps2.config.webDist);
|
|
8345
8832
|
}
|
|
8346
8833
|
return app2;
|
|
@@ -8376,16 +8863,16 @@ function registerStaticRoutes(app2, webDist) {
|
|
|
8376
8863
|
return c.json(errorBody("not_found", "Endpoint does not exist."), 404);
|
|
8377
8864
|
}
|
|
8378
8865
|
const rel = reqPath.replace(/^\/+/, "");
|
|
8379
|
-
const resolved =
|
|
8380
|
-
const base =
|
|
8381
|
-
const target = resolved === base || resolved.startsWith(base +
|
|
8866
|
+
const resolved = path16.resolve(webDist, rel === "" ? "index.html" : rel);
|
|
8867
|
+
const base = path16.resolve(webDist);
|
|
8868
|
+
const target = resolved === base || resolved.startsWith(base + path16.sep) ? resolved : path16.join(base, "index.html");
|
|
8382
8869
|
let file = target;
|
|
8383
8870
|
try {
|
|
8384
8871
|
const stat = await fsp.stat(file);
|
|
8385
|
-
if (stat.isDirectory()) file =
|
|
8872
|
+
if (stat.isDirectory()) file = path16.join(file, "index.html");
|
|
8386
8873
|
await fsp.access(file);
|
|
8387
8874
|
} catch {
|
|
8388
|
-
file =
|
|
8875
|
+
file = path16.join(base, "index.html");
|
|
8389
8876
|
}
|
|
8390
8877
|
let content;
|
|
8391
8878
|
try {
|
|
@@ -8393,7 +8880,7 @@ function registerStaticRoutes(app2, webDist) {
|
|
|
8393
8880
|
} catch {
|
|
8394
8881
|
return c.json(errorBody("not_found", "Resource does not exist."), 404);
|
|
8395
8882
|
}
|
|
8396
|
-
const type = CONTENT_TYPES2[
|
|
8883
|
+
const type = CONTENT_TYPES2[path16.extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
8397
8884
|
return new Response(new Uint8Array(content), {
|
|
8398
8885
|
status: 200,
|
|
8399
8886
|
headers: { "Content-Type": type }
|
|
@@ -8402,16 +8889,16 @@ function registerStaticRoutes(app2, webDist) {
|
|
|
8402
8889
|
}
|
|
8403
8890
|
|
|
8404
8891
|
// src/config.ts
|
|
8405
|
-
import
|
|
8406
|
-
import
|
|
8892
|
+
import fs18 from "fs";
|
|
8893
|
+
import path17 from "path";
|
|
8407
8894
|
import { fileURLToPath } from "url";
|
|
8408
8895
|
import { DEFAULT_SERVER_PORT, resolveRoot } from "@prismshadow/penguin-core";
|
|
8409
8896
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
8410
8897
|
function defaultWebDist() {
|
|
8411
|
-
const here =
|
|
8412
|
-
const bundled =
|
|
8413
|
-
if (
|
|
8414
|
-
return
|
|
8898
|
+
const here = path17.dirname(fileURLToPath(import.meta.url));
|
|
8899
|
+
const bundled = path17.resolve(here, "..", "web-dist");
|
|
8900
|
+
if (fs18.existsSync(bundled)) return bundled;
|
|
8901
|
+
return path17.resolve(here, "..", "..", "web", "dist");
|
|
8415
8902
|
}
|
|
8416
8903
|
function normalizePreviewOrigin(raw) {
|
|
8417
8904
|
if (!raw || raw.trim() === "") return null;
|
|
@@ -8436,7 +8923,7 @@ function resolveServerConfig(env = process.env) {
|
|
|
8436
8923
|
root,
|
|
8437
8924
|
host: env.HOST ?? "127.0.0.1",
|
|
8438
8925
|
port,
|
|
8439
|
-
dbPath: env.PENGUIN_WEB_DB ??
|
|
8926
|
+
dbPath: env.PENGUIN_WEB_DB ?? path17.join(root, "web.db"),
|
|
8440
8927
|
webDist: env.PENGUIN_WEB_DIST ?? defaultWebDist(),
|
|
8441
8928
|
previewOrigin: normalizePreviewOrigin(env.PENGUIN_PREVIEW_ORIGIN),
|
|
8442
8929
|
authSessionTtlMs: 7 * DAY_MS,
|