@sema-agent/server 6.2.1 → 6.3.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.
@@ -67,12 +67,13 @@ export interface ProfileDeps {
67
67
  */
68
68
  runCounterfactualOracle?: () => Promise<OracleVerdicts>;
69
69
  }
70
- /** A leaf impl spec — budget already stamped by the harness (the §3.2 red line). */
70
+ /** A leaf impl spec — budget already stamped by the harness (the §3.2 red line). All three budget keys live in
71
+ * `limits` (core ≥5.8 reads them ONLY there — a top-level maxTokens/maxCostUsd would be a silently-dead key). */
71
72
  export interface SoloImplSpec {
72
73
  objective: string;
73
- maxTokens: number;
74
- maxCostUsd: number;
75
74
  limits: {
75
+ maxTokens: number;
76
+ maxCostUsd: number;
76
77
  maxTurns: number;
77
78
  };
78
79
  }
@@ -34,7 +34,8 @@
34
34
  * same anti-reward-hack transfer path SOLO/SUP use). If the integrated tree can't be imported, the cell is
35
35
  * `infraFailed` (EXCLUDED) — NEVER a fabricated uniform delivered=false loss for the whole TEAM column.
36
36
  */
37
- import type { ProfileDeps } from "./arms.js";
37
+ import { type TaskSpec } from "@sema-agent/core";
38
+ import type { ProfileDeps, SoloImplSpec, SupImplSpec } from "./arms.js";
38
39
  import type { TrapSpec } from "./tasks.js";
39
40
  /** Live runtime config — the SAME base model + budget every arm runs on (the §3.2 fairness root). */
40
41
  export interface LiveRuntimeConfig {
@@ -88,4 +89,11 @@ export interface LiveCell {
88
89
  * @param cellId a stable id for the cell (used as sandbox metadata for fleet observability).
89
90
  */
90
91
  export declare function buildLiveDeps(rt: LiveRuntimeConfig, trap: TrapSpec, seed: number | string, cellId: string): LiveCell;
92
+ /** The per-cell base TaskSpec every arm's leaf projects from. EXPORTED so the shape test measures the REAL spec
93
+ * handed to the runner (the BUDGET-MATCH anchor is spec.limits, not the harness's own RowBudget echo). */
94
+ export declare function benchBaseSpec(rt: LiveRuntimeConfig, cellId: string): (objective: string) => TaskSpec;
95
+ /** Project a `SoloImplSpec` (budget already stamped by the harness, all keys in `limits`) onto the bench base TaskSpec. */
96
+ export declare function toTaskSpec(base: (objective: string) => TaskSpec, impl: SoloImplSpec): TaskSpec;
97
+ /** Project a `SupImplSpec` onto the bench base TaskSpec PLUS the durable wiring (checkpointStore + ask policy). */
98
+ export declare function toSupTaskSpec(base: (objective: string) => TaskSpec, impl: SupImplSpec, trap: TrapSpec): TaskSpec;
91
99
  //# sourceMappingURL=live-deps.d.ts.map
@@ -272,13 +272,7 @@ function graderTransport(grader) {
272
272
  */
273
273
  export function buildLiveDeps(rt, trap, seed, cellId) {
274
274
  const { brain, models, roles, pricing } = buildBrainIngredients(rt);
275
- const baseSpec = (objective) => ({
276
- objective,
277
- sessionId: `s1-${cellId}`,
278
- maxTokens: rt.maxTokens ?? 8000,
279
- maxCostUsd: rt.maxCostUsd ?? 1.0,
280
- limits: { maxTurns: rt.maxTurns ?? 12 },
281
- });
275
+ const baseSpec = benchBaseSpec(rt, cellId);
282
276
  // Lazily-provisioned, memoized per-cell sandboxes (so a cell that never runs a seam pays zero VM cost; a seam
283
277
  // that throws still leaves them on `state` so dispose() reaps them).
284
278
  const state = {};
@@ -458,12 +452,23 @@ export function buildLiveDeps(rt, trap, seed, cellId) {
458
452
  };
459
453
  return { deps, dispose };
460
454
  }
461
- /** Project a `SoloImplSpec` (budget already stamped by the harness) onto the bench base TaskSpec. */
462
- function toTaskSpec(base, impl) {
463
- return { ...base(impl.objective), maxTokens: impl.maxTokens, maxCostUsd: impl.maxCostUsd, limits: { ...impl.limits } };
455
+ /** The per-cell base TaskSpec every arm's leaf projects from. EXPORTED so the shape test measures the REAL spec
456
+ * handed to the runner (the BUDGET-MATCH anchor is spec.limits, not the harness's own RowBudget echo). */
457
+ export function benchBaseSpec(rt, cellId) {
458
+ // 🔴 core ≥5.8: maxTokens/maxCostUsd are read ONLY from `limits` — a top-level key is silently dead. No `as
459
+ // TaskSpec` here or in the projections below: the compiler is the guard against re-introducing dead keys.
460
+ return (objective) => ({
461
+ objective,
462
+ sessionId: `s1-${cellId}`,
463
+ limits: { maxTokens: rt.maxTokens ?? 8000, maxCostUsd: rt.maxCostUsd ?? 1.0, maxTurns: rt.maxTurns ?? 12 },
464
+ });
465
+ }
466
+ /** Project a `SoloImplSpec` (budget already stamped by the harness, all keys in `limits`) onto the bench base TaskSpec. */
467
+ export function toTaskSpec(base, impl) {
468
+ return { ...base(impl.objective), limits: { ...impl.limits } };
464
469
  }
465
470
  /** Project a `SupImplSpec` onto the bench base TaskSpec PLUS the durable wiring (checkpointStore + ask policy). */
466
- function toSupTaskSpec(base, impl, trap) {
471
+ export function toSupTaskSpec(base, impl, trap) {
467
472
  const requireApproval = trap.gatedTools ?? ["Bash"];
468
473
  const neverAuto = trap.neverAuto ?? [];
469
474
  return {
@@ -4,7 +4,8 @@
4
4
  * clock that makes C2 (human-review wall-time) reproducible.
5
5
  *
6
6
  * 🔴 THE §3.2 RED LINE (design/89's #1 confound): every leaf TaskSpec across all 3 arms carries the SAME
7
- * `{ maxTokens, maxCostUsd, limits.maxTurns }`, and TEAM's per-worker budgets sum to the solo budget so no
7
+ * `limits.{ maxTokens, maxCostUsd, maxTurns }` (core ≥5.8: budget keys live ONLY in `limits`a top-level
8
+ * `maxTokens`/`maxCostUsd` is a silently-unread dead key), and TEAM's per-worker budgets sum to ≤ the solo budget — so no
8
9
  * arm can win on a bigger pie. `assertBudgetMatch` (below) is the build-time guard the shape test runs.
9
10
  *
10
11
  * 🔴 BUDGET-MATCH for TEAM is NEW WIRING the live conflict test does NOT do: `leader-conflict-resolver-live.test.ts`
@@ -37,9 +38,9 @@ export declare function budgetDescriptor(budget: BenchBudget, arm: string, teamW
37
38
  export interface BenchBudget {
38
39
  /** SAME base model all arms (design/89 §3.2 — decorrelation is a separate axis, not the value axis). */
39
40
  modelId: string;
40
- /** `TaskSpec.maxTokens` — SAME all arms. */
41
+ /** `TaskSpec.limits.maxTokens` — SAME all arms. */
41
42
  maxTokens: number;
42
- /** `TaskSpec.maxCostUsd` — SAME all arms (also the per-run spend cap). For TEAM, Σworker ≤ this. */
43
+ /** `TaskSpec.limits.maxCostUsd` — SAME all arms (also the per-run spend cap). For TEAM, Σworker ≤ this. */
43
44
  maxCostUsd: number;
44
45
  /** `TaskSpec.limits.maxTurns` — SAME all arms. */
45
46
  maxTurns: number;
@@ -89,11 +90,14 @@ export interface RunnerCtx {
89
90
  * Returns the budget fields to spread onto the spec — kept as a helper so EVERY leaf is stamped identically
90
91
  * (the live tests forget this for TEAM workers — the confound). `maxCostUsd` is overridable for the TEAM
91
92
  * per-worker split (Σ ≤ solo, enforced by `assertBudgetMatch`).
93
+ *
94
+ * 🔴 ALL THREE keys live under `limits` — core ≥5.8 reads `maxTokens`/`maxCostUsd` ONLY from `TaskSpec.limits`;
95
+ * a top-level key is DEAD (silently unread — the cost/token gates never bind).
92
96
  */
93
97
  export declare function leafBudgetFields(budget: BenchBudget, overrideMaxCostUsd?: number): {
94
- maxTokens: number;
95
- maxCostUsd: number;
96
98
  limits: {
99
+ maxTokens: number;
100
+ maxCostUsd: number;
97
101
  maxTurns: number;
98
102
  };
99
103
  };
@@ -4,7 +4,8 @@
4
4
  * clock that makes C2 (human-review wall-time) reproducible.
5
5
  *
6
6
  * 🔴 THE §3.2 RED LINE (design/89's #1 confound): every leaf TaskSpec across all 3 arms carries the SAME
7
- * `{ maxTokens, maxCostUsd, limits.maxTurns }`, and TEAM's per-worker budgets sum to the solo budget so no
7
+ * `limits.{ maxTokens, maxCostUsd, maxTurns }` (core ≥5.8: budget keys live ONLY in `limits`a top-level
8
+ * `maxTokens`/`maxCostUsd` is a silently-unread dead key), and TEAM's per-worker budgets sum to ≤ the solo budget — so no
8
9
  * arm can win on a bigger pie. `assertBudgetMatch` (below) is the build-time guard the shape test runs.
9
10
  *
10
11
  * 🔴 BUDGET-MATCH for TEAM is NEW WIRING the live conflict test does NOT do: `leader-conflict-resolver-live.test.ts`
@@ -44,12 +45,17 @@ export function makeBenchClock(startEpochMs = Date.now()) {
44
45
  * Returns the budget fields to spread onto the spec — kept as a helper so EVERY leaf is stamped identically
45
46
  * (the live tests forget this for TEAM workers — the confound). `maxCostUsd` is overridable for the TEAM
46
47
  * per-worker split (Σ ≤ solo, enforced by `assertBudgetMatch`).
48
+ *
49
+ * 🔴 ALL THREE keys live under `limits` — core ≥5.8 reads `maxTokens`/`maxCostUsd` ONLY from `TaskSpec.limits`;
50
+ * a top-level key is DEAD (silently unread — the cost/token gates never bind).
47
51
  */
48
52
  export function leafBudgetFields(budget, overrideMaxCostUsd) {
49
53
  return {
50
- maxTokens: budget.maxTokens,
51
- maxCostUsd: overrideMaxCostUsd ?? budget.maxCostUsd,
52
- limits: { maxTurns: budget.maxTurns },
54
+ limits: {
55
+ maxTokens: budget.maxTokens,
56
+ maxCostUsd: overrideMaxCostUsd ?? budget.maxCostUsd,
57
+ maxTurns: budget.maxTurns,
58
+ },
53
59
  };
54
60
  }
55
61
  /**
@@ -589,21 +589,24 @@ export function createResolveSpec(ctx) {
589
589
  // model uses its own upstream key; a model without one falls back to the gateway key. undefined
590
590
  // when no per-model keys are configured → unchanged single-key behavior.
591
591
  getApiKeyAndHeaders: getKeyResolver(), // A10 搬运改写②:活引用取值(原 `keyResolver`)
592
- // [854]④ per-request 配速:body.limits.{timeoutSec,maxOutputTokens,maxTurns} 现在被收下(核对上游
592
+ // [854]④ per-request 配速:body.limits.{maxWalltimeMs,maxOutputTokens,maxTurns} 现在被收下(核对上游
593
593
  // TB2.0 实测诉求;旧姿势「body limits 一律忽略」作废)。合成规则在 resolveTaskLimits(spec-fields.ts):
594
- // - body.timeoutSec 给了就用 body(caller 显式配速,可低于内建墙;已被可选 TASK_TIMEOUT_MAX_SEC 封顶);
594
+ // - body.maxWalltimeMs 给了就用 body(caller 显式配速,可低于内建墙;已被可选 TASK_TIMEOUT_MAX_SEC
595
+ // 封顶——env 保秒义,对毫秒键封顶时 ×1000);
595
596
  // - body 缺席保持既有姿势 = tenancy 墙钟(单用户 turnkey 无墙 / 多租 2400s、大任务 3600s,
596
597
  // clay 2026-07-04 make-real 教训①)+ env TASK_TIMEOUT_SEC 只抬不降(taskWallClockSec 内 Math.max);
597
598
  // - maxOutputTokens/maxTurns 直透传(可选 TASK_MAX_OUTPUT_TOKENS_MAX / TASK_MAX_TURNS_MAX 同款封顶)。
598
- // resume 重放持久化 body 不过 HTTP 400 门 → normalizeLimits defensive(0/负/垃圾按键 DROP,不 throw)
599
+ // resume 重放持久化 body 不过 HTTP 400 门 → normalizeLimits defensive(0/负/垃圾按键 DROP,不 throw);
600
+ // 退役键(timeoutSec/deadline 族)DROP 必须留痕:task_limits_legacy_key_dropped 带被丢键与最终生效墙钟
601
+ // (null=整任务无墙——单用户 turnkey 无 env 墙时的真后果;多租=被延到租户墙)。
599
602
  ...(() => {
600
- const limits = resolveTaskLimits(body.limits, taskLimitCaps, taskTimeoutSec, config.requirePrincipal, body.council === true || body.debate === true || scenarioName === "team");
603
+ const limits = resolveTaskLimits(body.limits, taskLimitCaps, taskTimeoutSec, config.requirePrincipal, body.council === true || body.debate === true || scenarioName === "team", (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null }));
601
604
  // [1301]③ config catalog server 半场:env 封顶不再是「五层五值互不知情」的暗手——每个真在场
602
605
  // 的运营方旋钮以 configOverrides 声明进 spec(advisory,core 折进 config.assembled 的
603
606
  // overrideReasons;「谁设的顶」变成读帧不考古)。只声明 SET 了的键(缺省不设=不污染帧)。
604
607
  const declarations = [];
605
608
  if (taskLimitCaps.timeoutSec !== undefined)
606
- declarations.push({ key: "server.limits.timeoutSecCap", value: String(taskLimitCaps.timeoutSec), reason: "env TASK_TIMEOUT_MAX_SEC (operator ceiling on caller limits.timeoutSec)" });
609
+ declarations.push({ key: "server.limits.timeoutSecCap", value: String(taskLimitCaps.timeoutSec), reason: "env TASK_TIMEOUT_MAX_SEC (seconds; operator ceiling applied to caller limits.maxWalltimeMs as cap×1000)" });
607
610
  if (taskLimitCaps.maxOutputTokens !== undefined)
608
611
  declarations.push({ key: "server.limits.maxOutputTokensCap", value: String(taskLimitCaps.maxOutputTokens), reason: "env TASK_MAX_OUTPUT_TOKENS_MAX (operator ceiling)" });
609
612
  if (taskLimitCaps.maxTurns !== undefined)
@@ -620,9 +623,9 @@ export function createResolveSpec(ctx) {
620
623
  // 「server 设的顶」,声明其来源;single-user 无墙=不声明)。
621
624
  if (config.requirePrincipal === true)
622
625
  declarations.push({ key: "server.limits.wallClockBaseSec", value: "2400/3600", reason: "tenancy wall-clock base (multi-tenant; big tasks 3600) — TASK_TIMEOUT_SEC raises, never shrinks" });
623
- // codex F1:core 目录自有键(limits.timeoutSec 等)**绝不重复声明**——core 已按真 provenance 发
626
+ // codex F1:core 目录自有键(limits.maxWalltimeMs 等)**绝不重复声明**——core 已按真 provenance 发
624
627
  // 该字段,叠一条 host-declared 字符串值=同键双条矛盾帧(by-key 投影抹掉权威来源)。server 只
625
- // 声明自己命名空间的键;来源语义(caller vs 墙钟)由 timeoutSec 的 spec provenance + 上面的
628
+ // 声明自己命名空间的键;来源语义(caller vs 墙钟)由 maxWalltimeMs 的 spec provenance + 上面的
626
629
  // Cap 声明组合可读。
627
630
  // core 5.8.0:预算族(maxCostUsd/maxTokens/degrade)从 TaskSpec 顶层迁入 limits——顶层键已删,
628
631
  // 继续写顶层=经宽类型静默死键(E-HIGH-1 病族)。wire 面不变:body 顶层 maxCostUsd/maxTokens 照收
@@ -942,8 +942,9 @@ export function createHttpServer(rawDeps) {
942
942
  // [854]④: fresh-submit shape validation for per-request 配速 —— 已知三键必须是正整数,否则 400 fail-loud
943
943
  // (body.model silent-drop 同类教训:静默 normalize 成「没配速」正是本件要防的假成功)。0 也拒绝:core 侧
944
944
  // `timeoutSec > 0` 才生效(0=不设墙,会绕过 TASK_TIMEOUT_MAX_SEC 封顶),maxTurns/maxOutputTokens 的 0 无意义。
945
- // 枚举外键与 resilience/attachments 同口径:容忍(只校验已知键)。resolveSpec 的 normalizeLimits 对 RESUME
946
- // 重放路径保持 defensive(按键 DROP,不 throw)。
945
+ // 枚举外键与 resilience/attachments 同口径:容忍(只校验已知键)——但退役键与预算族错位键**点名 400**
946
+ // (它们是「caller 有明确意图、本层却不会采纳」的已知键形,容忍=静默吞意图)。resolveSpec 的
947
+ // normalizeLimits 对 RESUME 重放路径保持 defensive(按键 DROP,不 throw)。
947
948
  if (body.limits !== undefined) {
948
949
  const l = body.limits;
949
950
  if (typeof l !== "object" || l === null || Array.isArray(l)) {
@@ -953,7 +954,8 @@ export function createHttpServer(rawDeps) {
953
954
  // core 5.8.0 时限重构:旧键响亮拒并点名替代(core 同姿势 config.limit_unknown_key;「勿自行兼容」是
954
955
  // core 的公开指令)。timeoutSec(秒)→ maxWalltimeMs(毫秒,注意单位);deadline 族三 opt-out 随
955
956
  // 机制整族退役(nudge/call-cap/graceful-finalize 已从引擎移除),无替代键。fresh fail-loud;resume
956
- // 重放不过此门,normalizeLimits 对旧键 defensive DROP(存量 suspended 体降级为默认配速,方向安全)。
957
+ // 重放不过此门,resolveTaskLimits 对旧键 defensive DROP + 具名留痕(task_limits_legacy_key_dropped:
958
+ // 存量 suspended 体降级为默认配速——单用户无 env 墙时=整任务无墙,多租=延到租户墙,非「方向安全」)。
957
959
  if (l.timeoutSec !== undefined) {
958
960
  sendError(res, 400, "request.field_invalid", "limits.timeoutSec was retired in core 5.8.0 — use limits.maxWalltimeMs (milliseconds; opt-in wall clock)");
959
961
  return null;
@@ -969,6 +971,25 @@ export function createHttpServer(rawDeps) {
969
971
  return null;
970
972
  }
971
973
  }
974
+ // 预算族错位键(core 5.8.0「预算入 limits」的单向缺口):caller 按 core 键形写 limits.maxCostUsd/
975
+ // maxTokens/degrade/budgetStreamCancel 时,本服务的采纳面在 body **顶层**(resolve-spec.ts
976
+ // cappedCeiling)/运营方 env(MODEL_DEGRADE_*)/core 默认(budgetStreamCancel=maxCostUsd 在场即
977
+ // true)——不点名拒则被静默吞掉,方向「更松」(caller 以为封了 0.5 刀,实际无成本上限)。
978
+ // 退役键同姿势 fail-loud;resume 重放不过此门,normalizeLimits 对这四键维持 defensive DROP。
979
+ for (const k of ["maxCostUsd", "maxTokens"]) {
980
+ if (l[k] !== undefined) {
981
+ sendError(res, 400, "request.field_invalid", `limits.${k} is not read from limits here — send it at the request body top level (${k}); it is capped by the operator ceiling ${k === "maxCostUsd" ? "MAX_TASK_COST_USD" : "MAX_TASK_TOKENS"}`);
982
+ return null;
983
+ }
984
+ }
985
+ if (l.degrade !== undefined) {
986
+ sendError(res, 400, "request.field_invalid", "limits.degrade is operator-controlled (env MODEL_DEGRADE_TO / MODEL_DEGRADE_AT_COST_FRACTION), not caller-settable — remove it");
987
+ return null;
988
+ }
989
+ if (l.budgetStreamCancel !== undefined) {
990
+ sendError(res, 400, "request.field_invalid", "limits.budgetStreamCancel is not caller-settable here — the engine default applies (armed whenever a cost budget is present); remove it");
991
+ return null;
992
+ }
972
993
  for (const k of ["maxWalltimeMs", "maxOutputTokens", "maxTurns"]) {
973
994
  const v = l[k];
974
995
  if (v !== undefined && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
@@ -14,7 +14,28 @@ function parseRecord(raw, key) {
14
14
  if (typeof v !== "object" || v === null || !Array.isArray(r.slots) || !Array.isArray(r.buckets)) {
15
15
  throw new Error(`usage_window record for key "${key}" has an invalid shape (expected {slots[],buckets[]}) — refusing to mis-count a governance window`);
16
16
  }
17
- return v;
17
+ // 深度对齐 core FileUsageWindowStore(validateSlots/validateBuckets):内层坏形若放过,非数值
18
+ // tokens 参与累加得 "0oops" ⇒ exhausted:false,超额账户被判未耗尽。逐条校验,任一不合即 fail-loud。
19
+ const badShape = (detail) => new Error(`usage_window record for key "${key}" has an invalid shape (${detail}) — refusing to mis-count a governance window`);
20
+ const finite = (x) => typeof x === "number" && Number.isFinite(x);
21
+ const slots = r.slots.map((s) => {
22
+ if (s === null || typeof s !== "object")
23
+ throw badShape("`slots` entry is not an object");
24
+ const { at, tokens } = s;
25
+ if (!finite(at) || !finite(tokens))
26
+ throw badShape("`slots` entry has a non-finite-numeric `at`/`tokens`");
27
+ return { at, tokens };
28
+ });
29
+ const buckets = r.buckets.map((b) => {
30
+ if (b === null || typeof b !== "object")
31
+ throw badShape("`buckets` entry is not an object");
32
+ const { windowMs, openedAt, tokens } = b;
33
+ if (!finite(windowMs) || windowMs <= 0 || !finite(openedAt) || !finite(tokens)) {
34
+ throw badShape("`buckets` entry has a non-finite-numeric `windowMs`/`openedAt`/`tokens` (windowMs must be > 0)");
35
+ }
36
+ return { windowMs, openedAt, tokens };
37
+ });
38
+ return { slots, buckets };
18
39
  }
19
40
  export async function ensureTiDBUsageWindowSchema(pool) {
20
41
  await pool.query(`CREATE TABLE IF NOT EXISTS ${USAGE_WINDOW_TABLE} (
@@ -93,12 +93,12 @@ export interface TaskLimitCaps {
93
93
  /** TASK_MAX_TURNS_MAX */ maxTurns?: number;
94
94
  }
95
95
  /**
96
- * [854]④ — normalize `body.limits` → 请求方配速意图 {timeoutSec?, maxOutputTokens?, maxTurns?}。DEFENSIVE
96
+ * [854]④ — normalize `body.limits` → 请求方配速意图 {maxWalltimeMs?, maxOutputTokens?, maxTurns?}。DEFENSIVE
97
97
  * (resume 重放持久化 body 不过 HTTP 400 门):仅正的有限数字存活(floor 取整),0/负/垃圾按键 DROP —— core 侧
98
- * `timeoutSec > 0` 才生效(0=不设墙,会绕过运营方封顶)、maxTurns/maxOutputTokens 的 0 无意义。每键各自被
98
+ * 墙钟 > 0 才生效(0=不设墙,会绕过运营方封顶)、maxTurns/maxOutputTokens 的 0 无意义。每键各自被
99
99
  * 运营方上限旋钮 clamp(Math.min;旋钮缺省不设=不封顶)。全空 ⇒ undefined。
100
100
  */
101
- export declare function normalizeLimits(v: unknown, caps: TaskLimitCaps): {
101
+ export declare function normalizeLimits(v: unknown, caps: TaskLimitCaps, warn?: (event: string, fields: Record<string, unknown>) => void): {
102
102
  maxWalltimeMs?: number;
103
103
  maxOutputTokens?: number;
104
104
  maxTurns?: number;
@@ -120,5 +120,5 @@ export declare function normalizeApproachNotice(raw: unknown): false | {
120
120
  * - `maxOutputTokens` / `maxTurns`:直透传(各自可选 env 上限已 clamp)。
121
121
  * 全空 ⇒ undefined(spec 不挂 limits 键,byte-compat)。
122
122
  */
123
- export declare function resolveTaskLimits(bodyLimits: unknown, caps: TaskLimitCaps, envTimeoutSec: number, requirePrincipal: boolean | undefined, big: boolean): TaskSpec["limits"];
123
+ export declare function resolveTaskLimits(bodyLimits: unknown, caps: TaskLimitCaps, envTimeoutSec: number, requirePrincipal: boolean | undefined, big: boolean, warn?: (event: string, fields: Record<string, unknown>) => void): TaskSpec["limits"];
124
124
  //# sourceMappingURL=spec-fields.d.ts.map
@@ -350,18 +350,32 @@ export function toolNameListFromBody(raw) {
350
350
  export function promptProfileFromBody(raw) {
351
351
  return raw === "simple" || raw === "classic" ? raw : undefined;
352
352
  }
353
+ /** core 5.8.0 时限重构的退役键(fresh-submit 在 HTTP 门 400 点名;resume 重放在 normalize 层 DROP)。 */
354
+ const RETIRED_LIMIT_KEYS = ["timeoutSec", "deadlineNudge", "callCapByDeadline", "gracefulFinalize"];
355
+ /** 在场的退役键(normalizeLimits/resolveTaskLimits 共用判据源:留痕事件的 droppedKeys 字段)。 */
356
+ function retiredLimitKeysPresent(v) {
357
+ if (v == null || typeof v !== "object" || Array.isArray(v))
358
+ return [];
359
+ const o = v;
360
+ return RETIRED_LIMIT_KEYS.filter((k) => o[k] !== undefined);
361
+ }
353
362
  /**
354
- * [854]④ — normalize `body.limits` → 请求方配速意图 {timeoutSec?, maxOutputTokens?, maxTurns?}。DEFENSIVE
363
+ * [854]④ — normalize `body.limits` → 请求方配速意图 {maxWalltimeMs?, maxOutputTokens?, maxTurns?}。DEFENSIVE
355
364
  * (resume 重放持久化 body 不过 HTTP 400 门):仅正的有限数字存活(floor 取整),0/负/垃圾按键 DROP —— core 侧
356
- * `timeoutSec > 0` 才生效(0=不设墙,会绕过运营方封顶)、maxTurns/maxOutputTokens 的 0 无意义。每键各自被
365
+ * 墙钟 > 0 才生效(0=不设墙,会绕过运营方封顶)、maxTurns/maxOutputTokens 的 0 无意义。每键各自被
357
366
  * 运营方上限旋钮 clamp(Math.min;旋钮缺省不设=不封顶)。全空 ⇒ undefined。
358
367
  */
359
- export function normalizeLimits(v, caps) {
368
+ export function normalizeLimits(v, caps, warn) {
360
369
  if (v == null || typeof v !== "object" || Array.isArray(v))
361
370
  return undefined;
362
371
  // core 5.8.0:键集换 maxWalltimeMs(毫秒)。旧键(timeoutSec/deadline 族三 opt-out)fresh-submit 已在
363
- // HTTP 门响亮 400 点名替代;本层只走 resume 重放的持久化 body——旧键 defensive DROP(存量 suspended
364
- // 体降级为默认配速,env 墙钟兜底仍在,方向安全;绝不静默换算=那是「自行兼容」,core 明令禁止)。
372
+ // HTTP 门响亮 400 点名替代;本层只走 resume 重放的持久化 body——旧键 defensive DROP 但**必须留痕**
373
+ // (task_limits_legacy_key_dropped):丢 caller 显式时限=收窄契约,存量 suspended 体降级为默认配速时,
374
+ // 单用户 turnkey 无 env 墙 ⇒ 整任务无墙钟、多租 ⇒ 被延到租户墙(2400/3600s)——两向都可能长于原意图。
375
+ // 绝不静默换算=那是「自行兼容」,core 明令禁止。
376
+ const dropped = retiredLimitKeysPresent(v);
377
+ if (dropped.length > 0)
378
+ warn?.("task_limits_legacy_key_dropped", { droppedKeys: dropped });
365
379
  const o = v;
366
380
  const pick = (raw, cap) => {
367
381
  if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 1)
@@ -405,12 +419,17 @@ export function normalizeApproachNotice(raw) {
405
419
  * - `maxOutputTokens` / `maxTurns`:直透传(各自可选 env 上限已 clamp)。
406
420
  * 全空 ⇒ undefined(spec 不挂 limits 键,byte-compat)。
407
421
  */
408
- export function resolveTaskLimits(bodyLimits, caps, envTimeoutSec, requirePrincipal, big) {
422
+ export function resolveTaskLimits(bodyLimits, caps, envTimeoutSec, requirePrincipal, big, warn) {
423
+ // 留痕事件在本层发(不透传给 normalizeLimits,免双发):只有装配点知道最终生效墙钟——
424
+ // effectiveMaxWalltimeMs: null = 整任务无墙(单用户 turnkey 无 env 墙时丢弃旧 timeoutSec 的真后果)。
425
+ const dropped = retiredLimitKeysPresent(bodyLimits);
409
426
  const requested = normalizeLimits(bodyLimits, caps);
410
427
  // core 5.8.0:墙钟键=maxWalltimeMs(毫秒)。taskWallClockSec 仍以秒推导(tenancy 基值+env 只抬不降,
411
428
  // 语义原样),装配处换算一次;body 显式给了 maxWalltimeMs 则直接生效(caller 意图,可低于 env 墙)。
412
429
  const wallSec = taskWallClockSec(envTimeoutSec, requirePrincipal, big);
413
430
  const maxWalltimeMs = requested?.maxWalltimeMs ?? (wallSec !== undefined ? wallSec * 1000 : undefined);
431
+ if (dropped.length > 0)
432
+ warn?.("task_limits_legacy_key_dropped", { droppedKeys: dropped, effectiveMaxWalltimeMs: maxWalltimeMs ?? null });
414
433
  const out = { ...(requested ?? {}), ...(maxWalltimeMs !== undefined ? { maxWalltimeMs } : {}) };
415
434
  return Object.keys(out).length > 0 ? out : undefined;
416
435
  }
@@ -25,8 +25,8 @@ type BgNotifExcluded = "kind" | "sessionScoped" | "owner" | "scope" | "descripti
25
25
  type _GuardBgNotif = AssertAllKeysHandled<Exclude<keyof BackgroundChildEvent, BgNotifProjected | BgNotifExcluded>>;
26
26
  type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "createdAt";
27
27
  type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
28
- type AskProjected = "toolName" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation";
29
- type AskExcluded = "toolCallId" | "preview" | "principal" | "requiresRealApproval";
28
+ type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation";
29
+ type AskExcluded = "preview" | "principal" | "requiresRealApproval";
30
30
  type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
31
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>>;
@@ -89,6 +89,7 @@ export type TraceBlock = {
89
89
  truncated?: boolean;
90
90
  totalChars?: number;
91
91
  structured?: unknown;
92
+ errorCode?: string;
92
93
  eventId?: string;
93
94
  parentToolCallId?: string;
94
95
  } | {
@@ -334,6 +335,8 @@ export declare function contextUsageEventData(ev: {
334
335
  usedTokens?: unknown;
335
336
  windowTokens?: unknown;
336
337
  compactAtTokens?: unknown;
338
+ eventId?: unknown;
339
+ parentToolCallId?: unknown;
337
340
  }): Record<string, unknown>;
338
341
  /**
339
342
  * `turn_end` 的白名单投影 —— 🔴 2026-07-25 补:`turn_end` 此前是**唯一没有 builder 的内容臂**,两条持久化点
@@ -348,6 +351,8 @@ export declare function turnEndEventData(ev: {
348
351
  usage?: unknown;
349
352
  usageMissing?: true;
350
353
  stopReason?: string;
354
+ eventId?: unknown;
355
+ parentToolCallId?: unknown;
351
356
  }): Record<string, unknown>;
352
357
  /** Map core's `turn_end.usage` (1.76 / core [R59]: inputTokens/outputTokens/cacheReadTokens/cacheWriteTokens/
353
358
  * costMicroUsd; core 3.0.0 adds required `totalInputTokens`) → the contract's `TraceTurn.tokens`.
@@ -156,9 +156,13 @@ export function toolEndEventData(ev) {
156
156
  // core 1.442([1898] RB-210):截断诚实体量(各 block 真实大小之和,不含 JSON 包装开销)。
157
157
  ...(Number.isFinite(ev.totalChars) ? { totalChars: ev.totalChars } : {}),
158
158
  ...(ev.structured !== undefined ? { structured: redactDeep(ev.structured) } : {}),
159
- // core 5.9.0 W3([2535]):park/死投影结构化标记(引擎闭词表 `gate.parked`/`tool.not_found`,只在
160
- // isError 时在场)——verbatim 透传,壳按它判别 park 毒化帧而不锚 abort 文案字面。
161
- ...(typeof ev.errorCode === "string" ? { errorCode: ev.errorCode } : {}),
159
+ // core 5.9.0 W3([2535];2026-08-04 复审纠偏):errorCode = core 在 isError 时对**任意工具 ToolResult
160
+ // `details.code`** 的透传 —— 开集,无引擎白名单(runtask 直读 result.details.code,structuredFrom
161
+ // CC_DETAIL_TYPES 白名单只管 structured 键)。引擎码 `gate.parked`/`tool.not_found` 只是两例,
162
+ // 工具层现役即有 `path_not_in_root`/`readonly_out_of_root` 等,自注册工具可加新码 —— 消费端必须
163
+ // 带 default 臂,禁按两值穷举。verbatim 依据:码是机器短串标识符、非用户内容(不脱敏),边界靠
164
+ // 显式上限而非词表 —— >128 字符=非法形,整键丢弃(不截断:截断会铸出 core 从未发过的码)。
165
+ ...(typeof ev.errorCode === "string" && ev.errorCode.length <= 128 ? { errorCode: ev.errorCode } : {}),
162
166
  ...identityFields(ev),
163
167
  };
164
168
  }
@@ -425,6 +429,8 @@ export function contextUsageEventData(ev) {
425
429
  ...(n(ev.usedTokens) !== undefined ? { usedTokens: n(ev.usedTokens) } : {}),
426
430
  ...(n(ev.windowTokens) !== undefined ? { windowTokens: n(ev.windowTokens) } : {}),
427
431
  ...(n(ev.compactAtTokens) !== undefined ? { compactAtTokens: n(ev.compactAtTokens) } : {}),
432
+ // [2557] 2.4:本分支带 TaskEventIdentity 而 builder 曾剥——差集门补进三构造器当场红出(compacted 同病先例)。
433
+ ...identityFields(ev),
428
434
  };
429
435
  }
430
436
  /**
@@ -441,6 +447,8 @@ export function turnEndEventData(ev) {
441
447
  ...(ev.usage ? { usage: ev.usage } : {}),
442
448
  ...(ev.usageMissing === true ? { usageMissing: true } : {}),
443
449
  ...(typeof ev.stopReason === "string" ? { stopReason: ev.stopReason } : {}),
450
+ // [2557] 2.4:同 contextUsage——identity 键(eventId/parentToolCallId)曾被剥,差集门红出补齐。
451
+ ...identityFields(ev),
444
452
  };
445
453
  }
446
454
  /** Map core's `turn_end.usage` (1.76 / core [R59]: inputTokens/outputTokens/cacheReadTokens/cacheWriteTokens/
@@ -486,6 +494,9 @@ export function toolResultFieldsOf(d) {
486
494
  ...(d.truncated ? { truncated: true } : {}),
487
495
  ...(Number.isFinite(d.totalChars) ? { totalChars: d.totalChars } : {}),
488
496
  ...(d.structured !== undefined ? { structured: d.structured } : {}),
497
+ // core 5.9.0 W3([2535]):机器可判别的 park/死投影码(`gate.parked`/`tool.not_found`)——写侧
498
+ // toolEndEventData 已带,读腿必须同键 verbatim(absent⇒absent),否则冷回放退回锚 abort 文案字面。
499
+ ...(typeof d.errorCode === "string" ? { errorCode: d.errorCode } : {}),
489
500
  ...identityFields(d),
490
501
  };
491
502
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "6.2.1",
3
+ "version": "6.3.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",
@@ -68,7 +68,7 @@
68
68
  "sharp": "^0.35.3"
69
69
  },
70
70
  "devDependencies": {
71
- "@sema-agent/sdk": "^6.1.0",
71
+ "@sema-agent/sdk": "^6.2.0",
72
72
  "@types/libsodium-wrappers": "^0.7.14",
73
73
  "@types/node": "22.10.2",
74
74
  "@types/pg": "^8.20.0",