@sema-agent/server 7.56.0 → 7.58.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 (121) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +2 -2
  3. package/USAGE.md +101 -9
  4. package/dist/approval-ask-audit-store.d.ts +100 -1
  5. package/dist/approval-ask-audit-store.js +103 -2
  6. package/dist/approval-card.d.ts +148 -13
  7. package/dist/approval-card.js +82 -13
  8. package/dist/approval.d.ts +31 -0
  9. package/dist/approval.js +18 -0
  10. package/dist/auto-mode-face.d.ts +111 -0
  11. package/dist/auto-mode-face.js +99 -0
  12. package/dist/bench/s1/arms.js +5 -5
  13. package/dist/bench/s1/live-deps.js +11 -11
  14. package/dist/bench/s1/run-firm.js +3 -0
  15. package/dist/bench/s1/runner-ctx.d.ts +4 -0
  16. package/dist/bench/s1/runner-ctx.js +7 -0
  17. package/dist/boot/config-center.js +23 -1
  18. package/dist/boot/coordinators.js +13 -1
  19. package/dist/boot/parked-revive-gate.d.ts +7 -2
  20. package/dist/boot/parked-revive-gate.js +12 -1
  21. package/dist/boot/resolve-spec.d.ts +3 -0
  22. package/dist/boot/resolve-spec.js +3 -1
  23. package/dist/boot/runner-deps.d.ts +12 -0
  24. package/dist/boot/runner-deps.js +28 -3
  25. package/dist/boot/runtime-caps.d.ts +19 -9
  26. package/dist/boot/runtime-caps.js +49 -21
  27. package/dist/boot/session-shell-gate-registry.d.ts +39 -0
  28. package/dist/boot/session-shell-gate-registry.js +21 -0
  29. package/dist/boot/stores.js +12 -5
  30. package/dist/config-catalog.js +14 -5
  31. package/dist/config-center/http-client.js +7 -11
  32. package/dist/config-center/read-warnings.d.ts +20 -0
  33. package/dist/config-center/read-warnings.js +36 -0
  34. package/dist/config-provider.d.ts +3 -0
  35. package/dist/config-provider.js +4 -8
  36. package/dist/config-types.d.ts +41 -10
  37. package/dist/config.d.ts +21 -0
  38. package/dist/config.js +40 -5
  39. package/dist/hooks/hook-runner.js +8 -8
  40. package/dist/http/route-ctx.d.ts +75 -0
  41. package/dist/http/routes/a2a-serve.js +5 -3
  42. package/dist/http/routes/approvals-assistant.js +19 -6
  43. package/dist/http/routes/capabilities.js +21 -4
  44. package/dist/http/routes/memory-origin.d.ts +2 -2
  45. package/dist/http/routes/rules.js +3 -2
  46. package/dist/http/routes/runs.d.ts +0 -14
  47. package/dist/http/routes/runs.js +20 -8
  48. package/dist/http/routes/tasks.js +34 -8
  49. package/dist/http/routes/workflows.js +19 -4
  50. package/dist/http/server.d.ts +11 -1
  51. package/dist/http/server.js +215 -43
  52. package/dist/http/wire-types.d.ts +9 -4
  53. package/dist/leader/diffout.js +2 -1
  54. package/dist/leader/endpoint.js +45 -12
  55. package/dist/leader/fanout.js +6 -5
  56. package/dist/leader/leader.js +17 -14
  57. package/dist/leader/merge.js +17 -12
  58. package/dist/leader/planner.js +3 -2
  59. package/dist/leader/repair-oracle.js +6 -4
  60. package/dist/leader/repair-wire.js +5 -3
  61. package/dist/leader/wire.d.ts +30 -1
  62. package/dist/leader/wire.js +36 -19
  63. package/dist/main.js +43 -7
  64. package/dist/observability/err-text.d.ts +6 -0
  65. package/dist/observability/err-text.js +10 -0
  66. package/dist/observability/fail-open.d.ts +40 -0
  67. package/dist/observability/fail-open.js +19 -0
  68. package/dist/observability/metrics.js +2 -2
  69. package/dist/observability/run-terminal-log.d.ts +120 -0
  70. package/dist/observability/run-terminal-log.js +360 -0
  71. package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
  72. package/dist/orchestration/workflow-completion-inbox.js +8 -0
  73. package/dist/permission-rule-vocab.d.ts +40 -0
  74. package/dist/permission-rule-vocab.js +17 -0
  75. package/dist/plugins/checkpoint-store-sql.d.ts +14 -0
  76. package/dist/plugins/checkpoint-store-sql.js +19 -3
  77. package/dist/plugins/file-run-store.d.ts +3 -34
  78. package/dist/plugins/file-run-store.js +19 -0
  79. package/dist/plugins/local-checkpoint-store.d.ts +10 -0
  80. package/dist/plugins/local-checkpoint-store.js +3 -0
  81. package/dist/plugins/mailbox-store-sql.d.ts +28 -9
  82. package/dist/plugins/mailbox-store-sql.js +72 -10
  83. package/dist/plugins/memory-run-store.d.ts +3 -15
  84. package/dist/plugins/memory-run-store.js +19 -0
  85. package/dist/plugins/permission-rule-store-file.js +8 -3
  86. package/dist/plugins/permission-rule-store-sql.js +16 -10
  87. package/dist/plugins/remote-scratchpad.js +3 -2
  88. package/dist/plugins/run-store-sql.d.ts +3 -42
  89. package/dist/plugins/run-store-sql.js +30 -1
  90. package/dist/plugins/store-backend.d.ts +1 -1
  91. package/dist/plugins/store-contracts.d.ts +66 -1
  92. package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
  93. package/dist/plugins/workflow-run-store-sql.js +10 -2
  94. package/dist/rules-consent.js +20 -13
  95. package/dist/run-cancel-context.d.ts +13 -0
  96. package/dist/run-cancel-context.js +15 -0
  97. package/dist/run-local.js +2 -2
  98. package/dist/runs.d.ts +4 -1
  99. package/dist/runs.js +41 -10
  100. package/dist/runtime-caps-resolver.d.ts +133 -17
  101. package/dist/runtime-caps-resolver.js +41 -3
  102. package/dist/task-settings.d.ts +57 -3
  103. package/dist/task-settings.js +78 -5
  104. package/dist/task-workflow.d.ts +32 -2
  105. package/dist/task-workflow.js +8 -3
  106. package/dist/tool-approval.d.ts +26 -3
  107. package/dist/tool-approval.js +96 -13
  108. package/dist/trace/core-keyset-guard.d.ts +7 -8
  109. package/dist/trace/engine-notice-wire.d.ts +1 -1
  110. package/dist/trace/engine-notice-wire.js +2 -0
  111. package/dist/trace/ledger-sink.d.ts +13 -4
  112. package/dist/trace/ledger-sink.js +14 -6
  113. package/dist/trace/project.d.ts +58 -3
  114. package/dist/trace/project.js +33 -1
  115. package/dist/trace/redact.d.ts +22 -11
  116. package/dist/trace/redact.js +660 -21
  117. package/dist/trace/sema-provenance.d.ts +35 -0
  118. package/dist/trace/sema-provenance.js +14 -0
  119. package/dist/turn-activity.d.ts +32 -2
  120. package/dist/turn-activity.js +29 -4
  121. package/package.json +3 -3
package/dist/config.js CHANGED
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, isThinkingLevel, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules } from "@sema-agent/core";
5
+ import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules } from "@sema-agent/core";
6
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";
@@ -521,7 +521,8 @@ function noticeAutoCompactAboveWindow(slot, envName, m) {
521
521
  model: m.id,
522
522
  value: String(act),
523
523
  contextWindow: String(window),
524
- note: `${envName}=${act} is ABOVE the ${slot} model "${m.id}" physical context window (${window}) — core uses autoCompactTokens as the TRIGGER-side window, so a value above the physical window means autocompaction effectively never fires and the destructive context guard truncates history instead (the opposite of what this knob is for). The value is applied as configured (not clamped, not rejected); lower it below the window — CC's own shape is window-33000.`,
524
+ clampedTo: String(window),
525
+ note: `${envName}=${act} is ABOVE the ${slot} model "${m.id}" physical context window (${window}) — core uses autoCompactTokens as the TRIGGER-side window and CLAMPS it to the physical window (resolveTriggerWindow), so this knob is a NO-OP here: the trigger geometry falls back to ${window} and core mints one config.autocompact_window_clamped notice per prepare. server neither clamps nor rejects the value (clamping is core's, at the read site); lower it below the window to make the knob do something — CC's own shape is window-33000.`,
525
526
  },
526
527
  });
527
528
  }
@@ -637,8 +638,8 @@ function parseModelDomain() {
637
638
  return "gateway";
638
639
  })();
639
640
  const extraBody = buildExtraBody();
640
- const thinkingDefault = (() => { const v = process.env.MODEL_DEFAULT_THINKING; return v && isThinkingLevel(v) && v !== "off" ? v : undefined; })();
641
- const effortLevels = (process.env.MODEL_REASONING_EFFORT_LEVELS ?? "").split(",").map((s) => s.trim()).filter((s) => isThinkingLevel(s) && s !== "off");
641
+ const thinkingDefault = parseDefaultThinking(process.env.MODEL_DEFAULT_THINKING);
642
+ const effortLevels = parseReasoningEffortLevels(csv("MODEL_REASONING_EFFORT_LEVELS"));
642
643
  const model = {
643
644
  id: modelId,
644
645
  name: modelId,
@@ -777,6 +778,32 @@ function parseModelDomain() {
777
778
  : undefined,
778
779
  };
779
780
  }
781
+ const THINKING_LEVEL_TABLE = { minimal: true, low: true, medium: true, high: true, xhigh: true, max: true };
782
+ export const THINKING_LEVELS = Object.keys(THINKING_LEVEL_TABLE);
783
+ const isThinkingTier = (w) => THINKING_LEVELS.includes(w);
784
+ function parseDefaultThinking(raw) {
785
+ const v = raw?.trim();
786
+ if (v === undefined || v === "")
787
+ return undefined;
788
+ if (v === "off")
789
+ return undefined;
790
+ if (!isThinkingTier(v)) {
791
+ throw new Error(`env MODEL_DEFAULT_THINKING="${v}" is not a thinking tier — valid: ${THINKING_LEVELS.join(" | ")} | off ` +
792
+ `("off" = no model-level default). A typo used to be dropped SILENTLY, leaving the deployment on no default at all (#210 A 档).`);
793
+ }
794
+ return v;
795
+ }
796
+ function parseReasoningEffortLevels(words) {
797
+ if (words.length === 0)
798
+ return [];
799
+ const bad = words.filter((w) => !isThinkingTier(w));
800
+ if (bad.length > 0) {
801
+ throw new Error(`env MODEL_REASONING_EFFORT_LEVELS has unknown tier(s): ${bad.join(", ")} (valid: ${THINKING_LEVELS.join(" | ")})` +
802
+ `${bad.includes("off") ? ' — "off" belongs to MODEL_DEFAULT_THINKING (no model-level default), it is not an effort tier an endpoint can accept' : ""}. ` +
803
+ `Bad words used to be dropped SILENTLY, narrowing the declared ladder without a word (#210 A 档).`);
804
+ }
805
+ return words.filter(isThinkingTier);
806
+ }
780
807
  const DEGRADE_ON_WORDS = new Set(["breaker_open", "rate_limit", "budget", "server_error", "last_resort"]);
781
808
  function parseDegradeOn(words) {
782
809
  if (words.length === 0)
@@ -853,6 +880,7 @@ export function formatUnmatchableToolNames(source, bad) {
853
880
  return (`${source} names tool(s) that can never match a live tool (these lists are RAW whole-name comparisons — core 5.0.0 RB-476 removed the alias/auto-prefix folds): ` +
854
881
  bad.map((b) => `${presentToolName(b.name)} → ${b.guidance}`).join("; ")).replace(CONTROL_AND_BIDI_STRIP_RE, "");
855
882
  }
883
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
856
884
  function streamApprovalConfig(gates) {
857
885
  const cfg = {
858
886
  enabled: boolEnv("STREAM_APPROVAL_ENABLED", true),
@@ -866,6 +894,12 @@ function streamApprovalConfig(gates) {
866
894
  orphanTtlMs: numEnvBounded("STREAM_APPROVAL_ORPHAN_TTL_MS", String(7 * 24 * 60 * 60 * 1000), 60_000, 90 * 24 * 60 * 60 * 1000),
867
895
  unreachedTtlMs: numEnvBounded("UNREACHED_ASK_TTL_MS", String(60 * 60 * 1000), 60_000, 24 * 60 * 60 * 1000),
868
896
  };
897
+ if (cfg.windowMs < 0) {
898
+ throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} must not be negative — 0 means "close the window" (every gated ask goes straight to the unattended outcome and NO approval card is emitted), any positive value is the live-card window in ms. A negative value used to emit a card nobody could ever win.`);
899
+ }
900
+ if (cfg.windowMs > MAX_TIMER_DELAY_MS) {
901
+ throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} exceeds the Node timer ceiling ${MAX_TIMER_DELAY_MS} (2^31-1 ms ≈ 24.8 days) — setTimeout SILENTLY collapses a larger delay to 1ms, so the window you meant to lengthen would expire immediately and every gated ask would take the unattended route on the next tick. Pick a window at or below the ceiling (and remember STREAM_ASK_WINDOW_MS + STREAM_APPROVAL_ADHOC_GRACE_MS must stay strictly below STREAM_APPROVAL_ORPHAN_TTL_MS).`);
902
+ }
869
903
  if (cfg.windowMs + cfg.adhocGraceMs >= cfg.orphanTtlMs) {
870
904
  throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} + STREAM_APPROVAL_ADHOC_GRACE_MS=${cfg.adhocGraceMs} must be STRICTLY LESS than STREAM_APPROVAL_ORPHAN_TTL_MS=${cfg.orphanTtlMs} — the adhoc arm fires at (createdAt + window + grace) while the orphan backstop fires at (createdAt + ttl); if the backstop wins, every adhoc-leg row is attributed "orphan_ttl_exceeded" instead of "adhoc_leg_no_durable_domain" (attribution honesty, design/172 §6.1)`);
871
905
  }
@@ -945,6 +979,7 @@ function parseApprovalDomain(ctx) {
945
979
  `The words are matched EXACTLY (lowercase, surrounding whitespace trimmed): "OFF"/"Classify" land here too, deliberately — a closed word table with no case-folding is what keeps "it looked accepted" from meaning "it did nothing". ` +
946
980
  `Refusing to boot rather than treating a misspelled value as "unset": that silently ran the deployment with NO shell gate while the operator believed one was configured`);
947
981
  })(process.env.MANUAL_MODE_SHELL_GATE),
982
+ permissionsDisableAutoMode: boolEnv("PERMISSIONS_DISABLE_AUTO_MODE", false),
948
983
  mcpElicitation: {
949
984
  enabled: boolEnv("MCP_ELICITATION_ENABLED", false),
950
985
  throttle: {
@@ -1757,7 +1792,7 @@ const APPROVAL_GROUP_KEYS = [
1757
1792
  "approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
1758
1793
  "approvalHmacKeys", "durableApproval", "directApprovalDoor", "directDoorActive", "resourceSuspend",
1759
1794
  "resourceSuspendTtlSec", "resourceSuspendTotalTokens", "resourceSuspendTotalBudgetUsd", "resourceSuspendMaxSlices", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "permissionRulesEnabled", "permissionRulesEnabledExplicit", "streamAskWindowMarginMs", "streamApproval", "unattendedApprovalPolicy", "mcpElicitation",
1760
- "sensitiveWritePatterns", "manualModeShellGate",
1795
+ "sensitiveWritePatterns", "manualModeShellGate", "permissionsDisableAutoMode",
1761
1796
  ];
1762
1797
  const MEMORY_GROUP_KEYS = [
1763
1798
  "memoryEngineEnabled", "memoryEngineDir", "memoryEngineRemoteLaneAllowed", "memoryEngineBackend", "memoryScope",
@@ -673,7 +673,7 @@ async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, once
673
673
  if (!out)
674
674
  continue;
675
675
  if (typeof out.systemMessage === "string")
676
- ctx.logger.warn("hook_system_message", { event, message: clip(out.systemMessage, 500) });
676
+ ctx.logger.info("hook_system_message", { event, message: clip(out.systemMessage, 500) });
677
677
  if (out.decision !== undefined || out.continue === false || out.hookSpecificOutput !== undefined) {
678
678
  ctx.logger.warn("hook_result_unsupported", { event });
679
679
  }
@@ -747,7 +747,7 @@ export function createTaskHooks(config, ctx) {
747
747
  if (!out)
748
748
  continue;
749
749
  if (typeof out.systemMessage === "string")
750
- ctx.logger.warn("hook_system_message", { event: "PreToolUse", message: clip(out.systemMessage, 500) });
750
+ ctx.logger.info("hook_system_message", { event: "PreToolUse", message: clip(out.systemMessage, 500) });
751
751
  if (out.continue === false) {
752
752
  const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
753
753
  return { action: "deny", message: reason, ...buildHookContextField(contexts) };
@@ -845,7 +845,7 @@ export function createTaskHooks(config, ctx) {
845
845
  if (!out)
846
846
  continue;
847
847
  if (typeof out.systemMessage === "string")
848
- ctx.logger.warn("hook_system_message", { event: "PostToolUse", message: clip(out.systemMessage, 500) });
848
+ ctx.logger.info("hook_system_message", { event: "PostToolUse", message: clip(out.systemMessage, 500) });
849
849
  if (out.continue === false) {
850
850
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUse" });
851
851
  }
@@ -895,7 +895,7 @@ export function createTaskHooks(config, ctx) {
895
895
  if (!out)
896
896
  continue;
897
897
  if (typeof out.systemMessage === "string")
898
- ctx.logger.warn("hook_system_message", { event: "PostToolUseFailure", message: clip(out.systemMessage, 500) });
898
+ ctx.logger.info("hook_system_message", { event: "PostToolUseFailure", message: clip(out.systemMessage, 500) });
899
899
  if (out.continue === false)
900
900
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUseFailure" });
901
901
  if (out.decision === "block" && typeof out.reason === "string")
@@ -942,7 +942,7 @@ export function createTaskHooks(config, ctx) {
942
942
  if (!out)
943
943
  continue;
944
944
  if (typeof out.systemMessage === "string")
945
- ctx.logger.warn("hook_system_message", { event: "PostToolBatch", message: clip(out.systemMessage, 500) });
945
+ ctx.logger.info("hook_system_message", { event: "PostToolBatch", message: clip(out.systemMessage, 500) });
946
946
  if (out.continue === false)
947
947
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolBatch" });
948
948
  if (out.decision === "block" && typeof out.reason === "string")
@@ -974,7 +974,7 @@ export function createTaskHooks(config, ctx) {
974
974
  if (!out)
975
975
  continue;
976
976
  if (typeof out.systemMessage === "string")
977
- ctx.logger.warn("hook_system_message", { event: "UserPromptSubmit", message: clip(out.systemMessage, 500) });
977
+ ctx.logger.info("hook_system_message", { event: "UserPromptSubmit", message: clip(out.systemMessage, 500) });
978
978
  if (out.continue === false) {
979
979
  const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
980
980
  return { block: reason, ...buildHookContextField(contexts) };
@@ -1063,7 +1063,7 @@ export function createTaskHooks(config, ctx) {
1063
1063
  continue;
1064
1064
  }
1065
1065
  if (typeof out.systemMessage === "string")
1066
- ctx.logger.warn("hook_system_message", { event: "Stop", message: clip(out.systemMessage, 500) });
1066
+ ctx.logger.info("hook_system_message", { event: "Stop", message: clip(out.systemMessage, 500) });
1067
1067
  if (out.continue === false)
1068
1068
  sawContinueFalse = true;
1069
1069
  if (out.decision === "block" && block === undefined) {
@@ -1122,7 +1122,7 @@ export function createTaskHooks(config, ctx) {
1122
1122
  if (!out)
1123
1123
  continue;
1124
1124
  if (typeof out.systemMessage === "string")
1125
- ctx.logger.warn("hook_system_message", { event: "PreCompact", message: clip(out.systemMessage, 500) });
1125
+ ctx.logger.info("hook_system_message", { event: "PreCompact", message: clip(out.systemMessage, 500) });
1126
1126
  if (out.continue === false) {
1127
1127
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PreCompact" });
1128
1128
  }
@@ -47,6 +47,24 @@ export interface RouteHelpers {
47
47
  rateLimited(req: IncomingMessage, res: ServerResponse): boolean;
48
48
  quotaExceeded(req: IncomingMessage, res: ServerResponse): boolean;
49
49
  leaseDenied(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
50
+ /**
51
+ * 🔴 **[ref]:按**付费**方(不是请求方)判的成本配额准入 —— {@link quotaExceeded} 的孪生。**
52
+ *
53
+ * `quotaExceeded` 键在 `gatedPrincipal(req)` = 谁点的按钮;本门键在**这笔钱记谁的账**。两者在自助
54
+ * 场景恒相等,在「显式 operator 代别人动手」的场景不等 —— 那正是配额上限被绕过的那一支。
55
+ * 参数 `owner` 由调用点从**已属主校验过的那一行**取(run 行的 `owner` / checkpoint 的 scope),
56
+ * 本门不自己推身份(推得出来就说明调用点还有第二个身份源,那才是病)。
57
+ * `undefined`(匿名/开放行)⇒ 无付费方 ⇒ 不 gated,与 `quotaExceeded` 的成文语义同判。
58
+ * 拒 = 429 + `Retry-After`,码/文案/extras 与 `quotaExceeded` 逐字同一套。
59
+ * 位序纪律:**必须排在本路由的 owner 门之后**(反枚举 —— 否则 429 会变成「这行存在且它属主超顶了」
60
+ * 的谕示);且排在真正驱动模型之前。
61
+ */
62
+ ownerQuotaDenied(res: ServerResponse, owner: string | undefined): boolean;
63
+ /** 🔴 **[ref]:{@link ownerQuotaDenied} 的 lease 孪生 —— 按**付费**方判的 fleet-lease 准入。**
64
+ * `leaseDenied` 键在请求方;本门键在付费方(`driveResumeLeg` 的 `fleetLease.admit(卡属主)` 早就是这个
65
+ * 语义,本位把它交给路由域)。两门在自助场景恒同答,在「operator 代别人动手」时不同 —— 那正是
66
+ * 「拿自己的额度去烧别人的账」的那一支。同样必须排在 owner 门之后、驱动之前。 */
67
+ ownerLeaseDenied(res: ServerResponse, owner: string | undefined): Promise<boolean>;
50
68
  /** [ref]-T1:治理窗 pre-admission(异步 202 车道的唯一 429 出路;key 公式与 core 单源)。 */
51
69
  usageWindowDenied(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
52
70
  safeDecode(seg: string): string | null;
@@ -79,6 +97,17 @@ export interface RouteRequestState {
79
97
  streamDetached: boolean;
80
98
  /** 凭证派生的调用方系统身份(`source`)。handle() 的鉴权门是**唯一**赋值点,域模块只读。 */
81
99
  source: string | null;
100
+ /**
101
+ * [ref] 件2(cli [ref] sema-bug4 (c)):本请求驱动的那条 run 的**终局**(`completed` / `failed` /
102
+ * `suspended` / …)与结构化失败码。`handle()` 的 `request` 日志行读它 —— 让「端点 200 ≠ 模型成功」
103
+ * 这条歧义消失(取证:模型 connect timeout 的那一刻,日志里唯一的行是 `request status:200`)。
104
+ *
105
+ * 赋值点在 `routes/tasks.ts`(`/v1/tasks` 与 `/v1/tasks/stream` 两腿的终局到手处);拿不到 run 结果
106
+ * (受理即拒 / 幂等重放 / 非任务端点)⇒ **键缺席**,绝不写 null —— 缺席 = 「这条请求没驱动 run」,
107
+ * 与「run 结果不明」是两句话,而 `null` 会把两者压成一个形。
108
+ */
109
+ runStatus?: string;
110
+ runErrorCode?: string;
82
111
  }
83
112
  /** B4①(命名化):`prepareSpec` 的返回形(tasks.ts/runs.ts 两域共同消费的属性面)——原为 6 成员内联匿名对象。
84
113
  * 命名后好处不只是过 B4 门:两个消费域现在都能对同一个符号做 `import type` 标注,而不是各自重推断结构。 */
@@ -141,6 +170,52 @@ export interface DriveResumeArgs {
141
170
  * **唯一不设此位的**是内部 D-D SLA deny-sweep:它没有在等响应的人,提前受理对它零收益,而且那条
142
171
  * 循环靠 `await` 串行给每 tick 的重活限流(见其调用处的登记注)。 */
143
172
  acceptEarly?: boolean;
173
+ /**
174
+ * 🔴 **[ref](codex 交叉复审 r2-[medium] 验真后修):这条腿是**调用方发起**的吗?**
175
+ *
176
+ * `true` = 有人在 HTTP 上点了按钮(resume / plan_review / wake / decide / 赎回席)⇒ 吃按**付费方**
177
+ * (卡属主)判的成本配额准入(`ownerCostQuotaDenied`)。缺席/`false` = **部署自己的结算腿**
178
+ * (内部 D-D SLA deny-sweep)⇒ **不吃**那道门。
179
+ *
180
+ * 为什么系统腿必须豁免(亲核后改的判据,首版曾一并拦下 sweep):sweep 的活是把过了 SLA 的卡**结算掉**
181
+ * (graceful deny → 行收敛 → 释放 `task_active` claim)。拦住它并不能省钱,只会让超顶租户的卡**挂着**
182
+ * —— 而挂多久的兜底**比首版注释里写的差得多**:`checkpoint-store-sql.ts` 的 `reapExpired` 在
183
+ * **deadline 分支**上逐字排除了 `APPROVAL_GATE_KINDS`(`gate_kind NOT IN (…) OR tool_name='AskUserQuestion'`),
184
+ * 所以一张过期的 human / irreversible_ask 卡根本不走那条腿;真正兜住它的只有绝对 backstop
185
+ * `terminal_at_ms`(`TERMINAL_BACKSTOP_MS` 默认 **30 天**)。于是「拦 sweep」= 把一个由
186
+ * `APPROVAL_TIMEOUT_SEC` 界定的 SLA 换成「等成本窗滚过(`COST_QUOTA_WINDOW_SEC` 默认 24h)、最坏等 30 天」,
187
+ * 并且整段时间攥着会话 claim。**方向也自洽**:deny 是**停下**这条 run 的动作,为了省钱而拒绝执行「停」
188
+ * 与 preempt 路由早就成文的那句同一条(「否则超顶租户连自己的花费都停不下来」)。
189
+ *
190
+ * 判别子取 `req` 的在场与否 —— 这在 `resumeCheckpoint` 里**已经是**「系统腿 vs 调用方腿」的既有判据
191
+ * (绑定强制、parked 分流两处都写着 `req !== undefined` 与「内部 D-D SLA deny-sweep(req 缺席)豁免」),
192
+ * 本位只是把同一条判据显式化成一个有名字的位,而不是让下游去猜。
193
+ * ⚠️ **射程覆盖两道门**:紧邻的 fleet-lease 准入(`ownerFleetLeaseDenied`)读**同一位** —— [ref] 件⑤②
194
+ * (codex r3-[high])把它一并改吃了 `callerInitiated`,理由与三问逐字同 cost 半场。
195
+ * 🔴 本句是 [ref] R4 改写的(2026-09-03,7.57.0 发车前合并重扫 wf_1abaaa9a):此处原文写着「紧邻的
196
+ * fleet-lease 准入**没有**这条豁免(对 sweep 一视同仁),本批不动它,分歧已登记为遗留」—— 那是**同一批
197
+ * 件⑤② 落地之前**的话,件⑤② 修好后这条 JSDoc 没跟着改,于是注释与代码正相反(亲读
198
+ * `server.ts` 的 `ownerFleetLeaseDenied` 消费点:两处都是 `args.callerInitiated === true` /
199
+ * `req !== undefined`)。两道门在这一位上**无分歧**,[ref] 上也没有这一项。
200
+ */
201
+ callerInitiated?: boolean;
202
+ /**
203
+ * 🔴 **[ref] codex 对抗复审 r1-F3 [medium](验真后修):本次请求的属主 lease 准入**已经做过了**。**
204
+ *
205
+ * `resumeCheckpoint` 在 parked 子代赎回分流**之前**先判一次属主 lease([ref] R3:那条腿提前 `return`,
206
+ * 结构上够不到 `driveResumeLeg` 的同一道门)。分流**未命中**时请求落回 legacy 腿,于是同一条请求、
207
+ * 同一个付费方会向 center 协商**两次**。cost 半场可以「过两次门」(本地同步、纯读);lease 半场**不行**:
208
+ * `FleetLeaseManager.admit` 的五条 fail-open 路径(center 不可达 / 5xx / 超时 / 401-403 / 畸形响应)
209
+ * 一律**不写缓存态**(亲读 `fleet-lease.ts` 的 `issueOrRenewInner`),而 single-flight 只合并**并发**调用 ——
210
+ * 两道门是串行的 ⇒ 半开 center 下一个请求连吃两次 `LEASE_FETCH_TIMEOUT_MS`,持续故障时整条 legacy
211
+ * resume 车道把控制面负载翻倍。
212
+ *
213
+ * 所以由上游把「已受理」这个事实传下来,`driveResumeLeg` 据此跳过重复协商。**安全性**:两处的键是
214
+ * **同一个表达式**(`decodeCheckpointScope(cp.scope)`,上游门与下游 `auth.principal` 同源),射程条件也
215
+ * 同一条(`req !== undefined` ⇔ `callerInitiated`)⇒ 跳过的是一次**逐字相同**的判定,不是一道门。
216
+ * 缺席/`false` ⇒ 照常自己判(其余三条驱动腿与 sweep 一字不变)。
217
+ */
218
+ ownerLeaseAdmitted?: boolean;
144
219
  }
145
220
  /** 提交/续跑「腿」= 跨域共享的**有状态**长流程(不像 {@link RouteHelpers} 那样只依赖 deps/config:它们要写
146
221
  * run log、发 fleet 帧、走 markResuming CAS、驱动模型)。A9 上批的停点就在这里——tasks/runs 两域共用
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { memoRedactedRead, redactErrorMessage } from "../../observability/run-terminal-log.js";
2
3
  import { A2A_TASK_STATES } from "@sema-agent/core";
3
- import { isParkedRunStatus } from "../../plugins/store-contracts.js";
4
+ import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-contracts.js";
4
5
  import { HttpError } from "../../security.js";
5
6
  import { scopedIdempotencyKey } from "../idempotency.js";
6
7
  import { sendJson, sendError } from "../send.js";
@@ -80,10 +81,11 @@ function statusTextForRun(run, state) {
80
81
  return "this task is suspended awaiting an approval decision on the serving deployment; it resumes once that decision is made";
81
82
  if (run.result?.result)
82
83
  return run.result.result;
84
+ const memo = (seat, raw) => (isTerminalRunStatus(run.status) ? memoRedactedRead(`a2a.${seat}`, raw, (v) => redactErrorMessage(v) ?? "") : (redactErrorMessage(raw) ?? ""));
83
85
  if (run.error)
84
- return run.error;
86
+ return memo("error", run.error);
85
87
  if (run.result?.errorMessage)
86
- return run.result.errorMessage;
88
+ return memo("errorMessage", run.result.errorMessage);
87
89
  return undefined;
88
90
  }
89
91
  function taskFromRun(run, stale) {
@@ -107,6 +107,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
107
107
  : (principal ?? "__none__");
108
108
  sendJson(res, 200, {
109
109
  pending: (await cs.listPending(scope)).map((r) => projectPendingForWire(r, deps.config)),
110
+ serverNowMs: Date.now(),
110
111
  ...(deps.toolApproval ? { livePending: deps.toolApproval.listLivePending(scope) } : {}),
111
112
  ...(deps.approvalAskAudit ? { crashConverged: deps.approvalAskAudit.listCrashConverged(scope) } : {}),
112
113
  });
@@ -520,9 +521,20 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
520
521
  const APPROVALS_STREAM_POLL_MS = 3000;
521
522
  function redactPendingDisclosures(row) {
522
523
  const rd = row.riskDescriptor;
523
- if (rd === null || rd === undefined || typeof rd.shadowedRule !== "string")
524
+ if (rd === null || rd === undefined)
524
525
  return row;
525
- return { ...row, riskDescriptor: { ...rd, shadowedRule: redactSecrets(rd.shadowedRule) } };
526
+ const shadowedRule = typeof rd.shadowedRule === "string" ? redactSecrets(rd.shadowedRule) : undefined;
527
+ const summary = typeof rd.summary === "string" ? redactSecrets(rd.summary) : undefined;
528
+ if (shadowedRule === undefined && summary === undefined)
529
+ return row;
530
+ return {
531
+ ...row,
532
+ riskDescriptor: {
533
+ ...rd,
534
+ ...(shadowedRule !== undefined ? { shadowedRule } : {}),
535
+ ...(summary !== undefined ? { summary } : {}),
536
+ },
537
+ };
526
538
  }
527
539
  function projectPendingForWire(row, governance) {
528
540
  const redacted = redactPendingDisclosures(row);
@@ -553,19 +565,20 @@ export async function streamApprovals(req, res, cs, scope, governance, pollMs =
553
565
  }
554
566
  const cur = new Map(pending.map((p) => [keyOf(p), p]));
555
567
  const curFrames = new Map([...cur].map(([k, p]) => [k, JSON.stringify({ type: "pending", ...p })]));
568
+ const pendingFrame = (k) => JSON.stringify({ type: "pending", ...cur.get(k), serverNowMs: Date.now() });
556
569
  if (first) {
557
- for (const frame of curFrames.values())
558
- res.write(`event: pending\ndata: ${frame}\n\n`);
570
+ for (const k of curFrames.keys())
571
+ res.write(`event: pending\ndata: ${pendingFrame(k)}\n\n`);
559
572
  res.write(`event: synced\ndata: ${JSON.stringify({ type: "synced", count: pending.length })}\n\n`);
560
573
  first = false;
561
574
  }
562
575
  else {
563
576
  for (const [k, frame] of curFrames)
564
577
  if (prevFrames.get(k) !== frame)
565
- res.write(`event: pending\ndata: ${frame}\n\n`);
578
+ res.write(`event: pending\ndata: ${pendingFrame(k)}\n\n`);
566
579
  for (const [k, p] of prev)
567
580
  if (!cur.has(k))
568
- res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId })}\n\n`);
581
+ res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId, serverNowMs: Date.now() })}\n\n`);
569
582
  }
570
583
  prev = cur;
571
584
  prevFrames = curFrames;
@@ -9,8 +9,10 @@ import { mcpInjectionHonored } from "../../task-mcp.js";
9
9
  import { sendJson, sendError } from "../send.js";
10
10
  import { resolveStreamApprovalGate } from "../../tool-approval.js";
11
11
  import { projectMemoryEngineCapability } from "../../memory-posture.js";
12
- import { workflowModelAllowlistFor } from "../../task-workflow.js";
12
+ import { workflowModelAllowlistFor, selfOrchestrationDenial } from "../../task-workflow.js";
13
13
  import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
14
+ import { judgeAutoModeArming } from "../../auto-mode-face.js";
15
+ import { coercePermissionModeStrict } from "../../task-settings.js";
14
16
  import { gatedPrincipal } from "../principal-gate.js";
15
17
  function routePairingOf(deps, name, m) {
16
18
  if (isSealedKeyPoison(deps.config.modelApiKeys?.[name]))
@@ -43,6 +45,18 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
43
45
  return;
44
46
  }
45
47
  if (req.method === "GET" && url === "/v1/capabilities") {
48
+ const workflowsEngineCan = deps.workflowsCapable ?? Boolean(deps.workflowRunStore);
49
+ const workflowsDenial = selfOrchestrationDenial({ selfOrchestrationEnabled: deps.config.selfOrchestrationEnabled, requirePrincipal: deps.config.requirePrincipal }, centerEntitlementSourceWired(deps.config));
50
+ const requestedModeRaw = new URL(req.url ?? "", "http://x").searchParams.get("permissionMode");
51
+ const requestedMode = requestedModeRaw === null ? undefined : coercePermissionModeStrict(requestedModeRaw);
52
+ if (requestedModeRaw !== null && requestedMode === undefined) {
53
+ sendError(res, 400, "request.field_invalid", 'permissionMode must be one of "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto"');
54
+ return;
55
+ }
56
+ const entitlementSource = centerEntitlementSourceWired(deps.config);
57
+ const { fault: autoModeFault, ...autoMode } = await judgeAutoModeArming(deps.autoModeFace, { requestedMode, principal: gatedPrincipal(req, deps.config), entitlementSource, localDeny: deps.config.permissionsDisableAutoMode });
58
+ if (autoModeFault !== undefined)
59
+ deps.logger?.warn("auto_mode_face_resolver_fault", { fault: autoModeFault });
46
60
  sendJson(res, 200, {
47
61
  ...deps.capabilities,
48
62
  asyncRuns: Boolean(deps.runStore),
@@ -92,8 +106,10 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
92
106
  modeShellGateTranslation: true,
93
107
  permissionModeAuto: {
94
108
  accepted: true,
95
- classifierSeat: true,
96
- entitlementSource: centerEntitlementSourceWired(deps.config),
109
+ classifierSeat: deps.autoModeFace?.seatMounted === true,
110
+ entitlementSource,
111
+ intentArming: deps.autoModeFace?.engineArmsOnIntent === true,
112
+ ...autoMode,
97
113
  },
98
114
  readFace: deps.config.readFace ?? null,
99
115
  appendSystemPrompt: true,
@@ -141,8 +157,9 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
141
157
  sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
142
158
  s3PublicEndpoint: deps.config.sendUserFile?.publicEndpoint ?? null,
143
159
  sendUserFileLedger: Boolean(deps.sendFileLedger),
144
- workflows: deps.workflowsCapable ?? Boolean(deps.workflowRunStore),
160
+ workflows: workflowsEngineCan && workflowsDenial === null,
145
161
  workflowsList: Boolean(deps.workflowRunStore),
162
+ workflowsGate: { engineCan: workflowsEngineCan, denial: workflowsDenial },
146
163
  workflowModels: (() => {
147
164
  const allow = deps.workflowModelAllowlist
148
165
  ?? workflowModelAllowlistFor({
@@ -99,7 +99,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
99
99
  detail?: string | undefined;
100
100
  eventId: string;
101
101
  at: number;
102
- to: "failed" | "done";
102
+ to: "done" | "failed";
103
103
  requestId: string;
104
104
  }[];
105
105
  custodyBytes: number;
@@ -107,7 +107,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
107
107
  requestId: string;
108
108
  reason: string;
109
109
  at: number;
110
- status: "pending" | "failed" | "done";
110
+ status: "pending" | "done" | "failed";
111
111
  originUnknownKeys?: string[] | undefined;
112
112
  clearanceId: string;
113
113
  entryId: string;
@@ -6,6 +6,7 @@ import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, seri
6
6
  import { CONTROL_AND_BIDI_CHARS } from "../../text-bidi.js";
7
7
  import { HttpError } from "../../security.js";
8
8
  import { MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
9
+ import { PERSISTED_RULE_MATCHES, PERSISTED_RULE_TOOLS } from "../../permission-rule-vocab.js";
9
10
  const logger = createLogger();
10
11
  export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
11
12
  export const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
@@ -45,8 +46,8 @@ const LocalImportRowSchema = z
45
46
  .object({
46
47
  rule: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
47
48
  scope: z.string().min(1).max(MAX_RULE_SCOPE_CHARS),
48
- tool: z.literal("Bash"),
49
- match: z.enum(["exact", "prefix"]),
49
+ tool: z.enum(PERSISTED_RULE_TOOLS),
50
+ match: z.enum(PERSISTED_RULE_MATCHES),
50
51
  command: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
51
52
  adds: z.array(LocalImportAddSchema).max(64),
52
53
  })
@@ -1,17 +1,3 @@
1
- /**
2
- * [ref] A9 域模块 —— 异步 run 面与它的全部动词。
3
- *
4
- * 路由:`POST /v1/runs`(异步提交)· `GET /v1/runs/:id[/events]`(轮询 / 可恢复 SSE)· 运行期动词
5
- * `cancel` / `steer` / `compact` / `detach` · 子代理面 `subagents/:target/{steer,resume,output,stream}` ·
6
- * 通用任务句柄 `tasks/:handle/{output,stop}` · 三条 HITL 应答口 `elicitations|questions|tool-approvals/:id/respond`。
7
- *
8
- * 两段搬运:提交+run 级动词一段,子代理/任务句柄/HITL 应答一段——它们在 `handle()` 里原本被 SVC-5 的
9
- * workflow steer 隔开,拆分保持各自位置(本文件里的先后只是同域归并)。`POST /v1/runs` 与 tasks 域共用
10
- * `ctx.legs.prepareSpec`(同一条读体/校验/建 spec 的前段)。
11
- *
12
- * 可变状态全部来自跨域 registry(`idemCache` 幂等、`inflightRuns`/`preemptableRuns` 中止句柄、
13
- * `cancelledViaVerb` first-writer-wins 判别、`steerableRuns` 活流句柄、`wakeParkMints` park 铸造闩)。
14
- */
15
1
  import type { IncomingMessage, ServerResponse } from "node:http";
16
2
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
17
3
  import type { RouteCtx, PreparedTaskSubmission } from "../route-ctx.js";
@@ -1,3 +1,4 @@
1
+ import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventData, redactTerminalResult } from "../../observability/run-terminal-log.js";
1
2
  import { z } from "zod";
2
3
  import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer } from "@sema-agent/core";
3
4
  import { isTerminalRunStatus, isParkedRunStatus } from "../../plugins/store-contracts.js";
@@ -22,10 +23,12 @@ import { normalizeRunEventType } from "../../trace/project.js";
22
23
  import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
23
24
  import { buildActiveRunConflict } from "../active-run-conflict.js";
24
25
  import { clearTurnActivity, readTurnActivityMs } from "../../turn-activity.js";
26
+ import { buildCancelContext } from "../../run-cancel-context.js";
25
27
  import { buildCrossSliceUsage, readResourceWindow, recordResourceWindow } from "../../resource-window.js";
26
28
  import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
27
29
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
28
30
  import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
31
+ import { withPrincipal } from "../../observability/principal-context.js";
29
32
  export const RUN_ID_RE = /^\/v1\/runs\/([^/]+)(\/events)?$/;
30
33
  export const RUN_CANCEL_RE = /^\/v1\/runs\/([^/]+)\/cancel$/;
31
34
  export const RUN_STEER_RE = /^\/v1\/runs\/([^/]+)\/steer$/;
@@ -136,7 +139,10 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
136
139
  retainedFrom: (id) => runStore.retainedFrom(id),
137
140
  formatEvent: (ev) => {
138
141
  const type = normalizeRunEventType(ev.type);
139
- return { id: ev.seq, event: type, data: { type, ...(ev.data ?? {}) } };
142
+ const data = type === "done" && ev.data && typeof ev.data === "object" && ev.data.result && typeof ev.data.result === "object"
143
+ ? { ...ev.data, result: memoRedactTerminalResult("ev.done.result", ev.data.result) }
144
+ : type === "failed" ? memoRedactedRead("ev.failed", ev.data, (d) => redactLedgerEventData(type, d)) : redactLedgerEventData(type, ev.data);
145
+ return { id: ev.seq, event: type, data: { type, ...(data ?? {}) } };
140
146
  },
141
147
  staleFrame: () => ({ event: "failed", data: { type: "failed", errorMessage: "run stalled (instance lost?)" } }),
142
148
  }, taskId, staleMs);
@@ -251,11 +257,11 @@ async function handleRunsBody(req, res, url, ctx, miss) {
251
257
  status: stale ? "failed" : run.status,
252
258
  ...(lastActivityAt !== undefined ? { msSinceLastActivity: Math.max(0, Date.now() - lastActivityAt) } : {}),
253
259
  ...(crossSliceUsage !== undefined ? { crossSliceUsage } : {}),
254
- result: run.result ?? undefined,
260
+ result: isTerminalRunStatus(run.status) ? memoRedactTerminalResult("run.result", run.result ?? undefined) : redactTerminalResult(run.result ?? undefined),
255
261
  supervisorCost,
256
262
  suggestions,
257
263
  errorCode: stale ? undefined : (run.result?.errorCode ?? undefined),
258
- error: stale ? "run stalled (instance lost?)" : run.error ?? undefined,
264
+ error: stale ? "run stalled (instance lost?)" : isTerminalRunStatus(run.status) ? memoRedactedRead("run.error", run.error ?? undefined, redactErrorMessage) : redactErrorMessage(run.error ?? undefined),
259
265
  jobId: run.jobId ?? undefined,
260
266
  source: run.source ?? undefined,
261
267
  });
@@ -324,7 +330,8 @@ async function handleRunsBody(req, res, url, ctx, miss) {
324
330
  }
325
331
  const err = note;
326
332
  try {
327
- const result = { taskId, sessionId: run.sessionId, status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 } };
333
+ const cancelContext = buildCancelContext(taskId, Date.parse(run.createdAt));
334
+ const result = { taskId, sessionId: run.sessionId, status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 }, ...(cancelContext ? { cancelContext } : {}) };
328
335
  let lastErr;
329
336
  for (let attempt = 0;; attempt++) {
330
337
  try {
@@ -870,7 +877,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
870
877
  deps.logger?.[level]?.("manual_compact_outcome", { taskId, outcome, note: outcome === "compacted" ? "a compacted{trigger:'manual'} event rode the run stream" : `no event (see compaction.${outcome === "mooted" ? "mooted" : outcome} trace / task terminal events)` });
871
878
  }, (e) => {
872
879
  if (e.code !== "steering.not_running") {
873
- deps.logger?.warn?.("manual_compact_failed", { taskId, err: e instanceof Error ? e.message : String(e) });
880
+ deps.logger?.warn?.("manual_compact_failed", { taskId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
874
881
  }
875
882
  else {
876
883
  deps.logger?.info?.("manual_compact_not_running", { taskId, note: "stream settled between lookup and compact() — benign unless systematic" });
@@ -934,7 +941,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
934
941
  }
935
942
  async function handleRunVerbsBody(req, res, url, ctx, miss) {
936
943
  const { deps } = ctx;
937
- const { readJson, rateLimited, quotaExceeded, leaseDenied, safeDecode, runSessionAcceptOk } = ctx.helpers;
944
+ const { readJson, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, safeDecode, runSessionAcceptOk } = ctx.helpers;
938
945
  const subOutputMatch = req.method === "GET" ? RUN_SUBAGENT_OUTPUT_RE.exec(url) : null;
939
946
  if (subOutputMatch) {
940
947
  if (!deps.runStore || !deps.subagentTaskOutput) {
@@ -1159,7 +1166,12 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1159
1166
  send409("resume.retain_off", "the parent run did not retain sub-agent sessions (set retainSubagentSessions on the run to enable revival)");
1160
1167
  return;
1161
1168
  }
1162
- const marker = await handle.resume(redacted);
1169
+ if (ownerQuotaDenied(res, run.owner ?? undefined))
1170
+ return;
1171
+ if (await ownerLeaseDenied(res, run.owner ?? undefined))
1172
+ return;
1173
+ const resumeFn = handle.resume;
1174
+ const marker = await withPrincipal(run.owner ?? undefined, () => resumeFn(redacted));
1163
1175
  sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", marker, note: `Message queued for delivery to ${handle.agentName ?? target}; it will continue in the background.` });
1164
1176
  return;
1165
1177
  }
@@ -1593,7 +1605,7 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1593
1605
  backend: deps.backend,
1594
1606
  parkFacility: deps.checkpointStore !== undefined,
1595
1607
  }).active,
1596
- windowMs: deps.config.streamApproval?.windowMs ?? 0,
1608
+ windowMs: deps.config.streamApproval?.windowMs,
1597
1609
  windowMarginMs: deps.config.streamAskWindowMarginMs,
1598
1610
  }
1599
1611
  : undefined, deps.checkpointStore);