@sema-agent/server 7.48.0 → 7.49.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 (47) hide show
  1. package/dist/approval-card.d.ts +1 -1
  2. package/dist/auth-bridge.d.ts +2 -2
  3. package/dist/auth-bridge.js +1 -1
  4. package/dist/boot/config-center.js +2 -2
  5. package/dist/boot/runner-deps.d.ts +4 -2
  6. package/dist/config-center/apply-effective.js +1 -1
  7. package/dist/config-center/http-client.d.ts +1 -1
  8. package/dist/config-center/http-client.js +1 -1
  9. package/dist/config-center/restart-signal.js +1 -1
  10. package/dist/config-center/skills-mcp.js +1 -1
  11. package/dist/config-provider.d.ts +2 -2
  12. package/dist/config-provider.js +2 -2
  13. package/dist/config-types.d.ts +1 -1
  14. package/dist/config.js +1 -1
  15. package/dist/fleet-client.d.ts +1 -1
  16. package/dist/fleet-client.js +1 -1
  17. package/dist/fleet-lease.d.ts +1 -1
  18. package/dist/fleet-lease.js +1 -1
  19. package/dist/hooks/hook-runner.d.ts +1 -1
  20. package/dist/hooks/hook-runner.js +1 -1
  21. package/dist/http/routes/approvals-assistant.js +65 -0
  22. package/dist/http/routes/notify-wake.js +1 -1
  23. package/dist/http/routes/runs.d.ts +1 -1
  24. package/dist/http/routes/runs.js +1 -1
  25. package/dist/http/routes/tasks.js +3 -3
  26. package/dist/http/server.js +7 -7
  27. package/dist/orchestration/workflow-completion-inbox.d.ts +12 -1
  28. package/dist/orchestration/workflow-completion-inbox.js +6 -1
  29. package/dist/org-memory-admission.js +1 -1
  30. package/dist/plugins/checkpoint-store-sql.d.ts +108 -1
  31. package/dist/plugins/checkpoint-store-sql.js +65 -0
  32. package/dist/plugins/local-checkpoint-store.d.ts +14 -1
  33. package/dist/plugins/local-checkpoint-store.js +22 -1
  34. package/dist/plugins/scheduler-support.d.ts +1 -1
  35. package/dist/plugins/scheduler-support.js +1 -1
  36. package/dist/run-local.js +1 -1
  37. package/dist/runs.js +3 -3
  38. package/dist/runtime-caps-resolver.d.ts +1 -1
  39. package/dist/runtime-governance.d.ts +1 -1
  40. package/dist/task-settings.d.ts +1 -1
  41. package/dist/trace/engine-notice-wire.d.ts +13 -3
  42. package/dist/trace/engine-notice-wire.js +3 -0
  43. package/dist/trace/injection-tier.d.ts +33 -0
  44. package/dist/trace/injection-tier.js +7 -0
  45. package/dist/trace/project.d.ts +5 -1
  46. package/dist/trace/project.js +5 -0
  47. package/package.json +3 -3
@@ -13,7 +13,7 @@
13
13
  * 合法、语义上空」的卡。`safeParse` 让这种行**当场落地为「跳过 + 一次 warn」**(§5.3)。
14
14
  *
15
15
  * schema 属主裁定(设计稿 §12-2,属主 §14 裁 (a) 变体):v1 的 schema 属主 = server 本仓,server 加
16
- * **zod 直依赖**(此前 zod 只经 `@sema-agent/registry-core` 传递到场)。抽进 `@sema-agent/registry-core`
16
+ * **zod 直依赖**(此前 zod 只经 `@sema-agent/settings-schema` 传递到场)。抽进 `@sema-agent/settings-schema`
17
17
  * 的时机 = 出现第二个**运行期**消费者(cli 呈卡校验排期时)——届时是「属主迁移」而不是「复制形状」,
18
18
  * 不违单一属主宪法;此刻抽包 = 为不存在的消费者发一轮 registry-core 版本 + floor bump,零收益。
19
19
  *
@@ -1,7 +1,7 @@
1
- import { type AuthBridgeErrorCode, type VerifiedIdentity } from "@sema-agent/registry-core/api/auth-bridge";
1
+ import { type AuthBridgeErrorCode, type VerifiedIdentity } from "@sema-agent/settings-schema/api/auth-bridge";
2
2
  import type { Logger } from "./observability/logger.js";
3
3
  import type { Metrics } from "./observability/metrics.js";
4
- export type { VerifiedIdentity } from "@sema-agent/registry-core/api/auth-bridge";
4
+ export type { VerifiedIdentity } from "@sema-agent/settings-schema/api/auth-bridge";
5
5
  export interface RegistryJwtVerifierOptions {
6
6
  /** R4 — 期望 iss(registry 的 SSO_ISSUER;默认=SEMA_REGISTRY_URL,双侧去尾斜杠比较)。 */
7
7
  issuer: string;
@@ -1,5 +1,5 @@
1
1
  import { createPublicKey, verify as cryptoVerify } from "node:crypto";
2
- import { AUTH_BRIDGE_ALGS, DEFAULT_CLOCK_SKEW_SEC, DEFAULT_JWKS_REFRESH_MIN_INTERVAL_SEC, DEFAULT_OFFLINE_GRACE_SEC, REGISTRY_JWKS_PATH, RegistryJwks, classifyTimeClaims, jwksWithinOfflineGrace, shouldForceJwksRefresh, toVerifiedIdentity, } from "@sema-agent/registry-core/api/auth-bridge";
2
+ import { AUTH_BRIDGE_ALGS, DEFAULT_CLOCK_SKEW_SEC, DEFAULT_JWKS_REFRESH_MIN_INTERVAL_SEC, DEFAULT_OFFLINE_GRACE_SEC, REGISTRY_JWKS_PATH, RegistryJwks, classifyTimeClaims, jwksWithinOfflineGrace, shouldForceJwksRefresh, toVerifiedIdentity, } from "@sema-agent/settings-schema/api/auth-bridge";
3
3
  const DEFAULT_JWKS_TTL_SEC = 15 * 60;
4
4
  const MAX_JWKS_BYTES = 256 * 1024;
5
5
  const MAX_TOKEN_CHARS = 16 * 1024;
@@ -2,8 +2,8 @@ import { createHash } from "node:crypto";
2
2
  import { readFile as fsReadFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { loadRemoteExec } from "@sema-agent/registry-core/node";
6
- import { skillContentHash } from "@sema-agent/registry-core";
5
+ import { loadRemoteExec } from "@sema-agent/settings-schema/node";
6
+ import { skillContentHash } from "@sema-agent/settings-schema";
7
7
  import { CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, } from "@sema-agent/core";
8
8
  import { buildPricing } from "../budget.js";
9
9
  import { applyCenterPlugins } from "../capabilities/center-plugins.js";
@@ -175,8 +175,10 @@ export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pric
175
175
  * 1. **日志(全族,逐字保留)** —— 上面那段契约一个字节不变:任何 code、detail 原样、缺 detail 不铸键。
176
176
  * 运维观察面零回退是本腿的硬前提(wire 半场是**增量**,不是搬家)。
177
177
  * 2. **wire(白名单 + sessionId 路由)** —— `router.route(notice)`:码在
178
- * {@link import("../trace/engine-notice-wire.js").ENGINE_NOTICE_WIRE_CODES} 内**且** `detail.sessionId`
179
- * 在场时,投给那条会话的 run 腿(SSE + durable 账本);否则一帧不出。判据整条住在
178
+ * {@link import("../trace/engine-notice-wire.js").ENGINE_NOTICE_WIRE_CODES} 内**且**归属键在场时,
179
+ * 投给那条会话的 run 腿(SSE + durable 账本);否则一帧不出。归属键 = 顶层 `EngineNotice.sessionId`
180
+ * **优先**、缺席回落 `detail.sessionId`(单点 `engineNoticeSessionId`;此前这一行只写了 detail 那一半,
181
+ * 顶层键 core 5.46+ 就到了)。判据整条住在
180
182
  * `src/trace/engine-notice-wire.ts`(白名单/归属/消毒/隔离四件同源),这里只负责**先日志后分流**的次序。
181
183
  *
182
184
  * 🔴 次序是承重的:日志在前 ⇒ 即便 wire 半场整条坏掉(路由抛、口抛),事实仍然完整落在运维面。
@@ -1,5 +1,5 @@
1
1
  import { isThinkingLevel } from "@sema-agent/core";
2
- import { resolveActiveTiers, SEALED_BOX_ALG } from "@sema-agent/registry-core";
2
+ import { resolveActiveTiers, SEALED_BOX_ALG } from "@sema-agent/settings-schema";
3
3
  import { sealedKeyPoison } from "../sealed-key.js";
4
4
  import { applyAutoCompactWindow, findUnmatchableToolNames, formatUnmatchableToolNames, parseAutonomy } from "../config.js";
5
5
  import { validateCommandRules } from "../runtime-governance.js";
@@ -1,4 +1,4 @@
1
- import { type EntitlementRuntimeCaps } from "@sema-agent/registry-core";
1
+ import { type EntitlementRuntimeCaps } from "@sema-agent/settings-schema";
2
2
  import type { ScenarioRuling } from "../capabilities/scenarios.js";
3
3
  import type { EffectiveConfig, ExecutionRuling } from "./types.js";
4
4
  /** GET the effective config (Bearer + ETag). null = 304 (unchanged). Throws on transport/HTTP error, and on a
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { readEffectiveWire, PrincipalCapsWire } from "@sema-agent/registry-core";
2
+ import { readEffectiveWire, PrincipalCapsWire } from "@sema-agent/settings-schema";
3
3
  import { centerPromptsFromEffective } from "../prompts-domain-validate.js";
4
4
  export async function fetchEffective(baseUrl, token, etag, fetchImpl = fetch, worker) {
5
5
  const scheme = new URL(baseUrl).protocol;
@@ -1,4 +1,4 @@
1
- import { resolveActiveTiers } from "@sema-agent/registry-core";
1
+ import { resolveActiveTiers } from "@sema-agent/settings-schema";
2
2
  import { RUNTIME_GATE_KEYS, HOT_RUNTIME_GATE_KEYS, runtimeGatePresent, resolveDefaultModelName } from "./apply-effective.js";
3
3
  const RESTART_SLICES = ["skills", "mcp", "a2a", "scenarios", "runtime-gates", "models-tiers", "degrade-route", "read-face"];
4
4
  function stableStringify(v) {
@@ -1,4 +1,4 @@
1
- import { skillContentHash } from "@sema-agent/registry-core";
1
+ import { skillContentHash } from "@sema-agent/settings-schema";
2
2
  import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "../mcp-content-origin.js";
3
3
  import { fetchSkillContent } from "./http-client.js";
4
4
  import { SHA256_HEX_RE } from "../digest-form.js";
@@ -1,5 +1,5 @@
1
- import { FileConfigStore } from "@sema-agent/registry-core/node";
2
- import { type EffectiveConfig as AgentConfigEffective } from "@sema-agent/registry-core";
1
+ import { FileConfigStore } from "@sema-agent/settings-schema/node";
2
+ import { type EffectiveConfig as AgentConfigEffective } from "@sema-agent/settings-schema";
3
3
  import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetchSkillContent, type EffectiveConfig } from "./config-center/facade.js";
4
4
  /** Result of a `fetchEffective` — EXACTLY the config-center's return: the effective config + its etag, or
5
5
  * `null` for "unchanged" (remote 304; local: the version matched the caller's prior etag).
@@ -1,6 +1,6 @@
1
1
  import { createHmac, randomBytes } from "node:crypto";
2
- import { FileConfigStore } from "@sema-agent/registry-core/node";
3
- import { findSkillContent, skillContentHash, refIntegrityIssues, siblingResolver, } from "@sema-agent/registry-core";
2
+ import { FileConfigStore } from "@sema-agent/settings-schema/node";
3
+ import { findSkillContent, skillContentHash, refIntegrityIssues, siblingResolver, } from "@sema-agent/settings-schema";
4
4
  import { redactSecrets } from "./trace/redact.js";
5
5
  import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetchSkillContent, ConfigCenterHttpError, } from "./config-center/facade.js";
6
6
  import { createLogger } from "./observability/logger.js";
@@ -1471,7 +1471,7 @@ export interface ServiceConfigFlat {
1471
1471
  worker?: string;
1472
1472
  };
1473
1473
  /** Dual-mode config SOURCE (DUAL-MODE-DESIGN §4): "remote" (sema-registry HTTP) | "local" (.env + config.d
1474
- * via @sema-agent/registry-core FileConfigStore) | unset → remote if SEMA_REGISTRY_URL (legacy CONFIG_CENTER_URL now boot-rejects, no dual-read) is set, else pure-env.
1474
+ * via @sema-agent/settings-schema FileConfigStore) | unset → remote if SEMA_REGISTRY_URL (legacy CONFIG_CENTER_URL now boot-rejects, no dual-read) is set, else pure-env.
1475
1475
  * Env: CONFIG_PROVIDER. The same resolver/EffectiveConfig either way (can't drift). */
1476
1476
  configProvider?: string;
1477
1477
  /** Root dir holding `config.d/<domain>.json` for the local config source. Env: CONFIG_LOCAL_DIR. */
package/dist/config.js CHANGED
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, isThinkingLevel, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveLockedKeys, validatePermissionRules } from "@sema-agent/core";
6
- import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/registry-core";
6
+ import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/settings-schema";
7
7
  import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
8
8
  import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
9
9
  import { isV2ScopeKey } from "./memory-scope.js";
@@ -12,7 +12,7 @@
12
12
  * 向前合并不会双计 —— 只延迟,不丢量)。
13
13
  * - 全部 fire-and-forget + swallow-guarded:fleet 面是运维旁路,绝不影响任务面;网络错误只 warn 一次/连败类。
14
14
  */
15
- import { UsageReport } from "@sema-agent/registry-core/fleet";
15
+ import { UsageReport } from "@sema-agent/settings-schema/fleet";
16
16
  /** Bound on distinct principal×model keys per window — a runaway/multi-tenant burst collapses into
17
17
  * `__other__` instead of growing unbounded (same posture as budget.ts cardinalityGuard). */
18
18
  export declare const FLEET_USAGE_KEY_CAP = 1024;
@@ -1,4 +1,4 @@
1
- import { InstanceAnnouncement, UsageReport, DEFAULT_HEARTBEAT_MS } from "@sema-agent/registry-core/fleet";
1
+ import { InstanceAnnouncement, UsageReport, DEFAULT_HEARTBEAT_MS } from "@sema-agent/settings-schema/fleet";
2
2
  export const FLEET_USAGE_KEY_CAP = 1024;
3
3
  export const FLEET_CLIENT_FETCH_TIMEOUT_MS = 5000;
4
4
  export class FleetUsageAccumulator {
@@ -23,7 +23,7 @@
23
23
  * - 409 判别靠 center 返回的结构化 code(两端常量);center 侧变更时此处同步。
24
24
  * - 全部 swallow-guarded:lease 面绝不 fault 任务提交路径。
25
25
  */
26
- import { QuotaLease } from "@sema-agent/registry-core/fleet";
26
+ import { QuotaLease } from "@sema-agent/settings-schema/fleet";
27
27
  /** 负缓存 TTL:非 lease-enforced 的 principal 多久后再问一次 center(entitlement 升级为 lease 的收敛上界)。 */
28
28
  export declare const NOT_LEASE_TTL_MS: number;
29
29
  /** center 判 budget exhausted 后,多久内的重复提交直接拒绝而不再打 center(防 per-request 打爆)。 */
@@ -1,4 +1,4 @@
1
- import { QuotaLease, leaseExhausted, shouldRenewLease } from "@sema-agent/registry-core/fleet";
1
+ import { QuotaLease, leaseExhausted, shouldRenewLease } from "@sema-agent/settings-schema/fleet";
2
2
  export const NOT_LEASE_TTL_MS = 10 * 60 * 1000;
3
3
  export const EXHAUSTED_RETRY_MS = 30 * 1000;
4
4
  export const LEASE_PRINCIPAL_CAP = 4096;
@@ -1,4 +1,4 @@
1
- import { HooksConfig } from "@sema-agent/registry-core/hooks";
1
+ import { HooksConfig } from "@sema-agent/settings-schema/hooks";
2
2
  import type { Hooks } from "@sema-agent/core";
3
3
  import type { Logger } from "../observability/logger.js";
4
4
  import type { HookNoticeObservation } from "../fleet/fleet-bus.js";
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { hostShell, resolveHostShell } from "../plugins/host-platform.js";
3
- import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, } from "@sema-agent/registry-core/hooks";
3
+ import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, } from "@sema-agent/settings-schema/hooks";
4
4
  import { boundedNoticeText } from "../fleet/fleet-bus.js";
5
5
  import { recordFailOpen } from "../observability/fail-open.js";
6
6
  import { redactSecrets } from "../trace/redact.js";
@@ -1,3 +1,4 @@
1
+ import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
1
2
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
2
3
  import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
3
4
  import { verifyDirectDoorProof } from "../../principal-jwt.js";
@@ -30,6 +31,52 @@ export function isQuestionAnswer(v) {
30
31
  (note === undefined || typeof note === "string"));
31
32
  });
32
33
  }
34
+ async function decideIdempotentReplay(cs, args) {
35
+ const { sessionId, out, binding, decision, payload, deciderPrincipal, explicitOperator, warn } = args;
36
+ if (out.status !== 404 || out.body.errorCode !== "not_found.approval")
37
+ return undefined;
38
+ const boundCallId = binding.boundCallId;
39
+ if (boundCallId === undefined)
40
+ return undefined;
41
+ if (typeof cs.findDecidedApprovalsForBinding !== "function")
42
+ return undefined;
43
+ const rows = await cs.findDecidedApprovalsForBinding(sessionId, boundCallId);
44
+ if (rows.length === 0)
45
+ return undefined;
46
+ if (rows.length >= DECIDED_REPLAY_SCAN_CAP) {
47
+ warn("decide_replay_scan_cap_hit", { sessionId, rows: rows.length });
48
+ return undefined;
49
+ }
50
+ const requestFp = approvalPayloadFingerprint(payload);
51
+ if (requestFp === null)
52
+ return undefined;
53
+ for (const row of rows) {
54
+ if (row.decision === null)
55
+ return undefined;
56
+ if (!explicitOperator) {
57
+ const owner = decodeCheckpointScope(row.scope);
58
+ if (owner !== undefined && owner !== deciderPrincipal) {
59
+ warn("decide_replay_scope_mismatch", { sessionId });
60
+ return undefined;
61
+ }
62
+ }
63
+ if (row.boundInputHash !== (binding.boundInputHash ?? null))
64
+ return undefined;
65
+ if (row.decision !== decision)
66
+ return undefined;
67
+ if (row.payloadFp === null || row.payloadFp !== requestFp)
68
+ return undefined;
69
+ }
70
+ return {
71
+ status: 200,
72
+ body: {
73
+ sessionId,
74
+ idempotent: true,
75
+ decision,
76
+ bindingEnforced: true,
77
+ },
78
+ };
79
+ }
33
80
  export async function handleApprovalsAssistant(req, res, url, ctx) {
34
81
  const miss = { fell: false };
35
82
  await handleApprovalsAssistantBody(req, res, url, ctx, miss);
@@ -440,6 +487,24 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
440
487
  }
441
488
  : undefined;
442
489
  const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, "human", req, answer, binding, grantOnCommit, true);
490
+ const replay = await decideIdempotentReplay(cs, {
491
+ sessionId,
492
+ out,
493
+ binding,
494
+ decision,
495
+ payload: {
496
+ ...(decision === "approve" && binding.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}),
497
+ ...(answer !== undefined ? { answer } : {}),
498
+ ...(body.reason ? { reason: body.reason } : {}),
499
+ },
500
+ deciderPrincipal,
501
+ explicitOperator,
502
+ warn: (event, fields) => deps.logger?.warn?.(event, fields),
503
+ });
504
+ if (replay !== undefined) {
505
+ sendResumeOutcome(res, replay, deps.logger);
506
+ return;
507
+ }
443
508
  sendResumeOutcome(res, out, deps.logger, remember && deps.approvalExemptionStore ? { rememberApplied } : undefined);
444
509
  return;
445
510
  }
@@ -83,7 +83,7 @@ async function handleNotifyWakeBody(req, res, url, ctx, miss) {
83
83
  const liveStream = liveTaskId !== undefined && liveTaskId !== null ? steerableRuns.get(liveTaskId) : undefined;
84
84
  if (liveStream) {
85
85
  try {
86
- await liveStream.notify(payload);
86
+ await liveStream.notify(payload, { priority: "next" });
87
87
  sendJson(res, 200, { sessionId: notifySession, delivery: "live" });
88
88
  return;
89
89
  }
@@ -13,7 +13,7 @@
13
13
  * `cancelledViaVerb` first-writer-wins 判别、`steerableRuns` 活流句柄、`wakeParkMints` park 铸造闩)。
14
14
  */
15
15
  import type { IncomingMessage, ServerResponse } from "node:http";
16
- import { ActorAssertionWire } from "@sema-agent/registry-core";
16
+ import { ActorAssertionWire } from "@sema-agent/settings-schema";
17
17
  import type { RouteCtx, PreparedTaskSubmission } from "../route-ctx.js";
18
18
  export declare const RUN_ID_RE: RegExp;
19
19
  export declare const RUN_CANCEL_RE: RegExp;
@@ -23,7 +23,7 @@ import { buildActiveRunConflict } from "../active-run-conflict.js";
23
23
  import { clearTurnActivity, readTurnActivityMs } from "../../turn-activity.js";
24
24
  import { buildCrossSliceUsage, readResourceWindow, recordResourceWindow } from "../../resource-window.js";
25
25
  import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
26
- import { ActorAssertionWire } from "@sema-agent/registry-core";
26
+ import { ActorAssertionWire } from "@sema-agent/settings-schema";
27
27
  import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
28
28
  export const RUN_ID_RE = /^\/v1\/runs\/([^/]+)(\/events)?$/;
29
29
  export const RUN_CANCEL_RE = /^\/v1\/runs\/([^/]+)\/cancel$/;
@@ -336,7 +336,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
336
336
  sseData(res, { type: "reasoning_delta", delta: redactSecrets(rd.delta), ...(rd.eventId ? { eventId: rd.eventId } : {}), ...(rd.parentToolCallId ? { parentToolCallId: rd.parentToolCallId } : {}) });
337
337
  }
338
338
  },
339
- onTaskNotification: (n) => {
339
+ onTaskNotification: (n, opts) => {
340
340
  if (n.task_type === "workflow")
341
341
  return;
342
342
  defaultSubagentTailBus.forgetHandleContentMode(n.task_id);
@@ -348,13 +348,13 @@ async function handleTasksBody(req, res, url, ctx, miss) {
348
348
  const parked = !deliverable && Boolean(deps.workflowCompletionInbox && prepared.spec.sessionId);
349
349
  deps.logger?.info?.("task_notification_observed", { route: "sync-stream", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive: syncLegLive, parkedDurable: parked });
350
350
  if (parked) {
351
- void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(prepared.spec.sessionId, askOwner, n, Date.now(), durableTaskId)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "sync-stream", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
351
+ void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(prepared.spec.sessionId, askOwner, n, Date.now(), durableTaskId, opts?.priority)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "sync-stream", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
352
352
  }
353
353
  else if (deliverable) {
354
354
  const key = taskNotificationStreamKey(n);
355
355
  if (syncNotifiedKeys.get(key) === undefined) {
356
356
  syncNotifiedKeys.set(key, Promise.resolve(true));
357
- sseData(res, { type: "task_notification", ...taskNotificationEventData({ notification: n }) });
357
+ sseData(res, { type: "task_notification", ...taskNotificationEventData({ notification: n, ...(opts?.priority !== undefined ? { priority: opts.priority } : {}) }) });
358
358
  }
359
359
  }
360
360
  },
@@ -16,7 +16,7 @@ import { runInBackground, evictIfConflict, stripCheckpointToken, parkToolCallId,
16
16
  import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
17
17
  import { recordResumeResourceWindow } from "../resource-window.js";
18
18
  import { publicStoreProbeError } from "../store-live-probe.js";
19
- import { looksLikeJwt } from "@sema-agent/registry-core/api/auth-bridge";
19
+ import { looksLikeJwt } from "@sema-agent/settings-schema/api/auth-bridge";
20
20
  import {} from "../orchestration/workflow-agent-steer.js";
21
21
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
22
22
  import { HANDS_BAND_TOOL_NAMES } from "../capabilities/hands-lane.js";
@@ -1249,7 +1249,7 @@ export function createHttpServer(rawDeps) {
1249
1249
  void verifySink.appendStatus(e).catch((err) => deps.logger?.warn?.("status_event_append_failed", { route: "resume-verify", taskId, err: err instanceof Error ? err.message : String(err) }));
1250
1250
  }
1251
1251
  },
1252
- onTaskNotification: (n) => {
1252
+ onTaskNotification: (n, opts) => {
1253
1253
  if (n.task_type === "workflow")
1254
1254
  return;
1255
1255
  defaultSubagentTailBus.forgetHandleContentMode(n.task_id);
@@ -1260,12 +1260,12 @@ export function createHttpServer(rawDeps) {
1260
1260
  const parked = !resumeLegLive && Boolean(deps.workflowCompletionInbox && sessionId);
1261
1261
  deps.logger?.info?.("task_notification_observed", { route: "resume-verify", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive: resumeLegLive, parkedDurable: parked });
1262
1262
  if (parked) {
1263
- void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(sessionId, principal ?? null, n, Date.now(), taskId)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "resume-verify", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
1263
+ void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(sessionId, principal ?? null, n, Date.now(), taskId, opts?.priority)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "resume-verify", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
1264
1264
  }
1265
1265
  else if (resumeLegLive) {
1266
1266
  const key = taskNotificationStreamKey(n);
1267
1267
  if (resumeNotifiedKeys.get(key) === undefined) {
1268
- resumeNotifiedKeys.set(key, vAppend("task_notification", taskNotificationEventData({ notification: n })).then(() => true, () => false));
1268
+ resumeNotifiedKeys.set(key, vAppend("task_notification", taskNotificationEventData({ notification: n, ...(opts?.priority !== undefined ? { priority: opts.priority } : {}) })).then(() => true, () => false));
1269
1269
  }
1270
1270
  }
1271
1271
  },
@@ -1376,7 +1376,7 @@ export function createHttpServer(rawDeps) {
1376
1376
  void append("tool_end", toolEndEventData(e)).catch(warnAppend(t));
1377
1377
  }
1378
1378
  },
1379
- onTaskNotification: (n) => {
1379
+ onTaskNotification: (n, opts) => {
1380
1380
  if (n.task_type === "workflow")
1381
1381
  return;
1382
1382
  defaultSubagentTailBus.forgetHandleContentMode(n.task_id);
@@ -1387,12 +1387,12 @@ export function createHttpServer(rawDeps) {
1387
1387
  const parked = !resumeLegLive && Boolean(deps.workflowCompletionInbox && sessionId);
1388
1388
  deps.logger?.info?.("task_notification_observed", { route: "resume", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive: resumeLegLive, parkedDurable: parked });
1389
1389
  if (parked) {
1390
- void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(sessionId, principal ?? null, n, Date.now(), taskId)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "resume", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
1390
+ void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(sessionId, principal ?? null, n, Date.now(), taskId, opts?.priority)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "resume", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
1391
1391
  }
1392
1392
  else if (resumeLegLive) {
1393
1393
  const key = taskNotificationStreamKey(n);
1394
1394
  if (resumeNotifiedKeys.get(key) === undefined) {
1395
- resumeNotifiedKeys.set(key, append("task_notification", taskNotificationEventData({ notification: n })).then(() => true, () => false));
1395
+ resumeNotifiedKeys.set(key, append("task_notification", taskNotificationEventData({ notification: n, ...(opts?.priority !== undefined ? { priority: opts.priority } : {}) })).then(() => true, () => false));
1396
1396
  }
1397
1397
  }
1398
1398
  },
@@ -1,3 +1,4 @@
1
+ import type { SystemInjectionPriority } from "@sema-agent/core";
1
2
  import type { LedgerEventType } from "../trace/ledger-events.js";
2
3
  /** One pending completion, scoped to the session that must be told about it. `summary` is ALREADY
3
4
  * redacted + length-bounded by core's notifier seam (see {@link WorkflowCompletionPayload}). */
@@ -262,7 +263,17 @@ export declare function taskNotificationInboxEntry(sessionId: string, owner: str
262
263
  /** lead-b (dual-ID audit): the PARENT run's durable taskId (the uuid the shell's footer rows
263
264
  * key on) — rides the drained frame so a consumer can bind the a*-keyed child notification to its uuid-keyed
264
265
  * parent row without a side lookup. */
265
- parentTaskId?: string): WorkflowCompletionInboxEntry;
266
+ parentTaskId?: string,
267
+ /**
268
+ * design/373 §3.6(core 5.61.0,#366 / codex R1[high]):core 在 `onTaskNotification` 的**第二参**上报的
269
+ * 投递档位。park 是**跨 run** 的那条投递路径 —— 不带过去,档位就在最常走的一条路上恒缺席。
270
+ *
271
+ * 🔴 只在 core **真的给了**档位时传:缺席原样缺席(与 live 投影同一条「缺席≠later」纪律)。
272
+ * 闭集之外的值当作缺席(下面的 `INJECTION_TIERS` 守卫)——durable 载荷更不该出现引擎从没发过的词。
273
+ * ⚠️ 本仓自己铸的 park(`POST /v1/sessions/:id/notify` 的 idle 半场)**不传**这一位:那条 park 是
274
+ * 纯展示帧,没有任何引擎侧的档位承诺,填一个词就是凭空铸一条投递声明。
275
+ */
276
+ priority?: SystemInjectionPriority): WorkflowCompletionInboxEntry;
266
277
  /**
267
278
  * Resolve WHERE a workflow completion routes (core 1.208): the SESSION comes straight off
268
279
  * `originatingSessionId` (the Runner closes over it at RunWorkflow mount — lookup-free). The OWNER comes from,
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync, readFileSync, existsSync, openSync, writeSync, fsyncSync, closeSync, renameSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { redactSecrets } from "../trace/redact.js";
4
+ import { injectionTierOrAbsent } from "../trace/injection-tier.js";
4
5
  export const MAX_PENDING_PER_SESSION = 100;
5
6
  export const COMPACT_EVERY = 512;
6
7
  export const PURGE_FENCE_MS = 10 * 60 * 1000;
@@ -288,10 +289,14 @@ export async function emitPendingWorkflowCompletions(inbox, sessionId, callerPri
288
289
  }
289
290
  }
290
291
  }
291
- export function taskNotificationInboxEntry(sessionId, owner, n, enqueuedAt, parentTaskId) {
292
+ export function taskNotificationInboxEntry(sessionId, owner, n, enqueuedAt, parentTaskId, priority) {
292
293
  const cap = (s, max) => (s.length > max ? `${s.slice(0, max)}…[+${s.length - max} chars]` : s);
293
294
  const extras = {
294
295
  task_type: n.task_type,
296
+ ...(() => {
297
+ const tier = injectionTierOrAbsent(priority);
298
+ return tier !== undefined ? { priority: tier } : {};
299
+ })(),
295
300
  ...(parentTaskId ? { parentTaskId } : {}),
296
301
  ...(n.sessionId ? { sessionId: n.sessionId } : {}),
297
302
  ...(n.toolUseId ? { toolUseId: n.toolUseId } : {}),
@@ -1,4 +1,4 @@
1
- import { PrincipalOrgMemoryWire } from "@sema-agent/registry-core";
1
+ import { PrincipalOrgMemoryWire } from "@sema-agent/settings-schema";
2
2
  import { assertPrincipalShape } from "./security.js";
3
3
  const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
4
4
  const ORG_SCOPE_RE = /^org:\S+$/;
@@ -1,6 +1,6 @@
1
1
  import type { Pool as MySqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
- import { type Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
3
+ import { type Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolvedOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
4
4
  import { type RuleOfferProjection } from "../approval-card.js";
5
5
  import { type SqlDriver } from "./sql-driver.js";
6
6
  /**
@@ -175,6 +175,80 @@ export interface CheckpointAskCandidate {
175
175
  /** 在场 ⇒ 这行读不出(blob 版本超前 / JSON 坏)。收敛器视同不匹配;**从不是** false,缺席即可读。 */
176
176
  unparseable?: true;
177
177
  }
178
+ /**
179
+ * #368 件1 —— legacy `/v1/approvals/:sessionId/decide` 腿的**幂等回放**判别行(窄读,非凭据面)。
180
+ *
181
+ * 刻意**不带 token**:回放只需要「这条已决的行归谁、绑的是哪一次调用、判词是什么」三件,而 token 是
182
+ * resume 凭据(§12-C「checkpointToken 从不外发」;`CheckpointAskCandidate` 带它是因为收敛器要拿它去
183
+ * `bindBatch`,本口没有那个用途 ⇒ 不给)。
184
+ */
185
+ export interface DecidedApprovalRecord {
186
+ /** 行的属主 scope 列(`encodeCheckpointScope` 的产物:principal 或匿名哨兵 `"_"`)。回放属主门的输入。 */
187
+ scope: string;
188
+ /** 行上的 D-1 入参摘要(`bound_input_hash` 列);pre-D-1 行为 null。 */
189
+ boundInputHash: string | null;
190
+ /**
191
+ * 这条已决行的**工具审批判词**,或 `null` = **判别不出**(codex R1-[medium] 四的收敛形):
192
+ * outcome 读不出 / 没有 winner / winner 绑的是**另一个** callId / 判词是 REVIEW 门那三个词。
193
+ *
194
+ * 🔴 `null` **不是**「无所谓」:调用方必须把它当「这条命中的行我读不懂」⇒ **整次回放拒绝**。
195
+ * 一条读不懂的已决行与一条判词相反的已决行在风险上同级 —— 都意味着这只 callId 上的持久事实
196
+ * 不是本次回放能如实复述的。
197
+ */
198
+ decision: "approve" | "deny" | null;
199
+ /**
200
+ * 判词**之外**的决议载荷指纹(codex R2-[high] 一,验真后补):`updatedInput` / `answer` / `reason`
201
+ * 三件都在 core 的持久 winner 里,而**同判词不等于同决议** —— 「approve + 编辑后的实参 A」与
202
+ * 「approve + 实参 B」是两次不同的放行,只比判词就会把后者谎报成「你的请求已被处理」(而 B 从未执行)。
203
+ * 调用方按**同一份请求重发**的语义要求它与本次请求的同三件逐字相等。
204
+ * `null` = 算不出(载荷序列化不了)⇒ 与 `decision: null` 同判:整次拒绝。
205
+ * 铸法见 {@link approvalPayloadFingerprint}(两条车道共用同一只,免得指纹口径漂)。
206
+ */
207
+ payloadFp: string | null;
208
+ }
209
+ /**
210
+ * #368 件1(codex R2-[high] 一)—— 决议载荷的比对指纹:`[updatedInput, answer, reason]` 三件的规范串。
211
+ *
212
+ * 为什么是指纹而不是把三件原样交出来:`updatedInput` 可能携人写的命令原文/密钥形字节,而本读口的
213
+ * 契约是「窄读、非凭据面」—— 交指纹够比对,也不给任何调用点把它误投上 wire 的机会。
214
+ *
215
+ * 🔴 **必须是 canonical(键序无关)**,不是裸 `JSON.stringify`(codex R3-[high] 二,验真后修):行侧那份
216
+ * 载荷是从 `JSON`/`JSONB` 列**回读**的,而 **PG 的 jsonb 不保留输入键序**(它按自己的规则重排)⇒ 客户端
217
+ * 逐字重发原 body 时,请求侧按原键序、行侧按库的键序,裸 stringify 出来的两串不等 ⇒ 一次**合法重试**
218
+ * 被误拒成 404。方向虽保守(不是谎报成功),但那正是本件要修的那个场景,所以指纹按**递归键排序 +
219
+ * 数组保序**铸(见 {@link canonicalFingerprintJson})。
220
+ *
221
+ * 缺席一律折 `null` 哨兵(不是省位):`{updatedInput: undefined}` 与「没有这个键」在本比对里是同一件事。
222
+ * ⚠️ 调用方**必须**按 outcome 铸点的在场规则喂参(`updatedInput` 只在 approve 上、`reason` 走真值判)——
223
+ * 铸点丢掉的字节行上就没有,请求侧多喂一件会把一次合法重试误判成「不同的决议」。
224
+ * 序列化失败(循环引用/BigInt 等)⇒ `null` = 判别不出,调用方据此拒绝回放(fail-closed)。
225
+ */
226
+ export declare function approvalPayloadFingerprint(v: {
227
+ updatedInput?: unknown;
228
+ answer?: unknown;
229
+ reason?: string;
230
+ }): string | null;
231
+ /**
232
+ * #368 件1 —— {@link SqlCheckpointStore.findDecidedApprovalsForBinding} 的扫描上界。
233
+ *
234
+ * 🔴 它**同时是判据**,不只是一道 LIMIT:调用方按「命中集里每条判词都一致才回放」判,而一次被截断的
235
+ * 集合可能把分歧藏在第 N+1 行 ⇒ 消费点约定「**回满 = 判别不出**」,整次拒绝回放(回落 404)。现网正常形
236
+ * 恒 0 或 1 行,回满只可能是数据异常。
237
+ */
238
+ export declare const DECIDED_REPLAY_SCAN_CAP = 8;
239
+ /**
240
+ * #368 件1 —— core 的 `ResolvedOutcome` → 本腿(工具审批 decide)的二值词表。**闭集穷举**(#157):
241
+ * core 哪天给 `ResolvedOutcome.decision` 加员,下面的 `never` 当场编译期红,而不是让一个新判词被
242
+ * 静默折成 `deny` 或漏成「没有决议」。
243
+ *
244
+ * 三条判据,缺一即 `null`(= 不回放,调用方逐字回落修前 404 —— **绝不**把 not-found 洗成成功):
245
+ * ① 有 winner —— `resource_limit` / `wake` / `task_done` 的 resolve 不记 winner(core 契约),那些行
246
+ * 不是「一次人给的工具审批」,没有可回放的判词;
247
+ * ② winner 绑的正是这次请求回显的那个 `boundCallId` —— 身份第二锚(列谓词是第一锚),两锚同意才算同一件事;
248
+ * ③ 判词属于**工具审批**那两个词(`allow`/`deny`)—— `approve`/`reject`/`edit` 是 REVIEW 门
249
+ * (plan_review / dry_run_review)的判词,那些门的决议入口不是本腿,回放它们等于跨门作答。
250
+ */
251
+ export declare function approvalDecisionOfWinner(winner: ResolvedOutcome | undefined, boundCallId: string): "approve" | "deny" | null;
178
252
  /** `pending_steer` 列承载的两个 CheckpointState 字段(= core `appendPendingSteer` / `readPendingSteerQueue`
179
253
  * 的入参形)。列是**唯一**权威(suspend 时写的 blob 从不带它们)。 */
180
254
  type SteerColumnState = Pick<CheckpointState, "pendingSteer" | "pendingSteerQueue">;
@@ -431,6 +505,39 @@ export declare class SqlCheckpointStore implements CheckpointStore {
431
505
  * 维,列命中行结构上也必须解 blob 才拿得到它。
432
506
  */
433
507
  findCheckpointCandidatesForAsk(scope: string, sessionId: string, toolCallId: string, sinceMs: number): Promise<CheckpointAskCandidate[]>;
508
+ /**
509
+ * #368 件1 —— 「这条会话上的这一次工具调用,**是不是已经被决过了**?」的窄读口(legacy `/decide`
510
+ * 腿的幂等回放判别器;返回形见 {@link DecidedApprovalRecord})。
511
+ *
512
+ * 为什么必须有这一口(而不是复用 `findPendingTokenBySession`):后者硬 `status='pending'`,首决之后
513
+ * 恒 null ⇒ 重试与「压根没有这条审批」在 wire 上同形(404)。而合规客户端的重试体里**没有**
514
+ * checkpointToken(它只回显二元组),所以既有的 `approval_stale` 409 那条判别路对它结构性不可达。
515
+ *
516
+ * 四条口径,逐条都是判据:
517
+ * · **谓词 = (session_id, tool_call_id, status='resolved')**:`tool_call_id` 列是 `put()` 从
518
+ * `pendingAction.toolCallId` 盖下来的权威投影,也正是 listPending 交给客户端回显的 `boundCallId`
519
+ * ⇒ 客户端手里那把与库里这一列同源。`(session_id, status)` 是既有索引 `idx_checkpoint_session_status`
520
+ * 的前缀,不新增索引面。
521
+ * · **只认 `resolved`**:`expired`(窗到期被 reaper 抹掉)**不是**一次决议 —— 把它回放成
522
+ * 「已决」等于把「没人来得及答」谎报成「有人答过」。那一形照旧落 404(诚实缺席)。
523
+ * · **`decided_at_ms IS NOT NULL`**:与 respond 腿 `decidedRowIsAuthoritative` 的合取项③同判据
524
+ * (「这条决议真的被结算腿落过」的印记);顺带消掉 MySQL/PG 对 `ORDER BY … DESC` 里 NULL 排序
525
+ * 方向不同这个方言差(MySQL NULL 最后、PG NULL 最前)—— 谓词先滤掉,排序就不含 NULL。
526
+ * · **不带 scope 谓词、由调用方判属主**:与本类 `findPendingTokenBySession` 同姿势(读口不做租户
527
+ * 判决,判决在调用点)。回放腿据返回的 `scope` 做与 pending 那道**逐字同判**的属主门,非属主
528
+ * 拿到的仍是 404(不给存在性谕示)。
529
+ *
530
+ * 🔴 **返回的是全部命中行,不是「最近那一条」**(codex R1-[medium] 四,验真后改向):本口原先按
531
+ * `decided_at_ms DESC LIMIT 1` 取「最近一次决议」,而 local 孪生手里根本没有这个时间戳(core 的
532
+ * `Checkpoint` 上没有 decidedAt 字段),它只能按 `createdAt` 排 —— 两条车道于是可能对**同一批行**给出
533
+ * **不同**的判词(创建序与决议序相反时)。判据因此改成一条与排序**无关**的:调用方要求**全部命中行
534
+ * 判词一致**才回放,分歧即拒。同毫秒并列、两方言 NULL 排序差、跨车道排序键不同 —— 三个问题一起消失,
535
+ * 而现网正常形(恒 0 或 1 行)一个字节不变。上限 8 行足够:超过它的形已经不是「重试」而是数据异常。
536
+ *
537
+ * 坏 `outcome` cell **不抛**:回放是一条锦上添花的腿,让一条读不出的 JSON 把一次 decide 重试变成 500
538
+ * 比 404 更坏。留痕后交出 `decision: null`(= 判别不出),由调用方按「整次拒绝」处置。
539
+ */
540
+ findDecidedApprovalsForBinding(sessionId: string, boundCallId: string): Promise<DecidedApprovalRecord[]>;
434
541
  /**
435
542
  * The owner SCOPE of a session's pending checkpoint (the multi-tenant key === the owner principal in the
436
543
  * BFF/non-operator flow, the same key listPending filters by). For the /decide owner-gate: a non-operator
@@ -4,10 +4,12 @@ import { CheckpointError, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendi
4
4
  import { redactDeep, redactSecrets } from "../trace/redact.js";
5
5
  import { MAX_RULE_OFFERS, MAX_RULE_OFFER_BATCH_MEMBERS, RuleOfferRawSchema } from "../approval-card.js";
6
6
  import { parseJsonStrict as parseJson } from "./sql-row-helpers.js";
7
+ import { createLogger } from "../observability/logger.js";
7
8
  import { recordFailOpen } from "../observability/fail-open.js";
8
9
  import { mysqlDriver, pgDriver, dialectProtocolJsonEncoder } from "./sql-driver.js";
9
10
  import { isDupKeyError } from "./sql-errors.js";
10
11
  import { MANAGED_RETENTION } from "./retention-store-sql.js";
12
+ const fingerprintLogger = createLogger();
11
13
  const MAX_TOOL_INPUT_CHARS = 8192;
12
14
  export const TERMINAL_BACKSTOP_MS = Math.max(60_000, (Number.isFinite(Number(process.env.APPROVAL_TERMINAL_BACKSTOP_MS)) ? Number(process.env.APPROVAL_TERMINAL_BACKSTOP_MS) : 0) || 30 * 86_400_000);
13
15
  export const TERMINAL_GRACE_MS = 3_600_000;
@@ -79,6 +81,48 @@ export function boundedToolInput(args) {
79
81
  return { truncated: true, bytes: json.length };
80
82
  return redacted;
81
83
  }
84
+ function canonicalFingerprintJson(v) {
85
+ if (v === null || typeof v !== "object")
86
+ return JSON.stringify(v) ?? "null";
87
+ if (Array.isArray(v))
88
+ return `[${v.map(canonicalFingerprintJson).join(",")}]`;
89
+ const entries = Object.entries(v)
90
+ .filter(([, val]) => val !== undefined)
91
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
92
+ return `{${entries.map(([k, val]) => `${JSON.stringify(k)}:${canonicalFingerprintJson(val)}`).join(",")}}`;
93
+ }
94
+ export function approvalPayloadFingerprint(v) {
95
+ try {
96
+ const fp = canonicalFingerprintJson([v.updatedInput ?? null, v.answer ?? null, v.reason ?? null]);
97
+ return fp;
98
+ }
99
+ catch (e) {
100
+ fingerprintLogger.warn("approval decision payload is not serializable — idempotent replay refused", { error: e instanceof Error ? e.message : String(e) });
101
+ return null;
102
+ }
103
+ }
104
+ export const DECIDED_REPLAY_SCAN_CAP = 8;
105
+ export function approvalDecisionOfWinner(winner, boundCallId) {
106
+ if (winner === undefined)
107
+ return null;
108
+ if (winner.boundCallId !== boundCallId)
109
+ return null;
110
+ switch (winner.decision) {
111
+ case "allow":
112
+ return "approve";
113
+ case "deny":
114
+ return "deny";
115
+ case "approve":
116
+ case "reject":
117
+ case "edit":
118
+ return null;
119
+ default: {
120
+ const unhandled = winner.decision;
121
+ void unhandled;
122
+ return null;
123
+ }
124
+ }
125
+ }
82
126
  function pendingActionToolCallId(blob) {
83
127
  let parsed;
84
128
  try {
@@ -429,6 +473,27 @@ export class SqlCheckpointStore {
429
473
  }
430
474
  return out;
431
475
  }
476
+ async findDecidedApprovalsForBinding(sessionId, boundCallId) {
477
+ const { rows } = await this.db.query(this.q("SELECT scope, bound_input_hash, outcome FROM checkpoint " +
478
+ `WHERE session_id=? AND tool_call_id=? AND status='resolved' AND decided_at_ms IS NOT NULL ORDER BY decided_at_ms DESC LIMIT ${DECIDED_REPLAY_SCAN_CAP}`, "SELECT scope, bound_input_hash, outcome FROM checkpoint " +
479
+ `WHERE session_id=$1 AND tool_call_id=$2 AND status='resolved' AND decided_at_ms IS NOT NULL ORDER BY decided_at_ms DESC LIMIT ${DECIDED_REPLAY_SCAN_CAP}`), [sessionId, boundCallId]);
480
+ return rows.map((r) => {
481
+ let winner;
482
+ try {
483
+ const outcome = parseJson(r.outcome);
484
+ winner = outcome ? winnerFromOutcome(outcome) : undefined;
485
+ }
486
+ catch (e) {
487
+ this.logger?.info?.("checkpoint_decided_outcome_unreadable", { sessionId, error: e instanceof Error ? e.message : String(e) });
488
+ }
489
+ return {
490
+ scope: String(r.scope),
491
+ boundInputHash: r.bound_input_hash == null ? null : String(r.bound_input_hash),
492
+ decision: approvalDecisionOfWinner(winner, boundCallId),
493
+ payloadFp: winner === undefined ? null : approvalPayloadFingerprint(winner),
494
+ };
495
+ });
496
+ }
432
497
  async peekPendingScope(sessionId) {
433
498
  const { rows } = await this.db.query(this.q("SELECT scope FROM checkpoint WHERE session_id=? AND status='pending' LIMIT 1", "SELECT scope FROM checkpoint WHERE session_id=$1 AND status='pending' LIMIT 1"), [sessionId]);
434
499
  if (!rows[0])
@@ -1,5 +1,5 @@
1
1
  import { type Checkpoint, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ReopenReason, type ResolveExpectation, type ResumeOutcome, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
2
- import { type PendingCheckpoint } from "./checkpoint-store-sql.js";
2
+ import { type DecidedApprovalRecord, type PendingCheckpoint } from "./checkpoint-store-sql.js";
3
3
  export interface LocalCheckpointStoreOptions {
4
4
  /** taskId join for the pending card (the local FileRunStore's `getActiveTaskId`). Absent ⇒ `taskId: null`. */
5
5
  getActiveTaskId?: (sessionId: string) => Promise<string | undefined>;
@@ -91,6 +91,19 @@ export declare class LocalCheckpointStore {
91
91
  * 索引里,其 expired 对本探针不可见(core File store 的 listByScope 是 pending-only,无法枚举历史行回填)。
92
92
  * 旧存量锁死的恢复把手 = cancel verb([868]①,不依赖本索引);增量行全覆盖。 */
93
93
  hasExpiredBySession(sessionId: string): Promise<boolean>;
94
+ /**
95
+ * #368 件1 —— SQL 孪生 `findDecidedApprovalsForBinding` 的本地形(判据、词表、失败方向逐条同源;
96
+ * 那处顶注是唯一真源,这里只记两处**本地形自己的**差别)。
97
+ *
98
+ * · 枚举面走 [868] 的 `sessionTokens` 索引 —— 与 `hasExpiredBySession` 同一条路(core 的 File store
99
+ * 只能 pending-only 枚举,跨态读只有 `get(token)`),所以它那条**存量限制**在这里逐字同样成立:
100
+ * 索引建立之前 park 的老行对本口不可见 ⇒ 回放判别不出 ⇒ 调用方逐字回落 404(保守方向)。
101
+ * · **没有排序**:本口交出全部命中行,由调用方按「判词一致才回放」判(理由见 SQL 孪生顶注的
102
+ * codex R1-[medium] 四段——正是为了消掉「local 只有 createdAt、SQL 有 decided_at_ms」这条排序键
103
+ * 分岔)。同理回放的 wire 形**刻意不投** `decidedAtMs`:core 的 `Checkpoint` 上没有那个字段,
104
+ * 投了就是「SQL 车道有、local 车道没有」的键集分岔。
105
+ */
106
+ findDecidedApprovalsForBinding(sessionId: string, boundCallId: string): Promise<DecidedApprovalRecord[]>;
94
107
  /** `undefined` = no pending row; else the owner scope (local scopes are always strings — never null). */
95
108
  peekPendingScope(sessionId: string): Promise<string | null | undefined>;
96
109
  /**
@@ -3,7 +3,7 @@ import { existsSync, readFileSync, readdirSync, renameSync, unlinkSync, mkdirSyn
3
3
  import { join } from "node:path";
4
4
  import { isApprovalGateKind } from "../tool-approval.js";
5
5
  import { FileCheckpointStore, atomicWriteFile, sanitizePathComponent, } from "@sema-agent/core";
6
- import { boundedRuleOffers, boundedToolInput, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS } from "./checkpoint-store-sql.js";
6
+ import { approvalDecisionOfWinner, approvalPayloadFingerprint, boundedRuleOffers, boundedToolInput, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS } from "./checkpoint-store-sql.js";
7
7
  import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
8
8
  function terminalAtOf(cp) {
9
9
  return Math.max(cp.createdAt + TERMINAL_BACKSTOP_MS, (cp.deadline ?? 0) + TERMINAL_GRACE_MS);
@@ -195,6 +195,27 @@ export class LocalCheckpointStore {
195
195
  }
196
196
  return false;
197
197
  }
198
+ async findDecidedApprovalsForBinding(sessionId, boundCallId) {
199
+ const tokens = this.sessionTokens.get(sessionId);
200
+ if (!tokens)
201
+ return [];
202
+ const out = [];
203
+ for (const t of [...tokens]) {
204
+ const cp = await this.inner.get(t);
205
+ if (!cp || cp.status !== "resolved")
206
+ continue;
207
+ const pa = cp.pendingAction;
208
+ if (pa?.toolCallId !== boundCallId)
209
+ continue;
210
+ out.push({
211
+ scope: cp.scope,
212
+ boundInputHash: pa?.boundInputHash ?? null,
213
+ decision: approvalDecisionOfWinner(cp.resolvedOutcome, boundCallId),
214
+ payloadFp: cp.resolvedOutcome === undefined ? null : approvalPayloadFingerprint(cp.resolvedOutcome),
215
+ });
216
+ }
217
+ return out;
218
+ }
198
219
  async peekPendingScope(sessionId) {
199
220
  for (const { cp } of await this.pendings())
200
221
  if (cp.sessionId === sessionId)
@@ -1,6 +1,6 @@
1
1
  import { SchedulerError } from "@sema-agent/core";
2
2
  import type { SchedulerCapability, ScheduledIntent, SchedulerContext, ScheduledTaskId, ScheduledTaskSummary } from "@sema-agent/core";
3
- import type { SchedulerRecord } from "@sema-agent/registry-core/scheduler-store";
3
+ import type { SchedulerRecord } from "@sema-agent/settings-schema/scheduler-store";
4
4
  export type { SchedulerRecord };
5
5
  /** core `Result<T,E>` shape (harness/types) — `{ok:true,value} | {ok:false,error}`. */
6
6
  type Result<T, E> = {
@@ -2,7 +2,7 @@ import { join } from "node:path";
2
2
  import { homedir } from "node:os";
3
3
  import { randomBytes, createHash } from "node:crypto";
4
4
  import { SchedulerError, isValidCronExpr } from "@sema-agent/core";
5
- import { loadSchedulerStore, saveSchedulerStore } from "@sema-agent/registry-core/node";
5
+ import { loadSchedulerStore, saveSchedulerStore } from "@sema-agent/settings-schema/node";
6
6
  const ok = (value) => ({ ok: true, value });
7
7
  const err = (error) => ({ ok: false, error });
8
8
  export const DEFAULT_SCHEDULER_CAPS = {
package/dist/run-local.js CHANGED
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { realpathSync } from "node:fs";
5
5
  import { createInterface } from "node:readline/promises";
6
6
  import { fileURLToPath } from "node:url";
7
- import { loadRemoteExec } from "@sema-agent/registry-core/node";
7
+ import { loadRemoteExec } from "@sema-agent/settings-schema/node";
8
8
  import { Runner, uuidv7, parseModelMention, AdoptionError, FileStorageBackend, NodeExecutionEnv, NodeLspManager, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore } from "@sema-agent/core";
9
9
  import { hasOperatorGateIntent } from "./approval.js";
10
10
  import { createBrain } from "./brain.js";
package/dist/runs.js CHANGED
@@ -252,7 +252,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
252
252
  }
253
253
  }
254
254
  },
255
- onTaskNotification: (n) => {
255
+ onTaskNotification: (n, opts) => {
256
256
  if (n.task_type === "workflow")
257
257
  return;
258
258
  defaultSubagentTailBus.forgetHandleContentMode(n.task_id);
@@ -263,12 +263,12 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
263
263
  const parked = !legLive && Boolean(workflowCompletionInbox && spec.sessionId);
264
264
  completionDiagLog?.("task_notification_observed", { route: "bg-run-start", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive, parkedDurable: parked });
265
265
  if (parked) {
266
- void workflowCompletionInbox.enqueue(taskNotificationInboxEntry(spec.sessionId, elicitOwner ?? owner, n, Date.now(), taskId)).catch((err) => completionDiagLog?.("park_enqueue_failed", { route: "bg-run-start", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
266
+ void workflowCompletionInbox.enqueue(taskNotificationInboxEntry(spec.sessionId, elicitOwner ?? owner, n, Date.now(), taskId, opts?.priority)).catch((err) => completionDiagLog?.("park_enqueue_failed", { route: "bg-run-start", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
267
267
  }
268
268
  else if (legLive) {
269
269
  const key = taskNotificationStreamKey(n);
270
270
  if (notifiedKeys.get(key) === undefined) {
271
- notifiedKeys.set(key, append("task_notification", taskNotificationEventData({ notification: n })).then(() => true, () => false));
271
+ notifiedKeys.set(key, append("task_notification", taskNotificationEventData({ notification: n, ...(opts?.priority !== undefined ? { priority: opts.priority } : {}) })).then(() => true, () => false));
272
272
  }
273
273
  }
274
274
  },
@@ -27,7 +27,7 @@
27
27
  * this seam feeds the ENGINE, and `allowUltracode` is enforced at the service settings-resolution layer, not here.
28
28
  */
29
29
  import type { RuntimeCaps } from "@sema-agent/core";
30
- import type { EntitlementRuntimeCaps } from "@sema-agent/registry-core";
30
+ import type { EntitlementRuntimeCaps } from "@sema-agent/settings-schema";
31
31
  import { fetchPrincipalCaps, type ExecutionRuling } from "./config-center/facade.js";
32
32
  import type { ScenarioRuling } from "./capabilities/scenarios.js";
33
33
  import type { ServiceConfig } from "./config-types.js";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Runtime governance compiler — the "second baton" (center §10, core 1.114.0): turn the
3
- * operator's declarative `runtime.autonomy` + `runtime.commandPolicy` (`@sema-agent/registry-core`,
3
+ * operator's declarative `runtime.autonomy` + `runtime.commandPolicy` (`@sema-agent/settings-schema`,
4
4
  * carried to the service via sema-registry `EffectiveConfig.runtime`) into TIGHTEN-ONLY overrides on the
5
5
  * per-task `TaskSpec`. Pure so it is unit-testable in isolation (the spec is otherwise assembled inside
6
6
  * main.ts's `resolveSpec`).
@@ -36,7 +36,7 @@
36
36
  * main.ts 单用户闸),仅 malformed 才报 deferred;旧「需要 remote hook-runner」是接线前拍照。)
37
37
  */
38
38
  import { type ExecutionEnv, type TaskSpec, type ThinkingLevel, type ToolPolicy, type PromptProvider } from "@sema-agent/core";
39
- import type { HooksConfig } from "@sema-agent/registry-core/hooks";
39
+ import type { HooksConfig } from "@sema-agent/settings-schema/hooks";
40
40
  /** The permission MODE a client may request (SDK `SettingsPermissions.defaultMode`), the [820]/[822] five-word
41
41
  * table. ⚠️ This is a DELIBERATE deployment set, NOT a copy of any CC set (A-065 P2-1 corpus check): CC's own
42
42
  * full enum is SIX words (= ours + `dontAsk`), CC's managed/remote form accepts FOUR (= ours − `bypassPermissions`,
@@ -1,10 +1,20 @@
1
1
  /**
2
- * 上 wire 的通告码 —— **显式闭集常量表**(设计稿 ①,起步三码)。
2
+ * 上 wire 的通告码 —— **显式闭集常量表**(设计稿 ①,起步三码;今天九码)。
3
3
  *
4
- * 三码同族:都是「这条 session 的记忆姿态」的披露,resume 之后仍然相关 ⇒ 也进 durable 账本
4
+ * 起步的那六码同族:都是「这条 session 的记忆姿态」的披露,resume 之后仍然相关 ⇒ 也进 durable 账本
5
5
  * (cli 断连补看走既有 events 重放腿,[4634](ii))。
6
+ *
7
+ * ⚠️ **自 #366(core 5.61.0)起本表不再是单一族**:`task.turn_interrupted` 与 `task.user_*_undrained`
8
+ * 两族是前三条**非记忆面**的成员(前者=一次插话真的切断了在飞的 turn;后两者=用户的输入被彻底丢弃)。
9
+ * 判据没有变(受众 × 投递可行性 × 去重单位三条合取,见顶注),变的只是「碰巧六条都是记忆码」这个
10
+ * 事实——别再把「是不是记忆姿态码」当成入册的隐性第四条判据。这条隐性判据正是 `task.user_*_undrained`
11
+ * 两码在明拒表里陈腐多时的帮凶之一(见它们的键注)。
12
+ *
13
+ * 🔴 **表长了就得同步三处**(#366 实测,少一处即门红):`test/engine-notice-wire.test.ts` 的闭集 pin、
14
+ * 同文件 G1 e2e 的全码扇出夹具、以及契约文档附录 D.3 那张表(含**小节标题里的码数** —— 那道门本批
15
+ * 刚立,立完就在下一次加码时自己咬住了)。
6
16
  */
7
- export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed"];
17
+ export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "task.user_steer_undrained", "task.user_followup_undrained"];
8
18
  export type EngineNoticeWireCode = (typeof ENGINE_NOTICE_WIRE_CODES)[number];
9
19
  /** 白名单谓词(单点):路由与门都读这一个,不许第二处手抄码串。 */
10
20
  export declare function isEngineNoticeWireCode(code: string): code is EngineNoticeWireCode;
@@ -7,6 +7,9 @@ export const ENGINE_NOTICE_WIRE_CODES = [
7
7
  "memory.hold_opened",
8
8
  "memory.hold_released",
9
9
  "memory.hold_disposed",
10
+ "task.turn_interrupted",
11
+ "task.user_steer_undrained",
12
+ "task.user_followup_undrained",
10
13
  ];
11
14
  const WIRE_CODES = new Set(ENGINE_NOTICE_WIRE_CODES);
12
15
  export function isEngineNoticeWireCode(code) {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * design/373 注入档位(core 5.61.0)的**闭集单点**(#366)。
3
+ *
4
+ * ## 为什么本仓要自己留一份词表
5
+ * 理想写法是复用 core 的谓词 `isSystemInjectionPriority` / 数据表 `SYSTEM_INJECTION_PRIORITIES`。
6
+ * 但 core 5.61.0 **没有把它们放进根 barrel**(`dist/index.d.ts` 只导出了 `SystemInjectionPriority`
7
+ * 这个**类型**),而 `@sema-agent/core` 的 `package.json#exports` 只开了 `.` / `./bench` / `./fixtures`
8
+ * 三口 ⇒ 深导入被 exports map 关死,结构上够不着运行期的那份。
9
+ *
10
+ * 于是用本仓既有的**双向锚**姿势(`http/routes/notify-wake.ts` 的 `NOTIFY_STATUSES` 先例):
11
+ * · `satisfies` 管「表内不许有词表外的词」—— core 删/改档位 ⇒ 编译红;
12
+ * · 穷举断言管「词表不许有表外的词」—— core 加档位 ⇒ 编译红。
13
+ * 两条合起来 = 上游任何一侧的增删都在编译期被逼着处置,而不是运行时静默漂。
14
+ *
15
+ * ## 为什么单独一个叶子模块
16
+ * 消费点有两个且分属两条依赖链:`trace/project.ts`(wire 投影)与
17
+ * `orchestration/workflow-completion-inbox.ts`(durable park 载荷)。手抄两份就是值拷贝漂移源
18
+ * (本仓 [1532] 起的老病族);把它挂在任一方又会在两条链之间拉一条没必要的边。零依赖叶子最干净。
19
+ *
20
+ * 🔴 上游哪天补了根导出:删掉本文件,两处改成一行谓词调用。
21
+ */
22
+ import type { SystemInjectionPriority } from "@sema-agent/core";
23
+ /** 三档闭集,与 core `SystemInjectionPriority` 双向锚(见顶注)。 */
24
+ export declare const INJECTION_TIERS: readonly ["now", "next", "later"];
25
+ /**
26
+ * 边界守卫 —— 闭集内的值原样返回,**其余一律 `undefined`**(= 缺席)。
27
+ *
28
+ * 🔴 方向是**丢键**而不是补默认:core 的键注逐字写着「ABSENT is a fact, not a default …… never read
29
+ * absence as `"later"`」。一个闭集外的值意味着上游发了一个我们不认识的档位,这时候把它原样推上 wire
30
+ * 是在替上游对消费端做承诺,补一个默认则是把「不可知」洗成一条断言 —— 两者都比「说没有」更坏。
31
+ */
32
+ export declare function injectionTierOrAbsent(value: unknown): SystemInjectionPriority | undefined;
33
+ //# sourceMappingURL=injection-tier.d.ts.map
@@ -0,0 +1,7 @@
1
+ export const INJECTION_TIERS = ["now", "next", "later"];
2
+ const _injectionTierExhaustive = true;
3
+ void _injectionTierExhaustive;
4
+ export function injectionTierOrAbsent(value) {
5
+ return typeof value === "string" && INJECTION_TIERS.includes(value) ? value : undefined;
6
+ }
7
+ //# sourceMappingURL=injection-tier.js.map
@@ -12,7 +12,7 @@
12
12
  * old session-join `toolOutput` hook is gone, there's a real source now); `eventId`/`parentToolCallId` (§E2) ride
13
13
  * each content event, additive / tolerate-absent.
14
14
  */
15
- import type { ApprovalSettledBy, TaskResult } from "@sema-agent/core";
15
+ import type { ApprovalSettledBy, TaskResult, SystemInjectionPriority } from "@sema-agent/core";
16
16
  import { type AskDenyResolution } from "@sema-agent/core";
17
17
  import type { RunEvent, RunRecord } from "../plugins/store-contracts.js";
18
18
  import type { ModelUsageDelta, ModelUsageTracker, PromptManifestRecord } from "../budget.js";
@@ -212,6 +212,10 @@ export declare function taskNotificationEventData(ev: {
212
212
  };
213
213
  eventId?: string;
214
214
  parentToolCallId?: string;
215
+ /** design/373 §3.6(core 5.61.0,additive):本帧**投出去时的注入档位**。见下方投影处的键注 —— 它是
216
+ * **事件顶层**键,不是 {@link TaskNotificationPayload} 的键,所以只有把整只 core 事件交给本投影的腿
217
+ * 拿得到它;只拿到载荷的 park-drain 腿结构上没有这一位。 */
218
+ priority?: SystemInjectionPriority;
215
219
  }): Record<string, unknown>;
216
220
  /** The `compacted` payload — EXPLICIT WHITELIST shared by all three legs (bg runs.ts + resume append + the sync
217
221
  * live SSE frame; the 1.72 task_progress lesson: never `{...ev}`). trigger/tokensBefore (MF-18) verbatim;
@@ -1,5 +1,6 @@
1
1
  import { isApprovalSettledBy, isAskDenyResolution } from "@sema-agent/core";
2
2
  import { redactSecrets, redactDeep } from "./redact.js";
3
+ import { injectionTierOrAbsent } from "./injection-tier.js";
3
4
  export async function appendModelUsageDelta(append, modelUsage, taskId) {
4
5
  const delta = modelUsage?.drain(taskId);
5
6
  if (delta) {
@@ -141,6 +142,10 @@ export function taskNotificationEventData(ev) {
141
142
  ...(n.result !== undefined ? { result: redactSecrets(n.result) } : {}),
142
143
  ...(n.output_file !== undefined ? { output_file: redactSecrets(n.output_file) } : {}),
143
144
  ...(n.usage !== undefined ? { usage: n.usage } : {}),
145
+ ...(() => {
146
+ const tier = injectionTierOrAbsent(ev.priority);
147
+ return tier !== undefined ? { priority: tier } : {};
148
+ })(),
144
149
  ...identityFields(ev),
145
150
  };
146
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.48.0",
3
+ "version": "7.49.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,8 +54,8 @@
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.60.1",
58
- "@sema-agent/registry-core": "^0.19.0",
57
+ "@sema-agent/core": "^5.61.0",
58
+ "@sema-agent/settings-schema": "^1.0.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",
61
61
  "mysql2": "^3.22.4",