@sema-agent/server 7.93.0 → 7.93.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.
- package/USAGE.md +23 -1
- package/dist/config-catalog.js +1 -0
- package/dist/config-types.d.ts +7 -1
- package/dist/config.js +2 -1
- package/dist/plugins/store-backend.js +3 -2
- package/dist/plugins/store-lock-wait.d.ts +23 -0
- package/dist/plugins/store-lock-wait.js +39 -0
- package/dist/run-local.js +3 -2
- package/package.json +2 -2
package/USAGE.md
CHANGED
|
@@ -376,10 +376,32 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
|
|
|
376
376
|
三动作:`store.dir_in_use`=另一个活进程持有该目录 → 停掉它(或换数据根/迁 SQL 后端);
|
|
377
377
|
`store.dir_claiming`=另一进程正在接管崩溃者留下的锁 → **瞬态,稍候重试**(接管方要么完成要么
|
|
378
378
|
自己也被收割);`store.lock_unreadable`=锁文件坏形(无 owner 可解析/嵌套接管残留)→ 人工处置,
|
|
379
|
-
message 会点名要删的文件,**不要盲删数据根下其它东西**。server
|
|
379
|
+
message 会点名要删的文件,**不要盲删数据根下其它东西**。server 刻意不把这族错**吞掉**(fail-fast:
|
|
380
380
|
两进程共用 file 数据目录是禁止形,静默容忍比拒启更坏)。
|
|
381
|
+
⚠️ 自 7.93.1 起 `store.dir_in_use` **这一码**多一道有界等待(见下面 `STORE_LOCK_WAIT_MS` 那条):
|
|
382
|
+
拒启形与文案一格不变,只是拒之前会先在窗内反复重取;另两码(`dir_claiming` / `lock_unreadable`)
|
|
383
|
+
与非锁类失败**一秒都不等**,原样抛。
|
|
381
384
|
`DB_BACKEND=memory` 并非全无盘:workflow 相关账本仍挂数据根下——多个 memory 形引擎**不要共用**
|
|
382
385
|
数据根/HOME(1.309 起并发 boot 不再拒启,但账本内容级共享仍不受支持)。
|
|
386
|
+
- **`STORE_LOCK_WAIT_MS`(缺省 `60000`,单位 ms)—— 撞上活持锁人时等一会儿再拒**:
|
|
387
|
+
`DB_BACKEND=local` 的数据根锁被**还活着**的持有者占着时(最常见的成因:壳自己 spawn 引擎,用户关掉壳
|
|
388
|
+
又立刻重开,旧引擎正在 `DRAIN_GRACE_MS` 的优雅排空 + 停机收尾里,锁要到收尾链尾部的 `backend.close()`
|
|
389
|
+
才归还),新实例在这个窗内**反复重取**数据根:拿到就照常起动(启动日志一行
|
|
390
|
+
`store_dir_lock_wait{ownerPid,dir,waitMs}`,**只一行**),到窗仍拿不到才拒启 —— 拒句是原来那一句,
|
|
391
|
+
额外写明 `waited Nms`(运维据此区分「当场拒」与「等满了才拒」)。等待发生在 `listen` **之前**,语义是
|
|
392
|
+
「启动慢了 N 秒」而不是「一边服务一边等」。
|
|
393
|
+
🔴 **方向是多等,不是放行**:窗只决定「拒之前等多久」,任何情形下都不会让第二个写者进同一个数据根。
|
|
394
|
+
代价 = 两个真独立部署误指同一目录时,那句「被 pid N 占着」要等满窗才出现(编排面的 liveness 探针
|
|
395
|
+
initialDelay 请按这个窗放宽)。要立刻响 ⇒ `STORE_LOCK_WAIT_MS=0`(逐字回到 7.93.0 的当场拒)。
|
|
396
|
+
上界不设(愿意等多久是部署自己的事);负值夹到 0 并在启动日志点名。只有 file 后端读它。
|
|
397
|
+
🔴 **两条取数据根的路是同一条腿、同一只旋钮**:HTTP 服务的 local 臂与 `run-local`(一次性 CLI)共用
|
|
398
|
+
同一只等待腿 —— **不为前台形留第二套语义**。前台形要「立刻告诉我谁占着」就设 `STORE_LOCK_WAIT_MS=0`,
|
|
399
|
+
那是文档化的通道,而不是两条路各有一套默认。
|
|
400
|
+
⚠️ **裸 boot(没有显式 `DB_BACKEND`)那一形的时序也跟着变**:它的锁冲突走的不是拒启而是既有的
|
|
401
|
+
「退回内存并 warn」降级臂(`store_db_unreachable_fallback_memory`),所以第二只实例此前**当场**降级、
|
|
402
|
+
现在要**等满窗**才降级 —— 方向仍是好的(窗内拿到就用上真数据根,而不是静默丢掉持久面),但它是一次
|
|
403
|
+
真实的启动时长变化。显式 `DB_BACKEND=local` 不走降级臂(拒启),`0` 关掉两形的等待。
|
|
404
|
+
三问成文见 `docs/DEPLOY-PREREQS.md`。
|
|
383
405
|
- **`RUN_STORE_STRICT_HYDRATE=on|off`(缺省 `off`)—— file 后端 run 账本的 fail-closed 启动门**:
|
|
384
406
|
`DB_BACKEND=local` 的 run 账本在 boot 时把 `<数据根>/runs/` 列一遍重建内存索引。那次 `readdir` 失败
|
|
385
407
|
(权限、挂载丢失、路径被换走)时**缺省行为是照常起动**,只留一行 `file_run_store_hydrate_unreadable`
|
package/dist/config-catalog.js
CHANGED
|
@@ -423,6 +423,7 @@ export const CONFIG_CATALOG = [
|
|
|
423
423
|
r("SSH_PORT", "orchestration", "number", "ssh 端口", { derivedDefaultNote: "缺席 ⇒ 22" }),
|
|
424
424
|
r("SSH_PRIVATE_KEY", "orchestration", "secret", "ssh 私钥(adapter 持有,绝不进 workspace)", { danger: SEC }),
|
|
425
425
|
r("SSH_USER", "orchestration", "string", "ssh 用户(必需件)"),
|
|
426
|
+
r("STORE_LOCK_WAIT_MS", "store", "number", "local 数据根 LOCK 撞上活持锁人时的有界等待窗(ms;0=当场拒;到窗仍拿不到 ⇒ 原码拒启,永不放行第二个写者)", { staticDefault: "60000", danger: DATA, configKey: "storeLockWaitMs" }),
|
|
426
427
|
r("STORE_PROBE_INTERVAL_MS", "store", "number", "/health 活体 store 后台探针间隔(ms;0=显式关;仅 SQL 后端挂环)", { staticDefault: "15000", configKey: "storeProbeIntervalMs" }),
|
|
427
428
|
r("STREAM_APPROVAL_ADHOC_GRACE_MS", "approval", "number", "流内审批 adhoc 腿窗后宽限(ms;须 window+grace < orphanTtl,破坏形拒启)", { staticDefault: "60000" }),
|
|
428
429
|
r("STREAM_APPROVAL_ADMIT_MAX_PER_OWNER", "approval", "number", "流内审批 per-owner 未决帽", { staticDefault: "256" }),
|
package/dist/config-types.d.ts
CHANGED
|
@@ -502,6 +502,12 @@ export interface ServiceConfigFlat {
|
|
|
502
502
|
/** B1 /health 活体 store 探测:后台缓存探针间隔 ms(STORE_PROBE_INTERVAL_MS;0=显式关,负/非数 boot
|
|
503
503
|
* 响亮拒)。只对 SQL 后端接线;探针原语=backend.dbNowMs()(S10 时钟探针的真 DB 往返,零新 SQL 面)。 */
|
|
504
504
|
storeProbeIntervalMs: number;
|
|
505
|
+
/** S-553 —— `DB_BACKEND=local` 数据根 `LOCK` 撞上**活**持锁人时的**有界等待窗** ms
|
|
506
|
+
* (`STORE_LOCK_WAIT_MS`;缺省 60_000,地板 0 = 7.93.0 的当场拒,**不设上界**)。
|
|
507
|
+
* 病 = 壳自 spawn 引擎的单机形:旧引擎排空期间一直抱着锁,新引擎当场 fatal ⇒ 用户「刚关掉就再也起不来」。
|
|
508
|
+
* 失败方向一格不动:到窗仍拿不到 ⇒ 原码 fatal(文案多一句等了多久),**永不**放行第二个写者。
|
|
509
|
+
* 只有 file 后端读它;判据与偏离说明见 `plugins/store-lock-wait.ts` 顶注。 */
|
|
510
|
+
storeLockWaitMs: number;
|
|
505
511
|
/** Long-term memory ([ref] S1, clay 2026-07-08): the injection-first file-based memory ENGINE
|
|
506
512
|
* (core `RunnerDeps.memoryBackend` + `MemoryEngine`) is the ONLY memory plane — the legacy MemoryStore
|
|
507
513
|
* path (MEMORY_BACKEND/EMBEDDING_* SQL/file note stores) was dropped without migration. Default ON;
|
|
@@ -1858,7 +1864,7 @@ export interface ServiceConfigFlat {
|
|
|
1858
1864
|
configLocalDir?: string;
|
|
1859
1865
|
}
|
|
1860
1866
|
/** 组:store(持久化)—— DB 引擎三态、session 后端、SQL coords、快照 BLOB / SendUserFile 对象存储。 */
|
|
1861
|
-
export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "sessionCacheTtlSec" | "storeProbeIntervalMs" | "dbBackend" | "dbBackendExplicit" | "localDataRoot" | "runStoreStrictHydrate" | "tidb" | "pg" | "dbQueryTimeoutMs" | "snapshotBlobStore" | "snapshotBlobSqlMaxBytes" | "snapshotBlobAllowSql" | "fileHistory" | "sendUserFile">;
|
|
1867
|
+
export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "sessionCacheTtlSec" | "storeProbeIntervalMs" | "storeLockWaitMs" | "dbBackend" | "dbBackendExplicit" | "localDataRoot" | "runStoreStrictHydrate" | "tidb" | "pg" | "dbQueryTimeoutMs" | "snapshotBlobStore" | "snapshotBlobSqlMaxBytes" | "snapshotBlobAllowSql" | "fileHistory" | "sendUserFile">;
|
|
1862
1868
|
/** 组:modelPlane(模型面)—— 网关坐标、Anthropic 路线、韧性旋钮、主/廉价 model entry、role 表、降级梯。 */
|
|
1863
1869
|
export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "envModel" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "atModelAllowlist" | "cascadeLadder" | "degrade">;
|
|
1864
1870
|
/** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
|
package/dist/config.js
CHANGED
|
@@ -725,6 +725,7 @@ function parseStoreDomain(ctx) {
|
|
|
725
725
|
sessionBackend,
|
|
726
726
|
sessionCacheTtlSec: numEnv("SESSION_CACHE_TTL_SEC", "300"),
|
|
727
727
|
storeProbeIntervalMs: numEnvBounded("STORE_PROBE_INTERVAL_MS", "15000", 0, 3_600_000),
|
|
728
|
+
storeLockWaitMs: clampEnvWithWarn("STORE_LOCK_WAIT_MS", "60000", 0),
|
|
728
729
|
dbBackend,
|
|
729
730
|
dbBackendExplicit: dbBackendSet,
|
|
730
731
|
localDataRoot,
|
|
@@ -2030,7 +2031,7 @@ function parseA2aServeSkills(raw) {
|
|
|
2030
2031
|
});
|
|
2031
2032
|
}
|
|
2032
2033
|
const STORE_GROUP_KEYS = [
|
|
2033
|
-
"sessionBackend", "sessionCacheTtlSec", "storeProbeIntervalMs", "dbBackend", "dbBackendExplicit", "localDataRoot",
|
|
2034
|
+
"sessionBackend", "sessionCacheTtlSec", "storeProbeIntervalMs", "storeLockWaitMs", "dbBackend", "dbBackendExplicit", "localDataRoot",
|
|
2034
2035
|
"runStoreStrictHydrate",
|
|
2035
2036
|
"tidb", "pg", "dbQueryTimeoutMs", "snapshotBlobStore", "snapshotBlobSqlMaxBytes", "snapshotBlobAllowSql", "fileHistory", "sendUserFile",
|
|
2036
2037
|
];
|
|
@@ -8,6 +8,7 @@ import { FileOutcomeSink } from "./file-outcome-sink.js";
|
|
|
8
8
|
import { TiDBWorkflowRunStore, TiDBWorkflowCompletionInbox, TiDBWorkflowNotifyJournalStore, TiDBWorkflowAgentSessionIndex, PgWorkflowRunStore, PgWorkflowCompletionInbox, PgWorkflowNotifyJournalStore, PgWorkflowAgentSessionIndex } from "./workflow-run-store-sql.js";
|
|
9
9
|
import { FileRunStore } from "./file-run-store.js";
|
|
10
10
|
import { StoreBootRefusalError } from "./store-contracts.js";
|
|
11
|
+
import { acquireLocalBackendWithWait } from "./store-lock-wait.js";
|
|
11
12
|
import { NO_SQL_MIGRATIONS, runSqlOneTimeMigrations } from "./one-time-migrations.js";
|
|
12
13
|
import { LocalSessionStore } from "./local-session-store.js";
|
|
13
14
|
import { LocalCheckpointStore } from "./local-checkpoint-store.js";
|
|
@@ -215,7 +216,7 @@ class LocalBackend {
|
|
|
215
216
|
this.onWarn = onWarn;
|
|
216
217
|
this.onError = onError;
|
|
217
218
|
try {
|
|
218
|
-
this.fileBackend = new FileStorageBackend({
|
|
219
|
+
this.fileBackend = acquireLocalBackendWithWait(() => new FileStorageBackend({
|
|
219
220
|
root,
|
|
220
221
|
...(config.fileHistory !== undefined ? { fileHistory: config.fileHistory } : {}),
|
|
221
222
|
onCorruptRead: (info) => this.onWarn?.("file_store_corrupt_read", {
|
|
@@ -228,7 +229,7 @@ class LocalBackend {
|
|
|
228
229
|
"layer (deployment policy / hooks / shell gate still applied); session repo = a listing silently omitted " +
|
|
229
230
|
"rows; file-history = a rewind boundary or scope read as missing. Inspect the named path before deciding.",
|
|
230
231
|
}),
|
|
231
|
-
});
|
|
232
|
+
}), config.storeLockWaitMs, (fields) => this.onWarn?.("store_dir_lock_wait", { ...fields }));
|
|
232
233
|
}
|
|
233
234
|
catch (e) {
|
|
234
235
|
if (e instanceof AdoptionError)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** 轮询节拍。250ms:比人感知门槛粗、比最短真等待窗(壳重启窗 1-3s)细一个数量级。 */
|
|
2
|
+
export declare const STORE_LOCK_POLL_MS = 250;
|
|
3
|
+
/** 进入等待腿时的**一次性**披露(不刷屏:结构上只在第一次撞锁时调一次,不是靠一个 flag 挡)。 */
|
|
4
|
+
export type StoreLockWaitSink = (fields: {
|
|
5
|
+
readonly ownerPid: number;
|
|
6
|
+
readonly dir: string;
|
|
7
|
+
readonly waitMs: number;
|
|
8
|
+
}) => void;
|
|
9
|
+
/** 时钟两面。`sleep` 是**同步**的(理由见顶注);两面都是形参,判据不靠真时间。 */
|
|
10
|
+
export interface StoreLockWaitClock {
|
|
11
|
+
now(): number;
|
|
12
|
+
sleep(ms: number): void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 取数据根(= 跑一次 `construct()`);撞上活持锁人且窗为正 ⇒ 在窗内反复重取,到窗仍取不到 ⇒ 原码 fatal。
|
|
16
|
+
*
|
|
17
|
+
* @param construct 取数据根的那一次构造(本仓唯一调用点:`store-backend.ts` 的 local 臂,它在那里构造 `FileStorageBackend`)。
|
|
18
|
+
* **必须**可重入:core 先取锁后建店,取不到时零副作用(顶注已亲读核实)。
|
|
19
|
+
* @param waitMs `STORE_LOCK_WAIT_MS`。`> 0` 才等;`0` / 非数 ⇒ 7.93.0 的当场拒逐字同形。
|
|
20
|
+
* @param onWait 进入等待腿的一次性披露口。
|
|
21
|
+
*/
|
|
22
|
+
export declare function acquireLocalBackendWithWait<T>(construct: () => T, waitMs: number, onWait: StoreLockWaitSink, clock?: StoreLockWaitClock): T;
|
|
23
|
+
//# sourceMappingURL=store-lock-wait.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { FileStoreLockError } from "@sema-agent/core";
|
|
3
|
+
export const STORE_LOCK_POLL_MS = 250;
|
|
4
|
+
const REAL_CLOCK = {
|
|
5
|
+
now: () => performance.now(),
|
|
6
|
+
sleep: (ms) => void Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms),
|
|
7
|
+
};
|
|
8
|
+
function liveOwnerPidOf(err) {
|
|
9
|
+
if (!(err instanceof FileStoreLockError))
|
|
10
|
+
return undefined;
|
|
11
|
+
if (err.code !== "store.dir_in_use")
|
|
12
|
+
return undefined;
|
|
13
|
+
return err.ownerPid;
|
|
14
|
+
}
|
|
15
|
+
export function acquireLocalBackendWithWait(construct, waitMs, onWait, clock = REAL_CLOCK) {
|
|
16
|
+
let startedAt;
|
|
17
|
+
for (;;) {
|
|
18
|
+
try {
|
|
19
|
+
return construct();
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
const ownerPid = liveOwnerPidOf(err);
|
|
23
|
+
if (ownerPid === undefined || !(waitMs > 0))
|
|
24
|
+
throw err;
|
|
25
|
+
const lockErr = err;
|
|
26
|
+
if (startedAt === undefined) {
|
|
27
|
+
startedAt = clock.now();
|
|
28
|
+
onWait({ ownerPid, dir: dirname(lockErr.lockPath), waitMs });
|
|
29
|
+
}
|
|
30
|
+
const waitedMs = clock.now() - startedAt;
|
|
31
|
+
if (waitedMs >= waitMs) {
|
|
32
|
+
throw new FileStoreLockError(lockErr.code, `${lockErr.message} — waited ${Math.round(waitedMs)}ms for it to release the directory ` +
|
|
33
|
+
`(STORE_LOCK_WAIT_MS=${waitMs}); it is still holding it`, lockErr.lockPath, lockErr.ownerPid);
|
|
34
|
+
}
|
|
35
|
+
clock.sleep(Math.min(STORE_LOCK_POLL_MS, waitMs - waitedMs));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=store-lock-wait.js.map
|
package/dist/run-local.js
CHANGED
|
@@ -17,6 +17,7 @@ import { taskWallClockSec } from "./task-workflow.js";
|
|
|
17
17
|
import { applyCatalogToSource } from "./capabilities/center-prompts.js";
|
|
18
18
|
import { validatePromptsDomain, CORE_ENGINE_VERSION } from "./prompts-domain-validate.js";
|
|
19
19
|
import { buildUnpricedCeilingNotice, loadConfig, logConfigDiagnostics, parseNumOrFailNonNegative } from "./config.js";
|
|
20
|
+
import { acquireLocalBackendWithWait } from "./plugins/store-lock-wait.js";
|
|
20
21
|
import { createConfigProvider } from "./config-provider.js";
|
|
21
22
|
import { applyEffective, applyCenterReadFace, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./config-center/facade.js";
|
|
22
23
|
import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
|
|
@@ -253,7 +254,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
253
254
|
const pricing = buildPricing(config.models);
|
|
254
255
|
let fileBackend;
|
|
255
256
|
try {
|
|
256
|
-
fileBackend = new FileStorageBackend({
|
|
257
|
+
fileBackend = acquireLocalBackendWithWait(() => new FileStorageBackend({
|
|
257
258
|
root,
|
|
258
259
|
...(config.fileHistory !== undefined ? { fileHistory: config.fileHistory } : {}),
|
|
259
260
|
onCorruptRead: (info) => logger.warn("file_store_corrupt_read", {
|
|
@@ -266,7 +267,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
266
267
|
"layer (deployment policy / hooks / shell gate still applied); session repo = a listing silently omitted " +
|
|
267
268
|
"rows; file-snapshot = a snapshot or scope read as missing. Inspect the named path before deciding.",
|
|
268
269
|
}),
|
|
269
|
-
});
|
|
270
|
+
}), config.storeLockWaitMs, (fields) => logger.warn("store_dir_lock_wait", { ...fields }));
|
|
270
271
|
}
|
|
271
272
|
catch (e) {
|
|
272
273
|
printErr(`cannot open local data dir ${root}: ${e instanceof Error ? e.message : String(e)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.93.
|
|
3
|
+
"version": "7.93.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",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"sharp": "^0.35.3"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@sema-agent/sdk": "11.2.
|
|
77
|
+
"@sema-agent/sdk": "11.2.1",
|
|
78
78
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
79
79
|
"@types/node": "22.10.2",
|
|
80
80
|
"@types/pg": "^8.20.0",
|