@sema-agent/server 7.38.0-rc.1 → 7.38.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 +1 -1
- package/dist/boot/config-center.js +29 -9
- package/dist/config-center/apply-effective.d.ts +3 -0
- package/dist/config-center/apply-effective.js +12 -5
- package/dist/config-center/apply-ledger.d.ts +25 -10
- package/dist/config-center/apply-ledger.js +43 -22
- package/dist/http/routes/runs.js +10 -6
- package/dist/http/server.js +7 -4
- package/package.json +2 -2
package/USAGE.md
CHANGED
|
@@ -681,7 +681,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
|
|
|
681
681
|
| `GET /v1/capabilities/scenarios/<name>` | 单个场景的只读详情:工具面、提示词概览、**本部署现在跑不跑得动**(见下) |
|
|
682
682
|
| `POST /v1/admin/drain` `{"reason":"…"}` | **停机因由显式声明**(7.35.0+;operator-only —— principal 须在 `OPERATOR_PRINCIPALS` 内,空名单 = 谁都不是 operator ⇒ 恒 403)。编排壳在发 SIGTERM **之前**调它;server 记进程内 `drainState.reason`,draining 期由 `/health` 的 `drainReason` 与 503 `draining` 体的 `reason` 透出(未声明 = 两面键缺席)。声明是**覆盖式**、无 TTL、随进程重启清零 ⇒ 每次停机流程内都要重新声明。`reason` 非空、trim 后 ≤256 字符,坏值 400。🔴 **写口是特权的,读口不是**:`/health` 免鉴权(见下一行)且缺省全网卡监听 —— 任何能连到这个端口的人都读得到你写的那句话。**别在 reason 里写工单号、内部主机名或任何内部标识**。 |
|
|
683
683
|
| `POST /v1/admin/config/refresh` | **配置手动刷新**(#322 批1;operator-only,与上一行同门:空 `OPERATOR_PRINCIPALS` ⇒ 恒 403)。改完中心配置 / `config.d` 之后不想等 60s 轮询就调它:触发**一次既有**的 refresh 拍(在飞则汇入那一拍,绝不并发双拍),返回时该拍已落地。200 体 `{triggered:true,targetVersion,appliedVersion}` —— 两个世代号与 `/health` 的 `configTargetVersion`/`configAppliedVersion` 同源。本部署没有配置管道(纯 env worker:既没配 `CONFIG_CENTER`、也不是 `CONFIG_PROVIDER=local`)⇒ 409 `config.refresh_unavailable` 指路,不会回一个「触发了但什么都没发生」的 200。 |
|
|
684
|
-
| `GET /health` | 健康(无需鉴权)。7.37.0+
|
|
684
|
+
| `GET /health` | 健康(无需鉴权)。7.37.0+ 配置管道在场时带世代账键:`configTargetVersion`(本副本**最后见到**的配置版本,身份标)、`configAppliedVersion`(最后一次真落地的版本)、`configTargetOrdinal`/`configAppliedOrdinal`(7.38.0-rc.2 起,**收货序数**——`CONFIG_PROVIDER=local` 下 version 是内容哈希不承诺序,「改了没生效」看序数对不对齐)、`configApplyStaleMs`(target≠applied **持续**时长;收敛时键缺席)—— 编排器据此摘掉「配置持续落后」的副本 |
|
|
685
685
|
| `GET /metrics` | Prometheus 指标(有 token 时需带) |
|
|
686
686
|
|
|
687
687
|
> **场景可用性(7.7.0 起)**:场景详情里 `enabled` 与 `available` 是**两件事**。`enabled` = 这条场景
|
|
@@ -72,6 +72,19 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
72
72
|
// #322 世代账(§2):主键=上游 version(不造本地 epoch)。boot/refresh/迟到 boot 三条腿共用这一本;
|
|
73
73
|
// 候选被整拒的拍只推进 target ⇒ /health 的 stale 面从结构上可见。
|
|
74
74
|
const applyLedger = createConfigApplyLedger();
|
|
75
|
+
// #325(test [4809],[4810] 阻断裁定):**收货序数**——CAS 与世代账的序轴。source version 在
|
|
76
|
+
// local 腿是内容哈希(registry-core file-store getVersion(),无序承诺),不能当序数比;这里按
|
|
77
|
+
// 收货顺序铸进程内单调 ordinal:version 变即 ++,等版本重放(etag 未推进的下一拍)沿用同
|
|
78
|
+
// ordinal=幂等语义保留。version 仍是跨副本对账的身份标(账里两者双报)。
|
|
79
|
+
let generationOrdinal = 0;
|
|
80
|
+
let lastGenerationVersion;
|
|
81
|
+
const generationOf = (version) => {
|
|
82
|
+
if (version !== lastGenerationVersion) {
|
|
83
|
+
lastGenerationVersion = version;
|
|
84
|
+
generationOrdinal += 1;
|
|
85
|
+
}
|
|
86
|
+
return generationOrdinal;
|
|
87
|
+
};
|
|
75
88
|
// #322 追加件:手动触发口的实现指针 —— `startRefreshLoop` 起环时装上(cadence 与手动口共用的那只
|
|
76
89
|
// join 形拍)。未起环(纯 env 部署 / 还没到装配那一步)⇒ 恒 undefined ⇒ 端点诚实 4xx。
|
|
77
90
|
let manualRefresh;
|
|
@@ -376,9 +389,10 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
376
389
|
return { effective: read.lkg.effective, ...(read.lkg.etag !== undefined ? { etag: read.lkg.etag } : {}) };
|
|
377
390
|
};
|
|
378
391
|
const bootApplyLkgInline = async (lkgR) => {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
392
|
+
const lkgGen = generationOf(lkgR.effective.version);
|
|
393
|
+
applyLedger.seenTarget(lkgGen, lkgR.effective.version);
|
|
394
|
+
applyEffective(config, lkgR.effective, logger, { sealedKeys, generation: lkgGen, report: (rep) => applyLedger.recordApply(rep) });
|
|
395
|
+
applyLedger.applied(lkgGen, lkgR.effective.version);
|
|
382
396
|
await adoptCenterPrompts(lkgR.effective, "boot-lkg");
|
|
383
397
|
markRosterLanded(lkgR.effective);
|
|
384
398
|
effective = lkgR.effective;
|
|
@@ -442,16 +456,18 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
442
456
|
ccEtag = r.etag;
|
|
443
457
|
}
|
|
444
458
|
else {
|
|
445
|
-
|
|
459
|
+
const bootGen = generationOf(r.effective.version);
|
|
460
|
+
applyLedger.seenTarget(bootGen, r.effective.version);
|
|
446
461
|
let bootReport;
|
|
447
462
|
applyEffective(config, r.effective, logger, {
|
|
448
463
|
sealedKeys,
|
|
464
|
+
generation: bootGen,
|
|
449
465
|
report: (rep) => {
|
|
450
466
|
bootReport = rep;
|
|
451
467
|
applyLedger.recordApply(rep);
|
|
452
468
|
},
|
|
453
469
|
}); // sealedKeys: unseal sealedApiKey → in-memory key pipeline
|
|
454
|
-
applyLedger.applied(r.effective.version);
|
|
470
|
+
applyLedger.applied(bootGen, r.effective.version);
|
|
455
471
|
// codex R2-F1(boot 半场):某组被拒 ⇒ 这棵树**不干净** —— 与 domainErrors/坏 prompts 同处置
|
|
456
472
|
// (etag 不进、不进 LKG 候选),否则一次带坏 limits 的 boot 会把它写成盘上的 LKG。
|
|
457
473
|
if (bootReport?.fullyApplied === false)
|
|
@@ -796,7 +812,8 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
796
812
|
if (r) {
|
|
797
813
|
// #322 世代账:**拉到即算 target**(候选是否被采纳是下面的事)。整拒的拍只推进 target ⇒
|
|
798
814
|
// target≠applied 的持续时长在 /health 上可见 —— 「持续卡在旧代」的副本从结构上可被摘除。
|
|
799
|
-
|
|
815
|
+
const tickGen = generationOf(r.effective.version); // #325:收货序数(本拍全链同一个)
|
|
816
|
+
applyLedger.seenTarget(tickGen, r.effective.version);
|
|
800
817
|
// §9.5-5 candidate gate: ANY invalid domain rejects the WHOLE candidate — the live config
|
|
801
818
|
// keeps serving the LKG (`latestEffective`) instead of that domain falling to schema-default/
|
|
802
819
|
// empty. ccEtag is deliberately NOT advanced: the bad tree stays visible (re-judged each poll),
|
|
@@ -864,6 +881,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
864
881
|
const committed = applyEffective(config, r.effective, logger, {
|
|
865
882
|
teamsOnly: true,
|
|
866
883
|
sealedKeys,
|
|
884
|
+
generation: tickGen,
|
|
867
885
|
report: (rep) => {
|
|
868
886
|
applyReport = rep;
|
|
869
887
|
applyLedger.recordApply(rep);
|
|
@@ -885,7 +903,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
885
903
|
mutateInPlace(pricing, buildPricing(config.models)); // hot: cost/model changes; Runner reads this.deps.pricing live
|
|
886
904
|
keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys); // hot: per-model key add/remove/change (env-ref + sealed)
|
|
887
905
|
limitSync?.sync(); // #322 批1:限额座与 config 成对换代(同一条「派生件紧贴 commit」纪律)
|
|
888
|
-
applyLedger.applied(r.effective.version);
|
|
906
|
+
applyLedger.applied(tickGen, r.effective.version);
|
|
889
907
|
if (planeDeferred) {
|
|
890
908
|
logger.warn("models_tiers_plane_deferred", { version: r.effective.version, note: "tier-frozen Runner: the changed model plane (models/roles/tiers/default) is NOT hot-applied — admission stays on the Runner's generation; restart applies the new plane (models-tiers restart signal rides /health)" });
|
|
891
909
|
}
|
|
@@ -1082,7 +1100,8 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
1082
1100
|
}
|
|
1083
1101
|
};
|
|
1084
1102
|
const deferredBootApplyInner = async (r) => {
|
|
1085
|
-
|
|
1103
|
+
const lateGen = generationOf(r.effective.version); // #325:收货序数(迟到 boot 拍全链同一个)
|
|
1104
|
+
applyLedger.seenTarget(lateGen, r.effective.version); // #322:迟到 boot 也是「见过这一代」
|
|
1086
1105
|
const promptsRaw = r.effective.prompts;
|
|
1087
1106
|
const promptsGate = promptsRaw !== undefined ? validatePromptsDomain(promptsRaw) : { ok: true };
|
|
1088
1107
|
if ((r.domainErrors ?? []).length > 0 || !promptsGate.ok) {
|
|
@@ -1111,6 +1130,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
1111
1130
|
const committedLate = applyEffective(config, r.effective, logger, {
|
|
1112
1131
|
teamsOnly: true,
|
|
1113
1132
|
sealedKeys,
|
|
1133
|
+
generation: lateGen,
|
|
1114
1134
|
report: (rep) => {
|
|
1115
1135
|
applyReportLate = rep;
|
|
1116
1136
|
applyLedger.recordApply(rep);
|
|
@@ -1125,7 +1145,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
1125
1145
|
mutateInPlace(pricing, buildPricing(config.models));
|
|
1126
1146
|
keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
|
|
1127
1147
|
limitSync?.sync(); // #322 批1(late-boot 双胞胎:只修一条腿 = 另一条继续漏)
|
|
1128
|
-
applyLedger.applied(r.effective.version);
|
|
1148
|
+
applyLedger.applied(lateGen, r.effective.version);
|
|
1129
1149
|
if (planeDeferredLate)
|
|
1130
1150
|
logger.warn("models_tiers_plane_deferred", { version: r.effective.version, note: "tier-frozen Runner (env tiers): the late-boot center model plane is NOT hot-applied — restart applies it" });
|
|
1131
1151
|
else {
|
|
@@ -30,6 +30,8 @@ export declare const UNPARSABLE_AT_MODEL_ALLOWLIST_ENTRY = "\0<unparsable-at-mod
|
|
|
30
30
|
/** #322:一次 apply 的**世代账载荷**(post-commit 发,[2283]② —— commit 段仍是零回调零发射)。 */
|
|
31
31
|
export interface ApplyReport {
|
|
32
32
|
readonly version: number;
|
|
33
|
+
/** #325:本候选的收货序数(= opts.generation 原样带回;世代账按它记拒收,身份标仍是 version)。 */
|
|
34
|
+
readonly ordinal?: number;
|
|
33
35
|
readonly limits: {
|
|
34
36
|
/** 本世代真正换了值的键。 */
|
|
35
37
|
readonly applied: readonly string[];
|
|
@@ -53,6 +55,7 @@ export declare function applyEffective(config: ServiceConfig, eff: EffectiveConf
|
|
|
53
55
|
teamsOnly?: boolean;
|
|
54
56
|
sealedKeys?: SealedKeyOpener;
|
|
55
57
|
deferModelPlane?: boolean;
|
|
58
|
+
generation?: number;
|
|
56
59
|
report?: (r: ApplyReport) => void;
|
|
57
60
|
swapPlane?: (plane: {
|
|
58
61
|
models: Record<string, Model>;
|
|
@@ -158,12 +158,17 @@ function stageAtModelAllowlist(raw, models, logger) {
|
|
|
158
158
|
}
|
|
159
159
|
export function applyEffective(config, eff, logger, opts = {}) {
|
|
160
160
|
const staged = stageEffective(config, eff, logger, opts);
|
|
161
|
+
// #325(test [4809]):CAS 的序轴=**收货序数**(opts.generation,config-center 收货处铸的进程内
|
|
162
|
+
// 单调 ordinal),不再是 source version——local 腿的 version 是内容哈希(registry-core file-store
|
|
163
|
+
// getVersion(),上游只承诺「同 files⇒同 number」,无序承诺),按它比序会把哈希更小的合法新代
|
|
164
|
+
// 永久静默拒掉。generation 缺席(单元测试/一次性 lane)退回 version:既有单调 version 语义不变。
|
|
165
|
+
const gen = opts.generation ?? staged.version;
|
|
161
166
|
const live = appliedGeneration.get(config);
|
|
162
|
-
if (staged.version > 0 && live !== undefined &&
|
|
167
|
+
if (staged.version > 0 && live !== undefined && gen < live) {
|
|
163
168
|
// [2283]③:两次拉取的 staging 并发/乱序完成时,慢的旧世代不得整体覆盖快的新世代——每次都是
|
|
164
169
|
// 「整世代」,但方向反了。拒绝必须留痕(C6);caller 的 etag 纪律本就只在 apply 成功后推进,
|
|
165
|
-
// 下一拍拉到的自然是更新的世代。等版本重放(etag
|
|
166
|
-
logger?.warn("sema_registry_stale_generation_refused", { staged: staged.version, live, note: "an older staged generation must not overwrite a newer committed one — refused whole; next poll replays" });
|
|
170
|
+
// 下一拍拉到的自然是更新的世代。等版本重放(etag 未推进的幂等重放,同 ordinal)照常放行。
|
|
171
|
+
logger?.warn("sema_registry_stale_generation_refused", { staged: staged.version, stagedOrdinal: gen, live, note: "an older staged generation must not overwrite a newer committed one — refused whole; next poll replays" });
|
|
167
172
|
return false;
|
|
168
173
|
}
|
|
169
174
|
// core 5.49 `Runner.swapModels` 换装腿([4775]①,defer 臂退役):caller 传入 swapPlane 时,staged
|
|
@@ -185,10 +190,13 @@ export function applyEffective(config, eff, logger, opts = {}) {
|
|
|
185
190
|
}
|
|
186
191
|
}
|
|
187
192
|
commitStaged(config, staged);
|
|
193
|
+
if (staged.version > 0)
|
|
194
|
+
appliedGeneration.set(config, gen); // #325:CAS 的 live 端=收货序数
|
|
188
195
|
// ── post-commit notifications(全部世代描述性通知在最后一笔赋值之后,[2283]②)──
|
|
189
196
|
// #322 世代账:apply 报告也是「世代描述性通知」⇒ 同样在 commit 之后发(commit 段零回调纪律不破)。
|
|
190
197
|
opts.report?.({
|
|
191
198
|
version: staged.version,
|
|
199
|
+
...(opts.generation !== undefined ? { ordinal: opts.generation } : {}),
|
|
192
200
|
limits: {
|
|
193
201
|
applied: staged.limits.changed,
|
|
194
202
|
...(staged.limits.deferred.length > 0 ? { deferred: staged.limits.deferred } : {}),
|
|
@@ -256,8 +264,7 @@ function commitStaged(config, staged) {
|
|
|
256
264
|
registerCollabWorkflows(tp.workflows);
|
|
257
265
|
}
|
|
258
266
|
mutateInPlace(config.projects, staged.projects); // IN PLACE: keep the ref per-request consumers captured at boot
|
|
259
|
-
|
|
260
|
-
appliedGeneration.set(config, staged.version);
|
|
267
|
+
// 世代登记在 applyEffective 内做(#325:登记的是收货序数 gen,不是 source version)。
|
|
261
268
|
}
|
|
262
269
|
/** STAGE 段:一切计算/校验/解封/投影(可抛;抛=候选整体拒绝,活配置零触碰)。内容判定性 warn/error
|
|
263
270
|
* (描述 eff 真伪,与是否 commit 无关)在此发;世代描述性 info 只装载荷,post-commit 发。 */
|
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* #322 —— **世代账**(design/322 §2 世代账,codex 设计审 F4)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 身份标 = 上游 `EffectiveConfig.version`,**刻意不造跨进程 epoch**(codex F4 语义不变):
|
|
5
5
|
* · 同版本幂等重放(etag 未推进的下一拍)不该伪增世代;
|
|
6
|
-
* · 双副本对账「是不是同一代」必须能跨进程判 ——
|
|
6
|
+
* · 双副本对账「是不是同一代」必须能跨进程判 —— 同 source version = 同一代。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
8
|
+
* 🔴 #325(test [4809],[4810] 阻断裁定):**序轴与身份标分离**。source version 在
|
|
9
|
+
* `CONFIG_PROVIDER=local` 下是 registry-core file-store 的内容哈希(u53,上游契约只承诺
|
|
10
|
+
* 「同 files ⇒ 同 number」,从未承诺序)——把它当序数消费,哈希更小的合法新代会被永久
|
|
11
|
+
* 静默忽略且本账双盲(target/applied 都不动)。修后:**推进判断吃收货处铸的进程内单调
|
|
12
|
+
* ordinal**(config-center `generationOf`;等版本重放沿用同 ordinal=幂等语义保留),
|
|
13
|
+
* `version` 只做对账身份标——configTargetVersion 语义从「max(见过)」变为「**最后**见过的
|
|
14
|
+
* 源代」(remote 单调腿二者等价;local 腿这才是真相)。
|
|
15
|
+
*
|
|
16
|
+
* 读面两个:`/health` 的三键(target / applied / 失败持续时长;#325 起 additive 补
|
|
17
|
+
* targetOrdinal/appliedOrdinal 两键=推进/停滞的机器判据)给编排器判「这个副本要不要摘」;
|
|
18
|
+
* 诊断端点的 `configApply` 段给运维看**按组**的细节(哪一组停在哪一代、上一次为什么被拒)。
|
|
10
19
|
*
|
|
11
20
|
* 🔴 原子承诺的**域**是单进程(§2):本账记的是「本副本的收敛状态」,不是 fleet 的。fleet 级判据 = 各副本
|
|
12
21
|
* /health 的 applied == target,拉齐工具归运维面。
|
|
13
22
|
*/
|
|
14
23
|
/** 一组的收敛状态(批1 只有 limitsHttp 组;后续批次按组追加,读面形不变)。 */
|
|
15
24
|
export interface ConfigApplyGroupState {
|
|
16
|
-
/**
|
|
25
|
+
/** 本组最后一次**真的落地**的源版本(身份标)。 */
|
|
17
26
|
readonly appliedVersion: number;
|
|
18
27
|
/** 被判定为「只能重启生效」的键 + 铸因(热路径拒了,但拒因是姿态而不是坏值)。 */
|
|
19
28
|
readonly deferredKeys?: ReadonlyArray<{
|
|
@@ -29,10 +38,14 @@ export interface ConfigApplyGroupState {
|
|
|
29
38
|
};
|
|
30
39
|
}
|
|
31
40
|
export interface ConfigApplyLedgerView {
|
|
32
|
-
/**
|
|
41
|
+
/** 本副本**最后**见到的源版本(身份标;拉到即算,哪怕候选被整拒)。 */
|
|
33
42
|
readonly configTargetVersion: number;
|
|
34
|
-
/** 最后一次成功 commit
|
|
43
|
+
/** 最后一次成功 commit 的源版本(身份标)。 */
|
|
35
44
|
readonly configAppliedVersion: number;
|
|
45
|
+
/** #325:target 的收货序数(进程内单调)——「见过多少代」的推进判据,哈希方向免疫。 */
|
|
46
|
+
readonly configTargetOrdinal: number;
|
|
47
|
+
/** #325:applied 的收货序数。targetOrdinal 推进而这里停滞 = 「改了没生效」的机器判据。 */
|
|
48
|
+
readonly configAppliedOrdinal: number;
|
|
36
49
|
/** target≠applied 的**持续**时长(ms);收敛时键缺席(健康面不制造噪音)。 */
|
|
37
50
|
readonly configApplyStaleMs?: number;
|
|
38
51
|
readonly groups: Readonly<Record<string, ConfigApplyGroupState>>;
|
|
@@ -40,6 +53,8 @@ export interface ConfigApplyLedgerView {
|
|
|
40
53
|
/** applyEffective 的 post-commit 报告(见 apply-effective.ts `ApplyReport`)。 */
|
|
41
54
|
export interface ConfigApplyReport {
|
|
42
55
|
readonly version: number;
|
|
56
|
+
/** #325:本候选的收货序数(applyEffective opts.generation 原样带回;缺席=version 兼容臂)。 */
|
|
57
|
+
readonly ordinal?: number;
|
|
43
58
|
readonly limits: {
|
|
44
59
|
readonly applied: readonly string[];
|
|
45
60
|
readonly deferred?: ReadonlyArray<{
|
|
@@ -55,10 +70,10 @@ export interface ConfigApplyReport {
|
|
|
55
70
|
readonly fullyApplied?: boolean;
|
|
56
71
|
}
|
|
57
72
|
export interface ConfigApplyLedger {
|
|
58
|
-
/** 拉到一个候选(无论后续是否应用)。 */
|
|
59
|
-
seenTarget(version: number): void;
|
|
73
|
+
/** 拉到一个候选(无论后续是否应用)。ordinal=收货序数,version=身份标。 */
|
|
74
|
+
seenTarget(ordinal: number, version: number): void;
|
|
60
75
|
/** 一个候选整体 commit 成功。 */
|
|
61
|
-
applied(version: number): void;
|
|
76
|
+
applied(ordinal: number, version: number): void;
|
|
62
77
|
/** 消费 applyEffective 的报告(按组记账)。 */
|
|
63
78
|
recordApply(report: ConfigApplyReport): void;
|
|
64
79
|
view(): ConfigApplyLedgerView;
|
|
@@ -1,56 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* #322 —— **世代账**(design/322 §2 世代账,codex 设计审 F4)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 身份标 = 上游 `EffectiveConfig.version`,**刻意不造跨进程 epoch**(codex F4 语义不变):
|
|
5
5
|
* · 同版本幂等重放(etag 未推进的下一拍)不该伪增世代;
|
|
6
|
-
* · 双副本对账「是不是同一代」必须能跨进程判 ——
|
|
6
|
+
* · 双副本对账「是不是同一代」必须能跨进程判 —— 同 source version = 同一代。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
8
|
+
* 🔴 #325(test [4809],[4810] 阻断裁定):**序轴与身份标分离**。source version 在
|
|
9
|
+
* `CONFIG_PROVIDER=local` 下是 registry-core file-store 的内容哈希(u53,上游契约只承诺
|
|
10
|
+
* 「同 files ⇒ 同 number」,从未承诺序)——把它当序数消费,哈希更小的合法新代会被永久
|
|
11
|
+
* 静默忽略且本账双盲(target/applied 都不动)。修后:**推进判断吃收货处铸的进程内单调
|
|
12
|
+
* ordinal**(config-center `generationOf`;等版本重放沿用同 ordinal=幂等语义保留),
|
|
13
|
+
* `version` 只做对账身份标——configTargetVersion 语义从「max(见过)」变为「**最后**见过的
|
|
14
|
+
* 源代」(remote 单调腿二者等价;local 腿这才是真相)。
|
|
15
|
+
*
|
|
16
|
+
* 读面两个:`/health` 的三键(target / applied / 失败持续时长;#325 起 additive 补
|
|
17
|
+
* targetOrdinal/appliedOrdinal 两键=推进/停滞的机器判据)给编排器判「这个副本要不要摘」;
|
|
18
|
+
* 诊断端点的 `configApply` 段给运维看**按组**的细节(哪一组停在哪一代、上一次为什么被拒)。
|
|
10
19
|
*
|
|
11
20
|
* 🔴 原子承诺的**域**是单进程(§2):本账记的是「本副本的收敛状态」,不是 fleet 的。fleet 级判据 = 各副本
|
|
12
21
|
* /health 的 applied == target,拉齐工具归运维面。
|
|
13
22
|
*/
|
|
14
23
|
export function createConfigApplyLedger(now = () => Date.now()) {
|
|
15
|
-
let
|
|
16
|
-
let
|
|
24
|
+
let targetOrd = 0;
|
|
25
|
+
let appliedOrd = 0;
|
|
26
|
+
let targetVersion = 0;
|
|
27
|
+
let appliedVersion = 0;
|
|
17
28
|
/**
|
|
18
29
|
* 🔴 codex R1-F3(真 finding,红先):**有组被拒的世代不算「本副本应用了这一代」**。
|
|
19
30
|
*
|
|
20
31
|
* applyEffective 对一个 limits 被整组拒的候选仍返回 true(其余面照常提交),生产腿随后无条件调
|
|
21
|
-
* `applied(
|
|
22
|
-
*
|
|
32
|
+
* `applied(...)` —— 修前于是 `/health` 报 applied==target、stale 时长消失,而限额面其实停在旧代。
|
|
33
|
+
* 世代账存在的全部理由就是让这种漂移可见,所以拒过的世代在这里被记下,`applied()` 拒绝为它收敛。
|
|
34
|
+
* #325 起按 **ordinal** 记(local 腿同 version 可能隔代重现,序数才是「这一次候选」的唯一键)。
|
|
23
35
|
*/
|
|
24
|
-
const
|
|
36
|
+
const rejectedOrds = new Set();
|
|
25
37
|
/** 首次分歧的时刻 —— **持续**时长的锚(每拍刷新会把「卡了一小时」写成「卡了 0 秒」)。 */
|
|
26
38
|
let divergedSince;
|
|
27
39
|
const groups = {};
|
|
28
40
|
const reconcile = () => {
|
|
29
|
-
if (
|
|
41
|
+
if (targetOrd === appliedOrd)
|
|
30
42
|
divergedSince = undefined;
|
|
31
43
|
else
|
|
32
44
|
divergedSince ??= now();
|
|
33
45
|
};
|
|
34
46
|
return {
|
|
35
|
-
seenTarget(version) {
|
|
36
|
-
if (
|
|
37
|
-
|
|
47
|
+
seenTarget(ordinal, version) {
|
|
48
|
+
if (ordinal > targetOrd) {
|
|
49
|
+
targetOrd = ordinal;
|
|
50
|
+
targetVersion = version;
|
|
51
|
+
}
|
|
38
52
|
reconcile();
|
|
39
53
|
},
|
|
40
|
-
applied(version) {
|
|
41
|
-
if (
|
|
42
|
-
|
|
54
|
+
applied(ordinal, version) {
|
|
55
|
+
if (ordinal > targetOrd) {
|
|
56
|
+
targetOrd = ordinal; // 迟到 boot / LKG 腿:应用的那一代自然也是见过的
|
|
57
|
+
targetVersion = version;
|
|
58
|
+
}
|
|
43
59
|
// 本世代有组被拒 ⇒ 只推进 target(诚实的「见过、没能完整应用」),applied 停在上一个完整世代。
|
|
44
|
-
if (!
|
|
45
|
-
|
|
60
|
+
if (!rejectedOrds.has(ordinal) && ordinal > appliedOrd) {
|
|
61
|
+
appliedOrd = ordinal;
|
|
62
|
+
appliedVersion = version;
|
|
63
|
+
}
|
|
46
64
|
reconcile();
|
|
47
65
|
},
|
|
48
66
|
recordApply(report) {
|
|
49
67
|
const prev = groups.limitsHttp;
|
|
68
|
+
const ord = report.ordinal ?? report.version; // 兼容臂:未传 ordinal 的单元调用退回 version 键
|
|
50
69
|
if (report.limits.rejected)
|
|
51
|
-
|
|
70
|
+
rejectedOrds.add(ord);
|
|
52
71
|
else
|
|
53
|
-
|
|
72
|
+
rejectedOrds.delete(ord); // 同一世代重放且这次过了(拒因已修)⇒ 允许收敛
|
|
54
73
|
if (report.limits.rejected) {
|
|
55
74
|
groups.limitsHttp = {
|
|
56
75
|
appliedVersion: prev?.appliedVersion ?? 0, // 被拒的世代不许推进组的 applied
|
|
@@ -72,8 +91,10 @@ export function createConfigApplyLedger(now = () => Date.now()) {
|
|
|
72
91
|
view() {
|
|
73
92
|
const stale = divergedSince !== undefined ? now() - divergedSince : undefined;
|
|
74
93
|
return {
|
|
75
|
-
configTargetVersion:
|
|
76
|
-
configAppliedVersion:
|
|
94
|
+
configTargetVersion: targetVersion,
|
|
95
|
+
configAppliedVersion: appliedVersion,
|
|
96
|
+
configTargetOrdinal: targetOrd,
|
|
97
|
+
configAppliedOrdinal: appliedOrd,
|
|
77
98
|
...(stale !== undefined ? { configApplyStaleMs: stale } : {}),
|
|
78
99
|
groups: { ...groups },
|
|
79
100
|
};
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -1396,19 +1396,23 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1396
1396
|
// [1596]/[1600] core 1.397 三分:仲裁器**抛错**(店不可达)此前被误吞成 park_resume_won 的假话——
|
|
1397
1397
|
// 「输给了并发 resume」与「根本问不到仲裁结果」是两种不同的行动建议(前者重查状态,后者原样重试
|
|
1398
1398
|
// stop),新码 `park_arbiter_unreachable` 让二者不再共用一个 errorCode。
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1399
|
+
// [4797](core 5.50)details.code 孪生对表:stop 族座位今天仍只铸 `error`(族扫未及,已上板点名),
|
|
1400
|
+
// 但 core 契约方向是「code wins when both present」且未承诺新失败座位必带 error ⇒ 判类读
|
|
1401
|
+
// code-优先-error-回落、在场判据双键取并,未来只铸 code 的失败座位不会漏判成 200 成功。
|
|
1402
|
+
const stopCode = typeof details?.code === "string" ? details.code : details?.error;
|
|
1403
|
+
const parkArbiterUnreachable = stopCode === "park_arbiter_unreachable";
|
|
1404
|
+
const parkResumeWon = stopCode === "park_resume_won";
|
|
1405
|
+
const stillParked = stopCode === "parked_pending_approval" || details?.status === "parked";
|
|
1402
1406
|
// v3.1 批2(统检第二波 high):判别方向翻转——旧形是 (status,error) 的**失败白名单**(status==="running"
|
|
1403
1407
|
// || 三个具名码),core 新增失败码且行状态不在 {running,parked} 时整个分支被跳过落 200,与上面注释自陈
|
|
1404
1408
|
// 的不变量(kill 没落地绝不读成功)正相悖(B5 联合非穷尽的 default-放行形;not_local/park 族/arbiter
|
|
1405
1409
|
// 三次历史新增全靠人追认)。新形:stop 动词的 error **在场即失败**(not_found 已在上方早退),具名臂
|
|
1406
1410
|
// 保留各自 errorCode,未知码落 stop.not_landed 兜底 409——新失败码默认可见,不默认成功。
|
|
1407
|
-
if (stopVerb && details?.error !== undefined) {
|
|
1411
|
+
if (stopVerb && (details?.error !== undefined || details?.code !== undefined)) {
|
|
1408
1412
|
// 1.250:durable 回落臂(core stopTask agentStore 分支)对他实例 running 行应答 error="not_local"
|
|
1409
1413
|
// (没有 kill 被尝试)——与「kill 尝试了没落地」(stop.not_landed)是不同的失败形,各给各的
|
|
1410
1414
|
// errorCode(同 409 家族,additive)。
|
|
1411
|
-
const notLocal =
|
|
1415
|
+
const notLocal = stopCode === "not_local";
|
|
1412
1416
|
sendError(res, 409, notLocal
|
|
1413
1417
|
? "stop.not_local"
|
|
1414
1418
|
: parkArbiterUnreachable
|
|
@@ -1425,7 +1429,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1425
1429
|
? "the pending tool-approval was resolved (resumed) concurrently with this stop attempt — the resume won the race, so nothing was stopped; re-check the task's current status"
|
|
1426
1430
|
: stillParked
|
|
1427
1431
|
? "this task is durably parked awaiting a tool-approval decision, not actively running — there is no live process to interrupt; resolve (or let expire) the pending approval instead"
|
|
1428
|
-
: `stop did not land (${
|
|
1432
|
+
: `stop did not land (${stopCode}) — the process may still be running`, { taskId: runId, target, content: out.content, output: out.details });
|
|
1429
1433
|
return;
|
|
1430
1434
|
}
|
|
1431
1435
|
// Registry projection passed through verbatim (status/retrieval_status/cursor flags); `content` is
|
package/dist/http/server.js
CHANGED
|
@@ -496,10 +496,11 @@ export function createHttpServer(rawDeps) {
|
|
|
496
496
|
const stuck = deps.planeDeferredState?.();
|
|
497
497
|
return stuck ? { modelPlaneDeferred: { version: stuck.version, since: stuck.since, noHandoff: true, ...(stuck.blocked ? { blockedReasons: stuck.blocked } : {}) } } : {};
|
|
498
498
|
})(),
|
|
499
|
-
// #322 世代账三键(additive;未接线 ⇒
|
|
500
|
-
// 语义:target =
|
|
501
|
-
//
|
|
502
|
-
//
|
|
499
|
+
// #322 世代账三键(additive;未接线 ⇒ 键全缺席,/health 形状逐字不变)。
|
|
500
|
+
// 语义:target = 本副本**最后**见到的源版本(身份标);applied = 最后一次真的 commit 的版本;
|
|
501
|
+
// staleMs = 两者**持续**不等的时长(收敛即缺席)。#325 起 additive 补两 ordinal 键(收货序数,
|
|
502
|
+
// 进程内单调)——local 腿 version 是内容哈希无序承诺,「见过多少代/停在哪一代」的推进判断
|
|
503
|
+
// 用 ordinal 键;跨副本「同不同代」的对账仍用 version 身份标。
|
|
503
504
|
...(() => {
|
|
504
505
|
const gen = deps.configApplyState?.();
|
|
505
506
|
if (!gen)
|
|
@@ -507,6 +508,8 @@ export function createHttpServer(rawDeps) {
|
|
|
507
508
|
return {
|
|
508
509
|
configTargetVersion: gen.configTargetVersion,
|
|
509
510
|
configAppliedVersion: gen.configAppliedVersion,
|
|
511
|
+
configTargetOrdinal: gen.configTargetOrdinal,
|
|
512
|
+
configAppliedOrdinal: gen.configAppliedOrdinal,
|
|
510
513
|
...(gen.configApplyStaleMs !== undefined ? { configApplyStaleMs: gen.configApplyStaleMs } : {}),
|
|
511
514
|
};
|
|
512
515
|
})(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.38.0
|
|
3
|
+
"version": "7.38.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.
|
|
57
|
+
"@sema-agent/core": "^5.50.0",
|
|
58
58
|
"@sema-agent/registry-core": "^0.19.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|