@sema-agent/server 7.47.0-rc.2 → 7.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/USAGE.md CHANGED
@@ -128,6 +128,16 @@ MODEL_DEGRADE_ON=rate_limit,breaker_open # 可选,反应式触发器子集;词
128
128
  - **`_MAX_SLICES` 不冻结**:引擎不把它存进账本,每片现读当期部署值去比账本上累计的片数。⇒ 改它会**立刻**
129
129
  作用于已 park 的链(调大=放宽、设 0=解除兜底)。这是部署级旋钮跟随当期部署的常态,但别把它误读成
130
130
  「和两条总额一样冻在链上」——两者的运维含义不同。
131
+ - 🔭 **跑的过程中怎么看还剩多少**([3833] S-4,7.48 起):配了总额的 run 在 **running** 期间,
132
+ `GET /v1/runs/:id` 顶层多一只 additive 的 `crossSliceUsage`
133
+ (`{totalTokens, spentTokens, totalBudgetMicroUsd, spentMicroUsd, maxSlices, sliceCount}`,三轴各自成对、
134
+ 上限没配那根就整对缺席;`$` 轴是 **micro-USD** 整数,显示端 ÷1e6)。**用途 = 提前介入**:此前只有爆窗
135
+ 那一刻(行翻 `suspended` + `resource_limit` gate)才知道逼近过。
136
+ ⚠️ 三条读法纪律:① 读数是**下界**(用量在 turn 边界入账,滞后至多一个 turn;不含委派子代花费);
137
+ ② **整键缺席不等于「没花钱」**——没配总额 / 非 running / poll 落到没跑过这条 run 的副本(记录是同副本
138
+ best-effort,与 `msSinceLastActivity` 同族)/ 账本读不可用,四种都是缺席;后者计数在
139
+ `fail_open_total{tag="server.runs.cross-slice-usage-unavailable"}`;③ 它是**给人看的预警面**,真正执法的
140
+ 是引擎账本 —— 别拿它做自动重试/取消的机器判据。
131
141
  - **per-principal 累计配额**:用 `AsyncLocalStorage` 把 principal 透传到 cost tracer,所以**council/team 子任务的花费也算到发起人头上**。**配了 `SESSION_BACKEND=mysql` 时自动跨副本共享**(`cost_quota` 表,写后聚合的**原子自增** `micro=micro+delta`,对齐固定窗,最终一致——多副本花费 SUM 到一起、不丢增量);否则 in-memory per-replica 滚动窗(单副本兜底)。启动日志 `costQuota` 字段回显 `shared(mysql)`/`in-process`/`off`。跨副本是最终一致(flush 间隔内峰值可能略超,由**硬 per-task `maxCostUsd` 兜底**)。
132
142
  - **`RATE_LIMIT_RPM` 请求限流同样自动跨副本**(`SESSION_BACKEND=mysql` 时,`rate_limit` 表,与配额共用 `WriteBehindCounter`;启动日志 `rateLimit` 回显)。注意:写后聚合 = **软限流**(边界上短暂略超 OK,适合公平/热调用方防护);要**硬合规上限**得另走 CAS/原子计数,不靠写后聚合——和断路器跨副本同款权衡。
133
143
 
@@ -21,6 +21,8 @@ import { assertCloudSnapshotBlobPosture, openStoreBackendWithFallback } from "..
21
21
  import { buildMemoryRemoteLaneWarn, memoryEngineBackendFor, memoryEngineRemoteLanePosture } from "../memory-scope.js";
22
22
  import { assertToolResultProvenanceSchema } from "../plugins/tool-result-store-sql.js";
23
23
  import { assertPermissionRuleApprovalSchema } from "../plugins/permission-rule-store-sql.js";
24
+ import { assertApprovalAskRuleMaterialSchema } from "../plugins/approval-ask-store-sql.js";
25
+ import { resolveStreamApprovalGate } from "../tool-approval.js";
24
26
  import { buildMemoryWriteBoundaryAudit, effectiveMemoryPersistenceCapable } from "./memory-boundary.js";
25
27
  import { buildMemoryPosture } from "../memory-posture.js";
26
28
  const DARK_REASON_PROSE = {
@@ -54,6 +56,17 @@ export async function openStores(ctx) {
54
56
  else if (mysqlPool)
55
57
  await assertPermissionRuleApprovalSchema(async (sql) => ({ rows: (await mysqlPool.query(sql))[0] }), "tidb");
56
58
  }
59
+ if (resolveStreamApprovalGate({
60
+ toolApprovalEnabled: config.toolApprovalEnabled === true,
61
+ streamApprovalEnabled: config.streamApproval.enabled,
62
+ backend,
63
+ parkFacility: backend?.checkpoint !== undefined && config.durableApproval === true,
64
+ }).active) {
65
+ if (pgPool)
66
+ await assertApprovalAskRuleMaterialSchema(async (sql) => ({ rows: (await pgPool.query(sql)).rows }), "pg");
67
+ else if (mysqlPool)
68
+ await assertApprovalAskRuleMaterialSchema(async (sql) => ({ rows: (await mysqlPool.query(sql))[0] }), "tidb");
69
+ }
57
70
  }
58
71
  {
59
72
  const minioReq = ["MINIO_ENDPOINT", "MINIO_ACCESS_KEY", "MINIO_SECRET_KEY"];
package/dist/config.d.ts CHANGED
@@ -172,7 +172,7 @@ export declare function applyAutoCompactWindow(m: Model, explicit?: number): "ex
172
172
  * (常驻进程不 crash);
173
173
  * ③ per-request 腿(`settings.permissions.{ask,deny}`,task-settings.ts)—— 该请求 422。
174
174
  *
175
- * 四种拼法:
175
+ * 五种拼法:
176
176
  * - CC 形规则条目(`Bash(ps:*)`/`Edit(src/**)`,#186 [3047]§七 装机实测):CC 的权限规则 DSL 写法。这些
177
177
  * 名单比的是**整串工具名**,带括号那串不是任何活工具的名字 ⇒ 永不匹配。指引=按命令名走
178
178
  * `runtime.commandPolicy`,整条 shell 进门走 `MANUAL_MODE_SHELL_GATE=always`。判别式见
@@ -182,6 +182,9 @@ export declare function applyAutoCompactWindow(m: Model, explicit?: number): "ex
182
182
  * - 不完整 MCP 名(`mcp__`、`mcp__figma`):前缀对但缺段——MCP 实挂名恒是 `mcp__<server>__<tool>` 三段形,
183
183
  * 少一段同样永不匹配。旧判据「含 `__` 且 protocolOf 认不出」放过了这一族(前缀恰好认得出),从另一侧
184
184
  * 漏出去,失守方向与 `figma__x` 完全相同。
185
+ * - **core 的文法编不出来的串**(名字里带反斜杠、单条 >512 字符、空串):判词取自 core 自己的
186
+ * `validatePermissionRules`,见 {@link uncompilableRuleGuidance}。#359①(A-075.5)补齐 ——
187
+ * 此前只有 `ask` 腿(经 `createPermissionRulePolicy` 的 throw)拒得出这一族,`deny` 静默 no-op。
185
188
  *
186
189
  * 已接受的权衡(与退役表墓碑同一条):部署自有工具真叫 `figma__x` 的极端形会被误拒——boot 期无 roster 可
187
190
  * 豁免,且失败响亮、指引明确,好过静默失守。
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, resolveLockedKeys } from "@sema-agent/core";
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
6
  import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/registry-core";
7
7
  import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
8
8
  import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
@@ -753,6 +753,15 @@ export function findCcRuleFormNames(names) {
753
753
  }
754
754
  return out;
755
755
  }
756
+ function uncompilableRuleGuidance(name) {
757
+ const issues = validatePermissionRules([{ rule: name, behavior: "deny" }]);
758
+ const issue = issues[0];
759
+ if (issue === undefined)
760
+ return undefined;
761
+ return (`${issue.code} — core's rule grammar cannot compile this name (${issue.message}), so a table keyed by it matches nothing. ` +
762
+ `Tool names carry no backslashes and are far shorter than the 512-character rule cap; check the spelling, and use ` +
763
+ `mcp__<server>__* / a2a__<peer>__* for "every tool on that peer"`);
764
+ }
756
765
  export function findUnmatchableToolNames(names) {
757
766
  const out = [];
758
767
  for (const name of names) {
@@ -766,6 +775,11 @@ export function findUnmatchableToolNames(names) {
766
775
  out.push({ name, guidance: retired });
767
776
  continue;
768
777
  }
778
+ const uncompilable = uncompilableRuleGuidance(name);
779
+ if (uncompilable !== undefined) {
780
+ out.push({ name, guidance: uncompilable });
781
+ continue;
782
+ }
769
783
  const ns = protocolOf(name);
770
784
  if (ns === undefined) {
771
785
  if (name.includes("__"))
@@ -260,12 +260,14 @@ export type FleetFrame = {
260
260
  * 4. 🔴 **纯 observe,不改变运行** —— 它走 fleet 流(常开推送道),**不是** `additionalContext`
261
261
  * (那个通道本身会让这一轮不结束,见 hook-runner 折叠处那段旁注)。
262
262
  *
263
- * ⚠️ 可见性**与 `bg_notification` 同为 fail-CLOSED**:带 `ownerSessionId` 的帧只投给能证明是自己的
264
- * 订阅方(`undefined` ⇒ 对 scoped 订阅方**丢弃**)。理由同源:一条关于别人会话的通知落到这个壳上,
265
- * 会让用户以为是自己这轮出了问题。
263
+ * ⚠️ 可见性**与 `bg_notification` 同为 fail-CLOSED**(本帧**两族 kind 同规**):带 `ownerSessionId`
264
+ * 的帧只投给能证明是自己的订阅方(`undefined` ⇒ 对 scoped 订阅方**丢弃**)。理由同源:一条关于别人
265
+ * 会话的通知落到这个壳上,会让用户以为是自己这轮出了问题。
266
+ *
267
+ * ⚠️ `kind` 是**闭集**:第二族(`hook_non_blocking_failure`,#281)见下。加族必须同时在
268
+ * {@link hookNoticeWire} 表态(穷举 switch ⇒ 漏表态=编译红)。
266
269
  */
267
- export interface HookNotice {
268
- /** 目前只有一种;留成枚举位是为了下一类观测帧不必再开一个 frame 臂。 */
270
+ export interface HookDecisionUnavailable {
269
271
  kind: "hook_decision_unavailable";
270
272
  /** 哪个 hook 事件(`Stop` / `PreToolUse` / …)。 */
271
273
  event: string;
@@ -273,11 +275,90 @@ export interface HookNotice {
273
275
  reason: "no_content" | "unparsed" | "skipped";
274
276
  /** 补充说明(已脱敏)。例如 `skipped` 的具体原因。 */
275
277
  detail?: string;
278
+ }
279
+ /**
280
+ * **一次 hook 自己坏了**(非阻断故障)的观测帧 —— #281,黑板 [4250](test 实测立案)/ [4261](core 定性:
281
+ * 翻译层全在 server)/ [4263](CC 锚源码核实)/ [4381](server 裁量②)。
282
+ *
283
+ * ── 它解决什么(与上面那族是两件事)──────────────────────────────────────────────────────────
284
+ * 上面那族说的是「判定没做成」;这一族说的是「**你配的这个 hook 自己坏了**」:退出码非 0 且非 2、
285
+ * 超时、起不来、吐了坏 JSON。方向按 [4381] 裁量① **维持 CC parity 不动**(非阻断,绝不误伤任务),
286
+ * 但此前故障只进服务端结构化日志 ⇒ **任务视角零可见**:用户以为自己的守卫在跑,其实每次都在坏。
287
+ * 而 registry-core `hooks.ts:536` 对这一格逐字承诺过 "any other exit = non-blocking error
288
+ * (**stderr shown to the user**)" —— 承诺与实装的落差,正是本帧要补的那一格。
289
+ *
290
+ * ── CC 锚(cc-decoded/pretty223.js 亲读,[4263] 已独立核实同一段)────────────────────────────
291
+ * CC 的 `hook_non_blocking_error` 附件带 `hookName` / `hookEvent` / `stderr` / `stdout` / `exitCode`,
292
+ * UI 层对非 `Stop`/`SubagentStop` 事件真实渲染「`<hookName>` hook error」+ stderr(优先 stderr)。
293
+ * 本帧的键就是照这份形状取的(`hookName` 的取法同 CC 的 `OG`/`AEe`:`statusMessage ?? 命令/URL/提示`)。
294
+ *
295
+ * ⚠️ 与上面那族同为**纯 observe**、同走 fleet 流、同为 fail-CLOSED 可见性 —— 刻意**不开新通道**
296
+ * (`kind` 这个位当年就是为「下一类观测帧」留的)。
297
+ */
298
+ export interface HookNonBlockingFailure {
299
+ kind: "hook_non_blocking_failure";
300
+ /** 哪个 hook 事件(`PreToolUse` / `Stop` / …)。 */
301
+ event: string;
302
+ /**
303
+ * 机器可判的故障形(闭集,与 server 日志同源):
304
+ * · `exit_nonzero` —— 退出码非 0。**exit 2 的归属按「这个位置有没有兑现面」判**(#281 codex R1-F2):
305
+ * - 有兑现面 —— 阻断面(PreToolUse ⇒ deny / Stop / UserPromptSubmit / PreCompact ⇒ block)或
306
+ * 反馈面(PostToolUse 族 ⇒ stderr 喂模型)—— ⇒ exit 2 是**表过态的裁决/反馈**,不是故障,
307
+ * **绝不进本族**;
308
+ * - 没有兑现面(observe-only 的 StopFailure/PostCompact、以及 fire-and-forget 的纯 async 条目)
309
+ * ⇒ 作者的 exit 2 无处兑现,**进本族**并由 `detail` 逐字说清那里没有阻断面 —— 静默丢弃正是
310
+ * #281 要根除的形,而不说清则会让一次有意表态被渲染成「坏 hook」;
311
+ * - `asyncRewake` 的 exit 2 是**契约动作**(唤醒模型),不是故障,不进本族;
312
+ * · `timeout` —— 条目超时被击杀(无退出码);
313
+ * · `spawn_failed` —— 起不来 / http 连不上 / LLM 载体失败(无退出码);
314
+ * · `bad_json` —— exit 0 但 stdout 看着是 JSON 却读不成决策对象(裁决静默丢失的那一形)。
315
+ */
316
+ reason: "exit_nonzero" | "timeout" | "spawn_failed" | "bad_json";
317
+ /** hook 身份(CC `OG`/`AEe` 锚:`statusMessage` 优先,否则 command / url / prompt),已脱敏+有界。
318
+ * ⚠️ http 条目取 URL 时**剥掉 query 与 fragment**(#281 codex R1-F1:端点凭据最常见的挂法是
319
+ * `?token=…`,而通用脱敏器只认 `user:pass@` 形)。要更友好的名字,作者有 `statusMessage`。 */
320
+ hookName: string;
321
+ /** 条目类型 —— 没有它,`hookName` 是命令还是 URL 还是提示词全靠猜。 */
322
+ entryType: "command" | "http" | "prompt" | "agent";
323
+ /** 退出码。**超时 / spawn 失败没有退出码 ⇒ 缺席**(不铸 0 那种看着合法的假读数)。 */
324
+ exitCode?: number;
325
+ /** 工具事件才有(PreToolUse/PostToolUse/PostToolUseFailure);非工具事件缺席。 */
326
+ toolName?: string;
327
+ /** hook 的 stderr 摘要(已脱敏 + 截断,见 `MAX_HOOK_NOTICE_STDERR_CHARS`)。空 stderr ⇒ 缺席。 */
328
+ stderr?: string;
329
+ /** 补充说明(已脱敏)。 */
330
+ detail?: string;
331
+ }
332
+ /** 生产者视角的观测面(hook-runner 铸的形):归属位由 boot 层在发布时补。 */
333
+ export type HookNoticeObservation = HookDecisionUnavailable | HookNonBlockingFailure;
334
+ /** 租户/会话归属位 —— **只用于过滤,上 wire 前剥**(见 `hookNoticeWire`)。 */
335
+ export interface HookNoticeOwnership {
276
336
  /** 租户可见性(发布前带,上 wire 前剥)。 */
277
337
  ownerScope?: string;
278
338
  /** 宿主会话(发布前带,上 wire 前剥)——scoped 订阅方的 fail-closed 判据。 */
279
339
  ownerSessionId?: string;
280
340
  }
341
+ export type HookNotice = HookNoticeObservation & HookNoticeOwnership;
342
+ /** 观测帧自由文本的上限(`hookName` / `stderr` / `detail`),**含截断标记**。stderr 是**坏 hook**
343
+ * 吐的东西:一条它想吐多长就多长的流,既不能整条上 wire,也不能静默切掉(hook 作者会读错自己的输出)。 */
344
+ export declare const MAX_HOOK_NOTICE_TEXT_CHARS = 512;
345
+ /**
346
+ * 脱敏 + 有界 —— 观测帧自由文本的唯一整形口。
347
+ *
348
+ * 🔴 **先脱敏后截断**:反过来会把一枚密钥切成半截,脱敏器就再也认不出它了(fleet 行 `objective`
349
+ * 那条同款旁注:SECRET-SAFE 的次序是承重的)。
350
+ * 🔴 **幂等**(输出恒 ≤ 上限,含标记):铸点(hook-runner)与发布口(本文件)各过一遍 —— 前者拿得到
351
+ * 原文所以那一遍是**承重**的,后者是**边界兜底**(挡住忘了脱敏的未来生产者)。不幂等的话第二遍会
352
+ * 把第一遍的标记再切一刀,把「原长」写成一个假数字。
353
+ */
354
+ export declare function boundedNoticeText(s: string): string;
355
+ /**
356
+ * 上 wire 的载荷(归属位**逐 kind 穷举剥离**,不是 rest-spread)。
357
+ *
358
+ * 🔴 穷举 `switch` 而不是 `{...n}`:#157 闭集纪律 —— 新增一个 kind 而不在这里表态 = **编译红**,
359
+ * 而 spread 会把新 kind 的任意新键(可能带内部归属/密钥)静默推上 wire。
360
+ */
361
+ export declare function hookNoticeWire(n: HookNotice): HookNoticeObservation;
281
362
  /** design/129-B 缺口② (帧契约): a background child's COMPLETION notification pushed on the
282
363
  * ALWAYS-OPEN fleet stream — the only server-push lane an idle shell keeps subscribed, so a session-scoped bg
283
364
  * child finishing while the host sits idle notifies IMMEDIATELY (no more "late by one input cycle" durable-drain).
@@ -351,7 +432,13 @@ export declare class FleetEventBus {
351
432
  publishTask(delta: Partial<FleetTaskRow> & {
352
433
  id: string;
353
434
  }): void;
354
- /** 推一帧 hook 判定未完成的观测通知(事件语义,不进 snapshot —— 与 `bg_notification` 同族)。 */
435
+ /**
436
+ * 推一帧 hook 观测通知(事件语义,不进 snapshot —— 与 `bg_notification` 同族)。
437
+ *
438
+ * 自由文本在这里再过一遍 {@link boundedNoticeText}(幂等):铸点已经过一遍(那一遍拿得到原文,是
439
+ * 承重的),本遍是**边界兜底** —— 一个忘了脱敏的未来生产者不该靠"下游会渲染得挺好看"活着。
440
+ * ⚠️ 兜底救不了「先截断后脱敏」造出的半截密钥,所以铸点那一遍**不是可选的**。
441
+ */
355
442
  publishHookNotice(notice: HookNotice): void;
356
443
  /** Drop a task row (terminal + swept) + fan out the removal. Idempotent (a no-op if already gone).
357
444
  *
@@ -25,6 +25,37 @@ export function buildFleetWorkflowRow(id, run) {
25
25
  elapsedMs: (run.endedAt ?? Date.now()) - run.startedAt,
26
26
  };
27
27
  }
28
+ export const MAX_HOOK_NOTICE_TEXT_CHARS = 512;
29
+ export function boundedNoticeText(s) {
30
+ const r = redactSecrets(s);
31
+ if (r.length <= MAX_HOOK_NOTICE_TEXT_CHARS)
32
+ return r;
33
+ const marker = `…(${r.length})`;
34
+ return `${r.slice(0, MAX_HOOK_NOTICE_TEXT_CHARS - marker.length)}${marker}`;
35
+ }
36
+ export function hookNoticeWire(n) {
37
+ switch (n.kind) {
38
+ case "hook_decision_unavailable":
39
+ return {
40
+ kind: n.kind,
41
+ event: n.event,
42
+ reason: n.reason,
43
+ ...(n.detail !== undefined ? { detail: n.detail } : {}),
44
+ };
45
+ case "hook_non_blocking_failure":
46
+ return {
47
+ kind: n.kind,
48
+ event: n.event,
49
+ reason: n.reason,
50
+ hookName: n.hookName,
51
+ entryType: n.entryType,
52
+ ...(n.exitCode !== undefined ? { exitCode: n.exitCode } : {}),
53
+ ...(n.toolName !== undefined ? { toolName: n.toolName } : {}),
54
+ ...(n.stderr !== undefined ? { stderr: n.stderr } : {}),
55
+ ...(n.detail !== undefined ? { detail: n.detail } : {}),
56
+ };
57
+ }
58
+ }
28
59
  export class FleetEventBus {
29
60
  now;
30
61
  emitter = new EventEmitter();
@@ -50,7 +81,15 @@ export class FleetEventBus {
50
81
  this.emit({ type: "task", row: merged, ts: this.now() });
51
82
  }
52
83
  publishHookNotice(notice) {
53
- this.emit({ type: "hook_notice", notice, ts: this.now() });
84
+ const bounded = notice.kind === "hook_non_blocking_failure"
85
+ ? {
86
+ ...notice,
87
+ hookName: boundedNoticeText(notice.hookName),
88
+ ...(notice.stderr !== undefined ? { stderr: boundedNoticeText(notice.stderr) } : {}),
89
+ ...(notice.detail !== undefined ? { detail: boundedNoticeText(notice.detail) } : {}),
90
+ }
91
+ : { ...notice, ...(notice.detail !== undefined ? { detail: boundedNoticeText(notice.detail) } : {}) };
92
+ this.emit({ type: "hook_notice", notice: bounded, ts: this.now() });
54
93
  }
55
94
  removeTask(id, opts) {
56
95
  const existing = this.tasks.get(id);
@@ -1,6 +1,7 @@
1
1
  import { HooksConfig } from "@sema-agent/registry-core/hooks";
2
2
  import type { Hooks } from "@sema-agent/core";
3
3
  import type { Logger } from "../observability/logger.js";
4
+ import type { HookNoticeObservation } from "../fleet/fleet-bus.js";
4
5
  export declare const MAX_HOOK_ENTRIES_PER_EVENT = 32;
5
6
  export declare const MAX_HOOK_COMMAND_CHARS = 8192;
6
7
  /** 单条 hook 超时上限(秒;条目可自设 timeout,但被此值夹住——一个 86400 的 timeout 会挂死工具门)。 */
@@ -63,16 +64,15 @@ export interface HookRunnerContext {
63
64
  hookLlm?: HookLlmCall;
64
65
  /** 阶段三b:`agent` 条目的子代理载体(部署组装:runTask 读-only 手 + 可重入禁 + 独立小 budget)。 */
65
66
  hookAgent?: HookLlmCall;
66
- /** 一次 hook 判定**未能完成**时的观测回调(纯 observe;部署把它接到 fleet 流)
67
- * 🔴 存在的理由:方向可以 fail-open,但**不能连「我这轮没看住」都不说** —— 否则模型拿到的放行
68
- * 与「已达成」无法区分,而产品刚对它承诺过「没达成不许停」。
69
- * ⚠️ 它**必须**是不改变运行的通道:`additionalContext` 不行(那个会让这一轮不结束)。缺席 ⇒ 只进日志。 */
70
- onHookNotice?: (n: {
71
- kind: "hook_decision_unavailable";
72
- event: string;
73
- reason: "no_content" | "unparsed" | "skipped";
74
- detail?: string;
75
- }) => void;
67
+ /** hook 观测回调(纯 observe;部署把它接到 fleet 流)。**两族**:
68
+ * · `hook_decision_unavailable` —— 一次判定**未能完成**。方向可以 fail-open,但**不能连
69
+ * 「我这轮没看住」都不说** —— 否则模型拿到的放行与「已达成」无法区分,而产品刚对它承诺过
70
+ * 「没达成不许停」;
71
+ * · `hook_non_blocking_failure`(#281)—— **hook 自己坏了**(exit≠0∧≠2 / 超时 / 起不来 / 坏 JSON)。
72
+ * 裁决面按 [4381] 裁量① 维持 CC parity 不动,坏掉这件事本身必须让人看得见。
73
+ * ⚠️ 它**必须**是不改变运行的通道:`additionalContext` 不行(那个会让这一轮不结束)。缺席 ⇒ 只进日志。
74
+ * ⚠️ 回调**抛错不许误伤任务**(见 {@link emitHookNotice}):观察面绝不成为新的失败源。 */
75
+ onHookNotice?: (n: HookNoticeObservation) => void;
76
76
  /** Stop × `type:"prompt"` 是否把**会话 transcript** 交给评估者(CC `execPromptHook` 形)。
77
77
  * 缺省(undefined)= **开**。`false` ⇒ 整条路不启用(prompt 条目在 Stop 上被跳过并记账)——
78
78
  * 部署侧的显式逃生口(`SEMA_STOP_PROMPT_TRANSCRIPT=0`)。见 `hooks.stop` 里那段三问。 */