@sema-agent/server 5.14.0 → 6.1.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 +40 -4
- package/dist/bench/s1/arms.js +2 -2
- package/dist/boot/resolve-spec.js +26 -25
- package/dist/boot/runner-deps.d.ts +4 -2
- package/dist/boot/runner-deps.js +6 -0
- package/dist/boot/stores.d.ts +1 -0
- package/dist/boot/stores.js +28 -1
- package/dist/capabilities/code-review-council.js +3 -3
- package/dist/capabilities/team.js +1 -1
- package/dist/config-types.d.ts +11 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +32 -1
- package/dist/http/route-ctx.d.ts +2 -0
- package/dist/http/routes/runs.js +6 -4
- package/dist/http/routes/tasks.js +3 -3
- package/dist/http/server.d.ts +4 -0
- package/dist/http/server.js +63 -16
- package/dist/http/wire-types.d.ts +9 -10
- package/dist/leader/fanout.d.ts +2 -2
- package/dist/leader/leader.js +1 -1
- package/dist/leader/wire.js +8 -6
- package/dist/main.js +6 -4
- package/dist/plugins/remote-env-host.js +3 -51
- package/dist/plugins/remote-env-k8s.d.ts +5 -0
- package/dist/plugins/remote-env-k8s.js +7 -0
- package/dist/plugins/usage-window-store-sql.d.ts +42 -0
- package/dist/plugins/usage-window-store-sql.js +113 -0
- package/dist/run-local.js +2 -1
- package/dist/spec-fields.d.ts +11 -5
- package/dist/spec-fields.js +29 -8
- package/dist/trace/project.d.ts +1 -1
- package/package.json +3 -3
package/USAGE.md
CHANGED
|
@@ -53,7 +53,7 @@ ANTHROPIC_API_KEY=sk-ant-… # 可选:ANTHROPIC_BASE_URL / ANTHROPIC_VERSION /
|
|
|
53
53
|
|
|
54
54
|
**可选 — 韧性栈:分级超时 + 断路器(core 1.38,叠在 failover 之下)**
|
|
55
55
|
```bash
|
|
56
|
-
# ③ 分级超时:任务级
|
|
56
|
+
# ③ 分级超时:任务级 limits.maxWalltimeMs(6.0.0 起,毫秒)之下补两段,各记为可重试的 [network] → 触发重试/断路器/failover
|
|
57
57
|
MODEL_CONNECT_TIMEOUT_MS=8000 # fetch 迟迟不返响应头(网关连不上)→ 中止
|
|
58
58
|
MODEL_FIRST_TOKEN_TIMEOUT_MS=30000 # SSE 已开但迟迟不吐第一个 delta(网关 hang);reasoning 的首个 thinking 也算首 token
|
|
59
59
|
MODEL_IDLE_TIMEOUT_MS=20000 # (1.40.1)出过 token 后中途卡死:每个 delta 重置,静默超时→中止(补 first-token 只管首字)
|
|
@@ -221,6 +221,9 @@ SEMA_REGISTRY_URL=http://<config-center-host>:3100 # 启动拉 GET /api/config
|
|
|
221
221
|
SEMA_REGISTRY_TOKEN=<SERVICE_PULL_TOKEN 的值> # 取自配置控制面主机 .env;只读拉取令牌
|
|
222
222
|
SEMA_REGISTRY_DRY_RUN=true # 安全灰度:只 LOG 中心配置 vs env 推导的差异,不 apply
|
|
223
223
|
SEMA_REGISTRY_WORKER=<worker名> # 可选:拉取 /effective?worker=<名> 取该 worker 的 roster(reconciler 按 worker 注);不设=全局 roster(向后兼容)
|
|
224
|
+
FLEET_ADVERTISE_ADDRESS=http://<本机可达IP>:8090 # 可选:设了才启 fleet 上报腿(announce/heartbeat+usage 批报到中心)。
|
|
225
|
+
# 必须是可解析的 http(s) base URL——host:port 等坏形 5.13.0 起 boot 响亮拒
|
|
226
|
+
# (此前静默每拍 announce 400,worker 永不注册)。
|
|
224
227
|
```
|
|
225
228
|
- 中心**空/未发布** → `applyEffective` 回落 env + 内建 teams 并 warn `config_center_unpublished`,**不影响在跑的服务**(接了也安全)。
|
|
226
229
|
- **灰度姿势**(配置控制面 AI 建议):先 `SEMA_REGISTRY_DRY_RUN=true` 起一轮,看日志 `sema_registry_dry_run`(中心给的 models/roles/teams + 会否覆盖 default、per-model apiKeyEnv)对得上 env 再去掉该 flag 真正 apply。
|
|
@@ -412,14 +415,47 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
|
|
|
412
415
|
| HTTP `200` + `status:"completed"` | 成功 | 取 `result` |
|
|
413
416
|
| `status:"blocked"` | agent 主动报卡住 | 看 `blockedReason`,补信息再发 |
|
|
414
417
|
| `status:"failed"` + `errorCode:"conflict"` | 跨实例乐观锁丢失 | 直接重试(幂等) |
|
|
415
|
-
| `status:"
|
|
418
|
+
| `status:"failed"` + `errorCode:"limits.max_walltime_exceeded"` | 墙钟到限(6.0.0 起 `status:"timeout"` 退役) | 拆小任务 / 提高 `limits.maxWalltimeMs`(毫秒) |
|
|
416
419
|
| HTTP `401` | 缺 `Authorization` / 缺 `x-agent-principal`(要求时) | 补头 |
|
|
417
420
|
| HTTP `403` / `404`(session/run) | 不是该 principal 的资源 | 用正确身份 |
|
|
418
421
|
| HTTP `409`(`/v1/runs`) | 同 session 已有活跃 run | 等它完成 / 用返回的 `activeTaskId` |
|
|
419
422
|
| HTTP `429` | 限流 | 看 `Retry-After` 退避 |
|
|
423
|
+
| HTTP `429` + `errorCode:"usage.window_exhausted"` | 部署级 token 治理窗耗尽(`USAGE_WINDOWS`) | 看响应体 `retryAfterSec` 退避;窗滑动/桶到期后放行 |
|
|
420
424
|
| HTTP `501`(`/v1/runs`) | 内存模式不支持异步 | 配 MySQL 协议存储(`SESSION_BACKEND=mysql`) |
|
|
421
425
|
|
|
422
426
|
**机器码**:每个 4xx/5xx 响应体都带一个 `errorCode`(与人类文案 `error` 并列),这是**唯一**该拿来做
|
|
423
427
|
程序分支的字段——**别锚 `error` 文案**。前缀族固定:`auth.` / `request.` / `not_found.` / `conflict.` /
|
|
424
|
-
`limit.` / `capability.`(本部署没接这个面)/ `feature.`(开关没开)/
|
|
425
|
-
|
|
428
|
+
`limit.` / `usage.`(部署级治理窗)/ `capability.`(本部署没接这个面)/ `feature.`(开关没开)/
|
|
429
|
+
`internal.` / `state.`;未知码按前缀兜底永远安全。完整码表见 `docs/ASSISTANT-WIRE-CONTRACT.md` §附录 A。
|
|
430
|
+
|
|
431
|
+
### 9.1 limits.approachNotice(6.1.0 起)
|
|
432
|
+
|
|
433
|
+
引擎默认在上下文用量到 80%/95% 时各注入一次接近提示(SSE 上是 `steering_injected` 帧、
|
|
434
|
+
`source:"limit_approach"`)。批处理场景嫌噪声:`limits:{approachNotice:false}` 关闭;调阈:
|
|
435
|
+
`limits:{approachNotice:{at:[0.5,0.9]}}`(两分数 (0,1] 且 r1<=r2,相等合法;坏形 400 点名)。
|
|
436
|
+
|
|
437
|
+
### 9.2 部署级 token 治理窗(USAGE_WINDOWS,6.1.0 起)
|
|
438
|
+
|
|
439
|
+
跨任务的滑动 token 预算(core `usageWindows`),缺省不设=关。单键 JSON 数组:
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
USAGE_WINDOWS='[{"windowMs":18000000,"maxTokens":5000000,"anchor":"first-use"}]' # 5h 窗 500 万 token
|
|
443
|
+
# anchor:"first-use"=首次消费开桶、到期重开;"rolling"=滑动窗(最近 windowMs 内的消费之和)
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
- **key 语义是 per-key 而不是一口总锅**:带 principal 的请求计到各自 principal 的窗,匿名请求计到
|
|
447
|
+
GLOBAL 锅——`REQUIRE_PRINCIPAL=false` 的混流部署下,总放行量=(principal 数+1)×maxTokens。要 fleet
|
|
448
|
+
级总闸请用 quota/lease 面(那是按部署计费轴)。同一调用方有时带凭证有时不带,会在两口锅间劈叉计数。
|
|
449
|
+
- 耗尽:提交面立即 `429 usage.window_exhausted`(体 `retryAfterSec` + `Retry-After` 头);已受理的
|
|
450
|
+
任务撞窗时,基建合格(checkpoint+durable approval)⇒ suspend(reason `usage_window`)等窗放行,
|
|
451
|
+
不合格 ⇒ 响亮失败同码落终态记录。
|
|
452
|
+
- 计量覆盖:主任务、具名/委托 subagent、hook 内部任务全计(部署真花销都进窗)。
|
|
453
|
+
- 坏形 `USAGE_WINDOWS` 拒启(fail-loud 点名第几项);**改窗形≈重开账**(first-use 桶按 windowMs
|
|
454
|
+
配对,改了窗长老桶下次记账即弃)。
|
|
455
|
+
- 存储:SQL 后端=跨副本一致(`usage_window` 表,行锁守恒);local=文件;纯内存 dev=进程内。
|
|
456
|
+
|
|
457
|
+
### 9.3 沙箱寿命声明(6.1.0 起)
|
|
458
|
+
|
|
459
|
+
k8s 沙箱(pod `activeDeadlineSeconds` 真硬死线)向引擎声明 `lifetimeMs`——死线前 ~60s 引擎在 turn
|
|
460
|
+
边界尝试 suspend(reason `env_lifetime`,基建合格时)而不是被平台杀在半途;单个 >60s 的工具调用仍可能
|
|
461
|
+
冲过该窗口,声明是尽力座不是保证。e2b 沙箱有 keepalive 滚动续期、无固定死线,不声明(行为不变)。
|
package/dist/bench/s1/arms.js
CHANGED
|
@@ -76,8 +76,8 @@ function emptyCostBreakdown() {
|
|
|
76
76
|
* measurement was reward-hackable and never trustworthy) is ALSO non-scorable → excluded, never a withhold-credit.
|
|
77
77
|
*/
|
|
78
78
|
export function classifyRunStatus(input) {
|
|
79
|
-
if (input.status === "failed"
|
|
80
|
-
return "infra-failed";
|
|
79
|
+
if (input.status === "failed")
|
|
80
|
+
return "infra-failed"; // core 5.8.0:status "timeout" 退役(walltime 形并入 failed)
|
|
81
81
|
if (input.repairTerminal === "oracle.unprotected")
|
|
82
82
|
return "infra-failed"; // broken/un-isolated oracle → not scorable
|
|
83
83
|
return "scored";
|
|
@@ -624,7 +624,30 @@ export function createResolveSpec(ctx) {
|
|
|
624
624
|
// 该字段,叠一条 host-declared 字符串值=同键双条矛盾帧(by-key 投影抹掉权威来源)。server 只
|
|
625
625
|
// 声明自己命名空间的键;来源语义(caller vs 墙钟)由 timeoutSec 的 spec provenance + 上面的
|
|
626
626
|
// Cap 声明组合可读。
|
|
627
|
-
|
|
627
|
+
// core 5.8.0:预算族(maxCostUsd/maxTokens/degrade)从 TaskSpec 顶层迁入 limits——顶层键已删,
|
|
628
|
+
// 继续写顶层=经宽类型静默死键(E-HIGH-1 病族)。wire 面不变:body 顶层 maxCostUsd/maxTokens 照收
|
|
629
|
+
//(SDK 在发),装配目标改到 limits。budgetStreamCancel 语义照 core 默认(maxCostUsd 在场即 true)。
|
|
630
|
+
const maxCostUsd = cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd);
|
|
631
|
+
const maxTokens = cappedCeiling(body.maxTokens, config.maxTaskTokens);
|
|
632
|
+
const degrade = (() => {
|
|
633
|
+
if (!config.degrade || maxCostUsd === undefined)
|
|
634
|
+
return undefined;
|
|
635
|
+
if (Array.isArray(body.images) && body.images.length > 0 && !config.degrade.toSupportsImages) {
|
|
636
|
+
// S14 (SILENT-FALLBACK P0-f): this drop was undetectable — the task keeps its vision-capable main
|
|
637
|
+
// model and near-budget it hard-fails on cost instead of degrading. Surface it.
|
|
638
|
+
metrics.inc("degrade_dropped_total", { reason: "vision_target" });
|
|
639
|
+
logger.warn("degrade_dropped", { reason: "vision_target", model: picked.model, images: body.images.length });
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
return { to: config.degrade.to, atCostFraction: config.degrade.atCostFraction };
|
|
643
|
+
})();
|
|
644
|
+
const merged = {
|
|
645
|
+
...(limits ?? {}),
|
|
646
|
+
...(maxCostUsd !== undefined ? { maxCostUsd } : {}),
|
|
647
|
+
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
648
|
+
...(degrade !== undefined ? { degrade } : {}),
|
|
649
|
+
};
|
|
650
|
+
return { ...(Object.keys(merged).length > 0 ? { limits: merged } : {}), ...(declarations.length > 0 ? { configOverrides: declarations } : {}) };
|
|
628
651
|
})(),
|
|
629
652
|
// design/129: the TOC/interactive posture (single-user turnkey — the shell's
|
|
630
653
|
// session lane) defaults background children to SESSION scope = CC Backgrounded semantics (a bg Agent/
|
|
@@ -632,30 +655,8 @@ export function createResolveSpec(ctx) {
|
|
|
632
655
|
// deployments keep core's "task" default (no orphans burning tokens). Caller-trusted spec field
|
|
633
656
|
// (systemPrompt tier), NEVER read from the request body. Same tenancy predicate as the wall clock above.
|
|
634
657
|
...(config.requirePrincipal !== true ? { backgroundScope: "session" } : {}),
|
|
635
|
-
// ⑤ Per-task budget gate
|
|
636
|
-
//
|
|
637
|
-
// when crossed. budgetStreamCancel defaults true when maxCostUsd is set.
|
|
638
|
-
maxCostUsd: cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd),
|
|
639
|
-
maxTokens: cappedCeiling(body.maxTokens, config.maxTaskTokens),
|
|
640
|
-
// 1.40 near-budget degradation: only meaningful with a cost ceiling (the fraction is of it).
|
|
641
|
-
// When this task has one, switch to the cheaper model at atCostFraction instead of hard-failing.
|
|
642
|
-
// vision precheck (adversarial-review finding): DROP degrade for an image-carrying task when the
|
|
643
|
-
// degrade TARGET can't read images (toSupportsImages=false). The precheck only sees the picked model, not the
|
|
644
|
-
// external degrade target — so without this a runtime degrade would send the images to a text-only gateway (the
|
|
645
|
-
// opaque 400 the precheck prevents). The task keeps its vision-capable main model; near-budget it hard-fails on
|
|
646
|
-
// cost instead of image-failing. MODEL_DEGRADE_TO_VISION=true opts back in.
|
|
647
|
-
degrade: (() => {
|
|
648
|
-
if (!config.degrade || cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd) === undefined)
|
|
649
|
-
return undefined;
|
|
650
|
-
if (Array.isArray(body.images) && body.images.length > 0 && !config.degrade.toSupportsImages) {
|
|
651
|
-
// S14 (SILENT-FALLBACK P0-f): this drop was undetectable — the task keeps its vision-capable main
|
|
652
|
-
// model and near-budget it hard-fails on cost instead of degrading (rationale above). Surface it.
|
|
653
|
-
metrics.inc("degrade_dropped_total", { reason: "vision_target" });
|
|
654
|
-
logger.warn("degrade_dropped", { reason: "vision_target", model: picked.model, images: body.images.length });
|
|
655
|
-
return undefined;
|
|
656
|
-
}
|
|
657
|
-
return { to: config.degrade.to, atCostFraction: config.degrade.atCostFraction };
|
|
658
|
-
})(),
|
|
658
|
+
// ⑤ Per-task budget gate + 1.40 near-budget degrade:core 5.8.0 起全部在 limits(上方 IIFE 合成),
|
|
659
|
+
// 顶层键已随 core 类型删除。
|
|
659
660
|
// Long-term memory (design/138 S1): enabled when the memory ENGINE is wired AND a scope was derived
|
|
660
661
|
// (single-user only — multi-tenant derives none, memory dark). MF-30 PAUSE (option B, per-request —
|
|
661
662
|
// clay 2026-06-27 confirmed with core): `body.memoryWrite:false` makes THIS run read-only over memory
|
|
@@ -34,6 +34,8 @@ export interface RunnerDepsCtx {
|
|
|
34
34
|
rosterStore: RunnerDeps["rosterStore"];
|
|
35
35
|
backgroundAgentStore: RunnerDeps["backgroundAgentStore"];
|
|
36
36
|
mailboxStore: RunnerDeps["mailboxStore"];
|
|
37
|
+
/** design/166-T1:治理窗账本店(USAGE_WINDOWS 配置时才在场;与 config.usageWindows 成对进 deps)。 */
|
|
38
|
+
usageWindowStore: RunnerDeps["usageWindowStore"];
|
|
37
39
|
brain: ReturnType<typeof createBrain>;
|
|
38
40
|
pricing: ReturnType<typeof buildPricing>;
|
|
39
41
|
tracer: ReturnType<typeof createTracer>;
|
|
@@ -66,7 +68,7 @@ export interface RunnerDepsCtx {
|
|
|
66
68
|
}
|
|
67
69
|
/** design/158 A10 留档发现②:main runner `RunnerDeps` 与 main.ts subRunner 字面量之间此前手工重复
|
|
68
70
|
* 的 ~15 个键,类型标注见 {@link createSharedRunnerDeps} 头注。 */
|
|
69
|
-
export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "sessionPolicyStore">;
|
|
71
|
+
export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore">;
|
|
70
72
|
/**
|
|
71
73
|
* design/158 A10 留档发现②(review 2026-07-29,[1543]§三族A 同源修补的延续):main runner 的
|
|
72
74
|
* `RunnerDeps` 字面量(下方 `createRunnerDeps`)与 `main.ts` 里 subRunner 的 `new Runner({...})`
|
|
@@ -95,7 +97,7 @@ export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "
|
|
|
95
97
|
* 属性,比再抽一层共享基座更强的同源保证),不重复收纳进这里。
|
|
96
98
|
*/
|
|
97
99
|
/** 基座真实消费的窄面(Pick)——subRunner 调用点(main.ts)只需凑这 13 个字段,不必造全量 ctx。 */
|
|
98
|
-
export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "sessionPolicyStore">;
|
|
100
|
+
export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "sessionPolicyStore" | "usageWindowStore">;
|
|
99
101
|
export declare function createSharedRunnerDeps(ctx: SharedRunnerDepsCtx): SharedRunnerDeps;
|
|
100
102
|
export declare function createRunnerDeps(ctx: RunnerDepsCtx): RunnerDeps;
|
|
101
103
|
//# sourceMappingURL=runner-deps.d.ts.map
|
package/dist/boot/runner-deps.js
CHANGED
|
@@ -49,6 +49,12 @@ export function createSharedRunnerDeps(ctx) {
|
|
|
49
49
|
// E6: operator-tightened session tool rules — core folds them into the ToolPolicy FIRST (subtract-only) for tasks
|
|
50
50
|
// carrying a sessionId (a delegated subagent has none → inherits no rules). Opt-in: undefined ⇒ no rules read.
|
|
51
51
|
sessionPolicyStore: ctx.sessionPolicyStore ? ctx.sessionPolicyStore : undefined,
|
|
52
|
+
// design/166-T1:治理窗两键**成对**且进共享基座——主/sub/hook 三 runner 全覆盖(漏挂 subRunner=
|
|
53
|
+
// 子代用量不进窗,治理可被 subagent 刷穿;双记不存在:core stats.nested 是旁路侧栏不折入
|
|
54
|
+
// stats.tokens,governance.commit 是 per-任务 delta 制)。store 只在 config.usageWindows 配置时
|
|
55
|
+
// 构造(boot/stores.ts),两键同真同假。
|
|
56
|
+
usageWindows: ctx.config.usageWindows ? ctx.config.usageWindows : undefined,
|
|
57
|
+
usageWindowStore: ctx.usageWindowStore ? ctx.usageWindowStore : undefined,
|
|
52
58
|
};
|
|
53
59
|
return shared;
|
|
54
60
|
}
|
package/dist/boot/stores.d.ts
CHANGED
|
@@ -28,5 +28,6 @@ export declare function openStores(ctx: OpenStoresCtx): Promise<{
|
|
|
28
28
|
memorySyncRunner: MemorySyncRunner | undefined;
|
|
29
29
|
sessionStore: import("@sema-agent/core").SessionStore;
|
|
30
30
|
breakerState: import("../plugins/breaker-state-sql.js").TiDBBreakerState | import("../plugins/breaker-state-sql.js").PgBreakerState | undefined;
|
|
31
|
+
usageWindowStore: import("@sema-agent/core").UsageWindowStore | undefined;
|
|
31
32
|
}>;
|
|
32
33
|
//# sourceMappingURL=stores.d.ts.map
|
package/dist/boot/stores.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* 3. `breakerState` 放在段尾:brain(下一段)要它,而它要 backend。
|
|
14
14
|
*/
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
-
import { FileBackgroundAgentStore, FileMailboxStore, FileRosterStore } from "@sema-agent/core";
|
|
16
|
+
import { FileBackgroundAgentStore, FileMailboxStore, FileRosterStore, FileUsageWindowStore, InMemoryUsageWindowStore } from "@sema-agent/core";
|
|
17
17
|
import { createMemorySyncRunner, createMemorySyncTransport } from "../memory-sync-client.js";
|
|
18
18
|
import { PgMemoryEngineBackend, ensurePgMemoryEngineSchema } from "../plugins/memory-engine-pg.js";
|
|
19
19
|
import { TiDBMemoryEngineBackend, ensureTiDBMemoryEngineSchema } from "../plugins/memory-engine-tidb.js";
|
|
@@ -22,6 +22,7 @@ import { TiDBMemoryHistoryStore, TiDBMemorySyncStore, ensureTiDBMemoryHistorySch
|
|
|
22
22
|
import { MinioBlobBackend } from "../plugins/blob-backend.js";
|
|
23
23
|
import { PgBackgroundAgentStore, TiDBBackgroundAgentStore, ensurePgBackgroundAgentSchema, ensureTiDBBackgroundAgentSchema } from "../plugins/background-agent-store-sql.js";
|
|
24
24
|
import { PgMailboxStore, TiDBMailboxStore, ensurePgMailboxSchema, ensureTiDBMailboxSchema } from "../plugins/mailbox-store-sql.js";
|
|
25
|
+
import { PgUsageWindowStore, TiDBUsageWindowStore, ensurePgUsageWindowSchema, ensureTiDBUsageWindowSchema } from "../plugins/usage-window-store-sql.js";
|
|
25
26
|
import { PgRosterStore, TiDBRosterStore, ensurePgRosterSchema, ensureTiDBRosterSchema } from "../plugins/roster-store-sql.js";
|
|
26
27
|
import { LocalTaskAttachmentStore } from "../plugins/local-task-attachment-store.js";
|
|
27
28
|
import { PgTaskAttachmentStore, TiDBTaskAttachmentStore, ensurePgTaskAttachmentSchema, ensureTiDBTaskAttachmentSchema } from "../plugins/task-attachment-store.js";
|
|
@@ -261,6 +262,31 @@ export async function openStores(ctx) {
|
|
|
261
262
|
if (mailboxStore)
|
|
262
263
|
logger.info("mailbox_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
|
|
263
264
|
}
|
|
265
|
+
// design/166-T1:治理窗账本店——只在 USAGE_WINDOWS 配置时构造(两键成对进 RunnerDeps,core 的
|
|
266
|
+
// 「windows 有 store 无=INACTIVE 警告」在构造上被排除)。形态跟 mailbox:tidb/pg=SQL twins
|
|
267
|
+
// (ensure-first+行锁,真双库验证,usage-window-store-sql.ts 顶注);local=core FileUsageWindowStore
|
|
268
|
+
// (构造器内部自拼 usage-windows/,直传数据根——复审新6);纯内存 dev=core InMemoryUsageWindowStore
|
|
269
|
+
// (进程内计数,单进程 dev 的诚实形——治理窗设了就得生效,不因无持久层而静默失活)。
|
|
270
|
+
let usageWindowStore;
|
|
271
|
+
if (config.usageWindows) {
|
|
272
|
+
const mysqlPool = backend?.mysqlPool?.();
|
|
273
|
+
const pgPool = backend?.pgPool?.();
|
|
274
|
+
if (pgPool) {
|
|
275
|
+
await ensurePgUsageWindowSchema(async (text, params) => pgPool.query(text, params));
|
|
276
|
+
usageWindowStore = new PgUsageWindowStore(pgPool);
|
|
277
|
+
}
|
|
278
|
+
else if (mysqlPool) {
|
|
279
|
+
await ensureTiDBUsageWindowSchema(mysqlPool);
|
|
280
|
+
usageWindowStore = new TiDBUsageWindowStore(mysqlPool);
|
|
281
|
+
}
|
|
282
|
+
else if (backend?.kind === "local") {
|
|
283
|
+
usageWindowStore = new FileUsageWindowStore(config.localDataRoot ?? localRoot);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
usageWindowStore = new InMemoryUsageWindowStore();
|
|
287
|
+
}
|
|
288
|
+
logger.info("usage_window_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : backend?.kind === "local" ? "file" : "memory", windows: config.usageWindows.length });
|
|
289
|
+
}
|
|
264
290
|
// 142-S5 §1.4: the per-scope export READ face lights up only on the DB memory plane (the multi-tenant
|
|
265
291
|
// truth lives in the scope-partitioned tables; the file posture exports by copying the memory dir, so
|
|
266
292
|
// the route 501s honestly there). History-table WRITE wiring: inside the backend construction above
|
|
@@ -365,6 +391,7 @@ export async function openStores(ctx) {
|
|
|
365
391
|
return {
|
|
366
392
|
backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore,
|
|
367
393
|
taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState,
|
|
394
|
+
usageWindowStore,
|
|
368
395
|
};
|
|
369
396
|
}
|
|
370
397
|
//# sourceMappingURL=stores.js.map
|
|
@@ -86,7 +86,7 @@ async function runLensReviews(subRunner, repoTools, objective, signal, lenses) {
|
|
|
86
86
|
modelRole: "subagent",
|
|
87
87
|
tools: repoTools,
|
|
88
88
|
systemPrompt: lensSystem(lens),
|
|
89
|
-
limits: {
|
|
89
|
+
limits: { maxWalltimeMs: 180_000 },
|
|
90
90
|
enableBlockedReport: false,
|
|
91
91
|
signal,
|
|
92
92
|
});
|
|
@@ -113,7 +113,7 @@ async function runConvergence(subRunner, repoTools, objective, findings, opts, s
|
|
|
113
113
|
tools: repoTools, // members may verify a disputed claim against the real code (Tool-MAD)
|
|
114
114
|
maxTranscriptTokens: 8000,
|
|
115
115
|
synthesizer: { role: "arbiter", systemPrompt: ARBITER_SYSTEM },
|
|
116
|
-
limits: {
|
|
116
|
+
limits: { maxWalltimeMs: 400_000 },
|
|
117
117
|
signal, // 1.28: a parent abort now cascades into the debate members
|
|
118
118
|
});
|
|
119
119
|
// 1.32: synthesizer 两次都失败时 `conclusion` 是 `[unavailable…]` 垃圾串,与真结论无法区分。别当结论
|
|
@@ -134,7 +134,7 @@ async function runConvergence(subRunner, repoTools, objective, findings, opts, s
|
|
|
134
134
|
// The arbiter grounds/dedups/ranks — keep it on the strong `synthesize` role (= main model).
|
|
135
135
|
modelRole: "synthesize",
|
|
136
136
|
systemPrompt: ARBITER_SYSTEM,
|
|
137
|
-
limits: {
|
|
137
|
+
limits: { maxWalltimeMs: 240_000 },
|
|
138
138
|
enableBlockedReport: false,
|
|
139
139
|
signal,
|
|
140
140
|
});
|
|
@@ -68,7 +68,7 @@ export function createTeamTool(subRunner, template, opts) {
|
|
|
68
68
|
? { role: template.synthesizer.role, ...(template.synthesizer.model ? { model: template.synthesizer.model } : {}), modelRole: template.synthesizer.modelRole, systemPrompt: template.synthesizer.systemPrompt ?? "" }
|
|
69
69
|
: undefined,
|
|
70
70
|
maxTranscriptTokens: 8000,
|
|
71
|
-
limits: {
|
|
71
|
+
limits: { maxWalltimeMs: 600_000 },
|
|
72
72
|
signal: ctx.signal, // 1.28: a team abort now cascades into the member runs (review D)
|
|
73
73
|
});
|
|
74
74
|
ctx.reportUsage?.({ tokens: team.stats.tokens, turns: team.stats.turns, tasks: template.members.length * template.rounds + 1 });
|
package/dist/config-types.d.ts
CHANGED
|
@@ -642,6 +642,16 @@ export interface ServiceConfigFlat {
|
|
|
642
642
|
* Running rows are never reaped (that is the orphan-grace sweep's job). Default 30 days, floor 1m.
|
|
643
643
|
* `WORKFLOW_RUN_RETENTION_MS`. */
|
|
644
644
|
workflowRunRetentionMs: number;
|
|
645
|
+
/** design/166-T1 — core 5.8.0 部署级 token 治理窗(`RunnerDeps.usageWindows`):滑动窗内跨任务
|
|
646
|
+
* token 预算,key=principal(缺席=GLOBAL 一口锅,per-key 各一份额度——混流部署语义见 USAGE.md)。
|
|
647
|
+
* `USAGE_WINDOWS` 单键 JSON 数组(每项 {windowMs,maxTokens,anchor:"first-use"|"rolling"});
|
|
648
|
+
* 缺省未设=关(零行为变化)。fail-loud:坏 JSON/空数组/非正整数/未知 anchor → boot throw
|
|
649
|
+
* (parseUsageWindows)。窗形变更≈重开账(first-use 桶按 windowMs 配对,老桶下次 charge 即弃)。 */
|
|
650
|
+
usageWindows?: readonly {
|
|
651
|
+
windowMs: number;
|
|
652
|
+
maxTokens: number;
|
|
653
|
+
anchor: "first-use" | "rolling";
|
|
654
|
+
}[];
|
|
645
655
|
/** core 1.364 durable background agents([1503] 提货单③):reaper 经 `TaskRegistry.reapDurableAgents`
|
|
646
656
|
* (联合 reap——条件删赢了才 release 转录 session;裸 store.reap 会 strand 转录)按 scope 清 TERMINAL
|
|
647
657
|
* `a*` 行,老于此值的删。SQL twins 走 DISTINCT-scope 枚举;local FileBackgroundAgentStore 单租户
|
|
@@ -888,7 +898,7 @@ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled"
|
|
|
888
898
|
* 函数不产出它,但它与 `autonomy` 是同一根治理棒的两半,归本组。 */
|
|
889
899
|
export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
|
|
890
900
|
/** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */
|
|
891
|
-
export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "rosterRetentionMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "imageBakes">;
|
|
901
|
+
export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "rosterRetentionMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "imageBakes">;
|
|
892
902
|
/** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
|
|
893
903
|
export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachmentOrphanGraceMs" | "workspaceFileMaxBytes" | "attachmentMaxBytes" | "attachmentMimeAllowlist" | "attachmentUnboundTtlMs" | "infraCostRates" | "drainGraceMs" | "sighupIdleGraceMs" | "rateLimitPerMin" | "maxTaskCostUsd" | "maxTaskTokens" | "maxPrincipalCostUsd" | "costQuotaWindowSec" | "reapIntervalSec" | "runStaleSec" | "toolResultTtlSec">;
|
|
894
904
|
/** 组:observability(可观测)。 */
|
package/dist/config.d.ts
CHANGED
|
@@ -11,6 +11,15 @@ export type { ServiceConfig, ScopedMcpServer, ImageBakeConfig, ServiceConfigFlat
|
|
|
11
11
|
* config overlay (config-center/facade.ts `applyRuntimeHot`) can re-derive the ENV BASELINE to revert to when center
|
|
12
12
|
* stops managing `autonomy` (a stale center override must not stick — see applyRuntimeHot). */
|
|
13
13
|
export declare function parseAutonomy(raw: string | undefined): Autonomy | undefined;
|
|
14
|
+
/** design/166-T1 — parse `USAGE_WINDOWS`(单键 JSON 数组;逐窗散键在窗数可变时组合爆炸,JSON 是唯一
|
|
15
|
+
* 封闭形)。unset/空串 → undefined(关)。fail-loud 族(numEnv 同纪律):坏 JSON/非数组/空数组/
|
|
16
|
+
* 非正有限整数/未知 anchor 全部 boot throw 点名——「设了但无意义」静默关掉=部署方以为有治理实际没有。
|
|
17
|
+
* server 严于 core(core 允 maxTokens=0=恒拒锅;这里要求正整数,0 配置显然是笔误不是意图)。 */
|
|
18
|
+
export declare function parseUsageWindows(raw: string | undefined): readonly {
|
|
19
|
+
windowMs: number;
|
|
20
|
+
maxTokens: number;
|
|
21
|
+
anchor: "first-use" | "rolling";
|
|
22
|
+
}[] | undefined;
|
|
14
23
|
/** Numeric env with validation: a non-numeric value FAILS at startup instead of silently becoming `NaN`
|
|
15
24
|
* (which e.g. slips past the `runStaleSec` liveness guard and silently disables the reaper). */
|
|
16
25
|
export declare function numEnv(name: string, fallback: string): number;
|
package/dist/config.js
CHANGED
|
@@ -26,6 +26,34 @@ export function parseAutonomy(raw) {
|
|
|
26
26
|
}
|
|
27
27
|
return v;
|
|
28
28
|
}
|
|
29
|
+
/** design/166-T1 — parse `USAGE_WINDOWS`(单键 JSON 数组;逐窗散键在窗数可变时组合爆炸,JSON 是唯一
|
|
30
|
+
* 封闭形)。unset/空串 → undefined(关)。fail-loud 族(numEnv 同纪律):坏 JSON/非数组/空数组/
|
|
31
|
+
* 非正有限整数/未知 anchor 全部 boot throw 点名——「设了但无意义」静默关掉=部署方以为有治理实际没有。
|
|
32
|
+
* server 严于 core(core 允 maxTokens=0=恒拒锅;这里要求正整数,0 配置显然是笔误不是意图)。 */
|
|
33
|
+
export function parseUsageWindows(raw) {
|
|
34
|
+
const v = raw?.trim();
|
|
35
|
+
if (!v)
|
|
36
|
+
return undefined;
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(v);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
throw new Error(`env USAGE_WINDOWS is not valid JSON: ${String(e)} — expected e.g. [{"windowMs":18000000,"maxTokens":5000000,"anchor":"first-use"}]`);
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
45
|
+
throw new Error(`env USAGE_WINDOWS must be a NON-EMPTY JSON array of {windowMs,maxTokens,anchor} (an empty array is a governance no-op nobody chose — unset the env to turn windows off)`);
|
|
46
|
+
}
|
|
47
|
+
const out = parsed.map((w, i) => {
|
|
48
|
+
const o = w;
|
|
49
|
+
const posInt = (n) => typeof n === "number" && Number.isInteger(n) && n > 0;
|
|
50
|
+
if (typeof w !== "object" || w === null || !posInt(o.windowMs) || !posInt(o.maxTokens) || (o.anchor !== "first-use" && o.anchor !== "rolling")) {
|
|
51
|
+
throw new Error(`env USAGE_WINDOWS[${i}] is invalid: expected {windowMs: positive int ms, maxTokens: positive int, anchor: "first-use"|"rolling"} (got ${JSON.stringify(w)})`);
|
|
52
|
+
}
|
|
53
|
+
return Object.freeze({ windowMs: o.windowMs, maxTokens: o.maxTokens, anchor: o.anchor });
|
|
54
|
+
});
|
|
55
|
+
return Object.freeze(out);
|
|
56
|
+
}
|
|
29
57
|
/** Resolve a CSV of env-var NAMES into a {NAME: value} map from THIS process's env (out-of-band secret
|
|
30
58
|
* forwarding into a sandbox). A name with no value set is skipped (not forwarded as undefined). The values
|
|
31
59
|
* are secrets — they live only in the returned map (in-memory), never logged. */
|
|
@@ -1023,6 +1051,8 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1023
1051
|
throw new Error(`WORKFLOW_SIZE_GUIDELINE must be small|medium|large|unrestricted, got "${workflowSizeGuidelineRaw}"`);
|
|
1024
1052
|
}
|
|
1025
1053
|
const workflowSizeGuideline = workflowSizeGuidelineRaw;
|
|
1054
|
+
// design/166-T1:治理窗(fail-loud 解析;unset=undefined=关)。
|
|
1055
|
+
const usageWindows = parseUsageWindows(process.env.USAGE_WINDOWS);
|
|
1026
1056
|
const bakeRunnerPrincipal = env("BAKE_RUNNER_PRINCIPAL", "system:bake-runner");
|
|
1027
1057
|
// ── design/158 B4 ② — the four knobs whose canonical name went from negative to positive. Only the first
|
|
1028
1058
|
// lands on `ServiceConfig`; the other three are read lazily at their use sites (main.ts's LKG gate,
|
|
@@ -1193,6 +1223,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1193
1223
|
workflowOrphanGraceMs: Math.max(60_000, numEnv("WORKFLOW_ORPHAN_GRACE_MS", String(24 * 60 * 60 * 1000))), // SVC-1: 24h default, floor 1m
|
|
1194
1224
|
workflowJournalRetentionMs: Math.max(60_000, numEnv("WORKFLOW_JOURNAL_RETENTION_MS", String(7 * 24 * 60 * 60 * 1000))), // SVC-2: 7d default, floor 1m
|
|
1195
1225
|
workflowRunRetentionMs: Math.max(60_000, numEnv("WORKFLOW_RUN_RETENTION_MS", String(30 * 24 * 60 * 60 * 1000))), // 30d default, floor 1m; SQL twins only
|
|
1226
|
+
...(usageWindows ? { usageWindows } : {}), // design/166-T1:USAGE_WINDOWS 治理窗(fail-loud 已在 parseUsageWindows)
|
|
1196
1227
|
backgroundAgentRetentionMs: Math.max(60_000, numEnv("BG_AGENT_RETENTION_MS", String(7 * 24 * 60 * 60 * 1000))), // core 1.364: 7d default, floor 1m
|
|
1197
1228
|
backgroundAgentStaleRunningMs: Math.max(10 * 60_000, numEnv("BG_AGENT_STALE_RUNNING_MS", String(15 * 60_000))), // core 契约 ≥10min 硬钳(60s 写者心跳)
|
|
1198
1229
|
backgroundAgentParkClaimStaleMs: Math.max(60_000, numEnv("BG_AGENT_PARK_CLAIM_STALE_MS", String(60 * 60_000))), // [1575] F1 修:default 1h,floor 1m(零/负值都不该让整块清算失活)
|
|
@@ -1334,7 +1365,7 @@ const ORCHESTRATION_GROUP_KEYS = [
|
|
|
1334
1365
|
"remoteExec", "worktreeIsolation", "leaderEnabled", "leaderFanoutEnabled", "routerEnabled", "selfOrchestrationEnabled",
|
|
1335
1366
|
"selfOrchestrationModels", "selfOrchestrationWorkerIsolation", "forkEnabled", "experimentalObserverAgents",
|
|
1336
1367
|
"schedulerEnabled", "schedulerSessionWakeup", "schedulerSessionLifetime", "schedulerStorePath", "planModeEnabled", "workflowRunStoreBackend",
|
|
1337
|
-
"workflowOrphanGraceMs", "workflowJournalRetentionMs", "workflowRunRetentionMs", "workflowAgentsReadOnly",
|
|
1368
|
+
"workflowOrphanGraceMs", "workflowJournalRetentionMs", "workflowRunRetentionMs", "usageWindows", "workflowAgentsReadOnly",
|
|
1338
1369
|
"workflowSizeGuideline", "backgroundAgentRetentionMs", "backgroundAgentStaleRunningMs",
|
|
1339
1370
|
"backgroundAgentParkClaimStaleMs", "rosterRetentionMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
|
|
1340
1371
|
"sessionAutoTitle", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled",
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface RouteHelpers {
|
|
|
47
47
|
rateLimited(req: IncomingMessage, res: ServerResponse): boolean;
|
|
48
48
|
quotaExceeded(req: IncomingMessage, res: ServerResponse): boolean;
|
|
49
49
|
leaseDenied(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|
|
50
|
+
/** design/166-T1:治理窗 pre-admission(异步 202 车道的唯一 429 出路;key 公式与 core 单源)。 */
|
|
51
|
+
usageWindowDenied(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|
|
50
52
|
safeDecode(seg: string): string | null;
|
|
51
53
|
isFleetWide(req: IncomingMessage): boolean;
|
|
52
54
|
runOwnerOk(req: IncomingMessage, res: ServerResponse, owner: string | null): boolean;
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -111,7 +111,7 @@ export async function handleRunVerbs(req, res, url, ctx) {
|
|
|
111
111
|
async function handleRunsBody(req, res, url, ctx, miss) {
|
|
112
112
|
const { deps } = ctx;
|
|
113
113
|
const { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints } = ctx.registry;
|
|
114
|
-
const { readJson, rateLimited, quotaExceeded, leaseDenied, runOwnerOk, runSessionAcceptOk } = ctx.helpers;
|
|
114
|
+
const { readJson, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied, runOwnerOk, runSessionAcceptOk } = ctx.helpers;
|
|
115
115
|
const { prepareSpec } = ctx.legs;
|
|
116
116
|
const source = ctx.req.source;
|
|
117
117
|
// Async run: create + return ids immediately, execute in the background.
|
|
@@ -171,7 +171,9 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
171
171
|
}
|
|
172
172
|
// 🔴 复审 C1:lease gate for a genuinely-NEW submission (past the idempotency replay above). Keyed on the
|
|
173
173
|
// VERIFIED submitter = the principal billed for this run's spend (correct principal for the sync door).
|
|
174
|
-
|
|
174
|
+
// design/166-T1:治理窗 pre-admission 同位(idempotency replay 之后——重放已建 run 必须拿回 202 收据,
|
|
175
|
+
// 不得因窗满改判 429;异步 202 车道的唯一 429 出路,core 进场拒是竞态窗兜底)。
|
|
176
|
+
if ((await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
|
|
175
177
|
return;
|
|
176
178
|
const resp = await idemCache.run(idemKey, async () => {
|
|
177
179
|
const sessionId = prepared.spec.sessionId ?? uuidv7();
|
|
@@ -228,7 +230,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
228
230
|
rootTaskId: taskId,
|
|
229
231
|
...fleetRunLabels(prepared.spec.objective), // BC-1: name = short objective preview (description = live-activity, set by the publisher onEvent tool_start)
|
|
230
232
|
});
|
|
231
|
-
void runInBackground(deps.runner, { ...prepared.spec, sessionId }, runStore, taskId, deps.metrics, prepared.auth?.principal, prepared.verify, prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation, // E23: per-run elicitation context (onElicit routes inbound MCP elicitations to this run's stream)
|
|
233
|
+
void runInBackground(deps.runner, { ...prepared.spec, sessionId }, runStore, taskId, deps.metrics, prepared.auth?.principal, prepared.verify, prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, prepared.spec.limits?.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation, // E23: per-run elicitation context (onElicit routes inbound MCP elicitations to this run's stream)
|
|
232
234
|
gatedPrincipal(req, deps.config) ?? null, // E23: the VERIFIED principal that may answer (same source the respond gate uses — never the spoofable header)
|
|
233
235
|
captureTurnAnchor, // E18: per-turn (message eventId → leaf entryId) anchor capture
|
|
234
236
|
fleetPub, // MF-Fleet: the run-scoped fleet-row publisher (onStart/onEvent/onTerminal across all legs)
|
|
@@ -461,7 +463,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
461
463
|
// 并发腿可能先终态化(如 approval.expired),我方写打空却仍答 "cancelled" 就是响应与账本不一致。
|
|
462
464
|
// 写后复读,照实际行应答(锁一样释放了;谁先写赢谁的 errorCode)。
|
|
463
465
|
const finalRow = await rs.getRun(taskId).catch(() => undefined);
|
|
464
|
-
if (finalRow && finalRow.errorCode !== "cancelled" && (finalRow.status === "failed" || finalRow.status === "completed" || finalRow.status === "blocked"
|
|
466
|
+
if (finalRow && finalRow.errorCode !== "cancelled" && (finalRow.status === "failed" || finalRow.status === "completed" || finalRow.status === "blocked")) {
|
|
465
467
|
sendJson(res, 202, { taskId, status: finalRow.status, errorCode: finalRow.errorCode ?? null, note: `session unlocked; the run was terminalized concurrently (${finalRow.errorCode ?? finalRow.status}) before this cancel's write — reporting the actual ledger state` });
|
|
466
468
|
return;
|
|
467
469
|
}
|
|
@@ -22,7 +22,7 @@ export async function handleTasks(req, res, url, ctx) {
|
|
|
22
22
|
async function handleTasksBody(req, res, url, ctx, miss) {
|
|
23
23
|
const { deps } = ctx;
|
|
24
24
|
const { idemCache, inflightRuns, cancelledViaVerb, steerableRuns, counters } = ctx.registry;
|
|
25
|
-
const { rateLimited, quotaExceeded, leaseDenied } = ctx.helpers;
|
|
25
|
+
const { rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
|
|
26
26
|
const { prepareSpec, finalizeTaskResult } = ctx.legs;
|
|
27
27
|
const reqState = ctx.req;
|
|
28
28
|
const source = ctx.req.source;
|
|
@@ -61,7 +61,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
61
61
|
}
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
|
-
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
64
|
+
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
|
|
65
65
|
return;
|
|
66
66
|
const prepared = await prepareSpec(req, res);
|
|
67
67
|
if (!prepared)
|
|
@@ -1032,7 +1032,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1032
1032
|
// (verify → cascade → plain)、短路语义、传参**逐字不变**;`runWithVerification` 仍是整对象
|
|
1033
1033
|
// 直传(快审 F1:cost 顶不在重建中丢失),`runCascade` 仍从操作员梯子 + 本任务预算现算配置。
|
|
1034
1034
|
const verifyLeg = (v) => runWithVerification(deps.runner, specWithSignal, v);
|
|
1035
|
-
const cascadeLeg = () => runCascade(deps.runner, specWithSignal, cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd));
|
|
1035
|
+
const cascadeLeg = () => runCascade(deps.runner, specWithSignal, cascadeConfig(deps.config.cascadeLadder, prepared.spec.limits?.maxCostUsd));
|
|
1036
1036
|
const plainLeg = () => deps.runner.runTask(specWithSignal);
|
|
1037
1037
|
result = await withPrincipal(principal, () => prepared.verify ? verifyLeg(prepared.verify) : prepared.cascade ? cascadeLeg() : plainLeg());
|
|
1038
1038
|
}
|
package/dist/http/server.d.ts
CHANGED
|
@@ -344,6 +344,10 @@ export interface ServiceGovernanceDeps {
|
|
|
344
344
|
costQuota?: QuotaTracker;
|
|
345
345
|
/** D4 AP:worker 侧 quota-lease admission(协商式;center lane 未配 = 缺席 = 零行为)。 */
|
|
346
346
|
fleetLease?: FleetLeaseManager;
|
|
347
|
+
/** design/166-T1:治理窗账本店(USAGE_WINDOWS 配置时在场,与 config.usageWindows 成对)——提交面
|
|
348
|
+
* pre-admission 读窗用(主力 /v1/runs 是 202 异步,core 进场拒发生在 202 之后永远变不成 429;
|
|
349
|
+
* 这里是异步车道唯一的 429 出路)。enforcement 真源在 core(RunnerDeps 同店),此门是快速座。 */
|
|
350
|
+
usageWindowStore?: import("@sema-agent/core").UsageWindowStore;
|
|
347
351
|
}
|
|
348
352
|
/** **部署自述面**:只被 /health、/v1/capabilities 与提交前置门读的部署事实(多为 live getter,
|
|
349
353
|
* 不是快照——/health 必须回答「此刻」)。 */
|
package/dist/http/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import { once } from "node:events";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, HAND_TOOL_EFFECTS, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow } from "@sema-agent/core";
|
|
4
|
+
import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, HAND_TOOL_EFFECTS, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs } from "@sema-agent/core";
|
|
5
5
|
import { decideParkedAgent, findParkedAgentForCheckpoint } from "../parked-decide.js";
|
|
6
6
|
import { matchCatalogModel } from "../model-select.js";
|
|
7
7
|
import {} from "../config-center/facade.js";
|
|
@@ -10,7 +10,7 @@ import { verifyDirectDoorProof } from "../principal-jwt.js";
|
|
|
10
10
|
import {} from "../memory-sync.js";
|
|
11
11
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../task-settings.js";
|
|
12
12
|
import { parseHooksConfig } from "../hooks/hook-runner.js";
|
|
13
|
-
import { validateTaskAgents } from "../spec-fields.js";
|
|
13
|
+
import { normalizeApproachNotice, validateTaskAgents } from "../spec-fields.js";
|
|
14
14
|
import { isValidCwd, MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
|
|
15
15
|
import { runInBackground, evictIfConflict, stripCheckpointToken, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
|
|
16
16
|
import { looksLikeJwt } from "@sema-agent/registry-core/api/auth-bridge";
|
|
@@ -297,7 +297,7 @@ export function createHttpServer(rawDeps) {
|
|
|
297
297
|
const routeCtxBase = {
|
|
298
298
|
deps,
|
|
299
299
|
registry: { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters },
|
|
300
|
-
helpers: { readJson, readRawBody, rateLimited, quotaExceeded, leaseDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
|
|
300
|
+
helpers: { readJson, readRawBody, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
|
|
301
301
|
local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal() },
|
|
302
302
|
// A9 尾批:提交/续跑「腿」只装引用(实现仍在本闭包里,逐字未动)——tasks/runs 共用 prepareSpec,
|
|
303
303
|
// approvals/assistant/notify-wake 共用 resume 家族;走这一格才不会造出 routes/* 互相值 import 的环。
|
|
@@ -947,23 +947,40 @@ export function createHttpServer(rawDeps) {
|
|
|
947
947
|
if (body.limits !== undefined) {
|
|
948
948
|
const l = body.limits;
|
|
949
949
|
if (typeof l !== "object" || l === null || Array.isArray(l)) {
|
|
950
|
-
sendError(res, 400, "request.field_invalid", "limits must be an object {
|
|
950
|
+
sendError(res, 400, "request.field_invalid", "limits must be an object { maxWalltimeMs?, maxOutputTokens?, maxTurns? } (positive integers)");
|
|
951
951
|
return null;
|
|
952
952
|
}
|
|
953
|
-
|
|
953
|
+
// core 5.8.0 时限重构:旧键响亮拒并点名替代(core 同姿势 config.limit_unknown_key;「勿自行兼容」是
|
|
954
|
+
// core 的公开指令)。timeoutSec(秒)→ maxWalltimeMs(毫秒,注意单位);deadline 族三 opt-out 随
|
|
955
|
+
// 机制整族退役(nudge/call-cap/graceful-finalize 已从引擎移除),无替代键。fresh fail-loud;resume
|
|
956
|
+
// 重放不过此门,normalizeLimits 对旧键 defensive DROP(存量 suspended 体降级为默认配速,方向安全)。
|
|
957
|
+
if (l.timeoutSec !== undefined) {
|
|
958
|
+
sendError(res, 400, "request.field_invalid", "limits.timeoutSec was retired in core 5.8.0 — use limits.maxWalltimeMs (milliseconds; opt-in wall clock)");
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
// 166-T3(靶4b):deadlineNudge 的替代=approachNotice(core RETIRED 表同源);另两键随机制退役无替代。
|
|
962
|
+
if (l.deadlineNudge !== undefined) {
|
|
963
|
+
sendError(res, 400, "request.field_invalid", "limits.deadlineNudge was retired in core 5.8.0 — use limits.approachNotice (default on at 80%/95%; false turns it off, {at: [r1, r2]} tunes thresholds)");
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
for (const k of ["callCapByDeadline", "gracefulFinalize"]) {
|
|
967
|
+
if (l[k] !== undefined) {
|
|
968
|
+
sendError(res, 400, "request.field_invalid", `limits.${k} was retired in core 5.8.0 (the deadline-pacing mechanism family is gone; there is no replacement key — remove it)`);
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
for (const k of ["maxWalltimeMs", "maxOutputTokens", "maxTurns"]) {
|
|
954
973
|
const v = l[k];
|
|
955
974
|
if (v !== undefined && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
|
|
956
|
-
sendError(res, 400, "request.field_invalid", `limits.${k} must be a positive integer`);
|
|
975
|
+
sendError(res, 400, "request.field_invalid", `limits.${k} must be a positive integer${k === "maxWalltimeMs" ? " (milliseconds)" : ""}`);
|
|
957
976
|
return null;
|
|
958
977
|
}
|
|
959
978
|
}
|
|
960
|
-
//
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if (v !== undefined && v !== false) {
|
|
966
|
-
sendError(res, 400, "request.field_invalid", `limits.${k} accepts only literal false (it is an opt-out; omit to keep the default-on behavior)`);
|
|
979
|
+
// 166-T3:approachNotice fresh-submit fail-loud(判据单源 normalizeApproachNotice,阈值规则对齐 core a<=b)。
|
|
980
|
+
{
|
|
981
|
+
const v = l.approachNotice;
|
|
982
|
+
if (v !== undefined && normalizeApproachNotice(v) === undefined) {
|
|
983
|
+
sendError(res, 400, "request.field_invalid", "limits.approachNotice accepts false (turn off) or {at?: [r1, r2]} with fractions in (0, 1], r1 <= r2 (default on at 80%/95%)");
|
|
967
984
|
return null;
|
|
968
985
|
}
|
|
969
986
|
}
|
|
@@ -2200,10 +2217,11 @@ export function createHttpServer(rawDeps) {
|
|
|
2200
2217
|
deps.metrics?.inc("task_tokens_total", {}, result.stats.tokens);
|
|
2201
2218
|
if (result.stats.cacheHitRate !== undefined)
|
|
2202
2219
|
deps.metrics?.observe("task_cache_hit_rate", result.stats.cacheHitRate);
|
|
2203
|
-
// ⑤ budget/limit cutoffs
|
|
2204
|
-
//
|
|
2220
|
+
// ⑤ budget/limit cutoffs:count failures by their dotted code — core 5.8.0 起统一 `limits.` 前缀
|
|
2221
|
+
// (limits.max_{tokens,cost,turns,walltime}_exceeded;旧 budget.*/limit.* 双前缀同拍退役),
|
|
2222
|
+
// operator 按此对租户撞顶告警。metric 名保留 budget_exceeded_total(时序连续性;code label 自述新码)。
|
|
2205
2223
|
const code = result.errorCode;
|
|
2206
|
-
if (code &&
|
|
2224
|
+
if (code && code.startsWith("limits.")) {
|
|
2207
2225
|
deps.metrics?.inc("budget_exceeded_total", { code });
|
|
2208
2226
|
}
|
|
2209
2227
|
// Developer-mode verification gate (1.44): count the final verdict when this task was verified.
|
|
@@ -2266,6 +2284,35 @@ export function createHttpServer(rawDeps) {
|
|
|
2266
2284
|
sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", { retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
|
|
2267
2285
|
return true;
|
|
2268
2286
|
}
|
|
2287
|
+
/** design/166-T1 — 治理窗 pre-admission(quotaExceeded/leaseDenied 的兄弟门):key 公式与 core 单源
|
|
2288
|
+
* (`principal || GLOBAL_USAGE_KEY`,GLOBAL_USAGE_KEY 引 core 公共导出防漂移;principal=verified,
|
|
2289
|
+
* spoofable header 不进键)。窗耗尽 → 429 + retryAfterSec(族形一致,秒上取整)+ Retry-After 头。
|
|
2290
|
+
* 读窗失败(DB 瞬断/坏 record)→ warn + fail-open:enforcement 真源在 core 进场检查(同店同键,
|
|
2291
|
+
* 会响亮拒),此门只是异步车道的 429 快速座——门故障不该把提交面整个拖死。admission-pass 与
|
|
2292
|
+
* core entry-拒之间存在竞态窗:该形落终态 run 记录(errorCode usage.window_exhausted),契约文档在案。 */
|
|
2293
|
+
async function usageWindowDenied(req, res) {
|
|
2294
|
+
const store = deps.usageWindowStore;
|
|
2295
|
+
const windows = deps.config.usageWindows;
|
|
2296
|
+
if (!store || !windows || windows.length === 0)
|
|
2297
|
+
return false;
|
|
2298
|
+
const key = gatedPrincipal(req, deps.config) || GLOBAL_USAGE_KEY;
|
|
2299
|
+
let retryAfterMs;
|
|
2300
|
+
try {
|
|
2301
|
+
retryAfterMs = usageRetryAfterMs(await store.read(key, windows, Date.now()));
|
|
2302
|
+
}
|
|
2303
|
+
catch (e) {
|
|
2304
|
+
deps.logger?.warn?.("usage_window_admission_read_failed", { error: e instanceof Error ? e.message : String(e) });
|
|
2305
|
+
return false;
|
|
2306
|
+
}
|
|
2307
|
+
if (retryAfterMs === undefined)
|
|
2308
|
+
return false;
|
|
2309
|
+
deps.metrics?.inc("usage_window_rejected_total");
|
|
2310
|
+
deps.logger?.info?.("usage_window_exhausted", { hasPrincipal: key !== GLOBAL_USAGE_KEY, retryAfterMs });
|
|
2311
|
+
const retryAfterSec = Math.max(1, Math.ceil(retryAfterMs / 1000));
|
|
2312
|
+
res.setHeader("retry-after", String(retryAfterSec));
|
|
2313
|
+
sendError(res, 429, "usage.window_exhausted", "usage window exhausted (deployment token governance)", { retryAfterSec });
|
|
2314
|
+
return true;
|
|
2315
|
+
}
|
|
2269
2316
|
/** Apply the per-principal rate limit; responds 429 and returns true when the caller is over. */
|
|
2270
2317
|
function rateLimited(req, res) {
|
|
2271
2318
|
if (!deps.rateLimiter)
|
|
@@ -153,19 +153,18 @@ export interface TaskRequestBody {
|
|
|
153
153
|
};
|
|
154
154
|
/** design/132 (core 1.249): one-shot end-game verification nudge — opt-in by the AUTONOMY caller. */
|
|
155
155
|
finalVerification?: boolean;
|
|
156
|
-
/** [854]④ per-request
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
|
|
161
|
-
* 缺省不设=不封顶);timeoutSec 缺席时保持既有 tenancy 墙钟姿势(resolveTaskLimits,spec-fields.ts)。 */
|
|
156
|
+
/** [854]④ per-request 配速——core 5.8.0 时限重构后的键集(#124):`maxWalltimeMs`(毫秒,opt-in 墙钟)/
|
|
157
|
+
* `maxOutputTokens`/`maxTurns`,均正整数;可选运营方上限旋钮各自封顶(TASK_TIMEOUT_MAX_SEC 保**秒**义,
|
|
158
|
+
* 对毫秒键封顶 ×1000)。旧键 `timeoutSec` 与 deadline 族三 opt-out(机制退役)fresh-submit 400 响亮
|
|
159
|
+
* 点名替代;`maxWalltimeMs` 缺席时保持既有 tenancy 墙钟姿势(resolveTaskLimits,spec-fields.ts)。 */
|
|
160
|
+
/** 166-T3:approachNotice——core 默认 ON(80%/95% 各提示一次);false 关,{at:[r1,r2]} 调阈((0,1] 且 r1<=r2)。 */
|
|
162
161
|
limits?: {
|
|
163
|
-
|
|
162
|
+
maxWalltimeMs?: number;
|
|
164
163
|
maxOutputTokens?: number;
|
|
165
164
|
maxTurns?: number;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
approachNotice?: false | {
|
|
166
|
+
at?: [number, number];
|
|
167
|
+
};
|
|
169
168
|
};
|
|
170
169
|
/** design/133 (core 1.251) + G1 (core 1.253): turn-boundary attachment reminders (todo / changed-files /
|
|
171
170
|
* plan-mode) + one-shot boundary notices (post-compaction background-task recap / deferred-tools delta).
|
package/dist/leader/fanout.d.ts
CHANGED
|
@@ -38,8 +38,8 @@ export interface WorkerReport {
|
|
|
38
38
|
* it up to its bound but it didn't fully complete — e.g. the overall deadline hit or the slice cap). Present
|
|
39
39
|
* ⇒ resumable: the operator/leader can escalate (more budget) or accept the belt-diff partial progress. */
|
|
40
40
|
checkpointGate?: TaskResult["checkpointGate"];
|
|
41
|
-
/** design/74 Slice 6: the worker's terminal `TaskResult.errorCode` (e.g. `
|
|
42
|
-
* resume, `
|
|
41
|
+
/** design/74 Slice 6: the worker's terminal `TaskResult.errorCode` (e.g. `limits.max_cost_exceeded` on an
|
|
42
|
+
* exhausted resume, `limits.max_turns_exceeded` — core 5.8.0 码名轴) — surfaced for /v1/leader observability. */
|
|
43
43
|
errorCode?: TaskResult["errorCode"];
|
|
44
44
|
/** LEADER-REPAIRLOOP-INTEGRATION §10.6 (THE push chokepoint): the `runRepairLoop` terminal a single-agent
|
|
45
45
|
* worker resolved to, when the repair loop ran on this worker (LEADER_REPAIR_LOOP on). It MUST ride on the
|
package/dist/leader/leader.js
CHANGED
|
@@ -108,7 +108,7 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
|
|
|
108
108
|
// Map the TaskResult status directly (like fanOut's own mapStatus) — a `suspended` result that the
|
|
109
109
|
// auto-resume left unresumed (a non-resource gate, e.g. human-approval) stays `suspended` so the
|
|
110
110
|
// leader's C4 surface/cancel reaction still fires; only completed/timeout pass through, else failed.
|
|
111
|
-
status: r.status === "completed" ? "completed" : r.status === "suspended" ? "suspended" :
|
|
111
|
+
status: r.status === "completed" ? "completed" : r.status === "suspended" ? "suspended" : "failed", // core 5.8.0:TaskResult 无 "timeout"(内部词表的 timeout 仍由 fanout 墙钟臂生产)
|
|
112
112
|
stats: r.stats,
|
|
113
113
|
...(r.status === "completed" ? {} : { error: r.errorMessage ?? r.blockedReason ?? `worker ${r.status}` }),
|
|
114
114
|
...(r.checkpointGate ? { checkpointGate: r.checkpointGate } : {}),
|
package/dist/leader/wire.js
CHANGED
|
@@ -134,7 +134,8 @@ export function leaderResourceConfig(env = process.env) {
|
|
|
134
134
|
leaderTimeoutMs,
|
|
135
135
|
workerMaxTurns,
|
|
136
136
|
workerBudgetUsd,
|
|
137
|
-
|
|
137
|
+
// core 5.8.0:预算四键全入 limits(顶层 maxCostUsd 删);walltime 原生毫秒不再除
|
|
138
|
+
workerLimits: { limits: { maxTurns: workerMaxTurns, maxWalltimeMs: leaderTimeoutMs, maxCostUsd: workerBudgetUsd } },
|
|
138
139
|
presignTtlSec: Math.ceil(leaderTimeoutMs / 1000) + 600, // URL/sandbox must outlive the worker (BUG1/BUG2)
|
|
139
140
|
...(resourceSuspendOn
|
|
140
141
|
? {
|
|
@@ -223,8 +224,7 @@ export function createLeaderRunner(cfg) {
|
|
|
223
224
|
.runTaskStream({
|
|
224
225
|
objective,
|
|
225
226
|
sessionId: `leader-repair-${Date.now()}-${round}`,
|
|
226
|
-
maxCostUsd: repairBudgetUsd,
|
|
227
|
-
limits: { timeoutSec: Math.max(300, Math.floor(leaderTimeoutMs / 1000 / 2)) },
|
|
227
|
+
limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
|
|
228
228
|
})
|
|
229
229
|
.result();
|
|
230
230
|
// A non-completed repair (timeout/abort) may leave a process alive in the SAME Kata pod that would race
|
|
@@ -281,8 +281,7 @@ export function createLeaderRunner(cfg) {
|
|
|
281
281
|
.runTaskStream({
|
|
282
282
|
objective,
|
|
283
283
|
sessionId: `leader-conflict-${Date.now()}-${round}`,
|
|
284
|
-
maxCostUsd: repairBudgetUsd,
|
|
285
|
-
limits: { timeoutSec: Math.max(300, Math.floor(leaderTimeoutMs / 1000 / 2)) },
|
|
284
|
+
limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
|
|
286
285
|
})
|
|
287
286
|
.result();
|
|
288
287
|
if (res.status !== "completed")
|
|
@@ -422,7 +421,10 @@ export function createLeaderRunner(cfg) {
|
|
|
422
421
|
resourceSpec = {
|
|
423
422
|
checkpointStore: cfg.durable.checkpointStore,
|
|
424
423
|
resourceSuspend: { scope: `_leader-resource-${sub.workerId}`, totalBudgetUsd: resourceCfg.totalBudgetUsd },
|
|
425
|
-
maxCostUsd:
|
|
424
|
+
// core 5.8.0:maxCostUsd 入 limits 后,本 spread(spec: {...workerLimits, ..., ...resourceSpec})的
|
|
425
|
+
// limits 键是**整体覆盖**——必须显式并回 workerLimits.limits(maxTurns/maxWalltimeMs),否则
|
|
426
|
+
// per-slice 覆盖会顺手把 worker 的轮数/墙钟界丢掉(5.7 时代顶层键覆盖顶层键,无此陷阱)。
|
|
427
|
+
limits: { ...workerLimits.limits, maxCostUsd: resourceCfg.sliceMaxCostUsd },
|
|
426
428
|
maxSuspends: resourceCfg.maxSuspends,
|
|
427
429
|
};
|
|
428
430
|
}
|
package/dist/main.js
CHANGED
|
@@ -158,7 +158,7 @@ async function main() {
|
|
|
158
158
|
await configCenter.applyLocalRemoteExec();
|
|
159
159
|
// design/158 A10:持久层装配搬到 src/boot/stores.ts(逐字)。⚠️ 该段就地归一 `config.sessionBackend`
|
|
160
160
|
// 且承载三条 fail-loud 拒启断言 —— 位置即契约,理由见该文件头注。
|
|
161
|
-
const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, } = await openStores({ config, logger, metrics, localRoot });
|
|
161
|
+
const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, usageWindowStore, } = await openStores({ config, logger, metrics, localRoot });
|
|
162
162
|
// design/158 A10:计费/追踪/预算装配搬到 src/boot/budget-tracing.ts(逐字;tracer 与 side-query 同 sink 实例的
|
|
163
163
|
// 「同段构造」契约见该文件头注)。
|
|
164
164
|
const { brain, pricing, counterDegradeHook, costQuota, modelUsageTracker, promptManifestTracker, fleetUsage, fleetLease, tracer, sideQueryAccounting, toolResultStore, sessionPolicyStore, fileSnapshotStore, } = createBudgetAndTracing({ config, logger, metrics, backend, breakerState });
|
|
@@ -185,7 +185,7 @@ async function main() {
|
|
|
185
185
|
const { principalCaps, centerRuntimeCapsResolver, runtimeCapsResolver } = createRuntimeCaps({ config, logger });
|
|
186
186
|
// design/158 A10:RunnerDeps 装配段搬到 src/boot/runner-deps.ts(逐字;runStore 晚绑改取值,见该文件头注)。
|
|
187
187
|
const runnerDeps = createRunnerDeps({
|
|
188
|
-
config, logger, metrics, localRoot, promptSource: configCenter.promptSource, rosterStore, backgroundAgentStore, mailboxStore, brain,
|
|
188
|
+
config, logger, metrics, localRoot, promptSource: configCenter.promptSource, rosterStore, backgroundAgentStore, mailboxStore, usageWindowStore, brain,
|
|
189
189
|
pricing, tracer, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine,
|
|
190
190
|
memorySyncRunner, toolResultStore, sessionPolicyStore, runtimeCapsResolver, fileSnapshotStore,
|
|
191
191
|
executionEnvFactory, lspManager, fleetBus, deploymentHooks, workflowRunStore, workflowJournalStore,
|
|
@@ -228,8 +228,8 @@ async function main() {
|
|
|
228
228
|
model: pick.catalogRef,
|
|
229
229
|
handsReadOnly: true,
|
|
230
230
|
enableFork: false,
|
|
231
|
-
maxCostUsd
|
|
232
|
-
limits: { maxTurns: 10,
|
|
231
|
+
// core 5.8.0:预算四键全入 limits(顶层 maxCostUsd 删);timeoutSec(秒)→ maxWalltimeMs(毫秒,原生同单位)
|
|
232
|
+
limits: { maxTurns: 10, maxCostUsd: 0.05, maxWalltimeMs: Math.max(30_000, timeoutMs) },
|
|
233
233
|
// spec 无 toolPolicy → core `hasEffectAwareGate` 为假 → 每次 agent hook 触发一条
|
|
234
234
|
// error 级 UNGATED 警告(非致命但纯噪声)。挂已裁决的 auto-accept 基线(与 singleUserAutoAcceptBaseline
|
|
235
235
|
// 同构;hook 子代理本就 handsReadOnly + 单用户信任面,auto-accept 是既定姿态,只是让闸机制在场)。
|
|
@@ -329,6 +329,7 @@ async function main() {
|
|
|
329
329
|
deploymentHooks,
|
|
330
330
|
toolResultStore,
|
|
331
331
|
sessionPolicyStore,
|
|
332
|
+
usageWindowStore,
|
|
332
333
|
}),
|
|
333
334
|
// ── 以下为 subRunner 差异键(不在共享基座;逐个有因)──────────────────────────────────────
|
|
334
335
|
sessionStore: subRunnerSessions, // 子代转录=私有短 TTL fork 路由店,生命周期异于宿主 durable 店
|
|
@@ -800,6 +801,7 @@ async function main() {
|
|
|
800
801
|
rateLimiter,
|
|
801
802
|
costQuota,
|
|
802
803
|
fleetLease: fleetLease ? fleetLease : undefined, // lease admission 门(提交面,镜像 quotaExceeded)
|
|
804
|
+
usageWindowStore: usageWindowStore ? usageWindowStore : undefined, // design/166-T1:治理窗 pre-admission(与 RunnerDeps 同店同键)
|
|
803
805
|
};
|
|
804
806
|
// B1 活体 store 探测环(门与判据见 store-live-probe.ts 头注;探针原语=既有 S10 dbNowMs 真 DB 往返)
|
|
805
807
|
const storeLiveProbe = backend && backend.kind !== "local" && backend.dbNowMs && config.storeProbeIntervalMs > 0
|
|
@@ -534,27 +534,8 @@ export class RemoteHostExecutionEnv {
|
|
|
534
534
|
env: this.mergeEnv(options?.env),
|
|
535
535
|
...spawnGroupOptions(),
|
|
536
536
|
});
|
|
537
|
-
// core
|
|
538
|
-
//
|
|
539
|
-
if (options?.onSpawn && child.pid != null) {
|
|
540
|
-
const spawnedPid = child.pid;
|
|
541
|
-
try {
|
|
542
|
-
options.onSpawn({ pid: spawnedPid, kill: () => {
|
|
543
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
544
|
-
return;
|
|
545
|
-
try {
|
|
546
|
-
killTreeHard(spawnedPid);
|
|
547
|
-
}
|
|
548
|
-
catch {
|
|
549
|
-
try {
|
|
550
|
-
child.kill("SIGKILL");
|
|
551
|
-
}
|
|
552
|
-
catch { /* noop */ }
|
|
553
|
-
}
|
|
554
|
-
} });
|
|
555
|
-
}
|
|
556
|
-
catch { /* registry 回调失败不挡 exec(core 同姿势吞) */ }
|
|
557
|
-
}
|
|
537
|
+
// core 5.8.0:cut-kill registry seam(onSpawn/onDetachAdopted)随 tool-cut 机制退役(core 零生产者,
|
|
538
|
+
// 本臂为死码删除;1.321 时代的 still-running 守卫语义随之谢幕)。
|
|
558
539
|
const finish = (r) => {
|
|
559
540
|
if (settled)
|
|
560
541
|
return;
|
|
@@ -629,11 +610,6 @@ export class RemoteHostExecutionEnv {
|
|
|
629
610
|
clearTimeout(forceSettleTimer);
|
|
630
611
|
options?.abortSignal?.removeEventListener("abort", onAbort);
|
|
631
612
|
settled = true;
|
|
632
|
-
// cut-kill registry 注销(所有权已移交 bgManager——cut 不该再杀后台 shell;core 参考实现同位)。
|
|
633
|
-
try {
|
|
634
|
-
options?.onDetachAdopted?.();
|
|
635
|
-
}
|
|
636
|
-
catch { /* noop */ }
|
|
637
613
|
resolve(ok({ stdout: seedOut, stderr: seedErr, exitCode: 0, detached: { shellId } }));
|
|
638
614
|
};
|
|
639
615
|
// Named (not inline) so onDetach can `off` them when it hands the pipes to the adopted background shell.
|
|
@@ -874,26 +850,7 @@ export class RemoteHostExecutionEnv {
|
|
|
874
850
|
throw e;
|
|
875
851
|
}
|
|
876
852
|
closeSpoolFds(); // 父进程的 fd 副本立即关(文件 size/EOF 只反映子进程写;launch FILE 形同姿势)
|
|
877
|
-
// core
|
|
878
|
-
if (options?.onSpawn && child.pid != null) {
|
|
879
|
-
const spawnedPid = child.pid;
|
|
880
|
-
try {
|
|
881
|
-
options.onSpawn({ pid: spawnedPid, kill: () => {
|
|
882
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
883
|
-
return;
|
|
884
|
-
try {
|
|
885
|
-
killTreeHard(spawnedPid);
|
|
886
|
-
}
|
|
887
|
-
catch {
|
|
888
|
-
try {
|
|
889
|
-
child.kill("SIGKILL");
|
|
890
|
-
}
|
|
891
|
-
catch { /* noop */ }
|
|
892
|
-
}
|
|
893
|
-
} });
|
|
894
|
-
}
|
|
895
|
-
catch { /* registry 回调失败不挡 exec */ }
|
|
896
|
-
}
|
|
853
|
+
// core 5.8.0:cut-kill registry seam 退役(pipe 径同注)。
|
|
897
854
|
// 七轮复审:'error' 监听必须在任何可提前 return 的路径(下方 F5 already-aborted 查)之前装——异步
|
|
898
855
|
// spawn 失败(如 cwd 不存在)以 ChildProcess 'error' 事件发出,无监听=uncaught 直接打死 worker 进程
|
|
899
856
|
// (plain-node 最小形实证 crash;vitest/tsx 宿主会吸收,故该面无法用测试钉住,靠此排序保证)。
|
|
@@ -971,11 +928,6 @@ export class RemoteHostExecutionEnv {
|
|
|
971
928
|
if (shellId === undefined)
|
|
972
929
|
return; // 拒收(limit/无 pid)→ 继续 fg,泵照跑,close 正常 settle
|
|
973
930
|
handedOver = true; // spool 文件归 bg(dispose 面按 FILE 形清理)
|
|
974
|
-
// cut-kill registry 注销(所有权已移交 bgManager;pipe 径同位)。
|
|
975
|
-
try {
|
|
976
|
-
options?.onDetachAdopted?.();
|
|
977
|
-
}
|
|
978
|
-
catch { /* noop */ }
|
|
979
931
|
const o = outBuf.result();
|
|
980
932
|
const e = errBuf.result();
|
|
981
933
|
finish(ok({ stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes), exitCode: 0, detached: { shellId } }));
|
|
@@ -129,6 +129,11 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
129
129
|
private readonly cfg;
|
|
130
130
|
private readonly deps;
|
|
131
131
|
constructor(config: K8sEnvConfig, deps?: K8sEnvDeps);
|
|
132
|
+
/** 166-T2 — ExecutionEnv 寿命声明:pod `activeDeadlineSeconds = ceil(timeoutMs/1000)` 是平台真硬死线
|
|
133
|
+
* (k8s 到点杀 pod,无 keepalive 续期机制)⇒ 恒声明,core 在死线−60s 处 suspend(基建合格)或响亮
|
|
134
|
+
* 终局(env.lifetime_expired)。`lifetimeStartedAt` 有意不声明:core 只在 prepare 期读一次,lazy-connect
|
|
135
|
+
* 下 pod 尚未创建 ⇒ core 回落铸造时刻为锚——比 pod 真启动早,提早 suspend 是保守方向。 */
|
|
136
|
+
get lifetimeMs(): number;
|
|
132
137
|
private resolve;
|
|
133
138
|
workspaceHandle(): WorkspaceHandle;
|
|
134
139
|
/** Resolved lazily so an in-cluster pod picks up its service account without explicit config. */
|
|
@@ -159,6 +159,13 @@ export class RemoteK8sExecutionEnv {
|
|
|
159
159
|
supportsDetach: true,
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
|
+
/** 166-T2 — ExecutionEnv 寿命声明:pod `activeDeadlineSeconds = ceil(timeoutMs/1000)` 是平台真硬死线
|
|
163
|
+
* (k8s 到点杀 pod,无 keepalive 续期机制)⇒ 恒声明,core 在死线−60s 处 suspend(基建合格)或响亮
|
|
164
|
+
* 终局(env.lifetime_expired)。`lifetimeStartedAt` 有意不声明:core 只在 prepare 期读一次,lazy-connect
|
|
165
|
+
* 下 pod 尚未创建 ⇒ core 回落铸造时刻为锚——比 pod 真启动早,提早 suspend 是保守方向。 */
|
|
166
|
+
get lifetimeMs() {
|
|
167
|
+
return this.cfg.timeoutMs;
|
|
168
|
+
}
|
|
162
169
|
resolve(p) {
|
|
163
170
|
return path.posix.isAbsolute(p) ? path.posix.normalize(p) : path.posix.normalize(path.posix.join(this.cwd, p));
|
|
164
171
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UsageWindowStore SQL twins(design/166-T1)——core 5.8.0 部署级 token 治理窗
|
|
3
|
+
* (`RunnerDeps.usageWindows` + `usageWindowStore`)的 TiDB/PG 双方言实现。
|
|
4
|
+
*
|
|
5
|
+
* 语义单源 = core 纯函数(`chargeUsageRecord`/`readUsageRecord`,dist/core/usage-window-store.js):
|
|
6
|
+
* 本层只负责 record 的持久化与多副本并发正确性;窗算法(rolling slots 按最宽 horizon 自剪、
|
|
7
|
+
* first-use bucket lapsed 即弃)完全由 core 纯函数执行——SQL 层零窗语义。
|
|
8
|
+
*
|
|
9
|
+
* 并发正确性(本 store 的靶心;两副本同 key 并发 charge 绝不能丢账——丢账方向=治理窗漏放):
|
|
10
|
+
* - **ensure-first + 行锁**(mailbox twin 正字先例,design/166 复审靶1):事务内先幂等
|
|
11
|
+
* INSERT IGNORE / ON CONFLICT DO NOTHING 建空行 ⇒ SELECT ... FOR UPDATE 时行恒在,
|
|
12
|
+
* 「无行 FOR UPDATE + INSERT」的并发死锁(InnoDB gap-lock)/dup 撞车形整个消掉。
|
|
13
|
+
* - charge 频率低(core 只在任务进场/turn 边界/收尾 delta 提交),悲观锁简单正确,不用乐观 CAS。
|
|
14
|
+
* - `read` 无锁单 SELECT(readUsageRecord 纯函数);行缺席 = EMPTY_USAGE_WINDOW_RECORD。
|
|
15
|
+
* - record JSON 坏形(手工污染)= **fail-loud throw**:治理窗算错=计费面撒谎,不 fail-open。
|
|
16
|
+
*
|
|
17
|
+
* 列型:record 用 MySQL 方言 LONGTEXT(rolling slots 高并发长窗可破 64K TEXT——复审新5,
|
|
18
|
+
* background-agent LONGTEXT 同案);PG TEXT 不限长。key 列 VARCHAR(190)(principal 或
|
|
19
|
+
* GLOBAL_USAGE_KEY,均为受控标识,无字节等价诉求——不是用户可写身份键)。
|
|
20
|
+
*
|
|
21
|
+
* residue(design/166 04 §复审处置在档):不再被 charge 的 key 行永存(单行成本可忽略);
|
|
22
|
+
* GLOBAL 单行=并发 charge 串一把行锁的吞吐热点(出现瓶颈再分片)。
|
|
23
|
+
*/
|
|
24
|
+
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
25
|
+
import type { Pool as PgPool } from "pg";
|
|
26
|
+
import type { UsageWindow, UsageWindowReading, UsageWindowStore } from "@sema-agent/core";
|
|
27
|
+
export declare const USAGE_WINDOW_TABLE = "usage_window";
|
|
28
|
+
export declare function ensureTiDBUsageWindowSchema(pool: MySqlPool): Promise<void>;
|
|
29
|
+
export declare function ensurePgUsageWindowSchema(q: (text: string, params?: unknown[]) => Promise<unknown>): Promise<void>;
|
|
30
|
+
export declare class TiDBUsageWindowStore implements UsageWindowStore {
|
|
31
|
+
private readonly pool;
|
|
32
|
+
constructor(pool: MySqlPool);
|
|
33
|
+
charge(key: string, tokens: number, at: number, windows: readonly UsageWindow[]): Promise<void>;
|
|
34
|
+
read(key: string, windows: readonly UsageWindow[], now: number): Promise<readonly UsageWindowReading[]>;
|
|
35
|
+
}
|
|
36
|
+
export declare class PgUsageWindowStore implements UsageWindowStore {
|
|
37
|
+
private readonly pool;
|
|
38
|
+
constructor(pool: PgPool);
|
|
39
|
+
charge(key: string, tokens: number, at: number, windows: readonly UsageWindow[]): Promise<void>;
|
|
40
|
+
read(key: string, windows: readonly UsageWindow[], now: number): Promise<readonly UsageWindowReading[]>;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=usage-window-store-sql.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord } from "@sema-agent/core";
|
|
2
|
+
export const USAGE_WINDOW_TABLE = "usage_window";
|
|
3
|
+
/** record 列 → UsageWindowRecord。坏形 fail-loud(治理窗算错=计费面撒谎;与快照/凭据类 store 的
|
|
4
|
+
* onCorruptRead 披露座不同族——这里没有「跳过一行继续」的安全语义,账本坏=停)。 */
|
|
5
|
+
function parseRecord(raw, key) {
|
|
6
|
+
let v;
|
|
7
|
+
try {
|
|
8
|
+
v = JSON.parse(String(raw));
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
throw new Error(`usage_window record for key "${key}" is not valid JSON (${String(e)}) — refusing to mis-count a governance window`);
|
|
12
|
+
}
|
|
13
|
+
const r = v;
|
|
14
|
+
if (typeof v !== "object" || v === null || !Array.isArray(r.slots) || !Array.isArray(r.buckets)) {
|
|
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
|
+
}
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
export async function ensureTiDBUsageWindowSchema(pool) {
|
|
20
|
+
await pool.query(`CREATE TABLE IF NOT EXISTS ${USAGE_WINDOW_TABLE} (
|
|
21
|
+
k VARCHAR(190) NOT NULL,
|
|
22
|
+
record LONGTEXT NOT NULL,
|
|
23
|
+
updated_at_ms BIGINT NOT NULL,
|
|
24
|
+
PRIMARY KEY (k)
|
|
25
|
+
) COLLATE utf8mb4_bin`);
|
|
26
|
+
}
|
|
27
|
+
export async function ensurePgUsageWindowSchema(q) {
|
|
28
|
+
await q(`CREATE TABLE IF NOT EXISTS ${USAGE_WINDOW_TABLE} (
|
|
29
|
+
k VARCHAR(190) NOT NULL,
|
|
30
|
+
record TEXT NOT NULL,
|
|
31
|
+
updated_at_ms BIGINT NOT NULL,
|
|
32
|
+
PRIMARY KEY (k)
|
|
33
|
+
)`);
|
|
34
|
+
}
|
|
35
|
+
export class TiDBUsageWindowStore {
|
|
36
|
+
pool;
|
|
37
|
+
constructor(pool) {
|
|
38
|
+
this.pool = pool;
|
|
39
|
+
}
|
|
40
|
+
async charge(key, tokens, at, windows) {
|
|
41
|
+
const c = await this.pool.getConnection();
|
|
42
|
+
try {
|
|
43
|
+
await c.beginTransaction();
|
|
44
|
+
// ensure-first:幂等建空行(EMPTY record)⇒ 下一步 FOR UPDATE 行恒在(锁语义干净,无 gap-lock 死锁形)。
|
|
45
|
+
await c.query(`INSERT IGNORE INTO ${USAGE_WINDOW_TABLE} (k, record, updated_at_ms) VALUES (?, ?, ?)`, [
|
|
46
|
+
key,
|
|
47
|
+
JSON.stringify(EMPTY_USAGE_WINDOW_RECORD),
|
|
48
|
+
at,
|
|
49
|
+
]);
|
|
50
|
+
const [rows] = (await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE k = ? FOR UPDATE`, [key]));
|
|
51
|
+
if (rows.length === 0) {
|
|
52
|
+
// 本表无删除臂(不同于 mailbox 的 drop/reap),ensure 后空 = 真障碍 fail-loud。
|
|
53
|
+
throw new Error(`usage_window row for key "${key}" vanished between ensure and lock — refusing to drop a governance charge`);
|
|
54
|
+
}
|
|
55
|
+
const record = parseRecord(rows[0].record, key);
|
|
56
|
+
const next = chargeUsageRecord(record, tokens, at, windows);
|
|
57
|
+
await c.query(`UPDATE ${USAGE_WINDOW_TABLE} SET record = ?, updated_at_ms = ? WHERE k = ?`, [JSON.stringify(next), at, key]);
|
|
58
|
+
await c.commit();
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
await c.rollback().catch(() => { });
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
c.release();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async read(key, windows, now) {
|
|
69
|
+
const [rows] = (await this.pool.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE k = ?`, [key]));
|
|
70
|
+
const record = rows.length === 0 ? EMPTY_USAGE_WINDOW_RECORD : parseRecord(rows[0].record, key);
|
|
71
|
+
return readUsageRecord(record, windows, now);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export class PgUsageWindowStore {
|
|
75
|
+
pool;
|
|
76
|
+
constructor(pool) {
|
|
77
|
+
this.pool = pool;
|
|
78
|
+
}
|
|
79
|
+
async charge(key, tokens, at, windows) {
|
|
80
|
+
const c = await this.pool.connect();
|
|
81
|
+
try {
|
|
82
|
+
await c.query("BEGIN");
|
|
83
|
+
// ensure-first(TiDB 同注):ON CONFLICT DO NOTHING 幂等建空行,再锁——插入不带锁,锁只从
|
|
84
|
+
// FOR UPDATE 来(复审靶1 的 PG 交错:并发首充双双 ensure,后到方 DO NOTHING 后必须真锁行)。
|
|
85
|
+
await c.query(`INSERT INTO ${USAGE_WINDOW_TABLE} (k, record, updated_at_ms) VALUES ($1, $2, $3) ON CONFLICT (k) DO NOTHING`, [
|
|
86
|
+
key,
|
|
87
|
+
JSON.stringify(EMPTY_USAGE_WINDOW_RECORD),
|
|
88
|
+
at,
|
|
89
|
+
]);
|
|
90
|
+
const { rows } = await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE k = $1 FOR UPDATE`, [key]);
|
|
91
|
+
if (rows.length === 0) {
|
|
92
|
+
throw new Error(`usage_window row for key "${key}" vanished between ensure and lock — refusing to drop a governance charge`);
|
|
93
|
+
}
|
|
94
|
+
const record = parseRecord(rows[0].record, key);
|
|
95
|
+
const next = chargeUsageRecord(record, tokens, at, windows);
|
|
96
|
+
await c.query(`UPDATE ${USAGE_WINDOW_TABLE} SET record = $1, updated_at_ms = $2 WHERE k = $3`, [JSON.stringify(next), at, key]);
|
|
97
|
+
await c.query("COMMIT");
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
await c.query("ROLLBACK").catch(() => { });
|
|
101
|
+
throw e;
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
c.release();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async read(key, windows, now) {
|
|
108
|
+
const { rows } = await this.pool.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE k = $1`, [key]);
|
|
109
|
+
const record = rows.length === 0 ? EMPTY_USAGE_WINDOW_RECORD : parseRecord(rows[0].record, key);
|
|
110
|
+
return readUsageRecord(record, windows, now);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=usage-window-store-sql.js.map
|
package/dist/run-local.js
CHANGED
|
@@ -469,7 +469,8 @@ export async function runLocal(argv, deps = {}) {
|
|
|
469
469
|
// [849]→[2400] 场景层定死终验已无生产者(autonomous 退役);OR 折入形保留,与 resolveSpec 同语义。
|
|
470
470
|
...(cap.finalVerification === true ? { finalVerification: true } : {}),
|
|
471
471
|
...(mcp ? { mcp } : {}),
|
|
472
|
-
|
|
472
|
+
// core 5.8.0:timeoutSec 键退役 → maxWalltimeMs(毫秒);taskWallClockSec 仍产秒,此处换算一次。
|
|
473
|
+
...(timeoutSec !== undefined ? { limits: { maxWalltimeMs: timeoutSec * 1000 } } : {}),
|
|
473
474
|
};
|
|
474
475
|
logger.info("run_local_start", { scenario: scenarioName, model: spec.model, sessionId, exec: config.remoteExec?.provider ?? "in-process" });
|
|
475
476
|
// ── Run ONE task to completion (the sync /v1/tasks path: plain runTask, no verify/cascade). ──
|
package/dist/spec-fields.d.ts
CHANGED
|
@@ -87,7 +87,8 @@ export declare function toolNameListFromBody(raw: unknown): string[] | undefined
|
|
|
87
87
|
export declare function promptProfileFromBody(raw: unknown): "simple" | "classic" | undefined;
|
|
88
88
|
/** [854]④ — 每键的运营方上限旋钮(env,缺省不设=不封顶)。 */
|
|
89
89
|
export interface TaskLimitCaps {
|
|
90
|
-
/** TASK_TIMEOUT_MAX_SEC
|
|
90
|
+
/** TASK_TIMEOUT_MAX_SEC(env 名不动,仍按**秒**配置;core 5.8.0 起封顶对象是 limits.maxWalltimeMs,
|
|
91
|
+
* 封顶比较时 ×1000 换算——运维面零迁移)。 */ timeoutSec?: number;
|
|
91
92
|
/** TASK_MAX_OUTPUT_TOKENS_MAX */ maxOutputTokens?: number;
|
|
92
93
|
/** TASK_MAX_TURNS_MAX */ maxTurns?: number;
|
|
93
94
|
}
|
|
@@ -98,12 +99,17 @@ export interface TaskLimitCaps {
|
|
|
98
99
|
* 运营方上限旋钮 clamp(Math.min;旋钮缺省不设=不封顶)。全空 ⇒ undefined。
|
|
99
100
|
*/
|
|
100
101
|
export declare function normalizeLimits(v: unknown, caps: TaskLimitCaps): {
|
|
101
|
-
|
|
102
|
+
maxWalltimeMs?: number;
|
|
102
103
|
maxOutputTokens?: number;
|
|
103
104
|
maxTurns?: number;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
approachNotice?: false | {
|
|
106
|
+
at?: [number, number];
|
|
107
|
+
};
|
|
108
|
+
} | undefined;
|
|
109
|
+
/** 166-T3 — approachNotice 单键归一(normalizeLimits/HTTP 门共用判据源):合法 ⇒ 归一值,坏形 ⇒ undefined。
|
|
110
|
+
* 阈值规则对齐 core(prepare-task at 校验):二元组、(0,1] 有限数、a<=b(允许相等)。 */
|
|
111
|
+
export declare function normalizeApproachNotice(raw: unknown): false | {
|
|
112
|
+
at?: [number, number];
|
|
107
113
|
} | undefined;
|
|
108
114
|
/**
|
|
109
115
|
* [854]④ — body.limits 与 env 墙钟的合成规则(resolveSpec 的唯一 limits 装配点;run-local 无 body 车道不经此):
|
package/dist/spec-fields.js
CHANGED
|
@@ -359,6 +359,9 @@ export function promptProfileFromBody(raw) {
|
|
|
359
359
|
export function normalizeLimits(v, caps) {
|
|
360
360
|
if (v == null || typeof v !== "object" || Array.isArray(v))
|
|
361
361
|
return undefined;
|
|
362
|
+
// core 5.8.0:键集换 maxWalltimeMs(毫秒)。旧键(timeoutSec/deadline 族三 opt-out)fresh-submit 已在
|
|
363
|
+
// HTTP 门响亮 400 点名替代;本层只走 resume 重放的持久化 body——旧键 defensive DROP(存量 suspended
|
|
364
|
+
// 体降级为默认配速,env 墙钟兜底仍在,方向安全;绝不静默换算=那是「自行兼容」,core 明令禁止)。
|
|
362
365
|
const o = v;
|
|
363
366
|
const pick = (raw, cap) => {
|
|
364
367
|
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 1)
|
|
@@ -367,17 +370,32 @@ export function normalizeLimits(v, caps) {
|
|
|
367
370
|
return cap !== undefined ? Math.min(n, cap) : n;
|
|
368
371
|
};
|
|
369
372
|
const out = {
|
|
370
|
-
|
|
373
|
+
// caps.timeoutSec 是秒(env TASK_TIMEOUT_MAX_SEC 保名保义),封顶毫秒键时 ×1000。
|
|
374
|
+
...(() => { const n = pick(o.maxWalltimeMs, caps.timeoutSec !== undefined ? caps.timeoutSec * 1000 : undefined); return n !== undefined ? { maxWalltimeMs: n } : {}; })(),
|
|
371
375
|
...(() => { const n = pick(o.maxOutputTokens, caps.maxOutputTokens); return n !== undefined ? { maxOutputTokens: n } : {}; })(),
|
|
372
376
|
...(() => { const n = pick(o.maxTurns, caps.maxTurns); return n !== undefined ? { maxTurns: n } : {}; })(),
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
...(o.
|
|
376
|
-
...(o.callCapByDeadline === false ? { callCapByDeadline: false } : {}),
|
|
377
|
-
...(o.gracefulFinalize === false ? { gracefulFinalize: false } : {}),
|
|
377
|
+
// 166-T3:approachNotice(core 默认 ON;false=唯一关闭形,{at:[a,b]} 调阈,core 允 a<=b)。坏形整键
|
|
378
|
+
// DROP(降级为 core 默认阈,方向安全)——不 throw,resume 重放通道同门。
|
|
379
|
+
...(() => { const n = normalizeApproachNotice(o.approachNotice); return n !== undefined ? { approachNotice: n } : {}; })(),
|
|
378
380
|
};
|
|
379
381
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
380
382
|
}
|
|
383
|
+
/** 166-T3 — approachNotice 单键归一(normalizeLimits/HTTP 门共用判据源):合法 ⇒ 归一值,坏形 ⇒ undefined。
|
|
384
|
+
* 阈值规则对齐 core(prepare-task at 校验):二元组、(0,1] 有限数、a<=b(允许相等)。 */
|
|
385
|
+
export function normalizeApproachNotice(raw) {
|
|
386
|
+
if (raw === false)
|
|
387
|
+
return false;
|
|
388
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw))
|
|
389
|
+
return undefined;
|
|
390
|
+
const at = raw.at;
|
|
391
|
+
if (at === undefined)
|
|
392
|
+
return {};
|
|
393
|
+
if (!Array.isArray(at) || at.length !== 2)
|
|
394
|
+
return undefined;
|
|
395
|
+
const [a, b] = at;
|
|
396
|
+
const frac = (n) => typeof n === "number" && Number.isFinite(n) && n > 0 && n <= 1;
|
|
397
|
+
return frac(a) && frac(b) && a <= b ? { at: [a, b] } : undefined;
|
|
398
|
+
}
|
|
381
399
|
/**
|
|
382
400
|
* [854]④ — body.limits 与 env 墙钟的合成规则(resolveSpec 的唯一 limits 装配点;run-local 无 body 车道不经此):
|
|
383
401
|
* - `timeoutSec`:body 给了就用 body(caller 明确配速;已在 normalizeLimits 里被 TASK_TIMEOUT_MAX_SEC 封顶)。
|
|
@@ -389,8 +407,11 @@ export function normalizeLimits(v, caps) {
|
|
|
389
407
|
*/
|
|
390
408
|
export function resolveTaskLimits(bodyLimits, caps, envTimeoutSec, requirePrincipal, big) {
|
|
391
409
|
const requested = normalizeLimits(bodyLimits, caps);
|
|
392
|
-
|
|
393
|
-
|
|
410
|
+
// core 5.8.0:墙钟键=maxWalltimeMs(毫秒)。taskWallClockSec 仍以秒推导(tenancy 基值+env 只抬不降,
|
|
411
|
+
// 语义原样),装配处换算一次;body 显式给了 maxWalltimeMs 则直接生效(caller 意图,可低于 env 墙)。
|
|
412
|
+
const wallSec = taskWallClockSec(envTimeoutSec, requirePrincipal, big);
|
|
413
|
+
const maxWalltimeMs = requested?.maxWalltimeMs ?? (wallSec !== undefined ? wallSec * 1000 : undefined);
|
|
414
|
+
const out = { ...(requested ?? {}), ...(maxWalltimeMs !== undefined ? { maxWalltimeMs } : {}) };
|
|
394
415
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
395
416
|
}
|
|
396
417
|
//# sourceMappingURL=spec-fields.js.map
|
package/dist/trace/project.d.ts
CHANGED
|
@@ -287,7 +287,7 @@ export declare function compactedEventData(ev: {
|
|
|
287
287
|
modelFallback?: true;
|
|
288
288
|
fallbackReason?: "window";
|
|
289
289
|
clampedRatio?: number;
|
|
290
|
-
clampReason?: "budget" | "
|
|
290
|
+
clampReason?: "budget" | "tolerance";
|
|
291
291
|
tokensAfter?: number;
|
|
292
292
|
triggerTokensBefore?: number;
|
|
293
293
|
durationMs?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.1.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.8.0",
|
|
58
58
|
"@sema-agent/registry-core": "^0.14.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"sharp": "^0.35.3"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@sema-agent/sdk": "^
|
|
71
|
+
"@sema-agent/sdk": "^6.0.0",
|
|
72
72
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
73
73
|
"@types/node": "22.10.2",
|
|
74
74
|
"@types/pg": "^8.20.0",
|