@sema-agent/server 5.11.0 → 5.13.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.
@@ -103,6 +103,18 @@ export class FleetUsageAccumulator {
103
103
  return this.cells.size;
104
104
  }
105
105
  }
106
+ /** #123 F1 披露半场(§M):center 的拒绝响应体是操作员唯一的自诊线索(E2E #121:web 端明说了
107
+ * 「address must be a parseable http(s) base URL」而 server 日志只有 `announce 400`)——摘要进错误
108
+ * 信息(cap 200 字符);读体失败不改变结果面(照旧只有状态码)。 */
109
+ async function rejectionSnippet(res) {
110
+ try {
111
+ const text = (await res.text()).slice(0, 200).trim();
112
+ return text ? ` — center said: ${text}` : "";
113
+ }
114
+ catch {
115
+ return "";
116
+ }
117
+ }
106
118
  /**
107
119
  * Start the worker-side fleet loops. Caller has already gated on "center configured ∧ advertise address set"
108
120
  * (startFleetClientFromEnv below) — this function assumes a complete opts.
@@ -131,7 +143,7 @@ export function startFleetClient(opts) {
131
143
  // 半开连接钉住;心跳侧每 tick 叠一个在飞请求。
132
144
  const res = await f(`${base}/api/fleet/instances`, { method: "POST", headers, body: JSON.stringify(body), signal: AbortSignal.timeout(FLEET_CLIENT_FETCH_TIMEOUT_MS) });
133
145
  if (!res.ok)
134
- throw new Error(`announce ${res.status}`);
146
+ throw new Error(`announce ${res.status}${await rejectionSnippet(res)}`);
135
147
  if (failStreak > 0)
136
148
  opts.logger?.info("fleet_announce_recovered", { after: failStreak });
137
149
  failStreak = 0;
@@ -156,7 +168,7 @@ export function startFleetClient(opts) {
156
168
  try {
157
169
  const res = await f(`${base}/api/fleet/usage`, { method: "POST", headers, body: JSON.stringify({ reports: [report] }), signal: AbortSignal.timeout(FLEET_CLIENT_FETCH_TIMEOUT_MS) });
158
170
  if (!res.ok)
159
- throw new Error(`usage ${res.status}`);
171
+ throw new Error(`usage ${res.status}${await rejectionSnippet(res)}`);
160
172
  }
161
173
  catch (err) {
162
174
  // Delivery failed → the window was never recorded center-side; carry the amounts forward (no loss, no double).
@@ -196,6 +208,19 @@ export function startFleetClientFromEnv(cfg, opts) {
196
208
  const address = opts.advertiseAddress ?? process.env.FLEET_ADVERTISE_ADDRESS;
197
209
  if (!cc?.baseUrl || !cc.token || !address)
198
210
  return undefined;
211
+ // #123 F1(E2E #121 立案):旋钮**设了但坏形** ≠ 没设——center 端要求可解析的 http(s) base URL,
212
+ // 坏形(如缺 scheme 的 host:port)此前静默通过,启动后 fleet 腿每拍 announce 400、worker 永不注册。
213
+ // env fail-loud 族判据([2443] A1 同族):这里前置响亮拒,部署在起点就失败可见,而不是带病运行。
214
+ let parsed;
215
+ try {
216
+ parsed = new URL(address);
217
+ }
218
+ catch {
219
+ parsed = undefined;
220
+ }
221
+ if (parsed === undefined || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
222
+ throw new Error(`FLEET_ADVERTISE_ADDRESS must be a parseable http(s) base URL (e.g. "http://10.0.0.5:8090"); got "${address}" — the fleet center rejects any other shape on every announce`);
223
+ }
199
224
  if (!cc.worker) {
200
225
  opts.logger?.warn("fleet_client_no_worker_scope", { note: "FLEET_ADVERTISE_ADDRESS set but SEMA_REGISTRY_WORKER missing — announce needs the worker (dispatch key); fleet client not started" });
201
226
  return undefined;
@@ -1245,9 +1245,9 @@ export function createHttpServer(rawDeps) {
1245
1245
  ...(binding !== undefined ? { binding: { ...(binding.boundCallId !== undefined ? { boundCallId: binding.boundCallId } : {}), ...(binding.boundInputHash !== undefined ? { boundInputHash: binding.boundInputHash } : {}), ...(binding.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}) } } : {}),
1246
1246
  // remember 的 grant 闭包(onResumeCommitted 的唯一现役来源)在 parked 腿以行的 root/host
1247
1247
  // 会话为锚透传([1591] 候裁② server 修——落子代会话则探针键永不相交,见 grantOnCommit 注);
1248
- // answer 显式 400(复审 F6)。
1248
+ // answer 原样透传(RB-459,core 5.7.0 起 revive 侧有挂点;形状校验已在路由层 isQuestionAnswer)。
1249
1249
  ...(onResumeCommitted ? { grantRemember: (rootSessionId) => onResumeCommitted(rootSessionId) } : {}),
1250
- ...(answer !== undefined ? { answerPresent: true } : {}),
1250
+ ...(answer !== undefined ? { answer } : {}),
1251
1251
  });
1252
1252
  if (parked !== undefined) {
1253
1253
  void ctxPromise.catch(() => { }); // 并行 ctx 读不再被消费,吞掉可能的 rejection
@@ -1342,12 +1342,11 @@ export function createHttpServer(rawDeps) {
1342
1342
  return { status: 400, body: { error: `body.answer is only valid when approving a pending AskUserQuestion (decision: "${decision}", pending: "${pendingTool ?? "unknown"}")`, errorCode: "request.field_conflict" } };
1343
1343
  }
1344
1344
  const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
1345
- // Durable ask (TC-5.4, core 1.95): the resume re-mounts AskUserQuestion with the operator's answer
1346
- // closure the pending call executes against it and the model continues with "The user answered: …".
1347
- // This OVERRIDES the suspend-side QUESTION_AWAITS_RESUME placeholder resolveSpec just rebuilt. Absent
1348
- // answer ⇒ config unchanged (an F4 tool approval resumes exactly as before).
1349
- if (answer)
1350
- taskConfig.onQuestion = async () => answer;
1345
+ // Durable ask (TC-5.4 → RB-459, core 5.7.0): the operator's answer rides the ResumeOutcome — core's
1346
+ // answerFaceForRedeemedCall replays it to the REDEEMED question call only, so a NEW question on the
1347
+ // resumed leg gets an honest error instead of a silently replayed stale answer (which the pre-5.7.0
1348
+ // `onQuestion = async () => answer` closure form did). Absent answer outcome unchanged (an F4 tool
1349
+ // approval resumes exactly as before). The onQuestion seat is left alone.
1351
1350
  // D-1 decision-action binding (core 1.101, design/80 §2): echo the boundCallId + boundInputHash the operator
1352
1351
  // saw back into the outcome — core verifies opaque STRING equality against the persisted suspend-mint values
1353
1352
  // (fail-closed, PRE-CAS), so "saw action A, resolve B" is rejected. boundInputHash is NEVER recomputed here
@@ -1364,6 +1363,7 @@ export function createHttpServer(rawDeps) {
1364
1363
  decision: decision === "approve" ? "allow" : "deny",
1365
1364
  ...(decision === "approve" && binding?.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}),
1366
1365
  ...(reason ? { reason } : {}),
1366
+ ...(answer !== undefined ? { answer } : {}),
1367
1367
  };
1368
1368
  // Developer-mode verify (1.44) survives suspend/resume (core `resumeWithVerification`, design/51 P1-b): a
1369
1369
  // task submitted with verify:true that hit an F4 gate must STILL be gated by the adversarial verifier on
@@ -8,7 +8,7 @@
8
8
  * 判别必须 parked-first:legacy 腿的 `resumeStream` 会绕开 bg registry(无 consumeParkedFlip、
9
9
  * 行永 parked、生命周期分叉)。
10
10
  */
11
- import type { BackgroundAgentRecord, BackgroundAgentStore, CheckpointStore, TaskRegistry, ToolSpec } from "@sema-agent/core";
11
+ import type { BackgroundAgentRecord, BackgroundAgentStore, CheckpointStore, QuestionAnswer, TaskRegistry, ToolSpec } from "@sema-agent/core";
12
12
  export interface ParkedAgentMatch {
13
13
  handle: string;
14
14
  row: BackgroundAgentRecord;
@@ -72,9 +72,11 @@ export interface ParkedDecideRequest {
72
72
  * root 会话则宿主键闭包立即命中,复活子代的后续同工具 ask 被短路(探针 v2 实证),语义与同步车道/
73
73
  * 宿主任务的「本会话不再询问」一致(= host 会话树全域)。行无 root 锚(病态形)⇒ claim 前诚实 400。 */
74
74
  grantRemember?: (rootSessionId: string) => Promise<void>;
75
- /** body 带了 `answer`(复审 F6):parked 腿无 onQuestion 挂点,显式 400 与 legacy 腿对同一
76
- * 畸形输入的拒绝姿势对齐(legacy:「answer 仅在 approve AskUserQuestion 时合法」),不静默吞。 */
77
- answerPresent?: boolean;
75
+ /** RB-459(core 5.7.0,F6 翻案):AskUserQuestion approve 携操作员答案——原样透传进
76
+ * `ResumeOutcome.answer`(形状校验在路由层 isQuestionAnswer;header 逐字匹配等语义校验在 core,
77
+ * server 不预判)。core 拒绝形:同步腿 409 invalid_outcome field:"answer",parked 腿经 drive 失败
78
+ * 面 422 parked_resume.startup_failed。 */
79
+ answer?: QuestionAnswer;
78
80
  /** claim CAS 赢了(≈ legacy 腿的 markResuming 赢)之后、execute 之前触发——remember 豁免 grant
79
81
  * 的挂点(调用方自吞错,绝不抛落赎回)。claim 败/判别 miss 恒不触发。 */
80
82
  onDecideCommitted?: () => Promise<void>;
@@ -76,15 +76,13 @@ export async function decideParkedAgent(deps, req) {
76
76
  // remember 的 grant 必须锚在 root/host 会话(探针闭包键);行无任何会话锚 = 无处可落,claim 前诚实拒。
77
77
  return { status: 400, body: { error: "remember cannot be applied: this parked agent row carries no root/parent session anchor for the exemption", errorCode: "decide.parked_remember_unsupported", taskId: match.handle } };
78
78
  }
79
- if (req.answerPresent === true) {
80
- // 复审 F6:与 legacy 腿同姿势显式拒,不静默丢弃操作员的输入。
81
- return { status: 400, body: { error: "body.answer is not supported when deciding a parked background agent (no question hookpoint on the revive path)", errorCode: "decide.parked_answer_unsupported", taskId: match.handle } };
82
- }
83
79
  const pendingTool = req.pendingAction?.toolName;
84
- if (pendingTool === "AskUserQuestion" && req.decision === "approve") {
85
- // 复审 F1:只拦 approve(approve 必须携 answer 才有意义,而 parked 腿无 onQuestion 挂点);
86
- // deny 放行走赎回(deny outcome 不需要 answer),决策面不留死角。claim 之前拒,claim 不被消费。
87
- return { status: 400, body: { error: "pending action is AskUserQuestion — approve is not supported for a parked background agent (deny it, stop the agent, or let the gate expire)", errorCode: "decide.parked_question_unsupported", taskId: match.handle } };
80
+ if (pendingTool === "AskUserQuestion" && req.decision === "approve" && req.answer === undefined) {
81
+ // RB-459(core 5.7.0,F1/F6 翻案后唯一剩下的 claim 前拒):问题门的 approve 必须携 answer——
82
+ // parked 腿无 live answering face,core 侧同判(invalid_outcome field:"answer"),但那要消费 claim
83
+ // 并空跑一次 revive;这里 claim 前拒是同一判据的免损前置,不是 server 另立规则。带 answer
84
+ // approve 与任何 deny 都放行透传,core 是语义权威(header 逐字匹配等)。
85
+ return { status: 400, body: { error: "pending action is AskUserQuestion — approve requires body.answer (the operator's answers[]); deny needs none", errorCode: "decide.parked_answer_required", taskId: match.handle } };
88
86
  }
89
87
  if (row.name === undefined) {
90
88
  // forwardDurableApproval 只对具名子代成立,parked 行理应恒有 name;缺 = 行受损,claim 前诚实拒
@@ -135,6 +133,8 @@ export async function decideParkedAgent(deps, req) {
135
133
  boundInputHash: req.binding?.boundInputHash ?? persistedHash,
136
134
  decision: req.decision === "approve" ? "allow" : "deny",
137
135
  ...(req.decision === "approve" && req.binding?.updatedInput !== undefined ? { updatedInput: req.binding.updatedInput } : {}),
136
+ // RB-459:answer 原样透传(核不预判、不改写——header 逐字匹配/合法组合全在 core 的 resume 校验)。
137
+ ...(req.answer !== undefined ? { answer: req.answer } : {}),
138
138
  // B10:存在性判定,不是真值判定 —— reason 可以是空串("运维显式选择不写理由"),这与"没传 reason"
139
139
  // 是两件不同的事(姊妹 approval-hmac.ts `env.reason ?? null` 同判据:`??` 只在 null/undefined 时落
140
140
  // null,空串照样入签名载荷)。真值判定会把显式 "" 与缺席折成同一个结果,审计/签名面丢了这个区分。
@@ -200,6 +200,22 @@ export async function decideParkedAgent(deps, req) {
200
200
  // 无害 "lost"([1588] Q2 幂等核定),结果面仍诚实(操作员重查 pending 列表即可自愈)。
201
201
  const disp = await rollback("execute_rejected");
202
202
  const content = receipt.content;
203
+ const receiptError = receipt.details?.error;
204
+ if (receiptError === "parked_resume.startup_failed") {
205
+ // RB-459([2458] 裁定形):resume 已发起但启动即败(含 core 对 outcome.answer 的语义拒——
206
+ // header 不匹配/answer 配错门,invalid_outcome field:"answer" 在 drive 内抛)——422 透传 core
207
+ // 的码,不塌 409 revive_rejected(那是入参级早退的形)。core 已自滚,上面的补滚在 claimId CAS
208
+ // 下退化无害 lost(G-F5 口径),行回 parked 可再决。
209
+ return {
210
+ status: 422,
211
+ body: {
212
+ error: typeof content === "string" ? content : "the parked resume failed during startup",
213
+ errorCode: "parked_resume.startup_failed",
214
+ taskId: match.handle,
215
+ rollback: disp,
216
+ },
217
+ };
218
+ }
203
219
  return {
204
220
  status: 409,
205
221
  body: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "5.11.0",
3
+ "version": "5.13.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",
@@ -54,7 +54,7 @@
54
54
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
55
55
  },
56
56
  "dependencies": {
57
- "@sema-agent/core": "^5.6.0",
57
+ "@sema-agent/core": "^5.7.0",
58
58
  "@sema-agent/registry-core": "^0.14.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",
@@ -68,7 +68,7 @@
68
68
  "sharp": "^0.35.3"
69
69
  },
70
70
  "devDependencies": {
71
- "@sema-agent/sdk": "^5.1.0",
71
+ "@sema-agent/sdk": "^5.2.0",
72
72
  "@types/libsodium-wrappers": "^0.7.14",
73
73
  "@types/node": "22.10.2",
74
74
  "@types/pg": "^8.20.0",