@prismshadow/penguin-server 0.1.0 → 0.1.1
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 +10 -1
- package/dist/index.js +147 -38
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/web-dist/assets/index-C9EZP9d7.js +101 -0
- package/web-dist/assets/{index-B-18IFfR.js → index-CukW1Teq.js} +1 -1
- package/web-dist/assets/index-XqmoUN63.css +1 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-BfFhfbzq.css +0 -1
- package/web-dist/assets/index-BhliHHUc.js +0 -101
package/dist/api/types.d.ts
CHANGED
|
@@ -159,6 +159,13 @@ interface ModelInfo {
|
|
|
159
159
|
* unset (= treated as supported).
|
|
160
160
|
*/
|
|
161
161
|
vision?: boolean;
|
|
162
|
+
/**
|
|
163
|
+
* Per-model max output tokens (TOML `max_tokens` annotation; user-only, never preset by the
|
|
164
|
+
* built-in catalog): when set it wins over the Agent's `system_config.model.max_tokens`;
|
|
165
|
+
* unset = inherit the Agent value. Lets a small-context model cap its output below the
|
|
166
|
+
* seeded per-Agent default (32000), which cannot fit into e.g. a 32k context window.
|
|
167
|
+
*/
|
|
168
|
+
maxTokens?: number;
|
|
162
169
|
pricing?: ModelPricingDto;
|
|
163
170
|
/** Environment variable name to fall back to when api_key is empty (e.g. ANTHROPIC_API_KEY); unset if no known fallback. */
|
|
164
171
|
envKey?: string;
|
|
@@ -192,6 +199,8 @@ interface ModelUpdateEntry {
|
|
|
192
199
|
clientType?: string;
|
|
193
200
|
/** Whether image input (vision/multimodal) is supported; omitted = supported (not persisted). */
|
|
194
201
|
vision?: boolean;
|
|
202
|
+
/** Per-model max output tokens, a positive integer (wins over the Agent config); omitted = inherit the Agent value (the annotation is cleared). */
|
|
203
|
+
maxTokens?: number;
|
|
195
204
|
pricing?: ModelPricingDto;
|
|
196
205
|
/** Providing it overwrites and updates createdAt; omitting it keeps the existing value. */
|
|
197
206
|
apiKey?: string;
|
|
@@ -367,7 +376,7 @@ interface SessionInfo {
|
|
|
367
376
|
approvalMode: ApprovalMode;
|
|
368
377
|
/** Short title auto-generated by the model after the first turn; unset until generated (frontend shows "New Chat"). */
|
|
369
378
|
title?: string;
|
|
370
|
-
/** Session source (for list badges); unset for user-created sessions. */
|
|
379
|
+
/** Session source (for list badges/folders), derived from core session_meta — the single source of truth (not stored in the DB); unset for user-created sessions. */
|
|
371
380
|
source?: SessionSource;
|
|
372
381
|
createdAt: string;
|
|
373
382
|
status: SessionStatus;
|
package/dist/index.js
CHANGED
|
@@ -54,9 +54,8 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
54
54
|
approval_mode TEXT NOT NULL DEFAULT 'allow-all', -- allow-all|deny-all|read-only|always-ask
|
|
55
55
|
title TEXT, -- auto-generated by the model after the first exchange; NULL=not yet (frontend shows "New chat")
|
|
56
56
|
archived_at TEXT, -- archive time; NULL=not archived (shown by default; archived ones move under "Archived")
|
|
57
|
-
source TEXT, -- session origin: NULL=user-created | schedule (scheduled task) | subagent (child session)
|
|
58
57
|
created_at TEXT NOT NULL
|
|
59
|
-
);
|
|
58
|
+
); -- session origin is NOT stored: session_meta in the Trace is the single source of truth (see runtime/session-sources.ts)
|
|
60
59
|
CREATE TABLE IF NOT EXISTS usage_records (
|
|
61
60
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
62
61
|
ts TEXT NOT NULL,
|
|
@@ -525,7 +524,6 @@ function mapRow3(r) {
|
|
|
525
524
|
approvalMode: r.approval_mode,
|
|
526
525
|
title: r.title ?? null,
|
|
527
526
|
archivedAt: r.archived_at ?? null,
|
|
528
|
-
source: r.source ?? null,
|
|
529
527
|
createdAt: r.created_at
|
|
530
528
|
};
|
|
531
529
|
}
|
|
@@ -536,8 +534,8 @@ var SessionsRepo = class {
|
|
|
536
534
|
db;
|
|
537
535
|
insert(row) {
|
|
538
536
|
this.db.prepare(
|
|
539
|
-
`INSERT INTO sessions (session_id, project_id, agent_id, provider, model_id, workspace, approval_mode, title,
|
|
540
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
537
|
+
`INSERT INTO sessions (session_id, project_id, agent_id, provider, model_id, workspace, approval_mode, title, created_at)
|
|
538
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
541
539
|
).run(
|
|
542
540
|
row.sessionId,
|
|
543
541
|
row.projectId,
|
|
@@ -547,15 +545,14 @@ var SessionsRepo = class {
|
|
|
547
545
|
row.workspace,
|
|
548
546
|
row.approvalMode,
|
|
549
547
|
row.title,
|
|
550
|
-
row.source ?? null,
|
|
551
548
|
row.createdAt
|
|
552
549
|
);
|
|
553
550
|
}
|
|
554
551
|
/** Idempotent insert: used when Trace directory discovery backfills a row (concurrent listing discovering the same Session no longer triggers a UNIQUE violation). */
|
|
555
552
|
insertOrIgnore(row) {
|
|
556
553
|
this.db.prepare(
|
|
557
|
-
`INSERT OR IGNORE INTO sessions (session_id, project_id, agent_id, provider, model_id, workspace, approval_mode, title,
|
|
558
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
554
|
+
`INSERT OR IGNORE INTO sessions (session_id, project_id, agent_id, provider, model_id, workspace, approval_mode, title, created_at)
|
|
555
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
559
556
|
).run(
|
|
560
557
|
row.sessionId,
|
|
561
558
|
row.projectId,
|
|
@@ -565,7 +562,6 @@ var SessionsRepo = class {
|
|
|
565
562
|
row.workspace,
|
|
566
563
|
row.approvalMode,
|
|
567
564
|
row.title,
|
|
568
|
-
row.source ?? null,
|
|
569
565
|
row.createdAt
|
|
570
566
|
);
|
|
571
567
|
}
|
|
@@ -1073,8 +1069,10 @@ function requireValidId(c, name) {
|
|
|
1073
1069
|
return v;
|
|
1074
1070
|
}
|
|
1075
1071
|
function positiveIntParam(c, name) {
|
|
1076
|
-
const
|
|
1077
|
-
if (
|
|
1072
|
+
const raw = pathParam(c, name);
|
|
1073
|
+
if (!/^\d+$/.test(raw)) throw badRequest(`${name} must be a positive integer.`);
|
|
1074
|
+
const v = Number.parseInt(raw, 10);
|
|
1075
|
+
if (!Number.isSafeInteger(v) || v < 1) throw badRequest(`${name} must be a positive integer.`);
|
|
1078
1076
|
return v;
|
|
1079
1077
|
}
|
|
1080
1078
|
function paginationQuery(c) {
|
|
@@ -1087,6 +1085,21 @@ function paginationQuery(c) {
|
|
|
1087
1085
|
}
|
|
1088
1086
|
return { offset, limit };
|
|
1089
1087
|
}
|
|
1088
|
+
function optionalPagingQuery(c) {
|
|
1089
|
+
const rawLimit = c.req.query("limit");
|
|
1090
|
+
const rawOffset = c.req.query("offset");
|
|
1091
|
+
if (rawLimit === void 0 && rawOffset === void 0) return null;
|
|
1092
|
+
if (rawLimit === void 0) throw badRequest("offset requires limit.");
|
|
1093
|
+
const limit = Number.parseInt(rawLimit, 10);
|
|
1094
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 1e3) {
|
|
1095
|
+
throw badRequest("limit must be an integer between 1 and 1000.");
|
|
1096
|
+
}
|
|
1097
|
+
const offset = Number.parseInt(rawOffset ?? "0", 10);
|
|
1098
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
1099
|
+
throw badRequest("offset must be a non-negative integer.");
|
|
1100
|
+
}
|
|
1101
|
+
return { offset, limit };
|
|
1102
|
+
}
|
|
1090
1103
|
async function readJson(c) {
|
|
1091
1104
|
let body;
|
|
1092
1105
|
try {
|
|
@@ -1143,10 +1156,15 @@ function optionalNumber(obj, key, rule = {}) {
|
|
|
1143
1156
|
}
|
|
1144
1157
|
function optionalDateParam(value, label) {
|
|
1145
1158
|
if (value === void 0 || value === "") return void 0;
|
|
1146
|
-
|
|
1147
|
-
|
|
1159
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
1160
|
+
if (m) {
|
|
1161
|
+
const [, y, mo, d] = m;
|
|
1162
|
+
const dt = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
1163
|
+
if (!Number.isNaN(dt.getTime()) && dt.getUTCFullYear() === Number(y) && dt.getUTCMonth() + 1 === Number(mo) && dt.getUTCDate() === Number(d)) {
|
|
1164
|
+
return value;
|
|
1165
|
+
}
|
|
1148
1166
|
}
|
|
1149
|
-
|
|
1167
|
+
throw badRequest(`${label} must be a valid date in YYYY-MM-DD format.`);
|
|
1150
1168
|
}
|
|
1151
1169
|
|
|
1152
1170
|
// src/http/routes/admin.ts
|
|
@@ -1445,6 +1463,12 @@ function parseModelsUpdate(body) {
|
|
|
1445
1463
|
}
|
|
1446
1464
|
entry.vision = m.vision;
|
|
1447
1465
|
}
|
|
1466
|
+
if (m.maxTokens !== void 0) {
|
|
1467
|
+
if (typeof m.maxTokens !== "number" || !Number.isInteger(m.maxTokens) || m.maxTokens <= 0) {
|
|
1468
|
+
throw badRequest(`models[${i}].maxTokens must be a positive integer.`);
|
|
1469
|
+
}
|
|
1470
|
+
entry.maxTokens = m.maxTokens;
|
|
1471
|
+
}
|
|
1448
1472
|
if (m.pricing !== void 0) {
|
|
1449
1473
|
const p = m.pricing;
|
|
1450
1474
|
if (p === null || typeof p !== "object" || Array.isArray(p)) {
|
|
@@ -2656,7 +2680,12 @@ function agentSessionsRoutes(deps2) {
|
|
|
2656
2680
|
const agentId = requireValidId(c, "agentId");
|
|
2657
2681
|
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
2658
2682
|
await deps2.agentConfigService.requireExists(projectId, agentId);
|
|
2659
|
-
const
|
|
2683
|
+
const paging = optionalPagingQuery(c);
|
|
2684
|
+
const sessions = await deps2.sessionService.listSessions(
|
|
2685
|
+
projectId,
|
|
2686
|
+
agentId,
|
|
2687
|
+
...paging ? [paging] : []
|
|
2688
|
+
);
|
|
2660
2689
|
return c.json({ sessions });
|
|
2661
2690
|
});
|
|
2662
2691
|
app2.post("/", async (c) => {
|
|
@@ -2715,7 +2744,9 @@ function sessionsRoutes(deps2) {
|
|
|
2715
2744
|
app2.get("/:sessionId", async (c) => {
|
|
2716
2745
|
const row = resolveSession(c);
|
|
2717
2746
|
const hasTrace = await deps2.sessionService.hasTrace(row);
|
|
2718
|
-
return c.json({
|
|
2747
|
+
return c.json({
|
|
2748
|
+
session: await deps2.sessionService.toInfo(row, hasTrace)
|
|
2749
|
+
});
|
|
2719
2750
|
});
|
|
2720
2751
|
app2.patch("/:sessionId", async (c) => {
|
|
2721
2752
|
const row = resolveSession(c);
|
|
@@ -2761,7 +2792,7 @@ function sessionsRoutes(deps2) {
|
|
|
2761
2792
|
}
|
|
2762
2793
|
const hasTrace = await deps2.sessionService.hasTrace(updated);
|
|
2763
2794
|
return c.json({
|
|
2764
|
-
session: deps2.sessionService.toInfo(updated, hasTrace)
|
|
2795
|
+
session: await deps2.sessionService.toInfo(updated, hasTrace)
|
|
2765
2796
|
});
|
|
2766
2797
|
});
|
|
2767
2798
|
app2.delete("/:sessionId", async (c) => {
|
|
@@ -2780,6 +2811,7 @@ function sessionsRoutes(deps2) {
|
|
|
2780
2811
|
{ recursive: true, force: true }
|
|
2781
2812
|
);
|
|
2782
2813
|
deps2.sessionsRepo.deleteById(row.sessionId);
|
|
2814
|
+
deps2.sessionSources.delete(row.sessionId);
|
|
2783
2815
|
} finally {
|
|
2784
2816
|
deps2.manager.endSessionDeletion(row.sessionId);
|
|
2785
2817
|
}
|
|
@@ -3218,6 +3250,26 @@ function makeApprove(args) {
|
|
|
3218
3250
|
};
|
|
3219
3251
|
}
|
|
3220
3252
|
|
|
3253
|
+
// src/runtime/session-sources.ts
|
|
3254
|
+
function asSessionSource(v) {
|
|
3255
|
+
return v === "schedule" || v === "subagent" ? v : void 0;
|
|
3256
|
+
}
|
|
3257
|
+
var SessionSources = class {
|
|
3258
|
+
map = /* @__PURE__ */ new Map();
|
|
3259
|
+
/** Records a Session's origin as read from session_meta (`null` = meta seen, user-created). */
|
|
3260
|
+
set(sessionId, source) {
|
|
3261
|
+
this.map.set(sessionId, source);
|
|
3262
|
+
}
|
|
3263
|
+
/** Known origin, `null` for a known user-created Session, `undefined` when this process has not seen the meta. */
|
|
3264
|
+
get(sessionId) {
|
|
3265
|
+
return this.map.get(sessionId);
|
|
3266
|
+
}
|
|
3267
|
+
/** Drops a deleted Session's entry (bulk Agent/Project deletion may leave stale entries; they are never matched again). */
|
|
3268
|
+
delete(sessionId) {
|
|
3269
|
+
this.map.delete(sessionId);
|
|
3270
|
+
}
|
|
3271
|
+
};
|
|
3272
|
+
|
|
3221
3273
|
// src/runtime/stream-error-watcher.ts
|
|
3222
3274
|
import { isEventMessage, isModelMessage, isSessionMeta } from "@prismshadow/penguin-core";
|
|
3223
3275
|
import path7 from "path";
|
|
@@ -3396,7 +3448,7 @@ function compactUnavailable(why) {
|
|
|
3396
3448
|
};
|
|
3397
3449
|
return new HttpError(409, "nothing_to_compact", messages[why]);
|
|
3398
3450
|
}
|
|
3399
|
-
function createCoreSessionLoader(root) {
|
|
3451
|
+
function createCoreSessionLoader(root, sources) {
|
|
3400
3452
|
return {
|
|
3401
3453
|
async load(row) {
|
|
3402
3454
|
const agent = await createAgent({
|
|
@@ -3426,11 +3478,14 @@ function createCoreSessionLoader(root) {
|
|
|
3426
3478
|
`This Session's Workspace no longer exists: ${row.workspace}, so it cannot continue. Create a new Session.`
|
|
3427
3479
|
);
|
|
3428
3480
|
}
|
|
3481
|
+
const knownSource = sources?.get(row.sessionId);
|
|
3429
3482
|
try {
|
|
3430
3483
|
return await agent.createSession({
|
|
3431
3484
|
workspaceDir: row.workspace,
|
|
3432
3485
|
modelId: row.modelId,
|
|
3433
|
-
provider: row.provider
|
|
3486
|
+
provider: row.provider,
|
|
3487
|
+
// The rebuilt Session re-records a known origin in its fresh session_meta.
|
|
3488
|
+
...knownSource != null ? { source: knownSource } : {}
|
|
3434
3489
|
});
|
|
3435
3490
|
} catch (err) {
|
|
3436
3491
|
if (isMissingCredential(err)) throw modelCredentialMissing(row.modelId);
|
|
@@ -3958,6 +4013,8 @@ var SessionManager = class {
|
|
|
3958
4013
|
const p = msg.payload;
|
|
3959
4014
|
const agentId = path8.basename(path8.dirname(p.agent_state));
|
|
3960
4015
|
if (!agentId || agentId === "." || agentId === "..") return null;
|
|
4016
|
+
const source = asSessionSource(p.source) ?? "subagent";
|
|
4017
|
+
this.deps.sources.set(childSid, source);
|
|
3961
4018
|
this.deps.sessions.insertOrIgnore({
|
|
3962
4019
|
sessionId: childSid,
|
|
3963
4020
|
projectId: entry.projectId,
|
|
@@ -3969,7 +4026,6 @@ var SessionManager = class {
|
|
|
3969
4026
|
// inserted with defaults (matches the convention for Sessions discovered by the CLI).
|
|
3970
4027
|
approvalMode: "allow-all",
|
|
3971
4028
|
title: null,
|
|
3972
|
-
source: "subagent",
|
|
3973
4029
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3974
4030
|
});
|
|
3975
4031
|
this.publishEvent(entry, {
|
|
@@ -3977,7 +4033,7 @@ var SessionManager = class {
|
|
|
3977
4033
|
projectId: entry.projectId,
|
|
3978
4034
|
agentId,
|
|
3979
4035
|
sessionId: childSid,
|
|
3980
|
-
source
|
|
4036
|
+
source
|
|
3981
4037
|
});
|
|
3982
4038
|
const child = {
|
|
3983
4039
|
sessionId: childSid,
|
|
@@ -4774,6 +4830,7 @@ var ProjectConfigService = class {
|
|
|
4774
4830
|
const cat = catalogEntryFor(provider, modelId);
|
|
4775
4831
|
const envKey = resolveModelEnv(modelId, clientType)?.envKey;
|
|
4776
4832
|
const vision = typeof m.vision === "boolean" ? m.vision : cat?.supportsVision;
|
|
4833
|
+
const maxTokens = optNum(m.max_tokens);
|
|
4777
4834
|
const displayName = optStr(m.display_name) ?? cat?.displayName;
|
|
4778
4835
|
const apiKey = optStr(m.api_key);
|
|
4779
4836
|
const credBaseUrl = optStr(m.base_url);
|
|
@@ -4786,6 +4843,7 @@ var ProjectConfigService = class {
|
|
|
4786
4843
|
...optNum(m.context_window) !== void 0 ? { contextWindow: optNum(m.context_window) } : {},
|
|
4787
4844
|
...clientType ? { clientType } : {},
|
|
4788
4845
|
...vision !== void 0 ? { vision } : {},
|
|
4846
|
+
...maxTokens !== void 0 ? { maxTokens } : {},
|
|
4789
4847
|
...envKey ? { envKey } : {},
|
|
4790
4848
|
...pricingDto ? { pricing: pricingDto } : {},
|
|
4791
4849
|
...apiKey !== void 0 || credBaseUrl !== void 0 ? {
|
|
@@ -4844,6 +4902,7 @@ var ProjectConfigService = class {
|
|
|
4844
4902
|
delete next2.context_window;
|
|
4845
4903
|
delete next2.client_type;
|
|
4846
4904
|
delete next2.vision;
|
|
4905
|
+
delete next2.max_tokens;
|
|
4847
4906
|
delete next2.pricing;
|
|
4848
4907
|
delete next2.display_name;
|
|
4849
4908
|
delete next2.request_model_id;
|
|
@@ -4854,6 +4913,7 @@ var ProjectConfigService = class {
|
|
|
4854
4913
|
if (entry.contextWindow !== void 0) next2.context_window = entry.contextWindow;
|
|
4855
4914
|
if (entry.clientType) next2.client_type = entry.clientType;
|
|
4856
4915
|
if (entry.vision !== void 0) next2.vision = entry.vision;
|
|
4916
|
+
if (entry.maxTokens !== void 0) next2.max_tokens = entry.maxTokens;
|
|
4857
4917
|
if (entry.pricing !== void 0) {
|
|
4858
4918
|
next2.pricing = {
|
|
4859
4919
|
unit: "usd_per_mtok",
|
|
@@ -6004,8 +6064,13 @@ var SessionService = class {
|
|
|
6004
6064
|
this.deps = deps2;
|
|
6005
6065
|
}
|
|
6006
6066
|
deps;
|
|
6007
|
-
/**
|
|
6008
|
-
|
|
6067
|
+
/**
|
|
6068
|
+
* DB row -> SessionInfo (run status and pending approval count come from session-manager).
|
|
6069
|
+
* Async because `source` is derived from session_meta: a registry miss (Session predating
|
|
6070
|
+
* this process) falls back to reading the Trace head once (see sourceOf).
|
|
6071
|
+
*/
|
|
6072
|
+
async toInfo(row, hasTrace) {
|
|
6073
|
+
const source = await this.sourceOf(row, hasTrace);
|
|
6009
6074
|
return {
|
|
6010
6075
|
sessionId: row.sessionId,
|
|
6011
6076
|
projectId: row.projectId,
|
|
@@ -6015,7 +6080,7 @@ var SessionService = class {
|
|
|
6015
6080
|
workspace: row.workspace,
|
|
6016
6081
|
approvalMode: row.approvalMode,
|
|
6017
6082
|
...row.title !== null ? { title: row.title } : {},
|
|
6018
|
-
...
|
|
6083
|
+
...source !== void 0 ? { source } : {},
|
|
6019
6084
|
createdAt: row.createdAt,
|
|
6020
6085
|
status: this.deps.manager.statusOf(row.sessionId),
|
|
6021
6086
|
pendingApprovalCount: this.deps.manager.pendingApprovalCount(row.sessionId),
|
|
@@ -6023,13 +6088,36 @@ var SessionService = class {
|
|
|
6023
6088
|
archived: (row.archivedAt ?? null) !== null
|
|
6024
6089
|
};
|
|
6025
6090
|
}
|
|
6091
|
+
/**
|
|
6092
|
+
* A Session's origin, with session_meta as the single source of truth: the in-process
|
|
6093
|
+
* registry answers first (populated at creation / subagent registration / adoption);
|
|
6094
|
+
* on a miss (a Session created before this process started) the earliest Trace shard's
|
|
6095
|
+
* session_meta is read once and cached. A Session with no Trace yet stays unknown and
|
|
6096
|
+
* is NOT cached negatively — its meta may appear with the first run.
|
|
6097
|
+
*/
|
|
6098
|
+
async sourceOf(row, hasTrace) {
|
|
6099
|
+
const known = this.deps.sources.get(row.sessionId);
|
|
6100
|
+
if (known !== void 0) return known ?? void 0;
|
|
6101
|
+
if (!hasTrace) return void 0;
|
|
6102
|
+
const meta = await this.readTraceMeta(row.projectId, row.agentId, row.sessionId);
|
|
6103
|
+
if (!meta) return void 0;
|
|
6104
|
+
const source = asSessionSource(meta.payload.source) ?? null;
|
|
6105
|
+
this.deps.sources.set(row.sessionId, source);
|
|
6106
|
+
return source ?? void 0;
|
|
6107
|
+
}
|
|
6026
6108
|
/** Whether this Session already has a Trace record (a Task has been run). */
|
|
6027
6109
|
async hasTrace(row) {
|
|
6028
6110
|
const ids = await this.discoverTraceSessionIds(row.projectId, row.agentId);
|
|
6029
6111
|
return ids.has(row.sessionId);
|
|
6030
6112
|
}
|
|
6031
|
-
/**
|
|
6032
|
-
|
|
6113
|
+
/**
|
|
6114
|
+
* List: DB ∪ Trace directory discovery, sorted by createdAt descending. Optional
|
|
6115
|
+
* `paging` returns just that slice (the sidebar pages with limit+1 to detect "has
|
|
6116
|
+
* more"); slicing happens before toInfo, so per-request source derivation (lazy
|
|
6117
|
+
* Trace-head reads) stays bounded by the page size. Discovery/adoption still scans
|
|
6118
|
+
* the whole directory — the union and global ordering need every id.
|
|
6119
|
+
*/
|
|
6120
|
+
async listSessions(projectId, agentId, paging) {
|
|
6033
6121
|
const traceIds = await this.discoverTraceSessionIds(projectId, agentId);
|
|
6034
6122
|
const rows = new Map(
|
|
6035
6123
|
this.deps.sessions.listByAgent(projectId, agentId).map((r) => [r.sessionId, r])
|
|
@@ -6039,9 +6127,11 @@ var SessionService = class {
|
|
|
6039
6127
|
const discovered = await this.adoptTraceSession(projectId, agentId, sessionId);
|
|
6040
6128
|
if (discovered) rows.set(sessionId, discovered);
|
|
6041
6129
|
}
|
|
6042
|
-
|
|
6130
|
+
const sorted = [...rows.values()].sort(
|
|
6043
6131
|
(a, b) => b.createdAt.localeCompare(a.createdAt) || b.sessionId.localeCompare(a.sessionId)
|
|
6044
|
-
)
|
|
6132
|
+
);
|
|
6133
|
+
const page = paging ? sorted.slice(paging.offset, paging.offset + paging.limit) : sorted;
|
|
6134
|
+
return Promise.all(page.map((row) => this.toInfo(row, traceIds.has(row.sessionId))));
|
|
6045
6135
|
}
|
|
6046
6136
|
/**
|
|
6047
6137
|
* Session stats (Agents list card): total count = size of the union of DB index
|
|
@@ -6121,7 +6211,9 @@ var SessionService = class {
|
|
|
6121
6211
|
session = await agent.createSession({
|
|
6122
6212
|
modelId,
|
|
6123
6213
|
provider,
|
|
6124
|
-
...args.workspace !== void 0 ? { workspaceDir: args.workspace } : {}
|
|
6214
|
+
...args.workspace !== void 0 ? { workspaceDir: args.workspace } : {},
|
|
6215
|
+
// The origin is also recorded in core session_meta (Trace), not just the index row.
|
|
6216
|
+
...args.source !== void 0 ? { source: args.source } : {}
|
|
6125
6217
|
});
|
|
6126
6218
|
} catch (err) {
|
|
6127
6219
|
if (isMissingCredential(err)) throw modelCredentialMissing(modelId);
|
|
@@ -6131,6 +6223,11 @@ var SessionService = class {
|
|
|
6131
6223
|
err instanceof Error ? err.message : String(err)
|
|
6132
6224
|
);
|
|
6133
6225
|
}
|
|
6226
|
+
const metaMsg = session.metaMessage;
|
|
6227
|
+
this.deps.sources.set(
|
|
6228
|
+
session.sessionId,
|
|
6229
|
+
isSessionMeta4(metaMsg) ? asSessionSource(metaMsg.payload.source) ?? null : null
|
|
6230
|
+
);
|
|
6134
6231
|
const row = {
|
|
6135
6232
|
sessionId: session.sessionId,
|
|
6136
6233
|
projectId: args.projectId,
|
|
@@ -6140,8 +6237,7 @@ var SessionService = class {
|
|
|
6140
6237
|
workspace: session.workspaceDir,
|
|
6141
6238
|
approvalMode: args.approvalMode ?? "allow-all",
|
|
6142
6239
|
title: null,
|
|
6143
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6144
|
-
...args.source !== void 0 ? { source: args.source } : {}
|
|
6240
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6145
6241
|
};
|
|
6146
6242
|
this.deps.sessions.insert(row);
|
|
6147
6243
|
this.deps.manager.adopt(row, session);
|
|
@@ -6159,8 +6255,12 @@ var SessionService = class {
|
|
|
6159
6255
|
}
|
|
6160
6256
|
return ids;
|
|
6161
6257
|
}
|
|
6162
|
-
/**
|
|
6163
|
-
|
|
6258
|
+
/**
|
|
6259
|
+
* session_meta from the earliest Trace shard of a Session (the shard whose head carries
|
|
6260
|
+
* the original meta); null when there is no readable Trace or it has no meta. Shared by
|
|
6261
|
+
* adoption backfill and lazy `source` resolution.
|
|
6262
|
+
*/
|
|
6263
|
+
async readTraceMeta(projectId, agentId, sessionId) {
|
|
6164
6264
|
const dir = tracesDir2(this.deps.root, projectId, agentId);
|
|
6165
6265
|
let earliest = null;
|
|
6166
6266
|
for (const dateDir of await listDirsSafe(dir)) {
|
|
@@ -6180,9 +6280,14 @@ var SessionService = class {
|
|
|
6180
6280
|
} catch {
|
|
6181
6281
|
return null;
|
|
6182
6282
|
}
|
|
6183
|
-
|
|
6283
|
+
return messages.find(isSessionMeta4) ?? null;
|
|
6284
|
+
}
|
|
6285
|
+
/** Adopts a Session that exists only in the Trace directory: reads session_meta from the first line of the earliest index file. */
|
|
6286
|
+
async adoptTraceSession(projectId, agentId, sessionId) {
|
|
6287
|
+
const meta = await this.readTraceMeta(projectId, agentId, sessionId);
|
|
6184
6288
|
if (!meta) return null;
|
|
6185
6289
|
if (typeof meta.payload.provider !== "string") return null;
|
|
6290
|
+
this.deps.sources.set(sessionId, asSessionSource(meta.payload.source) ?? null);
|
|
6186
6291
|
const row = {
|
|
6187
6292
|
sessionId,
|
|
6188
6293
|
projectId,
|
|
@@ -6848,10 +6953,12 @@ function buildAppDeps(config2, overrides = {}) {
|
|
|
6848
6953
|
const recorder = new UsageRecorder(usageRepo, overrides.now ?? (() => /* @__PURE__ */ new Date()));
|
|
6849
6954
|
const errors = new ErrorRecorder(errorsRepo, overrides.now ?? (() => /* @__PURE__ */ new Date()));
|
|
6850
6955
|
const titles = overrides.titles ?? new TitleGenerator({ sessions: sessionsRepo, channels, recorder, errors, log });
|
|
6956
|
+
const sessionSources = new SessionSources();
|
|
6851
6957
|
const manager = new SessionManager({
|
|
6852
6958
|
sessions: sessionsRepo,
|
|
6853
6959
|
channels,
|
|
6854
|
-
loader: overrides.loader ?? createCoreSessionLoader(config2.root),
|
|
6960
|
+
loader: overrides.loader ?? createCoreSessionLoader(config2.root, sessionSources),
|
|
6961
|
+
sources: sessionSources,
|
|
6855
6962
|
recorder,
|
|
6856
6963
|
errors,
|
|
6857
6964
|
titles,
|
|
@@ -6890,7 +6997,8 @@ function buildAppDeps(config2, overrides = {}) {
|
|
|
6890
6997
|
root: config2.root,
|
|
6891
6998
|
sessions: sessionsRepo,
|
|
6892
6999
|
manager,
|
|
6893
|
-
projectConfig: projectConfigService
|
|
7000
|
+
projectConfig: projectConfigService,
|
|
7001
|
+
sources: sessionSources
|
|
6894
7002
|
});
|
|
6895
7003
|
const scheduler = new Scheduler({
|
|
6896
7004
|
root: config2.root,
|
|
@@ -6926,6 +7034,7 @@ function buildAppDeps(config2, overrides = {}) {
|
|
|
6926
7034
|
scheduler,
|
|
6927
7035
|
channels,
|
|
6928
7036
|
manager,
|
|
7037
|
+
sessionSources,
|
|
6929
7038
|
errors,
|
|
6930
7039
|
log
|
|
6931
7040
|
};
|
|
@@ -7043,7 +7152,7 @@ function registerStaticRoutes(app2, webDist) {
|
|
|
7043
7152
|
import fs17 from "fs";
|
|
7044
7153
|
import path16 from "path";
|
|
7045
7154
|
import { fileURLToPath } from "url";
|
|
7046
|
-
import { resolveRoot } from "@prismshadow/penguin-core";
|
|
7155
|
+
import { DEFAULT_SERVER_PORT, resolveRoot } from "@prismshadow/penguin-core";
|
|
7047
7156
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
7048
7157
|
function defaultWebDist() {
|
|
7049
7158
|
const here = path16.dirname(fileURLToPath(import.meta.url));
|
|
@@ -7053,7 +7162,7 @@ function defaultWebDist() {
|
|
|
7053
7162
|
}
|
|
7054
7163
|
function resolveServerConfig(env = process.env) {
|
|
7055
7164
|
const root = env.PENGUIN_HOME ?? resolveRoot();
|
|
7056
|
-
const port = Number(env.PORT ||
|
|
7165
|
+
const port = Number(env.PORT || DEFAULT_SERVER_PORT);
|
|
7057
7166
|
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
7058
7167
|
throw new Error(`Invalid port configuration PORT=${env.PORT}`);
|
|
7059
7168
|
}
|