@prismshadow/penguin-server 0.1.5 → 0.2.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/api/types.d.ts +38 -4
- package/dist/index.js +235 -75
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/web-dist/assets/index-BOvgZOwz.js +173 -0
- package/web-dist/assets/{index-DibOt1-H.js → index-BgHHHLga.js} +1 -1
- package/web-dist/assets/index-DGdg-i_E.css +1 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-Da8gVBDc.css +0 -1
- package/web-dist/assets/index-nay7sGjx.js +0 -171
package/dist/index.js
CHANGED
|
@@ -55,8 +55,11 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
55
55
|
approval_mode TEXT NOT NULL DEFAULT 'allow-all', -- allow-all|deny-all|read-only|always-ask
|
|
56
56
|
title TEXT, -- auto-generated by the model after the first exchange; NULL=not yet (frontend shows "New chat")
|
|
57
57
|
archived_at TEXT, -- archive time; NULL=not archived (shown by default; archived ones move under "Archived")
|
|
58
|
+
client TEXT, -- creating client: 'web' (created via the Web App) | 'cli' (adopted from a CLI Trace); NULL = legacy row, treated as web
|
|
59
|
+
has_trace INTEGER NOT NULL DEFAULT 0, -- cache: a Trace record exists (set at task start / adoption / subagent registration; lazily backfilled by list hydration)
|
|
58
60
|
created_at TEXT NOT NULL
|
|
59
|
-
); --
|
|
61
|
+
); -- the schedule/subagent SOURCE is NOT stored: session_meta in the Trace is the single source of truth (see runtime/session-sources.ts); "client" is a different, DB-only axis (who created the row)
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_agent_created ON sessions(project_id, agent_id, created_at DESC);
|
|
60
63
|
CREATE TABLE IF NOT EXISTS usage_records (
|
|
61
64
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
62
65
|
ts TEXT NOT NULL,
|
|
@@ -133,8 +136,15 @@ function openDatabase(dbPath) {
|
|
|
133
136
|
db.exec("PRAGMA journal_mode = WAL;");
|
|
134
137
|
db.exec("PRAGMA foreign_keys = ON;");
|
|
135
138
|
db.exec(SCHEMA_SQL);
|
|
139
|
+
ensureColumn(db, "sessions", "client", "TEXT");
|
|
140
|
+
ensureColumn(db, "sessions", "has_trace", "INTEGER NOT NULL DEFAULT 0");
|
|
136
141
|
return db;
|
|
137
142
|
}
|
|
143
|
+
function ensureColumn(db, table, column, ddl) {
|
|
144
|
+
const cols = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
145
|
+
if (cols.some((c) => c.name === column)) return;
|
|
146
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddl}`);
|
|
147
|
+
}
|
|
138
148
|
|
|
139
149
|
// src/db/repos/agents.ts
|
|
140
150
|
var AgentsRepo = class {
|
|
@@ -615,6 +625,8 @@ function mapRow4(r) {
|
|
|
615
625
|
approvalMode: r.approval_mode,
|
|
616
626
|
title: r.title ?? null,
|
|
617
627
|
archivedAt: r.archived_at ?? null,
|
|
628
|
+
client: r.client ?? null,
|
|
629
|
+
hasTrace: r.has_trace === 1,
|
|
618
630
|
createdAt: r.created_at
|
|
619
631
|
};
|
|
620
632
|
}
|
|
@@ -625,8 +637,8 @@ var SessionsRepo = class {
|
|
|
625
637
|
db;
|
|
626
638
|
insert(row) {
|
|
627
639
|
this.db.prepare(
|
|
628
|
-
`INSERT INTO sessions (session_id, project_id, agent_id, provider, model_id, workspace, approval_mode, title, created_at)
|
|
629
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
640
|
+
`INSERT INTO sessions (session_id, project_id, agent_id, provider, model_id, workspace, approval_mode, title, client, has_trace, created_at)
|
|
641
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
630
642
|
).run(
|
|
631
643
|
row.sessionId,
|
|
632
644
|
row.projectId,
|
|
@@ -636,14 +648,16 @@ var SessionsRepo = class {
|
|
|
636
648
|
row.workspace,
|
|
637
649
|
row.approvalMode,
|
|
638
650
|
row.title,
|
|
651
|
+
row.client ?? null,
|
|
652
|
+
row.hasTrace ? 1 : 0,
|
|
639
653
|
row.createdAt
|
|
640
654
|
);
|
|
641
655
|
}
|
|
642
656
|
/** Idempotent insert: used when Trace directory discovery backfills a row (concurrent listing discovering the same Session no longer triggers a UNIQUE violation). */
|
|
643
657
|
insertOrIgnore(row) {
|
|
644
658
|
this.db.prepare(
|
|
645
|
-
`INSERT OR IGNORE INTO sessions (session_id, project_id, agent_id, provider, model_id, workspace, approval_mode, title, created_at)
|
|
646
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
659
|
+
`INSERT OR IGNORE INTO sessions (session_id, project_id, agent_id, provider, model_id, workspace, approval_mode, title, client, has_trace, created_at)
|
|
660
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
647
661
|
).run(
|
|
648
662
|
row.sessionId,
|
|
649
663
|
row.projectId,
|
|
@@ -653,15 +667,30 @@ var SessionsRepo = class {
|
|
|
653
667
|
row.workspace,
|
|
654
668
|
row.approvalMode,
|
|
655
669
|
row.title,
|
|
670
|
+
row.client ?? null,
|
|
671
|
+
row.hasTrace ? 1 : 0,
|
|
656
672
|
row.createdAt
|
|
657
673
|
);
|
|
658
674
|
}
|
|
675
|
+
/** Flip the has_trace cache once a Trace record exists (task start / discovery hydration); idempotent. */
|
|
676
|
+
markHasTrace(sessionId) {
|
|
677
|
+
this.db.prepare("UPDATE sessions SET has_trace = 1 WHERE session_id = ?").run(sessionId);
|
|
678
|
+
}
|
|
659
679
|
findById(sessionId) {
|
|
660
680
|
const r = this.db.prepare("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
|
|
661
681
|
return r ? mapRow4(r) : null;
|
|
662
682
|
}
|
|
663
|
-
|
|
664
|
-
|
|
683
|
+
/**
|
|
684
|
+
* An Agent's rows, newest first (the list order the sidebar shows; served by
|
|
685
|
+
* idx_sessions_agent_created). `webOnly` keeps only web-created rows — NULL counts as
|
|
686
|
+
* web (legacy rows predate the column and the user chose to grandfather them as visible).
|
|
687
|
+
*/
|
|
688
|
+
listByAgent(projectId, agentId, opts = {}) {
|
|
689
|
+
const filter = opts.webOnly ? " AND (client IS NULL OR client = 'web')" : "";
|
|
690
|
+
const rows = this.db.prepare(
|
|
691
|
+
`SELECT * FROM sessions WHERE project_id = ? AND agent_id = ?${filter}
|
|
692
|
+
ORDER BY created_at DESC, session_id DESC`
|
|
693
|
+
).all(projectId, agentId);
|
|
665
694
|
return rows.map(mapRow4);
|
|
666
695
|
}
|
|
667
696
|
listByProject(projectId) {
|
|
@@ -2200,24 +2229,8 @@ async function readItem(deps2, projectId, agentId, name) {
|
|
|
2200
2229
|
// src/http/routes/benchmarks.ts
|
|
2201
2230
|
import { Hono as Hono10 } from "hono";
|
|
2202
2231
|
var TEXT_PREVIEW_BYTES = 256 * 1024;
|
|
2203
|
-
function
|
|
2204
|
-
|
|
2205
|
-
app2.get("/", async (c) => {
|
|
2206
|
-
const projectId = requireValidId(c, "projectId");
|
|
2207
|
-
const agentId = requireValidId(c, "agentId");
|
|
2208
|
-
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
2209
|
-
await deps2.agentConfigService.requireExists(projectId, agentId);
|
|
2210
|
-
return c.json(await deps2.benchmarks.list(projectId, agentId));
|
|
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) => {
|
|
2232
|
+
function listCaseFiles(deps2, material) {
|
|
2233
|
+
return async (c) => {
|
|
2221
2234
|
const projectId = requireValidId(c, "projectId");
|
|
2222
2235
|
const agentId = requireValidId(c, "agentId");
|
|
2223
2236
|
const benchmarkId = requireValidId(c, "benchmarkId");
|
|
@@ -2230,11 +2243,14 @@ function benchmarksRoutes(deps2) {
|
|
|
2230
2243
|
agentId,
|
|
2231
2244
|
benchmarkId,
|
|
2232
2245
|
caseId,
|
|
2233
|
-
c.req.query("path") ?? ""
|
|
2246
|
+
c.req.query("path") ?? "",
|
|
2247
|
+
material
|
|
2234
2248
|
)
|
|
2235
2249
|
);
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
function readCaseFile(deps2, material) {
|
|
2253
|
+
return async (c) => {
|
|
2238
2254
|
const projectId = requireValidId(c, "projectId");
|
|
2239
2255
|
const agentId = requireValidId(c, "agentId");
|
|
2240
2256
|
const benchmarkId = requireValidId(c, "benchmarkId");
|
|
@@ -2249,6 +2265,7 @@ function benchmarksRoutes(deps2) {
|
|
|
2249
2265
|
benchmarkId,
|
|
2250
2266
|
caseId,
|
|
2251
2267
|
c.req.query("path") ?? "",
|
|
2268
|
+
material,
|
|
2252
2269
|
boundedPreview ? { maxBytes: TEXT_PREVIEW_BYTES } : void 0
|
|
2253
2270
|
);
|
|
2254
2271
|
return new Response(new Uint8Array(data), {
|
|
@@ -2260,7 +2277,29 @@ function benchmarksRoutes(deps2) {
|
|
|
2260
2277
|
...truncated ? { "X-Content-Truncated": "1" } : {}
|
|
2261
2278
|
}
|
|
2262
2279
|
});
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
function benchmarksRoutes(deps2) {
|
|
2283
|
+
const app2 = new Hono10();
|
|
2284
|
+
app2.get("/", async (c) => {
|
|
2285
|
+
const projectId = requireValidId(c, "projectId");
|
|
2286
|
+
const agentId = requireValidId(c, "agentId");
|
|
2287
|
+
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
2288
|
+
await deps2.agentConfigService.requireExists(projectId, agentId);
|
|
2289
|
+
return c.json(await deps2.benchmarks.list(projectId, agentId));
|
|
2263
2290
|
});
|
|
2291
|
+
app2.get("/:benchmarkId/cases", async (c) => {
|
|
2292
|
+
const projectId = requireValidId(c, "projectId");
|
|
2293
|
+
const agentId = requireValidId(c, "agentId");
|
|
2294
|
+
const benchmarkId = requireValidId(c, "benchmarkId");
|
|
2295
|
+
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
2296
|
+
await deps2.agentConfigService.requireExists(projectId, agentId);
|
|
2297
|
+
return c.json(await deps2.benchmarks.listCases(projectId, agentId, benchmarkId));
|
|
2298
|
+
});
|
|
2299
|
+
app2.get("/:benchmarkId/cases/:caseId/files", listCaseFiles(deps2, "statement"));
|
|
2300
|
+
app2.get("/:benchmarkId/cases/:caseId/files/content", readCaseFile(deps2, "statement"));
|
|
2301
|
+
app2.get("/:benchmarkId/cases/:caseId/rubric/files", listCaseFiles(deps2, "rubric"));
|
|
2302
|
+
app2.get("/:benchmarkId/cases/:caseId/rubric/files/content", readCaseFile(deps2, "rubric"));
|
|
2264
2303
|
return app2;
|
|
2265
2304
|
}
|
|
2266
2305
|
|
|
@@ -3005,7 +3044,11 @@ var WorkspaceFilesService = class {
|
|
|
3005
3044
|
import fs7 from "fs/promises";
|
|
3006
3045
|
import path6 from "path";
|
|
3007
3046
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
3008
|
-
import {
|
|
3047
|
+
import {
|
|
3048
|
+
appendAttachmentLines,
|
|
3049
|
+
attachedFileLine,
|
|
3050
|
+
modelVisiblePath
|
|
3051
|
+
} from "@prismshadow/penguin-core";
|
|
3009
3052
|
var MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
3010
3053
|
var MAX_ATTACHMENT_COUNT = 20;
|
|
3011
3054
|
var MAX_TOTAL_ATTACHMENT_BYTES = 12 * 1024 * 1024;
|
|
@@ -3029,21 +3072,21 @@ function truncateBytes(value, maxBytes) {
|
|
|
3029
3072
|
return out;
|
|
3030
3073
|
}
|
|
3031
3074
|
var MAX_NAME_ATTEMPTS = 16;
|
|
3032
|
-
function parseAttachmentPart(part, index) {
|
|
3075
|
+
function parseAttachmentPart(part, index, field = "input") {
|
|
3033
3076
|
const fileName = part.fileName;
|
|
3034
3077
|
if (typeof fileName !== "string" || fileName.length === 0 || fileName.includes("/") || fileName.includes("\\") || fileName.includes("..") || fileName.includes("\0")) {
|
|
3035
3078
|
throw badRequest(
|
|
3036
|
-
|
|
3079
|
+
`${field}[${index}].fileName must be a non-empty file name without path separators or "..".`
|
|
3037
3080
|
);
|
|
3038
3081
|
}
|
|
3039
3082
|
const dataUrl = part.dataUrl;
|
|
3040
3083
|
const match = typeof dataUrl === "string" ? /^data:[^,]*;base64,([A-Za-z0-9+/=\s]+)$/.exec(dataUrl) : null;
|
|
3041
3084
|
if (!match) {
|
|
3042
|
-
throw badRequest(
|
|
3085
|
+
throw badRequest(`${field}[${index}].dataUrl must be a base64 data: URL of the file's bytes.`);
|
|
3043
3086
|
}
|
|
3044
3087
|
const bytes = Buffer.from(match[1], "base64");
|
|
3045
3088
|
if (bytes.length === 0) {
|
|
3046
|
-
throw badRequest(
|
|
3089
|
+
throw badRequest(`${field}[${index}].dataUrl decodes to an empty file.`);
|
|
3047
3090
|
}
|
|
3048
3091
|
if (bytes.length > MAX_ATTACHMENT_BYTES) {
|
|
3049
3092
|
throw new HttpError(
|
|
@@ -3125,7 +3168,13 @@ async function attachFilesToInput(messages, attachments, root, sessionId) {
|
|
|
3125
3168
|
await removeAttachments(written);
|
|
3126
3169
|
throw err;
|
|
3127
3170
|
}
|
|
3128
|
-
return {
|
|
3171
|
+
return {
|
|
3172
|
+
input: appendAttachmentLines(
|
|
3173
|
+
messages,
|
|
3174
|
+
written.map((filePath) => attachedFileLine(modelVisiblePath(filePath)))
|
|
3175
|
+
),
|
|
3176
|
+
written
|
|
3177
|
+
};
|
|
3129
3178
|
}
|
|
3130
3179
|
|
|
3131
3180
|
// src/http/routes/sessions.ts
|
|
@@ -3202,6 +3251,20 @@ function parseSteerImages(body) {
|
|
|
3202
3251
|
if (!Array.isArray(images)) throw badRequest("images must be an array.");
|
|
3203
3252
|
return images.map((url, i) => requireImageUrl(url, `images[${i}]`));
|
|
3204
3253
|
}
|
|
3254
|
+
function parseSteerFiles(body) {
|
|
3255
|
+
const files = body.files;
|
|
3256
|
+
if (files === void 0) return [];
|
|
3257
|
+
if (!Array.isArray(files)) throw badRequest("files must be an array.");
|
|
3258
|
+
const attachments = [];
|
|
3259
|
+
files.forEach((item, i) => {
|
|
3260
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
3261
|
+
throw badRequest(`files[${i}] must be an object.`);
|
|
3262
|
+
}
|
|
3263
|
+
attachments.push(parseAttachmentPart(item, i, "files"));
|
|
3264
|
+
assertAttachmentBudget(attachments);
|
|
3265
|
+
});
|
|
3266
|
+
return attachments;
|
|
3267
|
+
}
|
|
3205
3268
|
function parseGoalField(body) {
|
|
3206
3269
|
const goal = body.goal;
|
|
3207
3270
|
if (goal === void 0) return null;
|
|
@@ -3228,13 +3291,16 @@ function agentSessionsRoutes(deps2) {
|
|
|
3228
3291
|
}
|
|
3229
3292
|
const rawCounts = c.req.query("counts");
|
|
3230
3293
|
if (rawCounts !== void 0 && rawCounts !== "1") throw badRequest("counts only accepts 1.");
|
|
3294
|
+
const rawCli = c.req.query("cli");
|
|
3295
|
+
if (rawCli !== void 0 && rawCli !== "1") throw badRequest("cli only accepts 1.");
|
|
3231
3296
|
const { sessions, counts, workspaceCounts } = await deps2.sessionService.listSessions(
|
|
3232
3297
|
projectId,
|
|
3233
3298
|
agentId,
|
|
3234
3299
|
{
|
|
3235
3300
|
...paging ? { paging } : {},
|
|
3236
3301
|
...rawCategory !== void 0 ? { category: rawCategory } : {},
|
|
3237
|
-
...rawCounts !== void 0 ? { withCounts: true } : {}
|
|
3302
|
+
...rawCounts !== void 0 ? { withCounts: true } : {},
|
|
3303
|
+
...rawCli !== void 0 ? { includeCli: true } : {}
|
|
3238
3304
|
}
|
|
3239
3305
|
);
|
|
3240
3306
|
return c.json({
|
|
@@ -3433,11 +3499,15 @@ function sessionsRoutes(deps2) {
|
|
|
3433
3499
|
app2.get("/:sessionId/stream", (c) => {
|
|
3434
3500
|
const row = resolveSession(c);
|
|
3435
3501
|
const channel = deps2.channels.get(row.sessionId);
|
|
3502
|
+
const pendingSteering = deps2.manager.pendingSteeringOf(row.sessionId);
|
|
3436
3503
|
const initialEvents = [
|
|
3437
3504
|
{
|
|
3438
3505
|
type: "task_state",
|
|
3439
3506
|
state: deps2.manager.statusOf(row.sessionId),
|
|
3440
|
-
queued: deps2.manager.pendingFollowUpCount(row.sessionId)
|
|
3507
|
+
queued: deps2.manager.pendingFollowUpCount(row.sessionId),
|
|
3508
|
+
// Undelivered steering rides the snapshot too, so the composer's "steering queued"
|
|
3509
|
+
// hint (and what it says) survives a reload.
|
|
3510
|
+
...pendingSteering.length > 0 ? { pendingSteering } : {}
|
|
3441
3511
|
},
|
|
3442
3512
|
...deps2.manager.pendingApprovals(row.sessionId).map((p) => ({
|
|
3443
3513
|
type: "approval_request",
|
|
@@ -3493,13 +3563,26 @@ function sessionsRoutes(deps2) {
|
|
|
3493
3563
|
const body = await readJson(c);
|
|
3494
3564
|
const text = typeof body.text === "string" ? body.text.trim() : "";
|
|
3495
3565
|
const images = parseSteerImages(body);
|
|
3496
|
-
|
|
3497
|
-
|
|
3566
|
+
const files = parseSteerFiles(body);
|
|
3567
|
+
if (!text && images.length === 0 && files.length === 0) {
|
|
3568
|
+
throw badRequest("text, images or files must carry the steering message.");
|
|
3569
|
+
}
|
|
3570
|
+
const { input, written } = await attachFilesToInput(
|
|
3571
|
+
[...text ? [userText(text)] : [], ...images.map((url) => imageUrlMessage(url))],
|
|
3572
|
+
files,
|
|
3573
|
+
scratchpadDir(deps2.config.root, row.projectId, row.agentId),
|
|
3574
|
+
row.sessionId
|
|
3575
|
+
);
|
|
3576
|
+
try {
|
|
3577
|
+
deps2.manager.steer(row.sessionId, input, {
|
|
3578
|
+
text,
|
|
3579
|
+
images: images.length,
|
|
3580
|
+
files: files.length
|
|
3581
|
+
});
|
|
3582
|
+
} catch (err) {
|
|
3583
|
+
await removeAttachments(written);
|
|
3584
|
+
throw err;
|
|
3498
3585
|
}
|
|
3499
|
-
deps2.manager.steer(row.sessionId, [
|
|
3500
|
-
...text ? [userText(text)] : [],
|
|
3501
|
-
...images.map((url) => imageUrlMessage(url))
|
|
3502
|
-
]);
|
|
3503
3586
|
return c.body(null, 202);
|
|
3504
3587
|
});
|
|
3505
3588
|
app2.get("/:sessionId/goal", (c) => {
|
|
@@ -3962,6 +4045,7 @@ import {
|
|
|
3962
4045
|
goalTokenDelta,
|
|
3963
4046
|
isGoalRoundInput,
|
|
3964
4047
|
isSessionMeta as isSessionMeta2,
|
|
4048
|
+
parseUserSteeringText,
|
|
3965
4049
|
stripLeadingMarkerBlocks,
|
|
3966
4050
|
tracesDir
|
|
3967
4051
|
} from "@prismshadow/penguin-core";
|
|
@@ -4492,6 +4576,11 @@ var EARLY_TITLE_BODY_CHARS = 1e3;
|
|
|
4492
4576
|
function agentKey(projectId, agentId) {
|
|
4493
4577
|
return `${projectId}\0${agentId}`;
|
|
4494
4578
|
}
|
|
4579
|
+
function isDeliveredSteering(msg) {
|
|
4580
|
+
const p = msg.payload;
|
|
4581
|
+
if (msg.type !== "model_msg" || p.type !== "text" || p.role !== "user") return false;
|
|
4582
|
+
return typeof p.text === "string" && parseUserSteeringText(p.text) !== null;
|
|
4583
|
+
}
|
|
4495
4584
|
function runSubagentCall(msg) {
|
|
4496
4585
|
const p = msg.payload;
|
|
4497
4586
|
if (msg.type !== "model_msg" || p.type !== "tool_call" || p.name !== "run_subagent") return null;
|
|
@@ -4564,6 +4653,10 @@ var SessionManager = class {
|
|
|
4564
4653
|
pendingFollowUpCount(sessionId) {
|
|
4565
4654
|
return this.entries.get(sessionId)?.followUps.length ?? 0;
|
|
4566
4655
|
}
|
|
4656
|
+
/** Steering messages queued but not yet delivered to the model (display mirror; see RuntimeEntry.pendingSteering). */
|
|
4657
|
+
pendingSteeringOf(sessionId) {
|
|
4658
|
+
return this.entries.get(sessionId)?.pendingSteering ?? [];
|
|
4659
|
+
}
|
|
4567
4660
|
/**
|
|
4568
4661
|
* Live tail of a running session: one synthetic `partial_* start` OmniMessage per open
|
|
4569
4662
|
* streaming fragment, carrying the full accumulated content so far (see live-tail.ts).
|
|
@@ -4596,6 +4689,7 @@ var SessionManager = class {
|
|
|
4596
4689
|
running: null,
|
|
4597
4690
|
generation: this.generationOf(row.projectId, row.agentId),
|
|
4598
4691
|
followUps: [],
|
|
4692
|
+
pendingSteering: [],
|
|
4599
4693
|
lastActivityMs: Date.now()
|
|
4600
4694
|
});
|
|
4601
4695
|
}
|
|
@@ -4889,8 +4983,13 @@ var SessionManager = class {
|
|
|
4889
4983
|
* publishes). 409 when the Session isn't running a Task (idle / compacting / not loaded)
|
|
4890
4984
|
* or the run finished in the race window — the caller falls back to submitting a normal
|
|
4891
4985
|
* task, which carries the same text and images.
|
|
4986
|
+
*
|
|
4987
|
+
* `info` is the queued message's display summary: mirrored on the entry and broadcast via
|
|
4988
|
+
* `task_state` (and the SSE subscribe snapshot) until the delivered `[user_steering]`
|
|
4989
|
+
* message is observed on the stream — that is what keeps the composer's "steering queued"
|
|
4990
|
+
* hint, content included, alive across reloads.
|
|
4892
4991
|
*/
|
|
4893
|
-
steer(sessionId, input) {
|
|
4992
|
+
steer(sessionId, input, info) {
|
|
4894
4993
|
const entry = this.entries.get(sessionId);
|
|
4895
4994
|
if (!entry || entry.status !== "running" || !entry.session.steer(input)) {
|
|
4896
4995
|
throw new HttpError(
|
|
@@ -4899,7 +4998,9 @@ var SessionManager = class {
|
|
|
4899
4998
|
"This Session has no Task in progress; send the message as a new task instead."
|
|
4900
4999
|
);
|
|
4901
5000
|
}
|
|
5001
|
+
entry.pendingSteering.push(info);
|
|
4902
5002
|
entry.lastActivityMs = Date.now();
|
|
5003
|
+
this.publishState(entry, entry.status);
|
|
4903
5004
|
}
|
|
4904
5005
|
/**
|
|
4905
5006
|
* "Retry now" on the reconnect countdown: skip the in-progress backoff wait and fire
|
|
@@ -5114,6 +5215,7 @@ var SessionManager = class {
|
|
|
5114
5215
|
running: null,
|
|
5115
5216
|
generation,
|
|
5116
5217
|
followUps: [],
|
|
5218
|
+
pendingSteering: [],
|
|
5117
5219
|
lastActivityMs: Date.now()
|
|
5118
5220
|
};
|
|
5119
5221
|
this.entries.set(currentId, entry);
|
|
@@ -5126,6 +5228,7 @@ var SessionManager = class {
|
|
|
5126
5228
|
* a title): it collects model text for automatic title generation.
|
|
5127
5229
|
*/
|
|
5128
5230
|
async drive(entry, gen, titleSource) {
|
|
5231
|
+
this.deps.sessions.markHasTrace(entry.sessionId);
|
|
5129
5232
|
let earlyTitleFired = false;
|
|
5130
5233
|
let mainBodyChars = 0;
|
|
5131
5234
|
const ctx = {
|
|
@@ -5151,6 +5254,10 @@ var SessionManager = class {
|
|
|
5151
5254
|
if (denied) subagentPrompts.delete(denied);
|
|
5152
5255
|
const settled = settledToolCallId(msg);
|
|
5153
5256
|
if (settled) subagentPrompts.delete(settled);
|
|
5257
|
+
if (entry.pendingSteering.length > 0 && isDeliveredSteering(msg)) {
|
|
5258
|
+
entry.pendingSteering.shift();
|
|
5259
|
+
this.publishState(entry, entry.status);
|
|
5260
|
+
}
|
|
5154
5261
|
} else if (isSessionMeta2(msg)) {
|
|
5155
5262
|
try {
|
|
5156
5263
|
const child = this.registerChildSession(entry, msg, children);
|
|
@@ -5213,6 +5320,7 @@ var SessionManager = class {
|
|
|
5213
5320
|
entry.status = "idle";
|
|
5214
5321
|
entry.abort = null;
|
|
5215
5322
|
entry.running = null;
|
|
5323
|
+
entry.pendingSteering = [];
|
|
5216
5324
|
entry.lastActivityMs = Date.now();
|
|
5217
5325
|
this.publishState(entry, "idle");
|
|
5218
5326
|
if (titleSource && titleSource.userExcerpt.trim()) {
|
|
@@ -5271,6 +5379,8 @@ var SessionManager = class {
|
|
|
5271
5379
|
// inserted with defaults (matches the convention for Sessions discovered by the CLI).
|
|
5272
5380
|
approvalMode: "allow-all",
|
|
5273
5381
|
title: null,
|
|
5382
|
+
// Spawned by this server's run (client NULL = web); its Trace exists by construction.
|
|
5383
|
+
hasTrace: true,
|
|
5274
5384
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5275
5385
|
});
|
|
5276
5386
|
this.publishEvent(entry, {
|
|
@@ -5291,7 +5401,12 @@ var SessionManager = class {
|
|
|
5291
5401
|
return child;
|
|
5292
5402
|
}
|
|
5293
5403
|
publishState(entry, state) {
|
|
5294
|
-
this.publishEvent(entry, {
|
|
5404
|
+
this.publishEvent(entry, {
|
|
5405
|
+
type: "task_state",
|
|
5406
|
+
state,
|
|
5407
|
+
queued: entry.followUps.length,
|
|
5408
|
+
...entry.pendingSteering.length > 0 ? { pendingSteering: entry.pendingSteering } : {}
|
|
5409
|
+
});
|
|
5295
5410
|
}
|
|
5296
5411
|
publishEvent(entry, event) {
|
|
5297
5412
|
this.deps.channels.get(entry.sessionId).publish(event, "server_event");
|
|
@@ -6888,7 +7003,13 @@ var BenchmarkService = class {
|
|
|
6888
7003
|
for (const entry of entries.filter((item) => item.isDirectory() && item.name.startsWith("CASE-")).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
6889
7004
|
const fallback = { id: entry.name, title: entry.name };
|
|
6890
7005
|
try {
|
|
6891
|
-
const statementDir = await this.
|
|
7006
|
+
const statementDir = await this.caseMaterialRoot(
|
|
7007
|
+
projectId,
|
|
7008
|
+
agentId,
|
|
7009
|
+
benchmarkId,
|
|
7010
|
+
entry.name,
|
|
7011
|
+
"statement"
|
|
7012
|
+
);
|
|
6892
7013
|
const realReadme = await fs13.realpath(path12.join(statementDir, "README.md"));
|
|
6893
7014
|
if (!isWithin(statementDir, realReadme)) throw new Error("README escapes Statement");
|
|
6894
7015
|
cases.push({
|
|
@@ -6901,30 +7022,42 @@ var BenchmarkService = class {
|
|
|
6901
7022
|
}
|
|
6902
7023
|
return { cases };
|
|
6903
7024
|
}
|
|
6904
|
-
async listCaseFiles(projectId, agentId, benchmarkId, caseId, rel) {
|
|
6905
|
-
const
|
|
6906
|
-
|
|
7025
|
+
async listCaseFiles(projectId, agentId, benchmarkId, caseId, rel, material) {
|
|
7026
|
+
const materialRoot = await this.caseMaterialRoot(
|
|
7027
|
+
projectId,
|
|
7028
|
+
agentId,
|
|
7029
|
+
benchmarkId,
|
|
7030
|
+
caseId,
|
|
7031
|
+
material
|
|
7032
|
+
);
|
|
7033
|
+
return this.workspaceFiles.list(materialRoot, rel);
|
|
6907
7034
|
}
|
|
6908
|
-
async readCaseFile(projectId, agentId, benchmarkId, caseId, rel, options) {
|
|
6909
|
-
const
|
|
6910
|
-
|
|
7035
|
+
async readCaseFile(projectId, agentId, benchmarkId, caseId, rel, material, options) {
|
|
7036
|
+
const materialRoot = await this.caseMaterialRoot(
|
|
7037
|
+
projectId,
|
|
7038
|
+
agentId,
|
|
7039
|
+
benchmarkId,
|
|
7040
|
+
caseId,
|
|
7041
|
+
material
|
|
7042
|
+
);
|
|
7043
|
+
return this.workspaceFiles.read(materialRoot, rel, options);
|
|
6911
7044
|
}
|
|
6912
|
-
async
|
|
7045
|
+
async caseMaterialRoot(projectId, agentId, benchmarkId, caseId, material) {
|
|
6913
7046
|
const benchDir = path12.join(benchmarksDir(this.root, projectId, agentId), benchmarkId);
|
|
6914
7047
|
const caseDir = path12.join(benchDir, caseId);
|
|
6915
|
-
const
|
|
7048
|
+
const materialRoot = path12.join(caseDir, material);
|
|
6916
7049
|
try {
|
|
6917
|
-
const [realBenchDir, realCaseDir,
|
|
7050
|
+
const [realBenchDir, realCaseDir, realMaterialRoot] = await Promise.all([
|
|
6918
7051
|
fs13.realpath(benchDir),
|
|
6919
7052
|
fs13.realpath(caseDir),
|
|
6920
|
-
fs13.realpath(
|
|
7053
|
+
fs13.realpath(materialRoot)
|
|
6921
7054
|
]);
|
|
6922
|
-
if (!isWithin(realBenchDir, realCaseDir) || path12.dirname(
|
|
6923
|
-
throw new Error("
|
|
7055
|
+
if (!isWithin(realBenchDir, realCaseDir) || path12.dirname(realMaterialRoot) !== realCaseDir || path12.basename(realMaterialRoot) !== material) {
|
|
7056
|
+
throw new Error("Case material is not canonical");
|
|
6924
7057
|
}
|
|
6925
|
-
return
|
|
7058
|
+
return realMaterialRoot;
|
|
6926
7059
|
} catch {
|
|
6927
|
-
throw new HttpError(404, "not_found",
|
|
7060
|
+
throw new HttpError(404, "not_found", `Case ${material} does not exist.`);
|
|
6928
7061
|
}
|
|
6929
7062
|
}
|
|
6930
7063
|
async readBenchmark(benchDir, id) {
|
|
@@ -7524,11 +7657,19 @@ var SessionService = class {
|
|
|
7524
7657
|
return await this.sourceOf(row, hasTrace, traces) ?? "active";
|
|
7525
7658
|
}
|
|
7526
7659
|
/**
|
|
7527
|
-
* List
|
|
7528
|
-
* `
|
|
7529
|
-
*
|
|
7530
|
-
*
|
|
7531
|
-
*
|
|
7660
|
+
* List, sorted by createdAt descending. The default serves **web sessions straight from
|
|
7661
|
+
* the DB** (`client` web/NULL rows) with no Trace directory scanning — the answer to
|
|
7662
|
+
* many-session sidebar reloads re-walking the filesystem on every request (#139). One
|
|
7663
|
+
* lazy discovery walk still runs for a list call that contains rows this process has not
|
|
7664
|
+
* classified yet (no in-process source entry): it supplies the Trace locations for the
|
|
7665
|
+
* one-time head reads and backfills the `has_trace` cache; once every row is classified,
|
|
7666
|
+
* list calls touch only the DB. `includeCli` widens the list to DB ∪ Trace directory
|
|
7667
|
+
* discovery (adopting unmanaged CLI Traces as `client: "cli"` rows), which inherently
|
|
7668
|
+
* scans — that path is opt-in via the "show CLI sessions" preference.
|
|
7669
|
+
*
|
|
7670
|
+
* Optional `paging` returns just that slice (the sidebar pages with limit+1 to detect
|
|
7671
|
+
* "has more"); slicing happens before toInfo, so per-request source derivation (lazy
|
|
7672
|
+
* Trace-head reads) stays bounded by the page size.
|
|
7532
7673
|
*
|
|
7533
7674
|
* `category` filters to one sidebar bucket **before** paging, so offset/limit page
|
|
7534
7675
|
* within the category. Filtering needs each walked row's category (a possible
|
|
@@ -7539,20 +7680,32 @@ var SessionService = class {
|
|
|
7539
7680
|
* folders (and a workspace group can know its own share) without loading them.
|
|
7540
7681
|
*/
|
|
7541
7682
|
async listSessions(projectId, agentId, opts = {}) {
|
|
7542
|
-
const { paging, category, withCounts } = opts;
|
|
7543
|
-
const traces = await this.discoverTraces(projectId, agentId);
|
|
7683
|
+
const { paging, category, withCounts, includeCli } = opts;
|
|
7544
7684
|
const rows = new Map(
|
|
7545
|
-
this.deps.sessions.listByAgent(projectId, agentId).map((r) => [r.sessionId, r])
|
|
7685
|
+
this.deps.sessions.listByAgent(projectId, agentId, { webOnly: !includeCli }).map((r) => [r.sessionId, r])
|
|
7546
7686
|
);
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7687
|
+
let traces;
|
|
7688
|
+
if (includeCli) {
|
|
7689
|
+
traces = await this.discoverTraces(projectId, agentId);
|
|
7690
|
+
for (const [sessionId, location] of traces) {
|
|
7691
|
+
if (rows.has(sessionId)) continue;
|
|
7692
|
+
const discovered = await this.adoptTraceSession(projectId, agentId, sessionId, location);
|
|
7693
|
+
if (discovered) rows.set(sessionId, discovered);
|
|
7694
|
+
}
|
|
7695
|
+
} else if ([...rows.values()].some((r) => this.deps.sources.get(r.sessionId) === void 0)) {
|
|
7696
|
+
traces = await this.discoverTraces(projectId, agentId);
|
|
7697
|
+
for (const row of rows.values()) {
|
|
7698
|
+
if (!row.hasTrace && traces.has(row.sessionId)) {
|
|
7699
|
+
row.hasTrace = true;
|
|
7700
|
+
this.deps.sessions.markHasTrace(row.sessionId);
|
|
7701
|
+
}
|
|
7702
|
+
}
|
|
7551
7703
|
}
|
|
7552
7704
|
const sorted = [...rows.values()].sort(
|
|
7553
7705
|
(a, b) => b.createdAt.localeCompare(a.createdAt) || b.sessionId.localeCompare(a.sessionId)
|
|
7554
7706
|
);
|
|
7555
|
-
const
|
|
7707
|
+
const rowHasTrace = (row) => traces ? traces.has(row.sessionId) : row.hasTrace === true;
|
|
7708
|
+
const toPage = (page) => Promise.all(page.map((row) => this.toInfo(row, rowHasTrace(row), traces)));
|
|
7556
7709
|
if (category === void 0 && !withCounts) {
|
|
7557
7710
|
return {
|
|
7558
7711
|
sessions: await toPage(
|
|
@@ -7566,7 +7719,7 @@ var SessionService = class {
|
|
|
7566
7719
|
const matched = [];
|
|
7567
7720
|
for (const row of sorted) {
|
|
7568
7721
|
if (!withCounts && matched.length >= want) break;
|
|
7569
|
-
const cat = await this.categoryOf(row,
|
|
7722
|
+
const cat = await this.categoryOf(row, rowHasTrace(row), traces);
|
|
7570
7723
|
counts[cat] += 1;
|
|
7571
7724
|
if (withCounts) {
|
|
7572
7725
|
const ws = workspaceCounts[row.workspace] ??= {
|
|
@@ -7686,6 +7839,9 @@ var SessionService = class {
|
|
|
7686
7839
|
workspace: session.workspaceDir,
|
|
7687
7840
|
approvalMode: args.approvalMode ?? "allow-all",
|
|
7688
7841
|
title: null,
|
|
7842
|
+
// Everything created through this service is the Web App's (schedule runs included);
|
|
7843
|
+
// "cli" is stamped only by Trace adoption. NULL means a legacy row, treated as web.
|
|
7844
|
+
client: "web",
|
|
7689
7845
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7690
7846
|
};
|
|
7691
7847
|
this.deps.sessions.insert(row);
|
|
@@ -7758,6 +7914,10 @@ var SessionService = class {
|
|
|
7758
7914
|
// The approval mode for an unmanaged Session (started via the CLI) isn't in the Trace, so it's backfilled with the default value.
|
|
7759
7915
|
approvalMode: "allow-all",
|
|
7760
7916
|
title: null,
|
|
7917
|
+
// Adopted = a Trace this server never created, i.e. the CLI's: the default list
|
|
7918
|
+
// (web-only) excludes these rows; the "show CLI sessions" preference includes them.
|
|
7919
|
+
client: "cli",
|
|
7920
|
+
hasTrace: true,
|
|
7761
7921
|
createdAt: sessionIdCreatedAt(sessionId) ?? meta.timestamp
|
|
7762
7922
|
};
|
|
7763
7923
|
this.deps.sessions.insertOrIgnore(row);
|
|
@@ -7792,7 +7952,7 @@ import {
|
|
|
7792
7952
|
agentsDir as agentsDir3,
|
|
7793
7953
|
isSessionMeta as isSessionMeta5,
|
|
7794
7954
|
parseTraceLines as parseTraceLines2,
|
|
7795
|
-
parseUserSteeringText,
|
|
7955
|
+
parseUserSteeringText as parseUserSteeringText2,
|
|
7796
7956
|
readTraceTolerant as readTraceTolerant2,
|
|
7797
7957
|
tracesDir as tracesDir3
|
|
7798
7958
|
} from "@prismshadow/penguin-core";
|
|
@@ -7988,7 +8148,7 @@ var TraceService = class {
|
|
|
7988
8148
|
const msg = messages[mi];
|
|
7989
8149
|
const p = msg.payload;
|
|
7990
8150
|
const hasOrigin = msg.origin !== void 0 && msg.origin.length > 0;
|
|
7991
|
-
const isSteeringText = !hasOrigin && msg.type === "model_msg" && p.type === "text" && p.role === "user" && typeof p.text === "string" &&
|
|
8151
|
+
const isSteeringText = !hasOrigin && msg.type === "model_msg" && p.type === "text" && p.role === "user" && typeof p.text === "string" && parseUserSteeringText2(p.text) !== null;
|
|
7992
8152
|
if (isSteeringText) continuation = true;
|
|
7993
8153
|
const isImage = !hasOrigin && msg.type === "model_msg" && p.type === "image_url";
|
|
7994
8154
|
if (!hasOrigin && !isSteeringText && !(isImage && steeringImages)) steeringImages = false;
|