@sema-agent/server 3.7.1 → 3.9.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/budget.js CHANGED
@@ -139,6 +139,12 @@ export function createTracer(metrics, costQuota, modelUsage, fleetUsage, fleetLe
139
139
  if (principal)
140
140
  costQuota?.add(principal, e.costMicroUsd);
141
141
  }
142
+ else if (e.costMicroUsd === undefined) {
143
+ // D5:cost 整键缺席 = 未知(RB-368)——计数供 /metrics/summary 的 costUnknown/unpricedCalls
144
+ // 信号(costUsd 在 unpriced 部署恒 0,没有这个信号消费端会读成「零花费」)。显式 0 =
145
+ // declared free,走上面 truthy 为假的静默臂,不计入 unpriced。
146
+ metrics.inc("brain_calls_unpriced_total", { model: modelId(e.model) });
147
+ }
142
148
  // ┌─ #59 裁定 / RB-368 结案([2052]① cli 报案 → [2073] 立案 → [2076] 工单挂 core → [2083] 到货) ─┐
143
149
  // 病灶报案:下面三处曾是 `e.costMicroUsd ?? 0`(账本 + fleetUsage + fleetLease),把「成本未知」记成
144
150
  // 「免费」,与 `pricingConfigured=false` 的部署组合 ⇒ 全部账目假 0。
@@ -649,6 +649,12 @@ async function handleTasksBody(req, res, url, ctx, miss) {
649
649
  // 提示行。cwd 与已展示的工具 args 同信任级 → 共享白名单构造器(redactSecrets + bound)。
650
650
  res.write(`data: ${JSON.stringify({ type: "workspace_changed", ...workspaceChangedEventData(ev) })}\n\n`);
651
651
  }
652
+ else if (ev.type === "compaction_outcome") {
653
+ // core 2.5.0([2105]② 披露修):压缩非 compacted 结局上流帧——白名单投影:outcome/trigger
654
+ // 闭枚举 verbatim;reason 是引擎说明文本(可能引用配置值)→ redactSecrets;identity 两键同款。
655
+ const e = ev;
656
+ res.write(`data: ${JSON.stringify({ type: "compaction_outcome", outcome: e.outcome, trigger: e.trigger, ...(e.reason !== undefined ? { reason: redactSecrets(e.reason) } : {}), ...(e.eventId !== undefined ? { eventId: e.eventId } : {}), ...(e.parentToolCallId !== undefined ? { parentToolCallId: e.parentToolCallId } : {}) })}\n\n`);
657
+ }
652
658
  else if (ev.type === "text_delta" || ev.type === "turn_end" || ev.type === "message_committed" || ev.type === "context_usage") {
653
659
  // 裸 catch-all 白名单化收官(probe-wire-1 表征钉的根治半场):最后四个已知臂离开
654
660
  // `JSON.stringify(ev)`。**零字节变化**白名单——已知键(含 E2 四 identity 键:live 现状
@@ -34,6 +34,9 @@ export interface MetricsSummary {
34
34
  tokensTotal: number;
35
35
  costUsd: number;
36
36
  costUsdByModel: Record<string, number>;
37
+ /** true ⇒ 窗口内有 cost 未知的 brain.call(unpriced 部署),costUsd/costUsdByModel 是下界(D5)。 */
38
+ costUnknown: boolean;
39
+ unpricedCalls: number;
37
40
  taskDurationAvgSec: number | null;
38
41
  brainFirstTokenAvgMs: number | null;
39
42
  brainCallAvgMs: number | null;
@@ -163,6 +163,11 @@ export class Metrics {
163
163
  tokensTotal: ctotal("task_tokens_total"),
164
164
  costUsd: ctotal("model_cost_micro_usd_total") / 1e6,
165
165
  costUsdByModel: Object.fromEntries(Object.entries(cby("model_cost_micro_usd_total", "model")).map(([k, v]) => [k, v / 1e6])),
166
+ // D5([2122] 挂账,additive v1):unpriced 部署下 costUsd 恒 0 而消费端读成「零花费」。
167
+ // costUnknown=true ⇒ costUsd/costUsdByModel 是**下界**(RB-368 缺席=未知语义;显式 0 = declared
168
+ // free 不计入 unpriced)。计数点在 budget.ts 的 brain.call 臂。
169
+ costUnknown: ctotal("brain_calls_unpriced_total") > 0,
170
+ unpricedCalls: ctotal("brain_calls_unpriced_total"),
166
171
  taskDurationAvgSec: havg("task_duration_seconds"),
167
172
  brainFirstTokenAvgMs: havg("brain_first_token_ms"),
168
173
  brainCallAvgMs: havg("brain_call_latency_ms"),
@@ -109,12 +109,46 @@ export function withWorktreeIsolation(baseFactory, opts) {
109
109
  // down (core's hasDestroy() returns false for a plain base env). Behaviour identical to no wrapper.
110
110
  return baseFactory(ctx);
111
111
  }
112
+ // S3([2113] core 裁定的 server 半场):keep-if-changed 需要在保留分支跑 inner teardown(LSP evict
113
+ // 链等)而不跑 `git worktree remove` —— core addWorktree 把两者焊成一个复合 destroy 拆不开,所以在
114
+ // rootEnvAt 铸造点捕获 inner env 的**原** destroy 引用(per-mint 变量;core 稍后才把复合 destroy
115
+ // 装回同一对象上)。
116
+ let innerDestroy;
112
117
  const { env, worktreeDir } = await serialize(() => addWorktree(opts.baseEnvForGit, {
113
118
  repoRoot,
114
119
  sessionId: ctx.sessionId, // core sanitizes this into the worktree dir name (human-readable orphan diag)
115
- rootEnvAt: opts.rootEnvAt,
120
+ rootEnvAt: async (dir) => {
121
+ const inner = await opts.rootEnvAt(dir);
122
+ const d = inner.destroy;
123
+ if (typeof d === "function")
124
+ innerDestroy = d.bind(inner);
125
+ return inner;
126
+ },
116
127
  ...(opts.commit ? { commit: opts.commit } : {}),
117
128
  }));
129
+ // keep-if-changed 判定基点:mint 后立刻解析 worktree 的 HEAD(=detach 基点)。判定轴与 core Agent
130
+ // 车道逐字同款(subagent.js finish):dirty 文件 >0 ∥ base..HEAD commits >0 ⇒ changed。
131
+ let baseSha;
132
+ {
133
+ const r = await opts.baseEnvForGit.exec("git rev-parse HEAD", { cwd: worktreeDir });
134
+ if (r.ok && r.value.exitCode === 0)
135
+ baseSha = r.value.stdout.trim();
136
+ }
137
+ const isChanged = async () => {
138
+ // 判定失败 ⇒ 保守保留(Agent 车道 changed=true 缺省同款:删错=用户产出蒸发,留错=一个目录)。
139
+ const status = await opts.baseEnvForGit.exec("git status --porcelain", { cwd: worktreeDir });
140
+ if (!status.ok || status.value.exitCode !== 0)
141
+ return true;
142
+ const dirtyFiles = status.value.stdout.split("\n").filter((l) => l.trim() !== "").length;
143
+ if (dirtyFiles > 0)
144
+ return true;
145
+ if (baseSha === undefined)
146
+ return true;
147
+ const ahead = await opts.baseEnvForGit.exec(`git rev-list --count ${baseSha}..HEAD`, { cwd: worktreeDir });
148
+ if (!ahead.ok || ahead.value.exitCode !== 0)
149
+ return true;
150
+ return (parseInt(ahead.value.stdout.trim(), 10) || 0) > 0;
151
+ };
118
152
  opts.logger?.info?.("worktree_isolation_minted", {
119
153
  sessionId: ctx.sessionId,
120
154
  ...(ctx.taskId ? { taskId: ctx.taskId } : {}),
@@ -142,9 +176,30 @@ export function withWorktreeIsolation(baseFactory, opts) {
142
176
  // already reached; double/concurrent destroy is a legal Runner shape). The first call always goes through
143
177
  // coreDestroy (the inner env's own teardown must run).
144
178
  let destroyedOnce = false;
179
+ let keptOnce = false;
145
180
  env.destroy = () => serialize(async () => {
181
+ if (keptOnce)
182
+ return; // kept 是终态:二次 destroy 不得把保留的产出删掉
146
183
  if (destroyedOnce && !(await residue()))
147
- return; // idempotent repeat — done is done
184
+ return; // idempotent repeat — done is done(原语义:残留时放行重试)
185
+ // S3 keep-if-changed:有改动(或判不出来)⇒ 保留目录+registration 给 userland verify+merge,
186
+ // 只跑 inner teardown(LSP evict/host 收尾;rootEnvAt 铸的是 persistent 形,不 rm 目录),
187
+ // 并披露坐标 —— 不披露的保留=只是换了个姿势的蒸发。残留检查在本分支让位:目录在=目的。
188
+ if (await isChanged()) {
189
+ keptOnce = true;
190
+ try {
191
+ await innerDestroy?.();
192
+ }
193
+ finally {
194
+ opts.logger?.info?.("worktree_kept", {
195
+ worktreeDir,
196
+ sessionId: ctx.sessionId,
197
+ ...(ctx.taskId ? { taskId: ctx.taskId } : {}),
198
+ note: "changes present — kept for verify/merge; remove with `git worktree remove` when done",
199
+ });
200
+ }
201
+ return;
202
+ }
148
203
  await coreDestroy();
149
204
  destroyedOnce = true;
150
205
  if (!(await residue()))
@@ -18,7 +18,7 @@
18
18
  import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskRequest, TaskEvent } from "@sema-agent/core";
19
19
  /** 编译期断言:T 必须收敛到 never(有残余键 = tsc 红)。 */
20
20
  type AssertAllKeysHandled<T extends never> = T;
21
- type NotificationProjected = "task_id" | "task_type" | "toolUseId" | "status" | "summary" | "result" | "output_file" | "usage" | "sessionId" | "seq" | "lines" | "stoppedBy" | "source" | "exitCode" | "partial" | "diagnostics" | "recentSteps" | "editedFiles" | "resumable" | "completionId";
21
+ type NotificationProjected = "task_id" | "task_type" | "toolUseId" | "status" | "summary" | "result" | "output_file" | "usage" | "sessionId" | "seq" | "lines" | "stoppedBy" | "source" | "exitCode" | "partial" | "diagnostics" | "recentSteps" | "editedFiles" | "resumable" | "completionId" | "error" | "errorCode";
22
22
  type _GuardNotification = AssertAllKeysHandled<Exclude<keyof TaskNotificationPayload, NotificationProjected>>;
23
23
  type BgNotifProjected = "taskId" | "sessionId" | "seq" | "status" | "summary" | "stoppedBy" | "resumable" | "recentSteps" | "editedFiles" | "usage" | "transcriptId" | "rootSessionId" | "parentTaskId" | "parentToolCallId" | "completionId";
24
24
  type BgNotifExcluded = "kind" | "sessionScoped" | "owner" | "scope" | "description" | "agentType" | "name" | "currentAction" | "currentTool" | "parentSessionId" | "startedAt" | "workflowRunId" | "progressTaskId" | "progressParentTaskId";
@@ -28,7 +28,7 @@ type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjec
28
28
  type AskProjected = "toolName" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName";
29
29
  type AskExcluded = "toolCallId" | "preview" | "principal" | "requiresRealApproval";
30
30
  type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
31
- type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage";
31
+ type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome";
32
32
  type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
33
33
  export type CoreKeysetGuards = [_GuardNotification, _GuardBgNotif, _GuardRoster, _GuardAsk, _GuardTaskEvent];
34
34
  export {};
@@ -185,6 +185,8 @@ export declare function taskNotificationEventData(ev: {
185
185
  }>;
186
186
  resumable?: boolean;
187
187
  completionId?: string;
188
+ error?: string;
189
+ errorCode?: string;
188
190
  };
189
191
  eventId?: string;
190
192
  parentToolCallId?: string;
@@ -250,6 +252,8 @@ export declare function brainStatusEventData(ev: {
250
252
  parentToolCallId?: string;
251
253
  }): Record<string, unknown>;
252
254
  export declare function compactedEventData(ev: {
255
+ eventId?: string;
256
+ parentToolCallId?: string;
253
257
  trigger?: string;
254
258
  tokensBefore?: number;
255
259
  preserved_segment?: {
@@ -198,6 +198,10 @@ export function taskNotificationEventData(ev) {
198
198
  ...(n.stoppedBy !== undefined ? { stoppedBy: n.stoppedBy } : {}),
199
199
  // P1-3(core 1.452):完成幂等锚 verbatim(消费端跨通路去重;[1925] 一轮批)。
200
200
  ...(n.completionId !== undefined ? { completionId: n.completionId } : {}),
201
+ // RB-386(core 2.4.0 wire note,2.5.0 提货收编):bg agent 失败面归因两键——error 是引擎侧
202
+ // 失败说明(可能含网络地址等)→ redactSecrets;errorCode 是机器码 verbatim。缺席=非失败帧。
203
+ ...(n.error !== undefined ? { error: redactSecrets(n.error) } : {}),
204
+ ...(n.errorCode !== undefined ? { errorCode: n.errorCode } : {}),
201
205
  // [1543]§二 server①:白名单滞后 core payload 四键补齐——
202
206
  // source:external 通知来源标(caller 供 = UNTRUSTED)→ redactSecrets;
203
207
  // exitCode:数值 verbatim(bash 腿);
@@ -312,6 +316,8 @@ export function compactedEventData(ev) {
312
316
  return Object.keys(out).length > 0 ? out : undefined;
313
317
  })();
314
318
  return {
319
+ ...(ev.eventId !== undefined ? { eventId: ev.eventId } : {}),
320
+ ...(ev.parentToolCallId !== undefined ? { parentToolCallId: ev.parentToolCallId } : {}),
315
321
  // 🔴 2026-07-25:这三处此前**违反本函数注释自己写的那条不变量**(「畸形字段不得上线,与 1.72 同类」)——
316
322
  // ① `trigger`/`tokensBefore` 是**裸赋值**:`tokensBefore` 为 `NaN` 时 `JSON.stringify` 把它变成 **`null`**,
317
323
  // 于是压缩面板渲染成「压缩前 0 tokens」而不是「未知」;裸赋值还会留下 present-with-undefined 的键
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "3.7.1",
3
+ "version": "3.9.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",
@@ -46,6 +46,7 @@
46
46
  "dev:bake-runner": "tsx src/bake-runner/main.ts",
47
47
  "test": "vitest run",
48
48
  "test:db-integration": "vitest run integration",
49
+ "perf:baseline": "tsx scripts/perf-baseline.mjs",
49
50
  "build:binary": "bun build --compile src/main.ts --outfile dist/sema-server",
50
51
  "build:binary:linux-x64": "bun build --compile --target=bun-linux-x64 src/main.ts --outfile dist/sema-server-linux-x64",
51
52
  "build:binary:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/main.ts --outfile dist/sema-server-darwin-arm64",
@@ -53,7 +54,7 @@
53
54
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
54
55
  },
55
56
  "dependencies": {
56
- "@sema-agent/core": "^2.3.0",
57
+ "@sema-agent/core": "^2.5.0",
57
58
  "@sema-agent/registry-core": "^0.11.0",
58
59
  "e2b": "^2.28.0",
59
60
  "libsodium-wrappers": "^0.8.4",