@sema-agent/server 5.14.0 → 6.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.
package/USAGE.md CHANGED
@@ -221,6 +221,9 @@ SEMA_REGISTRY_URL=http://<config-center-host>:3100 # 启动拉 GET /api/config
221
221
  SEMA_REGISTRY_TOKEN=<SERVICE_PULL_TOKEN 的值> # 取自配置控制面主机 .env;只读拉取令牌
222
222
  SEMA_REGISTRY_DRY_RUN=true # 安全灰度:只 LOG 中心配置 vs env 推导的差异,不 apply
223
223
  SEMA_REGISTRY_WORKER=<worker名> # 可选:拉取 /effective?worker=<名> 取该 worker 的 roster(reconciler 按 worker 注);不设=全局 roster(向后兼容)
224
+ FLEET_ADVERTISE_ADDRESS=http://<本机可达IP>:8090 # 可选:设了才启 fleet 上报腿(announce/heartbeat+usage 批报到中心)。
225
+ # 必须是可解析的 http(s) base URL——host:port 等坏形 5.13.0 起 boot 响亮拒
226
+ # (此前静默每拍 announce 400,worker 永不注册)。
224
227
  ```
225
228
  - 中心**空/未发布** → `applyEffective` 回落 env + 内建 teams 并 warn `config_center_unpublished`,**不影响在跑的服务**(接了也安全)。
226
229
  - **灰度姿势**(配置控制面 AI 建议):先 `SEMA_REGISTRY_DRY_RUN=true` 起一轮,看日志 `sema_registry_dry_run`(中心给的 models/roles/teams + 会否覆盖 default、per-model apiKeyEnv)对得上 env 再去掉该 flag 真正 apply。
@@ -76,8 +76,8 @@ function emptyCostBreakdown() {
76
76
  * measurement was reward-hackable and never trustworthy) is ALSO non-scorable → excluded, never a withhold-credit.
77
77
  */
78
78
  export function classifyRunStatus(input) {
79
- if (input.status === "failed" || input.status === "timeout")
80
- return "infra-failed";
79
+ if (input.status === "failed")
80
+ return "infra-failed"; // core 5.8.0:status "timeout" 退役(walltime 形并入 failed)
81
81
  if (input.repairTerminal === "oracle.unprotected")
82
82
  return "infra-failed"; // broken/un-isolated oracle → not scorable
83
83
  return "scored";
@@ -624,7 +624,30 @@ export function createResolveSpec(ctx) {
624
624
  // 该字段,叠一条 host-declared 字符串值=同键双条矛盾帧(by-key 投影抹掉权威来源)。server 只
625
625
  // 声明自己命名空间的键;来源语义(caller vs 墙钟)由 timeoutSec 的 spec provenance + 上面的
626
626
  // Cap 声明组合可读。
627
- return { ...(limits !== undefined ? { limits } : {}), ...(declarations.length > 0 ? { configOverrides: declarations } : {}) };
627
+ // core 5.8.0:预算族(maxCostUsd/maxTokens/degrade)从 TaskSpec 顶层迁入 limits——顶层键已删,
628
+ // 继续写顶层=经宽类型静默死键(E-HIGH-1 病族)。wire 面不变:body 顶层 maxCostUsd/maxTokens 照收
629
+ //(SDK 在发),装配目标改到 limits。budgetStreamCancel 语义照 core 默认(maxCostUsd 在场即 true)。
630
+ const maxCostUsd = cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd);
631
+ const maxTokens = cappedCeiling(body.maxTokens, config.maxTaskTokens);
632
+ const degrade = (() => {
633
+ if (!config.degrade || maxCostUsd === undefined)
634
+ return undefined;
635
+ if (Array.isArray(body.images) && body.images.length > 0 && !config.degrade.toSupportsImages) {
636
+ // S14 (SILENT-FALLBACK P0-f): this drop was undetectable — the task keeps its vision-capable main
637
+ // model and near-budget it hard-fails on cost instead of degrading. Surface it.
638
+ metrics.inc("degrade_dropped_total", { reason: "vision_target" });
639
+ logger.warn("degrade_dropped", { reason: "vision_target", model: picked.model, images: body.images.length });
640
+ return undefined;
641
+ }
642
+ return { to: config.degrade.to, atCostFraction: config.degrade.atCostFraction };
643
+ })();
644
+ const merged = {
645
+ ...(limits ?? {}),
646
+ ...(maxCostUsd !== undefined ? { maxCostUsd } : {}),
647
+ ...(maxTokens !== undefined ? { maxTokens } : {}),
648
+ ...(degrade !== undefined ? { degrade } : {}),
649
+ };
650
+ return { ...(Object.keys(merged).length > 0 ? { limits: merged } : {}), ...(declarations.length > 0 ? { configOverrides: declarations } : {}) };
628
651
  })(),
629
652
  // design/129: the TOC/interactive posture (single-user turnkey — the shell's
630
653
  // session lane) defaults background children to SESSION scope = CC Backgrounded semantics (a bg Agent/
@@ -632,30 +655,8 @@ export function createResolveSpec(ctx) {
632
655
  // deployments keep core's "task" default (no orphans burning tokens). Caller-trusted spec field
633
656
  // (systemPrompt tier), NEVER read from the request body. Same tenancy predicate as the wall clock above.
634
657
  ...(config.requirePrincipal !== true ? { backgroundScope: "session" } : {}),
635
- // ⑤ Per-task budget gate (1.37): honor a caller's requested ceiling but CAP it to the operator
636
- // ceiling (a request can ask for less, never more). core fails the task with errorCode budget.*
637
- // when crossed. budgetStreamCancel defaults true when maxCostUsd is set.
638
- maxCostUsd: cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd),
639
- maxTokens: cappedCeiling(body.maxTokens, config.maxTaskTokens),
640
- // 1.40 near-budget degradation: only meaningful with a cost ceiling (the fraction is of it).
641
- // When this task has one, switch to the cheaper model at atCostFraction instead of hard-failing.
642
- // vision precheck (adversarial-review finding): DROP degrade for an image-carrying task when the
643
- // degrade TARGET can't read images (toSupportsImages=false). The precheck only sees the picked model, not the
644
- // external degrade target — so without this a runtime degrade would send the images to a text-only gateway (the
645
- // opaque 400 the precheck prevents). The task keeps its vision-capable main model; near-budget it hard-fails on
646
- // cost instead of image-failing. MODEL_DEGRADE_TO_VISION=true opts back in.
647
- degrade: (() => {
648
- if (!config.degrade || cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd) === undefined)
649
- return undefined;
650
- if (Array.isArray(body.images) && body.images.length > 0 && !config.degrade.toSupportsImages) {
651
- // S14 (SILENT-FALLBACK P0-f): this drop was undetectable — the task keeps its vision-capable main
652
- // model and near-budget it hard-fails on cost instead of degrading (rationale above). Surface it.
653
- metrics.inc("degrade_dropped_total", { reason: "vision_target" });
654
- logger.warn("degrade_dropped", { reason: "vision_target", model: picked.model, images: body.images.length });
655
- return undefined;
656
- }
657
- return { to: config.degrade.to, atCostFraction: config.degrade.atCostFraction };
658
- })(),
658
+ // ⑤ Per-task budget gate + 1.40 near-budget degrade:core 5.8.0 起全部在 limits(上方 IIFE 合成),
659
+ // 顶层键已随 core 类型删除。
659
660
  // Long-term memory (design/138 S1): enabled when the memory ENGINE is wired AND a scope was derived
660
661
  // (single-user only — multi-tenant derives none, memory dark). MF-30 PAUSE (option B, per-request —
661
662
  // clay 2026-06-27 confirmed with core): `body.memoryWrite:false` makes THIS run read-only over memory
@@ -86,7 +86,7 @@ async function runLensReviews(subRunner, repoTools, objective, signal, lenses) {
86
86
  modelRole: "subagent",
87
87
  tools: repoTools,
88
88
  systemPrompt: lensSystem(lens),
89
- limits: { timeoutSec: 180 },
89
+ limits: { maxWalltimeMs: 180_000 },
90
90
  enableBlockedReport: false,
91
91
  signal,
92
92
  });
@@ -113,7 +113,7 @@ async function runConvergence(subRunner, repoTools, objective, findings, opts, s
113
113
  tools: repoTools, // members may verify a disputed claim against the real code (Tool-MAD)
114
114
  maxTranscriptTokens: 8000,
115
115
  synthesizer: { role: "arbiter", systemPrompt: ARBITER_SYSTEM },
116
- limits: { timeoutSec: 400 },
116
+ limits: { maxWalltimeMs: 400_000 },
117
117
  signal, // 1.28: a parent abort now cascades into the debate members
118
118
  });
119
119
  // 1.32: synthesizer 两次都失败时 `conclusion` 是 `[unavailable…]` 垃圾串,与真结论无法区分。别当结论
@@ -134,7 +134,7 @@ async function runConvergence(subRunner, repoTools, objective, findings, opts, s
134
134
  // The arbiter grounds/dedups/ranks — keep it on the strong `synthesize` role (= main model).
135
135
  modelRole: "synthesize",
136
136
  systemPrompt: ARBITER_SYSTEM,
137
- limits: { timeoutSec: 240 },
137
+ limits: { maxWalltimeMs: 240_000 },
138
138
  enableBlockedReport: false,
139
139
  signal,
140
140
  });
@@ -68,7 +68,7 @@ export function createTeamTool(subRunner, template, opts) {
68
68
  ? { role: template.synthesizer.role, ...(template.synthesizer.model ? { model: template.synthesizer.model } : {}), modelRole: template.synthesizer.modelRole, systemPrompt: template.synthesizer.systemPrompt ?? "" }
69
69
  : undefined,
70
70
  maxTranscriptTokens: 8000,
71
- limits: { timeoutSec: 600 },
71
+ limits: { maxWalltimeMs: 600_000 },
72
72
  signal: ctx.signal, // 1.28: a team abort now cascades into the member runs (review D)
73
73
  });
74
74
  ctx.reportUsage?.({ tokens: team.stats.tokens, turns: team.stats.turns, tasks: template.members.length * template.rounds + 1 });
@@ -228,7 +228,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
228
228
  rootTaskId: taskId,
229
229
  ...fleetRunLabels(prepared.spec.objective), // BC-1: name = short objective preview (description = live-activity, set by the publisher onEvent tool_start)
230
230
  });
231
- void runInBackground(deps.runner, { ...prepared.spec, sessionId }, runStore, taskId, deps.metrics, prepared.auth?.principal, prepared.verify, prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation, // E23: per-run elicitation context (onElicit routes inbound MCP elicitations to this run's stream)
231
+ void runInBackground(deps.runner, { ...prepared.spec, sessionId }, runStore, taskId, deps.metrics, prepared.auth?.principal, prepared.verify, prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, prepared.spec.limits?.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation, // E23: per-run elicitation context (onElicit routes inbound MCP elicitations to this run's stream)
232
232
  gatedPrincipal(req, deps.config) ?? null, // E23: the VERIFIED principal that may answer (same source the respond gate uses — never the spoofable header)
233
233
  captureTurnAnchor, // E18: per-turn (message eventId → leaf entryId) anchor capture
234
234
  fleetPub, // MF-Fleet: the run-scoped fleet-row publisher (onStart/onEvent/onTerminal across all legs)
@@ -461,7 +461,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
461
461
  // 并发腿可能先终态化(如 approval.expired),我方写打空却仍答 "cancelled" 就是响应与账本不一致。
462
462
  // 写后复读,照实际行应答(锁一样释放了;谁先写赢谁的 errorCode)。
463
463
  const finalRow = await rs.getRun(taskId).catch(() => undefined);
464
- if (finalRow && finalRow.errorCode !== "cancelled" && (finalRow.status === "failed" || finalRow.status === "completed" || finalRow.status === "blocked" || finalRow.status === "timeout")) {
464
+ if (finalRow && finalRow.errorCode !== "cancelled" && (finalRow.status === "failed" || finalRow.status === "completed" || finalRow.status === "blocked")) {
465
465
  sendJson(res, 202, { taskId, status: finalRow.status, errorCode: finalRow.errorCode ?? null, note: `session unlocked; the run was terminalized concurrently (${finalRow.errorCode ?? finalRow.status}) before this cancel's write — reporting the actual ledger state` });
466
466
  return;
467
467
  }
@@ -1032,7 +1032,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
1032
1032
  // (verify → cascade → plain)、短路语义、传参**逐字不变**;`runWithVerification` 仍是整对象
1033
1033
  // 直传(快审 F1:cost 顶不在重建中丢失),`runCascade` 仍从操作员梯子 + 本任务预算现算配置。
1034
1034
  const verifyLeg = (v) => runWithVerification(deps.runner, specWithSignal, v);
1035
- const cascadeLeg = () => runCascade(deps.runner, specWithSignal, cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd));
1035
+ const cascadeLeg = () => runCascade(deps.runner, specWithSignal, cascadeConfig(deps.config.cascadeLadder, prepared.spec.limits?.maxCostUsd));
1036
1036
  const plainLeg = () => deps.runner.runTask(specWithSignal);
1037
1037
  result = await withPrincipal(principal, () => prepared.verify ? verifyLeg(prepared.verify) : prepared.cascade ? cascadeLeg() : plainLeg());
1038
1038
  }
@@ -947,23 +947,27 @@ export function createHttpServer(rawDeps) {
947
947
  if (body.limits !== undefined) {
948
948
  const l = body.limits;
949
949
  if (typeof l !== "object" || l === null || Array.isArray(l)) {
950
- sendError(res, 400, "request.field_invalid", "limits must be an object { timeoutSec?, maxOutputTokens?, maxTurns? } (positive integers)");
950
+ sendError(res, 400, "request.field_invalid", "limits must be an object { maxWalltimeMs?, maxOutputTokens?, maxTurns? } (positive integers)");
951
951
  return null;
952
952
  }
953
- for (const k of ["timeoutSec", "maxOutputTokens", "maxTurns"]) {
954
- const v = l[k];
955
- if (v !== undefined && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
956
- sendError(res, 400, "request.field_invalid", `limits.${k} must be a positive integer`);
953
+ // core 5.8.0 时限重构:旧键响亮拒并点名替代(core 同姿势 config.limit_unknown_key;「勿自行兼容」是
954
+ // core 的公开指令)。timeoutSec(秒)→ maxWalltimeMs(毫秒,注意单位);deadline 族三 opt-out 随
955
+ // 机制整族退役(nudge/call-cap/graceful-finalize 已从引擎移除),无替代键。fresh fail-loud;resume
956
+ // 重放不过此门,normalizeLimits 对旧键 defensive DROP(存量 suspended 体降级为默认配速,方向安全)
957
+ if (l.timeoutSec !== undefined) {
958
+ sendError(res, 400, "request.field_invalid", "limits.timeoutSec was retired in core 5.8.0 — use limits.maxWalltimeMs (milliseconds; opt-in wall clock)");
959
+ return null;
960
+ }
961
+ for (const k of ["deadlineNudge", "callCapByDeadline", "gracefulFinalize"]) {
962
+ if (l[k] !== undefined) {
963
+ sendError(res, 400, "request.field_invalid", `limits.${k} was retired in core 5.8.0 (the deadline-pacing mechanism family is gone; there is no replacement key — remove it)`);
957
964
  return null;
958
965
  }
959
966
  }
960
- // 快审 F2(1.254):deadline 族三 opt-out 在场必须 literal false——"false"/0 等畸形值此前 200 后被
961
- // normalizeLimits 静默丢=调用方以为关了其实恒开(B1 同类故障)。fresh fail-loud;resume 重放不过
962
- // 此门,normalizeLimits 保持 defensive。
963
- for (const k of ["deadlineNudge", "callCapByDeadline", "gracefulFinalize"]) {
964
- const v = body.limits[k];
965
- if (v !== undefined && v !== false) {
966
- sendError(res, 400, "request.field_invalid", `limits.${k} accepts only literal false (it is an opt-out; omit to keep the default-on behavior)`);
967
+ for (const k of ["maxWalltimeMs", "maxOutputTokens", "maxTurns"]) {
968
+ const v = l[k];
969
+ if (v !== undefined && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
970
+ sendError(res, 400, "request.field_invalid", `limits.${k} must be a positive integer${k === "maxWalltimeMs" ? " (milliseconds)" : ""}`);
967
971
  return null;
968
972
  }
969
973
  }
@@ -2200,10 +2204,11 @@ export function createHttpServer(rawDeps) {
2200
2204
  deps.metrics?.inc("task_tokens_total", {}, result.stats.tokens);
2201
2205
  if (result.stats.cacheHitRate !== undefined)
2202
2206
  deps.metrics?.observe("task_cache_hit_rate", result.stats.cacheHitRate);
2203
- // ⑤ budget/limit cutoffs (1.37): count failures by their dotted code (budget.precall/exceeded,
2204
- // limit.timeout/max_turns) so an operator can alert on tenants hitting the cost/token ceiling.
2207
+ // ⑤ budget/limit cutoffs:count failures by their dotted code — core 5.8.0 起统一 `limits.` 前缀
2208
+ // (limits.max_{tokens,cost,turns,walltime}_exceeded;旧 budget.*/limit.* 双前缀同拍退役),
2209
+ // operator 按此对租户撞顶告警。metric 名保留 budget_exceeded_total(时序连续性;code label 自述新码)。
2205
2210
  const code = result.errorCode;
2206
- if (code && (code.startsWith("budget.") || code.startsWith("limit."))) {
2211
+ if (code && code.startsWith("limits.")) {
2207
2212
  deps.metrics?.inc("budget_exceeded_total", { code });
2208
2213
  }
2209
2214
  // Developer-mode verification gate (1.44): count the final verdict when this task was verified.
@@ -153,19 +153,14 @@ export interface TaskRequestBody {
153
153
  };
154
154
  /** design/132 (core 1.249): one-shot end-game verification nudge — opt-in by the AUTONOMY caller. */
155
155
  finalVerification?: boolean;
156
- /** [854]④ per-request 配速(core spec.limits 全链:timeoutSec 墙钟 + deadlineNudge/callCapByDeadline/
157
- * gracefulFinalize;maxOutputTokens = 每次模型调用的输出上限;maxTurns = 上游 parity 的回合上限)。
158
- * 三键均为正整数(0 拒绝:core timeoutSec>0 才生效,放行 0 会绕过运营方封顶)。与 `maxTokens`
159
- * (per-task token 预算,cappedCeiling 车道)不同物 —— maxOutputTokens 是单次调用配速。
160
- * 每键各自被可选 env 上限封顶(TASK_TIMEOUT_MAX_SEC / TASK_MAX_OUTPUT_TOKENS_MAX / TASK_MAX_TURNS_MAX,
161
- * 缺省不设=不封顶);timeoutSec 缺席时保持既有 tenancy 墙钟姿势(resolveTaskLimits,spec-fields.ts)。 */
156
+ /** [854]④ per-request 配速——core 5.8.0 时限重构后的键集(#124):`maxWalltimeMs`(毫秒,opt-in 墙钟)/
157
+ * `maxOutputTokens`/`maxTurns`,均正整数;可选运营方上限旋钮各自封顶(TASK_TIMEOUT_MAX_SEC 保**秒**义,
158
+ * 对毫秒键封顶 ×1000)。旧键 `timeoutSec` deadline 族三 opt-out(机制退役)fresh-submit 400 响亮
159
+ * 点名替代;`maxWalltimeMs` 缺席时保持既有 tenancy 墙钟姿势(resolveTaskLimits,spec-fields.ts) */
162
160
  limits?: {
163
- timeoutSec?: number;
161
+ maxWalltimeMs?: number;
164
162
  maxOutputTokens?: number;
165
163
  maxTurns?: number;
166
- deadlineNudge?: false;
167
- callCapByDeadline?: false;
168
- gracefulFinalize?: false;
169
164
  };
170
165
  /** design/133 (core 1.251) + G1 (core 1.253): turn-boundary attachment reminders (todo / changed-files /
171
166
  * plan-mode) + one-shot boundary notices (post-compaction background-task recap / deferred-tools delta).
@@ -38,8 +38,8 @@ export interface WorkerReport {
38
38
  * it up to its bound but it didn't fully complete — e.g. the overall deadline hit or the slice cap). Present
39
39
  * ⇒ resumable: the operator/leader can escalate (more budget) or accept the belt-diff partial progress. */
40
40
  checkpointGate?: TaskResult["checkpointGate"];
41
- /** design/74 Slice 6: the worker's terminal `TaskResult.errorCode` (e.g. `budget.exceeded` on an exhausted
42
- * resume, `limit.max_turns`) — surfaced for /v1/leader observability. */
41
+ /** design/74 Slice 6: the worker's terminal `TaskResult.errorCode` (e.g. `limits.max_cost_exceeded` on an
42
+ * exhausted resume, `limits.max_turns_exceeded` — core 5.8.0 码名轴) — surfaced for /v1/leader observability. */
43
43
  errorCode?: TaskResult["errorCode"];
44
44
  /** LEADER-REPAIRLOOP-INTEGRATION §10.6 (THE push chokepoint): the `runRepairLoop` terminal a single-agent
45
45
  * worker resolved to, when the repair loop ran on this worker (LEADER_REPAIR_LOOP on). It MUST ride on the
@@ -108,7 +108,7 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
108
108
  // Map the TaskResult status directly (like fanOut's own mapStatus) — a `suspended` result that the
109
109
  // auto-resume left unresumed (a non-resource gate, e.g. human-approval) stays `suspended` so the
110
110
  // leader's C4 surface/cancel reaction still fires; only completed/timeout pass through, else failed.
111
- status: r.status === "completed" ? "completed" : r.status === "suspended" ? "suspended" : r.status === "timeout" ? "timeout" : "failed",
111
+ status: r.status === "completed" ? "completed" : r.status === "suspended" ? "suspended" : "failed", // core 5.8.0:TaskResult "timeout"(内部词表的 timeout 仍由 fanout 墙钟臂生产)
112
112
  stats: r.stats,
113
113
  ...(r.status === "completed" ? {} : { error: r.errorMessage ?? r.blockedReason ?? `worker ${r.status}` }),
114
114
  ...(r.checkpointGate ? { checkpointGate: r.checkpointGate } : {}),
@@ -134,7 +134,8 @@ export function leaderResourceConfig(env = process.env) {
134
134
  leaderTimeoutMs,
135
135
  workerMaxTurns,
136
136
  workerBudgetUsd,
137
- workerLimits: { limits: { maxTurns: workerMaxTurns, timeoutSec: Math.floor(leaderTimeoutMs / 1000) }, maxCostUsd: workerBudgetUsd },
137
+ // core 5.8.0:预算四键全入 limits(顶层 maxCostUsd );walltime 原生毫秒不再除
138
+ workerLimits: { limits: { maxTurns: workerMaxTurns, maxWalltimeMs: leaderTimeoutMs, maxCostUsd: workerBudgetUsd } },
138
139
  presignTtlSec: Math.ceil(leaderTimeoutMs / 1000) + 600, // URL/sandbox must outlive the worker (BUG1/BUG2)
139
140
  ...(resourceSuspendOn
140
141
  ? {
@@ -223,8 +224,7 @@ export function createLeaderRunner(cfg) {
223
224
  .runTaskStream({
224
225
  objective,
225
226
  sessionId: `leader-repair-${Date.now()}-${round}`,
226
- maxCostUsd: repairBudgetUsd,
227
- limits: { timeoutSec: Math.max(300, Math.floor(leaderTimeoutMs / 1000 / 2)) },
227
+ limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
228
228
  })
229
229
  .result();
230
230
  // A non-completed repair (timeout/abort) may leave a process alive in the SAME Kata pod that would race
@@ -281,8 +281,7 @@ export function createLeaderRunner(cfg) {
281
281
  .runTaskStream({
282
282
  objective,
283
283
  sessionId: `leader-conflict-${Date.now()}-${round}`,
284
- maxCostUsd: repairBudgetUsd,
285
- limits: { timeoutSec: Math.max(300, Math.floor(leaderTimeoutMs / 1000 / 2)) },
284
+ limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
286
285
  })
287
286
  .result();
288
287
  if (res.status !== "completed")
@@ -422,7 +421,10 @@ export function createLeaderRunner(cfg) {
422
421
  resourceSpec = {
423
422
  checkpointStore: cfg.durable.checkpointStore,
424
423
  resourceSuspend: { scope: `_leader-resource-${sub.workerId}`, totalBudgetUsd: resourceCfg.totalBudgetUsd },
425
- maxCostUsd: resourceCfg.sliceMaxCostUsd,
424
+ // core 5.8.0:maxCostUsd 入 limits 后,本 spread(spec: {...workerLimits, ..., ...resourceSpec})的
425
+ // limits 键是**整体覆盖**——必须显式并回 workerLimits.limits(maxTurns/maxWalltimeMs),否则
426
+ // per-slice 覆盖会顺手把 worker 的轮数/墙钟界丢掉(5.7 时代顶层键覆盖顶层键,无此陷阱)。
427
+ limits: { ...workerLimits.limits, maxCostUsd: resourceCfg.sliceMaxCostUsd },
426
428
  maxSuspends: resourceCfg.maxSuspends,
427
429
  };
428
430
  }
package/dist/main.js CHANGED
@@ -228,8 +228,8 @@ async function main() {
228
228
  model: pick.catalogRef,
229
229
  handsReadOnly: true,
230
230
  enableFork: false,
231
- maxCostUsd: 0.05,
232
- limits: { maxTurns: 10, timeoutSec: Math.max(30, Math.ceil(timeoutMs / 1000)) },
231
+ // core 5.8.0:预算四键全入 limits(顶层 maxCostUsd 删);timeoutSec(秒)→ maxWalltimeMs(毫秒,原生同单位)
232
+ limits: { maxTurns: 10, maxCostUsd: 0.05, maxWalltimeMs: Math.max(30_000, timeoutMs) },
233
233
  // spec 无 toolPolicy → core `hasEffectAwareGate` 为假 → 每次 agent hook 触发一条
234
234
  // error 级 UNGATED 警告(非致命但纯噪声)。挂已裁决的 auto-accept 基线(与 singleUserAutoAcceptBaseline
235
235
  // 同构;hook 子代理本就 handsReadOnly + 单用户信任面,auto-accept 是既定姿态,只是让闸机制在场)。
@@ -534,27 +534,8 @@ export class RemoteHostExecutionEnv {
534
534
  env: this.mergeEnv(options?.env),
535
535
  ...spawnGroupOptions(),
536
536
  });
537
- // core 1.321 cut-kill registry(三轮复审存量缺口):把杀柄交给上层(Bash 工具 cut 时强制终杀);
538
- // 柄内 still-running 守卫(exitCode/signalCode)防对已亡组的 pgid 复用误杀(core 参考实现同语义)
539
- if (options?.onSpawn && child.pid != null) {
540
- const spawnedPid = child.pid;
541
- try {
542
- options.onSpawn({ pid: spawnedPid, kill: () => {
543
- if (child.exitCode !== null || child.signalCode !== null)
544
- return;
545
- try {
546
- killTreeHard(spawnedPid);
547
- }
548
- catch {
549
- try {
550
- child.kill("SIGKILL");
551
- }
552
- catch { /* noop */ }
553
- }
554
- } });
555
- }
556
- catch { /* registry 回调失败不挡 exec(core 同姿势吞) */ }
557
- }
537
+ // core 5.8.0:cut-kill registry seam(onSpawn/onDetachAdopted) tool-cut 机制退役(core 零生产者,
538
+ // 本臂为死码删除;1.321 时代的 still-running 守卫语义随之谢幕)。
558
539
  const finish = (r) => {
559
540
  if (settled)
560
541
  return;
@@ -629,11 +610,6 @@ export class RemoteHostExecutionEnv {
629
610
  clearTimeout(forceSettleTimer);
630
611
  options?.abortSignal?.removeEventListener("abort", onAbort);
631
612
  settled = true;
632
- // cut-kill registry 注销(所有权已移交 bgManager——cut 不该再杀后台 shell;core 参考实现同位)。
633
- try {
634
- options?.onDetachAdopted?.();
635
- }
636
- catch { /* noop */ }
637
613
  resolve(ok({ stdout: seedOut, stderr: seedErr, exitCode: 0, detached: { shellId } }));
638
614
  };
639
615
  // Named (not inline) so onDetach can `off` them when it hands the pipes to the adopted background shell.
@@ -874,26 +850,7 @@ export class RemoteHostExecutionEnv {
874
850
  throw e;
875
851
  }
876
852
  closeSpoolFds(); // 父进程的 fd 副本立即关(文件 size/EOF 只反映子进程写;launch FILE 形同姿势)
877
- // core 1.321 cut-kill registry(三轮复审存量缺口):同 pipe 径——杀柄带 still-running 守卫。
878
- if (options?.onSpawn && child.pid != null) {
879
- const spawnedPid = child.pid;
880
- try {
881
- options.onSpawn({ pid: spawnedPid, kill: () => {
882
- if (child.exitCode !== null || child.signalCode !== null)
883
- return;
884
- try {
885
- killTreeHard(spawnedPid);
886
- }
887
- catch {
888
- try {
889
- child.kill("SIGKILL");
890
- }
891
- catch { /* noop */ }
892
- }
893
- } });
894
- }
895
- catch { /* registry 回调失败不挡 exec */ }
896
- }
853
+ // core 5.8.0:cut-kill registry seam 退役(pipe 径同注)。
897
854
  // 七轮复审:'error' 监听必须在任何可提前 return 的路径(下方 F5 already-aborted 查)之前装——异步
898
855
  // spawn 失败(如 cwd 不存在)以 ChildProcess 'error' 事件发出,无监听=uncaught 直接打死 worker 进程
899
856
  // (plain-node 最小形实证 crash;vitest/tsx 宿主会吸收,故该面无法用测试钉住,靠此排序保证)。
@@ -971,11 +928,6 @@ export class RemoteHostExecutionEnv {
971
928
  if (shellId === undefined)
972
929
  return; // 拒收(limit/无 pid)→ 继续 fg,泵照跑,close 正常 settle
973
930
  handedOver = true; // spool 文件归 bg(dispose 面按 FILE 形清理)
974
- // cut-kill registry 注销(所有权已移交 bgManager;pipe 径同位)。
975
- try {
976
- options?.onDetachAdopted?.();
977
- }
978
- catch { /* noop */ }
979
931
  const o = outBuf.result();
980
932
  const e = errBuf.result();
981
933
  finish(ok({ stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes), exitCode: 0, detached: { shellId } }));
package/dist/run-local.js CHANGED
@@ -469,7 +469,8 @@ export async function runLocal(argv, deps = {}) {
469
469
  // [849]→[2400] 场景层定死终验已无生产者(autonomous 退役);OR 折入形保留,与 resolveSpec 同语义。
470
470
  ...(cap.finalVerification === true ? { finalVerification: true } : {}),
471
471
  ...(mcp ? { mcp } : {}),
472
- ...(timeoutSec !== undefined ? { limits: { timeoutSec } } : {}),
472
+ // core 5.8.0:timeoutSec 键退役 maxWalltimeMs(毫秒);taskWallClockSec 仍产秒,此处换算一次。
473
+ ...(timeoutSec !== undefined ? { limits: { maxWalltimeMs: timeoutSec * 1000 } } : {}),
473
474
  };
474
475
  logger.info("run_local_start", { scenario: scenarioName, model: spec.model, sessionId, exec: config.remoteExec?.provider ?? "in-process" });
475
476
  // ── Run ONE task to completion (the sync /v1/tasks path: plain runTask, no verify/cascade). ──
@@ -87,7 +87,8 @@ export declare function toolNameListFromBody(raw: unknown): string[] | undefined
87
87
  export declare function promptProfileFromBody(raw: unknown): "simple" | "classic" | undefined;
88
88
  /** [854]④ — 每键的运营方上限旋钮(env,缺省不设=不封顶)。 */
89
89
  export interface TaskLimitCaps {
90
- /** TASK_TIMEOUT_MAX_SEC */ timeoutSec?: number;
90
+ /** TASK_TIMEOUT_MAX_SEC(env 名不动,仍按**秒**配置;core 5.8.0 起封顶对象是 limits.maxWalltimeMs,
91
+ * 封顶比较时 ×1000 换算——运维面零迁移)。 */ timeoutSec?: number;
91
92
  /** TASK_MAX_OUTPUT_TOKENS_MAX */ maxOutputTokens?: number;
92
93
  /** TASK_MAX_TURNS_MAX */ maxTurns?: number;
93
94
  }
@@ -98,12 +99,9 @@ export interface TaskLimitCaps {
98
99
  * 运营方上限旋钮 clamp(Math.min;旋钮缺省不设=不封顶)。全空 ⇒ undefined。
99
100
  */
100
101
  export declare function normalizeLimits(v: unknown, caps: TaskLimitCaps): {
101
- timeoutSec?: number;
102
+ maxWalltimeMs?: number;
102
103
  maxOutputTokens?: number;
103
104
  maxTurns?: number;
104
- deadlineNudge?: false;
105
- callCapByDeadline?: false;
106
- gracefulFinalize?: false;
107
105
  } | undefined;
108
106
  /**
109
107
  * [854]④ — body.limits 与 env 墙钟的合成规则(resolveSpec 的唯一 limits 装配点;run-local 无 body 车道不经此):
@@ -359,6 +359,9 @@ export function promptProfileFromBody(raw) {
359
359
  export function normalizeLimits(v, caps) {
360
360
  if (v == null || typeof v !== "object" || Array.isArray(v))
361
361
  return undefined;
362
+ // core 5.8.0:键集换 maxWalltimeMs(毫秒)。旧键(timeoutSec/deadline 族三 opt-out)fresh-submit 已在
363
+ // HTTP 门响亮 400 点名替代;本层只走 resume 重放的持久化 body——旧键 defensive DROP(存量 suspended
364
+ // 体降级为默认配速,env 墙钟兜底仍在,方向安全;绝不静默换算=那是「自行兼容」,core 明令禁止)。
362
365
  const o = v;
363
366
  const pick = (raw, cap) => {
364
367
  if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 1)
@@ -367,14 +370,10 @@ export function normalizeLimits(v, caps) {
367
370
  return cap !== undefined ? Math.min(n, cap) : n;
368
371
  };
369
372
  const out = {
370
- ...(() => { const n = pick(o.timeoutSec, caps.timeoutSec); return n !== undefined ? { timeoutSec: n } : {}; })(),
373
+ // caps.timeoutSec 是秒(env TASK_TIMEOUT_MAX_SEC 保名保义),封顶毫秒键时 ×1000。
374
+ ...(() => { const n = pick(o.maxWalltimeMs, caps.timeoutSec !== undefined ? caps.timeoutSec * 1000 : undefined); return n !== undefined ? { maxWalltimeMs: n } : {}; })(),
371
375
  ...(() => { const n = pick(o.maxOutputTokens, caps.maxOutputTokens); return n !== undefined ? { maxOutputTokens: n } : {}; })(),
372
376
  ...(() => { const n = pick(o.maxTurns, caps.maxTurns); return n !== undefined ? { maxTurns: n } : {}; })(),
373
- // 历史复审轴B #4(1.254):deadline 族三个 **opt-out**(design/128/130,timeoutSec 在场即默认 ON;
374
- // server 恒注 tenancy 墙钟 ⇒ 此前恒开且 wire 无法关)。只认 literal false(纯关闭旋钮,零扩权面)。
375
- ...(o.deadlineNudge === false ? { deadlineNudge: false } : {}),
376
- ...(o.callCapByDeadline === false ? { callCapByDeadline: false } : {}),
377
- ...(o.gracefulFinalize === false ? { gracefulFinalize: false } : {}),
378
377
  };
379
378
  return Object.keys(out).length > 0 ? out : undefined;
380
379
  }
@@ -389,8 +388,11 @@ export function normalizeLimits(v, caps) {
389
388
  */
390
389
  export function resolveTaskLimits(bodyLimits, caps, envTimeoutSec, requirePrincipal, big) {
391
390
  const requested = normalizeLimits(bodyLimits, caps);
392
- const timeoutSec = requested?.timeoutSec ?? taskWallClockSec(envTimeoutSec, requirePrincipal, big);
393
- const out = { ...(requested ?? {}), ...(timeoutSec !== undefined ? { timeoutSec } : {}) };
391
+ // core 5.8.0:墙钟键=maxWalltimeMs(毫秒)。taskWallClockSec 仍以秒推导(tenancy 基值+env 只抬不降,
392
+ // 语义原样),装配处换算一次;body 显式给了 maxWalltimeMs 则直接生效(caller 意图,可低于 env )
393
+ const wallSec = taskWallClockSec(envTimeoutSec, requirePrincipal, big);
394
+ const maxWalltimeMs = requested?.maxWalltimeMs ?? (wallSec !== undefined ? wallSec * 1000 : undefined);
395
+ const out = { ...(requested ?? {}), ...(maxWalltimeMs !== undefined ? { maxWalltimeMs } : {}) };
394
396
  return Object.keys(out).length > 0 ? out : undefined;
395
397
  }
396
398
  //# sourceMappingURL=spec-fields.js.map
@@ -287,7 +287,7 @@ export declare function compactedEventData(ev: {
287
287
  modelFallback?: true;
288
288
  fallbackReason?: "window";
289
289
  clampedRatio?: number;
290
- clampReason?: "budget" | "walltime" | "tolerance";
290
+ clampReason?: "budget" | "tolerance";
291
291
  tokensAfter?: number;
292
292
  triggerTokensBefore?: number;
293
293
  durationMs?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "5.14.0",
3
+ "version": "6.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",
@@ -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.7.0",
57
+ "@sema-agent/core": "^5.8.0",
58
58
  "@sema-agent/registry-core": "^0.14.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",