@sema-agent/server 2.0.0 → 3.0.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.
Files changed (63) hide show
  1. package/MIGRATION.md +74 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +391 -0
  5. package/dist/boot/budget-tracing.d.ts +48 -0
  6. package/dist/boot/budget-tracing.js +86 -0
  7. package/dist/boot/config-center.d.ts +62 -0
  8. package/dist/boot/config-center.js +1002 -0
  9. package/dist/boot/coordinators.d.ts +33 -0
  10. package/dist/boot/coordinators.js +97 -0
  11. package/dist/boot/execution-env.d.ts +26 -0
  12. package/dist/boot/execution-env.js +370 -0
  13. package/dist/boot/leader.d.ts +27 -0
  14. package/dist/boot/leader.js +81 -0
  15. package/dist/boot/reapers.d.ts +53 -0
  16. package/dist/boot/reapers.js +252 -0
  17. package/dist/boot/resolve-spec.d.ts +70 -0
  18. package/dist/boot/resolve-spec.js +1072 -0
  19. package/dist/boot/runner-deps.d.ts +101 -0
  20. package/dist/boot/runner-deps.js +343 -0
  21. package/dist/boot/runtime-caps.d.ts +21 -0
  22. package/dist/boot/runtime-caps.js +62 -0
  23. package/dist/boot/session-faces.d.ts +57 -0
  24. package/dist/boot/session-faces.js +157 -0
  25. package/dist/boot/shutdown.d.ts +50 -0
  26. package/dist/boot/shutdown.js +129 -0
  27. package/dist/boot/stores.d.ts +32 -0
  28. package/dist/boot/stores.js +361 -0
  29. package/dist/boot/workflow-orchestration.d.ts +46 -0
  30. package/dist/boot/workflow-orchestration.js +150 -0
  31. package/dist/capabilities/scenarios.d.ts +5 -3
  32. package/dist/capabilities/scenarios.js +5 -3
  33. package/dist/config-center/apply-effective.js +4 -3
  34. package/dist/config-lkg.d.ts +2 -1
  35. package/dist/config-lkg.js +2 -1
  36. package/dist/config-types.d.ts +47 -7
  37. package/dist/config.d.ts +30 -13
  38. package/dist/config.js +562 -387
  39. package/dist/hooks/hook-llm.js +9 -0
  40. package/dist/http/routes/approvals-assistant.js +1 -1
  41. package/dist/http/routes/attachments.js +2 -2
  42. package/dist/http/routes/memory-policy.js +3 -3
  43. package/dist/http/routes/runs.js +1 -1
  44. package/dist/http/routes/session-sync.js +2 -2
  45. package/dist/http/routes/sessions.js +2 -2
  46. package/dist/http/routes/tasks.js +2 -2
  47. package/dist/http/routes/trace-usage.js +2 -2
  48. package/dist/http/routes/workflows.js +3 -1
  49. package/dist/http/server.d.ts +1 -1
  50. package/dist/http/server.js +25 -5
  51. package/dist/http/sse-log.js +1 -1
  52. package/dist/main.js +164 -3799
  53. package/dist/model-select.d.ts +1 -1
  54. package/dist/model-select.js +1 -1
  55. package/dist/plugins/checkpoint-store-sql.d.ts +13 -5
  56. package/dist/plugins/checkpoint-store-sql.js +10 -3
  57. package/dist/plugins/local-checkpoint-store.js +8 -2
  58. package/dist/plugins/remote-env-host.d.ts +2 -1
  59. package/dist/run-local.js +2 -1
  60. package/dist/session-titler.d.ts +3 -1
  61. package/dist/session-titler.js +2 -2
  62. package/dist/trace/project.js +4 -1
  63. package/package.json +5 -3
@@ -10,7 +10,7 @@
10
10
  * untrusted body/text can pick WHICH configured model but can never inject a baseUrl/apiKey (parseModelMention
11
11
  * only ever returns an allow-listed catalog name).
12
12
  *
13
- * [865]②(clay 生产实锤:onboard 选 kimi 实跑 Qwen):
13
+ * [865]②(生产实锤:面板上选了 A 模型,实际跑的是目录里的另一个):
14
14
  * - explicit ref 走 name+id 双键索引({@link matchCatalogModel})——壳 boot 回读的 mainLoopModel 是 **id 形**,
15
15
  * 目录键是 **name 形**,单键索引把 id 形请求当「未知」静默落 default = session 中途静默换模型(最恶性的
16
16
  * 上下文污染路径)。
@@ -11,7 +11,7 @@ import { parseModelMention } from "@sema-agent/core";
11
11
  * untrusted body/text can pick WHICH configured model but can never inject a baseUrl/apiKey (parseModelMention
12
12
  * only ever returns an allow-listed catalog name).
13
13
  *
14
- * [865]②(clay 生产实锤:onboard 选 kimi 实跑 Qwen):
14
+ * [865]②(生产实锤:面板上选了 A 模型,实际跑的是目录里的另一个):
15
15
  * - explicit ref 走 name+id 双键索引({@link matchCatalogModel})——壳 boot 回读的 mainLoopModel 是 **id 形**,
16
16
  * 目录键是 **name 形**,单键索引把 id 形请求当「未知」静默落 default = session 中途静默换模型(最恶性的
17
17
  * 上下文污染路径)。
@@ -1,6 +1,6 @@
1
1
  import type { Pool as MySqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
- import { type Checkpoint, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type ResumeOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor } from "@sema-agent/core";
3
+ import { type Checkpoint, type CheckpointGate, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type ResumeOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor } from "@sema-agent/core";
4
4
  import { type SqlDriver } from "./sql-driver.js";
5
5
  /**
6
6
  * design/80 D-1 (§3 invariant #3 — crash-safe reaper backstop): an ABSOLUTE upper bound on a pending
@@ -38,10 +38,18 @@ export interface PendingCheckpoint {
38
38
  * resume binds to the EXACT action they saw (a stale view is then rejected fail-closed PRE-CAS). `boundCallId`
39
39
  * === `toolCallId` (the pending tool call); `boundInputHash` is the server-minted opaque hash of the shown
40
40
  * input (NEVER recomputed by the portal — echoed verbatim). Surfacing them here is what makes the D-1 TOCTOU
41
- * guard REACHABLE by the portal (without it the portal can only do the unbound legacy fallback). `null` on a
42
- * pre-D-1 / legacy-deserialized checkpoint the consumer falls back to the unbound resolve. */
43
- boundCallId: string | null;
44
- boundInputHash: string | null;
41
+ * guard REACHABLE by the portal (without it the portal can only do the unbound legacy fallback).
42
+ * 🔴 [1995]②: ABSENT (key omitted, never `null`) on a park with no tool action (plan_review /
43
+ * resource_limit / task_done) or a pre-D-1 legacy row ⇒ the consumer falls back to the unbound resolve.
44
+ * The wire contract (§1) is "an absent nullable field is OMITTED, not null" — a `null` here invited a
45
+ * consumer to treat the key's PRESENCE as "there is a binding" and echo `null` back into /decide. */
46
+ boundCallId?: string;
47
+ boundInputHash?: string;
48
+ /** 🔴 [1995]③ (additive): the checkpoint's gate kind — the operator queue mixes tool approvals with
49
+ * plan_review / resource_limit / task_done parks (all of which carry `toolName: null`), so a durable-recovery
50
+ * consumer needs this to ROUTE a row to the right wire (`/decide` vs `/plan_review` vs `/resume`). Absent
51
+ * only for a legacy SQL row suspended before the `gate_kind` column existed. */
52
+ gateKind?: CheckpointGate["kind"];
45
53
  /** The suspended run's taskId (task_active join) — the key to the run/trace ("view task context"). */
46
54
  taskId: string | null;
47
55
  /** The pending tool call's args (post-hook), redacted + bounded — for tool_approval the write payload, for a
@@ -305,13 +305,15 @@ export class SqlCheckpointStore {
305
305
  * joined from task_active (the JOIN KEY to the run/trace — a suspended run KEEPS its
306
306
  * session claim, so the join is live for every pending row; null only in pathological windows). */
307
307
  async listPending(scope) {
308
- const base = "SELECT c.session_id, c.scope, c.tool_name, c.tool_call_id, c.tool_input, c.bound_input_hash, c.risk_descriptor, c.created_at, c.deadline, ta.task_id " +
308
+ const base = "SELECT c.session_id, c.scope, c.tool_name, c.tool_call_id, c.tool_input, c.bound_input_hash, c.risk_descriptor, c.created_at, c.deadline, c.gate_kind, ta.task_id " +
309
309
  "FROM checkpoint c LEFT JOIN task_active ta ON ta.session_id = c.session_id WHERE c.status='pending'";
310
310
  const { rows } = scope
311
311
  ? await this.db.query(`${base}${this.q(" AND c.scope=?", " AND c.scope=$1")} ORDER BY c.created_at ASC`, [scope])
312
312
  : await this.db.query(`${base} ORDER BY c.created_at ASC`);
313
313
  const out = rows.map((r) => {
314
314
  const toolCallId = r.tool_call_id ?? null;
315
+ const boundInputHash = r.bound_input_hash ?? null;
316
+ const gateKind = r.gate_kind ?? null;
315
317
  return {
316
318
  sessionId: String(r.session_id),
317
319
  scope: String(r.scope),
@@ -319,8 +321,13 @@ export class SqlCheckpointStore {
319
321
  toolCallId,
320
322
  // D-1: boundCallId === the pending tool call id; boundInputHash is the server-minted opaque hash. The
321
323
  // portal echoes BOTH on /decide so the resume binds to the exact action shown (TOCTOU guard reachable).
322
- boundCallId: toolCallId,
323
- boundInputHash: r.bound_input_hash ?? null,
324
+ // [1995]②: a park with no tool action (plan_review / resource_limit / task_done — both columns NULL)
325
+ // OMITS the keys entirely; the wire contract says absent, never `null` (LOCAL twin does the same).
326
+ ...(toolCallId !== null ? { boundCallId: toolCallId } : {}),
327
+ ...(boundInputHash !== null ? { boundInputHash } : {}),
328
+ // [1995]③ additive: the gate kind the row is parked on (NULL only on a pre-`gate_kind` legacy row) —
329
+ // lets a durable-recovery consumer route plan / tool / resource off this ONE queue.
330
+ ...(gateKind !== null ? { gateKind } : {}),
324
331
  taskId: r.task_id ?? null,
325
332
  // Both drivers return the JSON column already parsed; null for pre-migration rows. PG 库内恒干净
326
333
  // (拒绝式)——直读即审阅面=执行面。
@@ -192,8 +192,14 @@ export class LocalCheckpointStore {
192
192
  scope: cp.scope,
193
193
  toolName: pa?.toolName ?? null,
194
194
  toolCallId,
195
- boundCallId: toolCallId,
196
- boundInputHash: pa?.boundInputHash ?? null,
195
+ // [1995]②: the D-1 binding fields are OMITTED when the park has no tool action (plan_review /
196
+ // resource_limit / task_done) — the wire contract says an absent nullable field is ABSENT, not `null`
197
+ // (a consumer branching on "key present ⇒ there is a binding" would echo `null` back into /decide).
198
+ ...(toolCallId !== null ? { boundCallId: toolCallId } : {}),
199
+ ...(pa?.boundInputHash !== undefined ? { boundInputHash: pa.boundInputHash } : {}),
200
+ // [1995]③ additive: which gate this row is parked on, so a durable-recovery consumer can route
201
+ // plan / tool / resource off the SAME queue (toolName is null for every non-tool gate).
202
+ ...(gate?.kind !== undefined ? { gateKind: gate.kind } : {}),
197
203
  taskId: (await this.opts.getActiveTaskId?.(cp.sessionId)) ?? null,
198
204
  input: boundedToolInput(pa?.args),
199
205
  createdAt: cp.createdAt,
@@ -43,7 +43,8 @@ export interface HostEnvConfig {
43
43
  * reports `supported:false` → INERT (no scheduler tools). Injected at boot only on the single-user host lane. */
44
44
  scheduler?: SchedulerCapability;
45
45
  /** design/103 background shell: `false` → INERT (multi-tenant host lane gate). Default (undefined) = on for the
46
- * single-user TOC host lane. `HOST_BG_ENABLED=false` is an additional global kill switch(旧名 HOST_BG_DISABLED=true 兼容期内仍认)。 */
46
+ * single-user TOC host lane. `HOST_BG_ENABLED=false` is an additional global kill switch(🪦 旧名
47
+ * `HOST_BG_DISABLED` 自 server 3.0.0 起是 fail-loud 墓碑:设了即拒启并指路新名,不再被读取)。 */
47
48
  backgroundShell?: boolean;
48
49
  }
49
50
  export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, SchedulerCapability, BackgroundShellCapability {
package/dist/run-local.js CHANGED
@@ -291,7 +291,8 @@ export async function runLocal(argv, deps = {}) {
291
291
  // TOC local LSP (core 1.190): run-local IS the pure-local host lane, so mount core's `NodeLspManager` (local
292
292
  // child_process language servers over stdio, CC `services/lsp` parity) on the host lane. Default ON (degrades
293
293
  // gracefully to grep/read if a server binary is absent); `LSP_HOST_ENABLED=false` opts out (design/158 B4 split
294
- // this lane off `LSP_ENABLED`, which still works as a back-compat opt-out). `requirePrincipal !== true`
294
+ // this lane off `LSP_ENABLED`; 🪦 server 3.0.0 `LSP_ENABLED=false` fail-loud 墓碑,拒启指路新名,
295
+ // 不再是可用的 opt-out)。`requirePrincipal !== true`
295
296
  // keeps it uniform with main.ts (a no-op here — run-local is single-user — but never relies on that). Both runners.
296
297
  const lspManager = hostRx && config.lspHostEnabled && config.requirePrincipal !== true ? new NodeLspManager({ log: (event, fields) => logger.info(event, fields) }) : undefined;
297
298
  // Long-term memory (design/138 S1): run-local IS the single-user lane, so the file-based memory ENGINE is
@@ -29,6 +29,7 @@ export interface SessionTitlerDeps {
29
29
  llm(opts: {
30
30
  prompt: string;
31
31
  timeoutMs: number;
32
+ model?: string;
32
33
  }): Promise<{
33
34
  ok: true;
34
35
  text: string;
@@ -47,7 +48,8 @@ export interface SessionTitlerDeps {
47
48
  }
48
49
  export interface SessionTitler {
49
50
  /** Fire-and-forget: maybe generate + persist a title for this session. Never throws, never blocks the caller. */
50
- maybeTitle(sessionId: string, objective: string): void;
51
+ /** `model`([1992]②)= turn 真实驱动的模型——带上则标题 hook 同链路,不再裸落 env 缺省。 */
52
+ maybeTitle(sessionId: string, objective: string, model?: string): void;
51
53
  }
52
54
  export declare const TITLE_MAX_CHARS = 80;
53
55
  /** Single line, control chars stripped, hard cap; surrounding quotes (a common LLM tic) trimmed. */
@@ -65,7 +65,7 @@ export function createSessionTitler(deps) {
65
65
  deps.logger?.warn?.("session_title_failed", { ...meta, note: "同 cause 后续降为 debug 级" });
66
66
  };
67
67
  return {
68
- maybeTitle(sessionId, objective) {
68
+ maybeTitle(sessionId, objective, model) {
69
69
  if (!sessionId || !objective.trim())
70
70
  return;
71
71
  if (seen.has(sessionId))
@@ -93,7 +93,7 @@ export function createSessionTitler(deps) {
93
93
  deps.metrics?.inc("session_title_total", { outcome: "already_titled" });
94
94
  return;
95
95
  }
96
- const r = await deps.llm({ prompt: buildTitlePrompt(objective), timeoutMs: LLM_TIMEOUT_MS });
96
+ const r = await deps.llm({ prompt: buildTitlePrompt(objective), timeoutMs: LLM_TIMEOUT_MS, model });
97
97
  if (!r.ok) {
98
98
  deps.metrics?.inc("session_title_total", { outcome: "llm_failed" });
99
99
  logFailure(sessionId, r.error);
@@ -510,7 +510,10 @@ export function mapTraceEvent(type, seq, data) {
510
510
  case "failed":
511
511
  // BL-13: redact at the read boundary too (defense-in-depth) — covers any errorMessage that reached the
512
512
  // log un-redacted via another write path; idempotent on the BL-12-redacted catch-path message.
513
- return { event: "error", data: { code: "WORKER_DOWN", message: redactSecrets(String(data.errorMessage ?? "run failed")) } };
513
+ // A1/A6/B(复审 2026-07-29):error 帧统一为 `{ type:"error", errorCode, message }` —— sse-log /
514
+ // approvals 的帧同形。这一处是 3.0.0 摘 legacy `code` 键时的漏网:帧在 src/trace/ 铸,而当时的门只扫
515
+ // src/http/,于是"SSE error 帧机器键统一为 errorCode"的公开声明与 wire 上跑的东西不一致(门是绿的)。
516
+ return { event: "error", data: { type: "error", errorCode: "WORKER_DOWN", message: redactSecrets(String(data.errorMessage ?? "run failed")) } };
514
517
  default:
515
518
  return null; // compacted etc. — not a client trace event
516
519
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -23,7 +23,9 @@
23
23
  "!dist/run-local-linux-x64",
24
24
  "!dist/run-local-darwin-arm64",
25
25
  "skills",
26
- "deploy/sema-up"
26
+ "deploy/sema-up",
27
+ "USAGE.md",
28
+ "MIGRATION.md"
27
29
  ],
28
30
  "bin": {
29
31
  "run-local": "dist/run-local.js",
@@ -51,7 +53,7 @@
51
53
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
52
54
  },
53
55
  "dependencies": {
54
- "@sema-agent/core": "^2.0.0",
56
+ "@sema-agent/core": "^2.1.0",
55
57
  "@sema-agent/registry-core": "^0.10.21",
56
58
  "e2b": "^2.28.0",
57
59
  "libsodium-wrappers": "^0.8.4",