@prismshadow/penguin-server 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,21 +2,79 @@ import {
2
2
  acquireServerLock,
3
3
  liveServerLock,
4
4
  releaseServerLock
5
- } from "./chunk-V6KH4FRG.js";
5
+ } from "./chunk-OMBFPEGQ.js";
6
+ import {
7
+ clearInitialAdminPassword,
8
+ readInitialAdminPassword,
9
+ renderInitialPasswordNotice,
10
+ storeInitialAdminPassword
11
+ } from "./chunk-UUJCF46V.js";
6
12
 
7
13
  // src/index.ts
8
- import fs22 from "fs";
9
- import path18 from "path";
14
+ import fs23 from "fs";
15
+ import path19 from "path";
10
16
  import { config as loadDotenv } from "dotenv";
11
17
  import { serve } from "@hono/node-server";
12
18
 
13
19
  // src/app.ts
14
- import fs20 from "fs";
20
+ import fs21 from "fs";
15
21
  import fsp from "fs/promises";
16
- import path16 from "path";
17
- import { Hono as Hono23 } from "hono";
22
+ import path17 from "path";
23
+ import { Hono as Hono25 } from "hono";
18
24
  import { bodyLimit } from "hono/body-limit";
19
25
 
26
+ // src/net/proxy.ts
27
+ import { Agent, EnvHttpProxyAgent, fetch as undiciFetch, setGlobalDispatcher } from "undici";
28
+ var LOOPBACK_NO_PROXY = ["localhost", "127.0.0.1", "::1"];
29
+ function mergedNoProxy(env = process.env) {
30
+ const raw = env.no_proxy ?? env.NO_PROXY ?? "";
31
+ const entries = raw.split(/[,\s]/).filter((e) => e !== "");
32
+ const present = new Set(entries.map((e) => e.toLowerCase()));
33
+ for (const name of LOOPBACK_NO_PROXY) {
34
+ if (!present.has(name)) entries.push(name);
35
+ }
36
+ return entries.join(",");
37
+ }
38
+ function normalizeProxyUrl(raw) {
39
+ const trimmed = raw.trim();
40
+ if (trimmed === "") return null;
41
+ const candidate = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed) ? trimmed : `http://${trimmed}`;
42
+ let url;
43
+ try {
44
+ url = new URL(candidate);
45
+ } catch {
46
+ return null;
47
+ }
48
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
49
+ if (url.hostname === "" || url.username !== "" || url.password !== "") return null;
50
+ if (url.pathname !== "/" && url.pathname !== "" || url.search !== "" || url.hash !== "") {
51
+ return null;
52
+ }
53
+ return url.origin;
54
+ }
55
+ function envProxyAgentOptions(proxyUrl, env) {
56
+ const noProxy = mergedNoProxy(env);
57
+ return proxyUrl === null ? { noProxy } : { httpProxy: proxyUrl, httpsProxy: proxyUrl, noProxy };
58
+ }
59
+ function buildProxyDispatcher(settings, env) {
60
+ if (!settings.proxyForApp) return new Agent();
61
+ return new EnvHttpProxyAgent(envProxyAgentOptions(settings.proxyUrl, env));
62
+ }
63
+ var current = { proxyForApp: true, proxyUrl: null };
64
+ var installed = false;
65
+ function installGlobalProxyDispatcher() {
66
+ installed = true;
67
+ setGlobalDispatcher(buildProxyDispatcher(current));
68
+ globalThis.fetch = undiciFetch;
69
+ }
70
+ function applyProxySettings(settings) {
71
+ if (settings.proxyForApp === current.proxyForApp && settings.proxyUrl === current.proxyUrl) {
72
+ return;
73
+ }
74
+ current = { proxyForApp: settings.proxyForApp, proxyUrl: settings.proxyUrl };
75
+ if (installed) setGlobalDispatcher(buildProxyDispatcher(current));
76
+ }
77
+
20
78
  // src/db/database.ts
21
79
  import { mkdirSync } from "fs";
22
80
  import path from "path";
@@ -133,6 +191,10 @@ CREATE TABLE IF NOT EXISTS ui_prefs (
133
191
  user_id TEXT PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE,
134
192
  prefs_json TEXT NOT NULL -- {theme?, lastProjectId?, ...} free-form JSON
135
193
  );
194
+ CREATE TABLE IF NOT EXISTS server_settings ( -- admin-level server-global settings (GET/PUT /api/admin/settings)
195
+ key TEXT PRIMARY KEY, -- setting name, e.g. 'proxy_for_app'
196
+ value TEXT NOT NULL -- JSON-encoded value; an absent row means the setting's built-in default
197
+ );
136
198
  CREATE TABLE IF NOT EXISTS trace_files ( -- DERIVED CACHE of the on-disk Trace tree (services/trace-index.ts): the directories stay the single source of truth, every row is rebuildable from disk, and a row is never authority for absence \u2014 consumers reconcile + retry on a miss, so a stale index costs one extra scan, never a false 404
137
199
  project_id TEXT NOT NULL,
138
200
  agent_id TEXT NOT NULL,
@@ -650,6 +712,67 @@ var SchedulesRepo = class {
650
712
  }
651
713
  };
652
714
 
715
+ // src/db/repos/server-settings.ts
716
+ var PROXY_FOR_APP_KEY = "proxy_for_app";
717
+ var PROXY_FOR_AGENT_KEY = "proxy_for_agent";
718
+ var LEGACY_USE_SYSTEM_PROXY_KEY = "use_system_proxy";
719
+ var PROXY_URL_KEY = "proxy_url";
720
+ var ServerSettingsRepo = class {
721
+ constructor(db) {
722
+ this.db = db;
723
+ }
724
+ db;
725
+ /** Returns the raw JSON-encoded value; null if never set. */
726
+ get(key) {
727
+ const r = this.db.prepare("SELECT value FROM server_settings WHERE key = ?").get(key);
728
+ return r ? r.value : null;
729
+ }
730
+ set(key, value) {
731
+ this.db.prepare(
732
+ `INSERT INTO server_settings (key, value) VALUES (?, ?)
733
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
734
+ ).run(key, value);
735
+ }
736
+ /** Shared switch read: this key if present, else the legacy single switch, else the default: on. */
737
+ getProxySwitch(key) {
738
+ const raw = this.get(key);
739
+ if (raw !== null) return raw !== "false";
740
+ return this.get(LEGACY_USE_SYSTEM_PROXY_KEY) !== "false";
741
+ }
742
+ /** The "application uses the proxy" switch (the server's own outbound dispatcher). */
743
+ getProxyForApp() {
744
+ return this.getProxySwitch(PROXY_FOR_APP_KEY);
745
+ }
746
+ setProxyForApp(value) {
747
+ this.set(PROXY_FOR_APP_KEY, JSON.stringify(value));
748
+ }
749
+ /** The "agent environment uses the proxy" switch (command subprocess env policy). */
750
+ getProxyForAgent() {
751
+ return this.getProxySwitch(PROXY_FOR_AGENT_KEY);
752
+ }
753
+ setProxyForAgent(value) {
754
+ this.set(PROXY_FOR_AGENT_KEY, JSON.stringify(value));
755
+ }
756
+ /**
757
+ * The explicit proxy address (normalized at write time by the settings route); null =
758
+ * follow the proxy environment variables. An absent or unreadable row reads as null
759
+ * (the safe default: behave as before the setting existed).
760
+ */
761
+ getProxyUrl() {
762
+ const raw = this.get(PROXY_URL_KEY);
763
+ if (raw === null) return null;
764
+ try {
765
+ const value = JSON.parse(raw);
766
+ return typeof value === "string" && value !== "" ? value : null;
767
+ } catch {
768
+ return null;
769
+ }
770
+ }
771
+ setProxyUrl(value) {
772
+ this.set(PROXY_URL_KEY, JSON.stringify(value));
773
+ }
774
+ };
775
+
653
776
  // src/db/repos/sessions.ts
654
777
  function mapRow4(r) {
655
778
  return {
@@ -1305,6 +1428,10 @@ var AuthService = class {
1305
1428
  }
1306
1429
  return password;
1307
1430
  }
1431
+ /** Whether the built-in admin still runs on its initial password (drives the startup reminder notice). */
1432
+ adminPasswordIsInitial() {
1433
+ return this.deps.users.findById(ADMIN_USER_ID)?.passwordIsInitial === true;
1434
+ }
1308
1435
  /** Consecutive login failures per userId (see the throttling comment on the constants). */
1309
1436
  loginFailures = /* @__PURE__ */ new Map();
1310
1437
  /** The wait imposed after `failures` consecutive failures (0 while within the free attempts). */
@@ -1366,6 +1493,7 @@ var AuthService = class {
1366
1493
  throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
1367
1494
  }
1368
1495
  this.deps.users.updatePassword(userId, await hashPassword(newPassword), false);
1496
+ this.deps.onPasswordChanged?.(userId);
1369
1497
  }
1370
1498
  /**
1371
1499
  * Desktop-session password set: no old-password check. Only reachable for sessions
@@ -1378,6 +1506,7 @@ var AuthService = class {
1378
1506
  throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
1379
1507
  }
1380
1508
  this.deps.users.updatePassword(userId, await hashPassword(newPassword), false);
1509
+ this.deps.onPasswordChanged?.(userId);
1381
1510
  }
1382
1511
  logout(token) {
1383
1512
  this.deps.authSessions.delete(sha256Hex(token));
@@ -1418,7 +1547,38 @@ var AuthService = class {
1418
1547
  };
1419
1548
 
1420
1549
  // src/http/routes/admin.ts
1550
+ import { Hono as Hono2 } from "hono";
1551
+
1552
+ // src/http/routes/desktop.ts
1421
1553
  import { Hono } from "hono";
1554
+ function rejectInDesktopMode(deps2) {
1555
+ return async (_c, next) => {
1556
+ if (deps2.desktop !== null) {
1557
+ throw new HttpError(
1558
+ 403,
1559
+ "desktop_single_user",
1560
+ "User management is disabled in the desktop app (single-user mode)."
1561
+ );
1562
+ }
1563
+ await next();
1564
+ };
1565
+ }
1566
+ var SHUTDOWN_DELAY_MS = 50;
1567
+ function desktopRoutes(deps2) {
1568
+ const app2 = new Hono();
1569
+ app2.post("/shutdown", (c) => {
1570
+ const desktop = deps2.desktop;
1571
+ if (!desktop) throw new HttpError(404, "not_found", "Desktop mode is not enabled.");
1572
+ const header = c.req.header("authorization") ?? "";
1573
+ const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
1574
+ if (token === "" || !desktop.verifyToken(token)) {
1575
+ throw new HttpError(401, "unauthorized", "Invalid desktop token.");
1576
+ }
1577
+ setTimeout(() => desktop.requestShutdown(), SHUTDOWN_DELAY_MS).unref();
1578
+ return c.body(null, 202);
1579
+ });
1580
+ return app2;
1581
+ }
1422
1582
 
1423
1583
  // src/http/validate.ts
1424
1584
  import { isValidId } from "@prismshadow/penguin-core";
@@ -1546,7 +1706,8 @@ function optionalDateParam(value, label) {
1546
1706
 
1547
1707
  // src/http/routes/admin.ts
1548
1708
  function adminUsersRoutes(deps2) {
1549
- const app2 = new Hono();
1709
+ const app2 = new Hono2();
1710
+ app2.use("*", rejectInDesktopMode(deps2));
1550
1711
  app2.use("*", async (c, next) => {
1551
1712
  if (!c.var.user.isAdmin) {
1552
1713
  throw new HttpError(403, "admin_required", "Only an admin can perform this operation.");
@@ -1576,8 +1737,57 @@ function adminUsersRoutes(deps2) {
1576
1737
  return app2;
1577
1738
  }
1578
1739
 
1740
+ // src/http/routes/admin-settings.ts
1741
+ import { Hono as Hono3 } from "hono";
1742
+ function parseProxyUrl(value) {
1743
+ if (value === null) return null;
1744
+ if (typeof value === "string") {
1745
+ if (value.trim() === "") return null;
1746
+ const normalized = normalizeProxyUrl(value);
1747
+ if (normalized !== null) return normalized;
1748
+ }
1749
+ throw new HttpError(
1750
+ 400,
1751
+ "invalid_proxy_url",
1752
+ "proxyUrl must be http://host[:port], https://host[:port], or host[:port] (empty or null clears it)."
1753
+ );
1754
+ }
1755
+ function adminSettingsRoutes(deps2) {
1756
+ const app2 = new Hono3();
1757
+ app2.use("*", async (c, next) => {
1758
+ if (!c.var.user.isAdmin) {
1759
+ throw new HttpError(403, "admin_required", "Only an admin can perform this operation.");
1760
+ }
1761
+ await next();
1762
+ });
1763
+ const settings = () => ({
1764
+ settings: {
1765
+ proxyForApp: deps2.serverSettingsRepo.getProxyForApp(),
1766
+ proxyForAgent: deps2.serverSettingsRepo.getProxyForAgent(),
1767
+ proxyUrl: deps2.serverSettingsRepo.getProxyUrl()
1768
+ }
1769
+ });
1770
+ app2.get("/", (c) => c.json(settings()));
1771
+ app2.put("/", async (c) => {
1772
+ const body = await readJson(c);
1773
+ const proxyForApp = optionalBoolean(body, "proxyForApp");
1774
+ const proxyForAgent = optionalBoolean(body, "proxyForAgent");
1775
+ const proxyUrlProvided = body.proxyUrl !== void 0;
1776
+ const proxyUrl = proxyUrlProvided ? parseProxyUrl(body.proxyUrl) : null;
1777
+ if (proxyForApp !== void 0) deps2.serverSettingsRepo.setProxyForApp(proxyForApp);
1778
+ if (proxyForAgent !== void 0) deps2.serverSettingsRepo.setProxyForAgent(proxyForAgent);
1779
+ if (proxyUrlProvided) deps2.serverSettingsRepo.setProxyUrl(proxyUrl);
1780
+ applyProxySettings({
1781
+ proxyForApp: deps2.serverSettingsRepo.getProxyForApp(),
1782
+ proxyUrl: deps2.serverSettingsRepo.getProxyUrl()
1783
+ });
1784
+ return c.json(settings());
1785
+ });
1786
+ return app2;
1787
+ }
1788
+
1579
1789
  // src/http/routes/auth.ts
1580
- import { Hono as Hono2 } from "hono";
1790
+ import { Hono as Hono4 } from "hono";
1581
1791
  import { deleteCookie, getCookie as getCookie2, setCookie } from "hono/cookie";
1582
1792
  function cookieOptions(c) {
1583
1793
  return {
@@ -1590,7 +1800,7 @@ function cookieOptions(c) {
1590
1800
  };
1591
1801
  }
1592
1802
  function authRoutes(deps2) {
1593
- const app2 = new Hono2();
1803
+ const app2 = new Hono4();
1594
1804
  app2.post("/login", async (c) => {
1595
1805
  const body = await readJson(c);
1596
1806
  const userId = requireString(body, "userId", { label: "userId" });
@@ -1620,7 +1830,7 @@ function authRoutes(deps2) {
1620
1830
  }
1621
1831
 
1622
1832
  // src/http/routes/me.ts
1623
- import { Hono as Hono3 } from "hono";
1833
+ import { Hono as Hono5 } from "hono";
1624
1834
 
1625
1835
  // src/services/preview-token.ts
1626
1836
  import { createHmac, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
@@ -1715,7 +1925,7 @@ function resolvePreviewTarget(requestUrl, hostHeader, configuredOrigin, serverBi
1715
1925
 
1716
1926
  // src/http/routes/me.ts
1717
1927
  function meRoutes(deps2) {
1718
- const app2 = new Hono3();
1928
+ const app2 = new Hono5();
1719
1929
  app2.get("/", (c) => {
1720
1930
  const target = resolvePreviewTarget(
1721
1931
  c.req.url,
@@ -1757,15 +1967,15 @@ function meRoutes(deps2) {
1757
1967
  app2.put("/prefs", async (c) => {
1758
1968
  const body = await readJson(c);
1759
1969
  const raw = deps2.prefsRepo.get(c.var.user.userId);
1760
- let current = {};
1970
+ let current2 = {};
1761
1971
  if (raw !== null) {
1762
1972
  try {
1763
- current = JSON.parse(raw);
1973
+ current2 = JSON.parse(raw);
1764
1974
  } catch {
1765
- current = {};
1975
+ current2 = {};
1766
1976
  }
1767
1977
  }
1768
- const merged = { ...current, ...body };
1978
+ const merged = { ...current2, ...body };
1769
1979
  deps2.prefsRepo.set(c.var.user.userId, JSON.stringify(merged));
1770
1980
  return c.json({ prefs: merged });
1771
1981
  });
@@ -1773,7 +1983,7 @@ function meRoutes(deps2) {
1773
1983
  }
1774
1984
 
1775
1985
  // src/http/routes/events.ts
1776
- import { Hono as Hono4 } from "hono";
1986
+ import { Hono as Hono6 } from "hono";
1777
1987
 
1778
1988
  // src/http/sse.ts
1779
1989
  import { streamSSE } from "hono/streaming";
@@ -1840,7 +2050,7 @@ function userChannelKey(userId) {
1840
2050
  return `user:${userId}`;
1841
2051
  }
1842
2052
  function eventsRoutes(deps2) {
1843
- const app2 = new Hono4();
2053
+ const app2 = new Hono6();
1844
2054
  app2.get("/", (c) => {
1845
2055
  const channel = deps2.channels.get(userChannelKey(c.var.user.userId));
1846
2056
  return sseEndpoint(c, channel, { initialEvents: [{ type: "hello" }] });
@@ -1849,9 +2059,9 @@ function eventsRoutes(deps2) {
1849
2059
  }
1850
2060
 
1851
2061
  // src/http/routes/projects.ts
1852
- import { Hono as Hono5 } from "hono";
2062
+ import { Hono as Hono7 } from "hono";
1853
2063
  function projectsRoutes(deps2) {
1854
- const app2 = new Hono5();
2064
+ const app2 = new Hono7();
1855
2065
  app2.get("/", async (c) => {
1856
2066
  const projects = await deps2.projectService.listProjects(c.var.user.userId);
1857
2067
  return c.json({ projects });
@@ -1879,9 +2089,10 @@ function projectsRoutes(deps2) {
1879
2089
  }
1880
2090
 
1881
2091
  // src/http/routes/members.ts
1882
- import { Hono as Hono6 } from "hono";
2092
+ import { Hono as Hono8 } from "hono";
1883
2093
  function membersRoutes(deps2) {
1884
- const app2 = new Hono6();
2094
+ const app2 = new Hono8();
2095
+ app2.use("*", rejectInDesktopMode(deps2));
1885
2096
  app2.get("/", (c) => {
1886
2097
  const members = deps2.projectService.listMembers(
1887
2098
  c.var.user.userId,
@@ -1911,7 +2122,7 @@ function membersRoutes(deps2) {
1911
2122
  }
1912
2123
 
1913
2124
  // src/http/routes/models.ts
1914
- import { Hono as Hono7 } from "hono";
2125
+ import { Hono as Hono9 } from "hono";
1915
2126
  function publishCredentialsUpdated(deps2, projectId) {
1916
2127
  const event = { type: "credentials_updated" };
1917
2128
  for (const row of deps2.sessionsRepo.listByProject(projectId)) {
@@ -2027,7 +2238,7 @@ function parseModelsUpdate(body) {
2027
2238
  return req;
2028
2239
  }
2029
2240
  function modelsRoutes(deps2) {
2030
- const app2 = new Hono7();
2241
+ const app2 = new Hono9();
2031
2242
  app2.get("/", async (c) => {
2032
2243
  const projectId = requireValidId(c, "projectId");
2033
2244
  deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
@@ -2089,13 +2300,13 @@ function modelsRoutes(deps2) {
2089
2300
  }
2090
2301
 
2091
2302
  // src/http/routes/chat-defaults.ts
2092
- import { Hono as Hono9 } from "hono";
2303
+ import { Hono as Hono11 } from "hono";
2093
2304
  import { DEFAULT_CHAT_THINKING_LEVELS, isValidId as isValidId2 } from "@prismshadow/penguin-core";
2094
2305
 
2095
2306
  // src/http/routes/sessions.ts
2096
2307
  import fs4 from "fs/promises";
2097
2308
  import path4 from "path";
2098
- import { Hono as Hono8 } from "hono";
2309
+ import { Hono as Hono10 } from "hono";
2099
2310
  import { imageUrlMessage, scratchpadDir, userText } from "@prismshadow/penguin-core";
2100
2311
 
2101
2312
  // src/services/message-window.ts
@@ -2323,7 +2534,7 @@ async function assertWorkspaceAllowed(args) {
2323
2534
  throw new HttpError(
2324
2535
  400,
2325
2536
  "workspace_not_found",
2326
- `Workspace does not exist or is inaccessible: ${args.workspace}. Specify an existing directory, or leave it empty to use a temporary directory.`
2537
+ `Workspace does not exist or is inaccessible: ${args.workspace}. Specify an existing directory, or leave it empty to use a temporary workspace.`
2327
2538
  );
2328
2539
  }
2329
2540
  const stat = await fs.stat(ws);
@@ -2770,6 +2981,12 @@ function pageLimit(raw, name) {
2770
2981
  }
2771
2982
  return v;
2772
2983
  }
2984
+ function appendPendingInputs(messages, pending) {
2985
+ if (pending.length === 0) return messages;
2986
+ const tail = new Set(messages.slice(-50).map((m) => JSON.stringify(m)));
2987
+ const missing = pending.filter((m) => !tail.has(JSON.stringify(m)));
2988
+ return missing.length > 0 ? [...messages, ...missing] : messages;
2989
+ }
2773
2990
  function messagesPageQuery(c) {
2774
2991
  const rawTail = c.req.query("tailLimit");
2775
2992
  const rawBefore = c.req.query("before");
@@ -2883,7 +3100,7 @@ function parseGoalField(body) {
2883
3100
  return { budget: budget ?? -1 };
2884
3101
  }
2885
3102
  function agentSessionsRoutes(deps2) {
2886
- const app2 = new Hono8();
3103
+ const app2 = new Hono10();
2887
3104
  app2.get("/", async (c) => {
2888
3105
  const projectId = requireValidId(c, "projectId");
2889
3106
  const agentId = requireValidId(c, "agentId");
@@ -2945,7 +3162,7 @@ function agentSessionsRoutes(deps2) {
2945
3162
  return app2;
2946
3163
  }
2947
3164
  function sessionsRoutes(deps2) {
2948
- const app2 = new Hono8();
3165
+ const app2 = new Hono10();
2949
3166
  const resolveSession = (c) => {
2950
3167
  const sessionId = c.req.param("sessionId");
2951
3168
  const row = sessionId ? deps2.sessionsRepo.findById(sessionId) : null;
@@ -3089,11 +3306,18 @@ function sessionsRoutes(deps2) {
3089
3306
  const row = resolveSession(c);
3090
3307
  const page = messagesPageQuery(c);
3091
3308
  let live;
3092
- if (page?.kind !== "before" && deps2.manager.statusOf(row.sessionId) !== "idle") {
3093
- live = {
3094
- cursor: deps2.channels.get(row.sessionId).lastEventId,
3095
- fragments: deps2.manager.liveFragments(row.sessionId)
3096
- };
3309
+ let pendingInputs = [];
3310
+ if (page?.kind !== "before") {
3311
+ if (deps2.manager.statusOf(row.sessionId) !== "idle") {
3312
+ live = {
3313
+ cursor: deps2.channels.get(row.sessionId).lastEventId,
3314
+ fragments: deps2.manager.liveFragments(row.sessionId)
3315
+ };
3316
+ }
3317
+ pendingInputs = [
3318
+ ...deps2.manager.pendingInputs(row.sessionId),
3319
+ ...deps2.manager.pendingBootstrap(row.sessionId)
3320
+ ];
3097
3321
  }
3098
3322
  if (page !== null) {
3099
3323
  const result = await deps2.traceService.readMessagesPage(
@@ -3113,7 +3337,7 @@ function sessionsRoutes(deps2) {
3113
3337
  }
3114
3338
  };
3115
3339
  return c.json({
3116
- messages: result.messages,
3340
+ messages: appendPendingInputs(result.messages, pendingInputs),
3117
3341
  ...live !== void 0 ? { live } : {},
3118
3342
  page: info
3119
3343
  });
@@ -3123,7 +3347,10 @@ function sessionsRoutes(deps2) {
3123
3347
  row.agentId,
3124
3348
  row.sessionId
3125
3349
  );
3126
- return c.json({ messages, ...live !== void 0 ? { live } : {} });
3350
+ return c.json({
3351
+ messages: appendPendingInputs(messages, pendingInputs),
3352
+ ...live !== void 0 ? { live } : {}
3353
+ });
3127
3354
  });
3128
3355
  app2.get("/:sessionId/stream", (c) => {
3129
3356
  const row = resolveSession(c);
@@ -3247,6 +3474,30 @@ function sessionsRoutes(deps2) {
3247
3474
  const aborted = deps2.manager.abortTask(row.sessionId);
3248
3475
  return c.body(null, aborted ? 202 : 204);
3249
3476
  });
3477
+ app2.get("/:sessionId/processes", (c) => {
3478
+ const row = resolveSession(c);
3479
+ const processes = deps2.manager.listProcesses(row.sessionId).map((p) => ({
3480
+ processId: p.processId,
3481
+ pid: p.pid,
3482
+ cmd: p.cmd,
3483
+ cwd: p.cwd,
3484
+ startedAt: new Date(p.startedAt).toISOString(),
3485
+ running: p.running
3486
+ }));
3487
+ return c.json({ processes });
3488
+ });
3489
+ app2.post("/:sessionId/processes/:processId/kill", (c) => {
3490
+ const row = resolveSession(c);
3491
+ const killed = deps2.manager.killProcess(row.sessionId, pathParam(c, "processId"));
3492
+ if (!killed) {
3493
+ throw new HttpError(
3494
+ 404,
3495
+ "process_not_found",
3496
+ "Process does not exist or has already exited."
3497
+ );
3498
+ }
3499
+ return c.body(null, 204);
3500
+ });
3250
3501
  app2.post("/:sessionId/retry-now", (c) => {
3251
3502
  const row = resolveSession(c);
3252
3503
  const skipped = deps2.manager.retryNow(row.sessionId);
@@ -3368,7 +3619,7 @@ function sessionsRoutes(deps2) {
3368
3619
 
3369
3620
  // src/http/routes/chat-defaults.ts
3370
3621
  function chatDefaultsRoutes(deps2) {
3371
- const app2 = new Hono9();
3622
+ const app2 = new Hono11();
3372
3623
  app2.get("/", async (c) => {
3373
3624
  const projectId = requireValidId(c, "projectId");
3374
3625
  deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
@@ -3402,7 +3653,7 @@ function chatDefaultsRoutes(deps2) {
3402
3653
  }
3403
3654
 
3404
3655
  // src/http/routes/vault.ts
3405
- import { Hono as Hono10 } from "hono";
3656
+ import { Hono as Hono12 } from "hono";
3406
3657
  function parseVaultUpdate(body) {
3407
3658
  if (!Array.isArray(body.entries)) throw badRequest("entries must be an array.");
3408
3659
  const entries = body.entries.map((item, i) => {
@@ -3424,7 +3675,7 @@ function parseVaultUpdate(body) {
3424
3675
  return { entries };
3425
3676
  }
3426
3677
  function vaultRoutes(deps2) {
3427
- const app2 = new Hono10();
3678
+ const app2 = new Hono12();
3428
3679
  app2.get("/", async (c) => {
3429
3680
  const projectId = requireValidId(c, "projectId");
3430
3681
  const agentId = requireValidId(c, "agentId");
@@ -3440,12 +3691,62 @@ function vaultRoutes(deps2) {
3440
3691
  deps2.manager.invalidateAgentRuntimes(projectId, agentId);
3441
3692
  return c.json(res);
3442
3693
  });
3694
+ app2.post("/template-placeholder", async (c) => {
3695
+ const projectId = requireValidId(c, "projectId");
3696
+ const agentId = requireValidId(c, "agentId");
3697
+ deps2.projectService.requireProjectOwner(c.var.user.userId, projectId);
3698
+ const view = await deps2.agentConfigService.insertTemplatePlaceholder(
3699
+ projectId,
3700
+ agentId,
3701
+ "vault"
3702
+ );
3703
+ return c.json(view.config.vault);
3704
+ });
3705
+ return app2;
3706
+ }
3707
+
3708
+ // src/http/routes/memory.ts
3709
+ import { Hono as Hono13 } from "hono";
3710
+ function memoryRoutes(deps2) {
3711
+ const app2 = new Hono13();
3712
+ const scope = (c) => {
3713
+ const projectId = requireValidId(c, "projectId");
3714
+ const agentId = requireValidId(c, "agentId");
3715
+ deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
3716
+ return { projectId, agentId };
3717
+ };
3718
+ app2.get("/", async (c) => {
3719
+ const { projectId, agentId } = scope(c);
3720
+ return c.json(await deps2.memoryService.overview(projectId, agentId));
3721
+ });
3722
+ app2.post("/template-placeholder", async (c) => {
3723
+ const { projectId, agentId } = scope(c);
3724
+ return c.json(await deps2.memoryService.insertTemplatePlaceholder(projectId, agentId));
3725
+ });
3726
+ app2.get("/scopes/:scopeKey/files", async (c) => {
3727
+ const { projectId, agentId } = scope(c);
3728
+ const key = pathParam(c, "scopeKey");
3729
+ return c.json(await deps2.memoryService.listFiles(projectId, agentId, key));
3730
+ });
3731
+ app2.get("/scopes/:scopeKey/files/:fileName", async (c) => {
3732
+ const { projectId, agentId } = scope(c);
3733
+ const key = pathParam(c, "scopeKey");
3734
+ const name = pathParam(c, "fileName");
3735
+ return c.json(await deps2.memoryService.readFile(projectId, agentId, key, name));
3736
+ });
3737
+ app2.delete("/scopes/:scopeKey/files/:fileName", async (c) => {
3738
+ const { projectId, agentId } = scope(c);
3739
+ const key = pathParam(c, "scopeKey");
3740
+ const name = pathParam(c, "fileName");
3741
+ await deps2.memoryService.deleteFile(projectId, agentId, key, name);
3742
+ return c.body(null, 204);
3743
+ });
3443
3744
  return app2;
3444
3745
  }
3445
3746
 
3446
3747
  // src/http/routes/schedules.ts
3447
3748
  import { createHash as createHash2 } from "crypto";
3448
- import { Hono as Hono11 } from "hono";
3749
+ import { Hono as Hono14 } from "hono";
3449
3750
  import { isValidId as isValidId3 } from "@prismshadow/penguin-core";
3450
3751
 
3451
3752
  // src/runtime/schedule-file.ts
@@ -3827,7 +4128,7 @@ function requireScheduleName(raw) {
3827
4128
  return raw;
3828
4129
  }
3829
4130
  function scheduleRoutes(deps2) {
3830
- const app2 = new Hono11();
4131
+ const app2 = new Hono14();
3831
4132
  app2.get("/", async (c) => {
3832
4133
  const projectId = requireValidId(c, "projectId");
3833
4134
  const agentId = requireValidId(c, "agentId");
@@ -3854,6 +4155,17 @@ function scheduleRoutes(deps2) {
3854
4155
  await upsert(deps2, c.var.user.userId, projectId, agentId, name, body);
3855
4156
  return c.json(await readItem(deps2, projectId, agentId, name), 201);
3856
4157
  });
4158
+ app2.post("/template-placeholder", async (c) => {
4159
+ const projectId = requireValidId(c, "projectId");
4160
+ const agentId = requireValidId(c, "agentId");
4161
+ deps2.projectService.requireProjectOwner(c.var.user.userId, projectId);
4162
+ const view = await deps2.agentConfigService.insertTemplatePlaceholder(
4163
+ projectId,
4164
+ agentId,
4165
+ "schedules"
4166
+ );
4167
+ return c.json(view.config.schedules);
4168
+ });
3857
4169
  app2.get("/:name", async (c) => {
3858
4170
  const projectId = requireValidId(c, "projectId");
3859
4171
  const agentId = requireValidId(c, "agentId");
@@ -3915,7 +4227,7 @@ async function readItem(deps2, projectId, agentId, name) {
3915
4227
  }
3916
4228
 
3917
4229
  // src/http/routes/benchmarks.ts
3918
- import { Hono as Hono12 } from "hono";
4230
+ import { Hono as Hono15 } from "hono";
3919
4231
  var TEXT_PREVIEW_BYTES = 256 * 1024;
3920
4232
  function listCaseFiles(deps2, material) {
3921
4233
  return async (c) => {
@@ -3968,7 +4280,7 @@ function readCaseFile(deps2, material) {
3968
4280
  };
3969
4281
  }
3970
4282
  function benchmarksRoutes(deps2) {
3971
- const app2 = new Hono12();
4283
+ const app2 = new Hono15();
3972
4284
  app2.get("/", async (c) => {
3973
4285
  const projectId = requireValidId(c, "projectId");
3974
4286
  const agentId = requireValidId(c, "agentId");
@@ -3995,7 +4307,7 @@ function benchmarksRoutes(deps2) {
3995
4307
  import fs7 from "fs/promises";
3996
4308
  import path6 from "path";
3997
4309
  import { unzipSync, strFromU8, zipSync } from "fflate";
3998
- import { Hono as Hono13 } from "hono";
4310
+ import { Hono as Hono16 } from "hono";
3999
4311
  import {
4000
4312
  installSkill,
4001
4313
  listInstalledSkills,
@@ -4142,12 +4454,12 @@ function parseInstallNames(body) {
4142
4454
  });
4143
4455
  }
4144
4456
  function skillLibraryRoutes() {
4145
- const app2 = new Hono13();
4457
+ const app2 = new Hono16();
4146
4458
  app2.get("/", (c) => c.json(libraryResponse()));
4147
4459
  return app2;
4148
4460
  }
4149
4461
  function agentSkillsRoutes(deps2) {
4150
- const app2 = new Hono13();
4462
+ const app2 = new Hono16();
4151
4463
  const listResponse = async (projectId, agentId) => ({
4152
4464
  skills: (await listInstalledSkills(deps2.config.root, projectId, agentId)).map(toMetadataItem)
4153
4465
  });
@@ -4158,6 +4470,17 @@ function agentSkillsRoutes(deps2) {
4158
4470
  await deps2.agentConfigService.requireExists(projectId, agentId);
4159
4471
  return c.json(await listResponse(projectId, agentId));
4160
4472
  });
4473
+ app2.post("/template-placeholder", async (c) => {
4474
+ const projectId = requireValidId(c, "projectId");
4475
+ const agentId = requireValidId(c, "agentId");
4476
+ deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
4477
+ const view = await deps2.agentConfigService.insertTemplatePlaceholder(
4478
+ projectId,
4479
+ agentId,
4480
+ "skills"
4481
+ );
4482
+ return c.json(view.config.skills);
4483
+ });
4161
4484
  app2.post("/", async (c) => {
4162
4485
  const projectId = requireValidId(c, "projectId");
4163
4486
  const agentId = requireValidId(c, "agentId");
@@ -4195,11 +4518,11 @@ function agentSkillsRoutes(deps2) {
4195
4518
  const skill = parseSkillArchive(archive);
4196
4519
  const dir = path6.join(skillsDir(deps2.config.root, projectId, agentId), skill.name);
4197
4520
  if (!overwrite) {
4198
- const installed = await fs7.access(path6.join(dir, "SKILL.md")).then(
4521
+ const installed2 = await fs7.access(path6.join(dir, "SKILL.md")).then(
4199
4522
  () => true,
4200
4523
  () => false
4201
4524
  );
4202
- if (installed) {
4525
+ if (installed2) {
4203
4526
  throw new HttpError(409, "skill_exists", `Skill is already installed: ${skill.name}`);
4204
4527
  }
4205
4528
  }
@@ -4254,10 +4577,10 @@ function agentSkillsRoutes(deps2) {
4254
4577
 
4255
4578
  // src/http/routes/agent-transfer.ts
4256
4579
  import fs8 from "fs/promises";
4257
- import { Hono as Hono14 } from "hono";
4580
+ import { Hono as Hono17 } from "hono";
4258
4581
  var MAX_ARCHIVE_BYTES2 = 14 * 1024 * 1024;
4259
4582
  function agentTransferRoutes(deps2) {
4260
- const app2 = new Hono14();
4583
+ const app2 = new Hono17();
4261
4584
  app2.get("/export", async (c) => {
4262
4585
  const projectId = requireValidId(c, "projectId");
4263
4586
  const agentId = requireValidId(c, "agentId");
@@ -4296,7 +4619,7 @@ function agentTransferRoutes(deps2) {
4296
4619
  }
4297
4620
 
4298
4621
  // src/http/routes/agents.ts
4299
- import { Hono as Hono15 } from "hono";
4622
+ import { Hono as Hono18 } from "hono";
4300
4623
 
4301
4624
  // src/http/settle.ts
4302
4625
  async function settleWithin(promises, ms) {
@@ -4310,7 +4633,7 @@ async function settleWithin(promises, ms) {
4310
4633
  // src/http/routes/agents.ts
4311
4634
  var ACTIVITY_DAYS = 30;
4312
4635
  function agentsRoutes(deps2) {
4313
- const app2 = new Hono15();
4636
+ const app2 = new Hono18();
4314
4637
  app2.get("/", async (c) => {
4315
4638
  const projectId = requireValidId(c, "projectId");
4316
4639
  deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
@@ -4377,9 +4700,9 @@ function agentsRoutes(deps2) {
4377
4700
  import fs9 from "fs/promises";
4378
4701
  import os from "os";
4379
4702
  import path7 from "path";
4380
- import { Hono as Hono16 } from "hono";
4703
+ import { Hono as Hono19 } from "hono";
4381
4704
  function dirsRoutes(deps2) {
4382
- const app2 = new Hono16();
4705
+ const app2 = new Hono19();
4383
4706
  app2.get("/", async (c) => {
4384
4707
  const projectId = requireValidId(c, "projectId");
4385
4708
  deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
@@ -4420,9 +4743,14 @@ function dirsRoutes(deps2) {
4420
4743
  }
4421
4744
 
4422
4745
  // src/http/routes/agent-config.ts
4423
- import { Hono as Hono17 } from "hono";
4746
+ import { mkdtemp, rm } from "fs/promises";
4747
+ import { tmpdir } from "os";
4748
+ import { join } from "path";
4749
+ import { Hono as Hono20 } from "hono";
4750
+ import { McpToolProvider, resolveMCPServer } from "@prismshadow/penguin-core";
4751
+ var MCP_TEST_TIMEOUT_CAP_MS = 3e4;
4424
4752
  function agentConfigRoutes(deps2) {
4425
- const app2 = new Hono17();
4753
+ const app2 = new Hono20();
4426
4754
  app2.get("/", async (c) => {
4427
4755
  const projectId = requireValidId(c, "projectId");
4428
4756
  const agentId = requireValidId(c, "agentId");
@@ -4454,6 +4782,62 @@ function agentConfigRoutes(deps2) {
4454
4782
  activeSessionCount: deps2.manager.activeCountForAgent(projectId, agentId)
4455
4783
  });
4456
4784
  });
4785
+ app2.post("/mcp-test", async (c) => {
4786
+ const projectId = requireValidId(c, "projectId");
4787
+ const agentId = requireValidId(c, "agentId");
4788
+ deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
4789
+ const body = await readJson(c);
4790
+ if (typeof body.name !== "string" || body.name.length === 0) {
4791
+ throw badRequest("name must be a non-empty string.");
4792
+ }
4793
+ if (body.config === null || typeof body.config !== "object" || Array.isArray(body.config)) {
4794
+ throw badRequest("config must be an object.");
4795
+ }
4796
+ const entry = { name: body.name, config: body.config };
4797
+ let resolved;
4798
+ try {
4799
+ resolved = resolveMCPServer(entry);
4800
+ } catch (err) {
4801
+ throw badRequest(err instanceof Error ? err.message : String(err));
4802
+ }
4803
+ if (resolved.connectTimeoutMs > MCP_TEST_TIMEOUT_CAP_MS) {
4804
+ entry.config = { ...entry.config, connectTimeoutMs: MCP_TEST_TIMEOUT_CAP_MS };
4805
+ }
4806
+ const warnings = [];
4807
+ const workspaceDir = await mkdtemp(join(tmpdir(), "penguin-mcp-test-"));
4808
+ const provider = new McpToolProvider([entry], {
4809
+ workspaceDir,
4810
+ warn: (m) => warnings.push(m)
4811
+ });
4812
+ const startedAt = Date.now();
4813
+ try {
4814
+ const tools = await provider.listTools();
4815
+ const latencyMs = Date.now() - startedAt;
4816
+ const outcome = provider.connectResults()[0];
4817
+ if (outcome === void 0 || outcome.status !== "completed") {
4818
+ return c.json({
4819
+ ok: false,
4820
+ error: outcome?.error ?? (warnings.length > 0 ? warnings.join("; ") : "connect failed"),
4821
+ latencyMs
4822
+ });
4823
+ }
4824
+ return c.json({
4825
+ ok: true,
4826
+ tools: tools.map((t) => t.name),
4827
+ latencyMs
4828
+ });
4829
+ } finally {
4830
+ await provider.close();
4831
+ void rm(workspaceDir, { recursive: true, force: true }).catch(() => void 0);
4832
+ }
4833
+ });
4834
+ app2.post("/kernel-update", async (c) => {
4835
+ const projectId = requireValidId(c, "projectId");
4836
+ const agentId = requireValidId(c, "agentId");
4837
+ deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
4838
+ const result = await deps2.agentConfigService.kernelUpdate(projectId, agentId);
4839
+ return c.json(result);
4840
+ });
4457
4841
  app2.post("/reset", async (c) => {
4458
4842
  const projectId = requireValidId(c, "projectId");
4459
4843
  const agentId = requireValidId(c, "agentId");
@@ -4469,7 +4853,7 @@ function agentConfigRoutes(deps2) {
4469
4853
  }
4470
4854
 
4471
4855
  // src/http/routes/agent-traces.ts
4472
- import { Hono as Hono18 } from "hono";
4856
+ import { Hono as Hono21 } from "hono";
4473
4857
  var MAX_TRACE_BYTES = 14 * 1024 * 1024;
4474
4858
  var SESSION_CATEGORIES2 = [
4475
4859
  "active",
@@ -4478,7 +4862,7 @@ var SESSION_CATEGORIES2 = [
4478
4862
  "archived"
4479
4863
  ];
4480
4864
  function agentTracesRoutes(deps2) {
4481
- const app2 = new Hono18();
4865
+ const app2 = new Hono21();
4482
4866
  app2.get("/", async (c) => {
4483
4867
  const projectId = requireValidId(c, "projectId");
4484
4868
  const agentId = requireValidId(c, "agentId");
@@ -4556,10 +4940,10 @@ function agentTracesRoutes(deps2) {
4556
4940
  }
4557
4941
 
4558
4942
  // src/http/routes/usage.ts
4559
- import { Hono as Hono19 } from "hono";
4943
+ import { Hono as Hono22 } from "hono";
4560
4944
  var GROUP_BYS = ["date", "agent", "model", "session"];
4561
4945
  function usageRoutes(deps2) {
4562
- const app2 = new Hono19();
4946
+ const app2 = new Hono22();
4563
4947
  app2.get("/", async (c) => {
4564
4948
  const projectId = requireValidId(c, "projectId");
4565
4949
  deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
@@ -4614,7 +4998,7 @@ function usageRoutes(deps2) {
4614
4998
  // src/http/routes/version.ts
4615
4999
  import { spawn } from "child_process";
4616
5000
  import { BUILD_DATE, VERSION } from "@prismshadow/penguin-core";
4617
- import { Hono as Hono20 } from "hono";
5001
+ import { Hono as Hono23 } from "hono";
4618
5002
  var OUTPUT_CAP = 64 * 1024;
4619
5003
  var UPDATE_TIMEOUT_MS = 10 * 60 * 1e3;
4620
5004
  var REFUSAL_MARKERS = [
@@ -4680,7 +5064,7 @@ function runSelfUpdate(cliEntry) {
4680
5064
  });
4681
5065
  }
4682
5066
  function versionRoutes(deps2) {
4683
- const app2 = new Hono20();
5067
+ const app2 = new Hono23();
4684
5068
  let inflightRun = null;
4685
5069
  app2.get("/", (c) => {
4686
5070
  return c.json({ version: VERSION, buildDate: BUILD_DATE });
@@ -5419,13 +5803,14 @@ function compactUnavailable(why) {
5419
5803
  };
5420
5804
  return new HttpError(409, "nothing_to_compact", messages[why]);
5421
5805
  }
5422
- function createCoreSessionLoader(root, sources) {
5806
+ function createCoreSessionLoader(root, sources, opts = {}) {
5423
5807
  return {
5424
5808
  async load(row) {
5425
5809
  const agent = await createAgent({
5426
5810
  root,
5427
5811
  projectId: row.projectId,
5428
- agentId: row.agentId
5812
+ agentId: row.agentId,
5813
+ ...opts.proxyEnv ? { proxyEnv: opts.proxyEnv } : {}
5429
5814
  });
5430
5815
  const located = await findLatestTraceFile(
5431
5816
  tracesDir(root, row.projectId, row.agentId),
@@ -5570,6 +5955,20 @@ var SessionManager = class {
5570
5955
  liveFragments(sessionId) {
5571
5956
  return this.liveTail.fragments(sessionId);
5572
5957
  }
5958
+ /**
5959
+ * The running Task's input messages as published at launch; empty when idle. The engine
5960
+ * writes these exact envelopes to the Trace only after the first run's bootstrap (MCP
5961
+ * connect + discovery), so GET /messages appends whichever of them the Trace read has
5962
+ * not caught up to yet — without this, a client rebuilding history during the connect
5963
+ * (the draft flow subscribes only after the input publish) loses the user's own message.
5964
+ */
5965
+ pendingInputs(sessionId) {
5966
+ return this.entries.get(sessionId)?.pendingInputs ?? [];
5967
+ }
5968
+ /** The running Task's streamed bootstrap records (see RuntimeEntry.pendingBootstrap); empty when idle. */
5969
+ pendingBootstrap(sessionId) {
5970
+ return this.entries.get(sessionId)?.pendingBootstrap ?? [];
5971
+ }
5573
5972
  /** Number of Sessions for this Agent that are currently running / compacting. */
5574
5973
  activeCountForAgent(projectId, agentId) {
5575
5974
  let n = 0;
@@ -5593,6 +5992,8 @@ var SessionManager = class {
5593
5992
  running: null,
5594
5993
  generation: this.generationOf(row.projectId, row.agentId),
5595
5994
  followUps: [],
5995
+ pendingInputs: [],
5996
+ pendingBootstrap: [],
5596
5997
  pendingSteering: [],
5597
5998
  lastActivityMs: Date.now()
5598
5999
  });
@@ -5696,6 +6097,8 @@ var SessionManager = class {
5696
6097
  entry.status = "running";
5697
6098
  entry.abort = ac;
5698
6099
  entry.lastActivityMs = Date.now();
6100
+ entry.pendingInputs = [...entry.pendingInputs, ...args.input];
6101
+ entry.pendingBootstrap = [];
5699
6102
  this.publishState(entry, "running");
5700
6103
  const approve = makeApprove({
5701
6104
  getMode: () => this.deps.sessions.findById(entry.sessionId)?.approvalMode ?? "always-ask",
@@ -5810,6 +6213,8 @@ var SessionManager = class {
5810
6213
  entry.status = "running";
5811
6214
  entry.abort = ac;
5812
6215
  entry.lastActivityMs = Date.now();
6216
+ entry.pendingInputs = [...entry.pendingInputs, ...input];
6217
+ entry.pendingBootstrap = [];
5813
6218
  for (const msg of input) channel.publish(msg);
5814
6219
  this.publishState(entry, "running");
5815
6220
  const approve = makeApprove({
@@ -5933,6 +6338,34 @@ var SessionManager = class {
5933
6338
  entry.abort.abort();
5934
6339
  return true;
5935
6340
  }
6341
+ /**
6342
+ * Background command processes of a LOADED session (empty when the entry is not in
6343
+ * the active table — a resumed entry starts with a fresh environment and can only
6344
+ * ever report an empty list, so nothing is resurrected just to answer a poll).
6345
+ */
6346
+ listProcesses(sessionId) {
6347
+ return this.entries.get(sessionId)?.session.listBackgroundCommands?.() ?? [];
6348
+ }
6349
+ /** Kills one background command process of a loaded session; false when the session isn't loaded or the id is unknown. */
6350
+ killProcess(sessionId, processId) {
6351
+ const entry = this.entries.get(sessionId);
6352
+ if (!entry) return false;
6353
+ const killed = entry.session.killBackgroundCommand?.(processId) ?? false;
6354
+ if (killed) entry.lastActivityMs = Date.now();
6355
+ return killed;
6356
+ }
6357
+ /**
6358
+ * Disposes a just-removed entry's runtime — after its in-flight drive (if any)
6359
+ * settles, so interrupt cleanup never races a dying environment. Deleting a Session /
6360
+ * Agent / Project is the one intent that must also end the background processes the
6361
+ * conversation started (a dev server surviving its deleted conversation is
6362
+ * unreachable from every UI, running forever).
6363
+ */
6364
+ disposeRemoved(entry) {
6365
+ const dispose = () => entry.session.dispose?.();
6366
+ if (entry.running) void entry.running.then(dispose, dispose);
6367
+ else dispose();
6368
+ }
5936
6369
  /**
5937
6370
  * Before deleting a Project, converge all its active runs and clear them out of the
5938
6371
  * active table. Returns the in-flight drive Promises of the affected entries: the
@@ -5947,6 +6380,7 @@ var SessionManager = class {
5947
6380
  entry.abort?.abort();
5948
6381
  if (entry.running) runnings.push(entry.running);
5949
6382
  this.entries.delete(key);
6383
+ this.disposeRemoved(entry);
5950
6384
  }
5951
6385
  return runnings;
5952
6386
  }
@@ -5968,6 +6402,7 @@ var SessionManager = class {
5968
6402
  entry.abort?.abort();
5969
6403
  if (entry.running) runnings.push(entry.running);
5970
6404
  this.entries.delete(key);
6405
+ this.disposeRemoved(entry);
5971
6406
  }
5972
6407
  return runnings;
5973
6408
  }
@@ -5992,6 +6427,7 @@ var SessionManager = class {
5992
6427
  entry.approvals.denyAll();
5993
6428
  entry.abort?.abort();
5994
6429
  this.entries.delete(sessionId);
6430
+ this.disposeRemoved(entry);
5995
6431
  return entry.running ? [entry.running] : [];
5996
6432
  }
5997
6433
  endSessionDeletion(sessionId) {
@@ -6027,6 +6463,7 @@ var SessionManager = class {
6027
6463
  for (const [key, entry] of this.entries) {
6028
6464
  if (entry.status !== "idle" || entry.approvals.size !== 0 || entry.running !== null) continue;
6029
6465
  if (entry.followUps.length > 0) continue;
6466
+ if (entry.session.listBackgroundCommands?.().some((p) => p.running)) continue;
6030
6467
  if (now - entry.lastActivityMs <= idleMs) continue;
6031
6468
  this.entries.delete(key);
6032
6469
  }
@@ -6119,6 +6556,8 @@ var SessionManager = class {
6119
6556
  running: null,
6120
6557
  generation,
6121
6558
  followUps: [],
6559
+ pendingInputs: [],
6560
+ pendingBootstrap: [],
6122
6561
  pendingSteering: [],
6123
6562
  lastActivityMs: Date.now()
6124
6563
  };
@@ -6202,6 +6641,16 @@ var SessionManager = class {
6202
6641
  }
6203
6642
  }
6204
6643
  }
6644
+ if (!msg.origin || msg.origin.length === 0) {
6645
+ const bt = msg.payload.type;
6646
+ if (bt === "mcp_connect_begin" || bt === "mcp_connect_end" || bt === "tool_list_ready") {
6647
+ entry.pendingBootstrap.push(msg);
6648
+ }
6649
+ if (bt === "request_begin") {
6650
+ entry.pendingInputs = [];
6651
+ entry.pendingBootstrap = [];
6652
+ }
6653
+ }
6205
6654
  this.liveTail.observe(entry.sessionId, msg);
6206
6655
  this.deps.channels.get(entry.sessionId).publish(msg);
6207
6656
  watcher?.observe(msg);
@@ -6849,6 +7298,7 @@ var AdminService = class {
6849
7298
  }
6850
7299
  this.deps.users.updatePassword(userId, await hashPassword(password), true);
6851
7300
  this.deps.authSessions.deleteByUser(userId);
7301
+ this.deps.onPasswordChanged?.(userId);
6852
7302
  }
6853
7303
  /** Delete user: the built-in admin cannot be deleted; owned Projects (including data directories) are deleted along with it. */
6854
7304
  async deleteUser(userId) {
@@ -6900,36 +7350,34 @@ var DesktopService = class {
6900
7350
  }
6901
7351
  };
6902
7352
 
6903
- // src/http/routes/desktop.ts
6904
- import { Hono as Hono21 } from "hono";
6905
- var SHUTDOWN_DELAY_MS = 50;
6906
- function desktopRoutes(deps2) {
6907
- const app2 = new Hono21();
6908
- app2.post("/shutdown", (c) => {
6909
- const desktop = deps2.desktop;
6910
- if (!desktop) throw new HttpError(404, "not_found", "Desktop mode is not enabled.");
6911
- const header = c.req.header("authorization") ?? "";
6912
- const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
6913
- if (token === "" || !desktop.verifyToken(token)) {
6914
- throw new HttpError(401, "unauthorized", "Invalid desktop token.");
6915
- }
6916
- setTimeout(() => desktop.requestShutdown(), SHUTDOWN_DELAY_MS).unref();
6917
- return c.body(null, 202);
6918
- });
6919
- return app2;
6920
- }
6921
-
6922
7353
  // src/services/agent-config-service.ts
6923
7354
  import fs13 from "fs/promises";
6924
7355
  import { parseDocument, parse as parseYaml } from "yaml";
6925
7356
  import {
7357
+ DEFAULT_MEMORY_PROMPT,
7358
+ DEFAULT_MEMORY_WORKSPACE_PROMPT,
7359
+ DEFAULT_SCHEDULES_PROMPT,
7360
+ DEFAULT_SKILLS_PROMPT,
7361
+ DEFAULT_VAULT_PROMPT,
7362
+ KERNEL_VERSION,
7363
+ LEGACY_SKILLS_SECTION,
7364
+ LEGACY_VAULT_SECTION,
6926
7365
  agentsMdPath,
7366
+ applyKernelUpdate,
7367
+ isKernelOutdated,
6927
7368
  agentStateDir,
6928
7369
  agentStateVersion,
7370
+ hasSchedulesPlaceholder,
7371
+ hasSkillsPlaceholder,
7372
+ hasVaultPlaceholder,
7373
+ insertSchedulesPlaceholder,
7374
+ insertSkillsPlaceholder,
7375
+ insertVaultPlaceholder,
6929
7376
  VAULT_VALUE_MAX_LENGTH,
6930
7377
  isValidVaultKey,
6931
7378
  loadAgentVault,
6932
7379
  resetSystemConfigToDefaults,
7380
+ resolveMCPServer as resolveMCPServer2,
6933
7381
  saveAgentVault,
6934
7382
  systemConfigPath
6935
7383
  } from "@prismshadow/penguin-core";
@@ -7530,10 +7978,13 @@ var AgentConfigService = class {
7530
7978
  ...typeof parsed.name === "string" ? { name: parsed.name } : {},
7531
7979
  ...typeof parsed.description === "string" ? { description: parsed.description } : {},
7532
7980
  toolCount: countOf(tools.builtin) + countOf(tools.mcpServers),
7533
- version: agentStateVersion({ version: parsed.version })
7981
+ version: agentStateVersion({ version: parsed.version }),
7982
+ kernelOutdated: isKernelOutdated(
7983
+ typeof parsed.kernel_version === "string" ? parsed.kernel_version : null
7984
+ )
7534
7985
  };
7535
7986
  } catch {
7536
- return { toolCount: 0, version: 1 };
7987
+ return { toolCount: 0, version: 1, kernelOutdated: true };
7537
7988
  }
7538
7989
  }
7539
7990
  /** Structured config view (matching the edit form's shape) + raw text + AGENTS.md + State path. */
@@ -7544,6 +7995,10 @@ var AgentConfigService = class {
7544
7995
  const parsed = asRecord(parseYaml(systemConfigYaml));
7545
7996
  const model = asRecord(parsed.model);
7546
7997
  const compaction = asRecord(parsed.compaction);
7998
+ const memory = asRecord(parsed.memory);
7999
+ const vault = asRecord(parsed.vault);
8000
+ const skills = asRecord(parsed.skills);
8001
+ const schedules = asRecord(parsed.schedules);
7547
8002
  const tools = asRecord(parsed.tools);
7548
8003
  let agentsMd = "";
7549
8004
  try {
@@ -7561,14 +8016,45 @@ var AgentConfigService = class {
7561
8016
  ...compaction.mode === "summarize" || compaction.mode === "discard" ? { mode: compaction.mode } : {},
7562
8017
  ...typeof compaction.prompt === "string" ? { prompt: compaction.prompt } : {}
7563
8018
  };
8019
+ const memoryDto = {
8020
+ enabled: memory.enabled !== false,
8021
+ prompt: typeof memory.prompt === "string" ? memory.prompt : DEFAULT_MEMORY_PROMPT,
8022
+ workspacePrompt: typeof memory.workspace_prompt === "string" ? memory.workspace_prompt : DEFAULT_MEMORY_WORKSPACE_PROMPT
8023
+ };
8024
+ const systemPrompt = typeof parsed.system_prompt === "string" ? parsed.system_prompt : "";
8025
+ const vaultDto = {
8026
+ enabled: vault.enabled !== false,
8027
+ prompt: typeof vault.prompt === "string" ? vault.prompt : DEFAULT_VAULT_PROMPT,
8028
+ templateHasPlaceholder: hasVaultPlaceholder(systemPrompt),
8029
+ legacySectionPresent: systemPrompt.includes(LEGACY_VAULT_SECTION)
8030
+ };
8031
+ const skillsDto = {
8032
+ enabled: skills.enabled !== false,
8033
+ prompt: typeof skills.prompt === "string" ? skills.prompt : DEFAULT_SKILLS_PROMPT,
8034
+ templateHasPlaceholder: hasSkillsPlaceholder(systemPrompt),
8035
+ legacySectionPresent: systemPrompt.includes(LEGACY_SKILLS_SECTION)
8036
+ };
8037
+ const schedulesDto = {
8038
+ enabled: schedules.enabled !== false,
8039
+ prompt: typeof schedules.prompt === "string" ? schedules.prompt : DEFAULT_SCHEDULES_PROMPT,
8040
+ templateHasPlaceholder: hasSchedulesPlaceholder(systemPrompt)
8041
+ };
8042
+ const kernelVersion = typeof parsed.kernel_version === "string" ? parsed.kernel_version : null;
7564
8043
  const config2 = {
7565
8044
  ...typeof parsed.name === "string" ? { name: parsed.name } : {},
7566
8045
  ...typeof parsed.description === "string" ? { description: parsed.description } : {},
7567
8046
  version: agentStateVersion({ version: parsed.version }),
7568
- systemPrompt: typeof parsed.system_prompt === "string" ? parsed.system_prompt : "",
8047
+ kernelVersion,
8048
+ kernelLatest: KERNEL_VERSION,
8049
+ kernelOutdated: isKernelOutdated(kernelVersion),
8050
+ systemPrompt,
7569
8051
  ...typeof parsed.max_turns === "number" ? { maxTurns: parsed.max_turns } : {},
7570
8052
  ...Object.keys(modelDto).length > 0 ? { model: modelDto } : {},
7571
8053
  ...Object.keys(compactionDto).length > 0 ? { compaction: compactionDto } : {},
8054
+ memory: memoryDto,
8055
+ vault: vaultDto,
8056
+ skills: skillsDto,
8057
+ schedules: schedulesDto,
7572
8058
  toolsBuiltin: Array.isArray(tools.builtin) ? tools.builtin : [],
7573
8059
  mcpServers: Array.isArray(tools.mcpServers) ? tools.mcpServers : []
7574
8060
  };
@@ -7605,12 +8091,22 @@ var AgentConfigService = class {
7605
8091
  await this.requireExists(projectId, agentId);
7606
8092
  await resetSystemConfigToDefaults(this.root, projectId, agentId);
7607
8093
  }
8094
+ /**
8095
+ * Smart-merges the config up to the current defaults generation (core's applyKernelUpdate):
8096
+ * leaves still carrying a recorded generation's default follow the new defaults, user
8097
+ * customizations are kept and reported; the config is stamped with the new kernel version.
8098
+ * The destructive full-refresh alternative stays resetConfig.
8099
+ */
8100
+ async kernelUpdate(projectId, agentId) {
8101
+ await this.requireExists(projectId, agentId);
8102
+ return applyKernelUpdate(this.root, projectId, agentId);
8103
+ }
7608
8104
  async applyConfigUpdate(projectId, agentId, config2) {
7609
8105
  const cfg = config2;
7610
8106
  const yamlPath = systemConfigPath(this.root, projectId, agentId);
7611
8107
  const doc = parseDocument(await fs13.readFile(yamlPath, "utf8"));
7612
- const setIfProvided = (path19, value) => {
7613
- if (value !== void 0) doc.setIn(path19, value);
8108
+ const setIfProvided = (path20, value) => {
8109
+ if (value !== void 0) doc.setIn(path20, value);
7614
8110
  };
7615
8111
  setIfProvided(["name"], optionalString(cfg, "name", { maxLen: 100, label: "name" }));
7616
8112
  setIfProvided(
@@ -7653,6 +8149,27 @@ var AgentConfigService = class {
7653
8149
  setIfProvided(["compaction", "mode"], optionalEnum(compaction, "mode", COMPACTION_MODES));
7654
8150
  setIfProvided(["compaction", "prompt"], optionalString(compaction, "prompt"));
7655
8151
  }
8152
+ if (cfg.memory !== void 0) {
8153
+ const memory = asRecord(cfg.memory);
8154
+ setIfProvided(["memory", "enabled"], optionalBoolean(memory, "enabled"));
8155
+ setIfProvided(["memory", "prompt"], optionalString(memory, "prompt"));
8156
+ setIfProvided(["memory", "workspace_prompt"], optionalString(memory, "workspacePrompt"));
8157
+ }
8158
+ if (cfg.vault !== void 0) {
8159
+ const vault = asRecord(cfg.vault);
8160
+ setIfProvided(["vault", "enabled"], optionalBoolean(vault, "enabled"));
8161
+ setIfProvided(["vault", "prompt"], optionalString(vault, "prompt"));
8162
+ }
8163
+ if (cfg.skills !== void 0) {
8164
+ const skills = asRecord(cfg.skills);
8165
+ setIfProvided(["skills", "enabled"], optionalBoolean(skills, "enabled"));
8166
+ setIfProvided(["skills", "prompt"], optionalString(skills, "prompt"));
8167
+ }
8168
+ if (cfg.schedules !== void 0) {
8169
+ const schedules = asRecord(cfg.schedules);
8170
+ setIfProvided(["schedules", "enabled"], optionalBoolean(schedules, "enabled"));
8171
+ setIfProvided(["schedules", "prompt"], optionalString(schedules, "prompt"));
8172
+ }
7656
8173
  if (cfg.toolsBuiltin !== void 0) {
7657
8174
  doc.setIn(["tools", "builtin"], validateToolsBuiltin(cfg.toolsBuiltin));
7658
8175
  }
@@ -7661,6 +8178,26 @@ var AgentConfigService = class {
7661
8178
  }
7662
8179
  await fs13.writeFile(yamlPath, doc.toString(), "utf8");
7663
8180
  }
8181
+ /**
8182
+ * Inserts a feature's section placeholder into the Agent's prompt template — the explicit
8183
+ * adoption path mirroring the Memory tab's endpoint; nothing ever inserts automatically.
8184
+ * For skills/vault the insert is migration-first (core's helpers): a stored template still
8185
+ * carrying the legacy hardcoded section verbatim gets it replaced in place by the
8186
+ * placeholder, otherwise the placeholder is inserted before `# Environment`. Idempotent;
8187
+ * returns the refreshed config view (the route picks out the feature's DTO).
8188
+ */
8189
+ async insertTemplatePlaceholder(projectId, agentId, feature) {
8190
+ const view = await this.getConfig(projectId, agentId);
8191
+ const insert = {
8192
+ vault: insertVaultPlaceholder,
8193
+ skills: insertSkillsPlaceholder,
8194
+ schedules: insertSchedulesPlaceholder
8195
+ }[feature];
8196
+ const next = insert(view.config.systemPrompt);
8197
+ if (next === view.config.systemPrompt) return view;
8198
+ await this.updateConfig(projectId, agentId, { config: { systemPrompt: next } });
8199
+ return this.getConfig(projectId, agentId);
8200
+ }
7664
8201
  /** Read the Agent vault (agent_state/.vault.toml): values are always masked, plaintext is never sent to the client. */
7665
8202
  async getVault(projectId, agentId) {
7666
8203
  await this.requireExists(projectId, agentId);
@@ -7743,6 +8280,7 @@ function validateToolsBuiltin(value) {
7743
8280
  }
7744
8281
  function validateMcpServers(value) {
7745
8282
  if (!Array.isArray(value)) throw badRequest("mcpServers must be an array.");
8283
+ const seen = /* @__PURE__ */ new Set();
7746
8284
  return value.map((item, i) => {
7747
8285
  const s = asRecord(item);
7748
8286
  if (typeof s.name !== "string" || s.name.length === 0) {
@@ -7751,13 +8289,273 @@ function validateMcpServers(value) {
7751
8289
  if (s.config === null || typeof s.config !== "object" || Array.isArray(s.config)) {
7752
8290
  throw badRequest(`mcpServers[${i}].config must be an object.`);
7753
8291
  }
8292
+ try {
8293
+ resolveMCPServer2(s);
8294
+ } catch (err) {
8295
+ throw badRequest(`mcpServers[${i}]: ${err instanceof Error ? err.message : String(err)}`);
8296
+ }
8297
+ if (seen.has(s.name)) {
8298
+ throw badRequest(`mcpServers[${i}]: duplicate server name "${s.name}".`);
8299
+ }
8300
+ seen.add(s.name);
7754
8301
  return s;
7755
8302
  });
7756
8303
  }
7757
8304
 
7758
- // src/services/agent-service.ts
8305
+ // src/services/memory-service.ts
7759
8306
  import fs14 from "fs/promises";
7760
8307
  import path11 from "path";
8308
+ import {
8309
+ MEMORY_INDEX_FILENAME,
8310
+ USER_SCOPE_KEY,
8311
+ hasMemoryPlaceholder,
8312
+ insertMemoryPlaceholder,
8313
+ memoryDir,
8314
+ memoryScopeDir,
8315
+ parseMemoryFrontmatter,
8316
+ readWorkspaceMarker
8317
+ } from "@prismshadow/penguin-core";
8318
+ var SCOPE_KEY_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
8319
+ function isTopicFileName(name) {
8320
+ return name.length > 0 && !name.startsWith(".") && !/[/\\]/.test(name) && name.toLowerCase().endsWith(".md") && name.toLowerCase() !== MEMORY_INDEX_FILENAME.toLowerCase();
8321
+ }
8322
+ var MemoryService = class {
8323
+ constructor(root, agentConfigService) {
8324
+ this.root = root;
8325
+ this.agentConfigService = agentConfigService;
8326
+ }
8327
+ root;
8328
+ agentConfigService;
8329
+ /** The tab's landing payload: the Agent-level switch and one entry per scope directory. */
8330
+ async overview(projectId, agentId) {
8331
+ const view = await this.agentConfigService.getConfig(projectId, agentId);
8332
+ return {
8333
+ enabled: view.config.memory.enabled,
8334
+ templateHasMemory: hasMemoryPlaceholder(view.config.systemPrompt),
8335
+ memoryDir: memoryDir(this.root, projectId, agentId),
8336
+ scopes: await this.listScopes(projectId, agentId)
8337
+ };
8338
+ }
8339
+ /**
8340
+ * Inserts the `{{MEMORY}}` placeholder into the Agent's prompt template — the explicit
8341
+ * adoption path for an Agent created before Memory shipped; nothing inserts automatically.
8342
+ * Idempotent: a template that already carries it is left as it is (the refreshed overview
8343
+ * reports `templateHasMemory` either way).
8344
+ */
8345
+ async insertTemplatePlaceholder(projectId, agentId) {
8346
+ const view = await this.agentConfigService.getConfig(projectId, agentId);
8347
+ const next = insertMemoryPlaceholder(view.config.systemPrompt);
8348
+ if (next !== view.config.systemPrompt) {
8349
+ await this.agentConfigService.updateConfig(projectId, agentId, {
8350
+ config: { systemPrompt: next }
8351
+ });
8352
+ }
8353
+ return this.overview(projectId, agentId);
8354
+ }
8355
+ /**
8356
+ * The scope directories under `memory/`: the User scope first — always listed, even before it
8357
+ * exists on disk, so a memory can be filed there without waiting for a Session to create it —
8358
+ * then the Workspaces, newest activity first (one with no topic file yet sorts last, by key).
8359
+ */
8360
+ async listScopes(projectId, agentId) {
8361
+ const base = memoryDir(this.root, projectId, agentId);
8362
+ let entries = [];
8363
+ try {
8364
+ entries = (await fs14.readdir(base, { withFileTypes: true })).filter((e) => e.isDirectory() && e.name !== USER_SCOPE_KEY).map((e) => e.name);
8365
+ } catch {
8366
+ }
8367
+ const workspaces = await Promise.all(
8368
+ entries.map((key) => this.scopeInfo(path11.join(base, key), key, "workspace"))
8369
+ );
8370
+ workspaces.sort((a, b) => {
8371
+ if (a.updatedAt && b.updatedAt) return b.updatedAt.localeCompare(a.updatedAt);
8372
+ if (a.updatedAt) return -1;
8373
+ if (b.updatedAt) return 1;
8374
+ return a.scopeKey.localeCompare(b.scopeKey);
8375
+ });
8376
+ const userScope = await this.scopeInfo(path11.join(base, USER_SCOPE_KEY), USER_SCOPE_KEY, "user");
8377
+ return [userScope, ...workspaces];
8378
+ }
8379
+ async scopeInfo(dir, key, kind) {
8380
+ const files = await this.topicFileNames(dir);
8381
+ let latest = 0;
8382
+ for (const name of files) {
8383
+ try {
8384
+ const stat = await fs14.stat(path11.join(dir, name));
8385
+ latest = Math.max(latest, stat.mtimeMs);
8386
+ } catch {
8387
+ }
8388
+ }
8389
+ const workspacePath = kind === "workspace" ? await readWorkspaceMarker(dir) : void 0;
8390
+ return {
8391
+ scopeKey: key,
8392
+ kind,
8393
+ ...workspacePath !== void 0 ? { workspacePath } : {},
8394
+ fileCount: files.length,
8395
+ ...latest > 0 ? { updatedAt: new Date(latest).toISOString() } : {}
8396
+ };
8397
+ }
8398
+ /** Topic files of one scope: regular Markdown files only — the index, the `.workspace` marker, stray directories and symlinks (dirents that are not regular files) all stay out. */
8399
+ async topicFileNames(dir) {
8400
+ try {
8401
+ return (await fs14.readdir(dir, { withFileTypes: true })).filter((e) => e.isFile() && isTopicFileName(e.name)).map((e) => e.name).sort((a, b) => a.localeCompare(b));
8402
+ } catch {
8403
+ return [];
8404
+ }
8405
+ }
8406
+ async listFiles(projectId, agentId, scopeKey) {
8407
+ const dir = await this.requireScopeDir(projectId, agentId, scopeKey);
8408
+ const names = await this.topicFileNames(dir);
8409
+ const files = [];
8410
+ for (const name of names) {
8411
+ const info = await this.fileInfo(dir, name);
8412
+ if (info) files.push(info);
8413
+ }
8414
+ return { scopeKey, files };
8415
+ }
8416
+ async readFile(projectId, agentId, scopeKey, fileName) {
8417
+ const dir = await this.requireScopeDir(projectId, agentId, scopeKey);
8418
+ const target = this.resolveFile(dir, fileName);
8419
+ try {
8420
+ const stat = await fs14.lstat(target);
8421
+ if (!stat.isFile()) throw new Error("not a regular file");
8422
+ const content = await fs14.readFile(target, "utf8");
8423
+ return {
8424
+ scopeKey,
8425
+ file: this.describe(fileName, content, stat.size, stat.mtime),
8426
+ content
8427
+ };
8428
+ } catch {
8429
+ throw new HttpError(404, "memory_file_not_found", `Memory file not found: ${fileName}`);
8430
+ }
8431
+ }
8432
+ /**
8433
+ * Deletes a topic file and mechanically drops its lines from the scope's `MEMORY.md`: any
8434
+ * line whose Markdown link target is exactly this file (`](<file>)`) goes; a mention of the
8435
+ * name in ordinary prose, or a link to a different file, survives. This is the only write the
8436
+ * API performs on Memory content — everything else is the model's, through a chat Session.
8437
+ */
8438
+ async deleteFile(projectId, agentId, scopeKey, fileName) {
8439
+ const dir = await this.requireScopeDir(projectId, agentId, scopeKey);
8440
+ const target = this.resolveFile(dir, fileName);
8441
+ try {
8442
+ await fs14.unlink(target);
8443
+ } catch {
8444
+ throw new HttpError(404, "memory_file_not_found", `Memory file not found: ${fileName}`);
8445
+ }
8446
+ await this.pruneIndexLines(dir, fileName);
8447
+ }
8448
+ /** Removes index lines linking to a deleted file. Best-effort: a missing or unreadable index is left alone. */
8449
+ async pruneIndexLines(dir, fileName) {
8450
+ const indexPath = path11.join(dir, MEMORY_INDEX_FILENAME);
8451
+ let content;
8452
+ try {
8453
+ content = await fs14.readFile(indexPath, "utf8");
8454
+ } catch {
8455
+ return;
8456
+ }
8457
+ const escaped = fileName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8458
+ const link = new RegExp(`\\]\\(\\s*<?(?:\\./)?${escaped}>?\\s*(?:"[^"]*"\\s*)?\\)`);
8459
+ const lines = content.split("\n");
8460
+ const kept = lines.filter((line) => !link.test(line));
8461
+ if (kept.length === lines.length) return;
8462
+ await fs14.writeFile(indexPath, kept.join("\n"), "utf8");
8463
+ }
8464
+ async fileInfo(dir, name) {
8465
+ try {
8466
+ const [content, stat] = await Promise.all([
8467
+ fs14.readFile(path11.join(dir, name), "utf8"),
8468
+ fs14.stat(path11.join(dir, name))
8469
+ ]);
8470
+ return this.describe(name, content, stat.size, stat.mtime);
8471
+ } catch {
8472
+ return null;
8473
+ }
8474
+ }
8475
+ /** One listing entry: frontmatter (falling back to the file name for an unparsed file) plus file stats. */
8476
+ describe(name, content, size, mtime) {
8477
+ const meta = parseMemoryFrontmatter(content, name);
8478
+ return {
8479
+ name,
8480
+ title: meta?.name ?? name,
8481
+ description: meta?.description ?? "",
8482
+ ...meta?.updatedAt !== void 0 ? { updatedAt: meta.updatedAt } : {},
8483
+ size,
8484
+ modifiedAt: mtime.toISOString()
8485
+ };
8486
+ }
8487
+ /**
8488
+ * Validates a scope key and returns its directory, 404 if it does not exist. The key is only
8489
+ * ever a single directory name.
8490
+ *
8491
+ * A Workspace directory is created by a Session, so this API never creates one — a key with no
8492
+ * directory means no Session has run there and there is nothing to list. The User scope is the
8493
+ * exception: it belongs to the Agent rather than to any Session, so it is created on demand
8494
+ * (Agents that predate Memory have no `memory/user/` until their next Session).
8495
+ */
8496
+ async requireScopeDir(projectId, agentId, scopeKey) {
8497
+ await this.agentConfigService.requireExists(projectId, agentId);
8498
+ if (!SCOPE_KEY_PATTERN.test(scopeKey)) {
8499
+ throw badRequest("scopeKey is invalid.");
8500
+ }
8501
+ const dir = memoryScopeDir(this.root, projectId, agentId, scopeKey);
8502
+ if (scopeKey === USER_SCOPE_KEY) {
8503
+ await fs14.mkdir(dir, { recursive: true });
8504
+ return this.requireRealScopeDir(projectId, agentId, scopeKey, dir);
8505
+ }
8506
+ try {
8507
+ if ((await fs14.stat(dir)).isDirectory()) {
8508
+ return this.requireRealScopeDir(projectId, agentId, scopeKey, dir);
8509
+ }
8510
+ } catch {
8511
+ }
8512
+ throw new HttpError(
8513
+ 404,
8514
+ "memory_scope_not_found",
8515
+ `No Memory directory for scope: ${scopeKey}`
8516
+ );
8517
+ }
8518
+ /**
8519
+ * Symlink hardening for the scope directory itself: a scope smuggled in as a symlink (the
8520
+ * model can create one with its file tools) would carry every read and delete outside
8521
+ * `memory/`, so the resolved real path must be exactly `<real memory/>/<scopeKey>` — not a
8522
+ * link's target, wherever it points.
8523
+ */
8524
+ async requireRealScopeDir(projectId, agentId, scopeKey, dir) {
8525
+ try {
8526
+ const realBase = await fs14.realpath(memoryDir(this.root, projectId, agentId));
8527
+ if (await fs14.realpath(dir) === path11.join(realBase, scopeKey)) return dir;
8528
+ } catch {
8529
+ }
8530
+ throw new HttpError(
8531
+ 404,
8532
+ "memory_scope_not_found",
8533
+ `No Memory directory for scope: ${scopeKey}`
8534
+ );
8535
+ }
8536
+ /**
8537
+ * The absolute path of a topic file inside a scope directory. The name must pass the
8538
+ * character rule, must not be the reserved index name, and the joined path is checked
8539
+ * for containment — belt and braces, since the rule already excludes separators.
8540
+ */
8541
+ resolveFile(dir, fileName) {
8542
+ if (!isTopicFileName(fileName)) {
8543
+ throw badRequest(
8544
+ `File name must be a Markdown topic file \u2014 no path, no leading dot, and not the ${MEMORY_INDEX_FILENAME} index.`
8545
+ );
8546
+ }
8547
+ const target = path11.join(dir, fileName);
8548
+ const rel = path11.relative(dir, target);
8549
+ if (rel !== fileName || rel.includes(path11.sep)) {
8550
+ throw badRequest("File name must not contain a path.");
8551
+ }
8552
+ return target;
8553
+ }
8554
+ };
8555
+
8556
+ // src/services/agent-service.ts
8557
+ import fs15 from "fs/promises";
8558
+ import path12 from "path";
7761
8559
  import {
7762
8560
  agentDir,
7763
8561
  agentsDir as agentsDir2,
@@ -7766,6 +8564,7 @@ import {
7766
8564
  createAgent as coreCreateAgent,
7767
8565
  isValidId as isValidId4,
7768
8566
  loadAgentVault as loadAgentVault2,
8567
+ memoryDir as memoryDir2,
7769
8568
  scheduleDir as scheduleDir2,
7770
8569
  skillsDir as skillsDir2,
7771
8570
  systemConfigPath as systemConfigPath2
@@ -7784,7 +8583,7 @@ var AgentService = class {
7784
8583
  const known = new Map(this.agents.list(projectId).map((r) => [r.agentId, r]));
7785
8584
  let entries = [];
7786
8585
  try {
7787
- const dirents = await fs14.readdir(agentsDir2(this.root, projectId), { withFileTypes: true });
8586
+ const dirents = await fs15.readdir(agentsDir2(this.root, projectId), { withFileTypes: true });
7788
8587
  entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
7789
8588
  } catch {
7790
8589
  }
@@ -7793,7 +8592,7 @@ var AgentService = class {
7793
8592
  const configPath = systemConfigPath2(this.root, projectId, agentId);
7794
8593
  let createdAt;
7795
8594
  try {
7796
- const stat = await fs14.stat(configPath);
8595
+ const stat = await fs15.stat(configPath);
7797
8596
  createdAt = (stat.birthtime.getTime() > 0 ? stat.birthtime : stat.mtime).toISOString();
7798
8597
  } catch {
7799
8598
  continue;
@@ -7807,12 +8606,13 @@ var AgentService = class {
7807
8606
  );
7808
8607
  return Promise.all(
7809
8608
  sorted.map(async (row) => {
7810
- const [meta, updatedAt, vaultKeyCount, scheduleCount, skillCount] = await Promise.all([
8609
+ const [meta, updatedAt, vaultKeyCount, scheduleCount, skillCount, memoryCount] = await Promise.all([
7811
8610
  this.agentConfig.readCardMeta(projectId, row.agentId),
7812
8611
  this.configUpdatedAt(projectId, row.agentId),
7813
8612
  this.vaultKeyCount(projectId, row.agentId),
7814
8613
  this.scheduleCount(projectId, row.agentId),
7815
- this.skillCount(projectId, row.agentId)
8614
+ this.skillCount(projectId, row.agentId),
8615
+ this.memoryCount(projectId, row.agentId)
7816
8616
  ]);
7817
8617
  return {
7818
8618
  agentId: row.agentId,
@@ -7821,7 +8621,8 @@ var AgentService = class {
7821
8621
  ...updatedAt !== void 0 ? { updatedAt } : {},
7822
8622
  vaultKeyCount,
7823
8623
  scheduleCount,
7824
- skillCount
8624
+ skillCount,
8625
+ memoryCount
7825
8626
  };
7826
8627
  })
7827
8628
  );
@@ -7837,7 +8638,7 @@ var AgentService = class {
7837
8638
  /** Number of scheduled tasks: count of .toml files under schedule/ (0 if the directory doesn't exist). */
7838
8639
  async scheduleCount(projectId, agentId) {
7839
8640
  try {
7840
- const names = await fs14.readdir(scheduleDir2(this.root, projectId, agentId));
8641
+ const names = await fs15.readdir(scheduleDir2(this.root, projectId, agentId));
7841
8642
  return names.filter((n) => n.endsWith(".toml")).length;
7842
8643
  } catch {
7843
8644
  return 0;
@@ -7848,14 +8649,14 @@ var AgentService = class {
7848
8649
  const base = skillsDir2(this.root, projectId, agentId);
7849
8650
  let dirents;
7850
8651
  try {
7851
- dirents = await fs14.readdir(base, { withFileTypes: true });
8652
+ dirents = await fs15.readdir(base, { withFileTypes: true });
7852
8653
  } catch {
7853
8654
  return 0;
7854
8655
  }
7855
8656
  const present = await Promise.all(
7856
8657
  dirents.filter((d) => d.isDirectory()).map(async (d) => {
7857
8658
  try {
7858
- await fs14.access(path11.join(base, d.name, "SKILL.md"));
8659
+ await fs15.access(path12.join(base, d.name, "SKILL.md"));
7859
8660
  return true;
7860
8661
  } catch {
7861
8662
  return false;
@@ -7864,6 +8665,27 @@ var AgentService = class {
7864
8665
  );
7865
8666
  return present.filter(Boolean).length;
7866
8667
  }
8668
+ /** Number of memory topic files: regular `*.md` files (minus each scope's MEMORY.md index) summed over the scope directories under memory/ (0 if the directory doesn't exist). */
8669
+ async memoryCount(projectId, agentId) {
8670
+ const base = memoryDir2(this.root, projectId, agentId);
8671
+ let scopes;
8672
+ try {
8673
+ scopes = await fs15.readdir(base, { withFileTypes: true });
8674
+ } catch {
8675
+ return 0;
8676
+ }
8677
+ const counts = await Promise.all(
8678
+ scopes.filter((d) => d.isDirectory()).map(async (d) => {
8679
+ try {
8680
+ const files = await fs15.readdir(path12.join(base, d.name), { withFileTypes: true });
8681
+ return files.filter((f) => f.isFile() && isTopicFileName(f.name)).length;
8682
+ } catch {
8683
+ return 0;
8684
+ }
8685
+ })
8686
+ );
8687
+ return counts.reduce((sum, n) => sum + n, 0);
8688
+ }
7867
8689
  /** Last config modification time: the later of system_config.yaml and AGENTS.md mtime; omitted if neither is readable. */
7868
8690
  async configUpdatedAt(projectId, agentId) {
7869
8691
  const paths = [
@@ -7873,7 +8695,7 @@ var AgentService = class {
7873
8695
  const times = await Promise.all(
7874
8696
  paths.map(async (p) => {
7875
8697
  try {
7876
- return (await fs14.stat(p)).mtime.getTime();
8698
+ return (await fs15.stat(p)).mtime.getTime();
7877
8699
  } catch {
7878
8700
  return 0;
7879
8701
  }
@@ -7898,7 +8720,7 @@ var AgentService = class {
7898
8720
  "Built-in Agents (default_agent) are provisioned with the Project and cannot be deleted from the web."
7899
8721
  );
7900
8722
  }
7901
- await fs14.rm(agentDir(this.root, projectId, agentId), { recursive: true, force: true });
8723
+ await fs15.rm(agentDir(this.root, projectId, agentId), { recursive: true, force: true });
7902
8724
  this.agents.delete(projectId, agentId);
7903
8725
  }
7904
8726
  /**
@@ -7915,7 +8737,7 @@ var AgentService = class {
7915
8737
  `Agent id must be 2\u201364 characters: ${SEMANTIC_ID_RULE}.`
7916
8738
  );
7917
8739
  }
7918
- const taken = this.agents.exists(projectId, agentId) || await fs14.stat(agentDir(this.root, projectId, agentId)).then(
8740
+ const taken = this.agents.exists(projectId, agentId) || await fs15.stat(agentDir(this.root, projectId, agentId)).then(
7919
8741
  () => true,
7920
8742
  () => false
7921
8743
  );
@@ -7929,7 +8751,7 @@ var AgentService = class {
7929
8751
  config: { name: displayName, ...description !== void 0 ? { description } : {} }
7930
8752
  });
7931
8753
  } catch (err) {
7932
- await fs14.rm(agentDir(this.root, projectId, agentId), { recursive: true, force: true }).catch(() => {
8754
+ await fs15.rm(agentDir(this.root, projectId, agentId), { recursive: true, force: true }).catch(() => {
7933
8755
  });
7934
8756
  throw err;
7935
8757
  }
@@ -7944,17 +8766,19 @@ var AgentService = class {
7944
8766
  updatedAt: createdAt,
7945
8767
  toolCount: meta.toolCount,
7946
8768
  version: meta.version,
8769
+ kernelOutdated: meta.kernelOutdated,
7947
8770
  vaultKeyCount: 0,
7948
8771
  scheduleCount: 0,
7949
8772
  // Read the real count: coreCreateAgent seeds the default skill set for default_agent.
7950
- skillCount: await this.skillCount(projectId, agentId)
8773
+ skillCount: await this.skillCount(projectId, agentId),
8774
+ memoryCount: 0
7951
8775
  };
7952
8776
  }
7953
8777
  };
7954
8778
 
7955
8779
  // src/services/benchmark-service.ts
7956
- import fs15 from "fs/promises";
7957
- import path12 from "path";
8780
+ import fs16 from "fs/promises";
8781
+ import path13 from "path";
7958
8782
  import { parse as parseToml3 } from "smol-toml";
7959
8783
  import { parse as parseYaml2 } from "yaml";
7960
8784
  import { benchmarksDir } from "@prismshadow/penguin-core";
@@ -7985,15 +8809,15 @@ function stringOr(v) {
7985
8809
  return typeof v === "string" && v !== "" ? v : void 0;
7986
8810
  }
7987
8811
  function isWithin(parent, child) {
7988
- const relative = path12.relative(parent, child);
7989
- return relative !== "" && !relative.startsWith("..") && !path12.isAbsolute(relative);
8812
+ const relative = path13.relative(parent, child);
8813
+ return relative !== "" && !relative.startsWith("..") && !path13.isAbsolute(relative);
7990
8814
  }
7991
8815
  function statementTitle(statement, fallback) {
7992
8816
  const heading = /^#\s+(.+)$/m.exec(statement)?.[1]?.trim();
7993
8817
  return heading?.replace(/^Case\s+\d+\s*:\s*/i, "") || fallback;
7994
8818
  }
7995
8819
  async function readStatementTitle(readme, fallback) {
7996
- const handle = await fs15.open(readme, "r");
8820
+ const handle = await fs16.open(readme, "r");
7997
8821
  try {
7998
8822
  const buffer = Buffer.alloc(STATEMENT_TITLE_READ_BYTES);
7999
8823
  const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
@@ -8075,28 +8899,28 @@ var BenchmarkService = class {
8075
8899
  const dir = benchmarksDir(this.root, projectId, agentId);
8076
8900
  let items;
8077
8901
  try {
8078
- const entries = await fs15.readdir(dir, { withFileTypes: true });
8902
+ const entries = await fs16.readdir(dir, { withFileTypes: true });
8079
8903
  items = entries.map((e) => ({ name: e.name, isDir: e.isDirectory() }));
8080
8904
  } catch {
8081
8905
  return { benchmarks: [] };
8082
8906
  }
8083
8907
  const benchmarks = [];
8084
8908
  for (const item of items.filter((i) => i.isDir).sort((a, b) => a.name.localeCompare(b.name))) {
8085
- benchmarks.push(await this.readBenchmark(path12.join(dir, item.name), item.name));
8909
+ benchmarks.push(await this.readBenchmark(path13.join(dir, item.name), item.name));
8086
8910
  }
8087
8911
  return { benchmarks };
8088
8912
  }
8089
8913
  async listCases(projectId, agentId, benchmarkId) {
8090
8914
  const baseDir = benchmarksDir(this.root, projectId, agentId);
8091
- const benchDir = path12.join(baseDir, benchmarkId);
8915
+ const benchDir = path13.join(baseDir, benchmarkId);
8092
8916
  let entries;
8093
8917
  let realBaseDir;
8094
8918
  let realBenchDir;
8095
8919
  try {
8096
8920
  [entries, realBaseDir, realBenchDir] = await Promise.all([
8097
- fs15.readdir(benchDir, { withFileTypes: true }),
8098
- fs15.realpath(baseDir),
8099
- fs15.realpath(benchDir)
8921
+ fs16.readdir(benchDir, { withFileTypes: true }),
8922
+ fs16.realpath(baseDir),
8923
+ fs16.realpath(benchDir)
8100
8924
  ]);
8101
8925
  } catch {
8102
8926
  return { cases: [] };
@@ -8113,7 +8937,7 @@ var BenchmarkService = class {
8113
8937
  entry.name,
8114
8938
  "statement"
8115
8939
  );
8116
- const realReadme = await fs15.realpath(path12.join(statementDir, "README.md"));
8940
+ const realReadme = await fs16.realpath(path13.join(statementDir, "README.md"));
8117
8941
  if (!isWithin(statementDir, realReadme)) throw new Error("README escapes Statement");
8118
8942
  cases.push({
8119
8943
  id: entry.name,
@@ -8146,16 +8970,16 @@ var BenchmarkService = class {
8146
8970
  return this.workspaceFiles.read(materialRoot, rel, options);
8147
8971
  }
8148
8972
  async caseMaterialRoot(projectId, agentId, benchmarkId, caseId, material) {
8149
- const benchDir = path12.join(benchmarksDir(this.root, projectId, agentId), benchmarkId);
8150
- const caseDir = path12.join(benchDir, caseId);
8151
- const materialRoot = path12.join(caseDir, material);
8973
+ const benchDir = path13.join(benchmarksDir(this.root, projectId, agentId), benchmarkId);
8974
+ const caseDir = path13.join(benchDir, caseId);
8975
+ const materialRoot = path13.join(caseDir, material);
8152
8976
  try {
8153
8977
  const [realBenchDir, realCaseDir, realMaterialRoot] = await Promise.all([
8154
- fs15.realpath(benchDir),
8155
- fs15.realpath(caseDir),
8156
- fs15.realpath(materialRoot)
8978
+ fs16.realpath(benchDir),
8979
+ fs16.realpath(caseDir),
8980
+ fs16.realpath(materialRoot)
8157
8981
  ]);
8158
- if (!isWithin(realBenchDir, realCaseDir) || path12.dirname(realMaterialRoot) !== realCaseDir || path12.basename(realMaterialRoot) !== material) {
8982
+ if (!isWithin(realBenchDir, realCaseDir) || path13.dirname(realMaterialRoot) !== realCaseDir || path13.basename(realMaterialRoot) !== material) {
8159
8983
  throw new Error("Case material is not canonical");
8160
8984
  }
8161
8985
  return realMaterialRoot;
@@ -8169,7 +8993,7 @@ var BenchmarkService = class {
8169
8993
  let runs;
8170
8994
  try {
8171
8995
  const config2 = asRecord2(
8172
- parseToml3(await fs15.readFile(path12.join(benchDir, "benchmark_config.toml"), "utf8"))
8996
+ parseToml3(await fs16.readFile(path13.join(benchDir, "benchmark_config.toml"), "utf8"))
8173
8997
  );
8174
8998
  if (typeof config2.title === "string" && config2.title !== "") title = config2.title;
8175
8999
  if (typeof config2.description === "string" && config2.description !== "") {
@@ -8184,7 +9008,7 @@ var BenchmarkService = class {
8184
9008
  let evaluations = [];
8185
9009
  try {
8186
9010
  const scoreboard = asRecord2(
8187
- parseYaml2(await fs15.readFile(path12.join(benchDir, "scoreboard.yaml"), "utf8"))
9011
+ parseYaml2(await fs16.readFile(path13.join(benchDir, "scoreboard.yaml"), "utf8"))
8188
9012
  );
8189
9013
  if (Array.isArray(scoreboard.evaluations)) {
8190
9014
  evaluations = scoreboard.evaluations.map(toEvaluation).filter((e) => e !== null);
@@ -8193,7 +9017,7 @@ var BenchmarkService = class {
8193
9017
  }
8194
9018
  let caseCount = 0;
8195
9019
  try {
8196
- const entries = await fs15.readdir(benchDir, { withFileTypes: true });
9020
+ const entries = await fs16.readdir(benchDir, { withFileTypes: true });
8197
9021
  caseCount = entries.filter((e) => e.isDirectory() && e.name.startsWith("CASE-")).length;
8198
9022
  } catch {
8199
9023
  }
@@ -8210,8 +9034,8 @@ var BenchmarkService = class {
8210
9034
 
8211
9035
  // src/services/snapshot-service.ts
8212
9036
  import { randomBytes as randomBytes6 } from "crypto";
8213
- import fs16 from "fs/promises";
8214
- import path13 from "path";
9037
+ import fs17 from "fs/promises";
9038
+ import path14 from "path";
8215
9039
  import * as tar from "tar";
8216
9040
  import { parse as parseYaml3 } from "yaml";
8217
9041
  import {
@@ -8224,7 +9048,7 @@ import {
8224
9048
  } from "@prismshadow/penguin-core";
8225
9049
  var VAULT_BASENAME = ".vault.toml";
8226
9050
  function isVaultEntry(entryPath) {
8227
- return path13.posix.basename(entryPath.replaceAll("\\", "/")) === VAULT_BASENAME;
9051
+ return path14.posix.basename(entryPath.replaceAll("\\", "/")) === VAULT_BASENAME;
8228
9052
  }
8229
9053
  var SnapshotService = class {
8230
9054
  constructor(root) {
@@ -8235,7 +9059,7 @@ var SnapshotService = class {
8235
9059
  async currentVersion(projectId, agentId) {
8236
9060
  let raw;
8237
9061
  try {
8238
- raw = await fs16.readFile(systemConfigPath3(this.root, projectId, agentId), "utf8");
9062
+ raw = await fs17.readFile(systemConfigPath3(this.root, projectId, agentId), "utf8");
8239
9063
  } catch {
8240
9064
  throw new HttpError(404, "agent_not_found", "Agent does not exist.");
8241
9065
  }
@@ -8248,13 +9072,13 @@ var SnapshotService = class {
8248
9072
  async ensureSnapshot(projectId, agentId) {
8249
9073
  const version = await this.currentVersion(projectId, agentId);
8250
9074
  const dir = snapshotsDir(this.root, projectId, agentId);
8251
- const file = path13.join(dir, `v${version}.tar.gz`);
9075
+ const file = path14.join(dir, `v${version}.tar.gz`);
8252
9076
  try {
8253
- await fs16.access(file);
9077
+ await fs17.access(file);
8254
9078
  return { version, file };
8255
9079
  } catch {
8256
9080
  }
8257
- await fs16.mkdir(dir, { recursive: true });
9081
+ await fs17.mkdir(dir, { recursive: true });
8258
9082
  const tmp = `${file}.tmp-${randomBytes6(4).toString("hex")}`;
8259
9083
  await tar.create(
8260
9084
  {
@@ -8266,7 +9090,7 @@ var SnapshotService = class {
8266
9090
  },
8267
9091
  ["agent_state"]
8268
9092
  );
8269
- await fs16.rename(tmp, file);
9093
+ await fs17.rename(tmp, file);
8270
9094
  return { version, file };
8271
9095
  }
8272
9096
  /** Export: automatically packs a snapshot first if none exists, returns the info needed for download. */
@@ -8281,15 +9105,15 @@ var SnapshotService = class {
8281
9105
  * replaces `agent_state/` while keeping the current vault.
8282
9106
  */
8283
9107
  async importArchive(projectId, agentId, archive, confirm) {
8284
- const current = await this.currentVersion(projectId, agentId);
9108
+ const current2 = await this.currentVersion(projectId, agentId);
8285
9109
  const base = agentDir2(this.root, projectId, agentId);
8286
- const staging = path13.join(base, `.import-${randomBytes6(6).toString("hex")}`);
8287
- await fs16.mkdir(staging, { recursive: true });
9110
+ const staging = path14.join(base, `.import-${randomBytes6(6).toString("hex")}`);
9111
+ await fs17.mkdir(staging, { recursive: true });
8288
9112
  try {
8289
- const archiveFile = path13.join(staging, "archive.tar.gz");
8290
- await fs16.writeFile(archiveFile, archive);
8291
- const extractDir = path13.join(staging, "extracted");
8292
- await fs16.mkdir(extractDir, { recursive: true });
9113
+ const archiveFile = path14.join(staging, "archive.tar.gz");
9114
+ await fs17.writeFile(archiveFile, archive);
9115
+ const extractDir = path14.join(staging, "extracted");
9116
+ await fs17.mkdir(extractDir, { recursive: true });
8293
9117
  try {
8294
9118
  await tar.extract({
8295
9119
  file: archiveFile,
@@ -8299,10 +9123,10 @@ var SnapshotService = class {
8299
9123
  } catch {
8300
9124
  throw badRequest("Import failed: not a valid tar.gz snapshot package.");
8301
9125
  }
8302
- const configPath = path13.join(extractDir, "agent_state", "system_config.yaml");
9126
+ const configPath = path14.join(extractDir, "agent_state", "system_config.yaml");
8303
9127
  let parsed;
8304
9128
  try {
8305
- parsed = parseYaml3(await fs16.readFile(configPath, "utf8"));
9129
+ parsed = parseYaml3(await fs17.readFile(configPath, "utf8"));
8306
9130
  } catch {
8307
9131
  throw badRequest("Import failed: the package is missing agent_state/system_config.yaml.");
8308
9132
  }
@@ -8316,46 +9140,46 @@ var SnapshotService = class {
8316
9140
  const incoming = agentStateVersion2({
8317
9141
  version: typeof incomingRaw === "number" ? incomingRaw : void 0
8318
9142
  });
8319
- if (incoming <= current && !confirm) {
9143
+ if (incoming <= current2 && !confirm) {
8320
9144
  throw new HttpError(
8321
9145
  409,
8322
9146
  "version_conflict",
8323
- `Package version v${incoming} is not higher than the current v${current}; confirmation is required to import.`
9147
+ `Package version v${incoming} is not higher than the current v${current2}; confirmation is required to import.`
8324
9148
  );
8325
9149
  }
8326
9150
  await this.ensureSnapshot(projectId, agentId);
8327
9151
  const stateDir = agentStateDir2(this.root, projectId, agentId);
8328
- const incomingState = path13.join(extractDir, "agent_state");
9152
+ const incomingState = path14.join(extractDir, "agent_state");
8329
9153
  let vault = null;
8330
9154
  try {
8331
- vault = await fs16.readFile(agentVaultPath(this.root, projectId, agentId));
9155
+ vault = await fs17.readFile(agentVaultPath(this.root, projectId, agentId));
8332
9156
  } catch {
8333
9157
  }
8334
9158
  if (vault !== null) {
8335
- await fs16.writeFile(path13.join(incomingState, VAULT_BASENAME), vault, { mode: 384 });
9159
+ await fs17.writeFile(path14.join(incomingState, VAULT_BASENAME), vault, { mode: 384 });
8336
9160
  }
8337
- const trash = path13.join(staging, "replaced-agent_state");
8338
- await fs16.rename(stateDir, trash);
9161
+ const trash = path14.join(staging, "replaced-agent_state");
9162
+ await fs17.rename(stateDir, trash);
8339
9163
  try {
8340
- await fs16.rename(incomingState, stateDir);
9164
+ await fs17.rename(incomingState, stateDir);
8341
9165
  } catch (err) {
8342
- await fs16.rename(trash, stateDir);
9166
+ await fs17.rename(trash, stateDir);
8343
9167
  throw err;
8344
9168
  }
8345
9169
  return { version: incoming };
8346
9170
  } finally {
8347
- await fs16.rm(staging, { recursive: true, force: true });
9171
+ await fs17.rm(staging, { recursive: true, force: true });
8348
9172
  }
8349
9173
  }
8350
9174
  };
8351
9175
 
8352
9176
  // src/services/project-service.ts
8353
- import fs17 from "fs/promises";
9177
+ import fs18 from "fs/promises";
8354
9178
  import { DEFAULT_PROJECT_ID, projectDir, provisionProjectAgents } from "@prismshadow/penguin-core";
8355
9179
  var ABORT_SETTLE_TIMEOUT_MS = 5e3;
8356
- async function dirExists(path19) {
9180
+ async function dirExists(path20) {
8357
9181
  try {
8358
- const stat = await fs17.stat(path19);
9182
+ const stat = await fs18.stat(path20);
8359
9183
  return stat.isDirectory();
8360
9184
  } catch {
8361
9185
  return false;
@@ -8480,12 +9304,12 @@ var ProjectService = class {
8480
9304
  throw err;
8481
9305
  }
8482
9306
  try {
8483
- await fs17.mkdir(projectDir(this.deps.root, projectId), { recursive: true });
9307
+ await fs18.mkdir(projectDir(this.deps.root, projectId), { recursive: true });
8484
9308
  await this.deps.projectConfig.writeInitialConfig(projectId, displayName);
8485
9309
  await this.provisionBuiltinAgents(projectId);
8486
9310
  } catch (err) {
8487
9311
  this.deps.projects.delete(projectId);
8488
- await fs17.rm(projectDir(this.deps.root, projectId), { recursive: true, force: true }).catch(() => {
9312
+ await fs18.rm(projectDir(this.deps.root, projectId), { recursive: true, force: true }).catch(() => {
8489
9313
  });
8490
9314
  throw err;
8491
9315
  }
@@ -8584,7 +9408,7 @@ var ProjectService = class {
8584
9408
  this.deps.schedules.deleteByProject(projectId);
8585
9409
  this.deps.goals.deleteByProject(projectId);
8586
9410
  this.deps.traceIndex.removeProject(projectId);
8587
- await fs17.rm(projectDir(this.deps.root, projectId), { recursive: true, force: true });
9411
+ await fs18.rm(projectDir(this.deps.root, projectId), { recursive: true, force: true });
8588
9412
  }
8589
9413
  // —— Member authorization ——
8590
9414
  /** Member list: owner (role=owner) + members. */
@@ -8658,8 +9482,8 @@ var ProjectService = class {
8658
9482
  import { createAgent as createAgent2, isSessionMeta as isSessionMeta5 } from "@prismshadow/penguin-core";
8659
9483
 
8660
9484
  // src/services/trace-index.ts
8661
- import fs18 from "fs/promises";
8662
- import path14 from "path";
9485
+ import fs19 from "fs/promises";
9486
+ import path15 from "path";
8663
9487
  import { agentsDir as agentsDir3, isSessionMeta as isSessionMeta4, tracesDir as tracesDir2 } from "@prismshadow/penguin-core";
8664
9488
 
8665
9489
  // src/internal/trace-head.ts
@@ -8692,11 +9516,11 @@ async function readTraceHead(filePath) {
8692
9516
  var TRACE_FILE_RE = /^(.+)_(\d{3})\.jsonl$/;
8693
9517
  function traceFilePath(root, row) {
8694
9518
  const name = `${row.sessionId}_${String(row.fileIndex).padStart(3, "0")}.jsonl`;
8695
- return path14.join(tracesDir2(root, row.projectId, row.agentId), row.date, name);
9519
+ return path15.join(tracesDir2(root, row.projectId, row.agentId), row.date, name);
8696
9520
  }
8697
9521
  async function listDirs(dir) {
8698
9522
  try {
8699
- const entries = await fs18.readdir(dir, { withFileTypes: true });
9523
+ const entries = await fs19.readdir(dir, { withFileTypes: true });
8700
9524
  return entries.filter((e) => e.isDirectory()).map((e) => e.name);
8701
9525
  } catch {
8702
9526
  return [];
@@ -8704,7 +9528,7 @@ async function listDirs(dir) {
8704
9528
  }
8705
9529
  async function listFiles(dir) {
8706
9530
  try {
8707
- const entries = await fs18.readdir(dir, { withFileTypes: true });
9531
+ const entries = await fs19.readdir(dir, { withFileTypes: true });
8708
9532
  return entries.filter((e) => e.isFile()).map((e) => e.name);
8709
9533
  } catch {
8710
9534
  return [];
@@ -8789,7 +9613,7 @@ var TraceIndexService = class {
8789
9613
  const newest = [...cached.dates.keys()].sort().at(-1);
8790
9614
  if (newest === void 0) return;
8791
9615
  this.counters.gateStats += 1;
8792
- const m = await statMtime(path14.join(dir, newest));
9616
+ const m = await statMtime(path15.join(dir, newest));
8793
9617
  if (m !== null && m === cached.dates.get(newest)) return;
8794
9618
  }
8795
9619
  this.counters.dirScans += 1;
@@ -8803,14 +9627,14 @@ var TraceIndexService = class {
8803
9627
  const newSessions = /* @__PURE__ */ new Set();
8804
9628
  const dateDirs = await listDirs(dir);
8805
9629
  for (const date of dateDirs) {
8806
- const m = await statMtime(path14.join(dir, date));
9630
+ const m = await statMtime(path15.join(dir, date));
8807
9631
  if (m === null) continue;
8808
9632
  next.dates.set(date, cacheable(m));
8809
9633
  if (!force && cached?.dates.get(date) === m) continue;
8810
9634
  const known = new Map(
8811
9635
  (knownByDate.get(date) ?? []).map((r) => [`${r.sessionId}\0${r.fileIndex}`, r])
8812
9636
  );
8813
- for (const file of await listFiles(path14.join(dir, date))) {
9637
+ for (const file of await listFiles(path15.join(dir, date))) {
8814
9638
  const match = TRACE_FILE_RE.exec(file);
8815
9639
  if (!match) continue;
8816
9640
  const sessionId = match[1];
@@ -8818,7 +9642,7 @@ var TraceIndexService = class {
8818
9642
  const fileKey = `${sessionId}\0${fileIndex}`;
8819
9643
  let size = 0;
8820
9644
  try {
8821
- size = (await fs18.stat(path14.join(dir, date, file))).size;
9645
+ size = (await fs19.stat(path15.join(dir, date, file))).size;
8822
9646
  } catch {
8823
9647
  continue;
8824
9648
  }
@@ -9113,7 +9937,8 @@ var SessionService = class {
9113
9937
  const agent = await createAgent2({
9114
9938
  root: this.deps.root,
9115
9939
  projectId: args.projectId,
9116
- agentId: args.agentId
9940
+ agentId: args.agentId,
9941
+ ...this.deps.proxyEnv ? { proxyEnv: this.deps.proxyEnv } : {}
9117
9942
  });
9118
9943
  let session;
9119
9944
  try {
@@ -9230,8 +10055,8 @@ function localDate(d) {
9230
10055
  }
9231
10056
 
9232
10057
  // src/services/trace-service.ts
9233
- import fs19 from "fs/promises";
9234
- import path15 from "path";
10058
+ import fs20 from "fs/promises";
10059
+ import path16 from "path";
9235
10060
  import {
9236
10061
  isSessionMeta as isSessionMeta6,
9237
10062
  parseTraceLines as parseTraceLines2,
@@ -9277,15 +10102,15 @@ var TraceService = class {
9277
10102
  }));
9278
10103
  }
9279
10104
  /** All shard reads funnel through here (deps.observeShardRead is the windowed-read tests' proof of which files were touched). */
9280
- async readShard(path19) {
9281
- this.deps.observeShardRead?.(path19);
9282
- return readTraceTolerant2(path19);
10105
+ async readShard(path20) {
10106
+ this.deps.observeShardRead?.(path20);
10107
+ return readTraceTolerant2(path20);
9283
10108
  }
9284
10109
  /** Deletes all of this Session's Trace files (called when the Session is deleted); the index rows go with them. */
9285
10110
  async deleteSessionTraces(projectId, agentId, sessionId) {
9286
10111
  const files = await this.locateAll(projectId, agentId, sessionId);
9287
10112
  for (const file of files) {
9288
- await fs19.rm(file.path, { force: true });
10113
+ await fs20.rm(file.path, { force: true });
9289
10114
  }
9290
10115
  this.deps.index.removeSession(projectId, agentId, sessionId);
9291
10116
  }
@@ -9534,7 +10359,7 @@ var TraceService = class {
9534
10359
  const files = await this.locateAll(projectId, agentId, sessionId);
9535
10360
  const out = [];
9536
10361
  for (const file of files) {
9537
- const stat = await fs19.stat(file.path);
10362
+ const stat = await fs20.stat(file.path);
9538
10363
  out.push({
9539
10364
  index: file.index,
9540
10365
  date: file.date,
@@ -9566,7 +10391,9 @@ var TraceService = class {
9566
10391
  const usageTrend = [];
9567
10392
  const modelSegments = [];
9568
10393
  const toolSpans = [];
10394
+ const otherSpans = [];
9569
10395
  const openSpansById = /* @__PURE__ */ new Map();
10396
+ let openMcpConnect = null;
9570
10397
  let prevSerialTs = null;
9571
10398
  let taskIndex = -1;
9572
10399
  let continuation = false;
@@ -9665,6 +10492,22 @@ var TraceService = class {
9665
10492
  }
9666
10493
  openRequest = null;
9667
10494
  }
10495
+ } else if (p.type === "mcp_connect_begin") {
10496
+ if (!hasOrigin) openMcpConnect = { beginTs: msg.timestamp };
10497
+ } else if (p.type === "mcp_connect_end") {
10498
+ if (!hasOrigin && openMcpConnect) {
10499
+ const q = p;
10500
+ const failed = q.status !== "completed";
10501
+ otherSpans.push({
10502
+ key: `mcp-connect-${openMcpConnect.beginTs}`,
10503
+ name: "mcp connect",
10504
+ startTs: openMcpConnect.beginTs,
10505
+ endTs: msg.timestamp,
10506
+ taskIndex: taskIndex + 1,
10507
+ ...failed ? { failed: true } : {}
10508
+ });
10509
+ openMcpConnect = null;
10510
+ }
9668
10511
  } else if (p.type === "compaction_begin") {
9669
10512
  compactionCount++;
9670
10513
  if (!hasOrigin) {
@@ -9785,6 +10628,7 @@ var TraceService = class {
9785
10628
  toolCalls,
9786
10629
  modelSegments,
9787
10630
  toolSpans,
10631
+ otherSpans,
9788
10632
  reconnectCount,
9789
10633
  compactionCount,
9790
10634
  usageTrend
@@ -9883,7 +10727,7 @@ var TraceService = class {
9883
10727
  shard.map(async (f) => {
9884
10728
  let sizeBytes = f.sizeBytes;
9885
10729
  try {
9886
- sizeBytes = (await fs19.stat(traceFilePath(this.root, f))).size;
10730
+ sizeBytes = (await fs20.stat(traceFilePath(this.root, f))).size;
9887
10731
  if (sizeBytes !== f.sizeBytes) {
9888
10732
  this.deps.index.repo.updateFileSize(
9889
10733
  projectId,
@@ -9929,7 +10773,7 @@ var TraceService = class {
9929
10773
  /** Raw bytes of the Trace file at the given index (export/download: the file is served verbatim). */
9930
10774
  async readFileRaw(projectId, agentId, sessionId, index) {
9931
10775
  const file = await this.locateByIndex(projectId, agentId, sessionId, index);
9932
- return fs19.readFile(file.path);
10776
+ return fs20.readFile(file.path);
9933
10777
  }
9934
10778
  /**
9935
10779
  * Imports an uploaded Trace file (raw JSONL content). Validates the content itself —
@@ -9947,7 +10791,7 @@ var TraceService = class {
9947
10791
  const invalid = (message) => new HttpError(400, "invalid_trace", message);
9948
10792
  let records;
9949
10793
  try {
9950
- records = parseTraceLines2(content);
10794
+ records = parseTraceLines2(content, { onMalformed: "throw" });
9951
10795
  } catch {
9952
10796
  throw invalid("The file is not valid Trace JSONL.");
9953
10797
  }
@@ -9968,12 +10812,12 @@ var TraceService = class {
9968
10812
  const ts = Date.parse(first.timestamp);
9969
10813
  const date = formatLocalDate(Number.isNaN(ts) ? /* @__PURE__ */ new Date() : new Date(ts));
9970
10814
  const index = 1;
9971
- const dir = path15.join(tracesDir3(this.root, projectId, agentId), date);
9972
- await fs19.mkdir(dir, { recursive: true });
9973
- const file = path15.join(dir, `${sessionId}_${String(index).padStart(3, "0")}.jsonl`);
10815
+ const dir = path16.join(tracesDir3(this.root, projectId, agentId), date);
10816
+ await fs20.mkdir(dir, { recursive: true });
10817
+ const file = path16.join(dir, `${sessionId}_${String(index).padStart(3, "0")}.jsonl`);
9974
10818
  const body = content.replace(/\n+$/, "") + "\n";
9975
10819
  try {
9976
- await fs19.writeFile(file, body, { encoding: "utf8", flag: "wx" });
10820
+ await fs20.writeFile(file, body, { encoding: "utf8", flag: "wx" });
9977
10821
  } catch (err) {
9978
10822
  if (err.code === "EEXIST") throw duplicate();
9979
10823
  throw err;
@@ -10289,10 +11133,10 @@ var UsageService = class {
10289
11133
  };
10290
11134
 
10291
11135
  // src/http/routes/preview.ts
10292
- import { Hono as Hono22 } from "hono";
11136
+ import { Hono as Hono24 } from "hono";
10293
11137
  var DIRECTORY_INDEX = "index.html";
10294
11138
  function previewRoutes(deps2) {
10295
- const app2 = new Hono22();
11139
+ const app2 = new Hono24();
10296
11140
  app2.get("/:token/*", async (c) => {
10297
11141
  const token = c.req.param("token");
10298
11142
  const payload = token ? deps2.previewTokens.verify(token) : null;
@@ -10349,11 +11193,18 @@ function buildAppDeps(config2, overrides = {}) {
10349
11193
  const usageRepo = new UsageRepo(db);
10350
11194
  const errorsRepo = new ErrorsRepo(db);
10351
11195
  const prefsRepo = new UiPrefsRepo(db);
11196
+ const serverSettingsRepo = new ServerSettingsRepo(db);
11197
+ const proxyEnv = () => {
11198
+ if (!serverSettingsRepo.getProxyForAgent()) return { mode: "strip" };
11199
+ const url = serverSettingsRepo.getProxyUrl();
11200
+ return url === null ? null : { mode: "inject", url, noProxy: mergedNoProxy() };
11201
+ };
10352
11202
  const schedulesRepo = new SchedulesRepo(db);
10353
11203
  const goalsRepo = new GoalsRepo(db);
10354
11204
  const projectConfigService = new ProjectConfigService(config2.root);
10355
11205
  const agentConfigService = new AgentConfigService(config2.root);
10356
11206
  const agentService = new AgentService(config2.root, agentsRepo, agentConfigService);
11207
+ const memoryService = new MemoryService(config2.root, agentConfigService);
10357
11208
  const sessionSources = new SessionSources();
10358
11209
  const traceIndexRepo = new TraceIndexRepo(db);
10359
11210
  const traceIndex = new TraceIndexService(config2.root, traceIndexRepo, sessionSources);
@@ -10383,7 +11234,7 @@ function buildAppDeps(config2, overrides = {}) {
10383
11234
  const manager = new SessionManager({
10384
11235
  sessions: sessionsRepo,
10385
11236
  channels,
10386
- loader: overrides.loader ?? createCoreSessionLoader(config2.root, sessionSources),
11237
+ loader: overrides.loader ?? createCoreSessionLoader(config2.root, sessionSources, { proxyEnv }),
10387
11238
  sources: sessionSources,
10388
11239
  recorder,
10389
11240
  errors,
@@ -10407,11 +11258,15 @@ function buildAppDeps(config2, overrides = {}) {
10407
11258
  manager,
10408
11259
  traceIndex
10409
11260
  });
11261
+ const onPasswordChanged = (userId) => {
11262
+ if (userId === ADMIN_USER_ID) clearInitialAdminPassword(config2.root);
11263
+ };
10410
11264
  const authService = new AuthService({
10411
11265
  users: usersRepo,
10412
11266
  authSessions: authSessionsRepo,
10413
11267
  provisionInitialProject: (user, isAdmin) => projectService.provisionInitialProject(user, isAdmin),
10414
11268
  seedAdminPassword: config2.seedAdminPassword,
11269
+ onPasswordChanged,
10415
11270
  sessionTtlMs: config2.authSessionTtlMs,
10416
11271
  sessionRenewMs: config2.authSessionRenewMs,
10417
11272
  ...overrides.now ? { now: overrides.now } : {}
@@ -10421,6 +11276,7 @@ function buildAppDeps(config2, overrides = {}) {
10421
11276
  authSessions: authSessionsRepo,
10422
11277
  projects: projectsRepo,
10423
11278
  projectService,
11279
+ onPasswordChanged,
10424
11280
  ...overrides.now ? { now: overrides.now } : {}
10425
11281
  });
10426
11282
  const sessionService = new SessionService({
@@ -10429,7 +11285,8 @@ function buildAppDeps(config2, overrides = {}) {
10429
11285
  manager,
10430
11286
  projectConfig: projectConfigService,
10431
11287
  sources: sessionSources,
10432
- traceIndex
11288
+ traceIndex,
11289
+ proxyEnv
10433
11290
  });
10434
11291
  const scheduler = new Scheduler({
10435
11292
  root: config2.root,
@@ -10450,12 +11307,14 @@ function buildAppDeps(config2, overrides = {}) {
10450
11307
  db,
10451
11308
  sessionsRepo,
10452
11309
  prefsRepo,
11310
+ serverSettingsRepo,
10453
11311
  authService,
10454
11312
  adminService,
10455
11313
  projectService,
10456
11314
  projectConfigService,
10457
11315
  agentService,
10458
11316
  agentConfigService,
11317
+ memoryService,
10459
11318
  sessionService,
10460
11319
  traceService,
10461
11320
  traceIndex,
@@ -10478,7 +11337,7 @@ function buildAppDeps(config2, overrides = {}) {
10478
11337
  };
10479
11338
  }
10480
11339
  function createApp(deps2) {
10481
- const app2 = new Hono23();
11340
+ const app2 = new Hono25();
10482
11341
  app2.onError((err, c) => {
10483
11342
  const projectId = attributedProjectId(c, deps2);
10484
11343
  deps2.errors.record({
@@ -10531,6 +11390,7 @@ function createApp(deps2) {
10531
11390
  app2.route("/api/me", meRoutes(deps2));
10532
11391
  app2.route("/api/version", versionRoutes(deps2));
10533
11392
  app2.route("/api/admin/users", adminUsersRoutes(deps2));
11393
+ app2.route("/api/admin/settings", adminSettingsRoutes(deps2));
10534
11394
  app2.route("/api/events", eventsRoutes(deps2));
10535
11395
  app2.route("/api/skills", skillLibraryRoutes());
10536
11396
  app2.route("/api/projects", projectsRoutes(deps2));
@@ -10541,6 +11401,7 @@ function createApp(deps2) {
10541
11401
  app2.route("/api/projects/:projectId/dirs", dirsRoutes(deps2));
10542
11402
  app2.route("/api/projects/:projectId/agents/:agentId/config", agentConfigRoutes(deps2));
10543
11403
  app2.route("/api/projects/:projectId/agents/:agentId/vault", vaultRoutes(deps2));
11404
+ app2.route("/api/projects/:projectId/agents/:agentId/memory", memoryRoutes(deps2));
10544
11405
  app2.route("/api/projects/:projectId/agents/:agentId/schedules", scheduleRoutes(deps2));
10545
11406
  app2.route("/api/projects/:projectId/agents/:agentId/benchmarks", benchmarksRoutes(deps2));
10546
11407
  app2.route("/api/projects/:projectId/agents/:agentId/skills", agentSkillsRoutes(deps2));
@@ -10550,7 +11411,7 @@ function createApp(deps2) {
10550
11411
  app2.route("/api/projects/:projectId/usage", usageRoutes(deps2));
10551
11412
  app2.route("/api/sessions", sessionsRoutes(deps2));
10552
11413
  app2.route("/preview", previewRoutes(deps2));
10553
- if (fs20.existsSync(deps2.config.webDist)) {
11414
+ if (fs21.existsSync(deps2.config.webDist)) {
10554
11415
  registerStaticRoutes(app2, deps2.config.webDist);
10555
11416
  }
10556
11417
  return app2;
@@ -10586,16 +11447,16 @@ function registerStaticRoutes(app2, webDist) {
10586
11447
  return c.json(errorBody("not_found", "Endpoint does not exist."), 404);
10587
11448
  }
10588
11449
  const rel = reqPath.replace(/^\/+/, "");
10589
- const resolved = path16.resolve(webDist, rel === "" ? "index.html" : rel);
10590
- const base = path16.resolve(webDist);
10591
- const target = resolved === base || resolved.startsWith(base + path16.sep) ? resolved : path16.join(base, "index.html");
11450
+ const resolved = path17.resolve(webDist, rel === "" ? "index.html" : rel);
11451
+ const base = path17.resolve(webDist);
11452
+ const target = resolved === base || resolved.startsWith(base + path17.sep) ? resolved : path17.join(base, "index.html");
10592
11453
  let file = target;
10593
11454
  try {
10594
11455
  const stat = await fsp.stat(file);
10595
- if (stat.isDirectory()) file = path16.join(file, "index.html");
11456
+ if (stat.isDirectory()) file = path17.join(file, "index.html");
10596
11457
  await fsp.access(file);
10597
11458
  } catch {
10598
- file = path16.join(base, "index.html");
11459
+ file = path17.join(base, "index.html");
10599
11460
  }
10600
11461
  let content;
10601
11462
  try {
@@ -10603,7 +11464,7 @@ function registerStaticRoutes(app2, webDist) {
10603
11464
  } catch {
10604
11465
  return c.json(errorBody("not_found", "Resource does not exist."), 404);
10605
11466
  }
10606
- const type = CONTENT_TYPES2[path16.extname(file).toLowerCase()] ?? "application/octet-stream";
11467
+ const type = CONTENT_TYPES2[path17.extname(file).toLowerCase()] ?? "application/octet-stream";
10607
11468
  return new Response(new Uint8Array(content), {
10608
11469
  status: 200,
10609
11470
  headers: { "Content-Type": type }
@@ -10613,16 +11474,16 @@ function registerStaticRoutes(app2, webDist) {
10613
11474
 
10614
11475
  // src/config.ts
10615
11476
  import { randomBytes as randomBytes7 } from "crypto";
10616
- import fs21 from "fs";
10617
- import path17 from "path";
11477
+ import fs22 from "fs";
11478
+ import path18 from "path";
10618
11479
  import { fileURLToPath } from "url";
10619
11480
  import { DEFAULT_SERVER_PORT, resolveRoot } from "@prismshadow/penguin-core";
10620
11481
  var DAY_MS = 24 * 60 * 60 * 1e3;
10621
11482
  function defaultWebDist() {
10622
- const here = path17.dirname(fileURLToPath(import.meta.url));
10623
- const bundled = path17.resolve(here, "..", "web-dist");
10624
- if (fs21.existsSync(bundled)) return bundled;
10625
- return path17.resolve(here, "..", "..", "web", "dist");
11483
+ const here = path18.dirname(fileURLToPath(import.meta.url));
11484
+ const bundled = path18.resolve(here, "..", "web-dist");
11485
+ if (fs22.existsSync(bundled)) return bundled;
11486
+ return path18.resolve(here, "..", "..", "web", "dist");
10626
11487
  }
10627
11488
  function normalizePreviewOrigin(raw) {
10628
11489
  if (!raw || raw.trim() === "") return null;
@@ -10652,7 +11513,7 @@ function resolveServerConfig(env = process.env) {
10652
11513
  root,
10653
11514
  host,
10654
11515
  port,
10655
- dbPath: env.PENGUIN_WEB_DB ?? path17.join(root, "web.db"),
11516
+ dbPath: env.PENGUIN_WEB_DB ?? path18.join(root, "web.db"),
10656
11517
  webDist: env.PENGUIN_WEB_DIST ?? defaultWebDist(),
10657
11518
  previewOrigin: normalizePreviewOrigin(env.PENGUIN_PREVIEW_ORIGIN),
10658
11519
  // An empty/whitespace value is treated as unset (→ random seed password). Desktop
@@ -10669,6 +11530,7 @@ function resolveServerConfig(env = process.env) {
10669
11530
 
10670
11531
  // src/index.ts
10671
11532
  loadDotenv({ quiet: true });
11533
+ installGlobalProxyDispatcher();
10672
11534
  var EXIT_ALREADY_RUNNING = 3;
10673
11535
  var config = resolveServerConfig();
10674
11536
  var existingLock = await liveServerLock(config.root);
@@ -10680,23 +11542,35 @@ if (existingLock) {
10680
11542
  process.exit(EXIT_ALREADY_RUNNING);
10681
11543
  }
10682
11544
  var deps = buildAppDeps(config);
11545
+ applyProxySettings({
11546
+ proxyForApp: deps.serverSettingsRepo.getProxyForApp(),
11547
+ proxyUrl: deps.serverSettingsRepo.getProxyUrl()
11548
+ });
10683
11549
  var app = createApp(deps);
10684
11550
  var seededAdminPassword = await deps.authService.seedAdmin();
10685
- if (seededAdminPassword !== null && config.desktopToken === null) {
10686
- console.log(
10687
- `Seeded built-in admin "admin" \u2014 initial password: ${seededAdminPassword} (change it after first sign-in)`
10688
- );
11551
+ if (config.desktopToken === null) {
11552
+ if (seededAdminPassword !== null) {
11553
+ storeInitialAdminPassword(config.root, seededAdminPassword);
11554
+ }
11555
+ if (deps.authService.adminPasswordIsInitial()) {
11556
+ const initialPassword = seededAdminPassword ?? readInitialAdminPassword(config.root);
11557
+ if (initialPassword !== null) {
11558
+ console.log(renderInitialPasswordNotice(ADMIN_USER_ID, initialPassword));
11559
+ }
11560
+ } else {
11561
+ clearInitialAdminPassword(config.root);
11562
+ }
10689
11563
  }
10690
11564
  await deps.scheduler.start();
10691
11565
  deps.goalsRepo.abortOrphanedActive();
10692
11566
  var appHost = loopbackHostRoles(config.host)?.app ?? config.host;
10693
11567
  var ipv6Loopback = null;
10694
11568
  function writePortFile(file, port) {
10695
- fs22.mkdirSync(path18.dirname(file), { recursive: true });
11569
+ fs23.mkdirSync(path19.dirname(file), { recursive: true });
10696
11570
  const tmp = `${file}.${process.pid}.tmp`;
10697
- fs22.writeFileSync(tmp, `${port}
11571
+ fs23.writeFileSync(tmp, `${port}
10698
11572
  `);
10699
- fs22.renameSync(tmp, file);
11573
+ fs23.renameSync(tmp, file);
10700
11574
  }
10701
11575
  var server = serve({ fetch: app.fetch, hostname: config.host, port: config.port }, (info) => {
10702
11576
  console.log(`penguin-server started: http://${appHost}:${info.port}`);
@@ -10723,7 +11597,7 @@ function cleanupInstanceFiles() {
10723
11597
  releaseServerLock(config.root);
10724
11598
  if (config.portFile !== null) {
10725
11599
  try {
10726
- fs22.rmSync(config.portFile, { force: true });
11600
+ fs23.rmSync(config.portFile, { force: true });
10727
11601
  } catch {
10728
11602
  }
10729
11603
  }