@sema-agent/server 7.71.0 → 7.72.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.
Files changed (53) hide show
  1. package/USAGE.md +2 -1
  2. package/dist/approval-card.d.ts +16 -17
  3. package/dist/approval-card.js +0 -9
  4. package/dist/boot/runner-deps.d.ts +32 -16
  5. package/dist/brain.js +2 -9
  6. package/dist/budget.d.ts +12 -1
  7. package/dist/budget.js +1 -1
  8. package/dist/config-catalog.js +12 -12
  9. package/dist/config-center/apply-effective.js +2 -4
  10. package/dist/config-center/facade.d.ts +1 -1
  11. package/dist/config-center/restart-signal.js +2 -2
  12. package/dist/config-center/skills-mcp.js +15 -0
  13. package/dist/config-types.d.ts +68 -16
  14. package/dist/config-types.js +19 -1
  15. package/dist/config.d.ts +42 -0
  16. package/dist/config.js +58 -21
  17. package/dist/http/durable-run-leg.d.ts +64 -0
  18. package/dist/http/durable-run-leg.js +63 -0
  19. package/dist/http/route-ctx.d.ts +14 -0
  20. package/dist/http/route-ctx.js +3 -0
  21. package/dist/http/routes/approvals-assistant.js +3 -3
  22. package/dist/http/routes/capabilities.js +2 -2
  23. package/dist/http/routes/runs.js +10 -58
  24. package/dist/http/routes/workflows.js +1 -0
  25. package/dist/http/server.js +55 -17
  26. package/dist/main.js +4 -1
  27. package/dist/observability/fail-open.d.ts +1 -1
  28. package/dist/observability/fail-open.js +1 -1
  29. package/dist/observability/metrics.js +1 -1
  30. package/dist/observability/tool-trace.d.ts +18 -1
  31. package/dist/observability/tool-trace.js +4 -3
  32. package/dist/parked-decide.js +3 -2
  33. package/dist/plugins/e2b-orphan-reclaim.d.ts +4 -4
  34. package/dist/plugins/e2b-orphan-reclaim.js +18 -20
  35. package/dist/plugins/store-contracts.d.ts +27 -0
  36. package/dist/plugins/store-contracts.js +3 -2
  37. package/dist/run-cancel-context.js +1 -1
  38. package/dist/run-local.js +4 -1
  39. package/dist/runs.d.ts +16 -2
  40. package/dist/runs.js +2 -1
  41. package/dist/runtime-caps-resolver.js +5 -3
  42. package/dist/task-a2a.js +3 -2
  43. package/dist/task-mcp.js +3 -2
  44. package/dist/tool-approval.d.ts +26 -21
  45. package/dist/tool-approval.js +2 -2
  46. package/dist/trace/core-keyset-guard.d.ts +10 -4
  47. package/dist/trace/project.d.ts +12 -0
  48. package/dist/trace/project.js +19 -3
  49. package/dist/usage-analytics.d.ts +9 -1
  50. package/dist/usage-analytics.js +8 -0
  51. package/dist/wire-name-bound.d.ts +14 -0
  52. package/dist/wire-name-bound.js +13 -0
  53. package/package.json +2 -2
package/dist/config.js CHANGED
@@ -9,7 +9,7 @@ import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
9
9
  import { isV2ScopeKey } from "./memory-scope.js";
10
10
  import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
11
11
  import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
12
- import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, MODEL_FIRST_TOKEN_TIMEOUT_MS_DEFAULT } from "./config-types.js";
12
+ import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS } from "./config-types.js";
13
13
  import { buildWriteProtectionPosture } from "./write-protection.js";
14
14
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
15
15
  import { readFaceForPosture } from "./read-face-posture.js";
@@ -260,16 +260,53 @@ function numEnvOr(name, fallback) {
260
260
  return fallback;
261
261
  return parseNumOrFail(name, raw);
262
262
  }
263
- function cheapCostEnv(name, mainPrice) {
263
+ function costEnv(name, emptyString) {
264
264
  const raw = process.env[name];
265
265
  if (raw === undefined)
266
- return mainPrice;
266
+ return undefined;
267
267
  if (raw === "") {
268
+ if (emptyString === "unset")
269
+ return undefined;
268
270
  CONFIG_WARNINGS.push({ env: name, raw: "", used: "0", kind: "empty-preserved" });
269
271
  return 0;
270
272
  }
271
273
  return parseNumOrFail(name, raw);
272
274
  }
275
+ function buildCostOrAbsent(read, inherited) {
276
+ const declared = read.input !== undefined || read.output !== undefined || read.cacheRead !== undefined || read.cacheWrite !== undefined;
277
+ if (!declared && inherited === undefined)
278
+ return undefined;
279
+ return {
280
+ input: read.input ?? inherited?.input ?? 0,
281
+ output: read.output ?? inherited?.output ?? 0,
282
+ cacheRead: read.cacheRead ?? inherited?.cacheRead ?? 0,
283
+ cacheWrite: read.cacheWrite ?? inherited?.cacheWrite ?? 0,
284
+ };
285
+ }
286
+ export function buildModelCostKey(cost) {
287
+ return (cost === undefined ? {} : { cost });
288
+ }
289
+ export function buildUnpricedCeilingNotice(models, limits) {
290
+ const ceilings = [
291
+ ...(limits.maxTaskCostUsd > 0 ? ["MAX_TASK_COST_USD"] : []),
292
+ ...(limits.maxPrincipalCostUsd > 0 ? ["MAX_PRINCIPAL_COST_USD"] : []),
293
+ ];
294
+ const entries = Object.values(models);
295
+ if (ceilings.length === 0 || entries.length === 0 || entries.some((m) => m.cost !== undefined))
296
+ return undefined;
297
+ return {
298
+ event: "cost_ceiling_without_pricing",
299
+ fields: {
300
+ ceilings: ceilings.join(","),
301
+ models: [...new Set(entries.map((m) => m.id))].join(","),
302
+ note: `${ceilings.join(" / ")} is configured but NO model in the effective catalog declares a price table — an ` +
303
+ "unpriced model reports NO cost at all (core RB-368), so these $ ceilings can never fire and this deployment " +
304
+ "has no cost guardrail despite looking like it does. Set MODEL_COST_INPUT / MODEL_COST_OUTPUT / " +
305
+ "MODEL_COST_CACHE_READ / MODEL_COST_CACHE_WRITE (env lane), or a per-model `cost` on the center roster row. " +
306
+ "The token ceiling (MAX_TASK_TOKENS) is unaffected and still fires.",
307
+ },
308
+ };
309
+ }
273
310
  function optNumEnvStrict(name) {
274
311
  const raw = process.env[name];
275
312
  if (!raw)
@@ -753,12 +790,12 @@ function parseModelDomain() {
753
790
  baseUrl: "",
754
791
  reasoning: boolEnv("MODEL_REASONING", true),
755
792
  input: boolEnv("MODEL_VISION", true) ? ["text", "image"] : ["text"],
756
- cost: {
757
- input: numEnv("MODEL_COST_INPUT", "0"),
758
- output: numEnv("MODEL_COST_OUTPUT", "0"),
759
- cacheRead: numEnv("MODEL_COST_CACHE_READ", "0"),
760
- cacheWrite: numEnv("MODEL_COST_CACHE_WRITE", "0"),
761
- },
793
+ ...buildModelCostKey(buildCostOrAbsent({
794
+ input: costEnv("MODEL_COST_INPUT", "unset"),
795
+ output: costEnv("MODEL_COST_OUTPUT", "unset"),
796
+ cacheRead: costEnv("MODEL_COST_CACHE_READ", "unset"),
797
+ cacheWrite: costEnv("MODEL_COST_CACHE_WRITE", "unset"),
798
+ }, undefined)),
762
799
  contextWindow: numEnv("MODEL_CONTEXT_WINDOW", "262144"),
763
800
  maxTokens: process.env.MODEL_MAX_TOKENS !== undefined && process.env.MODEL_MAX_TOKENS !== ""
764
801
  ? parseNumOrFail("MODEL_MAX_TOKENS", process.env.MODEL_MAX_TOKENS)
@@ -776,12 +813,12 @@ function parseModelDomain() {
776
813
  id: cheapId,
777
814
  name: cheapId,
778
815
  input: ((v) => (v === undefined ? model.input : v ? ["text", "image"] : ["text"]))(parseBoolWord("MODEL_CHEAP_VISION", process.env.MODEL_CHEAP_VISION)),
779
- cost: {
780
- input: cheapCostEnv("MODEL_CHEAP_COST_INPUT", model.cost.input),
781
- output: cheapCostEnv("MODEL_CHEAP_COST_OUTPUT", model.cost.output),
782
- cacheRead: cheapCostEnv("MODEL_CHEAP_COST_CACHE_READ", model.cost.cacheRead),
783
- cacheWrite: cheapCostEnv("MODEL_CHEAP_COST_CACHE_WRITE", model.cost.cacheWrite),
784
- },
816
+ ...buildModelCostKey(buildCostOrAbsent({
817
+ input: costEnv("MODEL_CHEAP_COST_INPUT", "declares-zero"),
818
+ output: costEnv("MODEL_CHEAP_COST_OUTPUT", "declares-zero"),
819
+ cacheRead: costEnv("MODEL_CHEAP_COST_CACHE_READ", "declares-zero"),
820
+ cacheWrite: costEnv("MODEL_CHEAP_COST_CACHE_WRITE", "declares-zero"),
821
+ }, model.cost)),
785
822
  contextWindow: numEnvOr("MODEL_CHEAP_CONTEXT_WINDOW", model.contextWindow),
786
823
  maxTokens: numEnvOr("MODEL_CHEAP_MAX_TOKENS", model.maxTokens),
787
824
  ...(optFinitePositiveEnv("MODEL_CHEAP_CHARS_PER_TOKEN") !== undefined ? { charsPerToken: optFinitePositiveEnv("MODEL_CHEAP_CHARS_PER_TOKEN") } : {}),
@@ -855,12 +892,12 @@ function parseModelDomain() {
855
892
  }
856
893
  : undefined,
857
894
  resilience: {
858
- connectTimeoutMs: numEnv("MODEL_CONNECT_TIMEOUT_MS", "30000"),
859
- firstTokenTimeoutMs: numEnv("MODEL_FIRST_TOKEN_TIMEOUT_MS", String(MODEL_FIRST_TOKEN_TIMEOUT_MS_DEFAULT)),
860
- idleTimeoutMs: numEnv("MODEL_IDLE_TIMEOUT_MS", "300000"),
861
- circuitBreaker: boolEnv("MODEL_CIRCUIT_BREAKER", false),
862
- failureThreshold: numEnv("MODEL_CB_FAILURE_THRESHOLD", "5"),
863
- cooldownMs: numEnv("MODEL_CB_COOLDOWN_MS", "30000"),
895
+ connectTimeoutMs: numEnv("MODEL_CONNECT_TIMEOUT_MS", String(RESILIENCE_DEFAULTS.connectTimeoutMs.value)),
896
+ firstTokenTimeoutMs: numEnv("MODEL_FIRST_TOKEN_TIMEOUT_MS", String(RESILIENCE_DEFAULTS.firstTokenTimeoutMs.value)),
897
+ idleTimeoutMs: numEnv("MODEL_IDLE_TIMEOUT_MS", String(RESILIENCE_DEFAULTS.idleTimeoutMs.value)),
898
+ circuitBreaker: boolEnv("MODEL_CIRCUIT_BREAKER", RESILIENCE_DEFAULTS.circuitBreaker.value),
899
+ failureThreshold: numEnv("MODEL_CB_FAILURE_THRESHOLD", String(RESILIENCE_DEFAULTS.failureThreshold.value)),
900
+ cooldownMs: numEnv("MODEL_CB_COOLDOWN_MS", String(RESILIENCE_DEFAULTS.cooldownMs.value)),
864
901
  },
865
902
  model,
866
903
  envModel: model,
@@ -0,0 +1,64 @@
1
+ /**
2
+ * **一条 durable run 的「建行 + 驱动」单一属主**(S-218 提取;此前整段住在 `routes/runs.ts` 的
3
+ * `createDurableRun` 里,只服务 HTTP/A2A 两条提交门)。
4
+ *
5
+ * 🔴 为什么提成模块:`/decide` 的 workflow 第三条车道在**宿主不在 park 上**时,同样要在宿主会话上起
6
+ * 一跑新 turn 把决定交给宿主的模型(S-218)。这条链上挂着 `createRun` 的会话 claim(以及它唯一诚实的
7
+ * 409 出路)、checkpoint ctx 落库、fleet 行发布、resume/rewind 两族锚点捕获、审批 ALS、通告投递……
8
+ * 手抄第二份 = 把这十来件事复制成两份会漂的实现,而漂开的那天没有任何东西会说话(`createDurableRun`
9
+ * 当年从路由体里提取出来时,头注写的就是同一条判词)。方向:`routes/runs.ts` 与 `http/server.ts`
10
+ * 都 import 本模块,本模块谁都不 import —— 与 `http/run-meta.ts` 同一条「两个域共用一件 ⇒ 单独成文件」。
11
+ *
12
+ * 边界(本腿**不**含的东西,留给各自的门):`body.taskId` 幂等重放、`Idempotency-Key` 内存去重、
13
+ * lease / 治理窗 pre-admission、属主门。它们是**提交面**的判据(读请求、写响应),而本腿只吃已经备好的
14
+ * `PreparedTaskSubmission` 并交回一份收据 —— 这正是它能被一条没有 `ServerResponse` 的内部车道复用的原因。
15
+ */
16
+ import { type RunInternals } from "@sema-agent/core";
17
+ import type { RunStore } from "../plugins/store-backend.js";
18
+ import type { PreparedTaskSubmission, RouteCtxBase } from "./route-ctx.js";
19
+ /**
20
+ * 🔴 **TRUSTED run internals** —— 由**部署方**(不是模型、不是请求体、不是工具参数)铸给这一跑的
21
+ * core 内部席位。今天唯一成员 `workflowParkedResume` = 人对一条 parked workflow 子代的决定
22
+ * (语义属主 = core `RunInternals.workflowParkedResume` 的 d.ts 顶注:「A host that decided a workflow
23
+ * child's parked checkpoint launches the run that re-invokes `Workflow({resumeFromRunId})` with the
24
+ * decision here」)。
25
+ *
26
+ * 为什么单独命名而不是直接摊 `RunInternals`:`RunInternals` 里绝大多数席位是**执行腿自己**铸的
27
+ * (onForwardEvent / onTaskNotification / onActivity …),把整只类型开给调用方等于邀请第二个写者;
28
+ * 本类型只开「部署方可以决定的那一格」,新增一格时**必须**在这里显式点名,编译期看得见。
29
+ * 缺席 ⇒ 对两条腿都是完整 no-op。
30
+ */
31
+ export type TrustedRunInternals = Pick<RunInternals, "workflowParkedResume">;
32
+ export interface DurableRunLegArgs {
33
+ /** `prepareSpec` 的产物,或内部车道按同一条 `resolveSpec` 装配出来的等价形。 */
34
+ prepared: PreparedTaskSubmission;
35
+ /** 调用方自带的 taskId(`POST /v1/runs` 的 `body.taskId` 幂等键);缺席 ⇒ 本腿铸 uuidv7。 */
36
+ clientTaskId?: string;
37
+ /** 🔒 这条 run 的 **fleet 行 scope** —— 必须与 `GET /v1/fleet/stream` 过滤用的是同一个身份,且绝不是
38
+ * 自报的 `x-agent-principal` 头(自报头会把行打到受害者的 scope 上,或打到 `default` 而真属主的流把它
39
+ * 过滤掉)。提交门传 `gatedPrincipal(req) ?? "default"`;`/decide` 的第三条车道传 resume 家族的
40
+ * `resumeFleetScope(req, auth)`(同一条 wire 上两条腿因此同答)。 */
41
+ fleetScope: string;
42
+ /** 🔒 这条 run 的 **elicit / question 应答方**(E23:与 respond 门读的是同一个已验身份,不是自报头)。
43
+ * 提交门传 `gatedPrincipal(req) ?? null` = 提交者本人;`/decide` 的第三条车道传**卡属主**(= 原提交者,
44
+ * 不是代为按下按钮的 operator —— 与 resume 腿那条成文口径逐字同源)。`null` = 匿名/开放部署。 */
45
+ elicitOwner: string | null;
46
+ /** run 行元数据的 `source` 列(调用方的系统身份;内部车道传自己的车道名)。 */
47
+ source: string | null;
48
+ /** 见 {@link TrustedRunInternals}。缺席 ⇒ 与本位存在之前逐字相同。 */
49
+ trustedInternals?: TrustedRunInternals;
50
+ }
51
+ /**
52
+ * 建行 + 驱动。返回 `202` = 新行已建并在跑(体带 `taskId`/`sessionId`);`409` = 会话被别的 run 占着,
53
+ * **没有**建行(体 = `buildActiveRunConflict` 的既有形,含 `errorCode: "conflict.session_active_run"`)。
54
+ * 协议渲染归调用方(HTTP `sendJson` / A2A 协议对象 / `/decide` 的受理体)。
55
+ */
56
+ export declare function mintAndDriveDurableRun(ctx: RouteCtxBase, runStore: RunStore, args: DurableRunLegArgs): Promise<{
57
+ status: number;
58
+ body: {
59
+ taskId: string;
60
+ sessionId: string;
61
+ status: string;
62
+ } | object;
63
+ }>;
64
+ //# sourceMappingURL=durable-run-leg.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { uuidv7 } from "@sema-agent/core";
2
+ import { runInBackground } from "../runs.js";
3
+ import { fleetRunPublisher, fleetRunLabels } from "../fleet/fleet-bus.js";
4
+ import { cascadeConfig, runMeta } from "./run-meta.js";
5
+ import { resolveStreamApprovalGate } from "../tool-approval.js";
6
+ import { buildActiveRunConflict } from "./active-run-conflict.js";
7
+ import { clearTurnActivity, readTurnActivityMs } from "../turn-activity.js";
8
+ import { recordResourceWindow } from "../resource-window.js";
9
+ export async function mintAndDriveDurableRun(ctx, runStore, args) {
10
+ const { deps } = ctx;
11
+ const { inflightRuns, preemptableRuns, steerableRuns } = ctx.registry;
12
+ const sessionId = args.prepared.spec.sessionId ?? uuidv7();
13
+ const taskId = args.clientTaskId ?? uuidv7();
14
+ const created = await runStore.createRun(taskId, sessionId, args.prepared.auth?.principal ?? null, deps.instanceId ?? "default", runMeta(args.prepared, args.source));
15
+ if (created.ok)
16
+ clearTurnActivity(taskId);
17
+ if (created.ok)
18
+ recordResourceWindow(taskId, args.prepared.spec.resourceSuspend);
19
+ if (created.ok)
20
+ deps.sessionTitler?.maybeTitle(sessionId, args.prepared.spec.objective, typeof args.prepared.spec.model === "string" ? args.prepared.spec.model : args.prepared.spec.model?.id);
21
+ if (!created.ok) {
22
+ if (args.clientTaskId && created.activeTaskId === args.clientTaskId) {
23
+ return { status: 202, body: { taskId: args.clientTaskId, sessionId, status: "running" } };
24
+ }
25
+ return { status: 409, body: await buildActiveRunConflict({ runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, sessionId, created.activeTaskId) };
26
+ }
27
+ if (deps.checkpointStore) {
28
+ await deps.checkpointStore.putCtx(sessionId, { body: args.prepared.body, memoryScope: args.prepared.auth?.memoryScope });
29
+ }
30
+ const anchorOwner = args.prepared.auth?.principal ?? null;
31
+ const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
32
+ const captureTurnAnchor = deps.resumeAnchorStore && getLeafId
33
+ ? async (eventId) => {
34
+ const leaf = await getLeafId(sessionId);
35
+ if (leaf)
36
+ await deps.resumeAnchorStore.put(sessionId, eventId, leaf, anchorOwner);
37
+ }
38
+ : undefined;
39
+ const captureUserMessageAnchor = deps.resumeAnchorStore
40
+ ? async (entryId) => { await deps.resumeAnchorStore.put(sessionId, taskId, entryId, anchorOwner); }
41
+ : undefined;
42
+ const fleetPub = fleetRunPublisher(deps.fleetBus, {
43
+ runId: taskId,
44
+ scope: args.fleetScope,
45
+ rootTaskId: taskId,
46
+ ...fleetRunLabels(args.prepared.spec.objective),
47
+ });
48
+ void runInBackground(args.prepared.runner, { ...args.prepared.spec, sessionId }, runStore, taskId, deps.metrics, args.prepared.auth?.principal, args.prepared.verify, args.prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, args.prepared.spec.limits?.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation, args.elicitOwner, captureTurnAnchor, fleetPub, captureUserMessageAnchor, deps.question, deps.workflowCompletionInbox, deps.subagentSteerRegistry, (m, x) => deps.logger?.info?.(m, x), deps.sendUserFile, deps.promptManifests, deps.toolApproval
49
+ ? {
50
+ coordinator: deps.toolApproval,
51
+ streamApprovalOn: resolveStreamApprovalGate({
52
+ toolApprovalEnabled: true,
53
+ streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
54
+ backend: deps.backend,
55
+ parkFacility: deps.checkpointStore !== undefined,
56
+ }).active,
57
+ windowMs: deps.config.streamApproval?.windowMs,
58
+ windowMarginMs: deps.config.streamAskWindowMarginMs,
59
+ }
60
+ : undefined, deps.checkpointStore, args.prepared.engineNotices, args.trustedInternals);
61
+ return { status: 202, body: { taskId, sessionId, status: "running" } };
62
+ }
63
+ //# sourceMappingURL=durable-run-leg.js.map
@@ -438,6 +438,20 @@ export interface RouteMiss {
438
438
  * 两臂逐字节同形,指路价值不丢。钉=audit-fixes-http「[ref]」格(两臂全文比对);文案变更走
439
439
  * api-error-text-freeze 基线流程。
440
440
  */
441
+ /**
442
+ * **「决定/续跑已受理」那一枚 200 收据的单一铸点**(S-218)。
443
+ *
444
+ * 两条腿投同一份体:①resume 家族的受理点(`driveResumeLeg`,宿主停在 park 上时);②`/decide` 的
445
+ * workflow 第三条车道在宿主**不在** park 上时新铸的那一跑。契约上这两条腿说的是同一句话 ——
446
+ * 「决定已被受理,拿 `taskId` 去 poll `GET /v1/runs/:id` / `/events` 跟终态」——所以消费端**一条分支
447
+ * 都不该加**;写成两份字面量,漂开的那天没有任何东西会说话。
448
+ *
449
+ * `bindingEnforced` 的语义不因铸点而变(「这次决定按绑定坐标受理,不是盲目放行」),故两腿同带。
450
+ */
451
+ export declare function acceptedResumingReceipt(taskId: string, sessionId: string): {
452
+ status: number;
453
+ body: object;
454
+ };
441
455
  export declare const RUN_NOT_FOUND_MESSAGE = "run not found \u2014 the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
442
456
  /** [ref] 前缀=域判别子既成契约:a\* 与 wa\* 形 id 给 TaskOutput/journal 指路;其余给 {@link RUN_NOT_FOUND_MESSAGE}。 */
443
457
  export declare function runNotFoundMessage(id: string): string;
@@ -17,6 +17,9 @@ export function sendResumeOutcome(res, out, logger, extraBody) {
17
17
  res.setHeader("retry-after", String(Math.ceil(wait)));
18
18
  sendJson(res, out.status, extraBody === undefined ? out.body : { ...out.body, ...extraBody });
19
19
  }
20
+ export function acceptedResumingReceipt(taskId, sessionId) {
21
+ return { status: 200, body: { taskId, sessionId, status: "resuming", bindingEnforced: true } };
22
+ }
20
23
  export const RUN_NOT_FOUND_MESSAGE = "run not found — the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
21
24
  export function runNotFoundMessage(id) {
22
25
  const aStar = /^(?:a|wa)[0-9a-f]{4,}$/i.test(id);
@@ -1,4 +1,4 @@
1
- import { screenedGate } from "../../trace/project.js";
1
+ import { screenedStoredGate } from "../../trace/project.js";
2
2
  import { personDecision } from "../../host-decision.js";
3
3
  import { sameExecutionRecord } from "@sema-agent/core";
4
4
  import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
@@ -87,7 +87,7 @@ async function decideIdempotentReplay(cs, args) {
87
87
  return undefined;
88
88
  }
89
89
  }
90
- return screenedGate(first);
90
+ return screenedStoredGate(first);
91
91
  })();
92
92
  return {
93
93
  status: 200,
@@ -200,7 +200,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
200
200
  ...(s.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
201
201
  ...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
202
202
  ...(s.origin !== undefined ? { origin: s.origin } : {}),
203
- ...(s.classifierUnavailable !== undefined ? { classifierUnavailable: s.classifierUnavailable } : {}),
203
+ ...(s.ruleStoreUnreadable !== undefined ? { ruleStoreUnreadable: s.ruleStoreUnreadable } : {}),
204
204
  objective: ctx?.body?.objective ?? null,
205
205
  input,
206
206
  };
@@ -1,4 +1,4 @@
1
- import { DEFAULT_EFFORT_LEVELS, expandTiers, routePairingStatus } from "@sema-agent/core";
1
+ import { expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
2
2
  import { isModelAllowlisted } from "../../model-select.js";
3
3
  import { resolveModelApiKey } from "../../key-resolver.js";
4
4
  import { isSealedKeyPoison } from "../../sealed-key.js";
@@ -194,7 +194,7 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
194
194
  vision: Array.isArray(m.input) ? m.input.includes("image") : false,
195
195
  ...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
196
196
  ...(m.maxTokens ? { maxOutputTokens: m.maxTokens } : {}),
197
- ...(m.reasoning ? { supportedEffortLevels: [...DEFAULT_EFFORT_LEVELS] } : {}),
197
+ ...((levels) => (levels !== undefined ? { supportedEffortLevels: [...levels] } : {}))(reasoningEffortLevelsOf(m)),
198
198
  ...(m.compat !== undefined ? { compat: m.compat } : {}),
199
199
  ...(atAllowlist.length > 0 ? { atMentionable: isModelAllowlisted(name, atCatalog, atAllowlist) } : {}),
200
200
  routePairing: routePairingOf(deps, name, m),
@@ -6,13 +6,10 @@ import { isTerminalRunStatus, isParkedRunStatus } from "../../plugins/store-cont
6
6
  import { LIVE_RUN_GATE_KINDS, TERMINAL_PARK_GATE_KINDS } from "../../plugins/checkpoint-store-sql.js";
7
7
  import { redactSecrets } from "../../trace/redact.js";
8
8
  import { HttpError, verifiedPrincipal, isUuidV7, encodeCheckpointScope } from "../../security.js";
9
- import { runInBackground } from "../../runs.js";
10
9
  import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
11
10
  import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
12
- import { fleetRunPublisher, fleetRunLabels } from "../../fleet/fleet-bus.js";
13
11
  import { composeSupervisorCost, infraCost, infraUsageFromEvents, hasInfraPricing } from "../../observability/cost-taxonomy.js";
14
12
  import { recordFailOpen } from "../../observability/fail-open.js";
15
- import { cascadeConfig, runMeta } from "../run-meta.js";
16
13
  import { createHash } from "node:crypto";
17
14
  import { scopedIdempotencyKey } from "../idempotency.js";
18
15
  import { streamSseLog } from "../sse-log.js";
@@ -22,12 +19,12 @@ import { ASK_DECISION_CONSUMER_ABSENT, resolveStreamApprovalGate } from "../../t
22
19
  import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
23
20
  import { normalizeRunEventType } from "../../trace/project.js";
24
21
  import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
25
- import { buildActiveRunConflict } from "../active-run-conflict.js";
26
- import { clearTurnActivity, readTurnActivityMs } from "../../turn-activity.js";
22
+ import { readTurnActivityMs } from "../../turn-activity.js";
27
23
  import { mintCancelledResult } from "../../run-cancel-context.js";
28
- import { buildCrossSliceUsage, readResourceWindow, recordResourceWindow } from "../../resource-window.js";
24
+ import { buildCrossSliceUsage, readResourceWindow } from "../../resource-window.js";
29
25
  import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
30
26
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
27
+ import { mintAndDriveDurableRun } from "../durable-run-leg.js";
31
28
  import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
32
29
  import { withPrincipal } from "../../observability/principal-context.js";
33
30
  export const RUN_ID_RE = /^\/v1\/runs\/([^/]+)(\/events)?$/;
@@ -1559,58 +1556,13 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1559
1556
  }
1560
1557
  if ((await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
1561
1558
  return null;
1562
- const resp = await idemCache.run(idemKey, async () => {
1563
- const sessionId = prepared.spec.sessionId ?? uuidv7();
1564
- const taskId = clientTaskId ?? uuidv7();
1565
- const created = await runStore.createRun(taskId, sessionId, prepared.auth?.principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
1566
- if (created.ok)
1567
- clearTurnActivity(taskId);
1568
- if (created.ok)
1569
- recordResourceWindow(taskId, prepared.spec.resourceSuspend);
1570
- if (created.ok)
1571
- deps.sessionTitler?.maybeTitle(sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id);
1572
- if (!created.ok) {
1573
- if (clientTaskId && created.activeTaskId === clientTaskId) {
1574
- return { status: 202, body: { taskId: clientTaskId, sessionId, status: "running" } };
1575
- }
1576
- return { status: 409, body: await buildActiveRunConflict({ runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, sessionId, created.activeTaskId) };
1577
- }
1578
- if (deps.checkpointStore) {
1579
- await deps.checkpointStore.putCtx(sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
1580
- }
1581
- const anchorOwner = prepared.auth?.principal ?? null;
1582
- const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
1583
- const captureTurnAnchor = deps.resumeAnchorStore && getLeafId
1584
- ? async (eventId) => {
1585
- const leaf = await getLeafId(sessionId);
1586
- if (leaf)
1587
- await deps.resumeAnchorStore.put(sessionId, eventId, leaf, anchorOwner);
1588
- }
1589
- : undefined;
1590
- const captureUserMessageAnchor = deps.resumeAnchorStore
1591
- ? async (entryId) => { await deps.resumeAnchorStore.put(sessionId, taskId, entryId, anchorOwner); }
1592
- : undefined;
1593
- const fleetPub = fleetRunPublisher(deps.fleetBus, {
1594
- runId: taskId,
1595
- scope: gatedPrincipal(req, deps.config) ?? "default",
1596
- rootTaskId: taskId,
1597
- ...fleetRunLabels(prepared.spec.objective),
1598
- });
1599
- void runInBackground(prepared.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, gatedPrincipal(req, deps.config) ?? null, captureTurnAnchor, fleetPub, captureUserMessageAnchor, deps.question, deps.workflowCompletionInbox, deps.subagentSteerRegistry, (m, x) => deps.logger?.info?.(m, x), deps.sendUserFile, deps.promptManifests, deps.toolApproval
1600
- ? {
1601
- coordinator: deps.toolApproval,
1602
- streamApprovalOn: resolveStreamApprovalGate({
1603
- toolApprovalEnabled: true,
1604
- streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
1605
- backend: deps.backend,
1606
- parkFacility: deps.checkpointStore !== undefined,
1607
- }).active,
1608
- windowMs: deps.config.streamApproval?.windowMs,
1609
- windowMarginMs: deps.config.streamAskWindowMarginMs,
1610
- }
1611
- : undefined, deps.checkpointStore, prepared.engineNotices);
1612
- return { status: 202, body: { taskId, sessionId, status: "running" } };
1613
- }, (r) => r.status === 202);
1559
+ const resp = await idemCache.run(idemKey, async () => await mintAndDriveDurableRun({ deps, registry: ctx.registry, helpers: ctx.helpers, local: ctx.local, legs: ctx.legs }, runStore, {
1560
+ prepared,
1561
+ ...(clientTaskId !== undefined ? { clientTaskId } : {}),
1562
+ fleetScope: gatedPrincipal(req, deps.config) ?? "default",
1563
+ elicitOwner: gatedPrincipal(req, deps.config) ?? null,
1564
+ source,
1565
+ }), (r) => r.status === 202);
1614
1566
  return resp;
1615
1567
  }
1616
1568
  //# sourceMappingURL=runs.js.map
@@ -338,6 +338,7 @@ function summarizeWorkflowDetail(run) {
338
338
  durationMs: durationOf(run),
339
339
  rev: run.rev,
340
340
  ...(run.error !== undefined ? { error: redactErrorMessage(run.error) ?? "" } : {}),
341
+ ...(run.errorCode !== undefined ? { errorCode: run.errorCode } : {}),
341
342
  ...(run.result !== undefined ? { result: redactSecrets(run.result) } : {}),
342
343
  phases,
343
344
  ...(unphasedView ? { unphased: unphasedView } : {}),
@@ -43,6 +43,8 @@ import { IdempotencyCache, scopedIdempotencyKey } from "./idempotency.js";
43
43
  export { scopedIdempotencyKey };
44
44
  import { streamSseLog } from "./sse-log.js";
45
45
  import { verifyRoundsFromBody } from "./verify-rounds.js";
46
+ import { acceptedResumingReceipt } from "./route-ctx.js";
47
+ import { mintAndDriveDurableRun } from "./durable-run-leg.js";
46
48
  import { RUN_NOT_FOUND_MESSAGE } from "./route-ctx.js";
47
49
  import { handleCapabilities } from "./routes/capabilities.js";
48
50
  import { handleObservability } from "./routes/observability.js";
@@ -1218,22 +1220,14 @@ export function createHttpServer(rawDeps) {
1218
1220
  const plan = planWorkflowParkedDecide(origin, parkedReq);
1219
1221
  if (!plan.ok)
1220
1222
  return plan.deny;
1223
+ const parkedResume = { workflowParkedResume: [{ runId: plan.runId, token, outcome: plan.outcome }] };
1221
1224
  const hostToken = await cs
1222
1225
  .findPendingTokenBySession(plan.hostSessionId, undefined, { gateKinds: TERMINAL_PARK_GATE_KINDS, pick: "oldest" })
1223
1226
  .catch(() => undefined);
1224
- if (!hostToken) {
1225
- return {
1226
- status: 409,
1227
- body: {
1228
- error: "the workflow's originating session is not parked awaiting this run — the deployment can only deliver the decision on the host's next turn; retry once the host session parks, or resume the workflow run yourself",
1229
- errorCode: "decide.workflow_host_not_parked",
1230
- runId: plan.runId,
1231
- },
1232
- };
1233
- }
1234
- return await resumeWake(plan.hostSessionId, plan.message, decider?.principal, req, acceptEarly, [
1235
- { runId: plan.runId, token, outcome: plan.outcome },
1236
- ]);
1227
+ const admittedPayer = decodeCheckpointScope(cp.scope);
1228
+ return hostToken
1229
+ ? await resumeWake(plan.hostSessionId, plan.message, decider?.principal, req, acceptEarly, parkedResume.workflowParkedResume, admittedPayer)
1230
+ : await startFreshHostTurn(plan.hostSessionId, plan.message, admittedPayer, req, parkedResume, plan.runId);
1237
1231
  }
1238
1232
  }
1239
1233
  let ctx;
@@ -1286,9 +1280,11 @@ export function createHttpServer(rawDeps) {
1286
1280
  noteLateSettleIfAccepted(driven);
1287
1281
  return driven;
1288
1282
  }
1283
+ const ONE_SHOT_HISTORY_KEYS = ["resumeAt", "restoreFiles", "acceptPartialRestore", "rewindFilesTo", "rewindFiles"];
1284
+ const ONE_SHOT_HISTORY_STRIP = Object.fromEntries(ONE_SHOT_HISTORY_KEYS.map((k) => [k, undefined]));
1289
1285
  const resolveResumeSpec = async (body, req, auth) => {
1290
1286
  const engineNotices = [];
1291
- const spec = await deps.resolveSpec({ ...body, resumeAt: undefined }, req, auth, { leg: "resume", onNotice: (n) => engineNotices.push(n) });
1287
+ const spec = await deps.resolveSpec({ ...body, ...ONE_SHOT_HISTORY_STRIP }, req, auth, { leg: "resume", onNotice: (n) => engineNotices.push(n) });
1292
1288
  return { spec, engineNotices };
1293
1289
  };
1294
1290
  async function driveResumeIntoRunLog(args) {
@@ -1638,7 +1634,7 @@ export function createHttpServer(rawDeps) {
1638
1634
  steerableRuns.set(taskId, stream);
1639
1635
  if (!taskId || !rs)
1640
1636
  return await stream.result();
1641
- onAccepted?.({ status: 200, body: { taskId, sessionId, status: "resuming", bindingEnforced: true } });
1637
+ onAccepted?.(acceptedResumingReceipt(taskId, sessionId));
1642
1638
  for await (const ev of stream) {
1643
1639
  recordTurnActivity(taskId);
1644
1640
  fleetPub?.onEvent(ev);
@@ -1832,7 +1828,7 @@ export function createHttpServer(rawDeps) {
1832
1828
  (e.code === "checkpoint.unsupported_version" && checkpointRowRedeemableElsewhere(e.detail?.reason));
1833
1829
  if (claimedRow && taskId && deps.runStore) {
1834
1830
  if (retriable && freshRunClaimed) {
1835
- const c = { taskId, sessionId, terminal: { kind: "failed", code: e.code, message: e.message }, stats: { turns: 0, tokens: 0 } };
1831
+ const c = { taskId, sessionId, terminal: { kind: "failed", code: e.code, message: e.message }, stats: { turns: 0, tokens: 0, usageMissing: true } };
1836
1832
  await deps.runStore.setTerminal(taskId, "failed", c, e.message).catch((err) => {
1837
1833
  deps.logger?.warn?.("wake_synthetic_run_release_failed", { taskId, sessionId, errorCode: e.code, error: err instanceof Error ? err.message : String(err), note: "the synthetic run row for this refused fresh-run resume could not be settled — its task_active claim is still held and the next attempt on this session will 409 conflict.session_active_run until reapStale collects it (runStaleSec)" });
1838
1834
  });
@@ -1987,7 +1983,48 @@ export function createHttpServer(rawDeps) {
1987
1983
  const verifyRounds = verifyRoundsFromBody(ctx.body);
1988
1984
  return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
1989
1985
  }
1990
- async function resumeWake(sessionId, message, caller, req, acceptEarly, workflowParkedResume) {
1986
+ async function startFreshHostTurn(sessionId, objective, principal, req, trustedInternals, runId) {
1987
+ const cs = deps.checkpointStore;
1988
+ if (!deps.runStore) {
1989
+ return { status: 501, body: { error: "delivering a workflow decision on the host's next turn requires a durable run store (DB_BACKEND=mysql|pg|local)", errorCode: "capability.run_store_required" } };
1990
+ }
1991
+ const ctx = await cs.getCtx(sessionId).catch(() => null);
1992
+ if (!ctx) {
1993
+ return {
1994
+ status: 409,
1995
+ body: {
1996
+ error: "the workflow's originating session no longer has the rebuild inputs for a new turn: a session's resume context is garbage-collected once it is stale AND that session itself has no pending checkpoint and no active run (REAP_RUN_STALE_SEC, default 120s) — a workflow CHILD's park does not hold the HOST session's row open. Raise REAP_RUN_STALE_SEC to cover how long approvals may sit before someone decides them, or resume the workflow run yourself with the runId below",
1997
+ errorCode: "conflict.resume_context_unavailable",
1998
+ runId,
1999
+ },
2000
+ };
2001
+ }
2002
+ const auth = { sessionId, principal, memoryScope: ctx.memoryScope };
2003
+ let spec;
2004
+ let engineNotices = [];
2005
+ try {
2006
+ ({ spec, engineNotices } = await resolveResumeSpec(ctx.body, req, auth));
2007
+ }
2008
+ catch (e) {
2009
+ if (e instanceof HttpError) {
2010
+ return { status: 409, body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) } };
2011
+ }
2012
+ throw e;
2013
+ }
2014
+ const hostSpec = { ...spec, objective, sessionId };
2015
+ const out = await mintAndDriveDurableRun(routeCtxBase, deps.runStore, {
2016
+ prepared: { spec: hostSpec, runner: runnerFor(hostSpec), auth, body: ctx.body, ...(engineNotices.length > 0 ? { engineNotices } : {}) },
2017
+ fleetScope: resumeFleetScope(req, auth),
2018
+ elicitOwner: principal ?? null,
2019
+ source: "workflow_parked_decide",
2020
+ trustedInternals,
2021
+ });
2022
+ if (out.status !== 202)
2023
+ return { status: out.status, body: out.body };
2024
+ const minted = out.body;
2025
+ return acceptedResumingReceipt(minted.taskId, minted.sessionId);
2026
+ }
2027
+ async function resumeWake(sessionId, message, caller, req, acceptEarly, workflowParkedResume, ownerLeaseAdmittedFor) {
1991
2028
  const cs = deps.checkpointStore;
1992
2029
  const wakeBinding = { gateKinds: TERMINAL_PARK_GATE_KINDS, pick: "oldest" };
1993
2030
  const token = (await cs.findPendingTokenBySession(sessionId, undefined, wakeBinding)) ?? (await cs.findPendingTokenBySession(sessionId));
@@ -2058,6 +2095,7 @@ export function createHttpServer(rawDeps) {
2058
2095
  ...(acceptEarly === true ? { acceptEarly: true } : {}),
2059
2096
  ...(req !== undefined ? { callerInitiated: true } : {}),
2060
2097
  ...(workflowParkedResume !== undefined ? { workflowParkedResume } : {}),
2098
+ ...(ownerLeaseAdmittedFor !== undefined && auth.principal === ownerLeaseAdmittedFor ? { ownerLeaseAdmitted: true } : {}),
2061
2099
  });
2062
2100
  }
2063
2101
  async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider) {
package/dist/main.js CHANGED
@@ -7,7 +7,7 @@ import { posIntEnv } from "./session-watch.js";
7
7
  import { selectEnvironmentTool } from "./capabilities/select-environment-tool.js";
8
8
  import { subagentSendUserFileExtraTools } from "./capabilities/send-user-file-tool.js";
9
9
  import { brainSummary } from "./brain.js";
10
- import { loadConfig, logConfigDiagnostics, resolveBindHost, splitLocalRoots, parseCapEnv, parseNumOrFailNonNegative } from "./config.js";
10
+ import { buildUnpricedCeilingNotice, loadConfig, logConfigDiagnostics, resolveBindHost, splitLocalRoots, parseCapEnv, parseNumOrFailNonNegative } from "./config.js";
11
11
  import { buildWriteProtectionPosture } from "./write-protection.js";
12
12
  import { cardRuleScopeRoot } from "./task-cwd.js";
13
13
  import { isPricingConfigured } from "./budget.js";
@@ -129,6 +129,9 @@ async function main() {
129
129
  }
130
130
  const configCenter = await createConfigCenterRuntime({ config, logger, metrics, localRoot });
131
131
  await configCenter.applyLocalRemoteExec();
132
+ const unpricedCeiling = buildUnpricedCeilingNotice(config.models, config);
133
+ if (unpricedCeiling)
134
+ logger.warn(unpricedCeiling.event, unpricedCeiling.fields);
132
135
  const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, usageWindowStore, memoryPosture, taskListLane, sqlEngineFacts, } = await openStores({ config, logger, metrics, localRoot });
133
136
  const instanceId = uuidv7();
134
137
  const sessionCaptureRecords = backend?.sessionCaptureRecords();
@@ -19,7 +19,7 @@ export declare const FAIL_OPEN_TAGS: {
19
19
  };
20
20
  readonly "server.trace.gate-outcome-defective": {
21
21
  readonly cls: "F";
22
- readonly note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`,写腿与两条读腿共用):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。";
22
+ readonly note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`:写腿 `toolEndEventData` 直用;两条耐久读腿(账本回放 `toolResultFieldsOf` / `/decide` 幂等回放 `executionOutcome`)经 `screenedStoredGate` 认过退役词旧拼法后同走这一只筛):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。";
23
23
  };
24
24
  readonly "server.trace.tool-roster-malformed": {
25
25
  readonly cls: "F";
@@ -10,7 +10,7 @@ export const FAIL_OPEN_TAGS = {
10
10
  },
11
11
  "server.trace.gate-outcome-defective": {
12
12
  cls: "F",
13
- note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`,写腿与两条读腿共用):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。",
13
+ note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`:写腿 `toolEndEventData` 直用;两条耐久读腿(账本回放 `toolResultFieldsOf` / `/decide` 幂等回放 `executionOutcome`)经 `screenedStoredGate` 认过退役词旧拼法后同走这一只筛):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。",
14
14
  },
15
15
  "server.trace.tool-roster-malformed": {
16
16
  cls: "F",
@@ -263,7 +263,7 @@ export function createMetrics() {
263
263
  m.counter("stale_cache_conflict_total", "Warm session-cache entries evicted by a cross-instance write while cached (S25)");
264
264
  m.counter("breaker_writethrough_failed_total", "Circuit-breaker cross-replica write-through failures (LOW), by backend");
265
265
  m.counter("web_search_bad_payload_total", "Web-search provider responses whose results field was not an array (LOW), by provider");
266
- m.counter("auto_mode_classifier_unavailable_total", "Auto-mode gate denials caused by a classifier OUTAGE, by cause (core AUTO_MODE_UNAVAILABLE_CAUSES: error/timeout; 'other' = this build cannot name the word). Distinct from permission_denied_total{deniedBy=\"classifier\"}, which also counts genuine safety blocks — a spike here means the classifier itself is failing (gateway/credential fault, or too slow), not that the model kept asking for dangerous things");
266
+ m.counter("auto_mode_classifier_deny_cause_total", "Auto-mode gate denials where the classifier did NOT rule, by the FORM of the deny (core CLASSIFIER_DENY_CAUSES on GateDisposition.cause: 'unavailable' = the classifier could not run at all (threw/rejected/ran past the cap — retryable), 'parse_error' = it ran but answered outside its verdict contract (blocked for safety); 'other' = this build cannot name the word). 🔴 BREAKING in server 7.72.0: this counter was named auto_mode_classifier_unavailable_total through 7.71.0 and is RENAMED with NO alias (a counter that also counts parse_error is not an 'unavailable' counter); its label vocabulary was 'error'/'timeout' (WHY it was unavailable) — those two collapsed into 'unavailable' (the WHY now rides the deny sentence and the auto_mode.classified trace frame), and 'parse_error' is a bucket the old counter could not see, so by-cause series do NOT splice across the upgrade. Distinct from permission_denied_total{deniedBy=\"classifier\"}, which also counts genuine safety blocks (the classifier's own <block> ruling carries NO cause) — a spike here means the classifier itself is not working (gateway/credential fault, too slow, or answering off-contract), not that the model kept asking for dangerous things");
267
267
  m.counter("permission_denied_total", "Tool-gate denials by the LAYER whose verdict is the deny (core's DeniedBy: policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution/persisted_rule; 'other' = this build cannot name the layer; label is `deniedBy` since server 7.64.0 — it was `source` over core's retired PermissionDeniedSource; goal B4 always-on deny meter)");
268
268
  m.counter("permission_rule_events_total", buildPermissionRuleEventsHelp());
269
269
  m.counter("compaction_events_total", "Compaction lifecycle events by outcome (started/completed/failed/skipped) and trigger");