@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.
@@ -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, source, created_at)
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, source, created_at)
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 v = Number.parseInt(pathParam(c, name), 10);
1077
- if (!Number.isInteger(v) || v < 1) throw badRequest(`${name} must be a positive integer.`);
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
- if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
1147
- throw badRequest(`${label} must be in YYYY-MM-DD format.`);
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
- return value;
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 sessions = await deps2.sessionService.listSessions(projectId, agentId);
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({ session: deps2.sessionService.toInfo(row, hasTrace) });
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: "subagent"
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
- /** DB row -> SessionInfo (run status and pending approval count come from session-manager). */
6008
- toInfo(row, hasTrace) {
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
- ...row.source != null ? { source: row.source } : {},
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
- /** List: DB ∪ Trace directory discovery, sorted by createdAt descending. */
6032
- async listSessions(projectId, agentId) {
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
- return [...rows.values()].sort(
6130
+ const sorted = [...rows.values()].sort(
6043
6131
  (a, b) => b.createdAt.localeCompare(a.createdAt) || b.sessionId.localeCompare(a.sessionId)
6044
- ).map((row) => this.toInfo(row, traceIds.has(row.sessionId)));
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
- /** Adopts a Session that exists only in the Trace directory: reads session_meta from the first line of the earliest index file. */
6163
- async adoptTraceSession(projectId, agentId, sessionId) {
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
- const meta = messages.find(isSessionMeta4);
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 || 7364);
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
  }