@sema-agent/server 3.5.0 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,13 @@ export type FleetTaskStatus = "queued" | "running" | "waiting" | "stopping" | "a
16
16
  * shell-local render state the renderer composes; the wire carries the data that TRANSITIONS). */
17
17
  export interface FleetTaskRow {
18
18
  id: string;
19
- name: string;
19
+ /** 显示名。**缺席不铸**([2070]③ cli 定谳,server 3.5.1):没有真名时本键**不发**。
20
+ * ⚠️ 两代语义留档 —— 3.5.0 及以前,三处在无真名时把**行 id** 顶到本位(匿名子代 tick 铸子代 uuid、
21
+ * `onChildTerminal` 合成终帧铸 a\* handle、`publishTask` 懒铸底座铸行 id),于是人眼看到一串乱码
22
+ * (clay 亲报的「子代名乱码」形),消费端也**分不清**「它就叫这个」与「我们不知道它叫什么」。
23
+ * 3.5.1 起后者 = 键缺席,显示占位归客户端渲(cli 已收敛 `'(unnamed)'`)。同一条纪律见 [2053]
24
+ * costUSD:诚实缺席 > 编造。(SDK `FleetTaskRow.name` 同步放宽 —— 见发车工单。) */
25
+ name?: string;
20
26
  description?: string;
21
27
  /** §K-7 (shell ASK 2026-06-28; byte-aligned to `@sema-agent/sdk` 0.0.39 `FleetTaskRow.agentType`/`agentName`) — a
22
28
  * SHORT agent-TYPE label so the picker renders like CC 187 (`Explore`/`general-purpose`/`code-review`) instead of
@@ -41,7 +41,9 @@ export class FleetEventBus {
41
41
  /** Upsert a task row (MERGE onto any existing row by `id`) + fan out the merged row. Publishers may send a
42
42
  * partial `{ id, tokens }` etc.; the row keeps its other fields. */
43
43
  publishTask(delta) {
44
- const merged = { ...(this.tasks.get(delta.id) ?? { id: delta.id, name: delta.id, status: "running" }), ...delta };
44
+ // [2070]③:懒铸底座**不带 name** —— 一条只有 `{id,…}` 的首帧此前会把行 id 铸成显示名,而这里
45
+ // 恰恰是「谁都没给过名字」的位置。真名后到时照常 MERGE 上行(见本文件末的 [2070]③ 第 4 条钉)。
46
+ const merged = { ...(this.tasks.get(delta.id) ?? { id: delta.id, status: "running" }), ...delta };
45
47
  this.tasks.set(delta.id, merged);
46
48
  this.emit({ type: "task", row: merged, ts: this.now() });
47
49
  }
@@ -157,16 +159,16 @@ export function fleetRunPublisher(bus, run) {
157
159
  // core 1.147 subagent usage tick (per-turn, cumulative) → a CHILD fleet row nested under the run.
158
160
  const cid = childId(ev.taskId);
159
161
  children.add(cid);
160
- // BC-2 (core 1.151): use the subagent's sanitized display `name` (taskName/agent-type) for the child row;
161
- // fall back to the taskId when absent (an anonymous subagent — core deliberately omits name rather than leak
162
- // the delegated objective). The row IDENTITY stays `cid` (runId+taskId); only the display label improves.
162
+ // BC-2 (core 1.151): use the subagent's sanitized display `name` (taskName/agent-type) for the child row.
163
+ // [2070]③:name 缺席时**不铸**(此前回落 taskId=子代 uuid,人眼乱码)——行 IDENTITY 一直是 `cid`
164
+ // (runId+taskId),显示名与身份是两件事,没有显示名就让客户端渲占位。
163
165
  // [WF2-A] redact the subagent display name for PARITY with the top-level run name (fleetRunLabels): core
164
166
  // sanitizes it for RENDERING (ANSI/NUL strip + truncate) but does NOT secret-redact, so a secret-shaped
165
167
  // taskName would leak verbatim on the child row without this.
166
168
  // §K-7: `ev.name` IS the subagent TYPE (BC-2: taskName / selected agent-type) → ALSO surface it as
167
169
  // `agentType` (the picker's short-label field), not only the display `name`. Absent name (anonymous
168
- // subagent) → no agentType (the row falls back to name=taskId). One redacted value (name === agentType).
169
- const childName = redactSecrets(ev.name ?? ev.taskId);
170
+ // subagent) → 两位一起缺席(name/agentType 同源同命)One redacted value (name === agentType).
171
+ const childName = ev.name !== undefined ? redactSecrets(ev.name) : undefined;
170
172
  if (!childStartedAt.has(cid))
171
173
  childStartedAt.set(cid, Date.now());
172
174
  if (ev.parentToolCallId)
@@ -174,7 +176,7 @@ export function fleetRunPublisher(bus, run) {
174
176
  // [1415]③ 点亮半场:core 1.354 起 settle 时补终态 tick(status 值域 additive 扩 completed|failed;
175
177
  // 此前恒 "running"=行为逐位不变)——workflow 子的 uuid 行终于有完成态可见(running→removed 的
176
178
  // [1414]#3 形就此闭)。
177
- bus.publishTask({ id: cid, name: childName, ...(ev.name ? { agentType: childName } : {}), parentId: run.runId, scope: run.scope, ...sessionField, status: tickStatus(ev.status), tokens: sumTokens(ev.usage), ...(() => { const t = toolUsesOf(ev.usage); return t !== undefined ? { toolUses: t } : {}; })(), elapsedMs: Date.now() - childStartedAt.get(cid) });
179
+ bus.publishTask({ id: cid, ...(childName !== undefined ? { name: childName, agentType: childName } : {}), parentId: run.runId, scope: run.scope, ...sessionField, status: tickStatus(ev.status), tokens: sumTokens(ev.usage), ...(() => { const t = toolUsesOf(ev.usage); return t !== undefined ? { toolUses: t } : {}; })(), elapsedMs: Date.now() - childStartedAt.get(cid) });
178
180
  }
179
181
  else if (ev.type === "tool_start" && ev.toolName) {
180
182
  // BC-1 (clay 2026-06-27): the top-level run row's `description` = LIVE ACTIVITY (the tool now running), NOT
@@ -212,12 +214,13 @@ export function fleetRunPublisher(bus, run) {
212
214
  const parentId = ev.parentTaskId && ev.parentTaskId !== run.rootTaskId ? childId(ev.parentTaskId) : run.runId;
213
215
  // [WF2-A] redact the forwarded subagent name for parity with the top-level run name (see onEvent above).
214
216
  // §K-7: ev.name IS the subagent type → also surface it as `agentType` (picker short label). Reuse the value.
215
- const fwdName = redactSecrets(ev.name ?? ev.taskId);
217
+ // [2070]③ onEvent 臂:无 name name/agentType 两位一起缺席(不拿 taskId 冒充显示名)
218
+ const fwdName = ev.name !== undefined ? redactSecrets(ev.name) : undefined;
216
219
  if (!childStartedAt.has(cid))
217
220
  childStartedAt.set(cid, Date.now());
218
221
  if (ev.parentToolCallId)
219
222
  childByToolCall.set(ev.parentToolCallId, cid);
220
- bus.publishTask({ id: cid, name: fwdName, ...(ev.name ? { agentType: fwdName } : {}), parentId, scope: run.scope, ...sessionField, status: tickStatus(ev.status), tokens: sumTokens(ev.usage), ...(() => { const t = toolUsesOf(ev.usage); return t !== undefined ? { toolUses: t } : {}; })(), elapsedMs: Date.now() - childStartedAt.get(cid) }); // [1415]③ 同上;[1748] toolUses 同 bg lane 口径
223
+ bus.publishTask({ id: cid, ...(fwdName !== undefined ? { name: fwdName, agentType: fwdName } : {}), parentId, scope: run.scope, ...sessionField, status: tickStatus(ev.status), tokens: sumTokens(ev.usage), ...(() => { const t = toolUsesOf(ev.usage); return t !== undefined ? { toolUses: t } : {}; })(), elapsedMs: Date.now() - childStartedAt.get(cid) }); // [1415]③ 同上;[1748] toolUses 同 bg lane 口径
221
224
  },
222
225
  onChildTerminal(taskId, status, altId, toolUseId) {
223
226
  // A background subagent/bash CHILD settles — task_progress never emits a terminal tick,
@@ -251,9 +254,11 @@ export function fleetRunPublisher(bus, run) {
251
254
  const liveCids = candidateCids.filter((c) => children.has(c));
252
255
  const hadRow = liveCids.length > 0;
253
256
  // 都没活行 = bash 无 tick 形:按首候选合成瞬态终帧(建即离,诚实且无害)。
257
+ // [2070]③:合成帧**不铸 name** —— 这条「瞬态」帧会被壳的终态留存池渲 60s,顶上去的 a\* handle
258
+ // 在那 60s 里就是用户看到的名字。父链接/租户/会话三位照发(它们是归属事实,不是显示名)。
254
259
  for (const cid of hadRow ? liveCids : [childId(taskId)]) {
255
260
  const startedAtChild = childStartedAt.get(cid);
256
- bus.publishTask({ id: cid, ...(children.has(cid) ? {} : { name: taskId, parentId: run.runId, scope: run.scope, ...sessionField }), status: fleetStatus, ...(startedAtChild !== undefined ? { elapsedMs: Date.now() - startedAtChild } : {}) });
261
+ bus.publishTask({ id: cid, ...(children.has(cid) ? {} : { parentId: run.runId, scope: run.scope, ...sessionField }), status: fleetStatus, ...(startedAtChild !== undefined ? { elapsedMs: Date.now() - startedAtChild } : {}) });
257
262
  bus.removeTask(cid);
258
263
  children.delete(cid);
259
264
  childStartedAt.delete(cid);
@@ -1,6 +1,7 @@
1
1
  import { canonicalToolName } from "@sema-agent/core";
2
2
  import { principalFrom, verifyDirectDoorProof, MAX_APPROVAL_REASON_CHARS } from "../../security.js";
3
3
  import { redactedPreview } from "../../trace/redact.js";
4
+ import { fleetRunLabels } from "../../fleet/fleet-bus.js"; // [2069]④ §3 行展示名与 fleet 行同源(见用处的注)
4
5
  import { sleep } from "../sse-log.js";
5
6
  import { sendJson, sendError, sseHeaders } from "../send.js";
6
7
  import { gatedPrincipal, explicitOperatorOk, isOperator } from "../principal-gate.js";
@@ -227,7 +228,24 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
227
228
  // ③ 补偿=(a) 行**不消失**——它仍在本列表里带真实 `status`,「所有还停着的任务」由 status 回答;
228
229
  // (b) failSuspendedWithExpiredCheckpoint 三个 run store 全实装、每 tick 无条件跑(不受
229
230
  // APPROVAL_TIMEOUT_SEC 开关),窗口由 REAP_INTERVAL_SEC 封顶后收敛成 failed。
230
- return { taskId: r.taskId, sessionId: r.sessionId, status: r.status, needsAttention: !!gate, gate, createdAt: r.createdAt, updatedAt: r.updatedAt };
231
+ // [2069]④(cli 2026-07-30):additive 展示位。行此前只有 taskId/status/needsAttention,面板
232
+ // 渲不出「这是哪个任务」。源 = run 行自己的 `objectivePreview`(写入时已脱敏 + ~120 截断)——
233
+ // **不**走 §2 inbox 那套 per-row `getCtx`:本视图头注逐字承诺「ONE query, no N+1」,为两个
234
+ // 展示位把它变成 N+1 是拿契约换排版。
235
+ // · `name` 经 `fleetRunLabels()`,与 fleet 行**同一个函数** ⇒ 同一个 run 在两个面板里名字一致
236
+ // (两处各写一遍必然漂),顺带白拿 [1414]④ 的控制帧形保护(`<task-notification…>` 给中性名)。
237
+ // · `description` = 未截断原值,**仅在与 name 不同时才发** —— 相同就是同一句话印两遍
238
+ // (BC-1 在 fleet 行上杀掉的 objective-shown-twice)。
239
+ // · 两键 OMIT-when-absent(无 preview ⇒ 都不发,占位归客户端),同批 [2070]③ 同一条纪律。
240
+ // 钉:test/assistant-tasks-display-keys.test.ts。
241
+ const preview = r.objectivePreview?.trim() ? r.objectivePreview : undefined;
242
+ const label = preview !== undefined ? fleetRunLabels(preview).name : undefined;
243
+ return {
244
+ taskId: r.taskId, sessionId: r.sessionId, status: r.status, needsAttention: !!gate, gate,
245
+ ...(label !== undefined ? { name: label } : {}),
246
+ ...(preview !== undefined && label !== preview ? { description: preview } : {}),
247
+ createdAt: r.createdAt, updatedAt: r.updatedAt,
248
+ };
231
249
  })
232
250
  .sort((a, b) => Number(b.needsAttention) - Number(a.needsAttention) || (b.gate?.severity ?? 0) - (a.gate?.severity ?? 0) || (b.gate?.spentMicroUsd ?? 0) - (a.gate?.spentMicroUsd ?? 0));
233
251
  sendJson(res, 200, { tasks });
package/dist/main.js CHANGED
@@ -808,22 +808,41 @@ async function main() {
808
808
  runDenySweep = server.denyExpiredApprovals;
809
809
  // [1934]:绑址旋钮 + 无鉴权自收窄(理由见 resolveBindHost 顶注)。undefined = Node 默认全接口。
810
810
  const bindHost = resolveBindHost(config);
811
- await new Promise((resolve) => (bindHost ? server.listen(config.port, bindHost, resolve) : server.listen(config.port, resolve)));
812
- logger.info("listening", {
813
- port: config.port,
814
- bindHost: bindHost ?? "0.0.0.0/::(all interfaces)",
815
- ...(bindHost === "127.0.0.1" && !config.bindHost
816
- ? { note: "auto-narrowed to loopback: write face is unauthenticated (ALLOW_UNAUTHED_WRITES with no service token). Set BIND_HOST explicitly to override." }
817
- : {}),
818
- });
819
811
  // [2062]③ HOST 继承暗通道告警:zsh 常把 HOST 设成机器名,`{...process.env}` 起底的壳会静默继承给
820
812
  // 引擎——bind 面被 shell 环境暗配而部署者不知情(HOST=机器名解析到 LAN 口时=静默扩暴露面)。
813
+ // 🔴 [2067]③ 次序承重:必须在 listen **之前**打——HOST=不可解析名时 listen 直接 ENOTFOUND 崩溃,
814
+ // 恰是「继承 HOST 把 boot 弄崩」这个最需要解释的场景;warn 在 listen 后=诊断在崩溃里缺席,用户只见
815
+ // 裸 getaddrinfo 栈(cli 真机取证)。
821
816
  if (config.bindHostSource === "HOST") {
822
817
  logger.warn("bind_host_from_HOST_env", {
823
818
  bindHost,
824
819
  note: "the bind address came from the HOST env var (not BIND_HOST). Shells like zsh commonly set HOST to the machine name — if you did not set it yourself, this may be an inherited shell variable silently configuring the bind face. Set BIND_HOST explicitly to silence this warning.",
825
820
  });
826
821
  }
822
+ try {
823
+ await new Promise((resolve, reject) => {
824
+ server.once("error", reject); // listen 失败(ENOTFOUND/EADDRINUSE…)不吊死这个 Promise
825
+ bindHost ? server.listen(config.port, bindHost, resolve) : server.listen(config.port, resolve);
826
+ });
827
+ }
828
+ catch (e) {
829
+ // [2067]③:崩溃路径自带来源诊断——不可解析的 HOST 继承名在这里最常见。
830
+ logger.error("listen_failed", {
831
+ port: config.port,
832
+ bindHost: bindHost ?? "0.0.0.0/::(all interfaces)",
833
+ ...(config.bindHostSource ? { bindHostSource: config.bindHostSource } : {}),
834
+ ...(config.bindHostSource === "HOST" ? { note: "bind failed for a host inherited from the HOST env var — set BIND_HOST explicitly (e.g. BIND_HOST=127.0.0.1)." } : {}),
835
+ error: e instanceof Error ? e.message : String(e),
836
+ });
837
+ throw e;
838
+ }
839
+ logger.info("listening", {
840
+ port: config.port,
841
+ bindHost: bindHost ?? "0.0.0.0/::(all interfaces)",
842
+ ...(bindHost === "127.0.0.1" && !config.bindHost
843
+ ? { note: "auto-narrowed to loopback: write face is unauthenticated (ALLOW_UNAUTHED_WRITES with no service token). Set BIND_HOST explicitly to override." }
844
+ : {}),
845
+ });
827
846
  // fleet worker 接线(announce/heartbeat + usage 批报;lease 消费后一拍):boot 完成后
828
847
  // 注册(=首次心跳)。门 = center lane 配置 ∧ FLEET_ADVERTISE_ADDRESS 显式声明(不自猜可达地址);任一缺 → undefined
829
848
  // = 非 fleet 部署零行为。draining 翻转的即刻 announce 在 drainThenShutdown;退出 DELETE 在 hardShutdown。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "3.5.0",
3
+ "version": "3.5.1",
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",
@@ -67,7 +67,7 @@
67
67
  "sharp": "^0.35.3"
68
68
  },
69
69
  "devDependencies": {
70
- "@sema-agent/sdk": "^1.1.0",
70
+ "@sema-agent/sdk": "^2.1.1",
71
71
  "@types/libsodium-wrappers": "^0.7.14",
72
72
  "@types/node": "22.10.2",
73
73
  "@types/pg": "^8.20.0",