@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 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
- # ③ 分级超时:任务级 timeoutSec 之下补两段,各记为可重试的 [network] → 触发重试/断路器/failover
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:"timeout"` | 超时 | 拆小任务 / 提高超时(服务端 `limits`) |
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.`(开关没开)/ `internal.` / `state.`;未知码按前缀
425
- 兜底永远安全。完整码表见 `docs/ASSISTANT-WIRE-CONTRACT.md` §附录 A。
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 滚动续期、无固定死线,不声明(行为不变)。
@@ -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" || input.status === "timeout")
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
- return { ...(limits !== undefined ? { limits } : {}), ...(declarations.length > 0 ? { configOverrides: declarations } : {}) };
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 (1.37): honor a caller's requested ceiling but CAP it to the operator
636
- // ceiling (a request can ask for less, never more). core fails the task with errorCode budget.*
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
@@ -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
  }
@@ -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
@@ -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: { timeoutSec: 180 },
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: { timeoutSec: 400 },
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: { timeoutSec: 240 },
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: { timeoutSec: 600 },
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 });
@@ -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",
@@ -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;
@@ -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
- if (await leaseDenied(req, res))
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" || finalRow.status === "timeout")) {
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
  }
@@ -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 必须回答「此刻」)。 */
@@ -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 { timeoutSec?, maxOutputTokens?, maxTurns? } (positive integers)");
950
+ sendError(res, 400, "request.field_invalid", "limits must be an object { maxWalltimeMs?, maxOutputTokens?, maxTurns? } (positive integers)");
951
951
  return null;
952
952
  }
953
- for (const k of ["timeoutSec", "maxOutputTokens", "maxTurns"]) {
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
- // 快审 F2(1.254):deadline 族三 opt-out 在场必须 literal false——"false"/0 等畸形值此前 200 后被
961
- // normalizeLimits 静默丢=调用方以为关了其实恒开(B1 同类故障)。fresh fail-loud;resume 重放不过
962
- // 此门,normalizeLimits 保持 defensive。
963
- for (const k of ["deadlineNudge", "callCapByDeadline", "gracefulFinalize"]) {
964
- const v = body.limits[k];
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 (1.37): count failures by their dotted code (budget.precall/exceeded,
2204
- // limit.timeout/max_turns) so an operator can alert on tenants hitting the cost/token ceiling.
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 && (code.startsWith("budget.") || code.startsWith("limit."))) {
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 配速(core spec.limits 全链:timeoutSec 墙钟 + deadlineNudge/callCapByDeadline/
157
- * gracefulFinalize;maxOutputTokens = 每次模型调用的输出上限;maxTurns = 上游 parity 的回合上限)。
158
- * 三键均为正整数(0 拒绝:core timeoutSec>0 才生效,放行 0 会绕过运营方封顶)。与 `maxTokens`
159
- * (per-task token 预算,cappedCeiling 车道)不同物 —— maxOutputTokens 是单次调用配速。
160
- * 每键各自被可选 env 上限封顶(TASK_TIMEOUT_MAX_SEC / TASK_MAX_OUTPUT_TOKENS_MAX / TASK_MAX_TURNS_MAX,
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
- timeoutSec?: number;
162
+ maxWalltimeMs?: number;
164
163
  maxOutputTokens?: number;
165
164
  maxTurns?: number;
166
- deadlineNudge?: false;
167
- callCapByDeadline?: false;
168
- gracefulFinalize?: false;
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).
@@ -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. `budget.exceeded` on an exhausted
42
- * resume, `limit.max_turns`) — surfaced for /v1/leader observability. */
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
@@ -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" : r.status === "timeout" ? "timeout" : "failed",
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 } : {}),
@@ -134,7 +134,8 @@ export function leaderResourceConfig(env = process.env) {
134
134
  leaderTimeoutMs,
135
135
  workerMaxTurns,
136
136
  workerBudgetUsd,
137
- workerLimits: { limits: { maxTurns: workerMaxTurns, timeoutSec: Math.floor(leaderTimeoutMs / 1000) }, maxCostUsd: workerBudgetUsd },
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: resourceCfg.sliceMaxCostUsd,
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: 0.05,
232
- limits: { maxTurns: 10, timeoutSec: Math.max(30, Math.ceil(timeoutMs / 1000)) },
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 1.321 cut-kill registry(三轮复审存量缺口):把杀柄交给上层(Bash 工具 cut 时强制终杀);
538
- // 柄内 still-running 守卫(exitCode/signalCode)防对已亡组的 pgid 复用误杀(core 参考实现同语义)
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 1.321 cut-kill registry(三轮复审存量缺口):同 pipe 径——杀柄带 still-running 守卫。
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
- ...(timeoutSec !== undefined ? { limits: { timeoutSec } } : {}),
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). ──
@@ -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 */ timeoutSec?: number;
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
- timeoutSec?: number;
102
+ maxWalltimeMs?: number;
102
103
  maxOutputTokens?: number;
103
104
  maxTurns?: number;
104
- deadlineNudge?: false;
105
- callCapByDeadline?: false;
106
- gracefulFinalize?: false;
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 车道不经此):
@@ -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
- ...(() => { const n = pick(o.timeoutSec, caps.timeoutSec); return n !== undefined ? { timeoutSec: n } : {}; })(),
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
- // 历史复审轴B #4(1.254):deadline 族三个 **opt-out**(design/128/130,timeoutSec 在场即默认 ON;
374
- // server 恒注 tenancy 墙钟 ⇒ 此前恒开且 wire 无法关)。只认 literal false(纯关闭旋钮,零扩权面)。
375
- ...(o.deadlineNudge === false ? { deadlineNudge: false } : {}),
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
- const timeoutSec = requested?.timeoutSec ?? taskWallClockSec(envTimeoutSec, requirePrincipal, big);
393
- const out = { ...(requested ?? {}), ...(timeoutSec !== undefined ? { timeoutSec } : {}) };
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
@@ -287,7 +287,7 @@ export declare function compactedEventData(ev: {
287
287
  modelFallback?: true;
288
288
  fallbackReason?: "window";
289
289
  clampedRatio?: number;
290
- clampReason?: "budget" | "walltime" | "tolerance";
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": "5.14.0",
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.7.0",
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": "^5.2.0",
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",