@sema-agent/server 5.13.0 → 5.14.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 里」来做,不要按「只在内存」。
@@ -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 = [
@@ -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
  }
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";
@@ -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 = {
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "5.13.0",
3
+ "version": "5.14.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",