@prismshadow/penguin-server 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { serve } from "@hono/node-server";
5
5
  // src/app.ts
6
6
  import fs16 from "fs";
7
7
  import fsp from "fs/promises";
8
- import path14 from "path";
8
+ import path15 from "path";
9
9
  import { Hono as Hono19 } from "hono";
10
10
 
11
11
  // src/db/database.ts
@@ -15,30 +15,30 @@ import path from "path";
15
15
  // src/db/schema.ts
16
16
  var SCHEMA_SQL = `
17
17
  CREATE TABLE IF NOT EXISTS users (
18
- user_id TEXT PRIMARY KEY, -- \u8BED\u4E49 id \u5373\u767B\u5F55\u540D\uFF1A^[a-z][a-z0-9_-]{1,31}$
18
+ user_id TEXT PRIMARY KEY, -- semantic id doubles as login name: ^[a-z][a-z0-9_-]{1,31}$
19
19
  password_hash TEXT NOT NULL, -- scrypt$N$r$p$salt$hash(base64)
20
- is_admin INTEGER NOT NULL DEFAULT 0, -- \u5185\u7F6E admin\uFF08\u542F\u52A8\u65F6\u79CD\u5B50\uFF09\u4E3A 1
21
- password_is_initial INTEGER NOT NULL DEFAULT 0, -- 1=\u521D\u59CB\u5BC6\u7801\uFF08\u79CD\u5B50/\u7BA1\u7406\u5458\u8BBE\u7F6E\uFF09\uFF1B\u672C\u4EBA\u6539\u5BC6\u540E\u6E05 0
20
+ is_admin INTEGER NOT NULL DEFAULT 0, -- 1 for the built-in admin (seeded at startup)
21
+ password_is_initial INTEGER NOT NULL DEFAULT 0, -- 1=initial password (seeded/admin-set); cleared once the user changes it
22
22
  created_at TEXT NOT NULL
23
23
  );
24
24
  CREATE TABLE IF NOT EXISTS auth_sessions (
25
- token_hash TEXT PRIMARY KEY, -- sha256(token) hex\uFF1Bcookie \u53EA\u5B58\u539F\u59CB token
25
+ token_hash TEXT PRIMARY KEY, -- sha256(token) hex; the cookie stores only the raw token
26
26
  user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
27
27
  created_at TEXT NOT NULL,
28
- expires_at TEXT NOT NULL -- 7 \u5929\u6ED1\u52A8\u7EED\u671F\uFF08\u5269\u4F59 <6 \u5929\u5219\u7EED\u6EE1\uFF09
28
+ expires_at TEXT NOT NULL -- 7-day sliding renewal (topped up when <6 days remain)
29
29
  );
30
30
  CREATE TABLE IF NOT EXISTS projects (
31
- project_id TEXT PRIMARY KEY, -- \u76EE\u5F55\u540D\u5373 id\uFF1B\u663E\u793A\u540D\u5728 project_config.toml
31
+ project_id TEXT PRIMARY KEY, -- directory name doubles as id; display name lives in project_config.toml
32
32
  owner_user_id TEXT NOT NULL REFERENCES users(user_id),
33
33
  created_at TEXT NOT NULL
34
34
  );
35
- CREATE TABLE IF NOT EXISTS project_members ( -- \u4EC5 member \u6388\u6743\u5173\u7CFB\uFF1Bowner \u4E0D\u5165\u6B64\u8868
35
+ CREATE TABLE IF NOT EXISTS project_members ( -- member grants only; owners are not stored here
36
36
  project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
37
37
  user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
38
38
  created_at TEXT NOT NULL,
39
39
  PRIMARY KEY (project_id, user_id)
40
40
  );
41
- CREATE TABLE IF NOT EXISTS agents ( -- \u7D22\u5F15\uFF1Bname/description \u5728 system_config.yaml
41
+ CREATE TABLE IF NOT EXISTS agents ( -- index only; name/description live in system_config.yaml
42
42
  project_id TEXT NOT NULL,
43
43
  agent_id TEXT NOT NULL,
44
44
  created_at TEXT NOT NULL,
@@ -48,64 +48,63 @@ CREATE TABLE IF NOT EXISTS sessions (
48
48
  session_id TEXT PRIMARY KEY,
49
49
  project_id TEXT NOT NULL,
50
50
  agent_id TEXT NOT NULL,
51
- provider TEXT NOT NULL, -- \u4F1A\u8BDD\u6A21\u578B\u7684\u5382\u5546\u5206\u7EC4\uFF08\u4E0E model_id \u6210\u5BF9\u6784\u6210\u6A21\u578B\u5F15\u7528\uFF09
52
- model_id TEXT NOT NULL, -- \u4E0A\u6E38\u6A21\u578B id\uFF08\u539F\u6837\u53D1\u7ED9 AgentHub\uFF1B\u7981\u6B62 <provider>/<id> \u62FC\u63A5\uFF09
51
+ provider TEXT NOT NULL, -- provider group of the session model (paired with model_id as a model reference)
52
+ model_id TEXT NOT NULL, -- upstream model id (sent to AgentHub as-is; never concatenate <provider>/<id>)
53
53
  workspace TEXT NOT NULL,
54
54
  approval_mode TEXT NOT NULL DEFAULT 'allow-all', -- allow-all|deny-all|read-only|always-ask
55
- title TEXT, -- \u9996\u6B21\u5BF9\u8BDD\u540E\u7531\u6A21\u578B\u81EA\u52A8\u751F\u6210\uFF1BNULL=\u672A\u751F\u6210\uFF08\u524D\u7AEF\u663E\u793A\u300C\u65B0\u5BF9\u8BDD\u300D\uFF09
56
- archived_at TEXT, -- \u5F52\u6863\u65F6\u523B\uFF1BNULL=\u672A\u5F52\u6863\uFF08\u9ED8\u8BA4\u5C55\u793A\uFF1B\u5F52\u6863\u540E\u6536\u8FDB\u300C\u5DF2\u5F52\u6863\u300D\uFF09
57
- source TEXT, -- \u4F1A\u8BDD\u6765\u6E90\uFF1ANULL=\u7528\u6237\u521B\u5EFA | schedule\uFF08\u5B9A\u65F6\u4EFB\u52A1\uFF09| subagent\uFF08\u5B50\u4F1A\u8BDD\uFF09
55
+ title TEXT, -- auto-generated by the model after the first exchange; NULL=not yet (frontend shows "New chat")
56
+ archived_at TEXT, -- archive time; NULL=not archived (shown by default; archived ones move under "Archived")
58
57
  created_at TEXT NOT NULL
59
- );
58
+ ); -- session origin is NOT stored: session_meta in the Trace is the single source of truth (see runtime/session-sources.ts)
60
59
  CREATE TABLE IF NOT EXISTS usage_records (
61
60
  id INTEGER PRIMARY KEY AUTOINCREMENT,
62
61
  ts TEXT NOT NULL,
63
- date TEXT NOT NULL, -- \u672C\u5730\u65E5\u671F yyyy-mm-dd\uFF08\u805A\u5408\u952E\uFF0C\u4E0E Trace \u65E5\u671F\u76EE\u5F55\u540C\u53E3\u5F84\uFF09
62
+ date TEXT NOT NULL, -- local date yyyy-mm-dd (aggregation key; same convention as Trace date directories)
64
63
  project_id TEXT NOT NULL,
65
64
  agent_id TEXT NOT NULL,
66
- session_id TEXT NOT NULL, -- \u9876\u5C42 Session\uFF08\u5B50\u4F1A\u8BDD\u6D88\u8017\u8BA1\u5165\u6240\u5C5E\u4E3B Session\uFF09
67
- origin_session_id TEXT, -- \u76F4\u63A5\u6765\u6E90\u5B50 Session\uFF08origin \u94FE\u672B\u9879\uFF09\uFF1BNULL=\u4E3B\u4F1A\u8BDD
68
- provider TEXT NOT NULL, -- \u5382\u5546\u5206\u7EC4\uFF1A\u4E0E model_id \u6210\u5BF9\u6784\u6210\u5F52\u56E0\u952E\uFF0C\u805A\u5408\u4E00\u5F8B GROUP BY provider, model_id
69
- model_id TEXT NOT NULL, -- \u4E0A\u6E38\u6A21\u578B id\uFF08\u4E0E provider \u6210\u5BF9\uFF1B\u540C\u540D model_id \u8DE8\u5382\u5546\u5206\u5F00\u805A\u5408\uFF09
65
+ session_id TEXT NOT NULL, -- top-level Session (child-session usage counts toward its main Session)
66
+ origin_session_id TEXT, -- immediate source child Session (last of the origin chain); NULL=main session
67
+ provider TEXT NOT NULL, -- provider group: paired with model_id as the attribution key; always GROUP BY provider, model_id
68
+ model_id TEXT NOT NULL, -- upstream model id (paired with provider; same model_id across providers aggregates separately)
70
69
  cache_read INTEGER NOT NULL,
71
70
  cache_write INTEGER NOT NULL,
72
71
  output INTEGER NOT NULL,
73
- total INTEGER NOT NULL, -- \u53D6 token_usage.request\uFF08\u6BCF Request \u4E00\u6761\uFF09
74
- status TEXT NOT NULL DEFAULT 'completed' -- \u8BF7\u6C42\u7ED3\u5C40\uFF1Acompleted=\u6210\u529F\uFF08\u542B token\uFF09\uFF1B\u5176\u4F59=\u5931\u8D25\uFF080 token\uFF0C\u4F9B\u6210\u529F\u7387\uFF09
75
- ); -- \u6210\u672C\u4E0D\u843D\u5E93\uFF1A\u67E5\u8BE2\u65F6\u6309\u5F53\u524D pricing \u5B9E\u65F6\u6298\u7B97
72
+ total INTEGER NOT NULL, -- from token_usage.request (one row per Request)
73
+ status TEXT NOT NULL DEFAULT 'completed' -- request outcome: completed=success (with tokens); others=failure (0 tokens, for success rate)
74
+ ); -- cost is not stored: computed at query time from current pricing
76
75
  CREATE INDEX IF NOT EXISTS idx_usage_project_date ON usage_records(project_id, date);
77
76
  CREATE INDEX IF NOT EXISTS idx_usage_session ON usage_records(session_id);
78
- CREATE TABLE IF NOT EXISTS error_records ( -- \u670D\u52A1\u7AEF\u5F02\u5E38\u6355\u83B7\uFF08\u7EDF\u8BA1\u4E2D\u5FC3\u300C\u5F02\u5E38\u300D\uFF09
77
+ CREATE TABLE IF NOT EXISTS error_records ( -- server-side error capture (the Costs page's "Errors" tab)
79
78
  id INTEGER PRIMARY KEY AUTOINCREMENT,
80
79
  ts TEXT NOT NULL,
81
- date TEXT NOT NULL, -- \u672C\u5730\u65E5\u671F yyyy-mm-dd\uFF08\u4E0E usage_records \u540C\u53E3\u5F84\uFF09
82
- project_id TEXT, -- \u53EF\u7A7A\uFF1A\u767B\u5F55/\u6CE8\u518C\u3001\u8FDB\u7A0B\u7EA7\u5F02\u5E38\u6CA1\u6709 Project \u4E0A\u4E0B\u6587
80
+ date TEXT NOT NULL, -- local date yyyy-mm-dd (same convention as usage_records)
81
+ project_id TEXT, -- nullable: sign-in/registration and process-level errors have no Project context
83
82
  agent_id TEXT,
84
83
  session_id TEXT,
85
84
  source TEXT NOT NULL, -- http | session | usage | title | subagent | process | llm | environment | schedule
86
- kind TEXT NOT NULL, -- expected\uFF08HttpError\uFF0C\u4E1A\u52A1 4xx\uFF09| unexpected\uFF08500/\u8FD0\u884C\u65F6\uFF09
85
+ kind TEXT NOT NULL, -- expected (HttpError, business 4xx) | unexpected (500/runtime)
87
86
  code TEXT NOT NULL, -- HttpError.code / internal / session_run_failed / ...
88
- status INTEGER, -- HTTP \u72B6\u6001\u7801\uFF1B\u975E HTTP \u6765\u6E90\u4E3A NULL
89
- message TEXT NOT NULL -- \u622A\u65AD 500 \u5B57\u7B26\uFF08\u4E0D\u5B58\u5806\u6808\uFF1A\u5806\u6808\u53EA\u8FDB\u65E5\u5FD7\uFF09
87
+ status INTEGER, -- HTTP status code; NULL for non-HTTP sources
88
+ message TEXT NOT NULL -- truncated to 500 chars (no stack stored: stacks go to logs only)
90
89
  );
91
90
  CREATE INDEX IF NOT EXISTS idx_error_project_date ON error_records(project_id, date);
92
- CREATE TABLE IF NOT EXISTS schedule_state ( -- \u5B9A\u65F6\u4EFB\u52A1\u8FD0\u884C\u72B6\u6001\uFF08\u6587\u4EF6\u662F\u58F0\u660E\u5F0F\u610F\u56FE\uFF0C\u7CFB\u7EDF\u4E0D\u5199\u56DE\uFF09
91
+ CREATE TABLE IF NOT EXISTS schedule_state ( -- schedule runtime state (files are declarative intent; the system never writes back)
93
92
  project_id TEXT NOT NULL,
94
93
  agent_id TEXT NOT NULL,
95
- name TEXT NOT NULL, -- \u6587\u4EF6\u540D\uFF08\u53BB .toml\uFF09\u5373\u6807\u8BC6
96
- creator_user_id TEXT, -- \u521B\u5EFA\u8005\uFF08API \u521B\u5EFA\u65F6\u8BB0\uFF1B\u624B\u7F16\u6587\u4EF6\u5BF9\u8D26\u767B\u8BB0\u56DE\u9000 Project owner\uFF09
97
- start_at_ms INTEGER NOT NULL, -- \u5B9A\u4E49\u8EAB\u4EFD\uFF1Astart_at \u53D8\u66F4\u89C6\u4E3A\u65B0\u4EFB\u52A1\u5B9E\u4F8B\uFF0C\u91CD\u7F6E\u89E6\u53D1\u72B6\u6001
98
- def_hash TEXT NOT NULL, -- \u6587\u4EF6\u5185\u5BB9\u6307\u7EB9\uFF1A\u53D8\u66F4\u5373\u6E05\u9664\u5931\u6548\u6807\u8BB0\uFF08\u6587\u4EF6\u4FEE\u6539\u540E\u91CD\u65B0\u751F\u6548\uFF09
99
- last_slot_ms INTEGER, -- \u5DF2\u6D88\u5316\u7684\u6700\u8FD1\u5E94\u89E6\u53D1\u65F6\u523B\uFF08\u89E6\u53D1\u6216\u8DF3\u8FC7\u90FD\u63A8\u8FDB\uFF1B\u91CD\u542F\u4E0D\u91CD\u590D\u89E6\u53D1\uFF09
100
- last_fired_at TEXT, -- \u6700\u8FD1\u5B9E\u9645\u53D1\u9001\u65F6\u523B\uFF08\u5C55\u793A\u7528\uFF09
101
- fired_once INTEGER NOT NULL DEFAULT 0, -- \u4E00\u6B21\u6027\u4EFB\u52A1\u5DF2\u89E6\u53D1
102
- missed INTEGER NOT NULL DEFAULT 0, -- \u4E00\u6B21\u6027\u4EFB\u52A1\u5DF2\u9519\u8FC7\uFF08\u542F\u52A8/\u767B\u8BB0\u5BF9\u8D26\u6807\u8BB0\uFF0C\u9519\u8FC7\u4E0D\u8865\uFF09
103
- invalid_reason TEXT, -- \u5931\u6548\u539F\u56E0\uFF08\u5982\u7ED1\u5B9A Session \u5DF2\u5220\u9664\uFF09\uFF1BNULL \u5373\u6B63\u5E38
94
+ name TEXT NOT NULL, -- file name (without .toml) doubles as the identifier
95
+ creator_user_id TEXT, -- creator (recorded on API create; hand-edited file reconciliation falls back to the Project owner)
96
+ start_at_ms INTEGER NOT NULL, -- defines identity: a start_at change counts as a new task instance, resetting fire state
97
+ def_hash TEXT NOT NULL, -- file content fingerprint: any change clears the invalid flag (the file takes effect again after edits)
98
+ last_slot_ms INTEGER, -- latest due slot already consumed (advances on fire or skip; no re-fire across restarts)
99
+ last_fired_at TEXT, -- last actual fire time (for display)
100
+ fired_once INTEGER NOT NULL DEFAULT 0, -- one-shot task has fired
101
+ missed INTEGER NOT NULL DEFAULT 0, -- one-shot task was missed (flagged by startup/registration reconciliation; not backfilled)
102
+ invalid_reason TEXT, -- invalidation reason (e.g. the bound Session was deleted); NULL=healthy
104
103
  PRIMARY KEY (project_id, agent_id, name)
105
104
  );
106
105
  CREATE TABLE IF NOT EXISTS ui_prefs (
107
106
  user_id TEXT PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE,
108
- prefs_json TEXT NOT NULL -- {theme?, lastProjectId?, ...} \u81EA\u7531 JSON
107
+ prefs_json TEXT NOT NULL -- {theme?, lastProjectId?, ...} free-form JSON
109
108
  );
110
109
  `;
111
110
 
@@ -460,7 +459,7 @@ var SchedulesRepo = class {
460
459
  ).run(args.defHash, args.projectId, args.agentId, args.name);
461
460
  }
462
461
  const row = this.find(args.projectId, args.agentId, args.name);
463
- if (!row) throw new Error("schedule_state \u767B\u8BB0\u540E\u8BFB\u53D6\u5931\u8D25");
462
+ if (!row) throw new Error("Failed to read back schedule_state after registration");
464
463
  return { row, fresh };
465
464
  }
466
465
  /** Advance the consumed scheduled time (advances whether triggered or skipped; restarts don't re-trigger). */
@@ -525,7 +524,6 @@ function mapRow3(r) {
525
524
  approvalMode: r.approval_mode,
526
525
  title: r.title ?? null,
527
526
  archivedAt: r.archived_at ?? null,
528
- source: r.source ?? null,
529
527
  createdAt: r.created_at
530
528
  };
531
529
  }
@@ -536,8 +534,8 @@ var SessionsRepo = class {
536
534
  db;
537
535
  insert(row) {
538
536
  this.db.prepare(
539
- `INSERT INTO sessions (session_id, project_id, agent_id, provider, model_id, workspace, approval_mode, title, source, created_at)
540
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
537
+ `INSERT INTO sessions (session_id, project_id, agent_id, provider, model_id, workspace, approval_mode, title, created_at)
538
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
541
539
  ).run(
542
540
  row.sessionId,
543
541
  row.projectId,
@@ -547,15 +545,14 @@ var SessionsRepo = class {
547
545
  row.workspace,
548
546
  row.approvalMode,
549
547
  row.title,
550
- row.source ?? null,
551
548
  row.createdAt
552
549
  );
553
550
  }
554
551
  /** Idempotent insert: used when Trace directory discovery backfills a row (concurrent listing discovering the same Session no longer triggers a UNIQUE violation). */
555
552
  insertOrIgnore(row) {
556
553
  this.db.prepare(
557
- `INSERT OR IGNORE INTO sessions (session_id, project_id, agent_id, provider, model_id, workspace, approval_mode, title, source, created_at)
558
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
554
+ `INSERT OR IGNORE INTO sessions (session_id, project_id, agent_id, provider, model_id, workspace, approval_mode, title, created_at)
555
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
559
556
  ).run(
560
557
  row.sessionId,
561
558
  row.projectId,
@@ -565,7 +562,6 @@ var SessionsRepo = class {
565
562
  row.workspace,
566
563
  row.approvalMode,
567
564
  row.title,
568
- row.source ?? null,
569
565
  row.createdAt
570
566
  );
571
567
  }
@@ -857,15 +853,15 @@ function modelCredentialMissing(modelId) {
857
853
  return new HttpError(
858
854
  400,
859
855
  "model_credential_missing",
860
- `\u6A21\u578B ${modelId} \u8FD8\u6CA1\u6709\u53EF\u7528\u7684 API key\uFF0C\u8BF7\u5148\u5728\u300C\u6A21\u578B\u300D\u9875\u4E3A\u5B83\u914D\u7F6E\u3002`
856
+ `Model ${modelId} has no API key yet. Configure it on the Models page first.`
861
857
  );
862
858
  }
863
859
  function handleError(err, c) {
864
860
  if (err instanceof HttpError) {
865
861
  return c.json(errorBody(err.code, err.message), err.status);
866
862
  }
867
- console.error(`[server] \u672A\u5904\u7406\u5F02\u5E38: ${err.stack ?? err.message}`);
868
- return c.json(errorBody("internal", "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF\u3002"), 500);
863
+ console.error(`[server] Unhandled exception: ${err.stack ?? err.message}`);
864
+ return c.json(errorBody("internal", "Internal server error."), 500);
869
865
  }
870
866
 
871
867
  // src/auth/middleware.ts
@@ -875,7 +871,7 @@ function authMiddleware(auth) {
875
871
  const token = getCookie(c, SESSION_COOKIE);
876
872
  const user = token ? auth.authenticate(token) : null;
877
873
  if (!user) {
878
- throw new HttpError(401, "unauthorized", "\u672A\u767B\u5F55\u6216\u767B\u5F55\u5DF2\u8FC7\u671F\u3002");
874
+ throw new HttpError(401, "unauthorized", "Not signed in or the sign-in has expired.");
879
875
  }
880
876
  c.set("user", user);
881
877
  await next();
@@ -886,7 +882,11 @@ var jsonOnlyWrites = async (c, next) => {
886
882
  if (WRITE_METHODS.has(c.req.method)) {
887
883
  const contentType = c.req.header("content-type");
888
884
  if (contentType && !contentType.toLowerCase().startsWith("application/json")) {
889
- throw new HttpError(415, "unsupported_media_type", "\u5199\u8BF7\u6C42\u4EC5\u63A5\u53D7 application/json\u3002");
885
+ throw new HttpError(
886
+ 415,
887
+ "unsupported_media_type",
888
+ "Write requests only accept application/json."
889
+ );
890
890
  }
891
891
  }
892
892
  await next();
@@ -950,7 +950,7 @@ async function verifyPassword(password, stored) {
950
950
  // src/auth/service.ts
951
951
  var MIN_PASSWORD_LENGTH = 8;
952
952
  var ADMIN_USER_ID = "admin";
953
- var ADMIN_INITIAL_PASSWORD = "admin123";
953
+ var ADMIN_INITIAL_PASSWORD = "penguin-2026";
954
954
  function sha256Hex(value) {
955
955
  return createHash("sha256").update(value).digest("hex");
956
956
  }
@@ -995,7 +995,7 @@ var AuthService = class {
995
995
  const row = this.deps.users.findById(userId);
996
996
  const ok = row !== null && await verifyPassword(password, row.passwordHash);
997
997
  if (!row || !ok) {
998
- throw new HttpError(401, "invalid_credentials", "\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF\u3002");
998
+ throw new HttpError(401, "invalid_credentials", "Incorrect username or password.");
999
999
  }
1000
1000
  this.deps.authSessions.deleteExpired(this.now().toISOString());
1001
1001
  return { user: toUserInfo(row), token: this.issueSession(row.userId) };
@@ -1004,10 +1004,10 @@ var AuthService = class {
1004
1004
  async changePassword(userId, oldPassword, newPassword) {
1005
1005
  const row = this.deps.users.findById(userId);
1006
1006
  if (!row || !await verifyPassword(oldPassword, row.passwordHash)) {
1007
- throw new HttpError(400, "password_mismatch", "\u5F53\u524D\u5BC6\u7801\u4E0D\u6B63\u786E\u3002");
1007
+ throw new HttpError(400, "password_mismatch", "Current password is incorrect.");
1008
1008
  }
1009
1009
  if (newPassword.length < MIN_PASSWORD_LENGTH) {
1010
- throw new HttpError(400, "invalid_password", "\u5BC6\u7801\u81F3\u5C11 8 \u4E2A\u5B57\u7B26\u3002");
1010
+ throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
1011
1011
  }
1012
1012
  this.deps.users.updatePassword(userId, await hashPassword(newPassword), false);
1013
1013
  }
@@ -1057,28 +1057,46 @@ function badRequest(message) {
1057
1057
  function pathParam(c, name) {
1058
1058
  const v = c.req.param(name);
1059
1059
  if (v === void 0 || v === "") {
1060
- throw new HttpError(404, "not_found", "\u8DEF\u5F84\u53C2\u6570\u7F3A\u5931\u3002");
1060
+ throw new HttpError(404, "not_found", "Missing path parameter.");
1061
1061
  }
1062
1062
  return v;
1063
1063
  }
1064
1064
  function requireValidId(c, name) {
1065
1065
  const v = pathParam(c, name);
1066
1066
  if (!isValidId(v)) {
1067
- throw new HttpError(404, "not_found", "\u8D44\u6E90\u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE\u3002");
1067
+ throw new HttpError(404, "not_found", "Resource does not exist or you do not have access.");
1068
1068
  }
1069
1069
  return v;
1070
1070
  }
1071
1071
  function positiveIntParam(c, name) {
1072
- const v = Number.parseInt(pathParam(c, name), 10);
1073
- if (!Number.isInteger(v) || v < 1) throw badRequest(`${name} \u5FC5\u987B\u662F\u6B63\u6574\u6570\u3002`);
1072
+ const raw = pathParam(c, name);
1073
+ if (!/^\d+$/.test(raw)) throw badRequest(`${name} must be a positive integer.`);
1074
+ const v = Number.parseInt(raw, 10);
1075
+ if (!Number.isSafeInteger(v) || v < 1) throw badRequest(`${name} must be a positive integer.`);
1074
1076
  return v;
1075
1077
  }
1076
1078
  function paginationQuery(c) {
1077
1079
  const offset = Number.parseInt(c.req.query("offset") ?? "0", 10);
1078
1080
  const limit = Number.parseInt(c.req.query("limit") ?? "200", 10);
1079
- if (!Number.isInteger(offset) || offset < 0) throw badRequest("offset \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570\u3002");
1081
+ if (!Number.isInteger(offset) || offset < 0)
1082
+ throw badRequest("offset must be a non-negative integer.");
1083
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1e3) {
1084
+ throw badRequest("limit must be an integer between 1 and 1000.");
1085
+ }
1086
+ return { offset, limit };
1087
+ }
1088
+ function optionalPagingQuery(c) {
1089
+ const rawLimit = c.req.query("limit");
1090
+ const rawOffset = c.req.query("offset");
1091
+ if (rawLimit === void 0 && rawOffset === void 0) return null;
1092
+ if (rawLimit === void 0) throw badRequest("offset requires limit.");
1093
+ const limit = Number.parseInt(rawLimit, 10);
1080
1094
  if (!Number.isInteger(limit) || limit < 1 || limit > 1e3) {
1081
- throw badRequest("limit \u5FC5\u987B\u662F 1~1000 \u7684\u6574\u6570\u3002");
1095
+ throw badRequest("limit must be an integer between 1 and 1000.");
1096
+ }
1097
+ const offset = Number.parseInt(rawOffset ?? "0", 10);
1098
+ if (!Number.isInteger(offset) || offset < 0) {
1099
+ throw badRequest("offset must be a non-negative integer.");
1082
1100
  }
1083
1101
  return { offset, limit };
1084
1102
  }
@@ -1087,25 +1105,25 @@ async function readJson(c) {
1087
1105
  try {
1088
1106
  body = await c.req.json();
1089
1107
  } catch {
1090
- throw badRequest("\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F\u5408\u6CD5 JSON\u3002");
1108
+ throw badRequest("Request body must be valid JSON.");
1091
1109
  }
1092
1110
  if (body === null || typeof body !== "object" || Array.isArray(body)) {
1093
- throw badRequest("\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F JSON \u5BF9\u8C61\u3002");
1111
+ throw badRequest("Request body must be a JSON object.");
1094
1112
  }
1095
1113
  return body;
1096
1114
  }
1097
1115
  function requireString(obj, key, rule = {}) {
1098
1116
  const v = obj[key];
1099
1117
  const label = rule.label ?? key;
1100
- if (typeof v !== "string") throw badRequest(`${label} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
1118
+ if (typeof v !== "string") throw badRequest(`${label} must be a string.`);
1101
1119
  if (rule.minLen !== void 0 && v.length < rule.minLen) {
1102
- throw badRequest(`${label} \u957F\u5EA6\u81F3\u5C11 ${rule.minLen} \u4E2A\u5B57\u7B26\u3002`);
1120
+ throw badRequest(`${label} must be at least ${rule.minLen} characters.`);
1103
1121
  }
1104
1122
  if (rule.maxLen !== void 0 && v.length > rule.maxLen) {
1105
- throw badRequest(`${label} \u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 ${rule.maxLen} \u4E2A\u5B57\u7B26\u3002`);
1123
+ throw badRequest(`${label} must be at most ${rule.maxLen} characters.`);
1106
1124
  }
1107
1125
  if (rule.pattern !== void 0 && !rule.pattern.test(v)) {
1108
- throw badRequest(`${label} \u683C\u5F0F\u4E0D\u5408\u6CD5\u3002`);
1126
+ throw badRequest(`${label} has an invalid format.`);
1109
1127
  }
1110
1128
  return v;
1111
1129
  }
@@ -1116,7 +1134,7 @@ function optionalString(obj, key, rule = {}) {
1116
1134
  function requireEnum(obj, key, values, label = key) {
1117
1135
  const v = obj[key];
1118
1136
  if (typeof v !== "string" || !values.includes(v)) {
1119
- throw badRequest(`${label} \u5FC5\u987B\u662F ${values.join(" / ")} \u4E4B\u4E00\u3002`);
1137
+ throw badRequest(`${label} must be one of ${values.join(" / ")}.`);
1120
1138
  }
1121
1139
  return v;
1122
1140
  }
@@ -1128,20 +1146,25 @@ function optionalNumber(obj, key, rule = {}) {
1128
1146
  const v = obj[key];
1129
1147
  if (v === void 0) return void 0;
1130
1148
  const label = rule.label ?? key;
1131
- if (typeof v !== "number" || !Number.isFinite(v)) throw badRequest(`${label} \u5FC5\u987B\u662F\u6570\u5B57\u3002`);
1132
- if (rule.integer && !Number.isInteger(v)) throw badRequest(`${label} \u5FC5\u987B\u662F\u6574\u6570\u3002`);
1149
+ if (typeof v !== "number" || !Number.isFinite(v)) throw badRequest(`${label} must be a number.`);
1150
+ if (rule.integer && !Number.isInteger(v)) throw badRequest(`${label} must be an integer.`);
1133
1151
  if (rule.positiveOrMinusOne && !(v > 0 || v === -1)) {
1134
- throw badRequest(`${label} \u5FC5\u987B\u5927\u4E8E 0 \u6216\u4E3A -1\u3002`);
1152
+ throw badRequest(`${label} must be greater than 0 or equal to -1.`);
1135
1153
  }
1136
- if (rule.nonNegative && v < 0) throw badRequest(`${label} \u4E0D\u80FD\u4E3A\u8D1F\u6570\u3002`);
1154
+ if (rule.nonNegative && v < 0) throw badRequest(`${label} must not be negative.`);
1137
1155
  return v;
1138
1156
  }
1139
1157
  function optionalDateParam(value, label) {
1140
1158
  if (value === void 0 || value === "") return void 0;
1141
- if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
1142
- throw badRequest(`${label} \u5FC5\u987B\u662F YYYY-MM-DD \u683C\u5F0F\u3002`);
1159
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
1160
+ if (m) {
1161
+ const [, y, mo, d] = m;
1162
+ const dt = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
1163
+ if (!Number.isNaN(dt.getTime()) && dt.getUTCFullYear() === Number(y) && dt.getUTCMonth() + 1 === Number(mo) && dt.getUTCDate() === Number(d)) {
1164
+ return value;
1165
+ }
1143
1166
  }
1144
- return value;
1167
+ throw badRequest(`${label} must be a valid date in YYYY-MM-DD format.`);
1145
1168
  }
1146
1169
 
1147
1170
  // src/http/routes/admin.ts
@@ -1149,7 +1172,7 @@ function adminUsersRoutes(deps2) {
1149
1172
  const app2 = new Hono();
1150
1173
  app2.use("*", async (c, next) => {
1151
1174
  if (!c.var.user.isAdmin) {
1152
- throw new HttpError(403, "admin_required", "\u8BE5\u64CD\u4F5C\u4EC5\u7BA1\u7406\u5458\u53EF\u6267\u884C\u3002");
1175
+ throw new HttpError(403, "admin_required", "Only an admin can perform this operation.");
1153
1176
  }
1154
1177
  await next();
1155
1178
  });
@@ -1386,7 +1409,7 @@ function membersRoutes(deps2) {
1386
1409
  import { Hono as Hono7 } from "hono";
1387
1410
  function parseRef(value, label) {
1388
1411
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
1389
- throw badRequest(`${label} \u5FC5\u987B\u662F { provider, modelId } \u5BF9\u8C61\u3002`);
1412
+ throw badRequest(`${label} must be a { provider, modelId } object.`);
1390
1413
  }
1391
1414
  const r = value;
1392
1415
  return {
@@ -1395,10 +1418,10 @@ function parseRef(value, label) {
1395
1418
  };
1396
1419
  }
1397
1420
  function parseModelsUpdate(body) {
1398
- if (!Array.isArray(body.models)) throw badRequest("models \u5FC5\u987B\u662F\u6570\u7EC4\u3002");
1421
+ if (!Array.isArray(body.models)) throw badRequest("models must be an array.");
1399
1422
  const models = body.models.map((item, i) => {
1400
1423
  if (item === null || typeof item !== "object" || Array.isArray(item)) {
1401
- throw badRequest(`models[${i}] \u5FC5\u987B\u662F\u5BF9\u8C61\u3002`);
1424
+ throw badRequest(`models[${i}] must be an object.`);
1402
1425
  }
1403
1426
  const m = item;
1404
1427
  const entry = {
@@ -1415,7 +1438,7 @@ function parseModelsUpdate(body) {
1415
1438
  };
1416
1439
  if (m.displayName !== void 0) {
1417
1440
  if (typeof m.displayName !== "string" || m.displayName.length > 100) {
1418
- throw badRequest(`models[${i}].displayName \u5FC5\u987B\u662F\u957F\u5EA6\u4E0D\u8D85\u8FC7 100 \u7684\u5B57\u7B26\u4E32\u3002`);
1441
+ throw badRequest(`models[${i}].displayName must be a string of at most 100 characters.`);
1419
1442
  }
1420
1443
  if (m.displayName) entry.displayName = m.displayName;
1421
1444
  }
@@ -1424,31 +1447,37 @@ function parseModelsUpdate(body) {
1424
1447
  }
1425
1448
  if (m.contextWindow !== void 0) {
1426
1449
  if (typeof m.contextWindow !== "number" || !(m.contextWindow > 0)) {
1427
- throw badRequest(`models[${i}].contextWindow \u5FC5\u987B\u662F\u6B63\u6570\u3002`);
1450
+ throw badRequest(`models[${i}].contextWindow must be a positive number.`);
1428
1451
  }
1429
1452
  entry.contextWindow = m.contextWindow;
1430
1453
  }
1431
1454
  if (m.clientType !== void 0) {
1432
1455
  if (typeof m.clientType !== "string" || m.clientType.length > 64) {
1433
- throw badRequest(`models[${i}].clientType \u5FC5\u987B\u662F\u957F\u5EA6\u4E0D\u8D85\u8FC7 64 \u7684\u5B57\u7B26\u4E32\u3002`);
1456
+ throw badRequest(`models[${i}].clientType must be a string of at most 64 characters.`);
1434
1457
  }
1435
1458
  if (m.clientType) entry.clientType = m.clientType;
1436
1459
  }
1437
1460
  if (m.vision !== void 0) {
1438
1461
  if (typeof m.vision !== "boolean") {
1439
- throw badRequest(`models[${i}].vision \u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002`);
1462
+ throw badRequest(`models[${i}].vision must be a boolean.`);
1440
1463
  }
1441
1464
  entry.vision = m.vision;
1442
1465
  }
1466
+ if (m.maxTokens !== void 0) {
1467
+ if (typeof m.maxTokens !== "number" || !Number.isInteger(m.maxTokens) || m.maxTokens <= 0) {
1468
+ throw badRequest(`models[${i}].maxTokens must be a positive integer.`);
1469
+ }
1470
+ entry.maxTokens = m.maxTokens;
1471
+ }
1443
1472
  if (m.pricing !== void 0) {
1444
1473
  const p = m.pricing;
1445
1474
  if (p === null || typeof p !== "object" || Array.isArray(p)) {
1446
- throw badRequest(`models[${i}].pricing \u5FC5\u987B\u662F\u5BF9\u8C61\u3002`);
1475
+ throw badRequest(`models[${i}].pricing must be an object.`);
1447
1476
  }
1448
1477
  for (const key of ["cacheRead", "cacheWrite", "output"]) {
1449
1478
  const v = p[key];
1450
1479
  if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
1451
- throw badRequest(`models[${i}].pricing.${key} \u5FC5\u987B\u662F\u975E\u8D1F\u6570\u5B57\u3002`);
1480
+ throw badRequest(`models[${i}].pricing.${key} must be a non-negative number.`);
1452
1481
  }
1453
1482
  }
1454
1483
  entry.pricing = {
@@ -1459,19 +1488,19 @@ function parseModelsUpdate(body) {
1459
1488
  }
1460
1489
  if (m.apiKey !== void 0) {
1461
1490
  if (typeof m.apiKey !== "string" || m.apiKey.length === 0) {
1462
- throw badRequest(`models[${i}].apiKey \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
1491
+ throw badRequest(`models[${i}].apiKey must be a non-empty string.`);
1463
1492
  }
1464
1493
  entry.apiKey = m.apiKey;
1465
1494
  }
1466
1495
  if (m.clearApiKey !== void 0) {
1467
1496
  if (typeof m.clearApiKey !== "boolean") {
1468
- throw badRequest(`models[${i}].clearApiKey \u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002`);
1497
+ throw badRequest(`models[${i}].clearApiKey must be a boolean.`);
1469
1498
  }
1470
1499
  entry.clearApiKey = m.clearApiKey;
1471
1500
  }
1472
1501
  if (m.baseUrl !== void 0) {
1473
1502
  if (m.baseUrl !== null && typeof m.baseUrl !== "string") {
1474
- throw badRequest(`models[${i}].baseUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216 null\u3002`);
1503
+ throw badRequest(`models[${i}].baseUrl must be a string or null.`);
1475
1504
  }
1476
1505
  entry.baseUrl = m.baseUrl;
1477
1506
  }
@@ -1508,21 +1537,25 @@ function modelsRoutes(deps2) {
1508
1537
  modelId: requireString(body, "modelId", { minLen: 1, maxLen: 200 })
1509
1538
  };
1510
1539
  if (body.apiKey !== void 0) {
1511
- if (typeof body.apiKey !== "string") throw badRequest("apiKey \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002");
1540
+ if (typeof body.apiKey !== "string") throw badRequest("apiKey must be a string.");
1512
1541
  if (body.apiKey) req.apiKey = body.apiKey;
1513
1542
  }
1514
1543
  if (body.clearApiKey !== void 0) {
1515
- if (typeof body.clearApiKey !== "boolean") throw badRequest("clearApiKey \u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002");
1544
+ if (typeof body.clearApiKey !== "boolean") throw badRequest("clearApiKey must be a boolean.");
1516
1545
  req.clearApiKey = body.clearApiKey;
1517
1546
  }
1547
+ if (body.speed !== void 0) {
1548
+ if (typeof body.speed !== "boolean") throw badRequest("speed must be a boolean.");
1549
+ req.speed = body.speed;
1550
+ }
1518
1551
  if (body.baseUrl !== void 0) {
1519
1552
  if (body.baseUrl !== null && typeof body.baseUrl !== "string") {
1520
- throw badRequest("baseUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216 null\u3002");
1553
+ throw badRequest("baseUrl must be a string or null.");
1521
1554
  }
1522
1555
  req.baseUrl = body.baseUrl ? body.baseUrl : null;
1523
1556
  }
1524
1557
  if (body.clientType !== void 0) {
1525
- if (typeof body.clientType !== "string") throw badRequest("clientType \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002");
1558
+ if (typeof body.clientType !== "string") throw badRequest("clientType must be a string.");
1526
1559
  if (body.clientType) req.clientType = body.clientType;
1527
1560
  }
1528
1561
  return c.json(await deps2.projectConfigService.testModel(projectId, req));
@@ -1533,10 +1566,10 @@ function modelsRoutes(deps2) {
1533
1566
  // src/http/routes/vault.ts
1534
1567
  import { Hono as Hono8 } from "hono";
1535
1568
  function parseVaultUpdate(body) {
1536
- if (!Array.isArray(body.entries)) throw badRequest("entries \u5FC5\u987B\u662F\u6570\u7EC4\u3002");
1569
+ if (!Array.isArray(body.entries)) throw badRequest("entries must be an array.");
1537
1570
  const entries = body.entries.map((item, i) => {
1538
1571
  if (item === null || typeof item !== "object" || Array.isArray(item)) {
1539
- throw badRequest(`entries[${i}] \u5FC5\u987B\u662F\u5BF9\u8C61\u3002`);
1572
+ throw badRequest(`entries[${i}] must be an object.`);
1540
1573
  }
1541
1574
  const e = item;
1542
1575
  const entry = {
@@ -1544,7 +1577,7 @@ function parseVaultUpdate(body) {
1544
1577
  };
1545
1578
  if (e.value !== void 0) {
1546
1579
  if (typeof e.value !== "string" || e.value.length === 0) {
1547
- throw badRequest(`entries[${i}].value \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
1580
+ throw badRequest(`entries[${i}].value must be a non-empty string.`);
1548
1581
  }
1549
1582
  entry.value = e.value;
1550
1583
  }
@@ -1566,6 +1599,7 @@ function vaultRoutes(deps2) {
1566
1599
  deps2.projectService.requireProjectOwner(c.var.user.userId, projectId);
1567
1600
  const req = parseVaultUpdate(await readJson(c));
1568
1601
  const res = await deps2.agentConfigService.updateVault(projectId, agentId, req);
1602
+ deps2.manager.invalidateAgentRuntimes(projectId, agentId);
1569
1603
  return c.json(res);
1570
1604
  });
1571
1605
  return app2;
@@ -1603,72 +1637,77 @@ function parseScheduleFile(name, raw) {
1603
1637
  } catch (err) {
1604
1638
  return {
1605
1639
  ok: false,
1606
- error: `TOML \u89E3\u6790\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`
1640
+ error: `Failed to parse TOML: ${err instanceof Error ? err.message : String(err)}`
1607
1641
  };
1608
1642
  }
1609
1643
  if (parsed === null || typeof parsed !== "object")
1610
- return { ok: false, error: "\u5185\u5BB9\u4E0D\u662F TOML \u8868" };
1644
+ return { ok: false, error: "Content is not a TOML table" };
1611
1645
  const t = parsed;
1612
1646
  const prompt = t["prompt"];
1613
1647
  if (typeof prompt !== "string" || prompt.trim() === "") {
1614
- return { ok: false, error: "\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 prompt" };
1648
+ return { ok: false, error: "Missing required field prompt" };
1615
1649
  }
1616
1650
  const enabled = t["enabled"] === void 0 ? false : t["enabled"];
1617
- if (typeof enabled !== "boolean") return { ok: false, error: "enabled \u5FC5\u987B\u662F\u5E03\u5C14\u503C" };
1651
+ if (typeof enabled !== "boolean") return { ok: false, error: "enabled must be a boolean" };
1618
1652
  const startAt = parseInstant(t["start_at"]);
1619
- if (startAt === null) return { ok: false, error: "start_at \u7F3A\u5931\u6216\u4E0D\u662F\u5408\u6CD5\u7684 ISO 8601 \u65F6\u523B" };
1653
+ if (startAt === null)
1654
+ return { ok: false, error: "start_at is missing or not a valid ISO 8601 instant" };
1620
1655
  let period;
1621
1656
  let periodMs;
1622
1657
  if (t["period"] !== void 0) {
1623
- if (typeof t["period"] !== "string") return { ok: false, error: "period \u5FC5\u987B\u662F\u5B57\u7B26\u4E32" };
1658
+ if (typeof t["period"] !== "string") return { ok: false, error: "period must be a string" };
1624
1659
  const ms = parsePeriod(t["period"]);
1625
- if (ms === null) return { ok: false, error: "period \u5FC5\u987B\u5F62\u5982 30m / 12h / 7d" };
1626
- if (ms < MIN_PERIOD_MS) return { ok: false, error: "period \u4F4E\u4E8E\u4E0B\u9650 5m" };
1660
+ if (ms === null) return { ok: false, error: "period must look like 30m / 12h / 7d" };
1661
+ if (ms < MIN_PERIOD_MS) return { ok: false, error: "period is below the 5m minimum" };
1627
1662
  period = t["period"].trim();
1628
1663
  periodMs = ms;
1629
1664
  }
1630
1665
  let endAt;
1631
1666
  if (t["end_at"] !== void 0) {
1632
1667
  const parsedEnd = parseInstant(t["end_at"]);
1633
- if (parsedEnd === null) return { ok: false, error: "end_at \u4E0D\u662F\u5408\u6CD5\u7684 ISO 8601 \u65F6\u523B" };
1634
- if (parsedEnd.ms <= startAt.ms) return { ok: false, error: "end_at \u5FC5\u987B\u665A\u4E8E start_at" };
1668
+ if (parsedEnd === null) return { ok: false, error: "end_at is not a valid ISO 8601 instant" };
1669
+ if (parsedEnd.ms <= startAt.ms)
1670
+ return { ok: false, error: "end_at must be later than start_at" };
1635
1671
  endAt = parsedEnd;
1636
1672
  }
1637
1673
  let sessionId;
1638
1674
  if (t["session_id"] !== void 0) {
1639
1675
  if (typeof t["session_id"] !== "string" || t["session_id"] === "") {
1640
- return { ok: false, error: "session_id \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32" };
1676
+ return { ok: false, error: "session_id must be a non-empty string" };
1641
1677
  }
1642
1678
  sessionId = t["session_id"];
1643
1679
  }
1644
1680
  let workspace;
1645
1681
  if (t["workspace"] !== void 0) {
1646
1682
  if (typeof t["workspace"] !== "string" || t["workspace"] === "") {
1647
- return { ok: false, error: "workspace \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32" };
1683
+ return { ok: false, error: "workspace must be a non-empty string" };
1648
1684
  }
1649
1685
  workspace = t["workspace"];
1650
1686
  }
1651
1687
  let modelId;
1652
1688
  if (t["model_id"] !== void 0) {
1653
1689
  if (typeof t["model_id"] !== "string" || t["model_id"] === "") {
1654
- return { ok: false, error: "model_id \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32" };
1690
+ return { ok: false, error: "model_id must be a non-empty string" };
1655
1691
  }
1656
1692
  modelId = t["model_id"];
1657
1693
  }
1658
1694
  let provider;
1659
1695
  if (t["provider"] !== void 0) {
1660
1696
  if (typeof t["provider"] !== "string" || t["provider"] === "") {
1661
- return { ok: false, error: "provider \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32" };
1697
+ return { ok: false, error: "provider must be a non-empty string" };
1662
1698
  }
1663
1699
  provider = t["provider"];
1664
1700
  }
1665
- if (provider !== void 0 && modelId === void 0) {
1666
- return { ok: false, error: "provider \u4EC5\u4E0E model_id \u6210\u5BF9\u4F7F\u7528\uFF08\u6A21\u578B\u5F15\u7528\u987B\u6210\u5BF9\u7ED9\u51FA\uFF09" };
1667
- }
1668
1701
  if (sessionId !== void 0 && (workspace !== void 0 || modelId !== void 0 || provider !== void 0)) {
1669
1702
  return {
1670
1703
  ok: false,
1671
- error: "\u76EE\u6807\u4E8C\u9009\u4E00\uFF1Aworkspace \u4E0E provider / model_id \u4EC5\u7528\u4E8E\u65B0\u5EFA Session \u6A21\u5F0F"
1704
+ error: "Pick one target: workspace and provider / model_id are only for new-Session mode"
1705
+ };
1706
+ }
1707
+ if (modelId === void 0 !== (provider === void 0)) {
1708
+ return {
1709
+ ok: false,
1710
+ error: "provider and model_id must be given together (a model reference is always a pair); omit both to use the Project's default model"
1672
1711
  };
1673
1712
  }
1674
1713
  return {
@@ -1763,7 +1802,10 @@ function serializeSchedule(fields) {
1763
1802
  `;
1764
1803
  }
1765
1804
  async function validateScheduleModelRef(root, projectId, def) {
1766
- if (def.modelId === void 0) return null;
1805
+ if (def.modelId === void 0 && def.provider === void 0) return null;
1806
+ if (def.modelId === void 0 || def.provider === void 0) {
1807
+ return "provider and model_id must be given together (a model reference is always a pair); omit both to use the Project's default model";
1808
+ }
1767
1809
  try {
1768
1810
  const cfg = await loadProjectConfig(root, projectId);
1769
1811
  resolveModelRef(cfg, def.modelId, def.provider);
@@ -1789,7 +1831,7 @@ async function deleteScheduleFile(root, projectId, agentId, name) {
1789
1831
 
1790
1832
  // src/http/routes/schedules.ts
1791
1833
  function parseUpsertBody(body) {
1792
- if (typeof body.enabled !== "boolean") throw badRequest("enabled \u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002");
1834
+ if (typeof body.enabled !== "boolean") throw badRequest("enabled must be a boolean.");
1793
1835
  const prompt = requireString(body, "prompt", { minLen: 1, maxLen: 1e5 });
1794
1836
  const startAt = requireString(body, "startAt", { minLen: 1, maxLen: 100 });
1795
1837
  const period = optionalString(body, "period", { minLen: 1, maxLen: 20 });
@@ -1849,7 +1891,7 @@ function toItem(def, state, queued, nowMs) {
1849
1891
  };
1850
1892
  }
1851
1893
  function requireScheduleName(raw) {
1852
- if (!raw || !isValidId2(raw)) throw badRequest("\u5B9A\u65F6\u4EFB\u52A1\u540D\u975E\u6CD5\u3002");
1894
+ if (!raw || !isValidId2(raw)) throw badRequest("Invalid schedule name.");
1853
1895
  return raw;
1854
1896
  }
1855
1897
  function scheduleRoutes(deps2) {
@@ -1875,7 +1917,7 @@ function scheduleRoutes(deps2) {
1875
1917
  const body = await readJson(c);
1876
1918
  const name = requireScheduleName(requireString(body, "name", { minLen: 1, maxLen: 100 }));
1877
1919
  if (await readScheduleFile(deps2.config.root, projectId, agentId, name)) {
1878
- throw new HttpError(409, "schedule_exists", `\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5B58\u5728\uFF1A${name}`);
1920
+ throw new HttpError(409, "schedule_exists", `Schedule already exists: ${name}`);
1879
1921
  }
1880
1922
  await upsert(deps2, c.var.user.userId, projectId, agentId, name, body);
1881
1923
  return c.json(await readItem(deps2, projectId, agentId, name), 201);
@@ -1894,7 +1936,7 @@ function scheduleRoutes(deps2) {
1894
1936
  deps2.projectService.requireProjectOwner(c.var.user.userId, projectId);
1895
1937
  const name = requireScheduleName(c.req.param("name"));
1896
1938
  if (!await readScheduleFile(deps2.config.root, projectId, agentId, name)) {
1897
- throw new HttpError(404, "schedule_not_found", `\u5B9A\u65F6\u4EFB\u52A1\u4E0D\u5B58\u5728\uFF1A${name}`);
1939
+ throw new HttpError(404, "schedule_not_found", `Schedule does not exist: ${name}`);
1898
1940
  }
1899
1941
  const body = await readJson(c);
1900
1942
  await upsert(deps2, c.var.user.userId, projectId, agentId, name, body);
@@ -1906,7 +1948,8 @@ function scheduleRoutes(deps2) {
1906
1948
  deps2.projectService.requireProjectOwner(c.var.user.userId, projectId);
1907
1949
  const name = requireScheduleName(c.req.param("name"));
1908
1950
  const removed = await deleteScheduleFile(deps2.config.root, projectId, agentId, name);
1909
- if (!removed) throw new HttpError(404, "schedule_not_found", `\u5B9A\u65F6\u4EFB\u52A1\u4E0D\u5B58\u5728\uFF1A${name}`);
1951
+ if (!removed)
1952
+ throw new HttpError(404, "schedule_not_found", `Schedule does not exist: ${name}`);
1910
1953
  deps2.scheduler.dropEntry(projectId, agentId, name);
1911
1954
  return c.body(null, 204);
1912
1955
  });
@@ -1916,9 +1959,9 @@ async function upsert(deps2, userId, projectId, agentId, name, body) {
1916
1959
  const fields = parseUpsertBody(body);
1917
1960
  const raw = serializeSchedule(fields);
1918
1961
  const parsed = parseScheduleFile(name, raw);
1919
- if (!parsed.ok) throw badRequest(`\u5B9A\u65F6\u4EFB\u52A1\u914D\u7F6E\u975E\u6CD5\uFF1A${parsed.error}`);
1962
+ if (!parsed.ok) throw badRequest(`Invalid schedule configuration: ${parsed.error}`);
1920
1963
  const refError = await validateScheduleModelRef(deps2.config.root, projectId, parsed.def);
1921
- if (refError !== null) throw badRequest(`\u5B9A\u65F6\u4EFB\u52A1\u914D\u7F6E\u975E\u6CD5\uFF1A${refError}`);
1964
+ if (refError !== null) throw badRequest(`Invalid schedule configuration: ${refError}`);
1922
1965
  await writeScheduleFile(deps2.config.root, projectId, agentId, name, raw);
1923
1966
  deps2.schedulesRepo.registerOrSync({
1924
1967
  projectId,
@@ -1935,8 +1978,8 @@ async function readItem(deps2, projectId, agentId, name) {
1935
1978
  const entry = entries.find((e) => e.def.name === name);
1936
1979
  if (entry) return toItem(entry.def, entry.state, entry.queued, Date.now());
1937
1980
  const bad = invalid.find((i) => i.name === name);
1938
- if (bad) throw badRequest(`\u5B9A\u65F6\u4EFB\u52A1\u6587\u4EF6\u975E\u6CD5\uFF1A${bad.error}`);
1939
- throw new HttpError(404, "schedule_not_found", `\u5B9A\u65F6\u4EFB\u52A1\u4E0D\u5B58\u5728\uFF1A${name}`);
1981
+ if (bad) throw badRequest(`Invalid schedule file: ${bad.error}`);
1982
+ throw new HttpError(404, "schedule_not_found", `Schedule does not exist: ${name}`);
1940
1983
  }
1941
1984
 
1942
1985
  // src/http/routes/benchmarks.ts
@@ -1987,11 +2030,11 @@ function libraryResponse() {
1987
2030
  }
1988
2031
  function parseInstallNames(body) {
1989
2032
  if (!Array.isArray(body.names) || body.names.length === 0) {
1990
- throw badRequest("names \u5FC5\u987B\u662F\u975E\u7A7A\u6570\u7EC4\u3002");
2033
+ throw badRequest("names must be a non-empty array.");
1991
2034
  }
1992
2035
  return body.names.map((v, i) => {
1993
2036
  if (typeof v !== "string" || v.length === 0) {
1994
- throw badRequest(`names[${i}] \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
2037
+ throw badRequest(`names[${i}] must be a non-empty string.`);
1995
2038
  }
1996
2039
  return v;
1997
2040
  });
@@ -2021,7 +2064,7 @@ function agentSkillsRoutes(deps2) {
2021
2064
  const names = parseInstallNames(await readJson(c));
2022
2065
  const skills = names.map((name) => {
2023
2066
  const skill = librarySkill(name);
2024
- if (!skill) throw new HttpError(404, "unknown_skill", `Skill \u5E93\u4E2D\u4E0D\u5B58\u5728\uFF1A${name}`);
2067
+ if (!skill) throw new HttpError(404, "unknown_skill", `Skill is not in the library: ${name}`);
2025
2068
  return skill;
2026
2069
  });
2027
2070
  for (const skill of skills) {
@@ -2038,7 +2081,7 @@ function agentSkillsRoutes(deps2) {
2038
2081
  try {
2039
2082
  await fs2.access(file);
2040
2083
  } catch {
2041
- throw new HttpError(404, "not_found", `Skill \u672A\u5B89\u88C5\uFF1A${name}`);
2084
+ throw new HttpError(404, "not_found", `Skill is not installed: ${name}`);
2042
2085
  }
2043
2086
  await removeSkill(deps2.config.root, projectId, agentId, name);
2044
2087
  return c.body(null, 204);
@@ -2077,10 +2120,11 @@ function agentTransferRoutes(deps2) {
2077
2120
  try {
2078
2121
  archive = Buffer.from(dataBase64, "base64");
2079
2122
  } catch {
2080
- throw badRequest("dataBase64 \u4E0D\u662F\u5408\u6CD5\u7684 base64\u3002");
2123
+ throw badRequest("dataBase64 is not valid base64.");
2081
2124
  }
2082
- if (archive.byteLength === 0) throw badRequest("\u5BFC\u5165\u5305\u4E3A\u7A7A\u3002");
2083
- if (archive.byteLength > MAX_ARCHIVE_BYTES) throw badRequest("\u5BFC\u5165\u5305\u8D85\u8FC7 14MB \u4E0A\u9650\u3002");
2125
+ if (archive.byteLength === 0) throw badRequest("Import package is empty.");
2126
+ if (archive.byteLength > MAX_ARCHIVE_BYTES)
2127
+ throw badRequest("Import package exceeds the 14MB limit.");
2084
2128
  const { version } = await deps2.snapshots.importArchive(projectId, agentId, archive, confirm);
2085
2129
  const res = { version };
2086
2130
  return c.json(res);
@@ -2175,17 +2219,21 @@ function dirsRoutes(deps2) {
2175
2219
  const raw = c.req.query("path");
2176
2220
  const target = raw && raw.trim() ? raw.trim() : os.homedir();
2177
2221
  if (!path4.isAbsolute(target)) {
2178
- throw new HttpError(400, "dir_not_absolute", "\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\u3002");
2222
+ throw new HttpError(400, "dir_not_absolute", "Directory must be an absolute path.");
2179
2223
  }
2180
2224
  let real;
2181
2225
  try {
2182
2226
  real = await fs4.realpath(target);
2183
2227
  } catch {
2184
- throw new HttpError(404, "dir_not_found", `\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u8BBF\u95EE\uFF1A${target}\u3002`);
2228
+ throw new HttpError(
2229
+ 404,
2230
+ "dir_not_found",
2231
+ `Directory does not exist or is inaccessible: ${target}.`
2232
+ );
2185
2233
  }
2186
2234
  const stat = await fs4.stat(real);
2187
2235
  if (!stat.isDirectory()) {
2188
- throw new HttpError(400, "not_a_dir", "\u4E0D\u662F\u76EE\u5F55\u3002");
2236
+ throw new HttpError(400, "not_a_dir", "Not a directory.");
2189
2237
  }
2190
2238
  let dirents = [];
2191
2239
  try {
@@ -2228,7 +2276,7 @@ function agentConfigRoutes(deps2) {
2228
2276
  if (agentsMd !== void 0) req.agentsMd = agentsMd;
2229
2277
  if (body.config !== void 0) {
2230
2278
  if (body.config === null || typeof body.config !== "object" || Array.isArray(body.config)) {
2231
- throw badRequest("config \u5FC5\u987B\u662F\u5BF9\u8C61\u3002");
2279
+ throw badRequest("config must be an object.");
2232
2280
  }
2233
2281
  req.config = body.config;
2234
2282
  }
@@ -2284,7 +2332,7 @@ function usageRoutes(deps2) {
2284
2332
  deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
2285
2333
  const groupByRaw = c.req.query("groupBy") ?? "date";
2286
2334
  if (!GROUP_BYS.includes(groupByRaw)) {
2287
- throw badRequest(`groupBy \u5FC5\u987B\u662F ${GROUP_BYS.join(" / ")} \u4E4B\u4E00\u3002`);
2335
+ throw badRequest(`groupBy must be one of ${GROUP_BYS.join(" / ")}.`);
2288
2336
  }
2289
2337
  const from = optionalDateParam(c.req.query("from"), "from");
2290
2338
  const to = optionalDateParam(c.req.query("to"), "to");
@@ -2326,12 +2374,16 @@ async function assertWorkspaceAllowed(args) {
2326
2374
  throw new HttpError(
2327
2375
  400,
2328
2376
  "workspace_not_found",
2329
- `Workspace \u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u8BBF\u95EE\uFF1A${args.workspace}\u3002\u8BF7\u6307\u5B9A\u4E00\u4E2A\u5DF2\u5B58\u5728\u7684\u76EE\u5F55\uFF0C\u6216\u7559\u7A7A\u4EE5\u4F7F\u7528\u4E34\u65F6\u76EE\u5F55\u3002`
2377
+ `Workspace does not exist or is inaccessible: ${args.workspace}. Specify an existing directory, or leave it empty to use a temporary directory.`
2330
2378
  );
2331
2379
  }
2332
2380
  const stat = await fs5.stat(ws);
2333
2381
  if (!stat.isDirectory()) {
2334
- throw new HttpError(400, "workspace_not_found", `Workspace \u4E0D\u662F\u76EE\u5F55\uFF1A${args.workspace}\u3002`);
2382
+ throw new HttpError(
2383
+ 400,
2384
+ "workspace_not_found",
2385
+ `Workspace is not a directory: ${args.workspace}.`
2386
+ );
2335
2387
  }
2336
2388
  return ws;
2337
2389
  }
@@ -2373,7 +2425,7 @@ var WorkspaceFilesService = class {
2373
2425
  try {
2374
2426
  return await fs6.realpath(path5.resolve(workspace));
2375
2427
  } catch {
2376
- throw new HttpError(404, "workspace_missing", "\u8BE5 Session \u7684 Workspace \u5DF2\u4E0D\u5B58\u5728\u3002");
2428
+ throw new HttpError(404, "workspace_missing", "This Session's Workspace no longer exists.");
2377
2429
  }
2378
2430
  }
2379
2431
  /**
@@ -2389,16 +2441,16 @@ var WorkspaceFilesService = class {
2389
2441
  }
2390
2442
  /** Lexical prefix check (a relative path, once resolved, must still be inside the Workspace); returns the absolute target path. */
2391
2443
  lexicalTarget(base, rel) {
2392
- if (rel.includes("\0")) throw badRequest("path \u975E\u6CD5\u3002");
2444
+ if (rel.includes("\0")) throw badRequest("path is invalid.");
2393
2445
  const target = path5.resolve(base, rel === "" ? "." : rel);
2394
2446
  if (!this.isInside(target, base)) {
2395
- throw badRequest("path \u5FC5\u987B\u4F4D\u4E8E Workspace \u5185\u3002");
2447
+ throw badRequest("path must be inside the Workspace.");
2396
2448
  }
2397
2449
  return target;
2398
2450
  }
2399
2451
  assertInside(real, realBase) {
2400
2452
  if (!this.isInside(real, realBase)) {
2401
- throw badRequest("path \u5FC5\u987B\u4F4D\u4E8E Workspace \u5185\u3002");
2453
+ throw badRequest("path must be inside the Workspace.");
2402
2454
  }
2403
2455
  }
2404
2456
  /**
@@ -2416,7 +2468,7 @@ var WorkspaceFilesService = class {
2416
2468
  canonical = await fs6.realpath(target);
2417
2469
  } catch (err) {
2418
2470
  if (err.code === "ENOENT") {
2419
- throw new HttpError(404, "path_not_found", "\u6587\u4EF6\u4E0D\u5B58\u5728\u3002");
2471
+ throw new HttpError(404, "path_not_found", "File does not exist.");
2420
2472
  }
2421
2473
  throw err;
2422
2474
  }
@@ -2441,7 +2493,7 @@ var WorkspaceFilesService = class {
2441
2493
  const realBase = await this.realBase(workspace);
2442
2494
  const target = this.lexicalTarget(path5.resolve(workspace), rel);
2443
2495
  const name = path5.basename(target);
2444
- if (name === "" || name === "." || name === "..") throw badRequest("path \u5FC5\u987B\u662F\u6587\u4EF6\u8DEF\u5F84\u3002");
2496
+ if (name === "" || name === "." || name === "..") throw badRequest("path must be a file path.");
2445
2497
  const parent = path5.dirname(target);
2446
2498
  let canonicalParent;
2447
2499
  try {
@@ -2456,7 +2508,7 @@ var WorkspaceFilesService = class {
2456
2508
  } catch (probeErr) {
2457
2509
  if (probeErr.code !== "ENOENT") throw probeErr;
2458
2510
  const up = path5.dirname(probe);
2459
- if (up === probe) throw badRequest("path \u975E\u6CD5\u3002");
2511
+ if (up === probe) throw badRequest("path is invalid.");
2460
2512
  probe = up;
2461
2513
  }
2462
2514
  }
@@ -2496,10 +2548,10 @@ var WorkspaceFilesService = class {
2496
2548
  dirents = await fs6.readdir(dir, { withFileTypes: true });
2497
2549
  } catch (err) {
2498
2550
  if (err.code === "ENOENT") {
2499
- throw new HttpError(404, "path_not_found", "\u76EE\u5F55\u4E0D\u5B58\u5728\u3002");
2551
+ throw new HttpError(404, "path_not_found", "Directory does not exist.");
2500
2552
  }
2501
2553
  if (err.code === "ENOTDIR") {
2502
- throw badRequest("path \u4E0D\u662F\u76EE\u5F55\u3002");
2554
+ throw badRequest("path is not a directory.");
2503
2555
  }
2504
2556
  throw err;
2505
2557
  }
@@ -2535,11 +2587,11 @@ var WorkspaceFilesService = class {
2535
2587
  try {
2536
2588
  stat = await fs6.stat(file);
2537
2589
  } catch {
2538
- throw new HttpError(404, "path_not_found", "\u6587\u4EF6\u4E0D\u5B58\u5728\u3002");
2590
+ throw new HttpError(404, "path_not_found", "File does not exist.");
2539
2591
  }
2540
- if (stat.isDirectory()) throw badRequest("path \u662F\u76EE\u5F55\u3002");
2592
+ if (stat.isDirectory()) throw badRequest("path is a directory.");
2541
2593
  if (stat.size > MAX_READ_BYTES) {
2542
- throw new HttpError(413, "file_too_large", "\u6587\u4EF6\u8D85\u8FC7 50MB \u8BFB\u53D6\u4E0A\u9650\u3002");
2594
+ throw new HttpError(413, "file_too_large", "File exceeds the 50MB read limit.");
2543
2595
  }
2544
2596
  const data = await fs6.readFile(file);
2545
2597
  const ext = path5.extname(file).toLowerCase();
@@ -2559,9 +2611,9 @@ var WorkspaceFilesService = class {
2559
2611
  * sandbox escapes).
2560
2612
  */
2561
2613
  async write(workspace, rel, data) {
2562
- if (rel === "" || rel.endsWith("/")) throw badRequest("path \u5FC5\u987B\u662F\u6587\u4EF6\u8DEF\u5F84\u3002");
2614
+ if (rel === "" || rel.endsWith("/")) throw badRequest("path must be a file path.");
2563
2615
  if (data.length > MAX_UPLOAD_BYTES) {
2564
- throw new HttpError(413, "file_too_large", "\u4E0A\u4F20\u6587\u4EF6\u8D85\u8FC7 14MB \u4E0A\u9650\u3002");
2616
+ throw new HttpError(413, "file_too_large", "Uploaded file exceeds the 14MB limit.");
2565
2617
  }
2566
2618
  const { dir, name } = await this.resolveWriteParent(workspace, rel);
2567
2619
  const file = path5.join(dir, name);
@@ -2571,9 +2623,10 @@ var WorkspaceFilesService = class {
2571
2623
  handle = await fs6.open(file, flags, 420);
2572
2624
  } catch (err) {
2573
2625
  const code = err.code;
2574
- if (code === "ELOOP") throw badRequest("path \u4E0D\u80FD\u662F\u7B26\u53F7\u94FE\u63A5\u3002");
2575
- if (code === "ENOENT") throw new HttpError(404, "path_not_found", "\u7236\u76EE\u5F55\u4E0D\u5B58\u5728\u3002");
2576
- if (code === "EISDIR") throw badRequest("path \u662F\u76EE\u5F55\u3002");
2626
+ if (code === "ELOOP") throw badRequest("path must not be a symlink.");
2627
+ if (code === "ENOENT")
2628
+ throw new HttpError(404, "path_not_found", "Parent directory does not exist.");
2629
+ if (code === "EISDIR") throw badRequest("path is a directory.");
2577
2630
  throw err;
2578
2631
  }
2579
2632
  try {
@@ -2597,27 +2650,27 @@ var APPROVAL_MODES = [
2597
2650
  function parseTaskInput(body) {
2598
2651
  const input = body.input;
2599
2652
  if (!Array.isArray(input) || input.length === 0) {
2600
- throw badRequest("input \u5FC5\u987B\u662F\u81F3\u5C11\u5305\u542B\u4E00\u9879\u7684\u6570\u7EC4\u3002");
2653
+ throw badRequest("input must be an array with at least one item.");
2601
2654
  }
2602
2655
  return input.map((item, i) => {
2603
2656
  if (item === null || typeof item !== "object" || Array.isArray(item)) {
2604
- throw badRequest(`input[${i}] \u5FC5\u987B\u662F\u5BF9\u8C61\u3002`);
2657
+ throw badRequest(`input[${i}] must be an object.`);
2605
2658
  }
2606
2659
  const part = item;
2607
2660
  if (part.type === "text") {
2608
2661
  if (typeof part.text !== "string" || part.text.length === 0) {
2609
- throw badRequest(`input[${i}].text \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
2662
+ throw badRequest(`input[${i}].text must be a non-empty string.`);
2610
2663
  }
2611
2664
  return userText(part.text);
2612
2665
  }
2613
2666
  if (part.type === "image_url") {
2614
2667
  const url = part.imageUrl;
2615
2668
  if (typeof url !== "string" || !(url.startsWith("data:") || url.startsWith("http://") || url.startsWith("https://"))) {
2616
- throw badRequest(`input[${i}].imageUrl \u4EC5\u652F\u6301 data: \u6216 http(s) URL\u3002`);
2669
+ throw badRequest(`input[${i}].imageUrl only supports data: or http(s) URLs.`);
2617
2670
  }
2618
2671
  return imageUrlMessage(url);
2619
2672
  }
2620
- throw badRequest(`input[${i}].type \u5FC5\u987B\u662F text / image_url \u4E4B\u4E00\u3002`);
2673
+ throw badRequest(`input[${i}].type must be one of text / image_url.`);
2621
2674
  });
2622
2675
  }
2623
2676
  function agentSessionsRoutes(deps2) {
@@ -2627,7 +2680,12 @@ function agentSessionsRoutes(deps2) {
2627
2680
  const agentId = requireValidId(c, "agentId");
2628
2681
  deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
2629
2682
  await deps2.agentConfigService.requireExists(projectId, agentId);
2630
- const sessions = await deps2.sessionService.listSessions(projectId, agentId);
2683
+ const paging = optionalPagingQuery(c);
2684
+ const sessions = await deps2.sessionService.listSessions(
2685
+ projectId,
2686
+ agentId,
2687
+ ...paging ? [paging] : []
2688
+ );
2631
2689
  return c.json({ sessions });
2632
2690
  });
2633
2691
  app2.post("/", async (c) => {
@@ -2638,8 +2696,10 @@ function agentSessionsRoutes(deps2) {
2638
2696
  const body = await readJson(c);
2639
2697
  const modelId = optionalString(body, "modelId", { minLen: 1, label: "modelId" });
2640
2698
  const provider = optionalString(body, "provider", { minLen: 1, label: "provider" });
2641
- if (provider !== void 0 && modelId === void 0) {
2642
- throw badRequest("\u6307\u5B9A\u4E86 provider \u5374\u672A\u6307\u5B9A modelId\uFF1A\u6A21\u578B\u5F15\u7528\u987B\u6210\u5BF9\u7ED9\u51FA\u3002");
2699
+ if (modelId === void 0 !== (provider === void 0)) {
2700
+ throw badRequest(
2701
+ "modelId and provider must be given together as a model reference pair: specify both, or neither to use the Project's default model."
2702
+ );
2643
2703
  }
2644
2704
  const approvalMode = optionalEnum(body, "approvalMode", APPROVAL_MODES);
2645
2705
  let workspace = optionalString(body, "workspace", { minLen: 1, label: "workspace" });
@@ -2664,19 +2724,29 @@ function sessionsRoutes(deps2) {
2664
2724
  const sessionId = c.req.param("sessionId");
2665
2725
  const row = sessionId ? deps2.sessionsRepo.findById(sessionId) : null;
2666
2726
  if (!row) {
2667
- throw new HttpError(404, "session_not_found", "Session \u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE\u3002");
2727
+ throw new HttpError(
2728
+ 404,
2729
+ "session_not_found",
2730
+ "Session does not exist or you do not have access."
2731
+ );
2668
2732
  }
2669
2733
  try {
2670
2734
  deps2.projectService.requireProjectAccess(c.var.user.userId, row.projectId);
2671
2735
  } catch {
2672
- throw new HttpError(404, "session_not_found", "Session \u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE\u3002");
2736
+ throw new HttpError(
2737
+ 404,
2738
+ "session_not_found",
2739
+ "Session does not exist or you do not have access."
2740
+ );
2673
2741
  }
2674
2742
  return row;
2675
2743
  };
2676
2744
  app2.get("/:sessionId", async (c) => {
2677
2745
  const row = resolveSession(c);
2678
2746
  const hasTrace = await deps2.sessionService.hasTrace(row);
2679
- return c.json({ session: deps2.sessionService.toInfo(row, hasTrace) });
2747
+ return c.json({
2748
+ session: await deps2.sessionService.toInfo(row, hasTrace)
2749
+ });
2680
2750
  });
2681
2751
  app2.patch("/:sessionId", async (c) => {
2682
2752
  const row = resolveSession(c);
@@ -2688,15 +2758,23 @@ function sessionsRoutes(deps2) {
2688
2758
  let title;
2689
2759
  if (titleRaw !== void 0) {
2690
2760
  if (typeof titleRaw !== "string") {
2691
- throw new HttpError(400, "invalid_title", "title \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002");
2761
+ throw new HttpError(400, "invalid_title", "title must be a string.");
2692
2762
  }
2693
2763
  title = titleRaw.trim();
2694
2764
  if (!title || title.length > SESSION_TITLE_MAX) {
2695
- throw new HttpError(400, "invalid_title", `title \u9700\u4E3A 1\u2013${SESSION_TITLE_MAX} \u4E2A\u5B57\u7B26\u3002`);
2765
+ throw new HttpError(
2766
+ 400,
2767
+ "invalid_title",
2768
+ `title must be 1\u2013${SESSION_TITLE_MAX} characters.`
2769
+ );
2696
2770
  }
2697
2771
  }
2698
2772
  if (approvalMode === void 0 && archived === void 0 && title === void 0) {
2699
- throw new HttpError(400, "no_update", "\u7F3A\u5C11\u53EF\u66F4\u65B0\u5B57\u6BB5\uFF08approvalMode / archived / title\uFF09\u3002");
2773
+ throw new HttpError(
2774
+ 400,
2775
+ "no_update",
2776
+ "No updatable field provided (approvalMode / archived / title)."
2777
+ );
2700
2778
  }
2701
2779
  let updated = { ...row };
2702
2780
  if (title !== void 0) {
@@ -2714,7 +2792,7 @@ function sessionsRoutes(deps2) {
2714
2792
  }
2715
2793
  const hasTrace = await deps2.sessionService.hasTrace(updated);
2716
2794
  return c.json({
2717
- session: deps2.sessionService.toInfo(updated, hasTrace)
2795
+ session: await deps2.sessionService.toInfo(updated, hasTrace)
2718
2796
  });
2719
2797
  });
2720
2798
  app2.delete("/:sessionId", async (c) => {
@@ -2733,6 +2811,7 @@ function sessionsRoutes(deps2) {
2733
2811
  { recursive: true, force: true }
2734
2812
  );
2735
2813
  deps2.sessionsRepo.deleteById(row.sessionId);
2814
+ deps2.sessionSources.delete(row.sessionId);
2736
2815
  } finally {
2737
2816
  deps2.manager.endSessionDeletion(row.sessionId);
2738
2817
  }
@@ -2742,7 +2821,7 @@ function sessionsRoutes(deps2) {
2742
2821
  const row = resolveSession(c);
2743
2822
  const fileName = c.req.param("fileName") ?? "";
2744
2823
  if (!/^[A-Za-z0-9._-]+$/.test(fileName) || fileName.includes("..")) {
2745
- throw new HttpError(404, "file_not_found", "\u6587\u4EF6\u4E0D\u5B58\u5728\u3002");
2824
+ throw new HttpError(404, "file_not_found", "File does not exist.");
2746
2825
  }
2747
2826
  const filePath = path6.join(
2748
2827
  scratchpadDir(deps2.config.root, row.projectId, row.agentId),
@@ -2753,7 +2832,7 @@ function sessionsRoutes(deps2) {
2753
2832
  try {
2754
2833
  bytes = await fs7.readFile(filePath);
2755
2834
  } catch {
2756
- throw new HttpError(404, "file_not_found", "\u6587\u4EF6\u4E0D\u5B58\u5728\u3002");
2835
+ throw new HttpError(404, "file_not_found", "File does not exist.");
2757
2836
  }
2758
2837
  const MIME_BY_EXT = {
2759
2838
  ".png": "image/png",
@@ -2802,7 +2881,11 @@ function sessionsRoutes(deps2) {
2802
2881
  const decision = requireEnum(body, "decision", ["allow", "deny"]);
2803
2882
  const ok = deps2.manager.decideApproval(row.sessionId, pathParam(c, "toolCallId"), decision);
2804
2883
  if (!ok) {
2805
- throw new HttpError(404, "approval_not_found", "\u8BE5\u5BA1\u6279\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u51B3\u5B9A\u3002");
2884
+ throw new HttpError(
2885
+ 404,
2886
+ "approval_not_found",
2887
+ "Approval does not exist or has already been decided."
2888
+ );
2806
2889
  }
2807
2890
  return c.body(null, 204);
2808
2891
  });
@@ -2825,18 +2908,22 @@ function sessionsRoutes(deps2) {
2825
2908
  const row = resolveSession(c);
2826
2909
  const rel = c.req.query("path") ?? "";
2827
2910
  const download = c.req.query("download") === "1";
2911
+ const preview = !download && c.req.query("preview") === "1";
2828
2912
  const { data, fileName, contentType, scriptable } = await deps2.workspaceFiles.read(
2829
2913
  row.workspace,
2830
2914
  rel
2831
2915
  );
2832
2916
  const disposition = download ? "attachment" : "inline";
2833
- const effectiveType = !download && scriptable ? "text/plain; charset=utf-8" : contentType;
2917
+ const effectiveType = !download && scriptable && !preview ? "text/plain; charset=utf-8" : contentType;
2834
2918
  return new Response(new Uint8Array(data), {
2835
2919
  status: 200,
2836
2920
  headers: {
2837
2921
  "Content-Type": effectiveType,
2838
2922
  "Content-Disposition": `${disposition}; filename*=UTF-8''${encodeURIComponent(fileName)}`,
2839
- "X-Content-Type-Options": "nosniff"
2923
+ "X-Content-Type-Options": "nosniff",
2924
+ ...preview && scriptable ? {
2925
+ "Content-Security-Policy": "sandbox allow-scripts allow-popups allow-modals allow-forms"
2926
+ } : {}
2840
2927
  }
2841
2928
  });
2842
2929
  });
@@ -2846,7 +2933,7 @@ function sessionsRoutes(deps2) {
2846
2933
  const paths = body.paths;
2847
2934
  if (!Array.isArray(paths) || paths.length > STAT_MAX_PATHS || !paths.every((p) => typeof p === "string" && p.length <= STAT_MAX_PATH_LEN)) {
2848
2935
  throw badRequest(
2849
- `paths \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4\uFF08\u2264${STAT_MAX_PATHS} \u9879\uFF0C\u6BCF\u9879 \u2264${STAT_MAX_PATH_LEN} \u5B57\u7B26\uFF09\u3002`
2936
+ `paths must be an array of strings (\u2264${STAT_MAX_PATHS} items, each \u2264${STAT_MAX_PATH_LEN} characters).`
2850
2937
  );
2851
2938
  }
2852
2939
  const existing = await deps2.workspaceFiles.statExisting(row.workspace, paths);
@@ -2857,11 +2944,11 @@ function sessionsRoutes(deps2) {
2857
2944
  const rel = c.req.query("path") ?? "";
2858
2945
  const body = await readJson(c);
2859
2946
  if (typeof body.dataBase64 !== "string") {
2860
- throw badRequest("dataBase64 \u5FC5\u987B\u662F base64 \u5B57\u7B26\u4E32\u3002");
2947
+ throw badRequest("dataBase64 must be a base64 string.");
2861
2948
  }
2862
2949
  const data = Buffer.from(body.dataBase64, "base64");
2863
2950
  if (data.length > MAX_UPLOAD_BYTES) {
2864
- throw new HttpError(413, "file_too_large", "\u4E0A\u4F20\u6587\u4EF6\u8D85\u8FC7 14MB \u4E0A\u9650\u3002");
2951
+ throw new HttpError(413, "file_too_large", "Uploaded file exceeds the 14MB limit.");
2865
2952
  }
2866
2953
  await deps2.workspaceFiles.write(row.workspace, rel, data);
2867
2954
  return c.body(null, 204);
@@ -2898,8 +2985,8 @@ function sessionsRoutes(deps2) {
2898
2985
 
2899
2986
  // src/runtime/channel.ts
2900
2987
  import { randomUUID } from "crypto";
2901
- var DEFAULT_MAX_COUNT = 1e3;
2902
- var DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
2988
+ var DEFAULT_MAX_COUNT = 1e4;
2989
+ var DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
2903
2990
  var Channel = class {
2904
2991
  /** Channel epoch: generated at instance creation, prefixed onto event ids (necessarily changes after recycle/recreate or restart). */
2905
2992
  epoch = randomUUID().slice(0, 8);
@@ -3163,18 +3250,42 @@ function makeApprove(args) {
3163
3250
  };
3164
3251
  }
3165
3252
 
3253
+ // src/runtime/session-sources.ts
3254
+ function asSessionSource(v) {
3255
+ return v === "schedule" || v === "subagent" ? v : void 0;
3256
+ }
3257
+ var SessionSources = class {
3258
+ map = /* @__PURE__ */ new Map();
3259
+ /** Records a Session's origin as read from session_meta (`null` = meta seen, user-created). */
3260
+ set(sessionId, source) {
3261
+ this.map.set(sessionId, source);
3262
+ }
3263
+ /** Known origin, `null` for a known user-created Session, `undefined` when this process has not seen the meta. */
3264
+ get(sessionId) {
3265
+ return this.map.get(sessionId);
3266
+ }
3267
+ /** Drops a deleted Session's entry (bulk Agent/Project deletion may leave stale entries; they are never matched again). */
3268
+ delete(sessionId) {
3269
+ this.map.delete(sessionId);
3270
+ }
3271
+ };
3272
+
3166
3273
  // src/runtime/stream-error-watcher.ts
3167
3274
  import { isEventMessage, isModelMessage, isSessionMeta } from "@prismshadow/penguin-core";
3168
3275
  import path7 from "path";
3169
3276
  var TOOL_NAMES_MAX = 1e3;
3170
3277
  var ORIGIN_CTX_MAX = 200;
3171
3278
  var LLM_FAILURES = {
3172
- failed: { code: "llm_failed", kind: "unexpected", text: "LLM \u8BF7\u6C42\u5931\u8D25\uFF08\u4E0D\u53EF\u91CD\u8BD5\uFF09\u3002" },
3173
- timeout: { code: "llm_timeout", kind: "expected", text: "LLM \u8BF7\u6C42\u8D85\u65F6\uFF08\u5F15\u64CE\u91CD\u8FDE\u91CD\u8BD5\uFF09\u3002" },
3279
+ failed: { code: "llm_failed", kind: "unexpected", text: "LLM request failed (not retryable)." },
3280
+ timeout: {
3281
+ code: "llm_timeout",
3282
+ kind: "expected",
3283
+ text: "LLM request timed out (the engine reconnects and retries)."
3284
+ },
3174
3285
  malformed: {
3175
3286
  code: "llm_malformed",
3176
3287
  kind: "expected",
3177
- text: "LLM \u54CD\u5E94\u65E0\u6CD5\u89E3\u6790\uFF08\u5F15\u64CE\u91CD\u8FDE\u91CD\u8BD5\uFF09\u3002"
3288
+ text: "LLM response could not be parsed (the engine reconnects and retries)."
3178
3289
  }
3179
3290
  };
3180
3291
  function isLlmFailure(s) {
@@ -3191,7 +3302,7 @@ function originKey(msg) {
3191
3302
  return origin && origin.length > 0 ? origin[origin.length - 1] : "";
3192
3303
  }
3193
3304
  function toolFailureText(output) {
3194
- if (!output) return "\u5DE5\u5177\u6267\u884C\u5931\u8D25\uFF08\u65E0\u8F93\u51FA\uFF09\u3002";
3305
+ if (!output) return "Tool execution failed (no output).";
3195
3306
  if (output.length <= MESSAGE_MAX) return output;
3196
3307
  return `\u2026${output.slice(output.length - (MESSAGE_MAX - 1))}`;
3197
3308
  }
@@ -3331,13 +3442,13 @@ var StreamErrorWatcher = class {
3331
3442
  // src/runtime/session-manager.ts
3332
3443
  function compactUnavailable(why) {
3333
3444
  const messages = {
3334
- unsupported: "\u8BE5 Agent \u672A\u914D\u7F6E\u4E0A\u4E0B\u6587\u538B\u7F29\u80FD\u529B\u3002",
3335
- empty: "\u5F53\u524D\u4E0A\u4E0B\u6587\u6CA1\u6709\u53EF\u538B\u7F29\u7684\u5185\u5BB9\uFF08\u5C1A\u65E0\u5DF2\u5B8C\u6210\u7684\u5BF9\u8BDD\u8F6E\u6B21\uFF09\u3002",
3336
- just_compacted: "\u4E0A\u4E0B\u6587\u521A\u538B\u7F29\u8FC7\uFF0C\u6B64\u540E\u8FD8\u6CA1\u6709\u65B0\u7684\u5BF9\u8BDD\uFF0C\u65E0\u9700\u518D\u6B21\u538B\u7F29\u3002"
3445
+ unsupported: "This Agent does not have context compaction configured.",
3446
+ empty: "The current context has nothing to compact (no completed conversation turns yet).",
3447
+ just_compacted: "The context was just compacted and there is no new conversation since; no need to compact again."
3337
3448
  };
3338
3449
  return new HttpError(409, "nothing_to_compact", messages[why]);
3339
3450
  }
3340
- function createCoreSessionLoader(root) {
3451
+ function createCoreSessionLoader(root, sources) {
3341
3452
  return {
3342
3453
  async load(row) {
3343
3454
  const agent = await createAgent({
@@ -3364,14 +3475,17 @@ function createCoreSessionLoader(root) {
3364
3475
  throw new HttpError(
3365
3476
  409,
3366
3477
  "workspace_missing",
3367
- `\u8BE5 Session \u7684 Workspace \u5DF2\u4E0D\u5B58\u5728\uFF1A${row.workspace}\uFF0C\u65E0\u6CD5\u7EE7\u7EED\u3002\u8BF7\u65B0\u5EFA Session\u3002`
3478
+ `This Session's Workspace no longer exists: ${row.workspace}, so it cannot continue. Create a new Session.`
3368
3479
  );
3369
3480
  }
3481
+ const knownSource = sources?.get(row.sessionId);
3370
3482
  try {
3371
3483
  return await agent.createSession({
3372
3484
  workspaceDir: row.workspace,
3373
3485
  modelId: row.modelId,
3374
- provider: row.provider
3486
+ provider: row.provider,
3487
+ // The rebuilt Session re-records a known origin in its fresh session_meta.
3488
+ ...knownSource != null ? { source: knownSource } : {}
3375
3489
  });
3376
3490
  } catch (err) {
3377
3491
  if (isMissingCredential(err)) throw modelCredentialMissing(row.modelId);
@@ -3391,6 +3505,7 @@ function toUnrecoverableError(err) {
3391
3505
  var ENTRY_IDLE_MS = 30 * 60 * 1e3;
3392
3506
  var ENTRY_SWEEP_INTERVAL_MS = 60 * 1e3;
3393
3507
  var TITLE_EXCERPT_LIMIT = 4e3;
3508
+ var EARLY_TITLE_BODY_CHARS = 1e3;
3394
3509
  function agentKey(projectId, agentId) {
3395
3510
  return `${projectId}\0${agentId}`;
3396
3511
  }
@@ -3447,6 +3562,8 @@ var SessionManager = class {
3447
3562
  deletingAgents = /* @__PURE__ */ new Set();
3448
3563
  /** Sessions currently being deleted (guards against the entry/Trace file being rebuilt and reviving it inside the deletion race window). */
3449
3564
  deletingSessions = /* @__PURE__ */ new Set();
3565
+ /** Per-Agent config generation (key = agentKey), bumped by invalidateAgentRuntimes on vault updates. */
3566
+ agentGenerations = /* @__PURE__ */ new Map();
3450
3567
  sweepTimer;
3451
3568
  // —— Query surface (used by Session listing / Agent active-count / SSE subscription replay) ——
3452
3569
  statusOf(sessionId) {
@@ -3479,9 +3596,23 @@ var SessionManager = class {
3479
3596
  approvals: new ApprovalRegistry(),
3480
3597
  abort: null,
3481
3598
  running: null,
3599
+ generation: this.generationOf(row.projectId, row.agentId),
3482
3600
  lastActivityMs: Date.now()
3483
3601
  });
3484
3602
  }
3603
+ /**
3604
+ * After an Agent's vault is updated: bump the Agent's config generation so every
3605
+ * runtime built before the update is discarded on its next idle access and
3606
+ * re-resumed via the loader — resume re-reads agent_state/.vault.toml, so the next
3607
+ * Task on any of this Agent's Sessions runs with the new values (history is
3608
+ * preserved through the Trace). A Task already in flight is neither aborted nor
3609
+ * hot-swapped: it keeps the values it started with, and its entry is rebuilt on
3610
+ * the first access after it returns to idle (see ensureEntry).
3611
+ */
3612
+ invalidateAgentRuntimes(projectId, agentId) {
3613
+ const key = agentKey(projectId, agentId);
3614
+ this.agentGenerations.set(key, this.generationOf(projectId, agentId) + 1);
3615
+ }
3485
3616
  // —— Task / compaction drive ——
3486
3617
  /**
3487
3618
  * Start a Task: get-or-load → 409
@@ -3657,38 +3788,70 @@ var SessionManager = class {
3657
3788
  // —— Internal ——
3658
3789
  assertOpen() {
3659
3790
  if (this.closed) {
3660
- throw new HttpError(503, "shutting_down", "\u670D\u52A1\u7AEF\u6B63\u5728\u5173\u505C\uFF0C\u6682\u4E0D\u63A5\u53D7\u65B0\u4EFB\u52A1\u3002");
3791
+ throw new HttpError(
3792
+ 503,
3793
+ "shutting_down",
3794
+ "Server is shutting down; not accepting new Tasks."
3795
+ );
3661
3796
  }
3662
3797
  }
3663
3798
  /** The Agent owning this Session is being deleted → 409 (guards against directory recreation inside the deletion race window). */
3664
3799
  assertAgentNotDeleting(sessionId) {
3665
3800
  const row = this.deps.sessions.findById(sessionId);
3666
3801
  if (row && this.deletingAgents.has(agentKey(row.projectId, row.agentId))) {
3667
- throw new HttpError(409, "agent_deleting", "\u8BE5 Agent \u6B63\u5728\u5220\u9664\uFF0C\u6682\u4E0D\u63A5\u53D7\u65B0\u4EFB\u52A1\u3002");
3802
+ throw new HttpError(
3803
+ 409,
3804
+ "agent_deleting",
3805
+ "This Agent is being deleted; not accepting new Tasks."
3806
+ );
3668
3807
  }
3669
3808
  }
3670
3809
  /** This Session is being deleted → 409 (guards against the entry/Trace being rebuilt and reviving it inside the deletion race window). */
3671
3810
  assertSessionNotDeleting(sessionId) {
3672
3811
  if (this.deletingSessions.has(sessionId)) {
3673
- throw new HttpError(409, "session_deleting", "\u8BE5 Session \u6B63\u5728\u5220\u9664\uFF0C\u6682\u4E0D\u63A5\u53D7\u65B0\u4EFB\u52A1\u3002");
3812
+ throw new HttpError(
3813
+ 409,
3814
+ "session_deleting",
3815
+ "This Session is being deleted; not accepting new Tasks."
3816
+ );
3674
3817
  }
3675
3818
  }
3676
3819
  assertIdle(entry) {
3677
3820
  if (entry.status === "running") {
3678
- throw new HttpError(409, "task_in_progress", "\u8BE5 Session \u5DF2\u6709\u8FDB\u884C\u4E2D\u7684 Task\u3002");
3821
+ throw new HttpError(409, "task_in_progress", "This Session already has a Task in progress.");
3679
3822
  }
3680
3823
  if (entry.status === "compacting") {
3681
- throw new HttpError(409, "compacting", "\u8BE5 Session \u6B63\u5728\u538B\u7F29\u4E0A\u4E0B\u6587\uFF0C\u6682\u4E0D\u63A5\u53D7\u65B0\u8F93\u5165\u3002");
3824
+ throw new HttpError(
3825
+ 409,
3826
+ "compacting",
3827
+ "This Session is compacting its context; not accepting new input."
3828
+ );
3682
3829
  }
3683
3830
  }
3831
+ generationOf(projectId, agentId) {
3832
+ return this.agentGenerations.get(agentKey(projectId, agentId)) ?? 0;
3833
+ }
3684
3834
  /** get-or-resume-or-heal: use directly on an active-table hit; otherwise load via the loader, updating the index's primary key on self-heal. */
3685
3835
  async ensureEntry(sessionId) {
3686
3836
  const existing = this.entries.get(sessionId);
3687
- if (existing) return existing;
3837
+ if (existing) {
3838
+ if (existing.generation === this.generationOf(existing.projectId, existing.agentId)) {
3839
+ return existing;
3840
+ }
3841
+ if (existing.status !== "idle" || existing.running !== null || existing.approvals.size !== 0) {
3842
+ return existing;
3843
+ }
3844
+ this.entries.delete(sessionId);
3845
+ }
3688
3846
  const row = this.deps.sessions.findById(sessionId);
3689
3847
  if (!row) {
3690
- throw new HttpError(404, "session_not_found", "Session \u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE\u3002");
3848
+ throw new HttpError(
3849
+ 404,
3850
+ "session_not_found",
3851
+ "Session does not exist or you do not have access."
3852
+ );
3691
3853
  }
3854
+ const generation = this.generationOf(row.projectId, row.agentId);
3692
3855
  const session = await this.deps.loader.load(row);
3693
3856
  this.assertSessionNotDeleting(row.sessionId);
3694
3857
  this.assertAgentNotDeleting(row.sessionId);
@@ -3708,6 +3871,7 @@ var SessionManager = class {
3708
3871
  approvals: new ApprovalRegistry(),
3709
3872
  abort: null,
3710
3873
  running: null,
3874
+ generation,
3711
3875
  lastActivityMs: Date.now()
3712
3876
  };
3713
3877
  this.entries.set(currentId, entry);
@@ -3720,6 +3884,8 @@ var SessionManager = class {
3720
3884
  * a title): it collects model text for automatic title generation.
3721
3885
  */
3722
3886
  async drive(entry, gen, titleSource) {
3887
+ let earlyTitleFired = false;
3888
+ let mainBodyChars = 0;
3723
3889
  const ctx = {
3724
3890
  projectId: entry.projectId,
3725
3891
  agentId: entry.agentId,
@@ -3755,7 +3921,7 @@ var SessionManager = class {
3755
3921
  }
3756
3922
  } catch (err) {
3757
3923
  this.log(
3758
- `[subagent] \u5B50\u4F1A\u8BDD\u767B\u8BB0\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`
3924
+ `[subagent] Failed to register child session: ${err instanceof Error ? err.message : String(err)}`
3759
3925
  );
3760
3926
  this.deps.errors?.record({
3761
3927
  source: "subagent",
@@ -3771,18 +3937,30 @@ var SessionManager = class {
3771
3937
  child.assistantExcerpt += (child.assistantExcerpt ? "\n" : "") + nested.text;
3772
3938
  }
3773
3939
  }
3940
+ if (!earlyTitleFired && titleSource?.userExcerpt.trim()) {
3941
+ const p = msg.payload;
3942
+ if ((!msg.origin || msg.origin.length === 0) && msg.type === "model_msg" && p.type === "text" && p.role === "assistant" && p.text) {
3943
+ mainBodyChars += p.text.length;
3944
+ if (mainBodyChars >= EARLY_TITLE_BODY_CHARS) {
3945
+ earlyTitleFired = true;
3946
+ this.deps.titles?.maybeGenerate(ctx, entry.session, {
3947
+ fallbackText: titleSource.userExcerpt
3948
+ });
3949
+ }
3950
+ }
3951
+ }
3774
3952
  this.deps.channels.get(entry.sessionId).publish(msg);
3775
3953
  watcher?.observe(msg);
3776
3954
  try {
3777
3955
  await this.deps.recorder.record(ctx, msg);
3778
3956
  } catch (err) {
3779
- this.log(`[usage] \u843D\u5E93\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
3957
+ this.log(`[usage] Insert failed: ${err instanceof Error ? err.message : String(err)}`);
3780
3958
  this.deps.errors?.record({ source: "usage", err, ctx, code: "usage_insert_failed" });
3781
3959
  }
3782
3960
  }
3783
3961
  } catch (err) {
3784
3962
  this.log(
3785
- `[session] \u8FD0\u884C\u5F02\u5E38: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
3963
+ `[session] Run failed: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
3786
3964
  );
3787
3965
  this.deps.errors?.record({ source: "session", err, ctx, code: "session_run_failed" });
3788
3966
  } finally {
@@ -3835,6 +4013,8 @@ var SessionManager = class {
3835
4013
  const p = msg.payload;
3836
4014
  const agentId = path8.basename(path8.dirname(p.agent_state));
3837
4015
  if (!agentId || agentId === "." || agentId === "..") return null;
4016
+ const source = asSessionSource(p.source) ?? "subagent";
4017
+ this.deps.sources.set(childSid, source);
3838
4018
  this.deps.sessions.insertOrIgnore({
3839
4019
  sessionId: childSid,
3840
4020
  projectId: entry.projectId,
@@ -3846,7 +4026,6 @@ var SessionManager = class {
3846
4026
  // inserted with defaults (matches the convention for Sessions discovered by the CLI).
3847
4027
  approvalMode: "allow-all",
3848
4028
  title: null,
3849
- source: "subagent",
3850
4029
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3851
4030
  });
3852
4031
  this.publishEvent(entry, {
@@ -3854,7 +4033,7 @@ var SessionManager = class {
3854
4033
  projectId: entry.projectId,
3855
4034
  agentId,
3856
4035
  sessionId: childSid,
3857
- source: "subagent"
4036
+ source
3858
4037
  });
3859
4038
  const child = {
3860
4039
  sessionId: childSid,
@@ -3913,7 +4092,7 @@ var Scheduler = class {
3913
4092
  now;
3914
4093
  intervalMs;
3915
4094
  timer = null;
3916
- /** key = `${projectId}${agentId}${name}` */
4095
+ /** key = `${projectId}\0${agentId}\0${name}` */
3917
4096
  pending = /* @__PURE__ */ new Map();
3918
4097
  ticking = false;
3919
4098
  /** Start: run one reconcile immediately (startup semantics: no backfill), then enter the periodic tick. */
@@ -4010,7 +4189,7 @@ var Scheduler = class {
4010
4189
  if (!file.parsed.ok) {
4011
4190
  this.deps.errors.record({
4012
4191
  source: "schedule",
4013
- err: new Error(`\u5B9A\u65F6\u4EFB\u52A1\u6587\u4EF6\u975E\u6CD5\uFF1A${file.name}.toml\u2014\u2014${file.parsed.error}`),
4192
+ err: new Error(`Invalid schedule file: ${file.name}.toml \u2014 ${file.parsed.error}`),
4014
4193
  code: "schedule_invalid_file",
4015
4194
  ctx: { projectId, agentId }
4016
4195
  });
@@ -4021,7 +4200,7 @@ var Scheduler = class {
4021
4200
  if (refError !== null) {
4022
4201
  this.deps.errors.record({
4023
4202
  source: "schedule",
4024
- err: new Error(`\u5B9A\u65F6\u4EFB\u52A1\u6587\u4EF6\u975E\u6CD5\uFF1A${file.name}.toml\u2014\u2014${refError}`),
4203
+ err: new Error(`Invalid schedule file: ${file.name}.toml \u2014 ${refError}`),
4025
4204
  code: "schedule_invalid_file",
4026
4205
  ctx: { projectId, agentId }
4027
4206
  });
@@ -4081,7 +4260,9 @@ var Scheduler = class {
4081
4260
  if (!row || row.projectId !== projectId || row.agentId !== agentId) {
4082
4261
  this.deps.errors.record({
4083
4262
  source: "schedule",
4084
- err: new Error(`\u5B9A\u65F6\u4EFB\u52A1 ${def.name} \u7ED1\u5B9A\u7684 Session \u4E0D\u5B58\u5728\uFF1A${def.sessionId}`),
4263
+ err: new Error(
4264
+ `Schedule ${def.name} is bound to a Session that does not exist: ${def.sessionId}`
4265
+ ),
4085
4266
  code: "schedule_session_missing",
4086
4267
  ctx: { projectId, agentId, sessionId: def.sessionId }
4087
4268
  });
@@ -4179,7 +4360,12 @@ var Scheduler = class {
4179
4360
  };
4180
4361
 
4181
4362
  // src/runtime/title-generator.ts
4182
- import { emptyTokenCounts, sanitizeTitle, tokenUsage } from "@prismshadow/penguin-core";
4363
+ import {
4364
+ emptyTokenCounts,
4365
+ sanitizeTitle,
4366
+ stripConversationMarkers,
4367
+ tokenUsage
4368
+ } from "@prismshadow/penguin-core";
4183
4369
  var TitleGenerator = class {
4184
4370
  constructor(deps2) {
4185
4371
  this.deps = deps2;
@@ -4195,7 +4381,7 @@ var TitleGenerator = class {
4195
4381
  if (this.inflight.has(ctx.sessionId)) return;
4196
4382
  this.inflight.add(ctx.sessionId);
4197
4383
  void this.generate(ctx, session, req).catch((err) => {
4198
- this.log(`[title] \u751F\u6210\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
4384
+ this.log(`[title] Generation failed: ${err instanceof Error ? err.message : String(err)}`);
4199
4385
  this.deps.errors?.record({ source: "title", err, ctx, code: "title_failed" });
4200
4386
  }).finally(() => {
4201
4387
  this.inflight.delete(ctx.sessionId);
@@ -4212,7 +4398,9 @@ var TitleGenerator = class {
4212
4398
  try {
4213
4399
  await this.deps.recorder.record(ctx, tokenUsage(emptyTokenCounts(), res.usage));
4214
4400
  } catch (err) {
4215
- this.log(`[title] \u7528\u91CF\u843D\u5E93\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
4401
+ this.log(
4402
+ `[title] Usage insert failed: ${err instanceof Error ? err.message : String(err)}`
4403
+ );
4216
4404
  this.deps.errors?.record({
4217
4405
  source: "title",
4218
4406
  err,
@@ -4222,7 +4410,7 @@ var TitleGenerator = class {
4222
4410
  }
4223
4411
  }
4224
4412
  } catch (err) {
4225
- this.log(`[title] \u6A21\u578B\u8BF7\u6C42\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
4413
+ this.log(`[title] Model request failed: ${err instanceof Error ? err.message : String(err)}`);
4226
4414
  this.deps.errors?.record({ source: "title", err, ctx, code: "title_llm_failed" });
4227
4415
  }
4228
4416
  const finalTitle = title ?? fallbackTitle(req.fallbackText);
@@ -4237,7 +4425,7 @@ var TitleGenerator = class {
4237
4425
  }
4238
4426
  };
4239
4427
  function fallbackTitle(text) {
4240
- const firstLine = text.split("\n").find((l) => l.trim().length > 0);
4428
+ const firstLine = stripConversationMarkers(text).split("\n").find((l) => l.trim().length > 0);
4241
4429
  if (!firstLine) return null;
4242
4430
  return sanitizeTitle(firstLine) ?? firstLine.trim().slice(0, 30);
4243
4431
  }
@@ -4317,7 +4505,7 @@ var SEMANTIC_ID_PATTERN = /^[a-z][a-z0-9_]{1,63}$/;
4317
4505
  var USERNAME_PATTERN = /^[a-z][a-z0-9_]{1,31}$/;
4318
4506
  var PROJECT_SUFFIX_PATTERN = /^[a-z0-9_]+$/;
4319
4507
  var PROJECT_ID_MAX_LENGTH = 64;
4320
- var SEMANTIC_ID_RULE = "\u5C0F\u5199\u5B57\u6BCD\u5F00\u5934\uFF0C\u4EC5\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u4E0E\u4E0B\u5212\u7EBF";
4508
+ var SEMANTIC_ID_RULE = "starts with a lowercase letter; lowercase letters, digits and underscores only";
4321
4509
 
4322
4510
  // src/services/admin-service.ts
4323
4511
  var AdminService = class {
@@ -4332,13 +4520,17 @@ var AdminService = class {
4332
4520
  }
4333
4521
  async createUser(userId, password) {
4334
4522
  if (!USERNAME_PATTERN.test(userId)) {
4335
- throw new HttpError(400, "invalid_user_id", `\u7528\u6237\u540D\u987B\u4E3A 2~32 \u4F4D\uFF1A${SEMANTIC_ID_RULE}\u3002`);
4523
+ throw new HttpError(
4524
+ 400,
4525
+ "invalid_user_id",
4526
+ `Username must be 2\u201332 characters: ${SEMANTIC_ID_RULE}.`
4527
+ );
4336
4528
  }
4337
4529
  if (password.length < MIN_PASSWORD_LENGTH) {
4338
- throw new HttpError(400, "invalid_password", "\u5BC6\u7801\u81F3\u5C11 8 \u4E2A\u5B57\u7B26\u3002");
4530
+ throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
4339
4531
  }
4340
4532
  if (this.deps.users.findById(userId)) {
4341
- throw new HttpError(409, "user_exists", `\u7528\u6237\u5DF2\u5B58\u5728\uFF1A${userId}\u3002`);
4533
+ throw new HttpError(409, "user_exists", `User already exists: ${userId}.`);
4342
4534
  }
4343
4535
  const user = {
4344
4536
  userId,
@@ -4359,10 +4551,10 @@ var AdminService = class {
4359
4551
  /** Reset another user's password: flags it as initial and clears all their sessions (prompts a password change on next login). */
4360
4552
  async resetPassword(userId, password) {
4361
4553
  if (!this.deps.users.findById(userId)) {
4362
- throw new HttpError(404, "user_not_found", `\u7528\u6237\u4E0D\u5B58\u5728\uFF1A${userId}\u3002`);
4554
+ throw new HttpError(404, "user_not_found", `User does not exist: ${userId}.`);
4363
4555
  }
4364
4556
  if (password.length < MIN_PASSWORD_LENGTH) {
4365
- throw new HttpError(400, "invalid_password", "\u5BC6\u7801\u81F3\u5C11 8 \u4E2A\u5B57\u7B26\u3002");
4557
+ throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
4366
4558
  }
4367
4559
  this.deps.users.updatePassword(userId, await hashPassword(password), true);
4368
4560
  this.deps.authSessions.deleteByUser(userId);
@@ -4371,10 +4563,10 @@ var AdminService = class {
4371
4563
  async deleteUser(userId) {
4372
4564
  const target = this.deps.users.findById(userId);
4373
4565
  if (!target) {
4374
- throw new HttpError(404, "user_not_found", `\u7528\u6237\u4E0D\u5B58\u5728\uFF1A${userId}\u3002`);
4566
+ throw new HttpError(404, "user_not_found", `User does not exist: ${userId}.`);
4375
4567
  }
4376
4568
  if (target.isAdmin) {
4377
- throw new HttpError(409, "cannot_delete_admin", "\u5185\u7F6E\u7BA1\u7406\u5458\u4E0D\u80FD\u5220\u9664\u3002");
4569
+ throw new HttpError(409, "cannot_delete_admin", "The built-in admin cannot be deleted.");
4378
4570
  }
4379
4571
  for (const project of this.deps.projects.listByOwner(userId)) {
4380
4572
  await this.deps.projectService.destroyProject(project.projectId);
@@ -4441,6 +4633,8 @@ function refKey(provider, modelId) {
4441
4633
  function showRef(provider, modelId) {
4442
4634
  return `(provider=${provider}, model_id=${modelId})`;
4443
4635
  }
4636
+ var PROBE_PROMPT = 'ping - reply with the single word "pong" and nothing else. Do not think or explain.\n<think></think>';
4637
+ var SPEED_PROBE_PROMPT = "Count from 1 to 50 as a comma-separated list, and nothing else. Do not think or explain.\n<think></think>";
4444
4638
  var ProjectConfigService = class {
4445
4639
  constructor(root) {
4446
4640
  this.root = root;
@@ -4537,12 +4731,19 @@ var ProjectConfigService = class {
4537
4731
  * as a pair in the request body; sends one minimal request using that model's
4538
4732
  * config (optionally overridden with an unsaved apiKey / baseUrl) — no tools, no
4539
4733
  * system prompt, thinking disabled, a tiny output cap, 20s timeout — just to see
4540
- * whether it completes normally. The model id sent to AgentHub is `modelId`
4734
+ * whether the endpoint answers. The model id sent to AgentHub is `modelId`
4541
4735
  * itself (the upstream id verbatim; client_type inference follows it).
4542
4736
  *
4737
+ * A reasoning-heavy model may ignore the disabled thinking level and burn the
4738
+ * whole tiny output cap on thinking (finish_reason=length with no text — AgentHub
4739
+ * raises EmptyResponseError, collapsed to a malformed outcome): the endpoint
4740
+ * demonstrably streamed model output, which is everything a connectivity test
4741
+ * proves, so that case counts as ok too (see probeVerdict).
4742
+ *
4543
4743
  * Never throws: the LLM layer collapses auth/parameter/network errors into an
4544
4744
  * `LLMOutcome`, which is translated here into ok / message. Consumes very few
4545
- * Tokens (single-digit output), and writes no Trace and records no usage.
4745
+ * Tokens (single-digit output; speed mode spends up to its 64-token cap to have a
4746
+ * window worth timing), and writes no Trace and records no usage.
4546
4747
  */
4547
4748
  async testModel(projectId, req) {
4548
4749
  const raw = await this.readRaw(projectId);
@@ -4561,18 +4762,39 @@ var ProjectConfigService = class {
4561
4762
  ...clientType ? { clientType } : {},
4562
4763
  tools: [],
4563
4764
  thinkingLevel: "none",
4564
- maxTokens: 16,
4765
+ // Speed mode pairs a raised cap with a prompt that keeps generating (see
4766
+ // SPEED_PROBE_PROMPT), so the stream lasts long enough for TTFT/TPS to describe
4767
+ // decoding rather than one round trip; the plain connectivity test keeps the
4768
+ // single-digit-token budget.
4769
+ maxTokens: req.speed ? 64 : 16,
4565
4770
  requestTimeoutMs: 2e4
4566
4771
  });
4567
- const gen = llm.streamGenerate({ newMessages: [userText3("ping")] });
4772
+ const gen = llm.streamGenerate({
4773
+ newMessages: [userText3(req.speed ? SPEED_PROBE_PROMPT : PROBE_PROMPT)]
4774
+ });
4775
+ let sawContent = false;
4776
+ let firstContentAt = null;
4777
+ let outputTokens = 0;
4568
4778
  for (; ; ) {
4569
4779
  const step = await gen.next();
4570
4780
  if (step.done) {
4571
- const outcome = step.value;
4572
- if (outcome.status === "completed")
4573
- return { ok: true, latencyMs: Date.now() - startedAt };
4574
- const detail = "message" in outcome && outcome.message ? outcome.message : outcome.status;
4575
- return { ok: false, message: String(detail).slice(0, 300) };
4781
+ const verdict = probeVerdict(step.value, sawContent);
4782
+ if (!verdict.ok) return verdict;
4783
+ const res = { ok: true, latencyMs: Date.now() - startedAt };
4784
+ if (firstContentAt !== null) {
4785
+ res.ttftMs = firstContentAt - startedAt;
4786
+ const tps = probeTps(outputTokens, Date.now() - firstContentAt);
4787
+ if (tps !== void 0) res.tps = tps;
4788
+ }
4789
+ return res;
4790
+ }
4791
+ if (isProbeContent(step.value)) {
4792
+ sawContent = true;
4793
+ if (firstContentAt === null) firstContentAt = Date.now();
4794
+ }
4795
+ const p = step.value.payload;
4796
+ if (p.type === "token_usage" && typeof p.request?.output === "number") {
4797
+ outputTokens = p.request.output;
4576
4798
  }
4577
4799
  }
4578
4800
  } catch (err) {
@@ -4608,6 +4830,7 @@ var ProjectConfigService = class {
4608
4830
  const cat = catalogEntryFor(provider, modelId);
4609
4831
  const envKey = resolveModelEnv(modelId, clientType)?.envKey;
4610
4832
  const vision = typeof m.vision === "boolean" ? m.vision : cat?.supportsVision;
4833
+ const maxTokens = optNum(m.max_tokens);
4611
4834
  const displayName = optStr(m.display_name) ?? cat?.displayName;
4612
4835
  const apiKey = optStr(m.api_key);
4613
4836
  const credBaseUrl = optStr(m.base_url);
@@ -4620,6 +4843,7 @@ var ProjectConfigService = class {
4620
4843
  ...optNum(m.context_window) !== void 0 ? { contextWindow: optNum(m.context_window) } : {},
4621
4844
  ...clientType ? { clientType } : {},
4622
4845
  ...vision !== void 0 ? { vision } : {},
4846
+ ...maxTokens !== void 0 ? { maxTokens } : {},
4623
4847
  ...envKey ? { envKey } : {},
4624
4848
  ...pricingDto ? { pricing: pricingDto } : {},
4625
4849
  ...apiKey !== void 0 || credBaseUrl !== void 0 ? {
@@ -4662,7 +4886,7 @@ var ProjectConfigService = class {
4662
4886
  const key = refKey(entry.provider, entry.modelId);
4663
4887
  if (seen.has(key)) {
4664
4888
  throw badRequest(
4665
- `models \u4E2D\u5B58\u5728\u91CD\u590D\u7684\u6A21\u578B\u5F15\u7528\uFF1A${showRef(entry.provider, entry.modelId)}\u3002`
4889
+ `models contains a duplicate model reference: ${showRef(entry.provider, entry.modelId)}.`
4666
4890
  );
4667
4891
  }
4668
4892
  seen.add(key);
@@ -4678,6 +4902,7 @@ var ProjectConfigService = class {
4678
4902
  delete next2.context_window;
4679
4903
  delete next2.client_type;
4680
4904
  delete next2.vision;
4905
+ delete next2.max_tokens;
4681
4906
  delete next2.pricing;
4682
4907
  delete next2.display_name;
4683
4908
  delete next2.request_model_id;
@@ -4688,6 +4913,7 @@ var ProjectConfigService = class {
4688
4913
  if (entry.contextWindow !== void 0) next2.context_window = entry.contextWindow;
4689
4914
  if (entry.clientType) next2.client_type = entry.clientType;
4690
4915
  if (entry.vision !== void 0) next2.vision = entry.vision;
4916
+ if (entry.maxTokens !== void 0) next2.max_tokens = entry.maxTokens;
4691
4917
  if (entry.pricing !== void 0) {
4692
4918
  next2.pricing = {
4693
4919
  unit: "usd_per_mtok",
@@ -4712,7 +4938,7 @@ var ProjectConfigService = class {
4712
4938
  if (req.defaultModel !== void 0) {
4713
4939
  if (!seen.has(refKey(req.defaultModel.provider, req.defaultModel.modelId))) {
4714
4940
  throw badRequest(
4715
- `defaultModel \u5FC5\u987B\u5305\u542B\u5728 models \u5185\uFF1A${showRef(req.defaultModel.provider, req.defaultModel.modelId)}\u3002`
4941
+ `defaultModel must be included in models: ${showRef(req.defaultModel.provider, req.defaultModel.modelId)}.`
4716
4942
  );
4717
4943
  }
4718
4944
  defaultModel = req.defaultModel;
@@ -4732,12 +4958,12 @@ var ProjectConfigService = class {
4732
4958
  if (req.visionModel !== void 0) {
4733
4959
  if (!seen.has(refKey(req.visionModel.provider, req.visionModel.modelId))) {
4734
4960
  throw badRequest(
4735
- `visionModel \u5FC5\u987B\u5305\u542B\u5728 models \u5185\uFF1A${showRef(req.visionModel.provider, req.visionModel.modelId)}\u3002`
4961
+ `visionModel must be included in models: ${showRef(req.visionModel.provider, req.visionModel.modelId)}.`
4736
4962
  );
4737
4963
  }
4738
4964
  if (targetOf(req.visionModel)?.vision === false) {
4739
4965
  throw badRequest(
4740
- `visionModel \u4E0D\u80FD\u6307\u5411\u6807\u6CE8\u4E3A\u4E0D\u652F\u6301\u56FE\u7247\u7684\u6A21\u578B\uFF1A${showRef(req.visionModel.provider, req.visionModel.modelId)}\u3002`
4966
+ `visionModel must not point to a model annotated as not supporting images: ${showRef(req.visionModel.provider, req.visionModel.modelId)}.`
4741
4967
  );
4742
4968
  }
4743
4969
  visionModel = req.visionModel;
@@ -4767,6 +4993,24 @@ var ProjectConfigService = class {
4767
4993
  return this.getModels(projectId);
4768
4994
  }
4769
4995
  };
4996
+ function isProbeContent(msg) {
4997
+ const p = msg.payload;
4998
+ if (p.type === "partial_thinking" || p.type === "thinking") return Boolean(p.thinking);
4999
+ if (p.type === "partial_text" || p.type === "text") return Boolean(p.text);
5000
+ return false;
5001
+ }
5002
+ function probeVerdict(outcome, sawContent) {
5003
+ if (outcome.status === "completed") return { ok: true };
5004
+ if (outcome.status === "malformed" && sawContent) return { ok: true };
5005
+ const detail = "message" in outcome && outcome.message ? outcome.message : outcome.status;
5006
+ return { ok: false, message: String(detail).slice(0, 300) };
5007
+ }
5008
+ var PROBE_TPS_MIN_TOKENS = 16;
5009
+ var PROBE_TPS_MIN_WINDOW_MS = 100;
5010
+ function probeTps(outputTokens, windowMs) {
5011
+ if (outputTokens < PROBE_TPS_MIN_TOKENS || windowMs <= PROBE_TPS_MIN_WINDOW_MS) return void 0;
5012
+ return Math.round(outputTokens / (windowMs / 1e3) * 10) / 10;
5013
+ }
4770
5014
 
4771
5015
  // src/services/agent-config-service.ts
4772
5016
  var THINKING_LEVELS = ["none", "low", "medium", "high", "xhigh"];
@@ -4790,7 +5034,7 @@ var AgentConfigService = class {
4790
5034
  }
4791
5035
  async requireExists(projectId, agentId) {
4792
5036
  if (!await this.exists(projectId, agentId)) {
4793
- throw new HttpError(404, "agent_not_found", "Agent \u4E0D\u5B58\u5728\u3002");
5037
+ throw new HttpError(404, "agent_not_found", "Agent does not exist.");
4794
5038
  }
4795
5039
  }
4796
5040
  /**
@@ -4875,8 +5119,8 @@ var AgentConfigService = class {
4875
5119
  const cfg = config2;
4876
5120
  const yamlPath = systemConfigPath(this.root, projectId, agentId);
4877
5121
  const doc = parseDocument(await fs10.readFile(yamlPath, "utf8"));
4878
- const setIfProvided = (path16, value) => {
4879
- if (value !== void 0) doc.setIn(path16, value);
5122
+ const setIfProvided = (path17, value) => {
5123
+ if (value !== void 0) doc.setIn(path17, value);
4880
5124
  };
4881
5125
  setIfProvided(["name"], optionalString(cfg, "name", { maxLen: 100, label: "name" }));
4882
5126
  setIfProvided(
@@ -4952,23 +5196,25 @@ var AgentConfigService = class {
4952
5196
  for (const entry of req.entries) {
4953
5197
  if (!isValidVaultKey(entry.key)) {
4954
5198
  throw badRequest(
4955
- `vault \u952E\u540D\u4E0D\u5408\u6CD5\uFF1A${entry.key}\uFF08\u4EC5\u5B57\u6BCD\u3001\u6570\u5B57\u4E0E\u4E0B\u5212\u7EBF\uFF0C\u4E14\u4E0D\u80FD\u4EE5\u6570\u5B57\u5F00\u5934\uFF09\u3002`
5199
+ `Invalid vault key name: ${entry.key} (letters, digits and underscores only, and must not start with a digit).`
4956
5200
  );
4957
5201
  }
4958
5202
  if (seen.has(entry.key)) {
4959
- throw badRequest(`entries \u4E2D\u5B58\u5728\u91CD\u590D\u7684\u952E\u540D\uFF1A${entry.key}\u3002`);
5203
+ throw badRequest(`entries contains a duplicate key: ${entry.key}.`);
4960
5204
  }
4961
5205
  seen.add(entry.key);
4962
5206
  const prevValue = prev[entry.key];
4963
5207
  if (entry.value !== void 0) {
4964
5208
  if (entry.value.length > VAULT_VALUE_MAX_LENGTH) {
4965
- throw badRequest(`vault \u503C\u8FC7\u957F\uFF1A${entry.key}\uFF08\u4E0A\u9650 ${VAULT_VALUE_MAX_LENGTH} \u5B57\u7B26\uFF09\u3002`);
5209
+ throw badRequest(
5210
+ `Vault value too long: ${entry.key} (limit ${VAULT_VALUE_MAX_LENGTH} characters).`
5211
+ );
4966
5212
  }
4967
5213
  nextVault[entry.key] = entry.value;
4968
5214
  } else if (prevValue !== void 0) {
4969
5215
  nextVault[entry.key] = prevValue;
4970
5216
  } else {
4971
- throw badRequest(`\u65B0\u589E\u952E ${entry.key} \u5FC5\u987B\u63D0\u4F9B value\u3002`);
5217
+ throw badRequest(`New key ${entry.key} must provide a value.`);
4972
5218
  }
4973
5219
  }
4974
5220
  await saveAgentVault(this.root, projectId, agentId, nextVault);
@@ -4976,20 +5222,20 @@ var AgentConfigService = class {
4976
5222
  }
4977
5223
  };
4978
5224
  function validateToolsBuiltin(value) {
4979
- if (!Array.isArray(value)) throw badRequest("toolsBuiltin \u5FC5\u987B\u662F\u6570\u7EC4\u3002");
5225
+ if (!Array.isArray(value)) throw badRequest("toolsBuiltin must be an array.");
4980
5226
  return value.map((item, i) => {
4981
5227
  const t = asRecord(item);
4982
5228
  if (typeof t.name !== "string" || t.name.length === 0) {
4983
- throw badRequest(`toolsBuiltin[${i}].name \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
5229
+ throw badRequest(`toolsBuiltin[${i}].name must be a non-empty string.`);
4984
5230
  }
4985
5231
  if (typeof t.description !== "string") {
4986
- throw badRequest(`toolsBuiltin[${i}].description \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
5232
+ throw badRequest(`toolsBuiltin[${i}].description must be a string.`);
4987
5233
  }
4988
5234
  if (t.permission !== void 0 && t.permission !== "r" && t.permission !== "rw") {
4989
- throw badRequest(`toolsBuiltin[${i}].permission \u5FC5\u987B\u662F r / rw \u4E4B\u4E00\u3002`);
5235
+ throw badRequest(`toolsBuiltin[${i}].permission must be one of r / rw.`);
4990
5236
  }
4991
5237
  if (t.forModel !== void 0 && t.forModel !== "vision" && t.forModel !== "text-only") {
4992
- throw badRequest(`toolsBuiltin[${i}].forModel \u5FC5\u987B\u662F vision / text-only \u4E4B\u4E00\u3002`);
5238
+ throw badRequest(`toolsBuiltin[${i}].forModel must be one of vision / text-only.`);
4993
5239
  }
4994
5240
  optionalNumber(t, "timeoutMs", {
4995
5241
  integer: true,
@@ -5005,14 +5251,14 @@ function validateToolsBuiltin(value) {
5005
5251
  });
5006
5252
  }
5007
5253
  function validateMcpServers(value) {
5008
- if (!Array.isArray(value)) throw badRequest("mcpServers \u5FC5\u987B\u662F\u6570\u7EC4\u3002");
5254
+ if (!Array.isArray(value)) throw badRequest("mcpServers must be an array.");
5009
5255
  return value.map((item, i) => {
5010
5256
  const s = asRecord(item);
5011
5257
  if (typeof s.name !== "string" || s.name.length === 0) {
5012
- throw badRequest(`mcpServers[${i}].name \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
5258
+ throw badRequest(`mcpServers[${i}].name must be a non-empty string.`);
5013
5259
  }
5014
5260
  if (s.config === null || typeof s.config !== "object" || Array.isArray(s.config)) {
5015
- throw badRequest(`mcpServers[${i}].config \u5FC5\u987B\u662F\u5BF9\u8C61\u3002`);
5261
+ throw badRequest(`mcpServers[${i}].config must be an object.`);
5016
5262
  }
5017
5263
  return s;
5018
5264
  });
@@ -5020,6 +5266,7 @@ function validateMcpServers(value) {
5020
5266
 
5021
5267
  // src/services/agent-service.ts
5022
5268
  import fs11 from "fs/promises";
5269
+ import path10 from "path";
5023
5270
  import {
5024
5271
  agentDir,
5025
5272
  agentsDir as agentsDir2,
@@ -5029,6 +5276,7 @@ import {
5029
5276
  isValidId as isValidId3,
5030
5277
  loadAgentVault as loadAgentVault2,
5031
5278
  scheduleDir as scheduleDir2,
5279
+ skillsDir as skillsDir2,
5032
5280
  systemConfigPath as systemConfigPath2
5033
5281
  } from "@prismshadow/penguin-core";
5034
5282
  var AgentService = class {
@@ -5068,11 +5316,12 @@ var AgentService = class {
5068
5316
  );
5069
5317
  return Promise.all(
5070
5318
  sorted.map(async (row) => {
5071
- const [meta, updatedAt, vaultKeyCount, scheduleCount] = await Promise.all([
5319
+ const [meta, updatedAt, vaultKeyCount, scheduleCount, skillCount] = await Promise.all([
5072
5320
  this.agentConfig.readCardMeta(projectId, row.agentId),
5073
5321
  this.configUpdatedAt(projectId, row.agentId),
5074
5322
  this.vaultKeyCount(projectId, row.agentId),
5075
- this.scheduleCount(projectId, row.agentId)
5323
+ this.scheduleCount(projectId, row.agentId),
5324
+ this.skillCount(projectId, row.agentId)
5076
5325
  ]);
5077
5326
  return {
5078
5327
  agentId: row.agentId,
@@ -5080,7 +5329,8 @@ var AgentService = class {
5080
5329
  createdAt: row.createdAt,
5081
5330
  ...updatedAt !== void 0 ? { updatedAt } : {},
5082
5331
  vaultKeyCount,
5083
- scheduleCount
5332
+ scheduleCount,
5333
+ skillCount
5084
5334
  };
5085
5335
  })
5086
5336
  );
@@ -5102,6 +5352,27 @@ var AgentService = class {
5102
5352
  return 0;
5103
5353
  }
5104
5354
  }
5355
+ /** Number of installed Skills: count of skills/<name>/ directories containing a SKILL.md (0 if the directory doesn't exist). */
5356
+ async skillCount(projectId, agentId) {
5357
+ const base = skillsDir2(this.root, projectId, agentId);
5358
+ let dirents;
5359
+ try {
5360
+ dirents = await fs11.readdir(base, { withFileTypes: true });
5361
+ } catch {
5362
+ return 0;
5363
+ }
5364
+ const present = await Promise.all(
5365
+ dirents.filter((d) => d.isDirectory()).map(async (d) => {
5366
+ try {
5367
+ await fs11.access(path10.join(base, d.name, "SKILL.md"));
5368
+ return true;
5369
+ } catch {
5370
+ return false;
5371
+ }
5372
+ })
5373
+ );
5374
+ return present.filter(Boolean).length;
5375
+ }
5105
5376
  /** Last config modification time: the later of system_config.yaml and AGENTS.md mtime; omitted if neither is readable. */
5106
5377
  async configUpdatedAt(projectId, agentId) {
5107
5378
  const paths = [
@@ -5133,7 +5404,7 @@ var AgentService = class {
5133
5404
  throw new HttpError(
5134
5405
  409,
5135
5406
  "cannot_delete_builtin_agent",
5136
- "\u5185\u7F6E Agent\uFF08default_agent\uFF09\u968F Project \u4F9B\u7ED9\uFF0C\u4E0D\u80FD\u4ECE Web \u5220\u9664\u3002"
5407
+ "Built-in Agents (default_agent) are provisioned with the Project and cannot be deleted from the web."
5137
5408
  );
5138
5409
  }
5139
5410
  await fs11.rm(agentDir(this.root, projectId, agentId), { recursive: true, force: true });
@@ -5147,14 +5418,18 @@ var AgentService = class {
5147
5418
  */
5148
5419
  async createAgent(projectId, agentId, name, description) {
5149
5420
  if (!SEMANTIC_ID_PATTERN.test(agentId)) {
5150
- throw new HttpError(400, "invalid_agent_id", `Agent id \u987B\u4E3A 2~64 \u4F4D\uFF1A${SEMANTIC_ID_RULE}\u3002`);
5421
+ throw new HttpError(
5422
+ 400,
5423
+ "invalid_agent_id",
5424
+ `Agent id must be 2\u201364 characters: ${SEMANTIC_ID_RULE}.`
5425
+ );
5151
5426
  }
5152
5427
  const taken = this.agents.exists(projectId, agentId) || await fs11.stat(agentDir(this.root, projectId, agentId)).then(
5153
5428
  () => true,
5154
5429
  () => false
5155
5430
  );
5156
5431
  if (taken) {
5157
- throw new HttpError(409, "agent_exists", `Agent id \u5DF2\u88AB\u5360\u7528\uFF1A${agentId}\u3002`);
5432
+ throw new HttpError(409, "agent_exists", `Agent id is already taken: ${agentId}.`);
5158
5433
  }
5159
5434
  const displayName = name ?? agentId;
5160
5435
  await coreCreateAgent({ root: this.root, projectId, agentId });
@@ -5179,14 +5454,16 @@ var AgentService = class {
5179
5454
  toolCount: meta.toolCount,
5180
5455
  version: meta.version,
5181
5456
  vaultKeyCount: 0,
5182
- scheduleCount: 0
5457
+ scheduleCount: 0,
5458
+ // Read the real count: coreCreateAgent seeds the default skill set for default_agent.
5459
+ skillCount: await this.skillCount(projectId, agentId)
5183
5460
  };
5184
5461
  }
5185
5462
  };
5186
5463
 
5187
5464
  // src/services/benchmark-service.ts
5188
5465
  import fs12 from "fs/promises";
5189
- import path10 from "path";
5466
+ import path11 from "path";
5190
5467
  import { parse as parseToml3 } from "smol-toml";
5191
5468
  import { parse as parseYaml2 } from "yaml";
5192
5469
  import { benchmarksDir } from "@prismshadow/penguin-core";
@@ -5288,7 +5565,7 @@ var BenchmarkService = class {
5288
5565
  }
5289
5566
  const benchmarks = [];
5290
5567
  for (const item of items.filter((i) => i.isDir).sort((a, b) => a.name.localeCompare(b.name))) {
5291
- benchmarks.push(await this.readBenchmark(path10.join(dir, item.name), item.name));
5568
+ benchmarks.push(await this.readBenchmark(path11.join(dir, item.name), item.name));
5292
5569
  }
5293
5570
  return { benchmarks };
5294
5571
  }
@@ -5298,7 +5575,7 @@ var BenchmarkService = class {
5298
5575
  let runs;
5299
5576
  try {
5300
5577
  const config2 = asRecord2(
5301
- parseToml3(await fs12.readFile(path10.join(benchDir, "benchmark_config.toml"), "utf8"))
5578
+ parseToml3(await fs12.readFile(path11.join(benchDir, "benchmark_config.toml"), "utf8"))
5302
5579
  );
5303
5580
  if (typeof config2.title === "string" && config2.title !== "") title = config2.title;
5304
5581
  if (typeof config2.description === "string" && config2.description !== "") {
@@ -5313,7 +5590,7 @@ var BenchmarkService = class {
5313
5590
  let evaluations = [];
5314
5591
  try {
5315
5592
  const scoreboard = asRecord2(
5316
- parseYaml2(await fs12.readFile(path10.join(benchDir, "scoreboard.yaml"), "utf8"))
5593
+ parseYaml2(await fs12.readFile(path11.join(benchDir, "scoreboard.yaml"), "utf8"))
5317
5594
  );
5318
5595
  if (Array.isArray(scoreboard.evaluations)) {
5319
5596
  evaluations = scoreboard.evaluations.map(toEvaluation).filter((e) => e !== null);
@@ -5340,7 +5617,7 @@ var BenchmarkService = class {
5340
5617
  // src/services/snapshot-service.ts
5341
5618
  import { randomBytes as randomBytes4 } from "crypto";
5342
5619
  import fs13 from "fs/promises";
5343
- import path11 from "path";
5620
+ import path12 from "path";
5344
5621
  import * as tar from "tar";
5345
5622
  import { parse as parseYaml3 } from "yaml";
5346
5623
  import {
@@ -5353,7 +5630,7 @@ import {
5353
5630
  } from "@prismshadow/penguin-core";
5354
5631
  var VAULT_BASENAME = ".vault.toml";
5355
5632
  function isVaultEntry(entryPath) {
5356
- return path11.posix.basename(entryPath.replaceAll("\\", "/")) === VAULT_BASENAME;
5633
+ return path12.posix.basename(entryPath.replaceAll("\\", "/")) === VAULT_BASENAME;
5357
5634
  }
5358
5635
  var SnapshotService = class {
5359
5636
  constructor(root) {
@@ -5366,7 +5643,7 @@ var SnapshotService = class {
5366
5643
  try {
5367
5644
  raw = await fs13.readFile(systemConfigPath3(this.root, projectId, agentId), "utf8");
5368
5645
  } catch {
5369
- throw new HttpError(404, "agent_not_found", "Agent \u4E0D\u5B58\u5728\u3002");
5646
+ throw new HttpError(404, "agent_not_found", "Agent does not exist.");
5370
5647
  }
5371
5648
  const parsed = parseYaml3(raw);
5372
5649
  return agentStateVersion2({
@@ -5377,7 +5654,7 @@ var SnapshotService = class {
5377
5654
  async ensureSnapshot(projectId, agentId) {
5378
5655
  const version = await this.currentVersion(projectId, agentId);
5379
5656
  const dir = snapshotsDir(this.root, projectId, agentId);
5380
- const file = path11.join(dir, `v${version}.tar.gz`);
5657
+ const file = path12.join(dir, `v${version}.tar.gz`);
5381
5658
  try {
5382
5659
  await fs13.access(file);
5383
5660
  return { version, file };
@@ -5412,12 +5689,12 @@ var SnapshotService = class {
5412
5689
  async importArchive(projectId, agentId, archive, confirm) {
5413
5690
  const current = await this.currentVersion(projectId, agentId);
5414
5691
  const base = agentDir2(this.root, projectId, agentId);
5415
- const staging = path11.join(base, `.import-${randomBytes4(6).toString("hex")}`);
5692
+ const staging = path12.join(base, `.import-${randomBytes4(6).toString("hex")}`);
5416
5693
  await fs13.mkdir(staging, { recursive: true });
5417
5694
  try {
5418
- const archiveFile = path11.join(staging, "archive.tar.gz");
5695
+ const archiveFile = path12.join(staging, "archive.tar.gz");
5419
5696
  await fs13.writeFile(archiveFile, archive);
5420
- const extractDir = path11.join(staging, "extracted");
5697
+ const extractDir = path12.join(staging, "extracted");
5421
5698
  await fs13.mkdir(extractDir, { recursive: true });
5422
5699
  try {
5423
5700
  await tar.extract({
@@ -5426,21 +5703,21 @@ var SnapshotService = class {
5426
5703
  filter: (p) => !isVaultEntry(p)
5427
5704
  });
5428
5705
  } catch {
5429
- throw badRequest("\u5BFC\u5165\u5931\u8D25\uFF1A\u4E0D\u662F\u5408\u6CD5\u7684 tar.gz \u5FEB\u7167\u5305\u3002");
5706
+ throw badRequest("Import failed: not a valid tar.gz snapshot package.");
5430
5707
  }
5431
- const configPath = path11.join(extractDir, "agent_state", "system_config.yaml");
5708
+ const configPath = path12.join(extractDir, "agent_state", "system_config.yaml");
5432
5709
  let parsed;
5433
5710
  try {
5434
5711
  parsed = parseYaml3(await fs13.readFile(configPath, "utf8"));
5435
5712
  } catch {
5436
- throw badRequest("\u5BFC\u5165\u5931\u8D25\uFF1A\u5305\u5185\u7F3A\u5C11 agent_state/system_config.yaml\u3002");
5713
+ throw badRequest("Import failed: the package is missing agent_state/system_config.yaml.");
5437
5714
  }
5438
5715
  if (parsed === null || typeof parsed !== "object" || typeof parsed.system_prompt !== "string") {
5439
- throw badRequest("\u5BFC\u5165\u5931\u8D25\uFF1A\u5305\u5185 system_config.yaml \u975E\u6CD5\u3002");
5716
+ throw badRequest("Import failed: the package's system_config.yaml is invalid.");
5440
5717
  }
5441
5718
  const incomingRaw = parsed.version;
5442
5719
  if (incomingRaw !== void 0 && (!Number.isInteger(incomingRaw) || incomingRaw < 1)) {
5443
- throw badRequest("\u5BFC\u5165\u5931\u8D25\uFF1A\u5305\u5185 version \u975E\u6CD5\u3002");
5720
+ throw badRequest("Import failed: the package's version is invalid.");
5444
5721
  }
5445
5722
  const incoming = agentStateVersion2({
5446
5723
  version: typeof incomingRaw === "number" ? incomingRaw : void 0
@@ -5449,21 +5726,21 @@ var SnapshotService = class {
5449
5726
  throw new HttpError(
5450
5727
  409,
5451
5728
  "version_conflict",
5452
- `\u5305\u7248\u672C v${incoming} \u4E0D\u9AD8\u4E8E\u5F53\u524D v${current}\uFF0C\u9700\u8981\u786E\u8BA4\u540E\u5BFC\u5165\u3002`
5729
+ `Package version v${incoming} is not higher than the current v${current}; confirmation is required to import.`
5453
5730
  );
5454
5731
  }
5455
5732
  await this.ensureSnapshot(projectId, agentId);
5456
5733
  const stateDir = agentStateDir2(this.root, projectId, agentId);
5457
- const incomingState = path11.join(extractDir, "agent_state");
5734
+ const incomingState = path12.join(extractDir, "agent_state");
5458
5735
  let vault = null;
5459
5736
  try {
5460
5737
  vault = await fs13.readFile(agentVaultPath(this.root, projectId, agentId));
5461
5738
  } catch {
5462
5739
  }
5463
5740
  if (vault !== null) {
5464
- await fs13.writeFile(path11.join(incomingState, VAULT_BASENAME), vault, { mode: 384 });
5741
+ await fs13.writeFile(path12.join(incomingState, VAULT_BASENAME), vault, { mode: 384 });
5465
5742
  }
5466
- const trash = path11.join(staging, "replaced-agent_state");
5743
+ const trash = path12.join(staging, "replaced-agent_state");
5467
5744
  await fs13.rename(stateDir, trash);
5468
5745
  try {
5469
5746
  await fs13.rename(incomingState, stateDir);
@@ -5482,9 +5759,9 @@ var SnapshotService = class {
5482
5759
  import fs14 from "fs/promises";
5483
5760
  import { DEFAULT_PROJECT_ID, projectDir, provisionProjectAgents } from "@prismshadow/penguin-core";
5484
5761
  var ABORT_SETTLE_TIMEOUT_MS = 5e3;
5485
- async function dirExists(path16) {
5762
+ async function dirExists(path17) {
5486
5763
  try {
5487
- const stat = await fs14.stat(path16);
5764
+ const stat = await fs14.stat(path17);
5488
5765
  return stat.isDirectory();
5489
5766
  } catch {
5490
5767
  return false;
@@ -5514,7 +5791,11 @@ var ProjectService = class {
5514
5791
  requireProjectAccess(userId, projectId) {
5515
5792
  const row = this.resolveAccess(userId, projectId);
5516
5793
  if (!row) {
5517
- throw new HttpError(404, "project_not_found", "Project \u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE\u3002");
5794
+ throw new HttpError(
5795
+ 404,
5796
+ "project_not_found",
5797
+ "Project does not exist or you do not have access."
5798
+ );
5518
5799
  }
5519
5800
  return row;
5520
5801
  }
@@ -5531,7 +5812,11 @@ var ProjectService = class {
5531
5812
  requireProjectOwner(userId, projectId) {
5532
5813
  const row = this.requireProjectAccess(userId, projectId);
5533
5814
  if (row.role !== "owner") {
5534
- throw new HttpError(403, "owner_required", "\u8BE5\u64CD\u4F5C\u4EC5 Project owner \u53EF\u6267\u884C\u3002");
5815
+ throw new HttpError(
5816
+ 403,
5817
+ "owner_required",
5818
+ "Only the Project owner can perform this operation."
5819
+ );
5535
5820
  }
5536
5821
  return row;
5537
5822
  }
@@ -5573,7 +5858,7 @@ var ProjectService = class {
5573
5858
  throw new HttpError(
5574
5859
  400,
5575
5860
  "invalid_project_id",
5576
- `Project id \u987B\u4E3A 2~64 \u4F4D\uFF1A${SEMANTIC_ID_RULE}\uFF08\u8FDE\u5B57\u7B26\u4FDD\u7559\u4F5C\u7528\u6237\u547D\u540D\u7A7A\u95F4\u5206\u9694\uFF09\u3002`
5861
+ `Project id must be 2\u201364 characters: ${SEMANTIC_ID_RULE} (the hyphen is reserved as the user namespace separator).`
5577
5862
  );
5578
5863
  }
5579
5864
  } else {
@@ -5583,12 +5868,12 @@ var ProjectService = class {
5583
5868
  throw new HttpError(
5584
5869
  400,
5585
5870
  "project_id_prefix_required",
5586
- `Project id \u987B\u4EE5 ${prefix} \u5F00\u5934\uFF0C\u540E\u63A5\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u6216\u4E0B\u5212\u7EBF\uFF08\u603B\u957F\u4E0D\u8D85\u8FC7 ${PROJECT_ID_MAX_LENGTH}\uFF09\u3002`
5871
+ `Project id must start with ${prefix}, followed by lowercase letters, digits or underscores (at most ${PROJECT_ID_MAX_LENGTH} in total).`
5587
5872
  );
5588
5873
  }
5589
5874
  }
5590
5875
  if (this.deps.projects.findById(projectId) !== null || await dirExists(projectDir(this.deps.root, projectId))) {
5591
- throw new HttpError(409, "project_exists", `Project id \u5DF2\u88AB\u5360\u7528\uFF1A${projectId}\u3002`);
5876
+ throw new HttpError(409, "project_exists", `Project id is already taken: ${projectId}.`);
5592
5877
  }
5593
5878
  const displayName = name ?? projectId;
5594
5879
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -5596,7 +5881,7 @@ var ProjectService = class {
5596
5881
  this.deps.projects.insert({ projectId, ownerUserId: owner.userId, createdAt });
5597
5882
  } catch (err) {
5598
5883
  if (err instanceof Error && err.message.includes("UNIQUE")) {
5599
- throw new HttpError(409, "project_exists", `Project id \u5DF2\u88AB\u5360\u7528\uFF1A${projectId}\u3002`);
5884
+ throw new HttpError(409, "project_exists", `Project id is already taken: ${projectId}.`);
5600
5885
  }
5601
5886
  throw err;
5602
5887
  }
@@ -5653,14 +5938,14 @@ var ProjectService = class {
5653
5938
  throw new HttpError(
5654
5939
  409,
5655
5940
  "cannot_delete_default_project",
5656
- "default_project \u4E0E CLI \u5171\u7528\uFF0C\u4E0D\u80FD\u4ECE Web \u5220\u9664\u3002"
5941
+ "default_project is shared with the CLI and cannot be deleted from the web."
5657
5942
  );
5658
5943
  }
5659
5944
  if (this.deps.projects.listAccessible(userId).length <= 1) {
5660
5945
  throw new HttpError(
5661
5946
  409,
5662
5947
  "cannot_delete_last_project",
5663
- "\u8FD9\u662F\u5F53\u524D\u8D26\u53F7\u6700\u540E\u4E00\u4E2A Project\uFF0C\u5220\u9664\u540E\u5C06\u65E0 Project \u53EF\u7528\uFF1B\u8BF7\u5148\u521B\u5EFA\u65B0\u7684 Project\u3002"
5948
+ "This is the account's last Project; deleting it would leave no Project available. Create a new Project first."
5664
5949
  );
5665
5950
  }
5666
5951
  await this.destroyProject(projectId);
@@ -5707,13 +5992,21 @@ var ProjectService = class {
5707
5992
  const project = this.requireProjectOwner(userId, projectId);
5708
5993
  const target = this.deps.users.findById(targetUserId);
5709
5994
  if (!target) {
5710
- throw new HttpError(404, "user_not_found", `\u7528\u6237\u4E0D\u5B58\u5728\uFF1A${targetUserId}\u3002`);
5995
+ throw new HttpError(404, "user_not_found", `User does not exist: ${targetUserId}.`);
5711
5996
  }
5712
5997
  if (target.userId === project.ownerUserId) {
5713
- throw new HttpError(409, "already_owner", "owner \u65E0\u9700\u6388\u6743\u7ED9\u81EA\u5DF1\u3002");
5998
+ throw new HttpError(
5999
+ 409,
6000
+ "already_owner",
6001
+ "The owner does not need to grant access to themselves."
6002
+ );
5714
6003
  }
5715
6004
  if (this.deps.members.isMember(projectId, target.userId)) {
5716
- throw new HttpError(409, "already_member", `${targetUserId} \u5DF2\u662F\u8BE5 Project \u7684\u6210\u5458\u3002`);
6005
+ throw new HttpError(
6006
+ 409,
6007
+ "already_member",
6008
+ `${targetUserId} is already a member of this Project.`
6009
+ );
5717
6010
  }
5718
6011
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
5719
6012
  this.deps.members.insert({ projectId, userId: target.userId, createdAt });
@@ -5723,7 +6016,7 @@ var ProjectService = class {
5723
6016
  removeMember(userId, projectId, targetUserId) {
5724
6017
  this.requireProjectOwner(userId, projectId);
5725
6018
  if (!this.deps.members.isMember(projectId, targetUserId)) {
5726
- throw new HttpError(404, "member_not_found", `\u8BE5 Project \u6CA1\u6709\u6210\u5458\uFF1A${targetUserId}\u3002`);
6019
+ throw new HttpError(404, "member_not_found", `This Project has no member: ${targetUserId}.`);
5727
6020
  }
5728
6021
  this.deps.members.delete(projectId, targetUserId);
5729
6022
  }
@@ -5749,7 +6042,7 @@ var ProjectService = class {
5749
6042
  };
5750
6043
 
5751
6044
  // src/services/session-service.ts
5752
- import path12 from "path";
6045
+ import path13 from "path";
5753
6046
  import { readdir as readdir2 } from "fs/promises";
5754
6047
  import {
5755
6048
  createAgent as createAgent2,
@@ -5771,8 +6064,13 @@ var SessionService = class {
5771
6064
  this.deps = deps2;
5772
6065
  }
5773
6066
  deps;
5774
- /** DB row -> SessionInfo (run status and pending approval count come from session-manager). */
5775
- toInfo(row, hasTrace) {
6067
+ /**
6068
+ * DB row -> SessionInfo (run status and pending approval count come from session-manager).
6069
+ * Async because `source` is derived from session_meta: a registry miss (Session predating
6070
+ * this process) falls back to reading the Trace head once (see sourceOf).
6071
+ */
6072
+ async toInfo(row, hasTrace) {
6073
+ const source = await this.sourceOf(row, hasTrace);
5776
6074
  return {
5777
6075
  sessionId: row.sessionId,
5778
6076
  projectId: row.projectId,
@@ -5782,7 +6080,7 @@ var SessionService = class {
5782
6080
  workspace: row.workspace,
5783
6081
  approvalMode: row.approvalMode,
5784
6082
  ...row.title !== null ? { title: row.title } : {},
5785
- ...row.source != null ? { source: row.source } : {},
6083
+ ...source !== void 0 ? { source } : {},
5786
6084
  createdAt: row.createdAt,
5787
6085
  status: this.deps.manager.statusOf(row.sessionId),
5788
6086
  pendingApprovalCount: this.deps.manager.pendingApprovalCount(row.sessionId),
@@ -5790,13 +6088,36 @@ var SessionService = class {
5790
6088
  archived: (row.archivedAt ?? null) !== null
5791
6089
  };
5792
6090
  }
6091
+ /**
6092
+ * A Session's origin, with session_meta as the single source of truth: the in-process
6093
+ * registry answers first (populated at creation / subagent registration / adoption);
6094
+ * on a miss (a Session created before this process started) the earliest Trace shard's
6095
+ * session_meta is read once and cached. A Session with no Trace yet stays unknown and
6096
+ * is NOT cached negatively — its meta may appear with the first run.
6097
+ */
6098
+ async sourceOf(row, hasTrace) {
6099
+ const known = this.deps.sources.get(row.sessionId);
6100
+ if (known !== void 0) return known ?? void 0;
6101
+ if (!hasTrace) return void 0;
6102
+ const meta = await this.readTraceMeta(row.projectId, row.agentId, row.sessionId);
6103
+ if (!meta) return void 0;
6104
+ const source = asSessionSource(meta.payload.source) ?? null;
6105
+ this.deps.sources.set(row.sessionId, source);
6106
+ return source ?? void 0;
6107
+ }
5793
6108
  /** Whether this Session already has a Trace record (a Task has been run). */
5794
6109
  async hasTrace(row) {
5795
6110
  const ids = await this.discoverTraceSessionIds(row.projectId, row.agentId);
5796
6111
  return ids.has(row.sessionId);
5797
6112
  }
5798
- /** List: DB ∪ Trace directory discovery, sorted by createdAt descending. */
5799
- async listSessions(projectId, agentId) {
6113
+ /**
6114
+ * List: DB ∪ Trace directory discovery, sorted by createdAt descending. Optional
6115
+ * `paging` returns just that slice (the sidebar pages with limit+1 to detect "has
6116
+ * more"); slicing happens before toInfo, so per-request source derivation (lazy
6117
+ * Trace-head reads) stays bounded by the page size. Discovery/adoption still scans
6118
+ * the whole directory — the union and global ordering need every id.
6119
+ */
6120
+ async listSessions(projectId, agentId, paging) {
5800
6121
  const traceIds = await this.discoverTraceSessionIds(projectId, agentId);
5801
6122
  const rows = new Map(
5802
6123
  this.deps.sessions.listByAgent(projectId, agentId).map((r) => [r.sessionId, r])
@@ -5806,9 +6127,11 @@ var SessionService = class {
5806
6127
  const discovered = await this.adoptTraceSession(projectId, agentId, sessionId);
5807
6128
  if (discovered) rows.set(sessionId, discovered);
5808
6129
  }
5809
- return [...rows.values()].sort(
6130
+ const sorted = [...rows.values()].sort(
5810
6131
  (a, b) => b.createdAt.localeCompare(a.createdAt) || b.sessionId.localeCompare(a.sessionId)
5811
- ).map((row) => this.toInfo(row, traceIds.has(row.sessionId)));
6132
+ );
6133
+ const page = paging ? sorted.slice(paging.offset, paging.offset + paging.limit) : sorted;
6134
+ return Promise.all(page.map((row) => this.toInfo(row, traceIds.has(row.sessionId))));
5812
6135
  }
5813
6136
  /**
5814
6137
  * Session stats (Agents list card): total count = size of the union of DB index
@@ -5828,7 +6151,7 @@ var SessionService = class {
5828
6151
  };
5829
6152
  const dir = tracesDir2(this.deps.root, projectId, agentId);
5830
6153
  for (const dateDir of await listDirsSafe(dir)) {
5831
- for (const file of await listFilesSafe(path12.join(dir, dateDir))) {
6154
+ for (const file of await listFilesSafe(path13.join(dir, dateDir))) {
5832
6155
  const match = TRACE_FILE_RE.exec(file);
5833
6156
  if (match) mark(dateDir, match[1]);
5834
6157
  }
@@ -5847,23 +6170,32 @@ var SessionService = class {
5847
6170
  return { sessionCount: all.size, activity };
5848
6171
  }
5849
6172
  /**
5850
- * Create a Session: model reference `(provider, modelId)` as a pair; defaults to
5851
- * the Project's default reference (400 prompting to configure a model first if
5852
- * there is none); when provider is omitted, core's resolveModelRef performs
5853
- * unique resolution (400 on zero matches / ambiguity). `workspace` is already
6173
+ * Create a Session: the model reference is a complete `(provider, modelId)` pair.
6174
+ * Half a reference is a client error, never something to resolve — the missing half
6175
+ * is never guessed, since a guessed provider would send an entry's credential to a
6176
+ * vendor nobody named. Omitting both falls back to the Project's default reference
6177
+ * (400 prompting to configure a model first if there is none). `workspace` is already
5854
6178
  * validated by the route guard. The new Session is added to the active table
5855
6179
  * (idle).
5856
6180
  */
5857
6181
  async createSession(args) {
5858
- let modelId = args.modelId;
5859
- let provider = args.provider;
5860
- if (modelId === void 0) {
6182
+ if (args.modelId === void 0 !== (args.provider === void 0)) {
6183
+ throw badRequest(
6184
+ "modelId and provider must be given together as a (provider, modelId) pair: specify both, or neither to use the Project's default model."
6185
+ );
6186
+ }
6187
+ let modelId;
6188
+ let provider;
6189
+ if (args.modelId !== void 0 && args.provider !== void 0) {
6190
+ modelId = args.modelId;
6191
+ provider = args.provider;
6192
+ } else {
5861
6193
  const def = await this.deps.projectConfig.getDefaultModelRef(args.projectId);
5862
6194
  if (def === void 0) {
5863
6195
  throw new HttpError(
5864
6196
  400,
5865
6197
  "no_default_model",
5866
- "\u8BE5 Project \u5C1A\u672A\u914D\u7F6E\u9ED8\u8BA4\u6A21\u578B\uFF0C\u8BF7\u5148\u5728\u300C\u6A21\u578B\u300D\u9875\u6DFB\u52A0\u6A21\u578B\u5E76\u8BBE\u4E3A\u9ED8\u8BA4\u3002"
6198
+ "This Project has no default model yet. Add a model on the Models page and set it as the default first."
5867
6199
  );
5868
6200
  }
5869
6201
  modelId = def.model_id;
@@ -5878,8 +6210,10 @@ var SessionService = class {
5878
6210
  try {
5879
6211
  session = await agent.createSession({
5880
6212
  modelId,
5881
- ...provider !== void 0 ? { provider } : {},
5882
- ...args.workspace !== void 0 ? { workspaceDir: args.workspace } : {}
6213
+ provider,
6214
+ ...args.workspace !== void 0 ? { workspaceDir: args.workspace } : {},
6215
+ // The origin is also recorded in core session_meta (Trace), not just the index row.
6216
+ ...args.source !== void 0 ? { source: args.source } : {}
5883
6217
  });
5884
6218
  } catch (err) {
5885
6219
  if (isMissingCredential(err)) throw modelCredentialMissing(modelId);
@@ -5889,6 +6223,11 @@ var SessionService = class {
5889
6223
  err instanceof Error ? err.message : String(err)
5890
6224
  );
5891
6225
  }
6226
+ const metaMsg = session.metaMessage;
6227
+ this.deps.sources.set(
6228
+ session.sessionId,
6229
+ isSessionMeta4(metaMsg) ? asSessionSource(metaMsg.payload.source) ?? null : null
6230
+ );
5892
6231
  const row = {
5893
6232
  sessionId: session.sessionId,
5894
6233
  projectId: args.projectId,
@@ -5898,8 +6237,7 @@ var SessionService = class {
5898
6237
  workspace: session.workspaceDir,
5899
6238
  approvalMode: args.approvalMode ?? "allow-all",
5900
6239
  title: null,
5901
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5902
- ...args.source !== void 0 ? { source: args.source } : {}
6240
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
5903
6241
  };
5904
6242
  this.deps.sessions.insert(row);
5905
6243
  this.deps.manager.adopt(row, session);
@@ -5910,24 +6248,28 @@ var SessionService = class {
5910
6248
  const dir = tracesDir2(this.deps.root, projectId, agentId);
5911
6249
  const ids = /* @__PURE__ */ new Set();
5912
6250
  for (const dateDir of await listDirsSafe(dir)) {
5913
- for (const file of await listFilesSafe(path12.join(dir, dateDir))) {
6251
+ for (const file of await listFilesSafe(path13.join(dir, dateDir))) {
5914
6252
  const match = TRACE_FILE_RE.exec(file);
5915
6253
  if (match) ids.add(match[1]);
5916
6254
  }
5917
6255
  }
5918
6256
  return ids;
5919
6257
  }
5920
- /** Adopts a Session that exists only in the Trace directory: reads session_meta from the first line of the earliest index file. */
5921
- async adoptTraceSession(projectId, agentId, sessionId) {
6258
+ /**
6259
+ * session_meta from the earliest Trace shard of a Session (the shard whose head carries
6260
+ * the original meta); null when there is no readable Trace or it has no meta. Shared by
6261
+ * adoption backfill and lazy `source` resolution.
6262
+ */
6263
+ async readTraceMeta(projectId, agentId, sessionId) {
5922
6264
  const dir = tracesDir2(this.deps.root, projectId, agentId);
5923
6265
  let earliest = null;
5924
6266
  for (const dateDir of await listDirsSafe(dir)) {
5925
- for (const file of await listFilesSafe(path12.join(dir, dateDir))) {
6267
+ for (const file of await listFilesSafe(path13.join(dir, dateDir))) {
5926
6268
  const match = TRACE_FILE_RE.exec(file);
5927
6269
  if (!match || match[1] !== sessionId) continue;
5928
6270
  const index = Number(match[2]);
5929
6271
  if (!earliest || index < earliest.index) {
5930
- earliest = { path: path12.join(dir, dateDir, file), index };
6272
+ earliest = { path: path13.join(dir, dateDir, file), index };
5931
6273
  }
5932
6274
  }
5933
6275
  }
@@ -5938,9 +6280,14 @@ var SessionService = class {
5938
6280
  } catch {
5939
6281
  return null;
5940
6282
  }
5941
- const meta = messages.find(isSessionMeta4);
6283
+ return messages.find(isSessionMeta4) ?? null;
6284
+ }
6285
+ /** Adopts a Session that exists only in the Trace directory: reads session_meta from the first line of the earliest index file. */
6286
+ async adoptTraceSession(projectId, agentId, sessionId) {
6287
+ const meta = await this.readTraceMeta(projectId, agentId, sessionId);
5942
6288
  if (!meta) return null;
5943
6289
  if (typeof meta.payload.provider !== "string") return null;
6290
+ this.deps.sources.set(sessionId, asSessionSource(meta.payload.source) ?? null);
5944
6291
  const row = {
5945
6292
  sessionId,
5946
6293
  projectId,
@@ -5980,7 +6327,7 @@ async function listFilesSafe(dir) {
5980
6327
 
5981
6328
  // src/services/trace-service.ts
5982
6329
  import fs15 from "fs/promises";
5983
- import path13 from "path";
6330
+ import path14 from "path";
5984
6331
  import { agentsDir as agentsDir3, readTraceTolerant as readTraceTolerant2, tracesDir as tracesDir3 } from "@prismshadow/penguin-core";
5985
6332
  var TRACE_FILE_RE2 = /^(.+)_(\d{3})\.jsonl$/;
5986
6333
  var MAX_SUBAGENT_DEPTH = 4;
@@ -6018,10 +6365,10 @@ var TraceService = class {
6018
6365
  const dir = tracesDir3(this.root, projectId, agentId);
6019
6366
  const out = [];
6020
6367
  for (const dateDir of await listDirs(dir)) {
6021
- for (const file of await listFiles(path13.join(dir, dateDir))) {
6368
+ for (const file of await listFiles(path14.join(dir, dateDir))) {
6022
6369
  const match = TRACE_FILE_RE2.exec(file);
6023
6370
  if (!match || match[1] !== sessionId) continue;
6024
- out.push({ path: path13.join(dir, dateDir, file), date: dateDir, index: Number(match[2]) });
6371
+ out.push({ path: path14.join(dir, dateDir, file), date: dateDir, index: Number(match[2]) });
6025
6372
  }
6026
6373
  }
6027
6374
  return out.sort((a, b) => a.index - b.index);
@@ -6068,7 +6415,7 @@ var TraceService = class {
6068
6415
  for (const agentId of await listDirs(agentsDir3(this.root, projectId))) {
6069
6416
  const dir = tracesDir3(this.root, projectId, agentId);
6070
6417
  for (const dateDir of await listDirs(dir)) {
6071
- for (const file of await listFiles(path13.join(dir, dateDir))) {
6418
+ for (const file of await listFiles(path14.join(dir, dateDir))) {
6072
6419
  const match = TRACE_FILE_RE2.exec(file);
6073
6420
  if (match && !index.has(match[1])) index.set(match[1], agentId);
6074
6421
  }
@@ -6368,11 +6715,11 @@ var TraceService = class {
6368
6715
  const out = { dates: [] };
6369
6716
  for (const date of dates) {
6370
6717
  const bySession = /* @__PURE__ */ new Map();
6371
- for (const file of await listFiles(path13.join(dir, date))) {
6718
+ for (const file of await listFiles(path14.join(dir, date))) {
6372
6719
  const match = TRACE_FILE_RE2.exec(file);
6373
6720
  if (!match) continue;
6374
6721
  const sessionId = match[1];
6375
- const stat = await fs15.stat(path13.join(dir, date, file));
6722
+ const stat = await fs15.stat(path14.join(dir, date, file));
6376
6723
  const files = bySession.get(sessionId) ?? [];
6377
6724
  files.push({ index: Number(match[2]), sizeBytes: stat.size });
6378
6725
  bySession.set(sessionId, files);
@@ -6396,7 +6743,7 @@ var TraceService = class {
6396
6743
  throw new HttpError(
6397
6744
  404,
6398
6745
  "trace_not_found",
6399
- `\u8BE5 Session \u6CA1\u6709 index \u4E3A ${index} \u7684 Trace \u6587\u4EF6\u3002`
6746
+ `This Session has no Trace file with index ${index}.`
6400
6747
  );
6401
6748
  }
6402
6749
  return readTraceTolerant2(file.path);
@@ -6606,10 +6953,12 @@ function buildAppDeps(config2, overrides = {}) {
6606
6953
  const recorder = new UsageRecorder(usageRepo, overrides.now ?? (() => /* @__PURE__ */ new Date()));
6607
6954
  const errors = new ErrorRecorder(errorsRepo, overrides.now ?? (() => /* @__PURE__ */ new Date()));
6608
6955
  const titles = overrides.titles ?? new TitleGenerator({ sessions: sessionsRepo, channels, recorder, errors, log });
6956
+ const sessionSources = new SessionSources();
6609
6957
  const manager = new SessionManager({
6610
6958
  sessions: sessionsRepo,
6611
6959
  channels,
6612
- loader: overrides.loader ?? createCoreSessionLoader(config2.root),
6960
+ loader: overrides.loader ?? createCoreSessionLoader(config2.root, sessionSources),
6961
+ sources: sessionSources,
6613
6962
  recorder,
6614
6963
  errors,
6615
6964
  titles,
@@ -6648,7 +6997,8 @@ function buildAppDeps(config2, overrides = {}) {
6648
6997
  root: config2.root,
6649
6998
  sessions: sessionsRepo,
6650
6999
  manager,
6651
- projectConfig: projectConfigService
7000
+ projectConfig: projectConfigService,
7001
+ sources: sessionSources
6652
7002
  });
6653
7003
  const scheduler = new Scheduler({
6654
7004
  root: config2.root,
@@ -6684,6 +7034,7 @@ function buildAppDeps(config2, overrides = {}) {
6684
7034
  scheduler,
6685
7035
  channels,
6686
7036
  manager,
7037
+ sessionSources,
6687
7038
  errors,
6688
7039
  log
6689
7040
  };
@@ -6699,7 +7050,7 @@ function createApp(deps2) {
6699
7050
  });
6700
7051
  return handleError(err, c);
6701
7052
  });
6702
- app2.notFound((c) => c.json(errorBody("not_found", "\u63A5\u53E3\u4E0D\u5B58\u5728\u3002"), 404));
7053
+ app2.notFound((c) => c.json(errorBody("not_found", "Endpoint does not exist."), 404));
6703
7054
  app2.use("*", async (c, next) => {
6704
7055
  const start = performance.now();
6705
7056
  await next();
@@ -6709,7 +7060,7 @@ function createApp(deps2) {
6709
7060
  app2.use("/api/*", async (c, next) => {
6710
7061
  const contentLength = Number(c.req.header("content-length") ?? 0);
6711
7062
  if (contentLength > MAX_BODY_BYTES) {
6712
- throw new HttpError(413, "payload_too_large", "\u8BF7\u6C42\u4F53\u8D85\u8FC7 20MB \u4E0A\u9650\u3002");
7063
+ throw new HttpError(413, "payload_too_large", "Request body exceeds the 20MB limit.");
6713
7064
  }
6714
7065
  await next();
6715
7066
  });
@@ -6769,27 +7120,27 @@ function registerStaticRoutes(app2, webDist) {
6769
7120
  app2.get("*", async (c) => {
6770
7121
  const reqPath = decodeURIComponent(c.req.path);
6771
7122
  if (reqPath.startsWith("/api/")) {
6772
- return c.json(errorBody("not_found", "\u63A5\u53E3\u4E0D\u5B58\u5728\u3002"), 404);
7123
+ return c.json(errorBody("not_found", "Endpoint does not exist."), 404);
6773
7124
  }
6774
7125
  const rel = reqPath.replace(/^\/+/, "");
6775
- const resolved = path14.resolve(webDist, rel === "" ? "index.html" : rel);
6776
- const base = path14.resolve(webDist);
6777
- const target = resolved === base || resolved.startsWith(base + path14.sep) ? resolved : path14.join(base, "index.html");
7126
+ const resolved = path15.resolve(webDist, rel === "" ? "index.html" : rel);
7127
+ const base = path15.resolve(webDist);
7128
+ const target = resolved === base || resolved.startsWith(base + path15.sep) ? resolved : path15.join(base, "index.html");
6778
7129
  let file = target;
6779
7130
  try {
6780
7131
  const stat = await fsp.stat(file);
6781
- if (stat.isDirectory()) file = path14.join(file, "index.html");
7132
+ if (stat.isDirectory()) file = path15.join(file, "index.html");
6782
7133
  await fsp.access(file);
6783
7134
  } catch {
6784
- file = path14.join(base, "index.html");
7135
+ file = path15.join(base, "index.html");
6785
7136
  }
6786
7137
  let content;
6787
7138
  try {
6788
7139
  content = await fsp.readFile(file);
6789
7140
  } catch {
6790
- return c.json(errorBody("not_found", "\u8D44\u6E90\u4E0D\u5B58\u5728\u3002"), 404);
7141
+ return c.json(errorBody("not_found", "Resource does not exist."), 404);
6791
7142
  }
6792
- const type = CONTENT_TYPES2[path14.extname(file).toLowerCase()] ?? "application/octet-stream";
7143
+ const type = CONTENT_TYPES2[path15.extname(file).toLowerCase()] ?? "application/octet-stream";
6793
7144
  return new Response(new Uint8Array(content), {
6794
7145
  status: 200,
6795
7146
  headers: { "Content-Type": type }
@@ -6799,27 +7150,27 @@ function registerStaticRoutes(app2, webDist) {
6799
7150
 
6800
7151
  // src/config.ts
6801
7152
  import fs17 from "fs";
6802
- import path15 from "path";
7153
+ import path16 from "path";
6803
7154
  import { fileURLToPath } from "url";
6804
- import { resolveRoot } from "@prismshadow/penguin-core";
7155
+ import { DEFAULT_SERVER_PORT, resolveRoot } from "@prismshadow/penguin-core";
6805
7156
  var DAY_MS = 24 * 60 * 60 * 1e3;
6806
7157
  function defaultWebDist() {
6807
- const here = path15.dirname(fileURLToPath(import.meta.url));
6808
- const bundled = path15.resolve(here, "..", "web-dist");
7158
+ const here = path16.dirname(fileURLToPath(import.meta.url));
7159
+ const bundled = path16.resolve(here, "..", "web-dist");
6809
7160
  if (fs17.existsSync(bundled)) return bundled;
6810
- return path15.resolve(here, "..", "..", "web", "dist");
7161
+ return path16.resolve(here, "..", "..", "web", "dist");
6811
7162
  }
6812
7163
  function resolveServerConfig(env = process.env) {
6813
7164
  const root = env.PENGUIN_HOME ?? resolveRoot();
6814
- const port = Number(env.PORT || 7364);
7165
+ const port = Number(env.PORT || DEFAULT_SERVER_PORT);
6815
7166
  if (!Number.isInteger(port) || port < 0 || port > 65535) {
6816
- throw new Error(`\u975E\u6CD5\u7AEF\u53E3\u914D\u7F6E PORT=${env.PORT}`);
7167
+ throw new Error(`Invalid port configuration PORT=${env.PORT}`);
6817
7168
  }
6818
7169
  return {
6819
7170
  root,
6820
7171
  host: env.HOST ?? "127.0.0.1",
6821
7172
  port,
6822
- dbPath: env.PENGUIN_WEB_DB ?? path15.join(root, "web.db"),
7173
+ dbPath: env.PENGUIN_WEB_DB ?? path16.join(root, "web.db"),
6823
7174
  webDist: env.PENGUIN_WEB_DIST ?? defaultWebDist(),
6824
7175
  authSessionTtlMs: 7 * DAY_MS,
6825
7176
  authSessionRenewMs: 6 * DAY_MS
@@ -6834,15 +7185,15 @@ var app = createApp(deps);
6834
7185
  await deps.authService.seedAdmin();
6835
7186
  await deps.scheduler.start();
6836
7187
  var server = serve({ fetch: app.fetch, hostname: config.host, port: config.port }, (info) => {
6837
- console.log(`penguin-server \u5DF2\u542F\u52A8: http://${config.host}:${info.port}`);
6838
- console.log(`\u6570\u636E\u6839\u76EE\u5F55: ${config.root}`);
7188
+ console.log(`penguin-server started: http://${config.host}:${info.port}`);
7189
+ console.log(`Data root: ${config.root}`);
6839
7190
  console.log(`SQLite: ${config.dbPath}`);
6840
7191
  });
6841
7192
  var shuttingDown = false;
6842
7193
  async function shutdown(signal, exitCode = 0) {
6843
7194
  if (shuttingDown) return;
6844
7195
  shuttingDown = true;
6845
- console.log(`\u6536\u5230 ${signal}\uFF0C\u6B63\u5728\u5173\u505C\u2026`);
7196
+ console.log(`Received ${signal}, shutting down\u2026`);
6846
7197
  deps.scheduler.stop();
6847
7198
  await deps.manager.shutdown(5e3);
6848
7199
  deps.channels.dispose();
@@ -6855,13 +7206,13 @@ async function shutdown(signal, exitCode = 0) {
6855
7206
  process.on("SIGINT", () => void shutdown("SIGINT"));
6856
7207
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
6857
7208
  process.on("uncaughtException", (err) => {
6858
- console.error(`[server] \u672A\u6355\u83B7\u5F02\u5E38: ${err.stack ?? err.message}`);
7209
+ console.error(`[server] Uncaught exception: ${err.stack ?? err.message}`);
6859
7210
  deps.errors.record({ source: "process", err, code: "uncaught_exception" });
6860
7211
  void shutdown("uncaughtException", 1).catch(() => process.exit(1));
6861
7212
  });
6862
7213
  process.on("unhandledRejection", (reason) => {
6863
7214
  const err = reason instanceof Error ? reason : new Error(String(reason));
6864
- console.error(`[server] \u672A\u5904\u7406\u7684 Promise \u62D2\u7EDD: ${err.stack ?? err.message}`);
7215
+ console.error(`[server] Unhandled promise rejection: ${err.stack ?? err.message}`);
6865
7216
  deps.errors.record({ source: "process", err, code: "unhandled_rejection" });
6866
7217
  });
6867
7218
  //# sourceMappingURL=index.js.map