@sema-agent/server 7.56.0 → 7.57.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 (110) 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/config-catalog.js +14 -5
  30. package/dist/config-types.d.ts +41 -10
  31. package/dist/config.d.ts +21 -0
  32. package/dist/config.js +40 -5
  33. package/dist/http/route-ctx.d.ts +75 -0
  34. package/dist/http/routes/a2a-serve.js +5 -3
  35. package/dist/http/routes/approvals-assistant.js +19 -6
  36. package/dist/http/routes/capabilities.js +21 -4
  37. package/dist/http/routes/memory-origin.d.ts +2 -2
  38. package/dist/http/routes/rules.js +3 -2
  39. package/dist/http/routes/runs.d.ts +0 -14
  40. package/dist/http/routes/runs.js +20 -8
  41. package/dist/http/routes/tasks.js +34 -8
  42. package/dist/http/routes/workflows.js +19 -4
  43. package/dist/http/server.d.ts +11 -1
  44. package/dist/http/server.js +215 -43
  45. package/dist/http/wire-types.d.ts +9 -4
  46. package/dist/leader/diffout.js +2 -1
  47. package/dist/leader/endpoint.js +45 -12
  48. package/dist/leader/fanout.js +6 -5
  49. package/dist/leader/leader.js +17 -14
  50. package/dist/leader/merge.js +17 -12
  51. package/dist/leader/planner.js +3 -2
  52. package/dist/leader/repair-oracle.js +6 -4
  53. package/dist/leader/repair-wire.js +5 -3
  54. package/dist/leader/wire.d.ts +30 -1
  55. package/dist/leader/wire.js +36 -19
  56. package/dist/main.js +43 -7
  57. package/dist/observability/err-text.d.ts +6 -0
  58. package/dist/observability/err-text.js +10 -0
  59. package/dist/observability/fail-open.d.ts +40 -0
  60. package/dist/observability/fail-open.js +19 -0
  61. package/dist/observability/metrics.js +1 -1
  62. package/dist/observability/run-terminal-log.d.ts +120 -0
  63. package/dist/observability/run-terminal-log.js +360 -0
  64. package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
  65. package/dist/orchestration/workflow-completion-inbox.js +8 -0
  66. package/dist/permission-rule-vocab.d.ts +40 -0
  67. package/dist/permission-rule-vocab.js +17 -0
  68. package/dist/plugins/checkpoint-store-sql.d.ts +14 -0
  69. package/dist/plugins/checkpoint-store-sql.js +19 -3
  70. package/dist/plugins/file-run-store.d.ts +3 -34
  71. package/dist/plugins/file-run-store.js +19 -0
  72. package/dist/plugins/local-checkpoint-store.d.ts +10 -0
  73. package/dist/plugins/local-checkpoint-store.js +3 -0
  74. package/dist/plugins/memory-run-store.d.ts +3 -15
  75. package/dist/plugins/memory-run-store.js +19 -0
  76. package/dist/plugins/permission-rule-store-file.js +8 -3
  77. package/dist/plugins/permission-rule-store-sql.js +16 -10
  78. package/dist/plugins/remote-scratchpad.js +3 -2
  79. package/dist/plugins/run-store-sql.d.ts +3 -42
  80. package/dist/plugins/run-store-sql.js +30 -1
  81. package/dist/plugins/store-backend.d.ts +1 -1
  82. package/dist/plugins/store-contracts.d.ts +66 -1
  83. package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
  84. package/dist/plugins/workflow-run-store-sql.js +10 -2
  85. package/dist/rules-consent.js +20 -13
  86. package/dist/run-cancel-context.d.ts +13 -0
  87. package/dist/run-cancel-context.js +15 -0
  88. package/dist/run-local.js +2 -2
  89. package/dist/runs.d.ts +4 -1
  90. package/dist/runs.js +41 -10
  91. package/dist/runtime-caps-resolver.d.ts +133 -17
  92. package/dist/runtime-caps-resolver.js +41 -3
  93. package/dist/task-settings.d.ts +57 -3
  94. package/dist/task-settings.js +78 -5
  95. package/dist/task-workflow.d.ts +32 -2
  96. package/dist/task-workflow.js +8 -3
  97. package/dist/tool-approval.d.ts +26 -3
  98. package/dist/tool-approval.js +96 -13
  99. package/dist/trace/core-keyset-guard.d.ts +2 -2
  100. package/dist/trace/ledger-sink.d.ts +13 -4
  101. package/dist/trace/ledger-sink.js +14 -6
  102. package/dist/trace/project.d.ts +13 -0
  103. package/dist/trace/project.js +10 -1
  104. package/dist/trace/redact.d.ts +22 -11
  105. package/dist/trace/redact.js +655 -20
  106. package/dist/trace/sema-provenance.d.ts +26 -0
  107. package/dist/trace/sema-provenance.js +11 -0
  108. package/dist/turn-activity.d.ts +32 -2
  109. package/dist/turn-activity.js +29 -4
  110. package/package.json +2 -2
@@ -3,12 +3,18 @@ import { findCcRuleFormNames, findUnmatchableToolNames, formatUnmatchableToolNam
3
3
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
4
4
  import { parseHooksConfig } from "./hooks/hook-runner.js";
5
5
  export function coercePermissionMode(raw) {
6
- if (raw === "plan" || raw === "acceptEdits" || raw === "default" || raw === "bypassPermissions" || raw === "auto")
7
- return raw;
6
+ const strict = coercePermissionModeStrict(raw);
7
+ if (strict !== undefined)
8
+ return strict;
8
9
  if (typeof raw === "string" && raw.length > 0)
9
10
  return "default";
10
11
  return undefined;
11
12
  }
13
+ export function coercePermissionModeStrict(raw) {
14
+ if (raw === "plan" || raw === "acceptEdits" || raw === "default" || raw === "bypassPermissions" || raw === "auto")
15
+ return raw;
16
+ return undefined;
17
+ }
12
18
  export function effectiveThinking(reasoningEffort, ultracode) {
13
19
  const explicit = isThinkingLevel(reasoningEffort) ? reasoningEffort : undefined;
14
20
  if (!ultracode)
@@ -21,7 +27,11 @@ export function withPermissionMode(settings, mode) {
21
27
  return { ...base, permissions: { ...base.permissions, defaultMode: mode } };
22
28
  }
23
29
  export function effectivePermissionMode(body, settings) {
24
- return coercePermissionMode(body.permissionMode) ?? settings?.permissions?.defaultMode;
30
+ const requested = coercePermissionMode(body.permissionMode) ?? settings?.permissions?.defaultMode;
31
+ const kill = settings?.permissions?.disableAutoMode;
32
+ if (requested === "auto" && (kill === "disable" || kill === true))
33
+ return "default";
34
+ return requested;
25
35
  }
26
36
  export function shellGateForMode(effMode) {
27
37
  switch (effMode) {
@@ -62,6 +72,61 @@ function cleanStringArray(v) {
62
72
  const out = v.filter((x) => typeof x === "string" && x.length > 0).slice(0, MAX_SETTINGS_PERMISSION_RULES);
63
73
  return out.length > 0 ? out : undefined;
64
74
  }
75
+ export const TASK_SETTINGS_KEYS = ["env", "hooks", "model", "outputStyle", "permissions", "ultracode", "webSearch"];
76
+ export const TASK_SETTINGS_PERMISSION_KEYS = ["allow", "ask", "defaultMode", "deny", "disableAutoMode"];
77
+ export const TASK_SETTINGS_UNSUPPORTED_KEYS = ["apiKeyHelper", "sandbox", "statusLine"];
78
+ export const TASK_SETTINGS_UNSUPPORTED_PERMISSION_KEYS = ["additionalDirectories", "disableBypassPermissionsMode"];
79
+ export const MAX_SETTINGS_KEY_ECHO = 20;
80
+ const MAX_SETTINGS_KEY_ECHO_CHARS = 64;
81
+ const ACCEPTED_TOP = new Set(TASK_SETTINGS_KEYS);
82
+ const ACCEPTED_PERM = new Set(TASK_SETTINGS_PERMISSION_KEYS);
83
+ const UNSUPPORTED_TOP = new Set(TASK_SETTINGS_UNSUPPORTED_KEYS);
84
+ const UNSUPPORTED_PERM = new Set(TASK_SETTINGS_UNSUPPORTED_PERMISSION_KEYS);
85
+ function echoSettingsKey(prefix, key) {
86
+ const clean = key.replace(CONTROL_AND_BIDI_STRIP_RE, "");
87
+ return `${prefix}${clean.length > MAX_SETTINGS_KEY_ECHO_CHARS ? `${clean.slice(0, MAX_SETTINGS_KEY_ECHO_CHARS)}…` : clean}`;
88
+ }
89
+ export function taskSettingsKeyIssue(raw) {
90
+ if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
91
+ return null;
92
+ const unknown = [];
93
+ const unsupported = [];
94
+ const scan = (bag, prefix, accepted, tolerated) => {
95
+ for (const k of Object.keys(bag)) {
96
+ if (accepted.has(k))
97
+ continue;
98
+ (tolerated.has(k) ? unsupported : unknown).push(echoSettingsKey(prefix, k));
99
+ }
100
+ };
101
+ scan(raw, "settings.", ACCEPTED_TOP, UNSUPPORTED_TOP);
102
+ const perms = raw.permissions;
103
+ if (perms !== null && typeof perms === "object" && !Array.isArray(perms)) {
104
+ scan(perms, "settings.permissions.", ACCEPTED_PERM, UNSUPPORTED_PERM);
105
+ }
106
+ if (unknown.length === 0 && unsupported.length === 0)
107
+ return null;
108
+ unknown.sort();
109
+ unsupported.sort();
110
+ const clause = (names, label) => {
111
+ const shown = names.slice(0, MAX_SETTINGS_KEY_ECHO);
112
+ const more = names.length - shown.length;
113
+ return `${label}: ${shown.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`;
114
+ };
115
+ const parts = [
116
+ ...(unknown.length > 0 ? [clause(unknown, "unknown (not a settings key here — check the spelling)")] : []),
117
+ ...(unsupported.length > 0 ? [clause(unsupported, "unsupported (a CC settings key this service does not wire)")] : []),
118
+ ];
119
+ const dirHint = unsupported.some((n) => n.endsWith(".additionalDirectories"))
120
+ ? " Extra directories ride the top-level body.additionalDirectories instead."
121
+ : "";
122
+ return {
123
+ message: `settings: unrecognized key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller. ` +
124
+ `${parts.join("; ")}.${dirHint} ` +
125
+ `Accepted: settings.{${[...TASK_SETTINGS_KEYS].join(",")}} and settings.permissions.{${[...TASK_SETTINGS_PERMISSION_KEYS].join(",")}}`,
126
+ unknownKeys: unknown.slice(0, MAX_SETTINGS_KEY_ECHO),
127
+ unsupportedKeys: unsupported.slice(0, MAX_SETTINGS_KEY_ECHO),
128
+ };
129
+ }
65
130
  export function parseTaskSettings(raw) {
66
131
  if (raw === null || raw === undefined || typeof raw !== "object")
67
132
  return { deferred: [] };
@@ -90,8 +155,16 @@ export function parseTaskSettings(raw) {
90
155
  const ask = cleanStringArray(p.ask);
91
156
  const mode = p.defaultMode;
92
157
  const defaultMode = mode === "default" || mode === "acceptEdits" || mode === "plan" || mode === "bypassPermissions" || mode === "auto" ? mode : undefined;
93
- if (allow || deny || ask || defaultMode) {
94
- out.permissions = { ...(allow ? { allow } : {}), ...(deny ? { deny } : {}), ...(ask ? { ask } : {}), ...(defaultMode ? { defaultMode } : {}) };
158
+ const rawKill = p.disableAutoMode;
159
+ const disableAutoMode = rawKill === "disable" || rawKill === true || rawKill === false ? rawKill : undefined;
160
+ if (allow || deny || ask || defaultMode || disableAutoMode !== undefined) {
161
+ out.permissions = {
162
+ ...(allow ? { allow } : {}),
163
+ ...(deny ? { deny } : {}),
164
+ ...(ask ? { ask } : {}),
165
+ ...(defaultMode ? { defaultMode } : {}),
166
+ ...(disableAutoMode !== undefined ? { disableAutoMode } : {}),
167
+ };
95
168
  }
96
169
  }
97
170
  if (typeof s.model === "string" && s.model.length > 0)
@@ -15,13 +15,43 @@
15
15
  * source must NOT leak the LLM-authored workflow engine). WITH a resolver, core enforces `allowWorkflows`
16
16
  * per-principal (deny → fail-closed). Single-user (owns its own worker) honors directly. Three-gate: engine-can ∧
17
17
  * center-may(allowWorkflows) ∧ shell-show.
18
+ *
19
+ * 🔴 [ref](2026-09-03):fail-closed 的**方向**从来是对的,错的是它**没人说**。多租户 + 无 entitlement 源
20
+ * 的部署上,这条腿把 `body.selfOrchestration:true`(以及 `settings.ultracode` 展开出来的同一个意图)静默
21
+ * 折成 `undefined`,而 `/v1/capabilities` 照报 `workflows:true` ⇒ 「壳显示支持、引擎按普通 run 跑」。修法
22
+ * 是把部署级半场抽成下面的 {@link selfOrchestrationDenial}(能力位与准入**同一只**谓词),提交面在拒因
23
+ * 在场时**响亮**拒(`routes` 的 501 `capability.self_orchestration_required`),不再静默折。
18
24
  */
19
25
  export declare function selfOrchestrationFromBody(body: {
20
26
  selfOrchestration?: unknown;
21
- }, config: {
27
+ }, config: SelfOrchestrationGateConfig, hasEntitlementResolver: boolean): true | undefined;
28
+ /** 本模块读的部署面(`selfOrchestrationEnabled` 恒在真 config 上;遗留双重断言 stub 可能真缺,故一律 `!== true` 读)。 */
29
+ export interface SelfOrchestrationGateConfig {
22
30
  selfOrchestrationEnabled: boolean;
23
31
  requirePrincipal?: boolean;
24
- }, hasEntitlementResolver: boolean): true | undefined;
32
+ }
33
+ /**
34
+ * [ref](DEBTS,P1)—— 自编排准入的**部署级拒因**闭集。`null` = 本部署这一层不拒(是否真跑还看 engine-can
35
+ * 与 core 的 per-principal `allowWorkflows`)。
36
+ *
37
+ * 唯一成员 `"entitlement_resolver_absent"`:多租户(`requirePrincipal === true`)+ 运维**显式开了**
38
+ * `SELF_ORCHESTRATION_ENABLED` + 没有任何 per-principal entitlement 源(`hasEntitlementResolver` —— 判据由
39
+ * 调用方从 `centerEntitlementSourceWired(config)` / 装配好的 caps 客户端取)。这一形下上面的准入腿对
40
+ * **每一个** principal 都 fail-closed(理由见函数上方注:core 的 `allowWorkflows` 是 tighten-only,
41
+ * resolver 缺席 = 无限制,所以本层必须自己关死),于是这台机器上的自编排是**结构性死的**。
42
+ *
43
+ * 🔴 为什么这不是布尔而是**闭集拒因**:能力面要能对壳说清「为什么暗」——「这份二进制/这台机器没有工作流
44
+ * 引擎」与「引擎在、但本部署配不出 per-principal 授权」是两件事,壳对它们要做的事不同(前者别给这一格;
45
+ * 后者该提示运维接 center)。合成一个布尔就把这两件事打成同一个 false —— 与 `permissionModeAuto` 那一位
46
+ * 「三项不合成一个布尔」同判据。
47
+ *
48
+ * 🔴 为什么 `selfOrchestrationEnabled` 关着时返回 `null` 而不是另一个拒因:那是**运维刻意的姿态**
49
+ * (多租户缺省就是关),能力面的 engine-can 半场自己就已经诚实为假;而本拒因说的是**半配置**
50
+ * (阀门开着而配不出来),两者的处置不同——后者才需要点名。这也正是本件的补偿旋钮:把
51
+ * `SELF_ORCHESTRATION_ENABLED` 关掉(对这台机器而言是唯一诚实的姿态)即刻回到姿态形。
52
+ */
53
+ export type SelfOrchestrationDenial = "entitlement_resolver_absent";
54
+ export declare function selfOrchestrationDenial(config: SelfOrchestrationGateConfig, hasEntitlementResolver: boolean): SelfOrchestrationDenial | null;
25
55
  /**
26
56
  * §4 (Fork) — map the per-task `body.enableFork` intent onto `TaskSpec.enableFork`. core 1.257 ([ref] BREAKING)
27
57
  * RETIRED the standalone `Fork` tool; a fork is now `Agent(subagent_type:"fork")`, and `enableFork` is one of the two
@@ -1,9 +1,14 @@
1
1
  export function selfOrchestrationFromBody(body, config, hasEntitlementResolver) {
2
- if (body.selfOrchestration !== true || !config.selfOrchestrationEnabled)
2
+ if (body.selfOrchestration !== true || config.selfOrchestrationEnabled !== true)
3
3
  return undefined;
4
+ return selfOrchestrationDenial(config, hasEntitlementResolver) === null ? true : undefined;
5
+ }
6
+ export function selfOrchestrationDenial(config, hasEntitlementResolver) {
7
+ if (config.selfOrchestrationEnabled !== true)
8
+ return null;
4
9
  if (config.requirePrincipal !== true)
5
- return true;
6
- return hasEntitlementResolver ? true : undefined;
10
+ return null;
11
+ return hasEntitlementResolver ? null : "entitlement_resolver_absent";
7
12
  }
8
13
  export function enableForkFromBody(body, config, hasEntitlementResolver) {
9
14
  if (!config.forkEnabled)
@@ -541,6 +541,10 @@ export declare function resolveStreamApprovalGate(input: StreamApprovalGateInput
541
541
  * `prepare-task.js` 的 durableMandate/contentMandate(:1415/:1417)与 durableQuestionFace(:2136)等),
542
542
  * 故不在本函数内。
543
543
  *
544
+ * 🔴 **[ref](半接线旋钮族)**:`windowZero` 的第一源(`STREAM_ASK_WINDOW_MS=0`)自此**不再挂在
545
+ * `streamApprovalOn` 后面** —— 那是部署级旋钮,它的语义不该因为这台机器有没有 durable ask 账而改变
546
+ * (`ApprovalLegAssembly.windowZero` 顶注写了那条病)。第三源(贴 deadline)照旧只在协议上场时参与。
547
+ *
544
548
  * **`windowZero` 的两件事必须同时做**(调用方契约):不包 ALS **且**注入 immediate-unavailable 闭包。
545
549
  * 只做一件另一条路仍会呈卡。⚠️ [ref] 起**三腿都有** per-task `spec.onAsk` 装配点(sync=routes/tasks.ts,
546
550
  * bg=runs.ts,resume=server.ts driveResumeLeg),windowZero 的第二件在三腿都是显式闭包 —— 旧文「bg/
@@ -554,7 +558,14 @@ export interface ApprovalLegAssembly {
554
558
  * 同 (owner, session) 活流呈活卡(sync 腿 [ref] 起本就如此;bg/resume 的这条行为差 = [ref] CHANGELOG
555
559
  * 披露的行为面变更)。旧文「本腿逐字保持协议之前的行为」只对协议件成立,别再把它读成「协议关 = 零呈卡」。 */
556
560
  active: boolean;
557
- /** §8.3 窗=0 恒 park。`active` 为假时恒 false(协议都没上场,谈不上关窗) */
561
+ /** §8.3 窗=0 恒 park。
562
+ * 🔴 **[ref] 起它与 `active` 解耦**(旧文「`active` 为假时恒 false —— 协议都没上场,谈不上关窗」已
563
+ * 作废):`STREAM_ASK_WINDOW_MS` 是**部署级**旋钮,`0` 的语义(运维显式关窗 ⇒ 恒走
564
+ * {@link ToolApprovalCoordinator.unattendedAskOutcome},连卡都不发)在两条车道上必须是同一个意思。
565
+ * 旧形下 local 车道(`volatile_ask_ledger`)/ 无 park 设施 / 显式关协议的部署配了 `0`,拿到的是一张
566
+ * 等满 5min 的活卡 —— 与它自己的域注正好相反,且没有任何一行说出来。
567
+ * ⚠️ 「窗**缺席**」(`windowMs: undefined` = 这份装配根本没有 streamApproval 段,只出现在 stub-harness)
568
+ * **不是**关窗:那时逐字保持活卡腿。 */
558
569
  windowZero: boolean;
559
570
  /** §7.3 本 leg 的 walltime deadline(`performance.now()` 单调基)。只在 `active ∧ ¬windowZero ∧
560
571
  * 有 walltime 墙` 时在场 —— 关着时供值会把既有 5min 活卡窗按三元公式缩短(§7.2 真行为变更)。 */
@@ -583,8 +594,13 @@ export declare function createApprovalCardEmitter(sinks: {
583
594
  export declare function resolveApprovalLeg(input: {
584
595
  /** = `resolveStreamApprovalGate(...).active`(协议在本 worker 上场没有)。 */
585
596
  streamApprovalOn: boolean;
586
- /** `config.streamApproval.windowMs`(`STREAM_ASK_WINDOW_MS`)。 */
587
- windowMs: number;
597
+ /** `config.streamApproval.windowMs`(`STREAM_ASK_WINDOW_MS`)。
598
+ * 🔴 [ref]:`undefined` = **这份装配没有 `streamApproval` 段**(只出现在只填被测键的 stub-harness ——
599
+ * 真实 boot 无条件铸该段,见 `config.ts`)。此前三个调用点各写 `?? 0` 把这一形折成「0」,在旧语义下
600
+ * 是死代码(`windowZero` 那时挂在 `streamApprovalOn` 后面,协议关就恒 false);[ref] 让 `0` 在协议关的
601
+ * 腿上也真生效之后,那个折算会把一台**什么都没配**的 harness 判成「运维显式关窗」。故本字段收成
602
+ * `number | undefined`,由每个调用点如实交出「有没有这个值」,不许再就地编一个。 */
603
+ windowMs: number | undefined;
588
604
  /** `config.streamAskWindowMarginMs`(`STREAM_ASK_WINDOW_MARGIN_MS`)。 */
589
605
  windowMarginMs: number;
590
606
  /** `spec.limits.maxWalltimeMs`。缺席 = 没有 walltime 墙 ⇒ 第三源不参与(**缺席 ≠ 0**)。 */
@@ -802,6 +818,8 @@ export declare class ToolApprovalCoordinator {
802
818
  /** 两份候选是否**逐条逐键**相等(顺序即展示序 ⇒ 顺序敏感)。两侧都缺席 = 相等;一侧缺席 = 不等。
803
819
  * 用在幂等重入的「行 = 真源」对账上(见 `ensureAsk` 那段撤回臂)。 */
804
820
  private static ruleOffersEqual;
821
+ private static batchMemberEqual;
822
+ private static uncoveredDetailEqual;
805
823
  /**
806
824
  * [ref] / 5.58 复审 F-1 —— OFFER 的**文本座尺**(红先修)。
807
825
  *
@@ -824,6 +842,11 @@ export declare class ToolApprovalCoordinator {
824
842
  * 位置在**素材铸点**(与基数帽同一处):两族帧 + `card_json` + 回决口的等式左边从此拿的是**同一份**
825
843
  * 已截素材,而不是两处各截一遍。
826
844
  */
845
+ /** 兑付侧座(single 的 rule/command、command 成员的 rule/command、directoryRead 成员的 rule/directory)任一超尺?
846
+ * `segment` 与 `uncoveredDetail[].segment` 是纯渲染座,不在此列(截长是诚实省略)。 */
847
+ private static ruleOfferTextsOverSize;
848
+ /** codex r1 [high]([ref]):offer 的自称座与 `parseAllowRuleText(rule)` 派生的三元组逐字一致?(理由见调用点) */
849
+ private static ruleOfferDerivedFactsAgree;
827
850
  private static boundRuleOfferTexts;
828
851
  private buildRuleLaneMaterial;
829
852
  /** [ref]([ref]/[ref] 双属主裁定):把某 wire run 的**流内未决 ask** 立即转 durable park——断连支专用。
@@ -1,6 +1,6 @@
1
1
  import { contentKindOf } from "./approval-content-kind.js";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
- import { uuidv7, MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
3
+ import { uuidv7, MAX_RULE_TEXT_CHARS, parseAllowRuleText } from "@sema-agent/core";
4
4
  import { precheckCardRuleText } from "./rules-consent.js";
5
5
  import { redactDeep, redactSecrets } from "./trace/redact.js";
6
6
  import { hasBidiControls } from "./text-bidi.js";
@@ -8,7 +8,7 @@ import { approvalPayloadFingerprint } from "./plugins/approval-payload-fingerpri
8
8
  import { createLogger } from "./observability/logger.js";
9
9
  import { recordFailOpen } from "./observability/fail-open.js";
10
10
  import { deriveAskId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
11
- import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readProbeCause, readRuleEvidence, readRuleOffersAbsence, } from "./approval-card.js";
11
+ import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readProbeCause, readRuleEvidence, readRuleOffersAbsence, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
12
12
  import { projectAskTerminalClaimTruth } from "./plugins/approval-ask-store-sql.js";
13
13
  import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
14
14
  const defaultLogger = createLogger();
@@ -195,7 +195,7 @@ export function createApprovalCardEmitter(sinks) {
195
195
  }
196
196
  export function resolveApprovalLeg(input) {
197
197
  if (!input.streamApprovalOn)
198
- return { active: false, windowZero: false };
198
+ return { active: false, windowZero: input.windowMs === 0 };
199
199
  const tightDeadline = input.legWalltimeMs !== undefined && input.legWalltimeMs - input.windowMarginMs <= 0;
200
200
  if (input.windowMs === 0 || tightDeadline)
201
201
  return { active: true, windowZero: true };
@@ -344,32 +344,115 @@ export class ToolApprovalCoordinator {
344
344
  if (x.kind === "batch" && y.kind === "batch") {
345
345
  return (x.uncoveredSegments === y.uncoveredSegments &&
346
346
  x.rules.length === y.rules.length &&
347
- x.rules.every((r, j) => r.rule === y.rules[j].rule && r.match === y.rules[j].match && r.command === y.rules[j].command && r.segment === y.rules[j].segment));
347
+ x.rules.every((r, j) => ToolApprovalCoordinator.batchMemberEqual(r, y.rules[j])) &&
348
+ ToolApprovalCoordinator.uncoveredDetailEqual(x.uncoveredDetail, y.uncoveredDetail));
348
349
  }
349
350
  return false;
350
351
  });
351
352
  }
353
+ static batchMemberEqual(r, s) {
354
+ if (r.kind !== s.kind)
355
+ return false;
356
+ if (r.kind === "command" && s.kind === "command")
357
+ return r.rule === s.rule && r.match === s.match && r.command === s.command && r.segment === s.segment;
358
+ if (r.kind === "directoryRead" && s.kind === "directoryRead")
359
+ return r.rule === s.rule && r.directory === s.directory && r.segment === s.segment;
360
+ return false;
361
+ }
362
+ static uncoveredDetailEqual(a, b) {
363
+ if (a === undefined || b === undefined)
364
+ return a === b;
365
+ return a.length === b.length && a.every((d, i) => d.segment === b[i].segment && d.reason === b[i].reason);
366
+ }
367
+ static ruleOfferTextsOverSize(offer) {
368
+ const over = (s) => s.length > MAX_RULE_TEXT_CHARS;
369
+ if (offer.kind === "single")
370
+ return over(offer.rule) || over(offer.command);
371
+ return offer.rules.some((r) => {
372
+ switch (r.kind) {
373
+ case "command":
374
+ return over(r.rule) || over(r.command);
375
+ case "directoryRead":
376
+ return over(r.rule) || over(r.directory);
377
+ default:
378
+ throw new Error(`unknown rule-offer batch member kind: ${JSON.stringify(r)}`);
379
+ }
380
+ });
381
+ }
382
+ static ruleOfferDerivedFactsAgree(offer) {
383
+ const derive = (rule) => {
384
+ const p = parseAllowRuleText(rule);
385
+ return "rule" in p ? p.rule : undefined;
386
+ };
387
+ if (offer.kind === "single") {
388
+ const d = derive(offer.rule);
389
+ return d !== undefined && d.rule === offer.rule && d.tool === "Bash" && d.match === offer.match && d.command === offer.command;
390
+ }
391
+ return offer.rules.every((m) => {
392
+ const d = derive(m.rule);
393
+ if (d === undefined || d.rule !== m.rule)
394
+ return false;
395
+ switch (m.kind) {
396
+ case "command":
397
+ return d.tool === "Bash" && d.match === m.match && d.command === m.command;
398
+ case "directoryRead":
399
+ return d.tool === "Read" && d.match === "subpath" && d.command === m.directory;
400
+ default:
401
+ throw new Error(`unknown rule-offer batch member kind: ${JSON.stringify(m)}`);
402
+ }
403
+ });
404
+ }
352
405
  static boundRuleOfferTexts(offers) {
353
406
  const over = (s) => s.length > MAX_RULE_TEXT_CHARS;
407
+ const clipSegment = (s) => (over(s) ? s.slice(0, MAX_RULE_TEXT_CHARS) : s);
354
408
  const out = [];
355
- for (const offer of offers) {
409
+ for (const raw of offers) {
410
+ const parsed = RuleOfferRawSchema.safeParse(screenUncoveredDetailSeat(backfillLegacyBatchMemberKind(raw)));
411
+ if (!parsed.success) {
412
+ defaultLogger.warn("rule_offer_dropped_unreadable_shape", {
413
+ kind: typeof raw === "object" && raw !== null ? String(raw.kind) : typeof raw,
414
+ note: "this offer's shape is outside the closed vocabulary this build reads (member kind / match / reason) — dropped whole; sibling offers still render (core/dist skew?)",
415
+ });
416
+ continue;
417
+ }
418
+ const offer = parsed.data;
419
+ if (ToolApprovalCoordinator.ruleOfferTextsOverSize(offer))
420
+ return undefined;
421
+ if (!ToolApprovalCoordinator.ruleOfferDerivedFactsAgree(offer)) {
422
+ defaultLogger.warn("rule_offer_dropped_derived_facts_disagree", {
423
+ kind: offer.kind,
424
+ note: "an offer's kind/match/command/directory seats do not agree with what the shared validator derives from its rule text — dropped whole so the card never shows one grant while redeeming another (core/dist skew or forged material?)",
425
+ });
426
+ continue;
427
+ }
356
428
  switch (offer.kind) {
357
429
  case "single":
358
- if (over(offer.rule) || over(offer.command))
359
- return undefined;
360
- out.push(offer);
430
+ out.push({ kind: "single", rule: offer.rule, match: offer.match, command: offer.command });
361
431
  break;
362
- case "batch":
363
- if (offer.rules.some((r) => over(r.rule) || over(r.command)))
364
- return undefined;
432
+ case "batch": {
433
+ const rules = [];
434
+ for (const r of offer.rules) {
435
+ switch (r.kind) {
436
+ case "command":
437
+ rules.push({ kind: "command", rule: r.rule, match: r.match, command: r.command, segment: clipSegment(r.segment) });
438
+ break;
439
+ case "directoryRead":
440
+ rules.push({ kind: "directoryRead", rule: r.rule, directory: r.directory, segment: clipSegment(r.segment) });
441
+ break;
442
+ default:
443
+ throw new Error(`unknown rule-offer batch member kind: ${JSON.stringify(r)}`);
444
+ }
445
+ }
365
446
  out.push({
366
447
  kind: "batch",
367
- rules: offer.rules.map((r) => (over(r.segment) ? { ...r, segment: r.segment.slice(0, MAX_RULE_TEXT_CHARS) } : r)),
448
+ rules,
368
449
  uncoveredSegments: offer.uncoveredSegments,
450
+ ...(offer.uncoveredDetail !== undefined ? { uncoveredDetail: offer.uncoveredDetail.map((d) => ({ segment: clipSegment(d.segment), reason: d.reason })) } : {}),
369
451
  });
370
452
  break;
453
+ }
371
454
  default:
372
- return undefined;
455
+ throw new Error(`unknown rule-offer kind: ${JSON.stringify(offer)}`);
373
456
  }
374
457
  }
375
458
  return out;
@@ -20,7 +20,7 @@
20
20
  import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskRequest, CheckpointSummary, TaskEvent, TraceEvent, MailboxMessage, MailboxStore, TaskSpec, TaskResult } from "@sema-agent/core";
21
21
  /** 编译期断言:T 必须收敛到 never(有残余键 = tsc 红)。 */
22
22
  type AssertAllKeysHandled<T extends never> = T;
23
- type NotificationProjected = "task_id" | "task_type" | "toolUseId" | "status" | "summary" | "result" | "output_file" | "usage" | "sessionId" | "seq" | "lines" | "stoppedBy" | "source" | "exitCode" | "partial" | "diagnostics" | "recentSteps" | "editedFiles" | "resumable" | "completionId" | "error" | "errorCode" | "apiFailure";
23
+ type NotificationProjected = "task_id" | "task_type" | "toolUseId" | "status" | "summary" | "result" | "output_file" | "usage" | "sessionId" | "seq" | "lines" | "stoppedBy" | "source" | "exitCode" | "partial" | "diagnostics" | "recentSteps" | "editedFiles" | "resumable" | "completionId" | "error" | "errorCode" | "apiFailure" | "agentMessage" | "_sema_provenance";
24
24
  type NotificationExcluded = "peer";
25
25
  type _GuardNotification = AssertAllKeysHandled<Exclude<keyof TaskNotificationPayload, NotificationProjected | NotificationExcluded>>;
26
26
  type BgNotifProjected = "taskId" | "sessionId" | "seq" | "status" | "summary" | "stoppedBy" | "resumable" | "recentSteps" | "editedFiles" | "usage" | "transcriptId" | "rootSessionId" | "parentTaskId" | "parentToolCallId" | "completionId";
@@ -29,7 +29,7 @@ type _GuardBgNotif = AssertAllKeysHandled<Exclude<keyof BackgroundChildEvent, Bg
29
29
  type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
30
30
  type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
31
31
  type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval";
32
- type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason" | "hasBidiControls" | "previewWithheld";
32
+ type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason" | "hasBidiControls" | "previewWithheld" | "execCwd";
33
33
  type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
34
34
  type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest" | "text_end";
35
35
  type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
@@ -58,8 +58,14 @@ export interface LedgerSink {
58
58
  onForwardEvent(e: {
59
59
  type?: string;
60
60
  }): void;
61
- /** Park event (durable F4 suspend / [ref] D-B review pause): ONLY the non-secret `checkpointGate` is
62
- * persisted — never the capability `checkpointToken` (search [18] Q1b). */
61
+ /** Park event (durable F4 suspend / [ref] D-B review pause): ONLY the `checkpointGate` is persisted —
62
+ * never the capability `checkpointToken` (search [18] Q1b).
63
+ * ⚠️ **"non-secret" was the old wording and it is not true** ([ref] R3, 2026-09-03 — wording only, behaviour
64
+ * unchanged; [ref] decides the arm): the gate carries NO capability token, but its `riskDescriptor.summary` /
65
+ * `shadowedRule` are model-/human-controlled display text that [ref] measured carrying a PEM private-key
66
+ * body verbatim. This durable row replays on every `GET /v1/runs/:id/events` reconnect and is NOT backfilled
67
+ * by core [ref]'s mint-point fix — it is the third face of the `checkpointGate` sister item recorded in
68
+ * `http/routes/approvals-assistant.ts` (`redactPendingDisclosures` header). */
63
69
  appendParked(kind: "suspended" | "needs_review", result: unknown): Promise<void>;
64
70
  /** Terminal `done` event. `result` must already be checkpoint-token-stripped (and, on the bg leg,
65
71
  * model-usage-attached) by the caller. */
@@ -101,7 +107,10 @@ export declare function createLedgerSink(opts: {
101
107
  * ——**text_delta/reasoning_delta 只进内存缓冲、到 flush 边界才 append**(codex S1-F1,2026-08-13:
102
108
  * 只打 append 会让长时间流式输出被误读成「无进展」,方向与本键的设计目的正好相反);②`append`
103
109
  * 入口——兜住不经 onEvent 的直写路径(appendParked/appendDone/onForwardEvent)。分诊提示性回调:
104
- * 抛错不得断链(打点失败丢的只是判别材料,账本不能陪葬)。 */
105
- onActivity?: () => void;
110
+ * 抛错不得断链(打点失败丢的只是判别材料,账本不能陪葬)。
111
+ * 🔴 [ref] 件3:回调带**事件类型**(`kind`)——`cancelContext.lastEventKind` 的数据源。两个打点位
112
+ * 各传自己看得见的那个词(位①=流事件 `ev.type`,位②=账本行 `type`);拿不到类型的打点方
113
+ * (core 的 onActivity 第五席、resume 腿手动打点)省参,`kind` 诚实缺席(见 turn-activity.ts)。 */
114
+ onActivity?: (kind?: string, statusData?: Record<string, unknown>) => void;
106
115
  }): LedgerSink;
107
116
  //# sourceMappingURL=ledger-sink.d.ts.map
@@ -17,15 +17,15 @@ export function createLedgerSink(opts) {
17
17
  let text = "";
18
18
  let reasoning = "";
19
19
  let textEventId;
20
- const beat = () => {
20
+ const beat = (kind, statusData) => {
21
21
  try {
22
- opts.onActivity?.();
22
+ opts.onActivity?.(kind, statusData);
23
23
  }
24
24
  catch {
25
25
  }
26
26
  };
27
27
  const append = (type, data) => {
28
- beat();
28
+ beat(type);
29
29
  const step = chain.then(() => opts.appendEvent(++seq, type, data));
30
30
  chain = step.then(() => undefined, () => undefined);
31
31
  return step;
@@ -41,7 +41,7 @@ export function createLedgerSink(opts) {
41
41
  }
42
42
  };
43
43
  const onEvent = async (ev) => {
44
- beat();
44
+ beat(ev.type);
45
45
  switch (ev.type) {
46
46
  case "text_delta": {
47
47
  const td = ev;
@@ -63,7 +63,11 @@ export function createLedgerSink(opts) {
63
63
  break;
64
64
  case "status":
65
65
  await flush();
66
- await append("status", brainStatusEventData(ev));
66
+ {
67
+ const statusData = brainStatusEventData(ev);
68
+ beat("status", statusData);
69
+ await append("status", statusData);
70
+ }
67
71
  break;
68
72
  case "context_usage":
69
73
  await flush();
@@ -134,7 +138,11 @@ export function createLedgerSink(opts) {
134
138
  void append("tool_end", toolEndEventData(e)).catch(() => undefined);
135
139
  }
136
140
  };
137
- const appendStatus = (ev) => append("status", brainStatusEventData(ev));
141
+ const appendStatus = (ev) => {
142
+ const statusData = brainStatusEventData(ev);
143
+ beat("status", statusData);
144
+ return append("status", statusData);
145
+ };
138
146
  const appendParked = (kind, result) => append(kind, {
139
147
  gate: result?.checkpointGate ?? null,
140
148
  ...(typeof result?.toolCallId === "string" ? { toolCallId: result.toolCallId } : {}),
@@ -16,6 +16,7 @@ import type { ApprovalSettledBy, TaskResult, SystemInjectionPriority } from "@se
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";
19
+ import { type SemaProvenanceInput } from "./sema-provenance.js";
19
20
  import type { LedgerEventType } from "./ledger-events.js";
20
21
  /** 账本写口的统一签名 —— `type` 是**闭集**(`ledger-events.ts`):新型必须先登记,否则调用点编译红
21
22
  * (codex 交叉复审 F2:别名门 `ap("x")` 与动态首参让纯词法门结构性抓不全)。 */
@@ -214,6 +215,18 @@ export declare function taskNotificationEventData(ev: {
214
215
  status?: number;
215
216
  requestId?: string;
216
217
  };
218
+ /** [ref](a) [ref] 片6 d1(core 7.2.0):子代→主代 uplink 帧的**渲染判别**成员。在场 = 这一帧是
219
+ * `SendMessage("main")` 的 agent-message(cli 渲 agent-message 卡;父模型面读的是顶层 `<agent-message from=…>`
220
+ * 帧而非 task-notification 外壳);缺席 = 老形卡。`body` 是子代文本 = UNTRUSTED(与 `result` 同姿势 redact);
221
+ * `from` 是 core 铸的发送方标签(引擎值,verbatim)。铸造点唯一:core `agents/send-message-tool.ts` uplink 腿。 */
222
+ agentMessage?: {
223
+ from: string;
224
+ body: string;
225
+ };
226
+ /** 同批:typed 归因侧记(wire 键;core 契约「a host projects it, the model never sees it」)。成员逐个挑取,
227
+ * `kind` 过闭集(`trace/sema-provenance.ts` 单点,park 半场同一判据);`agentType` 本 mount 恒缺席(工具无
228
+ * agent-type 席),在场即透传(fleet spawn 帧按 taskId 可 join 时上游给)。 */
229
+ _sema_provenance?: SemaProvenanceInput;
217
230
  };
218
231
  eventId?: string;
219
232
  parentToolCallId?: string;
@@ -1,6 +1,8 @@
1
+ import { redactErrorMessage } from "../observability/run-terminal-log.js";
1
2
  import { isApprovalSettledBy, isAskDenyResolution } from "@sema-agent/core";
2
3
  import { redactSecrets, redactDeep } from "./redact.js";
3
4
  import { injectionTierOrAbsent } from "./injection-tier.js";
5
+ import { semaProvenanceOrAbsent } from "./sema-provenance.js";
4
6
  export async function appendModelUsageDelta(append, modelUsage, taskId) {
5
7
  const delta = modelUsage?.drain(taskId);
6
8
  if (delta) {
@@ -151,6 +153,13 @@ export function taskNotificationEventData(ev) {
151
153
  ...(n.result !== undefined ? { result: redactSecrets(n.result) } : {}),
152
154
  ...(n.output_file !== undefined ? { output_file: redactSecrets(n.output_file) } : {}),
153
155
  ...(n.usage !== undefined ? { usage: n.usage } : {}),
156
+ ...(n.agentMessage !== undefined ? { agentMessage: { from: n.agentMessage.from, body: redactSecrets(n.agentMessage.body) } } : {}),
157
+ ...(() => {
158
+ if (n._sema_provenance === undefined)
159
+ return {};
160
+ const p = semaProvenanceOrAbsent(n._sema_provenance);
161
+ return p !== undefined ? { _sema_provenance: p } : {};
162
+ })(),
154
163
  ...(() => {
155
164
  const tier = injectionTierOrAbsent(ev.priority);
156
165
  return tier !== undefined ? { priority: tier } : {};
@@ -522,7 +531,7 @@ function mapTraceEventInner(type, seq, data) {
522
531
  case "needs_review":
523
532
  return { event: "done", data: { seq, suspended: true } };
524
533
  case "failed":
525
- return { event: "error", data: { type: "error", errorCode: "WORKER_DOWN", message: redactSecrets(String(data.errorMessage ?? "run failed")) } };
534
+ return { event: "error", data: { type: "error", errorCode: "WORKER_DOWN", message: redactErrorMessage(String(data.errorMessage ?? "run failed")) ?? "run failed" } };
526
535
  case "context_usage":
527
536
  case "compaction_outcome":
528
537
  case "wiring_manifest":
@@ -1,17 +1,28 @@
1
- export declare function setRedactionObserver(fn: ((patternLabel: string, count: number) => void) | undefined): void;
2
- /**
3
- * `opts.observe: false` = **照常脱敏,但这一遍不计数**(合并码重扫)
4
- *
5
- * 为什么需要它:本函数对自己的输出**值幂等、计数不幂等** —— URL-userinfo JSON 秘密字段两条 pattern
6
- * 会命中自己已经替换过的输出并再计一次。于是任何**重扫已脱敏文本**的读路径(耐久待批队列的回读腿,
7
- * `/v1/approvals/stream` 3s 一拍)都会持续给 `redactions_applied_total` 加数,把那个用来识别
8
- * 「过度脱敏 / pattern 从不触发」的指标灌成不可读。
1
+ /** 测试可见:扫描器 indexOf 实际扫过的字节数(摊还线性 常数 × 输入长) */
2
+ export declare function ppkScanBytesForTest(): number;
3
+ export declare function resetPpkScanBytesForTest(): void;
4
+ /** Redact unambiguous secret formats from reasoning/CoT before it is persisted to the durable trace log. */
5
+ /** S24 (SILENT-FALLBACK P1): optional redaction observer rewrites previously had zero fingerprint, so
6
+ * over-redaction (mangling legit content) and under-redaction (a pattern that never fires) were both
7
+ * invisible. main.ts wires this to `redactions_applied_total{pattern}`. Hot path: the observer only runs
8
+ * when a pattern actually matched; the label is derived once per pattern at module load. */
9
+ /** [ref]:第三参 `meta.arm` = 命中的是哪一组臂。加参而不是改参 —— 只读两参的既有观察者原样可用
10
+ * (TS 允许少形参),main.ts 把它铺成 `redactions_applied_total{pattern,arm}` 的第二个维度。
9
11
  *
10
- * 🔴 关的是**计数**,不是脱敏:重扫这一遍仍是纵深防御(手改过的行、未来某条写路径漏了脱敏,读面照样
11
- * 兜住)—— 少脱一次的代价是把凭证投上跨租户可见的运维队列,那条永远不能省。
12
- */
12
+ * 🔴 **词义如实**(codex r1 [medium] 三,采纳):这一维说的是「命中的是**哪一条臂**」,**不是**
13
+ * 「这段文本真的被上游截过」—— server 收到的是纯字节,core 的 `clip()` 是 `slice(0, 80)` **不留标记**
14
+ * (亲读 `@sema-agent/core/dist/agents/subagent-steps.js:9-11`),截过与天生就短在 wire 上不可区分。
15
+ * 所以一个 `arm="fragment"` 计数可能来自残片,也可能来自一个恰好落在阈值之上的 look-alike;
16
+ * core [ref] 落地后这一支**趋零是证据、不归零不是证明**。别把它当截断事实的度量用。 */
17
+ export type RedactionArm = "full" | "fragment";
18
+ export interface RedactionHitMeta {
19
+ /** `"fragment"` = {@link FRAGMENT_SECRET_PATTERNS} 里的前缀残片臂;`"full"` = 完整臂。闭集两词。 */
20
+ arm: RedactionArm;
21
+ }
22
+ export declare function setRedactionObserver(fn: ((patternLabel: string, count: number, meta: RedactionHitMeta) => void) | undefined): void;
13
23
  export declare function redactSecrets(text: string, opts?: {
14
24
  observe?: boolean;
25
+ json?: boolean;
15
26
  }): string;
16
27
  /**
17
28
  * 🔴 环 + 深度预算(2026-07-25 补)。原实现是纯递归、**既不记已访问节点也不限深度**,而它的调用位置是