@sema-agent/server 5.13.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
@@ -164,6 +164,11 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
164
164
  - `/health` 身份字段承诺:`pid`(= 引擎进程)与 `dataRoot`(= 生效数据根,解析恒回退 `~/.ai-agent`,
165
165
  与 DB_BACKEND 无关)**恒在**——宿主用它们验证「这个端口上的 /health 是不是我起的那个引擎」。
166
166
  钉:`test/health-identity-contract.test.ts`。
167
+ - `/health` 活体 store 探测(5.14.0,SQL 后端专属 additive):后台探针(`STORE_PROBE_INTERVAL_MS`,
168
+ 默认 15000;0=关;负/非数启动响亮拒)缓存一份 DB 真往返结果,`storeProbe:{live,ageMs,error?}` 在
169
+ 探针接线时恒在,顶层告警键 `storeLive:false` 只在死时出现。**status 保持 "ok"**(liveness≠readiness
170
+ ——DB 死不是进程死,摘流语义留给读键的编排器/LB)。local/memory 部署形状不变。
171
+ 钉:`test/health-store-live.test.ts` / `test/store-live-probe.test.ts`。
167
172
  - 数据驻留提示:`DB_BACKEND=local` 下显式 `SESSION_BACKEND=memory` 会被收编为 **durable(local)**
168
173
  (1.292+ 裸 boot 默认 durable;/health 的 `sessionBackend` 报 `durable(local)`)——session 行落盘在
169
174
  数据根下,清数据/隐私预期要按「sessions 在 engine-data 里」来做,不要按「只在内存」。
@@ -216,6 +221,9 @@ SEMA_REGISTRY_URL=http://<config-center-host>:3100 # 启动拉 GET /api/config
216
221
  SEMA_REGISTRY_TOKEN=<SERVICE_PULL_TOKEN 的值> # 取自配置控制面主机 .env;只读拉取令牌
217
222
  SEMA_REGISTRY_DRY_RUN=true # 安全灰度:只 LOG 中心配置 vs env 推导的差异,不 apply
218
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 永不注册)。
219
227
  ```
220
228
  - 中心**空/未发布** → `applyEffective` 回落 env + 内建 teams 并 warn `config_center_unpublished`,**不影响在跑的服务**(接了也安全)。
221
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 });
@@ -166,6 +166,9 @@ export interface ServiceConfigFlat {
166
166
  sessionBackend: "memory" | "tidb" | "auto";
167
167
  /** Warm-cache idle TTL (seconds) for woken TiDB sessions; 0 disables. Requires session affinity. */
168
168
  sessionCacheTtlSec: number;
169
+ /** B1 /health 活体 store 探测:后台缓存探针间隔 ms(STORE_PROBE_INTERVAL_MS;0=显式关,负/非数 boot
170
+ * 响亮拒)。只对 SQL 后端接线;探针原语=backend.dbNowMs()(S10 时钟探针的真 DB 往返,零新 SQL 面)。 */
171
+ storeProbeIntervalMs: number;
169
172
  /** Rewind snapshot bounds: byte cap in MB (env REWIND_SNAPSHOT_MAX_MB). Unset = core DEFAULT_SNAPSHOT_BOUNDS
170
173
  * (256 MiB / 10000 files as of core 2026-06-26 — was 2000/64MiB). BYTE-ONLY knob: raises OR lowers just the byte
171
174
  * cap; file-count/ignoreDirs stay core defaults (more knobs when a case shows up). */
@@ -873,7 +876,7 @@ export interface ServiceConfigFlat {
873
876
  configLocalDir?: string;
874
877
  }
875
878
  /** 组:store(持久化)—— DB 引擎三态、session 后端、SQL coords、快照 BLOB / SendUserFile 对象存储。 */
876
- export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "sessionCacheTtlSec" | "rewindSnapshotMaxMb" | "dbBackend" | "dbBackendExplicit" | "localDataRoot" | "tidb" | "pg" | "dbQueryTimeoutMs" | "snapshotBlobStore" | "snapshotBlobSqlMaxBytes" | "snapshotBlobAllowSql" | "sendUserFile">;
879
+ export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "sessionCacheTtlSec" | "storeProbeIntervalMs" | "rewindSnapshotMaxMb" | "dbBackend" | "dbBackendExplicit" | "localDataRoot" | "tidb" | "pg" | "dbQueryTimeoutMs" | "snapshotBlobStore" | "snapshotBlobSqlMaxBytes" | "snapshotBlobAllowSql" | "sendUserFile">;
877
880
  /** 组:modelPlane(模型面)—— 网关坐标、Anthropic 路线、韧性旋钮、主/廉价 model entry、role 表、降级梯。 */
878
881
  export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "cascadeLadder" | "degrade">;
879
882
  /** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
package/dist/config.js CHANGED
@@ -449,6 +449,9 @@ function parseStoreDomain(ctx) {
449
449
  return {
450
450
  sessionBackend,
451
451
  sessionCacheTtlSec: numEnv("SESSION_CACHE_TTL_SEC", "300"), // A2 同族:NaN ⇒ `> 0` 假 ⇒ 缓存静默关
452
+ // B1(/health 活体 store 探测):后台探针间隔;0=显式关,负值/非数响亮拒(fail-loud 族)。
453
+ // 只在 SQL 后端接线(main.ts 门 backend.kind!=="local"),local/memory 部署恒不挂环。
454
+ storeProbeIntervalMs: numEnvBounded("STORE_PROBE_INTERVAL_MS", "15000", 0, 3_600_000),
452
455
  rewindSnapshotMaxMb: optFinitePositiveEnv("REWIND_SNAPSHOT_MAX_MB"), // soft knob (S20: bad value warns + default)
453
456
  dbBackend,
454
457
  dbBackendExplicit: dbBackendSet,
@@ -1305,7 +1308,7 @@ function parseIntegrationsDomain() {
1305
1308
  };
1306
1309
  }
1307
1310
  const STORE_GROUP_KEYS = [
1308
- "sessionBackend", "sessionCacheTtlSec", "rewindSnapshotMaxMb", "dbBackend", "dbBackendExplicit", "localDataRoot",
1311
+ "sessionBackend", "sessionCacheTtlSec", "storeProbeIntervalMs", "rewindSnapshotMaxMb", "dbBackend", "dbBackendExplicit", "localDataRoot",
1309
1312
  "tidb", "pg", "dbQueryTimeoutMs", "snapshotBlobStore", "snapshotBlobSqlMaxBytes", "snapshotBlobAllowSql", "sendUserFile",
1310
1313
  ];
1311
1314
  const MODEL_PLANE_GROUP_KEYS = [
@@ -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
  }
@@ -398,6 +398,17 @@ export interface ServiceDeploymentDeps {
398
398
  since: number;
399
399
  blocked?: string[];
400
400
  } | undefined;
401
+ /** B1(鲁棒性批3 设计件):后台**缓存**探针的读座——/health 绝不逐请求打 DB(center/k8s 高频面),
402
+ * main.ts 只在 SQL 后端(backend.kind !== "local")挂环(createStoreLiveProbe,STORE_PROBE_INTERVAL_MS)。
403
+ * 缺席(local/memory/探针关)= /health 形状不变;接线 = `storeProbe{live,ageMs,error?}` 恒在
404
+ * (「探过且活」与「没接线」机读可分),顶层告警键 `storeLive:false` 只在死时出现。status 恒 "ok"
405
+ * ——liveness≠readiness,DB 死不是进程死;摘流语义留给读键的编排器(与 durable/ready 同姿势,
406
+ * 披露不代裁)。钉:test/health-store-live.test.ts。 */
407
+ storeLiveState?: () => {
408
+ live: boolean;
409
+ ageMs: number;
410
+ error?: string;
411
+ } | undefined;
401
412
  /** ① core ruling — SPLIT: `capabilities.workflows` = the ENGINE-CAN axis, boot-computed from core's own
402
413
  * `workflowsCapability(deps)` (hardened script runner ∧ governance), NOT the `Boolean(workflowRunStore)` store
403
414
  * proxy. Orthogonal to `workflowsList` (the durable-list axis = `workflowRunStore`). Falls back to the store
@@ -402,6 +402,16 @@ export function createHttpServer(rawDeps) {
402
402
  const stuck = deps.planeDeferredState?.();
403
403
  return stuck ? { modelPlaneDeferred: { version: stuck.version, since: stuck.since, noHandoff: true, ...(stuck.blocked ? { blockedReasons: stuck.blocked } : {}) } } : {};
404
404
  })(),
405
+ // B1 活体 store 探测(座注见 storeLiveState 声明;钉 test/health-store-live.test.ts)
406
+ ...(() => {
407
+ const probe = deps.storeLiveState?.();
408
+ if (!probe)
409
+ return {};
410
+ return {
411
+ ...(probe.live ? {} : { storeLive: false }),
412
+ storeProbe: { live: probe.live, ageMs: probe.ageMs, ...(probe.error !== undefined ? { error: probe.error } : {}) },
413
+ };
414
+ })(),
405
415
  });
406
416
  return;
407
417
  }
@@ -937,23 +947,27 @@ export function createHttpServer(rawDeps) {
937
947
  if (body.limits !== undefined) {
938
948
  const l = body.limits;
939
949
  if (typeof l !== "object" || l === null || Array.isArray(l)) {
940
- 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)");
941
951
  return null;
942
952
  }
943
- for (const k of ["timeoutSec", "maxOutputTokens", "maxTurns"]) {
944
- const v = l[k];
945
- if (v !== undefined && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
946
- 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)`);
947
964
  return null;
948
965
  }
949
966
  }
950
- // 快审 F2(1.254):deadline 族三 opt-out 在场必须 literal false——"false"/0 等畸形值此前 200 后被
951
- // normalizeLimits 静默丢=调用方以为关了其实恒开(B1 同类故障)。fresh fail-loud;resume 重放不过
952
- // 此门,normalizeLimits 保持 defensive。
953
- for (const k of ["deadlineNudge", "callCapByDeadline", "gracefulFinalize"]) {
954
- const v = body.limits[k];
955
- if (v !== undefined && v !== false) {
956
- 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)" : ""}`);
957
971
  return null;
958
972
  }
959
973
  }
@@ -2190,10 +2204,11 @@ export function createHttpServer(rawDeps) {
2190
2204
  deps.metrics?.inc("task_tokens_total", {}, result.stats.tokens);
2191
2205
  if (result.stats.cacheHitRate !== undefined)
2192
2206
  deps.metrics?.observe("task_cache_hit_rate", result.stats.cacheHitRate);
2193
- // ⑤ budget/limit cutoffs (1.37): count failures by their dotted code (budget.precall/exceeded,
2194
- // 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 自述新码)。
2195
2210
  const code = result.errorCode;
2196
- if (code && (code.startsWith("budget.") || code.startsWith("limit."))) {
2211
+ if (code && code.startsWith("limits.")) {
2197
2212
  deps.metrics?.inc("budget_exceeded_total", { code });
2198
2213
  }
2199
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
@@ -30,6 +30,7 @@ import { performMemorySync } from "./memory-sync.js";
30
30
  import { startOtlpExporter } from "./observability/otel-exporter.js";
31
31
  import { HEARTBEAT_MS, backgroundAgentOutput, taskHandleOutput, taskHandleStop } from "./runs.js";
32
32
  import { SQL_BLOB_DEFAULT_MAX_BYTES } from "./plugins/blob-backend.js";
33
+ import { createStoreLiveProbe } from "./store-live-probe.js";
33
34
  import { counterStoreLabel } from "./plugins/store-backend.js";
34
35
  import { composeHooks } from "./hooks/hook-runner.js";
35
36
  import { createHookLlm } from "./hooks/hook-llm.js";
@@ -227,8 +228,8 @@ async function main() {
227
228
  model: pick.catalogRef,
228
229
  handsReadOnly: true,
229
230
  enableFork: false,
230
- maxCostUsd: 0.05,
231
- 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) },
232
233
  // spec 无 toolPolicy → core `hasEffectAwareGate` 为假 → 每次 agent hook 触发一条
233
234
  // error 级 UNGATED 警告(非致命但纯噪声)。挂已裁决的 auto-accept 基线(与 singleUserAutoAcceptBaseline
234
235
  // 同构;hook 子代理本就 handsReadOnly + 单用户信任面,auto-accept 是既定姿态,只是让闸机制在场)。
@@ -800,6 +801,10 @@ async function main() {
800
801
  costQuota,
801
802
  fleetLease: fleetLease ? fleetLease : undefined, // lease admission 门(提交面,镜像 quotaExceeded)
802
803
  };
804
+ // B1 活体 store 探测环(门与判据见 store-live-probe.ts 头注;探针原语=既有 S10 dbNowMs 真 DB 往返)
805
+ const storeLiveProbe = backend && backend.kind !== "local" && backend.dbNowMs && config.storeProbeIntervalMs > 0
806
+ ? createStoreLiveProbe({ probe: () => backend.dbNowMs(), intervalMs: config.storeProbeIntervalMs, logger })
807
+ : undefined;
803
808
  /** 部署自述面(/health + /v1/capabilities + 提交前置门读的部署事实;多为 live getter) */
804
809
  const deployment = {
805
810
  // 海外 pilot 实机发现:/health 曾 verbatim 回显 sessionBackend 枚举——auto/DB_BACKEND 收编把它
@@ -833,6 +838,11 @@ async function main() {
833
838
  // shared drain state — SIGTERM flips `draining`, createServer assigns `inflight`, /health mirrors it.
834
839
  drainState,
835
840
  storeDegraded: storeBackendDegraded, // S5: /health twin of the store_backend_degraded gauge
841
+ // B1 活体 store 探测:后台缓存环(座注见 http/server.ts storeLiveState;环判据见 store-live-probe.ts)。
842
+ // 门 = SQL 后端 ∧ dbNowMs 在场 ∧ 旋钮>0;local/memory 恒不挂(/health 形状不变)。timer 全 unref,
843
+ // 进程退出零阻塞;pool 生命周期归 backend.close()。直键非条件展开(deps-literal-shape 门:条件
844
+ // 展开旁路多余属性检查——E-HIGH-1 死键病族)。
845
+ storeLiveState: storeLiveProbe ? () => storeLiveProbe.state() : undefined,
836
846
  };
837
847
  /** 数值旋钮(缺省写在消费点,此处只承载覆写) */
838
848
  const knobs = {
@@ -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
@@ -0,0 +1,40 @@
1
+ /**
2
+ * B1(鲁棒性批3 立案的设计件):/health 活体 store 探测的后台缓存环。
3
+ *
4
+ * 为什么是缓存环而不是逐请求探:/health 是 fleet center/k8s 的高频面(10-15s 一拍×N 消费方),
5
+ * 逐请求打 DB 会把健康检查本身变成负载源、且慢 store 直接拖垮 /health 的「必须回答此刻」承诺。
6
+ * 环产出的 state 由 /health 的 additive 键族消费(座注见 http/server.ts `storeLiveState`;
7
+ * 键形钉在 test/health-store-live.test.ts,本环判据钉在 test/store-live-probe.test.ts)。
8
+ *
9
+ * 判据:
10
+ * - 有界:单拍探针经 STORE_PROBE_TIMEOUT_MS race——挂死的 pool(半开连接)不得钉死环;
11
+ * - 翻转披露(§M):live→dead warn `store_probe_dead`、dead→live info `store_probe_recovered`,
12
+ * 只在翻转拍记(连续死不逐拍刷日志——down 的 DB 不该制造日志风暴);
13
+ * - 诚实缺席:首拍落地前 state() = undefined(/health 键缺席=「还没探过」,不冒充「活」)。
14
+ *
15
+ * 探针原语:调用方给 `probe`(main.ts 传 `() => backend.dbNowMs!()`——既有 S10 时钟探针的真 DB
16
+ * 往返,零新 SQL 面)。timer 全部 unref(环不得阻止进程退出)。
17
+ */
18
+ /** 单拍探针上界——挂死连接的判死时间;3s 对 15s 默认间隔留足余量(环内串行,无叠拍)。 */
19
+ export declare const STORE_PROBE_TIMEOUT_MS = 3000;
20
+ export interface StoreLiveState {
21
+ live: boolean;
22
+ /** 距上一次探针**落地**(成功或失败)的毫秒——消费方判「这份缓存多旧」。 */
23
+ ageMs: number;
24
+ error?: string;
25
+ }
26
+ export interface StoreLiveProbe {
27
+ /** /health 读座:undefined = 首拍未落地。 */
28
+ state: () => StoreLiveState | undefined;
29
+ stop: () => void;
30
+ }
31
+ export declare function createStoreLiveProbe(opts: {
32
+ probe: () => Promise<unknown>;
33
+ intervalMs: number;
34
+ timeoutMs?: number;
35
+ logger?: {
36
+ info?: (msg: string, meta?: Record<string, unknown>) => void;
37
+ warn?: (msg: string, meta?: Record<string, unknown>) => void;
38
+ };
39
+ }): StoreLiveProbe;
40
+ //# sourceMappingURL=store-live-probe.d.ts.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * B1(鲁棒性批3 立案的设计件):/health 活体 store 探测的后台缓存环。
3
+ *
4
+ * 为什么是缓存环而不是逐请求探:/health 是 fleet center/k8s 的高频面(10-15s 一拍×N 消费方),
5
+ * 逐请求打 DB 会把健康检查本身变成负载源、且慢 store 直接拖垮 /health 的「必须回答此刻」承诺。
6
+ * 环产出的 state 由 /health 的 additive 键族消费(座注见 http/server.ts `storeLiveState`;
7
+ * 键形钉在 test/health-store-live.test.ts,本环判据钉在 test/store-live-probe.test.ts)。
8
+ *
9
+ * 判据:
10
+ * - 有界:单拍探针经 STORE_PROBE_TIMEOUT_MS race——挂死的 pool(半开连接)不得钉死环;
11
+ * - 翻转披露(§M):live→dead warn `store_probe_dead`、dead→live info `store_probe_recovered`,
12
+ * 只在翻转拍记(连续死不逐拍刷日志——down 的 DB 不该制造日志风暴);
13
+ * - 诚实缺席:首拍落地前 state() = undefined(/health 键缺席=「还没探过」,不冒充「活」)。
14
+ *
15
+ * 探针原语:调用方给 `probe`(main.ts 传 `() => backend.dbNowMs!()`——既有 S10 时钟探针的真 DB
16
+ * 往返,零新 SQL 面)。timer 全部 unref(环不得阻止进程退出)。
17
+ */
18
+ /** 单拍探针上界——挂死连接的判死时间;3s 对 15s 默认间隔留足余量(环内串行,无叠拍)。 */
19
+ export const STORE_PROBE_TIMEOUT_MS = 3000;
20
+ export function createStoreLiveProbe(opts) {
21
+ const timeoutMs = opts.timeoutMs ?? STORE_PROBE_TIMEOUT_MS;
22
+ let last;
23
+ let stopped = false;
24
+ const probeOnce = async () => {
25
+ let timer;
26
+ let error;
27
+ try {
28
+ await Promise.race([
29
+ opts.probe(),
30
+ new Promise((_resolve, reject) => {
31
+ timer = setTimeout(() => reject(new Error(`store probe timed out after ${timeoutMs}ms`)), timeoutMs);
32
+ timer.unref?.();
33
+ }),
34
+ ]);
35
+ }
36
+ catch (e) {
37
+ error = e instanceof Error ? e.message : String(e);
38
+ }
39
+ finally {
40
+ if (timer !== undefined)
41
+ clearTimeout(timer);
42
+ }
43
+ if (stopped)
44
+ return; // stop() 与在飞拍竞速:停后不再改 state、不再记翻转
45
+ const live = error === undefined;
46
+ const prev = last;
47
+ last = { live, at: Date.now(), ...(error !== undefined ? { error } : {}) };
48
+ // 翻转披露:首拍即死也算翻转(prev undefined → dead);连续同态不刷。
49
+ if (!live && prev?.live !== false)
50
+ opts.logger?.warn?.("store_probe_dead", { error });
51
+ if (live && prev?.live === false)
52
+ opts.logger?.info?.("store_probe_recovered", { deadForMs: Date.now() - prev.at });
53
+ };
54
+ void probeOnce(); // 首拍立即(不等首个 interval——启动后 15s 盲窗没有必要)
55
+ const loop = setInterval(() => void probeOnce(), opts.intervalMs);
56
+ loop.unref?.();
57
+ return {
58
+ state: () => (last === undefined ? undefined : { live: last.live, ageMs: Date.now() - last.at, ...(last.error !== undefined ? { error: last.error } : {}) }),
59
+ stop: () => {
60
+ stopped = true;
61
+ clearInterval(loop);
62
+ },
63
+ };
64
+ }
65
+ //# sourceMappingURL=store-live-probe.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.13.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",