@prismshadow/penguin-server 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,64 @@ 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")
57
+ source TEXT, -- session origin: NULL=user-created | schedule (scheduled task) | subagent (child session)
58
58
  created_at TEXT NOT NULL
59
59
  );
60
60
  CREATE TABLE IF NOT EXISTS usage_records (
61
61
  id INTEGER PRIMARY KEY AUTOINCREMENT,
62
62
  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
63
+ date TEXT NOT NULL, -- local date yyyy-mm-dd (aggregation key; same convention as Trace date directories)
64
64
  project_id TEXT NOT NULL,
65
65
  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
66
+ session_id TEXT NOT NULL, -- top-level Session (child-session usage counts toward its main Session)
67
+ origin_session_id TEXT, -- immediate source child Session (last of the origin chain); NULL=main session
68
+ provider TEXT NOT NULL, -- provider group: paired with model_id as the attribution key; always GROUP BY provider, model_id
69
+ model_id TEXT NOT NULL, -- upstream model id (paired with provider; same model_id across providers aggregates separately)
70
70
  cache_read INTEGER NOT NULL,
71
71
  cache_write INTEGER NOT NULL,
72
72
  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
73
+ total INTEGER NOT NULL, -- from token_usage.request (one row per Request)
74
+ status TEXT NOT NULL DEFAULT 'completed' -- request outcome: completed=success (with tokens); others=failure (0 tokens, for success rate)
75
+ ); -- cost is not stored: computed at query time from current pricing
76
76
  CREATE INDEX IF NOT EXISTS idx_usage_project_date ON usage_records(project_id, date);
77
77
  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
78
+ CREATE TABLE IF NOT EXISTS error_records ( -- server-side error capture (the Costs page's "Errors" tab)
79
79
  id INTEGER PRIMARY KEY AUTOINCREMENT,
80
80
  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
81
+ date TEXT NOT NULL, -- local date yyyy-mm-dd (same convention as usage_records)
82
+ project_id TEXT, -- nullable: sign-in/registration and process-level errors have no Project context
83
83
  agent_id TEXT,
84
84
  session_id TEXT,
85
85
  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
86
+ kind TEXT NOT NULL, -- expected (HttpError, business 4xx) | unexpected (500/runtime)
87
87
  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
88
+ status INTEGER, -- HTTP status code; NULL for non-HTTP sources
89
+ message TEXT NOT NULL -- truncated to 500 chars (no stack stored: stacks go to logs only)
90
90
  );
91
91
  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
92
+ CREATE TABLE IF NOT EXISTS schedule_state ( -- schedule runtime state (files are declarative intent; the system never writes back)
93
93
  project_id TEXT NOT NULL,
94
94
  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
95
+ name TEXT NOT NULL, -- file name (without .toml) doubles as the identifier
96
+ creator_user_id TEXT, -- creator (recorded on API create; hand-edited file reconciliation falls back to the Project owner)
97
+ start_at_ms INTEGER NOT NULL, -- defines identity: a start_at change counts as a new task instance, resetting fire state
98
+ def_hash TEXT NOT NULL, -- file content fingerprint: any change clears the invalid flag (the file takes effect again after edits)
99
+ last_slot_ms INTEGER, -- latest due slot already consumed (advances on fire or skip; no re-fire across restarts)
100
+ last_fired_at TEXT, -- last actual fire time (for display)
101
+ fired_once INTEGER NOT NULL DEFAULT 0, -- one-shot task has fired
102
+ missed INTEGER NOT NULL DEFAULT 0, -- one-shot task was missed (flagged by startup/registration reconciliation; not backfilled)
103
+ invalid_reason TEXT, -- invalidation reason (e.g. the bound Session was deleted); NULL=healthy
104
104
  PRIMARY KEY (project_id, agent_id, name)
105
105
  );
106
106
  CREATE TABLE IF NOT EXISTS ui_prefs (
107
107
  user_id TEXT PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE,
108
- prefs_json TEXT NOT NULL -- {theme?, lastProjectId?, ...} \u81EA\u7531 JSON
108
+ prefs_json TEXT NOT NULL -- {theme?, lastProjectId?, ...} free-form JSON
109
109
  );
110
110
  `;
111
111
 
@@ -460,7 +460,7 @@ var SchedulesRepo = class {
460
460
  ).run(args.defHash, args.projectId, args.agentId, args.name);
461
461
  }
462
462
  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");
463
+ if (!row) throw new Error("Failed to read back schedule_state after registration");
464
464
  return { row, fresh };
465
465
  }
466
466
  /** Advance the consumed scheduled time (advances whether triggered or skipped; restarts don't re-trigger). */
@@ -857,15 +857,15 @@ function modelCredentialMissing(modelId) {
857
857
  return new HttpError(
858
858
  400,
859
859
  "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`
860
+ `Model ${modelId} has no API key yet. Configure it on the Models page first.`
861
861
  );
862
862
  }
863
863
  function handleError(err, c) {
864
864
  if (err instanceof HttpError) {
865
865
  return c.json(errorBody(err.code, err.message), err.status);
866
866
  }
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);
867
+ console.error(`[server] Unhandled exception: ${err.stack ?? err.message}`);
868
+ return c.json(errorBody("internal", "Internal server error."), 500);
869
869
  }
870
870
 
871
871
  // src/auth/middleware.ts
@@ -875,7 +875,7 @@ function authMiddleware(auth) {
875
875
  const token = getCookie(c, SESSION_COOKIE);
876
876
  const user = token ? auth.authenticate(token) : null;
877
877
  if (!user) {
878
- throw new HttpError(401, "unauthorized", "\u672A\u767B\u5F55\u6216\u767B\u5F55\u5DF2\u8FC7\u671F\u3002");
878
+ throw new HttpError(401, "unauthorized", "Not signed in or the sign-in has expired.");
879
879
  }
880
880
  c.set("user", user);
881
881
  await next();
@@ -886,7 +886,11 @@ var jsonOnlyWrites = async (c, next) => {
886
886
  if (WRITE_METHODS.has(c.req.method)) {
887
887
  const contentType = c.req.header("content-type");
888
888
  if (contentType && !contentType.toLowerCase().startsWith("application/json")) {
889
- throw new HttpError(415, "unsupported_media_type", "\u5199\u8BF7\u6C42\u4EC5\u63A5\u53D7 application/json\u3002");
889
+ throw new HttpError(
890
+ 415,
891
+ "unsupported_media_type",
892
+ "Write requests only accept application/json."
893
+ );
890
894
  }
891
895
  }
892
896
  await next();
@@ -950,7 +954,7 @@ async function verifyPassword(password, stored) {
950
954
  // src/auth/service.ts
951
955
  var MIN_PASSWORD_LENGTH = 8;
952
956
  var ADMIN_USER_ID = "admin";
953
- var ADMIN_INITIAL_PASSWORD = "admin123";
957
+ var ADMIN_INITIAL_PASSWORD = "penguin-2026";
954
958
  function sha256Hex(value) {
955
959
  return createHash("sha256").update(value).digest("hex");
956
960
  }
@@ -995,7 +999,7 @@ var AuthService = class {
995
999
  const row = this.deps.users.findById(userId);
996
1000
  const ok = row !== null && await verifyPassword(password, row.passwordHash);
997
1001
  if (!row || !ok) {
998
- throw new HttpError(401, "invalid_credentials", "\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF\u3002");
1002
+ throw new HttpError(401, "invalid_credentials", "Incorrect username or password.");
999
1003
  }
1000
1004
  this.deps.authSessions.deleteExpired(this.now().toISOString());
1001
1005
  return { user: toUserInfo(row), token: this.issueSession(row.userId) };
@@ -1004,10 +1008,10 @@ var AuthService = class {
1004
1008
  async changePassword(userId, oldPassword, newPassword) {
1005
1009
  const row = this.deps.users.findById(userId);
1006
1010
  if (!row || !await verifyPassword(oldPassword, row.passwordHash)) {
1007
- throw new HttpError(400, "password_mismatch", "\u5F53\u524D\u5BC6\u7801\u4E0D\u6B63\u786E\u3002");
1011
+ throw new HttpError(400, "password_mismatch", "Current password is incorrect.");
1008
1012
  }
1009
1013
  if (newPassword.length < MIN_PASSWORD_LENGTH) {
1010
- throw new HttpError(400, "invalid_password", "\u5BC6\u7801\u81F3\u5C11 8 \u4E2A\u5B57\u7B26\u3002");
1014
+ throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
1011
1015
  }
1012
1016
  this.deps.users.updatePassword(userId, await hashPassword(newPassword), false);
1013
1017
  }
@@ -1057,28 +1061,29 @@ function badRequest(message) {
1057
1061
  function pathParam(c, name) {
1058
1062
  const v = c.req.param(name);
1059
1063
  if (v === void 0 || v === "") {
1060
- throw new HttpError(404, "not_found", "\u8DEF\u5F84\u53C2\u6570\u7F3A\u5931\u3002");
1064
+ throw new HttpError(404, "not_found", "Missing path parameter.");
1061
1065
  }
1062
1066
  return v;
1063
1067
  }
1064
1068
  function requireValidId(c, name) {
1065
1069
  const v = pathParam(c, name);
1066
1070
  if (!isValidId(v)) {
1067
- throw new HttpError(404, "not_found", "\u8D44\u6E90\u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE\u3002");
1071
+ throw new HttpError(404, "not_found", "Resource does not exist or you do not have access.");
1068
1072
  }
1069
1073
  return v;
1070
1074
  }
1071
1075
  function positiveIntParam(c, name) {
1072
1076
  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`);
1077
+ if (!Number.isInteger(v) || v < 1) throw badRequest(`${name} must be a positive integer.`);
1074
1078
  return v;
1075
1079
  }
1076
1080
  function paginationQuery(c) {
1077
1081
  const offset = Number.parseInt(c.req.query("offset") ?? "0", 10);
1078
1082
  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");
1083
+ if (!Number.isInteger(offset) || offset < 0)
1084
+ throw badRequest("offset must be a non-negative integer.");
1080
1085
  if (!Number.isInteger(limit) || limit < 1 || limit > 1e3) {
1081
- throw badRequest("limit \u5FC5\u987B\u662F 1~1000 \u7684\u6574\u6570\u3002");
1086
+ throw badRequest("limit must be an integer between 1 and 1000.");
1082
1087
  }
1083
1088
  return { offset, limit };
1084
1089
  }
@@ -1087,25 +1092,25 @@ async function readJson(c) {
1087
1092
  try {
1088
1093
  body = await c.req.json();
1089
1094
  } catch {
1090
- throw badRequest("\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F\u5408\u6CD5 JSON\u3002");
1095
+ throw badRequest("Request body must be valid JSON.");
1091
1096
  }
1092
1097
  if (body === null || typeof body !== "object" || Array.isArray(body)) {
1093
- throw badRequest("\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F JSON \u5BF9\u8C61\u3002");
1098
+ throw badRequest("Request body must be a JSON object.");
1094
1099
  }
1095
1100
  return body;
1096
1101
  }
1097
1102
  function requireString(obj, key, rule = {}) {
1098
1103
  const v = obj[key];
1099
1104
  const label = rule.label ?? key;
1100
- if (typeof v !== "string") throw badRequest(`${label} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
1105
+ if (typeof v !== "string") throw badRequest(`${label} must be a string.`);
1101
1106
  if (rule.minLen !== void 0 && v.length < rule.minLen) {
1102
- throw badRequest(`${label} \u957F\u5EA6\u81F3\u5C11 ${rule.minLen} \u4E2A\u5B57\u7B26\u3002`);
1107
+ throw badRequest(`${label} must be at least ${rule.minLen} characters.`);
1103
1108
  }
1104
1109
  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`);
1110
+ throw badRequest(`${label} must be at most ${rule.maxLen} characters.`);
1106
1111
  }
1107
1112
  if (rule.pattern !== void 0 && !rule.pattern.test(v)) {
1108
- throw badRequest(`${label} \u683C\u5F0F\u4E0D\u5408\u6CD5\u3002`);
1113
+ throw badRequest(`${label} has an invalid format.`);
1109
1114
  }
1110
1115
  return v;
1111
1116
  }
@@ -1116,7 +1121,7 @@ function optionalString(obj, key, rule = {}) {
1116
1121
  function requireEnum(obj, key, values, label = key) {
1117
1122
  const v = obj[key];
1118
1123
  if (typeof v !== "string" || !values.includes(v)) {
1119
- throw badRequest(`${label} \u5FC5\u987B\u662F ${values.join(" / ")} \u4E4B\u4E00\u3002`);
1124
+ throw badRequest(`${label} must be one of ${values.join(" / ")}.`);
1120
1125
  }
1121
1126
  return v;
1122
1127
  }
@@ -1128,18 +1133,18 @@ function optionalNumber(obj, key, rule = {}) {
1128
1133
  const v = obj[key];
1129
1134
  if (v === void 0) return void 0;
1130
1135
  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`);
1136
+ if (typeof v !== "number" || !Number.isFinite(v)) throw badRequest(`${label} must be a number.`);
1137
+ if (rule.integer && !Number.isInteger(v)) throw badRequest(`${label} must be an integer.`);
1133
1138
  if (rule.positiveOrMinusOne && !(v > 0 || v === -1)) {
1134
- throw badRequest(`${label} \u5FC5\u987B\u5927\u4E8E 0 \u6216\u4E3A -1\u3002`);
1139
+ throw badRequest(`${label} must be greater than 0 or equal to -1.`);
1135
1140
  }
1136
- if (rule.nonNegative && v < 0) throw badRequest(`${label} \u4E0D\u80FD\u4E3A\u8D1F\u6570\u3002`);
1141
+ if (rule.nonNegative && v < 0) throw badRequest(`${label} must not be negative.`);
1137
1142
  return v;
1138
1143
  }
1139
1144
  function optionalDateParam(value, label) {
1140
1145
  if (value === void 0 || value === "") return void 0;
1141
1146
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
1142
- throw badRequest(`${label} \u5FC5\u987B\u662F YYYY-MM-DD \u683C\u5F0F\u3002`);
1147
+ throw badRequest(`${label} must be in YYYY-MM-DD format.`);
1143
1148
  }
1144
1149
  return value;
1145
1150
  }
@@ -1149,7 +1154,7 @@ function adminUsersRoutes(deps2) {
1149
1154
  const app2 = new Hono();
1150
1155
  app2.use("*", async (c, next) => {
1151
1156
  if (!c.var.user.isAdmin) {
1152
- throw new HttpError(403, "admin_required", "\u8BE5\u64CD\u4F5C\u4EC5\u7BA1\u7406\u5458\u53EF\u6267\u884C\u3002");
1157
+ throw new HttpError(403, "admin_required", "Only an admin can perform this operation.");
1153
1158
  }
1154
1159
  await next();
1155
1160
  });
@@ -1386,7 +1391,7 @@ function membersRoutes(deps2) {
1386
1391
  import { Hono as Hono7 } from "hono";
1387
1392
  function parseRef(value, label) {
1388
1393
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
1389
- throw badRequest(`${label} \u5FC5\u987B\u662F { provider, modelId } \u5BF9\u8C61\u3002`);
1394
+ throw badRequest(`${label} must be a { provider, modelId } object.`);
1390
1395
  }
1391
1396
  const r = value;
1392
1397
  return {
@@ -1395,10 +1400,10 @@ function parseRef(value, label) {
1395
1400
  };
1396
1401
  }
1397
1402
  function parseModelsUpdate(body) {
1398
- if (!Array.isArray(body.models)) throw badRequest("models \u5FC5\u987B\u662F\u6570\u7EC4\u3002");
1403
+ if (!Array.isArray(body.models)) throw badRequest("models must be an array.");
1399
1404
  const models = body.models.map((item, i) => {
1400
1405
  if (item === null || typeof item !== "object" || Array.isArray(item)) {
1401
- throw badRequest(`models[${i}] \u5FC5\u987B\u662F\u5BF9\u8C61\u3002`);
1406
+ throw badRequest(`models[${i}] must be an object.`);
1402
1407
  }
1403
1408
  const m = item;
1404
1409
  const entry = {
@@ -1415,7 +1420,7 @@ function parseModelsUpdate(body) {
1415
1420
  };
1416
1421
  if (m.displayName !== void 0) {
1417
1422
  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`);
1423
+ throw badRequest(`models[${i}].displayName must be a string of at most 100 characters.`);
1419
1424
  }
1420
1425
  if (m.displayName) entry.displayName = m.displayName;
1421
1426
  }
@@ -1424,31 +1429,31 @@ function parseModelsUpdate(body) {
1424
1429
  }
1425
1430
  if (m.contextWindow !== void 0) {
1426
1431
  if (typeof m.contextWindow !== "number" || !(m.contextWindow > 0)) {
1427
- throw badRequest(`models[${i}].contextWindow \u5FC5\u987B\u662F\u6B63\u6570\u3002`);
1432
+ throw badRequest(`models[${i}].contextWindow must be a positive number.`);
1428
1433
  }
1429
1434
  entry.contextWindow = m.contextWindow;
1430
1435
  }
1431
1436
  if (m.clientType !== void 0) {
1432
1437
  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`);
1438
+ throw badRequest(`models[${i}].clientType must be a string of at most 64 characters.`);
1434
1439
  }
1435
1440
  if (m.clientType) entry.clientType = m.clientType;
1436
1441
  }
1437
1442
  if (m.vision !== void 0) {
1438
1443
  if (typeof m.vision !== "boolean") {
1439
- throw badRequest(`models[${i}].vision \u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002`);
1444
+ throw badRequest(`models[${i}].vision must be a boolean.`);
1440
1445
  }
1441
1446
  entry.vision = m.vision;
1442
1447
  }
1443
1448
  if (m.pricing !== void 0) {
1444
1449
  const p = m.pricing;
1445
1450
  if (p === null || typeof p !== "object" || Array.isArray(p)) {
1446
- throw badRequest(`models[${i}].pricing \u5FC5\u987B\u662F\u5BF9\u8C61\u3002`);
1451
+ throw badRequest(`models[${i}].pricing must be an object.`);
1447
1452
  }
1448
1453
  for (const key of ["cacheRead", "cacheWrite", "output"]) {
1449
1454
  const v = p[key];
1450
1455
  if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
1451
- throw badRequest(`models[${i}].pricing.${key} \u5FC5\u987B\u662F\u975E\u8D1F\u6570\u5B57\u3002`);
1456
+ throw badRequest(`models[${i}].pricing.${key} must be a non-negative number.`);
1452
1457
  }
1453
1458
  }
1454
1459
  entry.pricing = {
@@ -1459,19 +1464,19 @@ function parseModelsUpdate(body) {
1459
1464
  }
1460
1465
  if (m.apiKey !== void 0) {
1461
1466
  if (typeof m.apiKey !== "string" || m.apiKey.length === 0) {
1462
- throw badRequest(`models[${i}].apiKey \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
1467
+ throw badRequest(`models[${i}].apiKey must be a non-empty string.`);
1463
1468
  }
1464
1469
  entry.apiKey = m.apiKey;
1465
1470
  }
1466
1471
  if (m.clearApiKey !== void 0) {
1467
1472
  if (typeof m.clearApiKey !== "boolean") {
1468
- throw badRequest(`models[${i}].clearApiKey \u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002`);
1473
+ throw badRequest(`models[${i}].clearApiKey must be a boolean.`);
1469
1474
  }
1470
1475
  entry.clearApiKey = m.clearApiKey;
1471
1476
  }
1472
1477
  if (m.baseUrl !== void 0) {
1473
1478
  if (m.baseUrl !== null && typeof m.baseUrl !== "string") {
1474
- throw badRequest(`models[${i}].baseUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216 null\u3002`);
1479
+ throw badRequest(`models[${i}].baseUrl must be a string or null.`);
1475
1480
  }
1476
1481
  entry.baseUrl = m.baseUrl;
1477
1482
  }
@@ -1508,21 +1513,25 @@ function modelsRoutes(deps2) {
1508
1513
  modelId: requireString(body, "modelId", { minLen: 1, maxLen: 200 })
1509
1514
  };
1510
1515
  if (body.apiKey !== void 0) {
1511
- if (typeof body.apiKey !== "string") throw badRequest("apiKey \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002");
1516
+ if (typeof body.apiKey !== "string") throw badRequest("apiKey must be a string.");
1512
1517
  if (body.apiKey) req.apiKey = body.apiKey;
1513
1518
  }
1514
1519
  if (body.clearApiKey !== void 0) {
1515
- if (typeof body.clearApiKey !== "boolean") throw badRequest("clearApiKey \u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002");
1520
+ if (typeof body.clearApiKey !== "boolean") throw badRequest("clearApiKey must be a boolean.");
1516
1521
  req.clearApiKey = body.clearApiKey;
1517
1522
  }
1523
+ if (body.speed !== void 0) {
1524
+ if (typeof body.speed !== "boolean") throw badRequest("speed must be a boolean.");
1525
+ req.speed = body.speed;
1526
+ }
1518
1527
  if (body.baseUrl !== void 0) {
1519
1528
  if (body.baseUrl !== null && typeof body.baseUrl !== "string") {
1520
- throw badRequest("baseUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216 null\u3002");
1529
+ throw badRequest("baseUrl must be a string or null.");
1521
1530
  }
1522
1531
  req.baseUrl = body.baseUrl ? body.baseUrl : null;
1523
1532
  }
1524
1533
  if (body.clientType !== void 0) {
1525
- if (typeof body.clientType !== "string") throw badRequest("clientType \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002");
1534
+ if (typeof body.clientType !== "string") throw badRequest("clientType must be a string.");
1526
1535
  if (body.clientType) req.clientType = body.clientType;
1527
1536
  }
1528
1537
  return c.json(await deps2.projectConfigService.testModel(projectId, req));
@@ -1533,10 +1542,10 @@ function modelsRoutes(deps2) {
1533
1542
  // src/http/routes/vault.ts
1534
1543
  import { Hono as Hono8 } from "hono";
1535
1544
  function parseVaultUpdate(body) {
1536
- if (!Array.isArray(body.entries)) throw badRequest("entries \u5FC5\u987B\u662F\u6570\u7EC4\u3002");
1545
+ if (!Array.isArray(body.entries)) throw badRequest("entries must be an array.");
1537
1546
  const entries = body.entries.map((item, i) => {
1538
1547
  if (item === null || typeof item !== "object" || Array.isArray(item)) {
1539
- throw badRequest(`entries[${i}] \u5FC5\u987B\u662F\u5BF9\u8C61\u3002`);
1548
+ throw badRequest(`entries[${i}] must be an object.`);
1540
1549
  }
1541
1550
  const e = item;
1542
1551
  const entry = {
@@ -1544,7 +1553,7 @@ function parseVaultUpdate(body) {
1544
1553
  };
1545
1554
  if (e.value !== void 0) {
1546
1555
  if (typeof e.value !== "string" || e.value.length === 0) {
1547
- throw badRequest(`entries[${i}].value \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
1556
+ throw badRequest(`entries[${i}].value must be a non-empty string.`);
1548
1557
  }
1549
1558
  entry.value = e.value;
1550
1559
  }
@@ -1566,6 +1575,7 @@ function vaultRoutes(deps2) {
1566
1575
  deps2.projectService.requireProjectOwner(c.var.user.userId, projectId);
1567
1576
  const req = parseVaultUpdate(await readJson(c));
1568
1577
  const res = await deps2.agentConfigService.updateVault(projectId, agentId, req);
1578
+ deps2.manager.invalidateAgentRuntimes(projectId, agentId);
1569
1579
  return c.json(res);
1570
1580
  });
1571
1581
  return app2;
@@ -1603,72 +1613,77 @@ function parseScheduleFile(name, raw) {
1603
1613
  } catch (err) {
1604
1614
  return {
1605
1615
  ok: false,
1606
- error: `TOML \u89E3\u6790\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`
1616
+ error: `Failed to parse TOML: ${err instanceof Error ? err.message : String(err)}`
1607
1617
  };
1608
1618
  }
1609
1619
  if (parsed === null || typeof parsed !== "object")
1610
- return { ok: false, error: "\u5185\u5BB9\u4E0D\u662F TOML \u8868" };
1620
+ return { ok: false, error: "Content is not a TOML table" };
1611
1621
  const t = parsed;
1612
1622
  const prompt = t["prompt"];
1613
1623
  if (typeof prompt !== "string" || prompt.trim() === "") {
1614
- return { ok: false, error: "\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 prompt" };
1624
+ return { ok: false, error: "Missing required field prompt" };
1615
1625
  }
1616
1626
  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" };
1627
+ if (typeof enabled !== "boolean") return { ok: false, error: "enabled must be a boolean" };
1618
1628
  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" };
1629
+ if (startAt === null)
1630
+ return { ok: false, error: "start_at is missing or not a valid ISO 8601 instant" };
1620
1631
  let period;
1621
1632
  let periodMs;
1622
1633
  if (t["period"] !== void 0) {
1623
- if (typeof t["period"] !== "string") return { ok: false, error: "period \u5FC5\u987B\u662F\u5B57\u7B26\u4E32" };
1634
+ if (typeof t["period"] !== "string") return { ok: false, error: "period must be a string" };
1624
1635
  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" };
1636
+ if (ms === null) return { ok: false, error: "period must look like 30m / 12h / 7d" };
1637
+ if (ms < MIN_PERIOD_MS) return { ok: false, error: "period is below the 5m minimum" };
1627
1638
  period = t["period"].trim();
1628
1639
  periodMs = ms;
1629
1640
  }
1630
1641
  let endAt;
1631
1642
  if (t["end_at"] !== void 0) {
1632
1643
  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" };
1644
+ if (parsedEnd === null) return { ok: false, error: "end_at is not a valid ISO 8601 instant" };
1645
+ if (parsedEnd.ms <= startAt.ms)
1646
+ return { ok: false, error: "end_at must be later than start_at" };
1635
1647
  endAt = parsedEnd;
1636
1648
  }
1637
1649
  let sessionId;
1638
1650
  if (t["session_id"] !== void 0) {
1639
1651
  if (typeof t["session_id"] !== "string" || t["session_id"] === "") {
1640
- return { ok: false, error: "session_id \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32" };
1652
+ return { ok: false, error: "session_id must be a non-empty string" };
1641
1653
  }
1642
1654
  sessionId = t["session_id"];
1643
1655
  }
1644
1656
  let workspace;
1645
1657
  if (t["workspace"] !== void 0) {
1646
1658
  if (typeof t["workspace"] !== "string" || t["workspace"] === "") {
1647
- return { ok: false, error: "workspace \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32" };
1659
+ return { ok: false, error: "workspace must be a non-empty string" };
1648
1660
  }
1649
1661
  workspace = t["workspace"];
1650
1662
  }
1651
1663
  let modelId;
1652
1664
  if (t["model_id"] !== void 0) {
1653
1665
  if (typeof t["model_id"] !== "string" || t["model_id"] === "") {
1654
- return { ok: false, error: "model_id \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32" };
1666
+ return { ok: false, error: "model_id must be a non-empty string" };
1655
1667
  }
1656
1668
  modelId = t["model_id"];
1657
1669
  }
1658
1670
  let provider;
1659
1671
  if (t["provider"] !== void 0) {
1660
1672
  if (typeof t["provider"] !== "string" || t["provider"] === "") {
1661
- return { ok: false, error: "provider \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32" };
1673
+ return { ok: false, error: "provider must be a non-empty string" };
1662
1674
  }
1663
1675
  provider = t["provider"];
1664
1676
  }
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
1677
  if (sessionId !== void 0 && (workspace !== void 0 || modelId !== void 0 || provider !== void 0)) {
1669
1678
  return {
1670
1679
  ok: false,
1671
- error: "\u76EE\u6807\u4E8C\u9009\u4E00\uFF1Aworkspace \u4E0E provider / model_id \u4EC5\u7528\u4E8E\u65B0\u5EFA Session \u6A21\u5F0F"
1680
+ error: "Pick one target: workspace and provider / model_id are only for new-Session mode"
1681
+ };
1682
+ }
1683
+ if (modelId === void 0 !== (provider === void 0)) {
1684
+ return {
1685
+ ok: false,
1686
+ 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
1687
  };
1673
1688
  }
1674
1689
  return {
@@ -1763,7 +1778,10 @@ function serializeSchedule(fields) {
1763
1778
  `;
1764
1779
  }
1765
1780
  async function validateScheduleModelRef(root, projectId, def) {
1766
- if (def.modelId === void 0) return null;
1781
+ if (def.modelId === void 0 && def.provider === void 0) return null;
1782
+ if (def.modelId === void 0 || def.provider === void 0) {
1783
+ 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";
1784
+ }
1767
1785
  try {
1768
1786
  const cfg = await loadProjectConfig(root, projectId);
1769
1787
  resolveModelRef(cfg, def.modelId, def.provider);
@@ -1789,7 +1807,7 @@ async function deleteScheduleFile(root, projectId, agentId, name) {
1789
1807
 
1790
1808
  // src/http/routes/schedules.ts
1791
1809
  function parseUpsertBody(body) {
1792
- if (typeof body.enabled !== "boolean") throw badRequest("enabled \u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002");
1810
+ if (typeof body.enabled !== "boolean") throw badRequest("enabled must be a boolean.");
1793
1811
  const prompt = requireString(body, "prompt", { minLen: 1, maxLen: 1e5 });
1794
1812
  const startAt = requireString(body, "startAt", { minLen: 1, maxLen: 100 });
1795
1813
  const period = optionalString(body, "period", { minLen: 1, maxLen: 20 });
@@ -1849,7 +1867,7 @@ function toItem(def, state, queued, nowMs) {
1849
1867
  };
1850
1868
  }
1851
1869
  function requireScheduleName(raw) {
1852
- if (!raw || !isValidId2(raw)) throw badRequest("\u5B9A\u65F6\u4EFB\u52A1\u540D\u975E\u6CD5\u3002");
1870
+ if (!raw || !isValidId2(raw)) throw badRequest("Invalid schedule name.");
1853
1871
  return raw;
1854
1872
  }
1855
1873
  function scheduleRoutes(deps2) {
@@ -1875,7 +1893,7 @@ function scheduleRoutes(deps2) {
1875
1893
  const body = await readJson(c);
1876
1894
  const name = requireScheduleName(requireString(body, "name", { minLen: 1, maxLen: 100 }));
1877
1895
  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}`);
1896
+ throw new HttpError(409, "schedule_exists", `Schedule already exists: ${name}`);
1879
1897
  }
1880
1898
  await upsert(deps2, c.var.user.userId, projectId, agentId, name, body);
1881
1899
  return c.json(await readItem(deps2, projectId, agentId, name), 201);
@@ -1894,7 +1912,7 @@ function scheduleRoutes(deps2) {
1894
1912
  deps2.projectService.requireProjectOwner(c.var.user.userId, projectId);
1895
1913
  const name = requireScheduleName(c.req.param("name"));
1896
1914
  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}`);
1915
+ throw new HttpError(404, "schedule_not_found", `Schedule does not exist: ${name}`);
1898
1916
  }
1899
1917
  const body = await readJson(c);
1900
1918
  await upsert(deps2, c.var.user.userId, projectId, agentId, name, body);
@@ -1906,7 +1924,8 @@ function scheduleRoutes(deps2) {
1906
1924
  deps2.projectService.requireProjectOwner(c.var.user.userId, projectId);
1907
1925
  const name = requireScheduleName(c.req.param("name"));
1908
1926
  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}`);
1927
+ if (!removed)
1928
+ throw new HttpError(404, "schedule_not_found", `Schedule does not exist: ${name}`);
1910
1929
  deps2.scheduler.dropEntry(projectId, agentId, name);
1911
1930
  return c.body(null, 204);
1912
1931
  });
@@ -1916,9 +1935,9 @@ async function upsert(deps2, userId, projectId, agentId, name, body) {
1916
1935
  const fields = parseUpsertBody(body);
1917
1936
  const raw = serializeSchedule(fields);
1918
1937
  const parsed = parseScheduleFile(name, raw);
1919
- if (!parsed.ok) throw badRequest(`\u5B9A\u65F6\u4EFB\u52A1\u914D\u7F6E\u975E\u6CD5\uFF1A${parsed.error}`);
1938
+ if (!parsed.ok) throw badRequest(`Invalid schedule configuration: ${parsed.error}`);
1920
1939
  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}`);
1940
+ if (refError !== null) throw badRequest(`Invalid schedule configuration: ${refError}`);
1922
1941
  await writeScheduleFile(deps2.config.root, projectId, agentId, name, raw);
1923
1942
  deps2.schedulesRepo.registerOrSync({
1924
1943
  projectId,
@@ -1935,8 +1954,8 @@ async function readItem(deps2, projectId, agentId, name) {
1935
1954
  const entry = entries.find((e) => e.def.name === name);
1936
1955
  if (entry) return toItem(entry.def, entry.state, entry.queued, Date.now());
1937
1956
  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}`);
1957
+ if (bad) throw badRequest(`Invalid schedule file: ${bad.error}`);
1958
+ throw new HttpError(404, "schedule_not_found", `Schedule does not exist: ${name}`);
1940
1959
  }
1941
1960
 
1942
1961
  // src/http/routes/benchmarks.ts
@@ -1987,11 +2006,11 @@ function libraryResponse() {
1987
2006
  }
1988
2007
  function parseInstallNames(body) {
1989
2008
  if (!Array.isArray(body.names) || body.names.length === 0) {
1990
- throw badRequest("names \u5FC5\u987B\u662F\u975E\u7A7A\u6570\u7EC4\u3002");
2009
+ throw badRequest("names must be a non-empty array.");
1991
2010
  }
1992
2011
  return body.names.map((v, i) => {
1993
2012
  if (typeof v !== "string" || v.length === 0) {
1994
- throw badRequest(`names[${i}] \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
2013
+ throw badRequest(`names[${i}] must be a non-empty string.`);
1995
2014
  }
1996
2015
  return v;
1997
2016
  });
@@ -2021,7 +2040,7 @@ function agentSkillsRoutes(deps2) {
2021
2040
  const names = parseInstallNames(await readJson(c));
2022
2041
  const skills = names.map((name) => {
2023
2042
  const skill = librarySkill(name);
2024
- if (!skill) throw new HttpError(404, "unknown_skill", `Skill \u5E93\u4E2D\u4E0D\u5B58\u5728\uFF1A${name}`);
2043
+ if (!skill) throw new HttpError(404, "unknown_skill", `Skill is not in the library: ${name}`);
2025
2044
  return skill;
2026
2045
  });
2027
2046
  for (const skill of skills) {
@@ -2038,7 +2057,7 @@ function agentSkillsRoutes(deps2) {
2038
2057
  try {
2039
2058
  await fs2.access(file);
2040
2059
  } catch {
2041
- throw new HttpError(404, "not_found", `Skill \u672A\u5B89\u88C5\uFF1A${name}`);
2060
+ throw new HttpError(404, "not_found", `Skill is not installed: ${name}`);
2042
2061
  }
2043
2062
  await removeSkill(deps2.config.root, projectId, agentId, name);
2044
2063
  return c.body(null, 204);
@@ -2077,10 +2096,11 @@ function agentTransferRoutes(deps2) {
2077
2096
  try {
2078
2097
  archive = Buffer.from(dataBase64, "base64");
2079
2098
  } catch {
2080
- throw badRequest("dataBase64 \u4E0D\u662F\u5408\u6CD5\u7684 base64\u3002");
2099
+ throw badRequest("dataBase64 is not valid base64.");
2081
2100
  }
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");
2101
+ if (archive.byteLength === 0) throw badRequest("Import package is empty.");
2102
+ if (archive.byteLength > MAX_ARCHIVE_BYTES)
2103
+ throw badRequest("Import package exceeds the 14MB limit.");
2084
2104
  const { version } = await deps2.snapshots.importArchive(projectId, agentId, archive, confirm);
2085
2105
  const res = { version };
2086
2106
  return c.json(res);
@@ -2175,17 +2195,21 @@ function dirsRoutes(deps2) {
2175
2195
  const raw = c.req.query("path");
2176
2196
  const target = raw && raw.trim() ? raw.trim() : os.homedir();
2177
2197
  if (!path4.isAbsolute(target)) {
2178
- throw new HttpError(400, "dir_not_absolute", "\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\u3002");
2198
+ throw new HttpError(400, "dir_not_absolute", "Directory must be an absolute path.");
2179
2199
  }
2180
2200
  let real;
2181
2201
  try {
2182
2202
  real = await fs4.realpath(target);
2183
2203
  } catch {
2184
- throw new HttpError(404, "dir_not_found", `\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u8BBF\u95EE\uFF1A${target}\u3002`);
2204
+ throw new HttpError(
2205
+ 404,
2206
+ "dir_not_found",
2207
+ `Directory does not exist or is inaccessible: ${target}.`
2208
+ );
2185
2209
  }
2186
2210
  const stat = await fs4.stat(real);
2187
2211
  if (!stat.isDirectory()) {
2188
- throw new HttpError(400, "not_a_dir", "\u4E0D\u662F\u76EE\u5F55\u3002");
2212
+ throw new HttpError(400, "not_a_dir", "Not a directory.");
2189
2213
  }
2190
2214
  let dirents = [];
2191
2215
  try {
@@ -2228,7 +2252,7 @@ function agentConfigRoutes(deps2) {
2228
2252
  if (agentsMd !== void 0) req.agentsMd = agentsMd;
2229
2253
  if (body.config !== void 0) {
2230
2254
  if (body.config === null || typeof body.config !== "object" || Array.isArray(body.config)) {
2231
- throw badRequest("config \u5FC5\u987B\u662F\u5BF9\u8C61\u3002");
2255
+ throw badRequest("config must be an object.");
2232
2256
  }
2233
2257
  req.config = body.config;
2234
2258
  }
@@ -2284,7 +2308,7 @@ function usageRoutes(deps2) {
2284
2308
  deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
2285
2309
  const groupByRaw = c.req.query("groupBy") ?? "date";
2286
2310
  if (!GROUP_BYS.includes(groupByRaw)) {
2287
- throw badRequest(`groupBy \u5FC5\u987B\u662F ${GROUP_BYS.join(" / ")} \u4E4B\u4E00\u3002`);
2311
+ throw badRequest(`groupBy must be one of ${GROUP_BYS.join(" / ")}.`);
2288
2312
  }
2289
2313
  const from = optionalDateParam(c.req.query("from"), "from");
2290
2314
  const to = optionalDateParam(c.req.query("to"), "to");
@@ -2326,12 +2350,16 @@ async function assertWorkspaceAllowed(args) {
2326
2350
  throw new HttpError(
2327
2351
  400,
2328
2352
  "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`
2353
+ `Workspace does not exist or is inaccessible: ${args.workspace}. Specify an existing directory, or leave it empty to use a temporary directory.`
2330
2354
  );
2331
2355
  }
2332
2356
  const stat = await fs5.stat(ws);
2333
2357
  if (!stat.isDirectory()) {
2334
- throw new HttpError(400, "workspace_not_found", `Workspace \u4E0D\u662F\u76EE\u5F55\uFF1A${args.workspace}\u3002`);
2358
+ throw new HttpError(
2359
+ 400,
2360
+ "workspace_not_found",
2361
+ `Workspace is not a directory: ${args.workspace}.`
2362
+ );
2335
2363
  }
2336
2364
  return ws;
2337
2365
  }
@@ -2373,7 +2401,7 @@ var WorkspaceFilesService = class {
2373
2401
  try {
2374
2402
  return await fs6.realpath(path5.resolve(workspace));
2375
2403
  } catch {
2376
- throw new HttpError(404, "workspace_missing", "\u8BE5 Session \u7684 Workspace \u5DF2\u4E0D\u5B58\u5728\u3002");
2404
+ throw new HttpError(404, "workspace_missing", "This Session's Workspace no longer exists.");
2377
2405
  }
2378
2406
  }
2379
2407
  /**
@@ -2389,16 +2417,16 @@ var WorkspaceFilesService = class {
2389
2417
  }
2390
2418
  /** Lexical prefix check (a relative path, once resolved, must still be inside the Workspace); returns the absolute target path. */
2391
2419
  lexicalTarget(base, rel) {
2392
- if (rel.includes("\0")) throw badRequest("path \u975E\u6CD5\u3002");
2420
+ if (rel.includes("\0")) throw badRequest("path is invalid.");
2393
2421
  const target = path5.resolve(base, rel === "" ? "." : rel);
2394
2422
  if (!this.isInside(target, base)) {
2395
- throw badRequest("path \u5FC5\u987B\u4F4D\u4E8E Workspace \u5185\u3002");
2423
+ throw badRequest("path must be inside the Workspace.");
2396
2424
  }
2397
2425
  return target;
2398
2426
  }
2399
2427
  assertInside(real, realBase) {
2400
2428
  if (!this.isInside(real, realBase)) {
2401
- throw badRequest("path \u5FC5\u987B\u4F4D\u4E8E Workspace \u5185\u3002");
2429
+ throw badRequest("path must be inside the Workspace.");
2402
2430
  }
2403
2431
  }
2404
2432
  /**
@@ -2416,7 +2444,7 @@ var WorkspaceFilesService = class {
2416
2444
  canonical = await fs6.realpath(target);
2417
2445
  } catch (err) {
2418
2446
  if (err.code === "ENOENT") {
2419
- throw new HttpError(404, "path_not_found", "\u6587\u4EF6\u4E0D\u5B58\u5728\u3002");
2447
+ throw new HttpError(404, "path_not_found", "File does not exist.");
2420
2448
  }
2421
2449
  throw err;
2422
2450
  }
@@ -2441,7 +2469,7 @@ var WorkspaceFilesService = class {
2441
2469
  const realBase = await this.realBase(workspace);
2442
2470
  const target = this.lexicalTarget(path5.resolve(workspace), rel);
2443
2471
  const name = path5.basename(target);
2444
- if (name === "" || name === "." || name === "..") throw badRequest("path \u5FC5\u987B\u662F\u6587\u4EF6\u8DEF\u5F84\u3002");
2472
+ if (name === "" || name === "." || name === "..") throw badRequest("path must be a file path.");
2445
2473
  const parent = path5.dirname(target);
2446
2474
  let canonicalParent;
2447
2475
  try {
@@ -2456,7 +2484,7 @@ var WorkspaceFilesService = class {
2456
2484
  } catch (probeErr) {
2457
2485
  if (probeErr.code !== "ENOENT") throw probeErr;
2458
2486
  const up = path5.dirname(probe);
2459
- if (up === probe) throw badRequest("path \u975E\u6CD5\u3002");
2487
+ if (up === probe) throw badRequest("path is invalid.");
2460
2488
  probe = up;
2461
2489
  }
2462
2490
  }
@@ -2496,10 +2524,10 @@ var WorkspaceFilesService = class {
2496
2524
  dirents = await fs6.readdir(dir, { withFileTypes: true });
2497
2525
  } catch (err) {
2498
2526
  if (err.code === "ENOENT") {
2499
- throw new HttpError(404, "path_not_found", "\u76EE\u5F55\u4E0D\u5B58\u5728\u3002");
2527
+ throw new HttpError(404, "path_not_found", "Directory does not exist.");
2500
2528
  }
2501
2529
  if (err.code === "ENOTDIR") {
2502
- throw badRequest("path \u4E0D\u662F\u76EE\u5F55\u3002");
2530
+ throw badRequest("path is not a directory.");
2503
2531
  }
2504
2532
  throw err;
2505
2533
  }
@@ -2535,11 +2563,11 @@ var WorkspaceFilesService = class {
2535
2563
  try {
2536
2564
  stat = await fs6.stat(file);
2537
2565
  } catch {
2538
- throw new HttpError(404, "path_not_found", "\u6587\u4EF6\u4E0D\u5B58\u5728\u3002");
2566
+ throw new HttpError(404, "path_not_found", "File does not exist.");
2539
2567
  }
2540
- if (stat.isDirectory()) throw badRequest("path \u662F\u76EE\u5F55\u3002");
2568
+ if (stat.isDirectory()) throw badRequest("path is a directory.");
2541
2569
  if (stat.size > MAX_READ_BYTES) {
2542
- throw new HttpError(413, "file_too_large", "\u6587\u4EF6\u8D85\u8FC7 50MB \u8BFB\u53D6\u4E0A\u9650\u3002");
2570
+ throw new HttpError(413, "file_too_large", "File exceeds the 50MB read limit.");
2543
2571
  }
2544
2572
  const data = await fs6.readFile(file);
2545
2573
  const ext = path5.extname(file).toLowerCase();
@@ -2559,9 +2587,9 @@ var WorkspaceFilesService = class {
2559
2587
  * sandbox escapes).
2560
2588
  */
2561
2589
  async write(workspace, rel, data) {
2562
- if (rel === "" || rel.endsWith("/")) throw badRequest("path \u5FC5\u987B\u662F\u6587\u4EF6\u8DEF\u5F84\u3002");
2590
+ if (rel === "" || rel.endsWith("/")) throw badRequest("path must be a file path.");
2563
2591
  if (data.length > MAX_UPLOAD_BYTES) {
2564
- throw new HttpError(413, "file_too_large", "\u4E0A\u4F20\u6587\u4EF6\u8D85\u8FC7 14MB \u4E0A\u9650\u3002");
2592
+ throw new HttpError(413, "file_too_large", "Uploaded file exceeds the 14MB limit.");
2565
2593
  }
2566
2594
  const { dir, name } = await this.resolveWriteParent(workspace, rel);
2567
2595
  const file = path5.join(dir, name);
@@ -2571,9 +2599,10 @@ var WorkspaceFilesService = class {
2571
2599
  handle = await fs6.open(file, flags, 420);
2572
2600
  } catch (err) {
2573
2601
  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");
2602
+ if (code === "ELOOP") throw badRequest("path must not be a symlink.");
2603
+ if (code === "ENOENT")
2604
+ throw new HttpError(404, "path_not_found", "Parent directory does not exist.");
2605
+ if (code === "EISDIR") throw badRequest("path is a directory.");
2577
2606
  throw err;
2578
2607
  }
2579
2608
  try {
@@ -2597,27 +2626,27 @@ var APPROVAL_MODES = [
2597
2626
  function parseTaskInput(body) {
2598
2627
  const input = body.input;
2599
2628
  if (!Array.isArray(input) || input.length === 0) {
2600
- throw badRequest("input \u5FC5\u987B\u662F\u81F3\u5C11\u5305\u542B\u4E00\u9879\u7684\u6570\u7EC4\u3002");
2629
+ throw badRequest("input must be an array with at least one item.");
2601
2630
  }
2602
2631
  return input.map((item, i) => {
2603
2632
  if (item === null || typeof item !== "object" || Array.isArray(item)) {
2604
- throw badRequest(`input[${i}] \u5FC5\u987B\u662F\u5BF9\u8C61\u3002`);
2633
+ throw badRequest(`input[${i}] must be an object.`);
2605
2634
  }
2606
2635
  const part = item;
2607
2636
  if (part.type === "text") {
2608
2637
  if (typeof part.text !== "string" || part.text.length === 0) {
2609
- throw badRequest(`input[${i}].text \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
2638
+ throw badRequest(`input[${i}].text must be a non-empty string.`);
2610
2639
  }
2611
2640
  return userText(part.text);
2612
2641
  }
2613
2642
  if (part.type === "image_url") {
2614
2643
  const url = part.imageUrl;
2615
2644
  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`);
2645
+ throw badRequest(`input[${i}].imageUrl only supports data: or http(s) URLs.`);
2617
2646
  }
2618
2647
  return imageUrlMessage(url);
2619
2648
  }
2620
- throw badRequest(`input[${i}].type \u5FC5\u987B\u662F text / image_url \u4E4B\u4E00\u3002`);
2649
+ throw badRequest(`input[${i}].type must be one of text / image_url.`);
2621
2650
  });
2622
2651
  }
2623
2652
  function agentSessionsRoutes(deps2) {
@@ -2638,8 +2667,10 @@ function agentSessionsRoutes(deps2) {
2638
2667
  const body = await readJson(c);
2639
2668
  const modelId = optionalString(body, "modelId", { minLen: 1, label: "modelId" });
2640
2669
  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");
2670
+ if (modelId === void 0 !== (provider === void 0)) {
2671
+ throw badRequest(
2672
+ "modelId and provider must be given together as a model reference pair: specify both, or neither to use the Project's default model."
2673
+ );
2643
2674
  }
2644
2675
  const approvalMode = optionalEnum(body, "approvalMode", APPROVAL_MODES);
2645
2676
  let workspace = optionalString(body, "workspace", { minLen: 1, label: "workspace" });
@@ -2664,12 +2695,20 @@ function sessionsRoutes(deps2) {
2664
2695
  const sessionId = c.req.param("sessionId");
2665
2696
  const row = sessionId ? deps2.sessionsRepo.findById(sessionId) : null;
2666
2697
  if (!row) {
2667
- throw new HttpError(404, "session_not_found", "Session \u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE\u3002");
2698
+ throw new HttpError(
2699
+ 404,
2700
+ "session_not_found",
2701
+ "Session does not exist or you do not have access."
2702
+ );
2668
2703
  }
2669
2704
  try {
2670
2705
  deps2.projectService.requireProjectAccess(c.var.user.userId, row.projectId);
2671
2706
  } catch {
2672
- throw new HttpError(404, "session_not_found", "Session \u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE\u3002");
2707
+ throw new HttpError(
2708
+ 404,
2709
+ "session_not_found",
2710
+ "Session does not exist or you do not have access."
2711
+ );
2673
2712
  }
2674
2713
  return row;
2675
2714
  };
@@ -2688,15 +2727,23 @@ function sessionsRoutes(deps2) {
2688
2727
  let title;
2689
2728
  if (titleRaw !== void 0) {
2690
2729
  if (typeof titleRaw !== "string") {
2691
- throw new HttpError(400, "invalid_title", "title \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002");
2730
+ throw new HttpError(400, "invalid_title", "title must be a string.");
2692
2731
  }
2693
2732
  title = titleRaw.trim();
2694
2733
  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`);
2734
+ throw new HttpError(
2735
+ 400,
2736
+ "invalid_title",
2737
+ `title must be 1\u2013${SESSION_TITLE_MAX} characters.`
2738
+ );
2696
2739
  }
2697
2740
  }
2698
2741
  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");
2742
+ throw new HttpError(
2743
+ 400,
2744
+ "no_update",
2745
+ "No updatable field provided (approvalMode / archived / title)."
2746
+ );
2700
2747
  }
2701
2748
  let updated = { ...row };
2702
2749
  if (title !== void 0) {
@@ -2742,7 +2789,7 @@ function sessionsRoutes(deps2) {
2742
2789
  const row = resolveSession(c);
2743
2790
  const fileName = c.req.param("fileName") ?? "";
2744
2791
  if (!/^[A-Za-z0-9._-]+$/.test(fileName) || fileName.includes("..")) {
2745
- throw new HttpError(404, "file_not_found", "\u6587\u4EF6\u4E0D\u5B58\u5728\u3002");
2792
+ throw new HttpError(404, "file_not_found", "File does not exist.");
2746
2793
  }
2747
2794
  const filePath = path6.join(
2748
2795
  scratchpadDir(deps2.config.root, row.projectId, row.agentId),
@@ -2753,7 +2800,7 @@ function sessionsRoutes(deps2) {
2753
2800
  try {
2754
2801
  bytes = await fs7.readFile(filePath);
2755
2802
  } catch {
2756
- throw new HttpError(404, "file_not_found", "\u6587\u4EF6\u4E0D\u5B58\u5728\u3002");
2803
+ throw new HttpError(404, "file_not_found", "File does not exist.");
2757
2804
  }
2758
2805
  const MIME_BY_EXT = {
2759
2806
  ".png": "image/png",
@@ -2802,7 +2849,11 @@ function sessionsRoutes(deps2) {
2802
2849
  const decision = requireEnum(body, "decision", ["allow", "deny"]);
2803
2850
  const ok = deps2.manager.decideApproval(row.sessionId, pathParam(c, "toolCallId"), decision);
2804
2851
  if (!ok) {
2805
- throw new HttpError(404, "approval_not_found", "\u8BE5\u5BA1\u6279\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u51B3\u5B9A\u3002");
2852
+ throw new HttpError(
2853
+ 404,
2854
+ "approval_not_found",
2855
+ "Approval does not exist or has already been decided."
2856
+ );
2806
2857
  }
2807
2858
  return c.body(null, 204);
2808
2859
  });
@@ -2825,18 +2876,22 @@ function sessionsRoutes(deps2) {
2825
2876
  const row = resolveSession(c);
2826
2877
  const rel = c.req.query("path") ?? "";
2827
2878
  const download = c.req.query("download") === "1";
2879
+ const preview = !download && c.req.query("preview") === "1";
2828
2880
  const { data, fileName, contentType, scriptable } = await deps2.workspaceFiles.read(
2829
2881
  row.workspace,
2830
2882
  rel
2831
2883
  );
2832
2884
  const disposition = download ? "attachment" : "inline";
2833
- const effectiveType = !download && scriptable ? "text/plain; charset=utf-8" : contentType;
2885
+ const effectiveType = !download && scriptable && !preview ? "text/plain; charset=utf-8" : contentType;
2834
2886
  return new Response(new Uint8Array(data), {
2835
2887
  status: 200,
2836
2888
  headers: {
2837
2889
  "Content-Type": effectiveType,
2838
2890
  "Content-Disposition": `${disposition}; filename*=UTF-8''${encodeURIComponent(fileName)}`,
2839
- "X-Content-Type-Options": "nosniff"
2891
+ "X-Content-Type-Options": "nosniff",
2892
+ ...preview && scriptable ? {
2893
+ "Content-Security-Policy": "sandbox allow-scripts allow-popups allow-modals allow-forms"
2894
+ } : {}
2840
2895
  }
2841
2896
  });
2842
2897
  });
@@ -2846,7 +2901,7 @@ function sessionsRoutes(deps2) {
2846
2901
  const paths = body.paths;
2847
2902
  if (!Array.isArray(paths) || paths.length > STAT_MAX_PATHS || !paths.every((p) => typeof p === "string" && p.length <= STAT_MAX_PATH_LEN)) {
2848
2903
  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`
2904
+ `paths must be an array of strings (\u2264${STAT_MAX_PATHS} items, each \u2264${STAT_MAX_PATH_LEN} characters).`
2850
2905
  );
2851
2906
  }
2852
2907
  const existing = await deps2.workspaceFiles.statExisting(row.workspace, paths);
@@ -2857,11 +2912,11 @@ function sessionsRoutes(deps2) {
2857
2912
  const rel = c.req.query("path") ?? "";
2858
2913
  const body = await readJson(c);
2859
2914
  if (typeof body.dataBase64 !== "string") {
2860
- throw badRequest("dataBase64 \u5FC5\u987B\u662F base64 \u5B57\u7B26\u4E32\u3002");
2915
+ throw badRequest("dataBase64 must be a base64 string.");
2861
2916
  }
2862
2917
  const data = Buffer.from(body.dataBase64, "base64");
2863
2918
  if (data.length > MAX_UPLOAD_BYTES) {
2864
- throw new HttpError(413, "file_too_large", "\u4E0A\u4F20\u6587\u4EF6\u8D85\u8FC7 14MB \u4E0A\u9650\u3002");
2919
+ throw new HttpError(413, "file_too_large", "Uploaded file exceeds the 14MB limit.");
2865
2920
  }
2866
2921
  await deps2.workspaceFiles.write(row.workspace, rel, data);
2867
2922
  return c.body(null, 204);
@@ -2898,8 +2953,8 @@ function sessionsRoutes(deps2) {
2898
2953
 
2899
2954
  // src/runtime/channel.ts
2900
2955
  import { randomUUID } from "crypto";
2901
- var DEFAULT_MAX_COUNT = 1e3;
2902
- var DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
2956
+ var DEFAULT_MAX_COUNT = 1e4;
2957
+ var DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
2903
2958
  var Channel = class {
2904
2959
  /** Channel epoch: generated at instance creation, prefixed onto event ids (necessarily changes after recycle/recreate or restart). */
2905
2960
  epoch = randomUUID().slice(0, 8);
@@ -3169,12 +3224,16 @@ import path7 from "path";
3169
3224
  var TOOL_NAMES_MAX = 1e3;
3170
3225
  var ORIGIN_CTX_MAX = 200;
3171
3226
  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" },
3227
+ failed: { code: "llm_failed", kind: "unexpected", text: "LLM request failed (not retryable)." },
3228
+ timeout: {
3229
+ code: "llm_timeout",
3230
+ kind: "expected",
3231
+ text: "LLM request timed out (the engine reconnects and retries)."
3232
+ },
3174
3233
  malformed: {
3175
3234
  code: "llm_malformed",
3176
3235
  kind: "expected",
3177
- text: "LLM \u54CD\u5E94\u65E0\u6CD5\u89E3\u6790\uFF08\u5F15\u64CE\u91CD\u8FDE\u91CD\u8BD5\uFF09\u3002"
3236
+ text: "LLM response could not be parsed (the engine reconnects and retries)."
3178
3237
  }
3179
3238
  };
3180
3239
  function isLlmFailure(s) {
@@ -3191,7 +3250,7 @@ function originKey(msg) {
3191
3250
  return origin && origin.length > 0 ? origin[origin.length - 1] : "";
3192
3251
  }
3193
3252
  function toolFailureText(output) {
3194
- if (!output) return "\u5DE5\u5177\u6267\u884C\u5931\u8D25\uFF08\u65E0\u8F93\u51FA\uFF09\u3002";
3253
+ if (!output) return "Tool execution failed (no output).";
3195
3254
  if (output.length <= MESSAGE_MAX) return output;
3196
3255
  return `\u2026${output.slice(output.length - (MESSAGE_MAX - 1))}`;
3197
3256
  }
@@ -3331,9 +3390,9 @@ var StreamErrorWatcher = class {
3331
3390
  // src/runtime/session-manager.ts
3332
3391
  function compactUnavailable(why) {
3333
3392
  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"
3393
+ unsupported: "This Agent does not have context compaction configured.",
3394
+ empty: "The current context has nothing to compact (no completed conversation turns yet).",
3395
+ just_compacted: "The context was just compacted and there is no new conversation since; no need to compact again."
3337
3396
  };
3338
3397
  return new HttpError(409, "nothing_to_compact", messages[why]);
3339
3398
  }
@@ -3364,7 +3423,7 @@ function createCoreSessionLoader(root) {
3364
3423
  throw new HttpError(
3365
3424
  409,
3366
3425
  "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`
3426
+ `This Session's Workspace no longer exists: ${row.workspace}, so it cannot continue. Create a new Session.`
3368
3427
  );
3369
3428
  }
3370
3429
  try {
@@ -3391,6 +3450,7 @@ function toUnrecoverableError(err) {
3391
3450
  var ENTRY_IDLE_MS = 30 * 60 * 1e3;
3392
3451
  var ENTRY_SWEEP_INTERVAL_MS = 60 * 1e3;
3393
3452
  var TITLE_EXCERPT_LIMIT = 4e3;
3453
+ var EARLY_TITLE_BODY_CHARS = 1e3;
3394
3454
  function agentKey(projectId, agentId) {
3395
3455
  return `${projectId}\0${agentId}`;
3396
3456
  }
@@ -3447,6 +3507,8 @@ var SessionManager = class {
3447
3507
  deletingAgents = /* @__PURE__ */ new Set();
3448
3508
  /** Sessions currently being deleted (guards against the entry/Trace file being rebuilt and reviving it inside the deletion race window). */
3449
3509
  deletingSessions = /* @__PURE__ */ new Set();
3510
+ /** Per-Agent config generation (key = agentKey), bumped by invalidateAgentRuntimes on vault updates. */
3511
+ agentGenerations = /* @__PURE__ */ new Map();
3450
3512
  sweepTimer;
3451
3513
  // —— Query surface (used by Session listing / Agent active-count / SSE subscription replay) ——
3452
3514
  statusOf(sessionId) {
@@ -3479,9 +3541,23 @@ var SessionManager = class {
3479
3541
  approvals: new ApprovalRegistry(),
3480
3542
  abort: null,
3481
3543
  running: null,
3544
+ generation: this.generationOf(row.projectId, row.agentId),
3482
3545
  lastActivityMs: Date.now()
3483
3546
  });
3484
3547
  }
3548
+ /**
3549
+ * After an Agent's vault is updated: bump the Agent's config generation so every
3550
+ * runtime built before the update is discarded on its next idle access and
3551
+ * re-resumed via the loader — resume re-reads agent_state/.vault.toml, so the next
3552
+ * Task on any of this Agent's Sessions runs with the new values (history is
3553
+ * preserved through the Trace). A Task already in flight is neither aborted nor
3554
+ * hot-swapped: it keeps the values it started with, and its entry is rebuilt on
3555
+ * the first access after it returns to idle (see ensureEntry).
3556
+ */
3557
+ invalidateAgentRuntimes(projectId, agentId) {
3558
+ const key = agentKey(projectId, agentId);
3559
+ this.agentGenerations.set(key, this.generationOf(projectId, agentId) + 1);
3560
+ }
3485
3561
  // —— Task / compaction drive ——
3486
3562
  /**
3487
3563
  * Start a Task: get-or-load → 409
@@ -3657,38 +3733,70 @@ var SessionManager = class {
3657
3733
  // —— Internal ——
3658
3734
  assertOpen() {
3659
3735
  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");
3736
+ throw new HttpError(
3737
+ 503,
3738
+ "shutting_down",
3739
+ "Server is shutting down; not accepting new Tasks."
3740
+ );
3661
3741
  }
3662
3742
  }
3663
3743
  /** The Agent owning this Session is being deleted → 409 (guards against directory recreation inside the deletion race window). */
3664
3744
  assertAgentNotDeleting(sessionId) {
3665
3745
  const row = this.deps.sessions.findById(sessionId);
3666
3746
  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");
3747
+ throw new HttpError(
3748
+ 409,
3749
+ "agent_deleting",
3750
+ "This Agent is being deleted; not accepting new Tasks."
3751
+ );
3668
3752
  }
3669
3753
  }
3670
3754
  /** This Session is being deleted → 409 (guards against the entry/Trace being rebuilt and reviving it inside the deletion race window). */
3671
3755
  assertSessionNotDeleting(sessionId) {
3672
3756
  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");
3757
+ throw new HttpError(
3758
+ 409,
3759
+ "session_deleting",
3760
+ "This Session is being deleted; not accepting new Tasks."
3761
+ );
3674
3762
  }
3675
3763
  }
3676
3764
  assertIdle(entry) {
3677
3765
  if (entry.status === "running") {
3678
- throw new HttpError(409, "task_in_progress", "\u8BE5 Session \u5DF2\u6709\u8FDB\u884C\u4E2D\u7684 Task\u3002");
3766
+ throw new HttpError(409, "task_in_progress", "This Session already has a Task in progress.");
3679
3767
  }
3680
3768
  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");
3769
+ throw new HttpError(
3770
+ 409,
3771
+ "compacting",
3772
+ "This Session is compacting its context; not accepting new input."
3773
+ );
3682
3774
  }
3683
3775
  }
3776
+ generationOf(projectId, agentId) {
3777
+ return this.agentGenerations.get(agentKey(projectId, agentId)) ?? 0;
3778
+ }
3684
3779
  /** 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
3780
  async ensureEntry(sessionId) {
3686
3781
  const existing = this.entries.get(sessionId);
3687
- if (existing) return existing;
3782
+ if (existing) {
3783
+ if (existing.generation === this.generationOf(existing.projectId, existing.agentId)) {
3784
+ return existing;
3785
+ }
3786
+ if (existing.status !== "idle" || existing.running !== null || existing.approvals.size !== 0) {
3787
+ return existing;
3788
+ }
3789
+ this.entries.delete(sessionId);
3790
+ }
3688
3791
  const row = this.deps.sessions.findById(sessionId);
3689
3792
  if (!row) {
3690
- throw new HttpError(404, "session_not_found", "Session \u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE\u3002");
3793
+ throw new HttpError(
3794
+ 404,
3795
+ "session_not_found",
3796
+ "Session does not exist or you do not have access."
3797
+ );
3691
3798
  }
3799
+ const generation = this.generationOf(row.projectId, row.agentId);
3692
3800
  const session = await this.deps.loader.load(row);
3693
3801
  this.assertSessionNotDeleting(row.sessionId);
3694
3802
  this.assertAgentNotDeleting(row.sessionId);
@@ -3708,6 +3816,7 @@ var SessionManager = class {
3708
3816
  approvals: new ApprovalRegistry(),
3709
3817
  abort: null,
3710
3818
  running: null,
3819
+ generation,
3711
3820
  lastActivityMs: Date.now()
3712
3821
  };
3713
3822
  this.entries.set(currentId, entry);
@@ -3720,6 +3829,8 @@ var SessionManager = class {
3720
3829
  * a title): it collects model text for automatic title generation.
3721
3830
  */
3722
3831
  async drive(entry, gen, titleSource) {
3832
+ let earlyTitleFired = false;
3833
+ let mainBodyChars = 0;
3723
3834
  const ctx = {
3724
3835
  projectId: entry.projectId,
3725
3836
  agentId: entry.agentId,
@@ -3755,7 +3866,7 @@ var SessionManager = class {
3755
3866
  }
3756
3867
  } catch (err) {
3757
3868
  this.log(
3758
- `[subagent] \u5B50\u4F1A\u8BDD\u767B\u8BB0\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`
3869
+ `[subagent] Failed to register child session: ${err instanceof Error ? err.message : String(err)}`
3759
3870
  );
3760
3871
  this.deps.errors?.record({
3761
3872
  source: "subagent",
@@ -3771,18 +3882,30 @@ var SessionManager = class {
3771
3882
  child.assistantExcerpt += (child.assistantExcerpt ? "\n" : "") + nested.text;
3772
3883
  }
3773
3884
  }
3885
+ if (!earlyTitleFired && titleSource?.userExcerpt.trim()) {
3886
+ const p = msg.payload;
3887
+ if ((!msg.origin || msg.origin.length === 0) && msg.type === "model_msg" && p.type === "text" && p.role === "assistant" && p.text) {
3888
+ mainBodyChars += p.text.length;
3889
+ if (mainBodyChars >= EARLY_TITLE_BODY_CHARS) {
3890
+ earlyTitleFired = true;
3891
+ this.deps.titles?.maybeGenerate(ctx, entry.session, {
3892
+ fallbackText: titleSource.userExcerpt
3893
+ });
3894
+ }
3895
+ }
3896
+ }
3774
3897
  this.deps.channels.get(entry.sessionId).publish(msg);
3775
3898
  watcher?.observe(msg);
3776
3899
  try {
3777
3900
  await this.deps.recorder.record(ctx, msg);
3778
3901
  } catch (err) {
3779
- this.log(`[usage] \u843D\u5E93\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
3902
+ this.log(`[usage] Insert failed: ${err instanceof Error ? err.message : String(err)}`);
3780
3903
  this.deps.errors?.record({ source: "usage", err, ctx, code: "usage_insert_failed" });
3781
3904
  }
3782
3905
  }
3783
3906
  } catch (err) {
3784
3907
  this.log(
3785
- `[session] \u8FD0\u884C\u5F02\u5E38: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
3908
+ `[session] Run failed: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
3786
3909
  );
3787
3910
  this.deps.errors?.record({ source: "session", err, ctx, code: "session_run_failed" });
3788
3911
  } finally {
@@ -3913,7 +4036,7 @@ var Scheduler = class {
3913
4036
  now;
3914
4037
  intervalMs;
3915
4038
  timer = null;
3916
- /** key = `${projectId}${agentId}${name}` */
4039
+ /** key = `${projectId}\0${agentId}\0${name}` */
3917
4040
  pending = /* @__PURE__ */ new Map();
3918
4041
  ticking = false;
3919
4042
  /** Start: run one reconcile immediately (startup semantics: no backfill), then enter the periodic tick. */
@@ -4010,7 +4133,7 @@ var Scheduler = class {
4010
4133
  if (!file.parsed.ok) {
4011
4134
  this.deps.errors.record({
4012
4135
  source: "schedule",
4013
- err: new Error(`\u5B9A\u65F6\u4EFB\u52A1\u6587\u4EF6\u975E\u6CD5\uFF1A${file.name}.toml\u2014\u2014${file.parsed.error}`),
4136
+ err: new Error(`Invalid schedule file: ${file.name}.toml \u2014 ${file.parsed.error}`),
4014
4137
  code: "schedule_invalid_file",
4015
4138
  ctx: { projectId, agentId }
4016
4139
  });
@@ -4021,7 +4144,7 @@ var Scheduler = class {
4021
4144
  if (refError !== null) {
4022
4145
  this.deps.errors.record({
4023
4146
  source: "schedule",
4024
- err: new Error(`\u5B9A\u65F6\u4EFB\u52A1\u6587\u4EF6\u975E\u6CD5\uFF1A${file.name}.toml\u2014\u2014${refError}`),
4147
+ err: new Error(`Invalid schedule file: ${file.name}.toml \u2014 ${refError}`),
4025
4148
  code: "schedule_invalid_file",
4026
4149
  ctx: { projectId, agentId }
4027
4150
  });
@@ -4081,7 +4204,9 @@ var Scheduler = class {
4081
4204
  if (!row || row.projectId !== projectId || row.agentId !== agentId) {
4082
4205
  this.deps.errors.record({
4083
4206
  source: "schedule",
4084
- err: new Error(`\u5B9A\u65F6\u4EFB\u52A1 ${def.name} \u7ED1\u5B9A\u7684 Session \u4E0D\u5B58\u5728\uFF1A${def.sessionId}`),
4207
+ err: new Error(
4208
+ `Schedule ${def.name} is bound to a Session that does not exist: ${def.sessionId}`
4209
+ ),
4085
4210
  code: "schedule_session_missing",
4086
4211
  ctx: { projectId, agentId, sessionId: def.sessionId }
4087
4212
  });
@@ -4179,7 +4304,12 @@ var Scheduler = class {
4179
4304
  };
4180
4305
 
4181
4306
  // src/runtime/title-generator.ts
4182
- import { emptyTokenCounts, sanitizeTitle, tokenUsage } from "@prismshadow/penguin-core";
4307
+ import {
4308
+ emptyTokenCounts,
4309
+ sanitizeTitle,
4310
+ stripConversationMarkers,
4311
+ tokenUsage
4312
+ } from "@prismshadow/penguin-core";
4183
4313
  var TitleGenerator = class {
4184
4314
  constructor(deps2) {
4185
4315
  this.deps = deps2;
@@ -4195,7 +4325,7 @@ var TitleGenerator = class {
4195
4325
  if (this.inflight.has(ctx.sessionId)) return;
4196
4326
  this.inflight.add(ctx.sessionId);
4197
4327
  void this.generate(ctx, session, req).catch((err) => {
4198
- this.log(`[title] \u751F\u6210\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
4328
+ this.log(`[title] Generation failed: ${err instanceof Error ? err.message : String(err)}`);
4199
4329
  this.deps.errors?.record({ source: "title", err, ctx, code: "title_failed" });
4200
4330
  }).finally(() => {
4201
4331
  this.inflight.delete(ctx.sessionId);
@@ -4212,7 +4342,9 @@ var TitleGenerator = class {
4212
4342
  try {
4213
4343
  await this.deps.recorder.record(ctx, tokenUsage(emptyTokenCounts(), res.usage));
4214
4344
  } catch (err) {
4215
- this.log(`[title] \u7528\u91CF\u843D\u5E93\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
4345
+ this.log(
4346
+ `[title] Usage insert failed: ${err instanceof Error ? err.message : String(err)}`
4347
+ );
4216
4348
  this.deps.errors?.record({
4217
4349
  source: "title",
4218
4350
  err,
@@ -4222,7 +4354,7 @@ var TitleGenerator = class {
4222
4354
  }
4223
4355
  }
4224
4356
  } catch (err) {
4225
- this.log(`[title] \u6A21\u578B\u8BF7\u6C42\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
4357
+ this.log(`[title] Model request failed: ${err instanceof Error ? err.message : String(err)}`);
4226
4358
  this.deps.errors?.record({ source: "title", err, ctx, code: "title_llm_failed" });
4227
4359
  }
4228
4360
  const finalTitle = title ?? fallbackTitle(req.fallbackText);
@@ -4237,7 +4369,7 @@ var TitleGenerator = class {
4237
4369
  }
4238
4370
  };
4239
4371
  function fallbackTitle(text) {
4240
- const firstLine = text.split("\n").find((l) => l.trim().length > 0);
4372
+ const firstLine = stripConversationMarkers(text).split("\n").find((l) => l.trim().length > 0);
4241
4373
  if (!firstLine) return null;
4242
4374
  return sanitizeTitle(firstLine) ?? firstLine.trim().slice(0, 30);
4243
4375
  }
@@ -4317,7 +4449,7 @@ var SEMANTIC_ID_PATTERN = /^[a-z][a-z0-9_]{1,63}$/;
4317
4449
  var USERNAME_PATTERN = /^[a-z][a-z0-9_]{1,31}$/;
4318
4450
  var PROJECT_SUFFIX_PATTERN = /^[a-z0-9_]+$/;
4319
4451
  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";
4452
+ var SEMANTIC_ID_RULE = "starts with a lowercase letter; lowercase letters, digits and underscores only";
4321
4453
 
4322
4454
  // src/services/admin-service.ts
4323
4455
  var AdminService = class {
@@ -4332,13 +4464,17 @@ var AdminService = class {
4332
4464
  }
4333
4465
  async createUser(userId, password) {
4334
4466
  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`);
4467
+ throw new HttpError(
4468
+ 400,
4469
+ "invalid_user_id",
4470
+ `Username must be 2\u201332 characters: ${SEMANTIC_ID_RULE}.`
4471
+ );
4336
4472
  }
4337
4473
  if (password.length < MIN_PASSWORD_LENGTH) {
4338
- throw new HttpError(400, "invalid_password", "\u5BC6\u7801\u81F3\u5C11 8 \u4E2A\u5B57\u7B26\u3002");
4474
+ throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
4339
4475
  }
4340
4476
  if (this.deps.users.findById(userId)) {
4341
- throw new HttpError(409, "user_exists", `\u7528\u6237\u5DF2\u5B58\u5728\uFF1A${userId}\u3002`);
4477
+ throw new HttpError(409, "user_exists", `User already exists: ${userId}.`);
4342
4478
  }
4343
4479
  const user = {
4344
4480
  userId,
@@ -4359,10 +4495,10 @@ var AdminService = class {
4359
4495
  /** Reset another user's password: flags it as initial and clears all their sessions (prompts a password change on next login). */
4360
4496
  async resetPassword(userId, password) {
4361
4497
  if (!this.deps.users.findById(userId)) {
4362
- throw new HttpError(404, "user_not_found", `\u7528\u6237\u4E0D\u5B58\u5728\uFF1A${userId}\u3002`);
4498
+ throw new HttpError(404, "user_not_found", `User does not exist: ${userId}.`);
4363
4499
  }
4364
4500
  if (password.length < MIN_PASSWORD_LENGTH) {
4365
- throw new HttpError(400, "invalid_password", "\u5BC6\u7801\u81F3\u5C11 8 \u4E2A\u5B57\u7B26\u3002");
4501
+ throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
4366
4502
  }
4367
4503
  this.deps.users.updatePassword(userId, await hashPassword(password), true);
4368
4504
  this.deps.authSessions.deleteByUser(userId);
@@ -4371,10 +4507,10 @@ var AdminService = class {
4371
4507
  async deleteUser(userId) {
4372
4508
  const target = this.deps.users.findById(userId);
4373
4509
  if (!target) {
4374
- throw new HttpError(404, "user_not_found", `\u7528\u6237\u4E0D\u5B58\u5728\uFF1A${userId}\u3002`);
4510
+ throw new HttpError(404, "user_not_found", `User does not exist: ${userId}.`);
4375
4511
  }
4376
4512
  if (target.isAdmin) {
4377
- throw new HttpError(409, "cannot_delete_admin", "\u5185\u7F6E\u7BA1\u7406\u5458\u4E0D\u80FD\u5220\u9664\u3002");
4513
+ throw new HttpError(409, "cannot_delete_admin", "The built-in admin cannot be deleted.");
4378
4514
  }
4379
4515
  for (const project of this.deps.projects.listByOwner(userId)) {
4380
4516
  await this.deps.projectService.destroyProject(project.projectId);
@@ -4441,6 +4577,8 @@ function refKey(provider, modelId) {
4441
4577
  function showRef(provider, modelId) {
4442
4578
  return `(provider=${provider}, model_id=${modelId})`;
4443
4579
  }
4580
+ var PROBE_PROMPT = 'ping - reply with the single word "pong" and nothing else. Do not think or explain.\n<think></think>';
4581
+ 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
4582
  var ProjectConfigService = class {
4445
4583
  constructor(root) {
4446
4584
  this.root = root;
@@ -4537,12 +4675,19 @@ var ProjectConfigService = class {
4537
4675
  * as a pair in the request body; sends one minimal request using that model's
4538
4676
  * config (optionally overridden with an unsaved apiKey / baseUrl) — no tools, no
4539
4677
  * 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`
4678
+ * whether the endpoint answers. The model id sent to AgentHub is `modelId`
4541
4679
  * itself (the upstream id verbatim; client_type inference follows it).
4542
4680
  *
4681
+ * A reasoning-heavy model may ignore the disabled thinking level and burn the
4682
+ * whole tiny output cap on thinking (finish_reason=length with no text — AgentHub
4683
+ * raises EmptyResponseError, collapsed to a malformed outcome): the endpoint
4684
+ * demonstrably streamed model output, which is everything a connectivity test
4685
+ * proves, so that case counts as ok too (see probeVerdict).
4686
+ *
4543
4687
  * Never throws: the LLM layer collapses auth/parameter/network errors into an
4544
4688
  * `LLMOutcome`, which is translated here into ok / message. Consumes very few
4545
- * Tokens (single-digit output), and writes no Trace and records no usage.
4689
+ * Tokens (single-digit output; speed mode spends up to its 64-token cap to have a
4690
+ * window worth timing), and writes no Trace and records no usage.
4546
4691
  */
4547
4692
  async testModel(projectId, req) {
4548
4693
  const raw = await this.readRaw(projectId);
@@ -4561,18 +4706,39 @@ var ProjectConfigService = class {
4561
4706
  ...clientType ? { clientType } : {},
4562
4707
  tools: [],
4563
4708
  thinkingLevel: "none",
4564
- maxTokens: 16,
4709
+ // Speed mode pairs a raised cap with a prompt that keeps generating (see
4710
+ // SPEED_PROBE_PROMPT), so the stream lasts long enough for TTFT/TPS to describe
4711
+ // decoding rather than one round trip; the plain connectivity test keeps the
4712
+ // single-digit-token budget.
4713
+ maxTokens: req.speed ? 64 : 16,
4565
4714
  requestTimeoutMs: 2e4
4566
4715
  });
4567
- const gen = llm.streamGenerate({ newMessages: [userText3("ping")] });
4716
+ const gen = llm.streamGenerate({
4717
+ newMessages: [userText3(req.speed ? SPEED_PROBE_PROMPT : PROBE_PROMPT)]
4718
+ });
4719
+ let sawContent = false;
4720
+ let firstContentAt = null;
4721
+ let outputTokens = 0;
4568
4722
  for (; ; ) {
4569
4723
  const step = await gen.next();
4570
4724
  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) };
4725
+ const verdict = probeVerdict(step.value, sawContent);
4726
+ if (!verdict.ok) return verdict;
4727
+ const res = { ok: true, latencyMs: Date.now() - startedAt };
4728
+ if (firstContentAt !== null) {
4729
+ res.ttftMs = firstContentAt - startedAt;
4730
+ const tps = probeTps(outputTokens, Date.now() - firstContentAt);
4731
+ if (tps !== void 0) res.tps = tps;
4732
+ }
4733
+ return res;
4734
+ }
4735
+ if (isProbeContent(step.value)) {
4736
+ sawContent = true;
4737
+ if (firstContentAt === null) firstContentAt = Date.now();
4738
+ }
4739
+ const p = step.value.payload;
4740
+ if (p.type === "token_usage" && typeof p.request?.output === "number") {
4741
+ outputTokens = p.request.output;
4576
4742
  }
4577
4743
  }
4578
4744
  } catch (err) {
@@ -4662,7 +4828,7 @@ var ProjectConfigService = class {
4662
4828
  const key = refKey(entry.provider, entry.modelId);
4663
4829
  if (seen.has(key)) {
4664
4830
  throw badRequest(
4665
- `models \u4E2D\u5B58\u5728\u91CD\u590D\u7684\u6A21\u578B\u5F15\u7528\uFF1A${showRef(entry.provider, entry.modelId)}\u3002`
4831
+ `models contains a duplicate model reference: ${showRef(entry.provider, entry.modelId)}.`
4666
4832
  );
4667
4833
  }
4668
4834
  seen.add(key);
@@ -4712,7 +4878,7 @@ var ProjectConfigService = class {
4712
4878
  if (req.defaultModel !== void 0) {
4713
4879
  if (!seen.has(refKey(req.defaultModel.provider, req.defaultModel.modelId))) {
4714
4880
  throw badRequest(
4715
- `defaultModel \u5FC5\u987B\u5305\u542B\u5728 models \u5185\uFF1A${showRef(req.defaultModel.provider, req.defaultModel.modelId)}\u3002`
4881
+ `defaultModel must be included in models: ${showRef(req.defaultModel.provider, req.defaultModel.modelId)}.`
4716
4882
  );
4717
4883
  }
4718
4884
  defaultModel = req.defaultModel;
@@ -4732,12 +4898,12 @@ var ProjectConfigService = class {
4732
4898
  if (req.visionModel !== void 0) {
4733
4899
  if (!seen.has(refKey(req.visionModel.provider, req.visionModel.modelId))) {
4734
4900
  throw badRequest(
4735
- `visionModel \u5FC5\u987B\u5305\u542B\u5728 models \u5185\uFF1A${showRef(req.visionModel.provider, req.visionModel.modelId)}\u3002`
4901
+ `visionModel must be included in models: ${showRef(req.visionModel.provider, req.visionModel.modelId)}.`
4736
4902
  );
4737
4903
  }
4738
4904
  if (targetOf(req.visionModel)?.vision === false) {
4739
4905
  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`
4906
+ `visionModel must not point to a model annotated as not supporting images: ${showRef(req.visionModel.provider, req.visionModel.modelId)}.`
4741
4907
  );
4742
4908
  }
4743
4909
  visionModel = req.visionModel;
@@ -4767,6 +4933,24 @@ var ProjectConfigService = class {
4767
4933
  return this.getModels(projectId);
4768
4934
  }
4769
4935
  };
4936
+ function isProbeContent(msg) {
4937
+ const p = msg.payload;
4938
+ if (p.type === "partial_thinking" || p.type === "thinking") return Boolean(p.thinking);
4939
+ if (p.type === "partial_text" || p.type === "text") return Boolean(p.text);
4940
+ return false;
4941
+ }
4942
+ function probeVerdict(outcome, sawContent) {
4943
+ if (outcome.status === "completed") return { ok: true };
4944
+ if (outcome.status === "malformed" && sawContent) return { ok: true };
4945
+ const detail = "message" in outcome && outcome.message ? outcome.message : outcome.status;
4946
+ return { ok: false, message: String(detail).slice(0, 300) };
4947
+ }
4948
+ var PROBE_TPS_MIN_TOKENS = 16;
4949
+ var PROBE_TPS_MIN_WINDOW_MS = 100;
4950
+ function probeTps(outputTokens, windowMs) {
4951
+ if (outputTokens < PROBE_TPS_MIN_TOKENS || windowMs <= PROBE_TPS_MIN_WINDOW_MS) return void 0;
4952
+ return Math.round(outputTokens / (windowMs / 1e3) * 10) / 10;
4953
+ }
4770
4954
 
4771
4955
  // src/services/agent-config-service.ts
4772
4956
  var THINKING_LEVELS = ["none", "low", "medium", "high", "xhigh"];
@@ -4790,7 +4974,7 @@ var AgentConfigService = class {
4790
4974
  }
4791
4975
  async requireExists(projectId, agentId) {
4792
4976
  if (!await this.exists(projectId, agentId)) {
4793
- throw new HttpError(404, "agent_not_found", "Agent \u4E0D\u5B58\u5728\u3002");
4977
+ throw new HttpError(404, "agent_not_found", "Agent does not exist.");
4794
4978
  }
4795
4979
  }
4796
4980
  /**
@@ -4875,8 +5059,8 @@ var AgentConfigService = class {
4875
5059
  const cfg = config2;
4876
5060
  const yamlPath = systemConfigPath(this.root, projectId, agentId);
4877
5061
  const doc = parseDocument(await fs10.readFile(yamlPath, "utf8"));
4878
- const setIfProvided = (path16, value) => {
4879
- if (value !== void 0) doc.setIn(path16, value);
5062
+ const setIfProvided = (path17, value) => {
5063
+ if (value !== void 0) doc.setIn(path17, value);
4880
5064
  };
4881
5065
  setIfProvided(["name"], optionalString(cfg, "name", { maxLen: 100, label: "name" }));
4882
5066
  setIfProvided(
@@ -4952,23 +5136,25 @@ var AgentConfigService = class {
4952
5136
  for (const entry of req.entries) {
4953
5137
  if (!isValidVaultKey(entry.key)) {
4954
5138
  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`
5139
+ `Invalid vault key name: ${entry.key} (letters, digits and underscores only, and must not start with a digit).`
4956
5140
  );
4957
5141
  }
4958
5142
  if (seen.has(entry.key)) {
4959
- throw badRequest(`entries \u4E2D\u5B58\u5728\u91CD\u590D\u7684\u952E\u540D\uFF1A${entry.key}\u3002`);
5143
+ throw badRequest(`entries contains a duplicate key: ${entry.key}.`);
4960
5144
  }
4961
5145
  seen.add(entry.key);
4962
5146
  const prevValue = prev[entry.key];
4963
5147
  if (entry.value !== void 0) {
4964
5148
  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`);
5149
+ throw badRequest(
5150
+ `Vault value too long: ${entry.key} (limit ${VAULT_VALUE_MAX_LENGTH} characters).`
5151
+ );
4966
5152
  }
4967
5153
  nextVault[entry.key] = entry.value;
4968
5154
  } else if (prevValue !== void 0) {
4969
5155
  nextVault[entry.key] = prevValue;
4970
5156
  } else {
4971
- throw badRequest(`\u65B0\u589E\u952E ${entry.key} \u5FC5\u987B\u63D0\u4F9B value\u3002`);
5157
+ throw badRequest(`New key ${entry.key} must provide a value.`);
4972
5158
  }
4973
5159
  }
4974
5160
  await saveAgentVault(this.root, projectId, agentId, nextVault);
@@ -4976,20 +5162,20 @@ var AgentConfigService = class {
4976
5162
  }
4977
5163
  };
4978
5164
  function validateToolsBuiltin(value) {
4979
- if (!Array.isArray(value)) throw badRequest("toolsBuiltin \u5FC5\u987B\u662F\u6570\u7EC4\u3002");
5165
+ if (!Array.isArray(value)) throw badRequest("toolsBuiltin must be an array.");
4980
5166
  return value.map((item, i) => {
4981
5167
  const t = asRecord(item);
4982
5168
  if (typeof t.name !== "string" || t.name.length === 0) {
4983
- throw badRequest(`toolsBuiltin[${i}].name \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
5169
+ throw badRequest(`toolsBuiltin[${i}].name must be a non-empty string.`);
4984
5170
  }
4985
5171
  if (typeof t.description !== "string") {
4986
- throw badRequest(`toolsBuiltin[${i}].description \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
5172
+ throw badRequest(`toolsBuiltin[${i}].description must be a string.`);
4987
5173
  }
4988
5174
  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`);
5175
+ throw badRequest(`toolsBuiltin[${i}].permission must be one of r / rw.`);
4990
5176
  }
4991
5177
  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`);
5178
+ throw badRequest(`toolsBuiltin[${i}].forModel must be one of vision / text-only.`);
4993
5179
  }
4994
5180
  optionalNumber(t, "timeoutMs", {
4995
5181
  integer: true,
@@ -5005,14 +5191,14 @@ function validateToolsBuiltin(value) {
5005
5191
  });
5006
5192
  }
5007
5193
  function validateMcpServers(value) {
5008
- if (!Array.isArray(value)) throw badRequest("mcpServers \u5FC5\u987B\u662F\u6570\u7EC4\u3002");
5194
+ if (!Array.isArray(value)) throw badRequest("mcpServers must be an array.");
5009
5195
  return value.map((item, i) => {
5010
5196
  const s = asRecord(item);
5011
5197
  if (typeof s.name !== "string" || s.name.length === 0) {
5012
- throw badRequest(`mcpServers[${i}].name \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002`);
5198
+ throw badRequest(`mcpServers[${i}].name must be a non-empty string.`);
5013
5199
  }
5014
5200
  if (s.config === null || typeof s.config !== "object" || Array.isArray(s.config)) {
5015
- throw badRequest(`mcpServers[${i}].config \u5FC5\u987B\u662F\u5BF9\u8C61\u3002`);
5201
+ throw badRequest(`mcpServers[${i}].config must be an object.`);
5016
5202
  }
5017
5203
  return s;
5018
5204
  });
@@ -5020,6 +5206,7 @@ function validateMcpServers(value) {
5020
5206
 
5021
5207
  // src/services/agent-service.ts
5022
5208
  import fs11 from "fs/promises";
5209
+ import path10 from "path";
5023
5210
  import {
5024
5211
  agentDir,
5025
5212
  agentsDir as agentsDir2,
@@ -5029,6 +5216,7 @@ import {
5029
5216
  isValidId as isValidId3,
5030
5217
  loadAgentVault as loadAgentVault2,
5031
5218
  scheduleDir as scheduleDir2,
5219
+ skillsDir as skillsDir2,
5032
5220
  systemConfigPath as systemConfigPath2
5033
5221
  } from "@prismshadow/penguin-core";
5034
5222
  var AgentService = class {
@@ -5068,11 +5256,12 @@ var AgentService = class {
5068
5256
  );
5069
5257
  return Promise.all(
5070
5258
  sorted.map(async (row) => {
5071
- const [meta, updatedAt, vaultKeyCount, scheduleCount] = await Promise.all([
5259
+ const [meta, updatedAt, vaultKeyCount, scheduleCount, skillCount] = await Promise.all([
5072
5260
  this.agentConfig.readCardMeta(projectId, row.agentId),
5073
5261
  this.configUpdatedAt(projectId, row.agentId),
5074
5262
  this.vaultKeyCount(projectId, row.agentId),
5075
- this.scheduleCount(projectId, row.agentId)
5263
+ this.scheduleCount(projectId, row.agentId),
5264
+ this.skillCount(projectId, row.agentId)
5076
5265
  ]);
5077
5266
  return {
5078
5267
  agentId: row.agentId,
@@ -5080,7 +5269,8 @@ var AgentService = class {
5080
5269
  createdAt: row.createdAt,
5081
5270
  ...updatedAt !== void 0 ? { updatedAt } : {},
5082
5271
  vaultKeyCount,
5083
- scheduleCount
5272
+ scheduleCount,
5273
+ skillCount
5084
5274
  };
5085
5275
  })
5086
5276
  );
@@ -5102,6 +5292,27 @@ var AgentService = class {
5102
5292
  return 0;
5103
5293
  }
5104
5294
  }
5295
+ /** Number of installed Skills: count of skills/<name>/ directories containing a SKILL.md (0 if the directory doesn't exist). */
5296
+ async skillCount(projectId, agentId) {
5297
+ const base = skillsDir2(this.root, projectId, agentId);
5298
+ let dirents;
5299
+ try {
5300
+ dirents = await fs11.readdir(base, { withFileTypes: true });
5301
+ } catch {
5302
+ return 0;
5303
+ }
5304
+ const present = await Promise.all(
5305
+ dirents.filter((d) => d.isDirectory()).map(async (d) => {
5306
+ try {
5307
+ await fs11.access(path10.join(base, d.name, "SKILL.md"));
5308
+ return true;
5309
+ } catch {
5310
+ return false;
5311
+ }
5312
+ })
5313
+ );
5314
+ return present.filter(Boolean).length;
5315
+ }
5105
5316
  /** Last config modification time: the later of system_config.yaml and AGENTS.md mtime; omitted if neither is readable. */
5106
5317
  async configUpdatedAt(projectId, agentId) {
5107
5318
  const paths = [
@@ -5133,7 +5344,7 @@ var AgentService = class {
5133
5344
  throw new HttpError(
5134
5345
  409,
5135
5346
  "cannot_delete_builtin_agent",
5136
- "\u5185\u7F6E Agent\uFF08default_agent\uFF09\u968F Project \u4F9B\u7ED9\uFF0C\u4E0D\u80FD\u4ECE Web \u5220\u9664\u3002"
5347
+ "Built-in Agents (default_agent) are provisioned with the Project and cannot be deleted from the web."
5137
5348
  );
5138
5349
  }
5139
5350
  await fs11.rm(agentDir(this.root, projectId, agentId), { recursive: true, force: true });
@@ -5147,14 +5358,18 @@ var AgentService = class {
5147
5358
  */
5148
5359
  async createAgent(projectId, agentId, name, description) {
5149
5360
  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`);
5361
+ throw new HttpError(
5362
+ 400,
5363
+ "invalid_agent_id",
5364
+ `Agent id must be 2\u201364 characters: ${SEMANTIC_ID_RULE}.`
5365
+ );
5151
5366
  }
5152
5367
  const taken = this.agents.exists(projectId, agentId) || await fs11.stat(agentDir(this.root, projectId, agentId)).then(
5153
5368
  () => true,
5154
5369
  () => false
5155
5370
  );
5156
5371
  if (taken) {
5157
- throw new HttpError(409, "agent_exists", `Agent id \u5DF2\u88AB\u5360\u7528\uFF1A${agentId}\u3002`);
5372
+ throw new HttpError(409, "agent_exists", `Agent id is already taken: ${agentId}.`);
5158
5373
  }
5159
5374
  const displayName = name ?? agentId;
5160
5375
  await coreCreateAgent({ root: this.root, projectId, agentId });
@@ -5179,14 +5394,16 @@ var AgentService = class {
5179
5394
  toolCount: meta.toolCount,
5180
5395
  version: meta.version,
5181
5396
  vaultKeyCount: 0,
5182
- scheduleCount: 0
5397
+ scheduleCount: 0,
5398
+ // Read the real count: coreCreateAgent seeds the default skill set for default_agent.
5399
+ skillCount: await this.skillCount(projectId, agentId)
5183
5400
  };
5184
5401
  }
5185
5402
  };
5186
5403
 
5187
5404
  // src/services/benchmark-service.ts
5188
5405
  import fs12 from "fs/promises";
5189
- import path10 from "path";
5406
+ import path11 from "path";
5190
5407
  import { parse as parseToml3 } from "smol-toml";
5191
5408
  import { parse as parseYaml2 } from "yaml";
5192
5409
  import { benchmarksDir } from "@prismshadow/penguin-core";
@@ -5288,7 +5505,7 @@ var BenchmarkService = class {
5288
5505
  }
5289
5506
  const benchmarks = [];
5290
5507
  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));
5508
+ benchmarks.push(await this.readBenchmark(path11.join(dir, item.name), item.name));
5292
5509
  }
5293
5510
  return { benchmarks };
5294
5511
  }
@@ -5298,7 +5515,7 @@ var BenchmarkService = class {
5298
5515
  let runs;
5299
5516
  try {
5300
5517
  const config2 = asRecord2(
5301
- parseToml3(await fs12.readFile(path10.join(benchDir, "benchmark_config.toml"), "utf8"))
5518
+ parseToml3(await fs12.readFile(path11.join(benchDir, "benchmark_config.toml"), "utf8"))
5302
5519
  );
5303
5520
  if (typeof config2.title === "string" && config2.title !== "") title = config2.title;
5304
5521
  if (typeof config2.description === "string" && config2.description !== "") {
@@ -5313,7 +5530,7 @@ var BenchmarkService = class {
5313
5530
  let evaluations = [];
5314
5531
  try {
5315
5532
  const scoreboard = asRecord2(
5316
- parseYaml2(await fs12.readFile(path10.join(benchDir, "scoreboard.yaml"), "utf8"))
5533
+ parseYaml2(await fs12.readFile(path11.join(benchDir, "scoreboard.yaml"), "utf8"))
5317
5534
  );
5318
5535
  if (Array.isArray(scoreboard.evaluations)) {
5319
5536
  evaluations = scoreboard.evaluations.map(toEvaluation).filter((e) => e !== null);
@@ -5340,7 +5557,7 @@ var BenchmarkService = class {
5340
5557
  // src/services/snapshot-service.ts
5341
5558
  import { randomBytes as randomBytes4 } from "crypto";
5342
5559
  import fs13 from "fs/promises";
5343
- import path11 from "path";
5560
+ import path12 from "path";
5344
5561
  import * as tar from "tar";
5345
5562
  import { parse as parseYaml3 } from "yaml";
5346
5563
  import {
@@ -5353,7 +5570,7 @@ import {
5353
5570
  } from "@prismshadow/penguin-core";
5354
5571
  var VAULT_BASENAME = ".vault.toml";
5355
5572
  function isVaultEntry(entryPath) {
5356
- return path11.posix.basename(entryPath.replaceAll("\\", "/")) === VAULT_BASENAME;
5573
+ return path12.posix.basename(entryPath.replaceAll("\\", "/")) === VAULT_BASENAME;
5357
5574
  }
5358
5575
  var SnapshotService = class {
5359
5576
  constructor(root) {
@@ -5366,7 +5583,7 @@ var SnapshotService = class {
5366
5583
  try {
5367
5584
  raw = await fs13.readFile(systemConfigPath3(this.root, projectId, agentId), "utf8");
5368
5585
  } catch {
5369
- throw new HttpError(404, "agent_not_found", "Agent \u4E0D\u5B58\u5728\u3002");
5586
+ throw new HttpError(404, "agent_not_found", "Agent does not exist.");
5370
5587
  }
5371
5588
  const parsed = parseYaml3(raw);
5372
5589
  return agentStateVersion2({
@@ -5377,7 +5594,7 @@ var SnapshotService = class {
5377
5594
  async ensureSnapshot(projectId, agentId) {
5378
5595
  const version = await this.currentVersion(projectId, agentId);
5379
5596
  const dir = snapshotsDir(this.root, projectId, agentId);
5380
- const file = path11.join(dir, `v${version}.tar.gz`);
5597
+ const file = path12.join(dir, `v${version}.tar.gz`);
5381
5598
  try {
5382
5599
  await fs13.access(file);
5383
5600
  return { version, file };
@@ -5412,12 +5629,12 @@ var SnapshotService = class {
5412
5629
  async importArchive(projectId, agentId, archive, confirm) {
5413
5630
  const current = await this.currentVersion(projectId, agentId);
5414
5631
  const base = agentDir2(this.root, projectId, agentId);
5415
- const staging = path11.join(base, `.import-${randomBytes4(6).toString("hex")}`);
5632
+ const staging = path12.join(base, `.import-${randomBytes4(6).toString("hex")}`);
5416
5633
  await fs13.mkdir(staging, { recursive: true });
5417
5634
  try {
5418
- const archiveFile = path11.join(staging, "archive.tar.gz");
5635
+ const archiveFile = path12.join(staging, "archive.tar.gz");
5419
5636
  await fs13.writeFile(archiveFile, archive);
5420
- const extractDir = path11.join(staging, "extracted");
5637
+ const extractDir = path12.join(staging, "extracted");
5421
5638
  await fs13.mkdir(extractDir, { recursive: true });
5422
5639
  try {
5423
5640
  await tar.extract({
@@ -5426,21 +5643,21 @@ var SnapshotService = class {
5426
5643
  filter: (p) => !isVaultEntry(p)
5427
5644
  });
5428
5645
  } catch {
5429
- throw badRequest("\u5BFC\u5165\u5931\u8D25\uFF1A\u4E0D\u662F\u5408\u6CD5\u7684 tar.gz \u5FEB\u7167\u5305\u3002");
5646
+ throw badRequest("Import failed: not a valid tar.gz snapshot package.");
5430
5647
  }
5431
- const configPath = path11.join(extractDir, "agent_state", "system_config.yaml");
5648
+ const configPath = path12.join(extractDir, "agent_state", "system_config.yaml");
5432
5649
  let parsed;
5433
5650
  try {
5434
5651
  parsed = parseYaml3(await fs13.readFile(configPath, "utf8"));
5435
5652
  } catch {
5436
- throw badRequest("\u5BFC\u5165\u5931\u8D25\uFF1A\u5305\u5185\u7F3A\u5C11 agent_state/system_config.yaml\u3002");
5653
+ throw badRequest("Import failed: the package is missing agent_state/system_config.yaml.");
5437
5654
  }
5438
5655
  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");
5656
+ throw badRequest("Import failed: the package's system_config.yaml is invalid.");
5440
5657
  }
5441
5658
  const incomingRaw = parsed.version;
5442
5659
  if (incomingRaw !== void 0 && (!Number.isInteger(incomingRaw) || incomingRaw < 1)) {
5443
- throw badRequest("\u5BFC\u5165\u5931\u8D25\uFF1A\u5305\u5185 version \u975E\u6CD5\u3002");
5660
+ throw badRequest("Import failed: the package's version is invalid.");
5444
5661
  }
5445
5662
  const incoming = agentStateVersion2({
5446
5663
  version: typeof incomingRaw === "number" ? incomingRaw : void 0
@@ -5449,21 +5666,21 @@ var SnapshotService = class {
5449
5666
  throw new HttpError(
5450
5667
  409,
5451
5668
  "version_conflict",
5452
- `\u5305\u7248\u672C v${incoming} \u4E0D\u9AD8\u4E8E\u5F53\u524D v${current}\uFF0C\u9700\u8981\u786E\u8BA4\u540E\u5BFC\u5165\u3002`
5669
+ `Package version v${incoming} is not higher than the current v${current}; confirmation is required to import.`
5453
5670
  );
5454
5671
  }
5455
5672
  await this.ensureSnapshot(projectId, agentId);
5456
5673
  const stateDir = agentStateDir2(this.root, projectId, agentId);
5457
- const incomingState = path11.join(extractDir, "agent_state");
5674
+ const incomingState = path12.join(extractDir, "agent_state");
5458
5675
  let vault = null;
5459
5676
  try {
5460
5677
  vault = await fs13.readFile(agentVaultPath(this.root, projectId, agentId));
5461
5678
  } catch {
5462
5679
  }
5463
5680
  if (vault !== null) {
5464
- await fs13.writeFile(path11.join(incomingState, VAULT_BASENAME), vault, { mode: 384 });
5681
+ await fs13.writeFile(path12.join(incomingState, VAULT_BASENAME), vault, { mode: 384 });
5465
5682
  }
5466
- const trash = path11.join(staging, "replaced-agent_state");
5683
+ const trash = path12.join(staging, "replaced-agent_state");
5467
5684
  await fs13.rename(stateDir, trash);
5468
5685
  try {
5469
5686
  await fs13.rename(incomingState, stateDir);
@@ -5482,9 +5699,9 @@ var SnapshotService = class {
5482
5699
  import fs14 from "fs/promises";
5483
5700
  import { DEFAULT_PROJECT_ID, projectDir, provisionProjectAgents } from "@prismshadow/penguin-core";
5484
5701
  var ABORT_SETTLE_TIMEOUT_MS = 5e3;
5485
- async function dirExists(path16) {
5702
+ async function dirExists(path17) {
5486
5703
  try {
5487
- const stat = await fs14.stat(path16);
5704
+ const stat = await fs14.stat(path17);
5488
5705
  return stat.isDirectory();
5489
5706
  } catch {
5490
5707
  return false;
@@ -5514,7 +5731,11 @@ var ProjectService = class {
5514
5731
  requireProjectAccess(userId, projectId) {
5515
5732
  const row = this.resolveAccess(userId, projectId);
5516
5733
  if (!row) {
5517
- throw new HttpError(404, "project_not_found", "Project \u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE\u3002");
5734
+ throw new HttpError(
5735
+ 404,
5736
+ "project_not_found",
5737
+ "Project does not exist or you do not have access."
5738
+ );
5518
5739
  }
5519
5740
  return row;
5520
5741
  }
@@ -5531,7 +5752,11 @@ var ProjectService = class {
5531
5752
  requireProjectOwner(userId, projectId) {
5532
5753
  const row = this.requireProjectAccess(userId, projectId);
5533
5754
  if (row.role !== "owner") {
5534
- throw new HttpError(403, "owner_required", "\u8BE5\u64CD\u4F5C\u4EC5 Project owner \u53EF\u6267\u884C\u3002");
5755
+ throw new HttpError(
5756
+ 403,
5757
+ "owner_required",
5758
+ "Only the Project owner can perform this operation."
5759
+ );
5535
5760
  }
5536
5761
  return row;
5537
5762
  }
@@ -5573,7 +5798,7 @@ var ProjectService = class {
5573
5798
  throw new HttpError(
5574
5799
  400,
5575
5800
  "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`
5801
+ `Project id must be 2\u201364 characters: ${SEMANTIC_ID_RULE} (the hyphen is reserved as the user namespace separator).`
5577
5802
  );
5578
5803
  }
5579
5804
  } else {
@@ -5583,12 +5808,12 @@ var ProjectService = class {
5583
5808
  throw new HttpError(
5584
5809
  400,
5585
5810
  "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`
5811
+ `Project id must start with ${prefix}, followed by lowercase letters, digits or underscores (at most ${PROJECT_ID_MAX_LENGTH} in total).`
5587
5812
  );
5588
5813
  }
5589
5814
  }
5590
5815
  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`);
5816
+ throw new HttpError(409, "project_exists", `Project id is already taken: ${projectId}.`);
5592
5817
  }
5593
5818
  const displayName = name ?? projectId;
5594
5819
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -5596,7 +5821,7 @@ var ProjectService = class {
5596
5821
  this.deps.projects.insert({ projectId, ownerUserId: owner.userId, createdAt });
5597
5822
  } catch (err) {
5598
5823
  if (err instanceof Error && err.message.includes("UNIQUE")) {
5599
- throw new HttpError(409, "project_exists", `Project id \u5DF2\u88AB\u5360\u7528\uFF1A${projectId}\u3002`);
5824
+ throw new HttpError(409, "project_exists", `Project id is already taken: ${projectId}.`);
5600
5825
  }
5601
5826
  throw err;
5602
5827
  }
@@ -5653,14 +5878,14 @@ var ProjectService = class {
5653
5878
  throw new HttpError(
5654
5879
  409,
5655
5880
  "cannot_delete_default_project",
5656
- "default_project \u4E0E CLI \u5171\u7528\uFF0C\u4E0D\u80FD\u4ECE Web \u5220\u9664\u3002"
5881
+ "default_project is shared with the CLI and cannot be deleted from the web."
5657
5882
  );
5658
5883
  }
5659
5884
  if (this.deps.projects.listAccessible(userId).length <= 1) {
5660
5885
  throw new HttpError(
5661
5886
  409,
5662
5887
  "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"
5888
+ "This is the account's last Project; deleting it would leave no Project available. Create a new Project first."
5664
5889
  );
5665
5890
  }
5666
5891
  await this.destroyProject(projectId);
@@ -5707,13 +5932,21 @@ var ProjectService = class {
5707
5932
  const project = this.requireProjectOwner(userId, projectId);
5708
5933
  const target = this.deps.users.findById(targetUserId);
5709
5934
  if (!target) {
5710
- throw new HttpError(404, "user_not_found", `\u7528\u6237\u4E0D\u5B58\u5728\uFF1A${targetUserId}\u3002`);
5935
+ throw new HttpError(404, "user_not_found", `User does not exist: ${targetUserId}.`);
5711
5936
  }
5712
5937
  if (target.userId === project.ownerUserId) {
5713
- throw new HttpError(409, "already_owner", "owner \u65E0\u9700\u6388\u6743\u7ED9\u81EA\u5DF1\u3002");
5938
+ throw new HttpError(
5939
+ 409,
5940
+ "already_owner",
5941
+ "The owner does not need to grant access to themselves."
5942
+ );
5714
5943
  }
5715
5944
  if (this.deps.members.isMember(projectId, target.userId)) {
5716
- throw new HttpError(409, "already_member", `${targetUserId} \u5DF2\u662F\u8BE5 Project \u7684\u6210\u5458\u3002`);
5945
+ throw new HttpError(
5946
+ 409,
5947
+ "already_member",
5948
+ `${targetUserId} is already a member of this Project.`
5949
+ );
5717
5950
  }
5718
5951
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
5719
5952
  this.deps.members.insert({ projectId, userId: target.userId, createdAt });
@@ -5723,7 +5956,7 @@ var ProjectService = class {
5723
5956
  removeMember(userId, projectId, targetUserId) {
5724
5957
  this.requireProjectOwner(userId, projectId);
5725
5958
  if (!this.deps.members.isMember(projectId, targetUserId)) {
5726
- throw new HttpError(404, "member_not_found", `\u8BE5 Project \u6CA1\u6709\u6210\u5458\uFF1A${targetUserId}\u3002`);
5959
+ throw new HttpError(404, "member_not_found", `This Project has no member: ${targetUserId}.`);
5727
5960
  }
5728
5961
  this.deps.members.delete(projectId, targetUserId);
5729
5962
  }
@@ -5749,7 +5982,7 @@ var ProjectService = class {
5749
5982
  };
5750
5983
 
5751
5984
  // src/services/session-service.ts
5752
- import path12 from "path";
5985
+ import path13 from "path";
5753
5986
  import { readdir as readdir2 } from "fs/promises";
5754
5987
  import {
5755
5988
  createAgent as createAgent2,
@@ -5828,7 +6061,7 @@ var SessionService = class {
5828
6061
  };
5829
6062
  const dir = tracesDir2(this.deps.root, projectId, agentId);
5830
6063
  for (const dateDir of await listDirsSafe(dir)) {
5831
- for (const file of await listFilesSafe(path12.join(dir, dateDir))) {
6064
+ for (const file of await listFilesSafe(path13.join(dir, dateDir))) {
5832
6065
  const match = TRACE_FILE_RE.exec(file);
5833
6066
  if (match) mark(dateDir, match[1]);
5834
6067
  }
@@ -5847,23 +6080,32 @@ var SessionService = class {
5847
6080
  return { sessionCount: all.size, activity };
5848
6081
  }
5849
6082
  /**
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
6083
+ * Create a Session: the model reference is a complete `(provider, modelId)` pair.
6084
+ * Half a reference is a client error, never something to resolve — the missing half
6085
+ * is never guessed, since a guessed provider would send an entry's credential to a
6086
+ * vendor nobody named. Omitting both falls back to the Project's default reference
6087
+ * (400 prompting to configure a model first if there is none). `workspace` is already
5854
6088
  * validated by the route guard. The new Session is added to the active table
5855
6089
  * (idle).
5856
6090
  */
5857
6091
  async createSession(args) {
5858
- let modelId = args.modelId;
5859
- let provider = args.provider;
5860
- if (modelId === void 0) {
6092
+ if (args.modelId === void 0 !== (args.provider === void 0)) {
6093
+ throw badRequest(
6094
+ "modelId and provider must be given together as a (provider, modelId) pair: specify both, or neither to use the Project's default model."
6095
+ );
6096
+ }
6097
+ let modelId;
6098
+ let provider;
6099
+ if (args.modelId !== void 0 && args.provider !== void 0) {
6100
+ modelId = args.modelId;
6101
+ provider = args.provider;
6102
+ } else {
5861
6103
  const def = await this.deps.projectConfig.getDefaultModelRef(args.projectId);
5862
6104
  if (def === void 0) {
5863
6105
  throw new HttpError(
5864
6106
  400,
5865
6107
  "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"
6108
+ "This Project has no default model yet. Add a model on the Models page and set it as the default first."
5867
6109
  );
5868
6110
  }
5869
6111
  modelId = def.model_id;
@@ -5878,7 +6120,7 @@ var SessionService = class {
5878
6120
  try {
5879
6121
  session = await agent.createSession({
5880
6122
  modelId,
5881
- ...provider !== void 0 ? { provider } : {},
6123
+ provider,
5882
6124
  ...args.workspace !== void 0 ? { workspaceDir: args.workspace } : {}
5883
6125
  });
5884
6126
  } catch (err) {
@@ -5910,7 +6152,7 @@ var SessionService = class {
5910
6152
  const dir = tracesDir2(this.deps.root, projectId, agentId);
5911
6153
  const ids = /* @__PURE__ */ new Set();
5912
6154
  for (const dateDir of await listDirsSafe(dir)) {
5913
- for (const file of await listFilesSafe(path12.join(dir, dateDir))) {
6155
+ for (const file of await listFilesSafe(path13.join(dir, dateDir))) {
5914
6156
  const match = TRACE_FILE_RE.exec(file);
5915
6157
  if (match) ids.add(match[1]);
5916
6158
  }
@@ -5922,12 +6164,12 @@ var SessionService = class {
5922
6164
  const dir = tracesDir2(this.deps.root, projectId, agentId);
5923
6165
  let earliest = null;
5924
6166
  for (const dateDir of await listDirsSafe(dir)) {
5925
- for (const file of await listFilesSafe(path12.join(dir, dateDir))) {
6167
+ for (const file of await listFilesSafe(path13.join(dir, dateDir))) {
5926
6168
  const match = TRACE_FILE_RE.exec(file);
5927
6169
  if (!match || match[1] !== sessionId) continue;
5928
6170
  const index = Number(match[2]);
5929
6171
  if (!earliest || index < earliest.index) {
5930
- earliest = { path: path12.join(dir, dateDir, file), index };
6172
+ earliest = { path: path13.join(dir, dateDir, file), index };
5931
6173
  }
5932
6174
  }
5933
6175
  }
@@ -5980,7 +6222,7 @@ async function listFilesSafe(dir) {
5980
6222
 
5981
6223
  // src/services/trace-service.ts
5982
6224
  import fs15 from "fs/promises";
5983
- import path13 from "path";
6225
+ import path14 from "path";
5984
6226
  import { agentsDir as agentsDir3, readTraceTolerant as readTraceTolerant2, tracesDir as tracesDir3 } from "@prismshadow/penguin-core";
5985
6227
  var TRACE_FILE_RE2 = /^(.+)_(\d{3})\.jsonl$/;
5986
6228
  var MAX_SUBAGENT_DEPTH = 4;
@@ -6018,10 +6260,10 @@ var TraceService = class {
6018
6260
  const dir = tracesDir3(this.root, projectId, agentId);
6019
6261
  const out = [];
6020
6262
  for (const dateDir of await listDirs(dir)) {
6021
- for (const file of await listFiles(path13.join(dir, dateDir))) {
6263
+ for (const file of await listFiles(path14.join(dir, dateDir))) {
6022
6264
  const match = TRACE_FILE_RE2.exec(file);
6023
6265
  if (!match || match[1] !== sessionId) continue;
6024
- out.push({ path: path13.join(dir, dateDir, file), date: dateDir, index: Number(match[2]) });
6266
+ out.push({ path: path14.join(dir, dateDir, file), date: dateDir, index: Number(match[2]) });
6025
6267
  }
6026
6268
  }
6027
6269
  return out.sort((a, b) => a.index - b.index);
@@ -6068,7 +6310,7 @@ var TraceService = class {
6068
6310
  for (const agentId of await listDirs(agentsDir3(this.root, projectId))) {
6069
6311
  const dir = tracesDir3(this.root, projectId, agentId);
6070
6312
  for (const dateDir of await listDirs(dir)) {
6071
- for (const file of await listFiles(path13.join(dir, dateDir))) {
6313
+ for (const file of await listFiles(path14.join(dir, dateDir))) {
6072
6314
  const match = TRACE_FILE_RE2.exec(file);
6073
6315
  if (match && !index.has(match[1])) index.set(match[1], agentId);
6074
6316
  }
@@ -6368,11 +6610,11 @@ var TraceService = class {
6368
6610
  const out = { dates: [] };
6369
6611
  for (const date of dates) {
6370
6612
  const bySession = /* @__PURE__ */ new Map();
6371
- for (const file of await listFiles(path13.join(dir, date))) {
6613
+ for (const file of await listFiles(path14.join(dir, date))) {
6372
6614
  const match = TRACE_FILE_RE2.exec(file);
6373
6615
  if (!match) continue;
6374
6616
  const sessionId = match[1];
6375
- const stat = await fs15.stat(path13.join(dir, date, file));
6617
+ const stat = await fs15.stat(path14.join(dir, date, file));
6376
6618
  const files = bySession.get(sessionId) ?? [];
6377
6619
  files.push({ index: Number(match[2]), sizeBytes: stat.size });
6378
6620
  bySession.set(sessionId, files);
@@ -6396,7 +6638,7 @@ var TraceService = class {
6396
6638
  throw new HttpError(
6397
6639
  404,
6398
6640
  "trace_not_found",
6399
- `\u8BE5 Session \u6CA1\u6709 index \u4E3A ${index} \u7684 Trace \u6587\u4EF6\u3002`
6641
+ `This Session has no Trace file with index ${index}.`
6400
6642
  );
6401
6643
  }
6402
6644
  return readTraceTolerant2(file.path);
@@ -6699,7 +6941,7 @@ function createApp(deps2) {
6699
6941
  });
6700
6942
  return handleError(err, c);
6701
6943
  });
6702
- app2.notFound((c) => c.json(errorBody("not_found", "\u63A5\u53E3\u4E0D\u5B58\u5728\u3002"), 404));
6944
+ app2.notFound((c) => c.json(errorBody("not_found", "Endpoint does not exist."), 404));
6703
6945
  app2.use("*", async (c, next) => {
6704
6946
  const start = performance.now();
6705
6947
  await next();
@@ -6709,7 +6951,7 @@ function createApp(deps2) {
6709
6951
  app2.use("/api/*", async (c, next) => {
6710
6952
  const contentLength = Number(c.req.header("content-length") ?? 0);
6711
6953
  if (contentLength > MAX_BODY_BYTES) {
6712
- throw new HttpError(413, "payload_too_large", "\u8BF7\u6C42\u4F53\u8D85\u8FC7 20MB \u4E0A\u9650\u3002");
6954
+ throw new HttpError(413, "payload_too_large", "Request body exceeds the 20MB limit.");
6713
6955
  }
6714
6956
  await next();
6715
6957
  });
@@ -6769,27 +7011,27 @@ function registerStaticRoutes(app2, webDist) {
6769
7011
  app2.get("*", async (c) => {
6770
7012
  const reqPath = decodeURIComponent(c.req.path);
6771
7013
  if (reqPath.startsWith("/api/")) {
6772
- return c.json(errorBody("not_found", "\u63A5\u53E3\u4E0D\u5B58\u5728\u3002"), 404);
7014
+ return c.json(errorBody("not_found", "Endpoint does not exist."), 404);
6773
7015
  }
6774
7016
  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");
7017
+ const resolved = path15.resolve(webDist, rel === "" ? "index.html" : rel);
7018
+ const base = path15.resolve(webDist);
7019
+ const target = resolved === base || resolved.startsWith(base + path15.sep) ? resolved : path15.join(base, "index.html");
6778
7020
  let file = target;
6779
7021
  try {
6780
7022
  const stat = await fsp.stat(file);
6781
- if (stat.isDirectory()) file = path14.join(file, "index.html");
7023
+ if (stat.isDirectory()) file = path15.join(file, "index.html");
6782
7024
  await fsp.access(file);
6783
7025
  } catch {
6784
- file = path14.join(base, "index.html");
7026
+ file = path15.join(base, "index.html");
6785
7027
  }
6786
7028
  let content;
6787
7029
  try {
6788
7030
  content = await fsp.readFile(file);
6789
7031
  } catch {
6790
- return c.json(errorBody("not_found", "\u8D44\u6E90\u4E0D\u5B58\u5728\u3002"), 404);
7032
+ return c.json(errorBody("not_found", "Resource does not exist."), 404);
6791
7033
  }
6792
- const type = CONTENT_TYPES2[path14.extname(file).toLowerCase()] ?? "application/octet-stream";
7034
+ const type = CONTENT_TYPES2[path15.extname(file).toLowerCase()] ?? "application/octet-stream";
6793
7035
  return new Response(new Uint8Array(content), {
6794
7036
  status: 200,
6795
7037
  headers: { "Content-Type": type }
@@ -6799,27 +7041,27 @@ function registerStaticRoutes(app2, webDist) {
6799
7041
 
6800
7042
  // src/config.ts
6801
7043
  import fs17 from "fs";
6802
- import path15 from "path";
7044
+ import path16 from "path";
6803
7045
  import { fileURLToPath } from "url";
6804
7046
  import { resolveRoot } from "@prismshadow/penguin-core";
6805
7047
  var DAY_MS = 24 * 60 * 60 * 1e3;
6806
7048
  function defaultWebDist() {
6807
- const here = path15.dirname(fileURLToPath(import.meta.url));
6808
- const bundled = path15.resolve(here, "..", "web-dist");
7049
+ const here = path16.dirname(fileURLToPath(import.meta.url));
7050
+ const bundled = path16.resolve(here, "..", "web-dist");
6809
7051
  if (fs17.existsSync(bundled)) return bundled;
6810
- return path15.resolve(here, "..", "..", "web", "dist");
7052
+ return path16.resolve(here, "..", "..", "web", "dist");
6811
7053
  }
6812
7054
  function resolveServerConfig(env = process.env) {
6813
7055
  const root = env.PENGUIN_HOME ?? resolveRoot();
6814
7056
  const port = Number(env.PORT || 7364);
6815
7057
  if (!Number.isInteger(port) || port < 0 || port > 65535) {
6816
- throw new Error(`\u975E\u6CD5\u7AEF\u53E3\u914D\u7F6E PORT=${env.PORT}`);
7058
+ throw new Error(`Invalid port configuration PORT=${env.PORT}`);
6817
7059
  }
6818
7060
  return {
6819
7061
  root,
6820
7062
  host: env.HOST ?? "127.0.0.1",
6821
7063
  port,
6822
- dbPath: env.PENGUIN_WEB_DB ?? path15.join(root, "web.db"),
7064
+ dbPath: env.PENGUIN_WEB_DB ?? path16.join(root, "web.db"),
6823
7065
  webDist: env.PENGUIN_WEB_DIST ?? defaultWebDist(),
6824
7066
  authSessionTtlMs: 7 * DAY_MS,
6825
7067
  authSessionRenewMs: 6 * DAY_MS
@@ -6834,15 +7076,15 @@ var app = createApp(deps);
6834
7076
  await deps.authService.seedAdmin();
6835
7077
  await deps.scheduler.start();
6836
7078
  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}`);
7079
+ console.log(`penguin-server started: http://${config.host}:${info.port}`);
7080
+ console.log(`Data root: ${config.root}`);
6839
7081
  console.log(`SQLite: ${config.dbPath}`);
6840
7082
  });
6841
7083
  var shuttingDown = false;
6842
7084
  async function shutdown(signal, exitCode = 0) {
6843
7085
  if (shuttingDown) return;
6844
7086
  shuttingDown = true;
6845
- console.log(`\u6536\u5230 ${signal}\uFF0C\u6B63\u5728\u5173\u505C\u2026`);
7087
+ console.log(`Received ${signal}, shutting down\u2026`);
6846
7088
  deps.scheduler.stop();
6847
7089
  await deps.manager.shutdown(5e3);
6848
7090
  deps.channels.dispose();
@@ -6855,13 +7097,13 @@ async function shutdown(signal, exitCode = 0) {
6855
7097
  process.on("SIGINT", () => void shutdown("SIGINT"));
6856
7098
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
6857
7099
  process.on("uncaughtException", (err) => {
6858
- console.error(`[server] \u672A\u6355\u83B7\u5F02\u5E38: ${err.stack ?? err.message}`);
7100
+ console.error(`[server] Uncaught exception: ${err.stack ?? err.message}`);
6859
7101
  deps.errors.record({ source: "process", err, code: "uncaught_exception" });
6860
7102
  void shutdown("uncaughtException", 1).catch(() => process.exit(1));
6861
7103
  });
6862
7104
  process.on("unhandledRejection", (reason) => {
6863
7105
  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}`);
7106
+ console.error(`[server] Unhandled promise rejection: ${err.stack ?? err.message}`);
6865
7107
  deps.errors.record({ source: "process", err, code: "unhandled_rejection" });
6866
7108
  });
6867
7109
  //# sourceMappingURL=index.js.map