@sema-agent/server 6.2.0 → 6.3.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/dist/bake-runner/runner.js +11 -0
- package/dist/bench/s1/arms.d.ts +4 -3
- package/dist/bench/s1/live-deps.d.ts +9 -1
- package/dist/bench/s1/live-deps.js +16 -11
- package/dist/bench/s1/runner-ctx.d.ts +9 -5
- package/dist/bench/s1/runner-ctx.js +10 -4
- package/dist/boot/config-center.d.ts +2 -0
- package/dist/boot/config-center.js +10 -1
- package/dist/boot/resolve-spec.js +10 -7
- package/dist/boot/shutdown.d.ts +10 -0
- package/dist/boot/shutdown.js +3 -1
- package/dist/http/idempotency.js +10 -1
- package/dist/http/routes/tasks.js +5 -0
- package/dist/http/server.js +24 -3
- package/dist/main.js +1 -0
- package/dist/memory-sync-client.js +5 -1
- package/dist/plugins/background-shell-support.d.ts +4 -0
- package/dist/plugins/background-shell-support.js +14 -4
- package/dist/plugins/local-task-attachment-store.d.ts +1 -0
- package/dist/plugins/local-task-attachment-store.js +28 -24
- package/dist/plugins/usage-window-store-sql.js +22 -1
- package/dist/spec-fields.d.ts +4 -4
- package/dist/spec-fields.js +25 -6
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +10 -3
- package/package.json +2 -2
|
@@ -191,6 +191,17 @@ export class BakeRunner {
|
|
|
191
191
|
enqueueIngest(dockerBuildTickFrame(nextOrd(ord), Math.floor(elapsed / 1000)));
|
|
192
192
|
}
|
|
193
193
|
})();
|
|
194
|
+
// #131-6:创建即挂接——rejection 若等到 stopSupervisor(child 退出后)才有人接,会在整个构建
|
|
195
|
+
// 时长内悬置,Node 默认 unhandled-rejections=throw 直接终结 runner 进程。挂接后 supervisor 腿
|
|
196
|
+
// 静默死掉的后果有界:lease 心跳停 → image-api reaper 兜底(与 heartbeat error continue 同口径)。
|
|
197
|
+
void supervisor.catch((err) => {
|
|
198
|
+
try {
|
|
199
|
+
this.o.log.warn("bake_supervisor_error", { bakeId: bake.bakeId, err: String(err) });
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
/* 日志面故障不再外抛 */
|
|
203
|
+
}
|
|
204
|
+
});
|
|
194
205
|
const exit = await child.exited;
|
|
195
206
|
childExited = true;
|
|
196
207
|
this.stopSupervisor(supervisor);
|
package/dist/bench/s1/arms.d.ts
CHANGED
|
@@ -67,12 +67,13 @@ export interface ProfileDeps {
|
|
|
67
67
|
*/
|
|
68
68
|
runCounterfactualOracle?: () => Promise<OracleVerdicts>;
|
|
69
69
|
}
|
|
70
|
-
/** A leaf impl spec — budget already stamped by the harness (the §3.2 red line).
|
|
70
|
+
/** A leaf impl spec — budget already stamped by the harness (the §3.2 red line). All three budget keys live in
|
|
71
|
+
* `limits` (core ≥5.8 reads them ONLY there — a top-level maxTokens/maxCostUsd would be a silently-dead key). */
|
|
71
72
|
export interface SoloImplSpec {
|
|
72
73
|
objective: string;
|
|
73
|
-
maxTokens: number;
|
|
74
|
-
maxCostUsd: number;
|
|
75
74
|
limits: {
|
|
75
|
+
maxTokens: number;
|
|
76
|
+
maxCostUsd: number;
|
|
76
77
|
maxTurns: number;
|
|
77
78
|
};
|
|
78
79
|
}
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
* same anti-reward-hack transfer path SOLO/SUP use). If the integrated tree can't be imported, the cell is
|
|
35
35
|
* `infraFailed` (EXCLUDED) — NEVER a fabricated uniform delivered=false loss for the whole TEAM column.
|
|
36
36
|
*/
|
|
37
|
-
import type
|
|
37
|
+
import { type TaskSpec } from "@sema-agent/core";
|
|
38
|
+
import type { ProfileDeps, SoloImplSpec, SupImplSpec } from "./arms.js";
|
|
38
39
|
import type { TrapSpec } from "./tasks.js";
|
|
39
40
|
/** Live runtime config — the SAME base model + budget every arm runs on (the §3.2 fairness root). */
|
|
40
41
|
export interface LiveRuntimeConfig {
|
|
@@ -88,4 +89,11 @@ export interface LiveCell {
|
|
|
88
89
|
* @param cellId a stable id for the cell (used as sandbox metadata for fleet observability).
|
|
89
90
|
*/
|
|
90
91
|
export declare function buildLiveDeps(rt: LiveRuntimeConfig, trap: TrapSpec, seed: number | string, cellId: string): LiveCell;
|
|
92
|
+
/** The per-cell base TaskSpec every arm's leaf projects from. EXPORTED so the shape test measures the REAL spec
|
|
93
|
+
* handed to the runner (the BUDGET-MATCH anchor is spec.limits, not the harness's own RowBudget echo). */
|
|
94
|
+
export declare function benchBaseSpec(rt: LiveRuntimeConfig, cellId: string): (objective: string) => TaskSpec;
|
|
95
|
+
/** Project a `SoloImplSpec` (budget already stamped by the harness, all keys in `limits`) onto the bench base TaskSpec. */
|
|
96
|
+
export declare function toTaskSpec(base: (objective: string) => TaskSpec, impl: SoloImplSpec): TaskSpec;
|
|
97
|
+
/** Project a `SupImplSpec` onto the bench base TaskSpec PLUS the durable wiring (checkpointStore + ask policy). */
|
|
98
|
+
export declare function toSupTaskSpec(base: (objective: string) => TaskSpec, impl: SupImplSpec, trap: TrapSpec): TaskSpec;
|
|
91
99
|
//# sourceMappingURL=live-deps.d.ts.map
|
|
@@ -272,13 +272,7 @@ function graderTransport(grader) {
|
|
|
272
272
|
*/
|
|
273
273
|
export function buildLiveDeps(rt, trap, seed, cellId) {
|
|
274
274
|
const { brain, models, roles, pricing } = buildBrainIngredients(rt);
|
|
275
|
-
const baseSpec = (
|
|
276
|
-
objective,
|
|
277
|
-
sessionId: `s1-${cellId}`,
|
|
278
|
-
maxTokens: rt.maxTokens ?? 8000,
|
|
279
|
-
maxCostUsd: rt.maxCostUsd ?? 1.0,
|
|
280
|
-
limits: { maxTurns: rt.maxTurns ?? 12 },
|
|
281
|
-
});
|
|
275
|
+
const baseSpec = benchBaseSpec(rt, cellId);
|
|
282
276
|
// Lazily-provisioned, memoized per-cell sandboxes (so a cell that never runs a seam pays zero VM cost; a seam
|
|
283
277
|
// that throws still leaves them on `state` so dispose() reaps them).
|
|
284
278
|
const state = {};
|
|
@@ -458,12 +452,23 @@ export function buildLiveDeps(rt, trap, seed, cellId) {
|
|
|
458
452
|
};
|
|
459
453
|
return { deps, dispose };
|
|
460
454
|
}
|
|
461
|
-
/**
|
|
462
|
-
|
|
463
|
-
|
|
455
|
+
/** The per-cell base TaskSpec every arm's leaf projects from. EXPORTED so the shape test measures the REAL spec
|
|
456
|
+
* handed to the runner (the BUDGET-MATCH anchor is spec.limits, not the harness's own RowBudget echo). */
|
|
457
|
+
export function benchBaseSpec(rt, cellId) {
|
|
458
|
+
// 🔴 core ≥5.8: maxTokens/maxCostUsd are read ONLY from `limits` — a top-level key is silently dead. No `as
|
|
459
|
+
// TaskSpec` here or in the projections below: the compiler is the guard against re-introducing dead keys.
|
|
460
|
+
return (objective) => ({
|
|
461
|
+
objective,
|
|
462
|
+
sessionId: `s1-${cellId}`,
|
|
463
|
+
limits: { maxTokens: rt.maxTokens ?? 8000, maxCostUsd: rt.maxCostUsd ?? 1.0, maxTurns: rt.maxTurns ?? 12 },
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/** Project a `SoloImplSpec` (budget already stamped by the harness, all keys in `limits`) onto the bench base TaskSpec. */
|
|
467
|
+
export function toTaskSpec(base, impl) {
|
|
468
|
+
return { ...base(impl.objective), limits: { ...impl.limits } };
|
|
464
469
|
}
|
|
465
470
|
/** Project a `SupImplSpec` onto the bench base TaskSpec PLUS the durable wiring (checkpointStore + ask policy). */
|
|
466
|
-
function toSupTaskSpec(base, impl, trap) {
|
|
471
|
+
export function toSupTaskSpec(base, impl, trap) {
|
|
467
472
|
const requireApproval = trap.gatedTools ?? ["Bash"];
|
|
468
473
|
const neverAuto = trap.neverAuto ?? [];
|
|
469
474
|
return {
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* clock that makes C2 (human-review wall-time) reproducible.
|
|
5
5
|
*
|
|
6
6
|
* 🔴 THE §3.2 RED LINE (design/89's #1 confound): every leaf TaskSpec across all 3 arms carries the SAME
|
|
7
|
-
* `{ maxTokens, maxCostUsd,
|
|
7
|
+
* `limits.{ maxTokens, maxCostUsd, maxTurns }` (core ≥5.8: budget keys live ONLY in `limits` — a top-level
|
|
8
|
+
* `maxTokens`/`maxCostUsd` is a silently-unread dead key), and TEAM's per-worker budgets sum to ≤ the solo budget — so no
|
|
8
9
|
* arm can win on a bigger pie. `assertBudgetMatch` (below) is the build-time guard the shape test runs.
|
|
9
10
|
*
|
|
10
11
|
* 🔴 BUDGET-MATCH for TEAM is NEW WIRING the live conflict test does NOT do: `leader-conflict-resolver-live.test.ts`
|
|
@@ -37,9 +38,9 @@ export declare function budgetDescriptor(budget: BenchBudget, arm: string, teamW
|
|
|
37
38
|
export interface BenchBudget {
|
|
38
39
|
/** SAME base model all arms (design/89 §3.2 — decorrelation is a separate axis, not the value axis). */
|
|
39
40
|
modelId: string;
|
|
40
|
-
/** `TaskSpec.maxTokens` — SAME all arms. */
|
|
41
|
+
/** `TaskSpec.limits.maxTokens` — SAME all arms. */
|
|
41
42
|
maxTokens: number;
|
|
42
|
-
/** `TaskSpec.maxCostUsd` — SAME all arms (also the per-run spend cap). For TEAM, Σworker ≤ this. */
|
|
43
|
+
/** `TaskSpec.limits.maxCostUsd` — SAME all arms (also the per-run spend cap). For TEAM, Σworker ≤ this. */
|
|
43
44
|
maxCostUsd: number;
|
|
44
45
|
/** `TaskSpec.limits.maxTurns` — SAME all arms. */
|
|
45
46
|
maxTurns: number;
|
|
@@ -89,11 +90,14 @@ export interface RunnerCtx {
|
|
|
89
90
|
* Returns the budget fields to spread onto the spec — kept as a helper so EVERY leaf is stamped identically
|
|
90
91
|
* (the live tests forget this for TEAM workers — the confound). `maxCostUsd` is overridable for the TEAM
|
|
91
92
|
* per-worker split (Σ ≤ solo, enforced by `assertBudgetMatch`).
|
|
93
|
+
*
|
|
94
|
+
* 🔴 ALL THREE keys live under `limits` — core ≥5.8 reads `maxTokens`/`maxCostUsd` ONLY from `TaskSpec.limits`;
|
|
95
|
+
* a top-level key is DEAD (silently unread — the cost/token gates never bind).
|
|
92
96
|
*/
|
|
93
97
|
export declare function leafBudgetFields(budget: BenchBudget, overrideMaxCostUsd?: number): {
|
|
94
|
-
maxTokens: number;
|
|
95
|
-
maxCostUsd: number;
|
|
96
98
|
limits: {
|
|
99
|
+
maxTokens: number;
|
|
100
|
+
maxCostUsd: number;
|
|
97
101
|
maxTurns: number;
|
|
98
102
|
};
|
|
99
103
|
};
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* clock that makes C2 (human-review wall-time) reproducible.
|
|
5
5
|
*
|
|
6
6
|
* 🔴 THE §3.2 RED LINE (design/89's #1 confound): every leaf TaskSpec across all 3 arms carries the SAME
|
|
7
|
-
* `{ maxTokens, maxCostUsd,
|
|
7
|
+
* `limits.{ maxTokens, maxCostUsd, maxTurns }` (core ≥5.8: budget keys live ONLY in `limits` — a top-level
|
|
8
|
+
* `maxTokens`/`maxCostUsd` is a silently-unread dead key), and TEAM's per-worker budgets sum to ≤ the solo budget — so no
|
|
8
9
|
* arm can win on a bigger pie. `assertBudgetMatch` (below) is the build-time guard the shape test runs.
|
|
9
10
|
*
|
|
10
11
|
* 🔴 BUDGET-MATCH for TEAM is NEW WIRING the live conflict test does NOT do: `leader-conflict-resolver-live.test.ts`
|
|
@@ -44,12 +45,17 @@ export function makeBenchClock(startEpochMs = Date.now()) {
|
|
|
44
45
|
* Returns the budget fields to spread onto the spec — kept as a helper so EVERY leaf is stamped identically
|
|
45
46
|
* (the live tests forget this for TEAM workers — the confound). `maxCostUsd` is overridable for the TEAM
|
|
46
47
|
* per-worker split (Σ ≤ solo, enforced by `assertBudgetMatch`).
|
|
48
|
+
*
|
|
49
|
+
* 🔴 ALL THREE keys live under `limits` — core ≥5.8 reads `maxTokens`/`maxCostUsd` ONLY from `TaskSpec.limits`;
|
|
50
|
+
* a top-level key is DEAD (silently unread — the cost/token gates never bind).
|
|
47
51
|
*/
|
|
48
52
|
export function leafBudgetFields(budget, overrideMaxCostUsd) {
|
|
49
53
|
return {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
limits: {
|
|
55
|
+
maxTokens: budget.maxTokens,
|
|
56
|
+
maxCostUsd: overrideMaxCostUsd ?? budget.maxCostUsd,
|
|
57
|
+
maxTurns: budget.maxTurns,
|
|
58
|
+
},
|
|
53
59
|
};
|
|
54
60
|
}
|
|
55
61
|
/**
|
|
@@ -42,6 +42,8 @@ export interface ConfigCenterRuntime {
|
|
|
42
42
|
runnerTierFrozen: boolean;
|
|
43
43
|
pricing: ReturnType<typeof buildPricing>;
|
|
44
44
|
}): void;
|
|
45
|
+
/** #131-2:停刷新环(hardShutdown 收尾链;幂等,未起环时 no-op)。 */
|
|
46
|
+
stopRefreshLoop(): void;
|
|
45
47
|
/** 晚绑取值:refresh 热应用会整个换引用(hook LLM / resolveSpec 每次现取)。 */
|
|
46
48
|
getKeyResolver(): ((model: ModelKeyRef) => Promise<{
|
|
47
49
|
apiKey: string;
|
|
@@ -485,6 +485,8 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
485
485
|
// 函数而非常量:`config.degrade` 今天由 env 独占(applyEffective 不写它),但若哪天中心接管这面,这里
|
|
486
486
|
// 自动跟着走,不会退化成 boot 期快照。车道关(默认)⇒ ctx 空 ⇒ 该片恒 null ⇒ 零行为变化。
|
|
487
487
|
const restartCtx = () => (config.degrade?.reactive ? { reactiveDegradeTo: config.degrade.to } : {});
|
|
488
|
+
// #131-2:60s 刷新环的句柄提到 runtime 闭包层——stopRefreshLoop(hardShutdown 收尾链)要能清它。
|
|
489
|
+
let ccTimer;
|
|
488
490
|
return {
|
|
489
491
|
providerKind: configProvider?.kind,
|
|
490
492
|
promptSource,
|
|
@@ -913,7 +915,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
913
915
|
refreshInFlight = false;
|
|
914
916
|
}
|
|
915
917
|
};
|
|
916
|
-
|
|
918
|
+
ccTimer = setInterval(() => void refreshTick(), 60_000);
|
|
917
919
|
ccTimer.unref?.();
|
|
918
920
|
// Boot-deferred continuation(二轮复审 F5 改形):到货结果按「迟到的 boot」处理,而不是转普通 tick——
|
|
919
921
|
// 普通 tick 的 restartReasons(undefined, r) 会把 prompts/skills 面全判为差异 → restart → 中心持续慢时
|
|
@@ -1020,6 +1022,13 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
1020
1022
|
void bootConfigPending.then((r) => (lkgBooted ? refreshTick(r) : deferredBootApply(r)), () => { });
|
|
1021
1023
|
}
|
|
1022
1024
|
},
|
|
1025
|
+
stopRefreshLoop() {
|
|
1026
|
+
// #131-2:hardShutdown 收尾链——停机中途不再热应用配置(幂等;env 部署没起环时是 no-op)。
|
|
1027
|
+
if (ccTimer !== undefined) {
|
|
1028
|
+
clearInterval(ccTimer);
|
|
1029
|
+
ccTimer = undefined;
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1023
1032
|
};
|
|
1024
1033
|
}
|
|
1025
1034
|
//# sourceMappingURL=config-center.js.map
|
|
@@ -589,21 +589,24 @@ export function createResolveSpec(ctx) {
|
|
|
589
589
|
// model uses its own upstream key; a model without one falls back to the gateway key. undefined
|
|
590
590
|
// when no per-model keys are configured → unchanged single-key behavior.
|
|
591
591
|
getApiKeyAndHeaders: getKeyResolver(), // A10 搬运改写②:活引用取值(原 `keyResolver`)
|
|
592
|
-
// [854]④ per-request 配速:body.limits.{
|
|
592
|
+
// [854]④ per-request 配速:body.limits.{maxWalltimeMs,maxOutputTokens,maxTurns} 现在被收下(核对上游
|
|
593
593
|
// TB2.0 实测诉求;旧姿势「body limits 一律忽略」作废)。合成规则在 resolveTaskLimits(spec-fields.ts):
|
|
594
|
-
// - body.
|
|
594
|
+
// - body.maxWalltimeMs 给了就用 body(caller 显式配速,可低于内建墙;已被可选 TASK_TIMEOUT_MAX_SEC
|
|
595
|
+
// 封顶——env 保秒义,对毫秒键封顶时 ×1000);
|
|
595
596
|
// - body 缺席保持既有姿势 = tenancy 墙钟(单用户 turnkey 无墙 / 多租 2400s、大任务 3600s,
|
|
596
597
|
// clay 2026-07-04 make-real 教训①)+ env TASK_TIMEOUT_SEC 只抬不降(taskWallClockSec 内 Math.max);
|
|
597
598
|
// - maxOutputTokens/maxTurns 直透传(可选 TASK_MAX_OUTPUT_TOKENS_MAX / TASK_MAX_TURNS_MAX 同款封顶)。
|
|
598
|
-
// resume 重放持久化 body 不过 HTTP 400 门 → normalizeLimits defensive(0/负/垃圾按键 DROP,不 throw)
|
|
599
|
+
// resume 重放持久化 body 不过 HTTP 400 门 → normalizeLimits defensive(0/负/垃圾按键 DROP,不 throw);
|
|
600
|
+
// 退役键(timeoutSec/deadline 族)DROP 必须留痕:task_limits_legacy_key_dropped 带被丢键与最终生效墙钟
|
|
601
|
+
// (null=整任务无墙——单用户 turnkey 无 env 墙时的真后果;多租=被延到租户墙)。
|
|
599
602
|
...(() => {
|
|
600
|
-
const limits = resolveTaskLimits(body.limits, taskLimitCaps, taskTimeoutSec, config.requirePrincipal, body.council === true || body.debate === true || scenarioName === "team");
|
|
603
|
+
const limits = resolveTaskLimits(body.limits, taskLimitCaps, taskTimeoutSec, config.requirePrincipal, body.council === true || body.debate === true || scenarioName === "team", (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null }));
|
|
601
604
|
// [1301]③ config catalog server 半场:env 封顶不再是「五层五值互不知情」的暗手——每个真在场
|
|
602
605
|
// 的运营方旋钮以 configOverrides 声明进 spec(advisory,core 折进 config.assembled 的
|
|
603
606
|
// overrideReasons;「谁设的顶」变成读帧不考古)。只声明 SET 了的键(缺省不设=不污染帧)。
|
|
604
607
|
const declarations = [];
|
|
605
608
|
if (taskLimitCaps.timeoutSec !== undefined)
|
|
606
|
-
declarations.push({ key: "server.limits.timeoutSecCap", value: String(taskLimitCaps.timeoutSec), reason: "env TASK_TIMEOUT_MAX_SEC (operator ceiling
|
|
609
|
+
declarations.push({ key: "server.limits.timeoutSecCap", value: String(taskLimitCaps.timeoutSec), reason: "env TASK_TIMEOUT_MAX_SEC (seconds; operator ceiling applied to caller limits.maxWalltimeMs as cap×1000)" });
|
|
607
610
|
if (taskLimitCaps.maxOutputTokens !== undefined)
|
|
608
611
|
declarations.push({ key: "server.limits.maxOutputTokensCap", value: String(taskLimitCaps.maxOutputTokens), reason: "env TASK_MAX_OUTPUT_TOKENS_MAX (operator ceiling)" });
|
|
609
612
|
if (taskLimitCaps.maxTurns !== undefined)
|
|
@@ -620,9 +623,9 @@ export function createResolveSpec(ctx) {
|
|
|
620
623
|
// 「server 设的顶」,声明其来源;single-user 无墙=不声明)。
|
|
621
624
|
if (config.requirePrincipal === true)
|
|
622
625
|
declarations.push({ key: "server.limits.wallClockBaseSec", value: "2400/3600", reason: "tenancy wall-clock base (multi-tenant; big tasks 3600) — TASK_TIMEOUT_SEC raises, never shrinks" });
|
|
623
|
-
// codex F1:core 目录自有键(limits.
|
|
626
|
+
// codex F1:core 目录自有键(limits.maxWalltimeMs 等)**绝不重复声明**——core 已按真 provenance 发
|
|
624
627
|
// 该字段,叠一条 host-declared 字符串值=同键双条矛盾帧(by-key 投影抹掉权威来源)。server 只
|
|
625
|
-
// 声明自己命名空间的键;来源语义(caller vs 墙钟)由
|
|
628
|
+
// 声明自己命名空间的键;来源语义(caller vs 墙钟)由 maxWalltimeMs 的 spec provenance + 上面的
|
|
626
629
|
// Cap 声明组合可读。
|
|
627
630
|
// core 5.8.0:预算族(maxCostUsd/maxTokens/degrade)从 TaskSpec 顶层迁入 limits——顶层键已删,
|
|
628
631
|
// 继续写顶层=经宽类型静默死键(E-HIGH-1 病族)。wire 面不变:body 顶层 maxCostUsd/maxTokens 照收
|
package/dist/boot/shutdown.d.ts
CHANGED
|
@@ -44,6 +44,16 @@ export interface ShutdownCtx {
|
|
|
44
44
|
inflight?: () => number;
|
|
45
45
|
lastActivityAt?: () => number;
|
|
46
46
|
};
|
|
47
|
+
/** #131-2:store 活体探针(main 建)——不停的话 hardShutdown 后仍对正在关闭的池发探针,刷假
|
|
48
|
+
* store_probe_dead 告警(文件头契约 3 的同族漏网)。缺席 = 该部署形没建探针。 */
|
|
49
|
+
storeLiveProbe: {
|
|
50
|
+
stop(): void;
|
|
51
|
+
} | undefined;
|
|
52
|
+
/** #131-2:config 60s 刷新环(config-center startRefreshLoop)——不停的话停机中途还可能热应用
|
|
53
|
+
* 一份新配置。缺席 = 纯 env 部署没起环。 */
|
|
54
|
+
configCenter: {
|
|
55
|
+
stopRefreshLoop(): void;
|
|
56
|
+
} | undefined;
|
|
47
57
|
}
|
|
48
58
|
/** 注册 SIGTERM/SIGINT/SIGHUP 收尾链。**必须在 listen 之后调用**(见文件头「位置即契约」)。 */
|
|
49
59
|
export declare function installShutdownHandlers(ctx: ShutdownCtx): void;
|
package/dist/boot/shutdown.js
CHANGED
|
@@ -17,13 +17,15 @@ import { Runner, defaultTaskRegistry } from "@sema-agent/core";
|
|
|
17
17
|
import { createSighupIdleHandler } from "../sighup-idle.js";
|
|
18
18
|
/** 注册 SIGTERM/SIGINT/SIGHUP 收尾链。**必须在 listen 之后调用**(见文件头「位置即契约」)。 */
|
|
19
19
|
export function installShutdownHandlers(ctx) {
|
|
20
|
-
const { config, logger, server, reaper, otelExporter, breakerState, costQuota, rateLimiter, runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState, } = ctx;
|
|
20
|
+
const { config, logger, server, reaper, otelExporter, breakerState, costQuota, rateLimiter, runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState, storeLiveProbe, configCenter, } = ctx;
|
|
21
21
|
let closing = false;
|
|
22
22
|
const hardShutdown = () => {
|
|
23
23
|
if (closing)
|
|
24
24
|
return;
|
|
25
25
|
closing = true;
|
|
26
26
|
clearInterval(reaper);
|
|
27
|
+
storeLiveProbe?.stop(); // #131-2:契约 3 同族——收尾期不再有探针 tick 打向正在关闭的池
|
|
28
|
+
configCenter?.stopRefreshLoop(); // #131-2:停机中途不再热应用配置
|
|
27
29
|
otelExporter?.stop();
|
|
28
30
|
breakerState?.stop();
|
|
29
31
|
if (costQuota && "stop" in costQuota)
|
package/dist/http/idempotency.js
CHANGED
|
@@ -43,7 +43,16 @@ export class IdempotencyCache {
|
|
|
43
43
|
// cacheable success for the TTL. Handlers also avert an unhandled-rejection on the cached copy; the returned
|
|
44
44
|
// promise is the same object the caller awaits, so errors still surface to the caller.
|
|
45
45
|
promise.then((value) => {
|
|
46
|
-
|
|
46
|
+
// #131-7:谓词抛=当不可缓存(重试重跑)。裸调时谓词一抛,这条**派生** promise 就成了无人接的
|
|
47
|
+
// rejection(调用方拿的是原 promise,接不到它),且条目滞留缓存——双错方向。
|
|
48
|
+
let cacheable = false;
|
|
49
|
+
try {
|
|
50
|
+
cacheable = shouldCache(value);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* fall through: cacheable=false */
|
|
54
|
+
}
|
|
55
|
+
if (!cacheable)
|
|
47
56
|
this.entries.delete(key);
|
|
48
57
|
}, () => this.entries.delete(key));
|
|
49
58
|
return promise;
|
|
@@ -45,6 +45,10 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
45
45
|
if (!res.writableEnded)
|
|
46
46
|
res.write(`event: heartbeat\ndata: {}\n\n`); // real frame (not an SSE comment) — a per-frame-parsing BFF drops comments, so downstream saw a zero-frame window; EventSource clients without a heartbeat listener ignore it (zero break)
|
|
47
47
|
}, 15_000);
|
|
48
|
+
hb.unref?.(); // #131-5①:同文件 durableHeartbeat 口径——停机时心跳不拖事件循环
|
|
49
|
+
// #131-5②:重试客户端断连即清——`cached` 可悬到原始流 settle(deadline 级时长),此前这个
|
|
50
|
+
// timer 一直空转到那时(写有 writableEnded 守卫,烧的是 tick 本身);finally 仍是兜底清。
|
|
51
|
+
res.on("close", () => clearInterval(hb));
|
|
48
52
|
try {
|
|
49
53
|
const resp = await cached;
|
|
50
54
|
if (!res.writableEnded)
|
|
@@ -160,6 +164,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
160
164
|
if (!res.writableEnded && !res.destroyed)
|
|
161
165
|
res.write(`event: heartbeat\ndata: {}\n\n`); // real frame (not an SSE comment) — a per-frame-parsing BFF drops comments, so downstream saw a zero-frame window; EventSource clients without a heartbeat listener ignore it (zero break)
|
|
162
166
|
}, 15_000);
|
|
167
|
+
hb.unref?.(); // #131-5①:同文件 durableHeartbeat 口径——停机时心跳不拖事件循环
|
|
163
168
|
// Wrap the whole stream in idemCache.run so the IN-FLIGHT promise is stored BEFORE work begins — a retry
|
|
164
169
|
// that arrives WHILE this stream is still running (relay timeout → re-send same key) is then caught by the
|
|
165
170
|
// peek above and replays this result instead of starting a second billable stream (council). `ranLive`
|
package/dist/http/server.js
CHANGED
|
@@ -942,8 +942,9 @@ export function createHttpServer(rawDeps) {
|
|
|
942
942
|
// [854]④: fresh-submit shape validation for per-request 配速 —— 已知三键必须是正整数,否则 400 fail-loud
|
|
943
943
|
// (body.model silent-drop 同类教训:静默 normalize 成「没配速」正是本件要防的假成功)。0 也拒绝:core 侧
|
|
944
944
|
// `timeoutSec > 0` 才生效(0=不设墙,会绕过 TASK_TIMEOUT_MAX_SEC 封顶),maxTurns/maxOutputTokens 的 0 无意义。
|
|
945
|
-
// 枚举外键与 resilience/attachments 同口径:容忍(只校验已知键)
|
|
946
|
-
//
|
|
945
|
+
// 枚举外键与 resilience/attachments 同口径:容忍(只校验已知键)——但退役键与预算族错位键**点名 400**
|
|
946
|
+
// (它们是「caller 有明确意图、本层却不会采纳」的已知键形,容忍=静默吞意图)。resolveSpec 的
|
|
947
|
+
// normalizeLimits 对 RESUME 重放路径保持 defensive(按键 DROP,不 throw)。
|
|
947
948
|
if (body.limits !== undefined) {
|
|
948
949
|
const l = body.limits;
|
|
949
950
|
if (typeof l !== "object" || l === null || Array.isArray(l)) {
|
|
@@ -953,7 +954,8 @@ export function createHttpServer(rawDeps) {
|
|
|
953
954
|
// core 5.8.0 时限重构:旧键响亮拒并点名替代(core 同姿势 config.limit_unknown_key;「勿自行兼容」是
|
|
954
955
|
// core 的公开指令)。timeoutSec(秒)→ maxWalltimeMs(毫秒,注意单位);deadline 族三 opt-out 随
|
|
955
956
|
// 机制整族退役(nudge/call-cap/graceful-finalize 已从引擎移除),无替代键。fresh fail-loud;resume
|
|
956
|
-
// 重放不过此门,
|
|
957
|
+
// 重放不过此门,resolveTaskLimits 对旧键 defensive DROP + 具名留痕(task_limits_legacy_key_dropped:
|
|
958
|
+
// 存量 suspended 体降级为默认配速——单用户无 env 墙时=整任务无墙,多租=延到租户墙,非「方向安全」)。
|
|
957
959
|
if (l.timeoutSec !== undefined) {
|
|
958
960
|
sendError(res, 400, "request.field_invalid", "limits.timeoutSec was retired in core 5.8.0 — use limits.maxWalltimeMs (milliseconds; opt-in wall clock)");
|
|
959
961
|
return null;
|
|
@@ -969,6 +971,25 @@ export function createHttpServer(rawDeps) {
|
|
|
969
971
|
return null;
|
|
970
972
|
}
|
|
971
973
|
}
|
|
974
|
+
// 预算族错位键(core 5.8.0「预算入 limits」的单向缺口):caller 按 core 键形写 limits.maxCostUsd/
|
|
975
|
+
// maxTokens/degrade/budgetStreamCancel 时,本服务的采纳面在 body **顶层**(resolve-spec.ts
|
|
976
|
+
// cappedCeiling)/运营方 env(MODEL_DEGRADE_*)/core 默认(budgetStreamCancel=maxCostUsd 在场即
|
|
977
|
+
// true)——不点名拒则被静默吞掉,方向「更松」(caller 以为封了 0.5 刀,实际无成本上限)。
|
|
978
|
+
// 退役键同姿势 fail-loud;resume 重放不过此门,normalizeLimits 对这四键维持 defensive DROP。
|
|
979
|
+
for (const k of ["maxCostUsd", "maxTokens"]) {
|
|
980
|
+
if (l[k] !== undefined) {
|
|
981
|
+
sendError(res, 400, "request.field_invalid", `limits.${k} is not read from limits here — send it at the request body top level (${k}); it is capped by the operator ceiling ${k === "maxCostUsd" ? "MAX_TASK_COST_USD" : "MAX_TASK_TOKENS"}`);
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if (l.degrade !== undefined) {
|
|
986
|
+
sendError(res, 400, "request.field_invalid", "limits.degrade is operator-controlled (env MODEL_DEGRADE_TO / MODEL_DEGRADE_AT_COST_FRACTION), not caller-settable — remove it");
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
if (l.budgetStreamCancel !== undefined) {
|
|
990
|
+
sendError(res, 400, "request.field_invalid", "limits.budgetStreamCancel is not caller-settable here — the engine default applies (armed whenever a cost budget is present); remove it");
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
972
993
|
for (const k of ["maxWalltimeMs", "maxOutputTokens", "maxTurns"]) {
|
|
973
994
|
const v = l[k];
|
|
974
995
|
if (v !== undefined && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
|
package/dist/main.js
CHANGED
|
@@ -999,6 +999,7 @@ async function main() {
|
|
|
999
999
|
installShutdownHandlers({
|
|
1000
1000
|
config, logger, server, reaper, otelExporter, breakerState, costQuota, rateLimiter,
|
|
1001
1001
|
runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState,
|
|
1002
|
+
storeLiveProbe, configCenter, // #131-2:两个漏网的进程级后台环进收尾链
|
|
1002
1003
|
});
|
|
1003
1004
|
}
|
|
1004
1005
|
void main().catch((err) => {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// ③ 串行化 + 节流:syncOnce 按 promise 链串行(两轮并发会在同一文件面上交错写);trigger =
|
|
11
11
|
// fire-and-forget,上一轮在飞(排队中/执行中)则跳过(设计:简单 inflight 布尔)。
|
|
12
12
|
// conflicts = logger.warn 逐条上报(不自动 ladder——败者铸 sibling 的解决动作留给 operator/后续单)。
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
13
14
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
14
15
|
import { dirname, join } from "node:path";
|
|
15
16
|
import { encodeScopeSegment, syncMemoryScope, } from "@sema-agent/core";
|
|
@@ -85,7 +86,10 @@ async function loadCursor(path, scope, peer, log) {
|
|
|
85
86
|
/** 写 cursor:tmp + rename 原子(半写的 cursor = 下轮坏 JSON 当首轮,重收敛而非错基线)。 */
|
|
86
87
|
async function saveCursor(path, cursor) {
|
|
87
88
|
await mkdir(dirname(path), { recursive: true });
|
|
88
|
-
|
|
89
|
+
// #131-3:staging 名带熵(pid+random)——共享 memoryRoot 的跨进程形(server + run-local 同 data 根)
|
|
90
|
+
// 用固定 `.tmp` 会互抢 staging(一侧 rename 走对方的 tmp ⇒ 对方 ENOENT / 写出对方字节);同仓其余
|
|
91
|
+
// 原子写(config-lkg / local-session-store / skills-mcp / workflow-completion-inbox)全带熵,此处对齐。
|
|
92
|
+
const tmp = `${path}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
89
93
|
await writeFile(tmp, JSON.stringify(cursor), "utf8");
|
|
90
94
|
await rename(tmp, path);
|
|
91
95
|
}
|
|
@@ -143,6 +143,10 @@ export declare class BackgroundShellManager<S> {
|
|
|
143
143
|
* all-or-nothing: put every operation that can throw BEFORE the first side effect, or clean up on the throw
|
|
144
144
|
* path itself. Today's three lane builders are entirely non-throwing (listener attach + object construction).
|
|
145
145
|
*/
|
|
146
|
+
/** #131-T0:两条注册腿共用的 BG 超时钳制。非有限值(NaN/±Infinity)当缺席回退 default——
|
|
147
|
+
* Math.max/min 对 NaN 全塌 NaN,setTimeout(NaN) 被 Node 折成 1ms = 后台 shell 秒杀
|
|
148
|
+
* (方向反转:想给超时变成即杀;同族判例 TASK_TIMEOUT_SEC / leader 旋钮非法值)。 */
|
|
149
|
+
private boundedBgTimeoutSec;
|
|
146
150
|
adoptSync(builder: (ctx: LaunchCtx) => S, timeoutSec?: number): {
|
|
147
151
|
shellId: BackgroundShellId;
|
|
148
152
|
} | undefined;
|
|
@@ -146,11 +146,22 @@ export class BackgroundShellManager {
|
|
|
146
146
|
* all-or-nothing: put every operation that can throw BEFORE the first side effect, or clean up on the throw
|
|
147
147
|
* path itself. Today's three lane builders are entirely non-throwing (listener attach + object construction).
|
|
148
148
|
*/
|
|
149
|
+
/** #131-T0:两条注册腿共用的 BG 超时钳制。非有限值(NaN/±Infinity)当缺席回退 default——
|
|
150
|
+
* Math.max/min 对 NaN 全塌 NaN,setTimeout(NaN) 被 Node 折成 1ms = 后台 shell 秒杀
|
|
151
|
+
* (方向反转:想给超时变成即杀;同族判例 TASK_TIMEOUT_SEC / leader 旋钮非法值)。 */
|
|
152
|
+
boundedBgTimeoutSec(timeoutSec) {
|
|
153
|
+
const wanted = timeoutSec !== undefined && Number.isFinite(timeoutSec) ? timeoutSec : undefined;
|
|
154
|
+
// caps 同座防御:default/max 由各 lane 从常数×cfg 推导(k8s 腿含 cfg.timeoutMs 除法),坏输入会把
|
|
155
|
+
// NaN 带进 caps——任何一格非有限即回退安全常数(30min/24h,方向:保「有界但不即杀」,§3.6 不破)。
|
|
156
|
+
const dflt = Number.isFinite(this.caps.defaultBgTimeoutSec) ? this.caps.defaultBgTimeoutSec : 1_800;
|
|
157
|
+
const max = Number.isFinite(this.caps.maxBgTimeoutSec) ? this.caps.maxBgTimeoutSec : 86_400;
|
|
158
|
+
// Bounded BG timeout — never unbounded (design/103 §3.6). `maxBgTimeoutSec` is the fail-closed ceiling.
|
|
159
|
+
return Math.min(Math.max(1, wanted ?? dflt), max);
|
|
160
|
+
}
|
|
149
161
|
adoptSync(builder, timeoutSec) {
|
|
150
162
|
if (this.liveCount() >= this.caps.maxConcurrent)
|
|
151
163
|
return undefined; // refused → exec keeps the foreground
|
|
152
|
-
|
|
153
|
-
const bgTimeoutSec = Math.min(Math.max(1, timeoutSec ?? this.caps.defaultBgTimeoutSec), this.caps.maxBgTimeoutSec);
|
|
164
|
+
const bgTimeoutSec = this.boundedBgTimeoutSec(timeoutSec);
|
|
154
165
|
const shellId = `bg_${++this.counter}_${randomUUID()}`; // opaque — never the provider pid (§3.8)
|
|
155
166
|
const applyTerminal = (e, failed, exitCode) => {
|
|
156
167
|
if (e.status !== "running")
|
|
@@ -198,8 +209,7 @@ export class BackgroundShellManager {
|
|
|
198
209
|
if (this.liveCount() >= this.caps.maxConcurrent) {
|
|
199
210
|
return fail(new BackgroundShellError("limit_exceeded", `Too many running background shells (max ${this.caps.maxConcurrent}); KillShell one first.`));
|
|
200
211
|
}
|
|
201
|
-
|
|
202
|
-
const bgTimeoutSec = Math.min(Math.max(1, timeoutSec ?? this.caps.defaultBgTimeoutSec), this.caps.maxBgTimeoutSec);
|
|
212
|
+
const bgTimeoutSec = this.boundedBgTimeoutSec(timeoutSec); // #131-T0:与 adoptSync 同座(NaN 当缺席)
|
|
203
213
|
// Opaque brand — NOT derived from the provider job/pid (design/103 §3.8 越权红线).
|
|
204
214
|
const shellId = `bg_${++this.counter}_${randomUUID()}`;
|
|
205
215
|
const applyTerminal = (e, failed, exitCode) => {
|
|
@@ -2,6 +2,7 @@ import type { TaskAttachmentStore, TaskAttachmentRecord } from "./task-attachmen
|
|
|
2
2
|
export declare class LocalTaskAttachmentStore implements TaskAttachmentStore {
|
|
3
3
|
private readonly dir;
|
|
4
4
|
private metas;
|
|
5
|
+
private hydration;
|
|
5
6
|
constructor(rootDir: string);
|
|
6
7
|
private hydrate;
|
|
7
8
|
put(rec: TaskAttachmentRecord & {
|
|
@@ -13,35 +13,39 @@ const ID_RE = /^[0-9a-f-]{16,64}$/i; // uuid 形(server 铸);水合时非此形
|
|
|
13
13
|
export class LocalTaskAttachmentStore {
|
|
14
14
|
dir;
|
|
15
15
|
metas;
|
|
16
|
+
hydration;
|
|
16
17
|
constructor(rootDir) {
|
|
17
18
|
this.dir = join(rootDir, "attachments");
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
hydrate() {
|
|
21
|
+
// #131-1:记「在飞的 promise」而非「结果」(同仓正确形=local-session-store.loaded)。记结果时,
|
|
22
|
+
// 同进程两条并发首触各自 readdir,晚到的空快照会覆盖已被 put() 写入的索引——盘上有档、内存答 null。
|
|
23
|
+
this.hydration ??= (async () => {
|
|
24
|
+
const m = new Map();
|
|
25
|
+
try {
|
|
26
|
+
for (const f of await fsp.readdir(this.dir)) {
|
|
27
|
+
if (!f.endsWith(".json"))
|
|
28
|
+
continue;
|
|
29
|
+
const id = f.slice(0, -5);
|
|
30
|
+
if (!ID_RE.test(id))
|
|
31
|
+
continue;
|
|
32
|
+
try {
|
|
33
|
+
const meta = JSON.parse(await fsp.readFile(join(this.dir, f), "utf8"));
|
|
34
|
+
if (meta && meta.id === id)
|
|
35
|
+
m.set(id, meta);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* 半写/损坏 meta:跳过(bin 无 meta 即孤儿,reap 收) */
|
|
39
|
+
}
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
catch {
|
|
43
|
+
/* 目录不存在 = 空店 */
|
|
44
|
+
}
|
|
45
|
+
this.metas = m;
|
|
46
|
+
return m;
|
|
47
|
+
})();
|
|
48
|
+
return this.hydration;
|
|
45
49
|
}
|
|
46
50
|
async put(rec) {
|
|
47
51
|
const metas = await this.hydrate();
|
|
@@ -14,7 +14,28 @@ function parseRecord(raw, key) {
|
|
|
14
14
|
if (typeof v !== "object" || v === null || !Array.isArray(r.slots) || !Array.isArray(r.buckets)) {
|
|
15
15
|
throw new Error(`usage_window record for key "${key}" has an invalid shape (expected {slots[],buckets[]}) — refusing to mis-count a governance window`);
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
// 深度对齐 core FileUsageWindowStore(validateSlots/validateBuckets):内层坏形若放过,非数值
|
|
18
|
+
// tokens 参与累加得 "0oops" ⇒ exhausted:false,超额账户被判未耗尽。逐条校验,任一不合即 fail-loud。
|
|
19
|
+
const badShape = (detail) => new Error(`usage_window record for key "${key}" has an invalid shape (${detail}) — refusing to mis-count a governance window`);
|
|
20
|
+
const finite = (x) => typeof x === "number" && Number.isFinite(x);
|
|
21
|
+
const slots = r.slots.map((s) => {
|
|
22
|
+
if (s === null || typeof s !== "object")
|
|
23
|
+
throw badShape("`slots` entry is not an object");
|
|
24
|
+
const { at, tokens } = s;
|
|
25
|
+
if (!finite(at) || !finite(tokens))
|
|
26
|
+
throw badShape("`slots` entry has a non-finite-numeric `at`/`tokens`");
|
|
27
|
+
return { at, tokens };
|
|
28
|
+
});
|
|
29
|
+
const buckets = r.buckets.map((b) => {
|
|
30
|
+
if (b === null || typeof b !== "object")
|
|
31
|
+
throw badShape("`buckets` entry is not an object");
|
|
32
|
+
const { windowMs, openedAt, tokens } = b;
|
|
33
|
+
if (!finite(windowMs) || windowMs <= 0 || !finite(openedAt) || !finite(tokens)) {
|
|
34
|
+
throw badShape("`buckets` entry has a non-finite-numeric `windowMs`/`openedAt`/`tokens` (windowMs must be > 0)");
|
|
35
|
+
}
|
|
36
|
+
return { windowMs, openedAt, tokens };
|
|
37
|
+
});
|
|
38
|
+
return { slots, buckets };
|
|
18
39
|
}
|
|
19
40
|
export async function ensureTiDBUsageWindowSchema(pool) {
|
|
20
41
|
await pool.query(`CREATE TABLE IF NOT EXISTS ${USAGE_WINDOW_TABLE} (
|
package/dist/spec-fields.d.ts
CHANGED
|
@@ -93,12 +93,12 @@ export interface TaskLimitCaps {
|
|
|
93
93
|
/** TASK_MAX_TURNS_MAX */ maxTurns?: number;
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
|
-
* [854]④ — normalize `body.limits` → 请求方配速意图 {
|
|
96
|
+
* [854]④ — normalize `body.limits` → 请求方配速意图 {maxWalltimeMs?, maxOutputTokens?, maxTurns?}。DEFENSIVE
|
|
97
97
|
* (resume 重放持久化 body 不过 HTTP 400 门):仅正的有限数字存活(floor 取整),0/负/垃圾按键 DROP —— core 侧
|
|
98
|
-
*
|
|
98
|
+
* 墙钟 > 0 才生效(0=不设墙,会绕过运营方封顶)、maxTurns/maxOutputTokens 的 0 无意义。每键各自被
|
|
99
99
|
* 运营方上限旋钮 clamp(Math.min;旋钮缺省不设=不封顶)。全空 ⇒ undefined。
|
|
100
100
|
*/
|
|
101
|
-
export declare function normalizeLimits(v: unknown, caps: TaskLimitCaps): {
|
|
101
|
+
export declare function normalizeLimits(v: unknown, caps: TaskLimitCaps, warn?: (event: string, fields: Record<string, unknown>) => void): {
|
|
102
102
|
maxWalltimeMs?: number;
|
|
103
103
|
maxOutputTokens?: number;
|
|
104
104
|
maxTurns?: number;
|
|
@@ -120,5 +120,5 @@ export declare function normalizeApproachNotice(raw: unknown): false | {
|
|
|
120
120
|
* - `maxOutputTokens` / `maxTurns`:直透传(各自可选 env 上限已 clamp)。
|
|
121
121
|
* 全空 ⇒ undefined(spec 不挂 limits 键,byte-compat)。
|
|
122
122
|
*/
|
|
123
|
-
export declare function resolveTaskLimits(bodyLimits: unknown, caps: TaskLimitCaps, envTimeoutSec: number, requirePrincipal: boolean | undefined, big: boolean): TaskSpec["limits"];
|
|
123
|
+
export declare function resolveTaskLimits(bodyLimits: unknown, caps: TaskLimitCaps, envTimeoutSec: number, requirePrincipal: boolean | undefined, big: boolean, warn?: (event: string, fields: Record<string, unknown>) => void): TaskSpec["limits"];
|
|
124
124
|
//# sourceMappingURL=spec-fields.d.ts.map
|
package/dist/spec-fields.js
CHANGED
|
@@ -350,18 +350,32 @@ export function toolNameListFromBody(raw) {
|
|
|
350
350
|
export function promptProfileFromBody(raw) {
|
|
351
351
|
return raw === "simple" || raw === "classic" ? raw : undefined;
|
|
352
352
|
}
|
|
353
|
+
/** core 5.8.0 时限重构的退役键(fresh-submit 在 HTTP 门 400 点名;resume 重放在 normalize 层 DROP)。 */
|
|
354
|
+
const RETIRED_LIMIT_KEYS = ["timeoutSec", "deadlineNudge", "callCapByDeadline", "gracefulFinalize"];
|
|
355
|
+
/** 在场的退役键(normalizeLimits/resolveTaskLimits 共用判据源:留痕事件的 droppedKeys 字段)。 */
|
|
356
|
+
function retiredLimitKeysPresent(v) {
|
|
357
|
+
if (v == null || typeof v !== "object" || Array.isArray(v))
|
|
358
|
+
return [];
|
|
359
|
+
const o = v;
|
|
360
|
+
return RETIRED_LIMIT_KEYS.filter((k) => o[k] !== undefined);
|
|
361
|
+
}
|
|
353
362
|
/**
|
|
354
|
-
* [854]④ — normalize `body.limits` → 请求方配速意图 {
|
|
363
|
+
* [854]④ — normalize `body.limits` → 请求方配速意图 {maxWalltimeMs?, maxOutputTokens?, maxTurns?}。DEFENSIVE
|
|
355
364
|
* (resume 重放持久化 body 不过 HTTP 400 门):仅正的有限数字存活(floor 取整),0/负/垃圾按键 DROP —— core 侧
|
|
356
|
-
*
|
|
365
|
+
* 墙钟 > 0 才生效(0=不设墙,会绕过运营方封顶)、maxTurns/maxOutputTokens 的 0 无意义。每键各自被
|
|
357
366
|
* 运营方上限旋钮 clamp(Math.min;旋钮缺省不设=不封顶)。全空 ⇒ undefined。
|
|
358
367
|
*/
|
|
359
|
-
export function normalizeLimits(v, caps) {
|
|
368
|
+
export function normalizeLimits(v, caps, warn) {
|
|
360
369
|
if (v == null || typeof v !== "object" || Array.isArray(v))
|
|
361
370
|
return undefined;
|
|
362
371
|
// core 5.8.0:键集换 maxWalltimeMs(毫秒)。旧键(timeoutSec/deadline 族三 opt-out)fresh-submit 已在
|
|
363
|
-
// HTTP 门响亮 400 点名替代;本层只走 resume 重放的持久化 body——旧键 defensive DROP
|
|
364
|
-
//
|
|
372
|
+
// HTTP 门响亮 400 点名替代;本层只走 resume 重放的持久化 body——旧键 defensive DROP 但**必须留痕**
|
|
373
|
+
// (task_limits_legacy_key_dropped):丢 caller 显式时限=收窄契约,存量 suspended 体降级为默认配速时,
|
|
374
|
+
// 单用户 turnkey 无 env 墙 ⇒ 整任务无墙钟、多租 ⇒ 被延到租户墙(2400/3600s)——两向都可能长于原意图。
|
|
375
|
+
// 绝不静默换算=那是「自行兼容」,core 明令禁止。
|
|
376
|
+
const dropped = retiredLimitKeysPresent(v);
|
|
377
|
+
if (dropped.length > 0)
|
|
378
|
+
warn?.("task_limits_legacy_key_dropped", { droppedKeys: dropped });
|
|
365
379
|
const o = v;
|
|
366
380
|
const pick = (raw, cap) => {
|
|
367
381
|
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 1)
|
|
@@ -405,12 +419,17 @@ export function normalizeApproachNotice(raw) {
|
|
|
405
419
|
* - `maxOutputTokens` / `maxTurns`:直透传(各自可选 env 上限已 clamp)。
|
|
406
420
|
* 全空 ⇒ undefined(spec 不挂 limits 键,byte-compat)。
|
|
407
421
|
*/
|
|
408
|
-
export function resolveTaskLimits(bodyLimits, caps, envTimeoutSec, requirePrincipal, big) {
|
|
422
|
+
export function resolveTaskLimits(bodyLimits, caps, envTimeoutSec, requirePrincipal, big, warn) {
|
|
423
|
+
// 留痕事件在本层发(不透传给 normalizeLimits,免双发):只有装配点知道最终生效墙钟——
|
|
424
|
+
// effectiveMaxWalltimeMs: null = 整任务无墙(单用户 turnkey 无 env 墙时丢弃旧 timeoutSec 的真后果)。
|
|
425
|
+
const dropped = retiredLimitKeysPresent(bodyLimits);
|
|
409
426
|
const requested = normalizeLimits(bodyLimits, caps);
|
|
410
427
|
// core 5.8.0:墙钟键=maxWalltimeMs(毫秒)。taskWallClockSec 仍以秒推导(tenancy 基值+env 只抬不降,
|
|
411
428
|
// 语义原样),装配处换算一次;body 显式给了 maxWalltimeMs 则直接生效(caller 意图,可低于 env 墙)。
|
|
412
429
|
const wallSec = taskWallClockSec(envTimeoutSec, requirePrincipal, big);
|
|
413
430
|
const maxWalltimeMs = requested?.maxWalltimeMs ?? (wallSec !== undefined ? wallSec * 1000 : undefined);
|
|
431
|
+
if (dropped.length > 0)
|
|
432
|
+
warn?.("task_limits_legacy_key_dropped", { droppedKeys: dropped, effectiveMaxWalltimeMs: maxWalltimeMs ?? null });
|
|
414
433
|
const out = { ...(requested ?? {}), ...(maxWalltimeMs !== undefined ? { maxWalltimeMs } : {}) };
|
|
415
434
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
416
435
|
}
|
|
@@ -25,8 +25,8 @@ type BgNotifExcluded = "kind" | "sessionScoped" | "owner" | "scope" | "descripti
|
|
|
25
25
|
type _GuardBgNotif = AssertAllKeysHandled<Exclude<keyof BackgroundChildEvent, BgNotifProjected | BgNotifExcluded>>;
|
|
26
26
|
type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "createdAt";
|
|
27
27
|
type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
|
|
28
|
-
type AskProjected = "toolName" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation";
|
|
29
|
-
type AskExcluded = "
|
|
28
|
+
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation";
|
|
29
|
+
type AskExcluded = "preview" | "principal" | "requiresRealApproval";
|
|
30
30
|
type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
|
|
31
31
|
type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome";
|
|
32
32
|
type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
|
package/dist/trace/project.d.ts
CHANGED
package/dist/trace/project.js
CHANGED
|
@@ -156,9 +156,13 @@ export function toolEndEventData(ev) {
|
|
|
156
156
|
// core 1.442([1898] RB-210):截断诚实体量(各 block 真实大小之和,不含 JSON 包装开销)。
|
|
157
157
|
...(Number.isFinite(ev.totalChars) ? { totalChars: ev.totalChars } : {}),
|
|
158
158
|
...(ev.structured !== undefined ? { structured: redactDeep(ev.structured) } : {}),
|
|
159
|
-
// core 5.9.0 W3([2535]):
|
|
160
|
-
//
|
|
161
|
-
|
|
159
|
+
// core 5.9.0 W3([2535];2026-08-04 复审纠偏):errorCode = core 在 isError 时对**任意工具 ToolResult
|
|
160
|
+
// 的 `details.code`** 的透传 —— 开集,无引擎白名单(runtask 直读 result.details.code,structuredFrom
|
|
161
|
+
// 的 CC_DETAIL_TYPES 白名单只管 structured 键)。引擎码 `gate.parked`/`tool.not_found` 只是两例,
|
|
162
|
+
// 工具层现役即有 `path_not_in_root`/`readonly_out_of_root` 等,自注册工具可加新码 —— 消费端必须
|
|
163
|
+
// 带 default 臂,禁按两值穷举。verbatim 依据:码是机器短串标识符、非用户内容(不脱敏),边界靠
|
|
164
|
+
// 显式上限而非词表 —— >128 字符=非法形,整键丢弃(不截断:截断会铸出 core 从未发过的码)。
|
|
165
|
+
...(typeof ev.errorCode === "string" && ev.errorCode.length <= 128 ? { errorCode: ev.errorCode } : {}),
|
|
162
166
|
...identityFields(ev),
|
|
163
167
|
};
|
|
164
168
|
}
|
|
@@ -486,6 +490,9 @@ export function toolResultFieldsOf(d) {
|
|
|
486
490
|
...(d.truncated ? { truncated: true } : {}),
|
|
487
491
|
...(Number.isFinite(d.totalChars) ? { totalChars: d.totalChars } : {}),
|
|
488
492
|
...(d.structured !== undefined ? { structured: d.structured } : {}),
|
|
493
|
+
// core 5.9.0 W3([2535]):机器可判别的 park/死投影码(`gate.parked`/`tool.not_found`)——写侧
|
|
494
|
+
// toolEndEventData 已带,读腿必须同键 verbatim(absent⇒absent),否则冷回放退回锚 abort 文案字面。
|
|
495
|
+
...(typeof d.errorCode === "string" ? { errorCode: d.errorCode } : {}),
|
|
489
496
|
...identityFields(d),
|
|
490
497
|
};
|
|
491
498
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.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",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"sharp": "^0.35.3"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@sema-agent/sdk": "^6.
|
|
71
|
+
"@sema-agent/sdk": "^6.2.0",
|
|
72
72
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
73
73
|
"@types/node": "22.10.2",
|
|
74
74
|
"@types/pg": "^8.20.0",
|