@sema-agent/server 7.114.0 → 7.116.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 (159) hide show
  1. package/MIGRATION.md +237 -2
  2. package/USAGE.md +39 -3
  3. package/deploy/sema-up/cluster-up.sh +13 -1
  4. package/deploy/sema-up/kube-up.sh +13 -1
  5. package/deploy/sema-up/preflight.sh +13 -1
  6. package/deploy/sema-up/sema-up.sh +13 -1
  7. package/deploy/sema-up/smoke.sh +13 -1
  8. package/dist/adoption/wire.d.ts +0 -3
  9. package/dist/approval-ask-audit-store.d.ts +25 -6
  10. package/dist/approval-ask-audit-store.js +6 -1
  11. package/dist/approval-card.d.ts +25 -24
  12. package/dist/approval-card.js +14 -0
  13. package/dist/approval-crash-convergence.d.ts +60 -0
  14. package/dist/approval-crash-convergence.js +75 -0
  15. package/dist/approval-deny-reasons.d.ts +13 -0
  16. package/dist/approval-deny-reasons.js +2 -0
  17. package/dist/approval.js +7 -3
  18. package/dist/boot/coordinators.js +0 -12
  19. package/dist/boot/execution-env.js +2 -1
  20. package/dist/boot/exit-boundary.d.ts +3 -2
  21. package/dist/boot/exit-boundary.js +3 -0
  22. package/dist/boot/resolve-spec.js +5 -1
  23. package/dist/boot/shutdown.d.ts +19 -0
  24. package/dist/boot/shutdown.js +12 -1
  25. package/dist/boot/stage-07-capability-layer.js +21 -15
  26. package/dist/boot/stores.js +4 -4
  27. package/dist/capabilities/composite-members.d.ts +1 -0
  28. package/dist/capabilities/composite-members.js +1 -0
  29. package/dist/capabilities/hands-lane.d.ts +0 -23
  30. package/dist/capabilities/hands-lane.js +3 -2
  31. package/dist/capabilities/send-user-file-tool.d.ts +10 -22
  32. package/dist/capabilities/send-user-file-tool.js +1 -8
  33. package/dist/capability-facts.d.ts +296 -0
  34. package/dist/capability-facts.js +354 -0
  35. package/dist/config-catalog.js +2 -1
  36. package/dist/config-center/apply-effective.js +11 -11
  37. package/dist/config-provider.d.ts +16 -0
  38. package/dist/config-provider.js +11 -2
  39. package/dist/config-types.d.ts +20 -1
  40. package/dist/config.d.ts +55 -29
  41. package/dist/config.js +48 -25
  42. package/dist/deployment-governance.d.ts +15 -4
  43. package/dist/deployment-governance.js +20 -2
  44. package/dist/fleet/fleet-bus.d.ts +1 -22
  45. package/dist/hooks/hook-runner.d.ts +29 -3
  46. package/dist/hooks/hook-runner.js +30 -17
  47. package/dist/http/admission.d.ts +6 -0
  48. package/dist/http/admission.js +29 -7
  49. package/dist/http/approval-sweeps.js +20 -0
  50. package/dist/http/checkpoint-ctx.d.ts +68 -0
  51. package/dist/http/checkpoint-ctx.js +36 -0
  52. package/dist/http/durable-run-leg.js +2 -1
  53. package/dist/http/principal-gate.d.ts +3 -0
  54. package/dist/http/principal-gate.js +3 -0
  55. package/dist/http/resume-legs.js +19 -1
  56. package/dist/http/retired-skill-manifest.d.ts +23 -0
  57. package/dist/http/retired-skill-manifest.js +22 -0
  58. package/dist/http/route-ctx.d.ts +7 -0
  59. package/dist/http/route-table.d.ts +3 -2
  60. package/dist/http/route-table.js +5 -5
  61. package/dist/http/routes/a2a-serve.d.ts +1 -3
  62. package/dist/http/routes/a2a-serve.js +14 -23
  63. package/dist/http/routes/admin-config-refresh.js +7 -2
  64. package/dist/http/routes/admin-drain.js +7 -2
  65. package/dist/http/routes/admin-memory-optout.d.ts +0 -2
  66. package/dist/http/routes/admin-memory-optout.js +20 -7
  67. package/dist/http/routes/adoption.js +19 -5
  68. package/dist/http/routes/agents-roster.js +4 -2
  69. package/dist/http/routes/approvals-assistant.js +41 -18
  70. package/dist/http/routes/background.d.ts +0 -2
  71. package/dist/http/routes/background.js +4 -5
  72. package/dist/http/routes/capabilities.d.ts +2 -1
  73. package/dist/http/routes/capabilities.js +63 -65
  74. package/dist/http/routes/config-catalog.d.ts +4 -5
  75. package/dist/http/routes/config-catalog.js +16 -2
  76. package/dist/http/routes/devices.js +17 -11
  77. package/dist/http/routes/diagnostics.d.ts +5 -0
  78. package/dist/http/routes/diagnostics.js +8 -1
  79. package/dist/http/routes/fleet.js +4 -2
  80. package/dist/http/routes/images.js +30 -3
  81. package/dist/http/routes/leader.js +7 -1
  82. package/dist/http/routes/mcp-reconnect.js +17 -10
  83. package/dist/http/routes/memory-bundle.js +20 -6
  84. package/dist/http/routes/memory-compliance.js +19 -5
  85. package/dist/http/routes/memory-consolidation.d.ts +9 -12
  86. package/dist/http/routes/memory-consolidation.js +17 -3
  87. package/dist/http/routes/memory-origin.js +19 -5
  88. package/dist/http/routes/notify-wake.js +5 -3
  89. package/dist/http/routes/observability.js +19 -5
  90. package/dist/http/routes/retention-ops.js +6 -1
  91. package/dist/http/routes/rules.js +49 -21
  92. package/dist/http/routes/run-memory-capture.js +5 -2
  93. package/dist/http/routes/runs.js +90 -60
  94. package/dist/http/routes/session-sync.js +64 -53
  95. package/dist/http/routes/sessions-list.js +8 -5
  96. package/dist/http/routes/sessions.js +40 -25
  97. package/dist/http/routes/shared-memory.js +5 -3
  98. package/dist/http/routes/tasks.js +5 -3
  99. package/dist/http/routes/trace-usage.js +18 -11
  100. package/dist/http/routes/workflows.js +36 -13
  101. package/dist/http/server.js +4 -1
  102. package/dist/http/wire-types.d.ts +6 -0
  103. package/dist/index.d.ts +1 -1
  104. package/dist/leader/wire.js +2 -1
  105. package/dist/local-data-root.d.ts +1 -1
  106. package/dist/mcp-content-origin.d.ts +0 -8
  107. package/dist/model-entry-refusal.d.ts +10 -2
  108. package/dist/model-entry-refusal.js +19 -3
  109. package/dist/namespaced-rule-name.d.ts +54 -0
  110. package/dist/namespaced-rule-name.js +33 -0
  111. package/dist/observability/corrupt-read-seat.d.ts +4 -3
  112. package/dist/observability/corrupt-read-seat.js +4 -3
  113. package/dist/observability/fail-open.d.ts +10 -9
  114. package/dist/observability/fail-open.js +5 -1
  115. package/dist/observability/permission-rule-events.d.ts +3 -0
  116. package/dist/observability/permission-rule-events.js +4 -0
  117. package/dist/permission-rule-list.d.ts +81 -0
  118. package/dist/permission-rule-list.js +17 -0
  119. package/dist/plugins/file-run-store.d.ts +7 -1
  120. package/dist/plugins/file-run-store.js +20 -7
  121. package/dist/plugins/memory-run-store.js +2 -1
  122. package/dist/plugins/one-time-migrations.d.ts +4 -2
  123. package/dist/plugins/one-time-migrations.js +26 -3
  124. package/dist/plugins/pg-session-storage.js +1 -1
  125. package/dist/plugins/run-store-sql.js +4 -4
  126. package/dist/plugins/session-policy-store-sql.js +4 -2
  127. package/dist/plugins/store-backend.d.ts +6 -1
  128. package/dist/plugins/store-backend.js +2 -1
  129. package/dist/plugins/store-contracts.d.ts +27 -8
  130. package/dist/plugins/store-contracts.js +12 -3
  131. package/dist/plugins/tidb-session-store.js +1 -1
  132. package/dist/request-key-closure.js +1 -1
  133. package/dist/run-local.d.ts +37 -3
  134. package/dist/run-local.js +52 -6
  135. package/dist/runs.d.ts +0 -58
  136. package/dist/runs.js +15 -20
  137. package/dist/runtime-governance.d.ts +21 -1
  138. package/dist/runtime-governance.js +6 -1
  139. package/dist/security.js +2 -2
  140. package/dist/session-policy-wire.d.ts +40 -59
  141. package/dist/session-policy-wire.js +60 -32
  142. package/dist/spec-fields.d.ts +32 -0
  143. package/dist/spec-fields.js +31 -16
  144. package/dist/standing-answer-grant.d.ts +8 -0
  145. package/dist/standing-answer-grant.js +5 -0
  146. package/dist/task-settings.d.ts +17 -6
  147. package/dist/task-settings.js +16 -30
  148. package/dist/tool-approval.d.ts +32 -4
  149. package/dist/tool-approval.js +31 -8
  150. package/dist/trace/core-keyset-guard.d.ts +13 -8
  151. package/dist/trace/project.js +9 -0
  152. package/dist/trace/self-minted-wire.d.ts +42 -0
  153. package/dist/trace/self-minted-wire.js +13 -0
  154. package/dist/user-input-ledger.d.ts +8 -5
  155. package/package.json +6 -4
  156. package/scripts/drain-check-local.mjs +528 -0
  157. package/scripts/drain-check.mysql.sql +67 -0
  158. package/scripts/drain-check.pg.sql +71 -0
  159. package/scripts/retired-mode-seed-drain-local.mjs +0 -317
@@ -9,8 +9,12 @@
9
9
  * 当时:local 车道 ask 账进程内易失(`InMemoryApprovalAskStore`),活卡窗内崩溃 = **零痕迹**:重启后
10
10
  * `/v1/approvals` 两数组空,那次审批连一条审计行都没有。**S-384 起 local 的 ask 账已是持久的**
11
11
  * (`FileApprovalAskStore`),所以「零痕迹」那句不再成立;本店今天还在的理由收窄成它**独有**的那一格:
12
- * `crashConverged` 这个 local-only 的 additive 读面(孤儿行的 `orphanState` / `resumeSafe` 两位),
13
- * ask 账本身没有「boot 收敛孤儿」这条动词。两本同根账并存是已登记的抽象欠账。本店把 ask **铸造 / 决议 / 腿闭**三类事件
12
+ * `crashConverged` 这个 local-only 的 additive 读面(孤儿行的 `orphanState` / `resumeSafe` 两位)—— **腿证据只在这里**
13
+ * (`legTracked` / `leg_closed` / 关停记号),ask 账没有。🔴 **「这一问还能不能决」不归本店答**:答案源是 ask 账的行状态
14
+ * (决断口只读它)。启动收敛是**一只**函数(`approval-crash-convergence.ts`):先跑本店的收敛(下面「boot 收敛判据」),再把 ask 账里
15
+ * 前世留下的 `STREAM_PENDING` 行收成 `VOID`,成因词取本店对同一 `approvalId` 的 {@link FileApprovalAskAuditStore.causeFor} —— 成因
16
+ * 判一次、写两处(ask 行 `deniedReason` 与本店收敛行 `cause` 同词)。并账(把腿证据搬上 ask 行、本店退役)不是本形:本店在
17
+ * 协议不上场的 local 部署上也在,那里没有 ask 行可并。本店把 ask **铸造 / 决议 / 腿闭**三类事件
14
18
  * append 进磁盘账本(挂线点 = `ToolApprovalCoordinator`,见 `askAudit` ctor opt),boot 时收敛器把
15
19
  * 孤儿行标 `crashed_before_park` 终态成因注,`GET /v1/approvals` 的 additive 键 `crashConverged` 供
16
20
  * 操作员追溯。
@@ -165,8 +169,11 @@
165
169
  */
166
170
  import type { HostDecision } from "./host-decision.js";
167
171
  import { AppendLog } from "@sema-agent/core";
172
+ import type { HolderGoneReason, VOID_REASONS } from "./approval-deny-reasons.js";
168
173
  /** 数据子目录名(挂在 local 数据根下,与 `workflow-notify/` 同层同形)。 */
169
174
  export declare const APPROVAL_ASK_AUDIT_DIR = "approval-ask-audit";
175
+ /** 账本文件名(目录内唯一一只;启动收敛据它判「盘上有没有前世审计证据」,见 `approval-crash-convergence.ts` 的 `openAuditEvidenceForConvergence`)。 */
176
+ export declare const APPROVAL_ASK_AUDIT_LEDGER = "ledger.jsonl";
170
177
  /** 崩溃收敛行的读面上限(新前旧后截断;有界 wire —— 读面不许随月份线性长)。 */
171
178
  export declare const CRASH_CONVERGED_LIST_MAX = 200;
172
179
  /** 追加口的窄形(core `AppendLog` 的 `append` / `close`)—— 店的测试注入席({@link FileApprovalAskAuditStore} 构造器 `internals.appendLog`)按它造替身。 */
@@ -193,7 +200,7 @@ export interface AuditAskInput {
193
200
  * **计坏行**(`corruptLines`,坏值响亮律 —— 不静默读成「没写成因」),但**不丢该 settle 行的结清**(F4:outcome / parkRouted 照认 ——
194
201
  * 因为一个成因词把整条结清行丢掉,会把一张已结清的卡收成 pending 孤儿,方向比「少认一个成因」更坏)。
195
202
  */
196
- export type AuditShutdownCause = "shutdown_before_park";
203
+ export type AuditShutdownCause = typeof VOID_REASONS.HOLDER_SHUTDOWN;
197
204
  /**
198
205
  * 成因词的读盘词表(F4:`satisfies Record<AuditShutdownCause, true>` ⇒ 闭集加一个词而本表没跟 = **编译红**;判定走 `Object.hasOwn`,
199
206
  * 原型链上的名字不算词 —— CLAUDE.md 词表律)。唯一读者 = {@link readShutdownCause}。
@@ -202,9 +209,11 @@ export declare const AUDIT_SHUTDOWN_CAUSE_WORDS: {
202
209
  readonly shutdown_before_park: true;
203
210
  };
204
211
  /** 收敛行 `cause` 的闭集(`GET /v1/approvals` `crashConverged[].cause`):崩溃孤儿 / 有序关停截断的腿(S-929 +1;与 `crashed_before_park` 同形)。
212
+ * 🔴 **词表属主 = `approval-deny-reasons.ts` 的 `VOID_REASONS`**(`HOLDER_CRASHED` / `HOLDER_SHUTDOWN`):启动收敛把同一问的问账行收成 VOID 时
213
+ * `deniedReason` 写的就是这两个词 —— 同一问在 `crashConverged` 与决断口 410 体上只有一个成因词(本型不再自带一份字面量)。
205
214
  * wire 值闭集登记在 `trace/self-minted-wire.ts` 的 `crashConvergedCause`(`satisfies Record<CrashConvergedCause, …>` 双向绑定 ⇒ 加词不登记 = tsc 红,
206
215
  * wire 变更清单从那张表 diff —— F3)。 */
207
- export type CrashConvergedCause = "crashed_before_park" | AuditShutdownCause;
216
+ export type CrashConvergedCause = HolderGoneReason;
208
217
  /** 决议点入参(`settle()` 公共咽喉挂线;五臂全过,不漏窗到期/断连/取消)。 */
209
218
  export interface AuditSettleInput {
210
219
  approvalId: string;
@@ -287,7 +296,11 @@ export interface CrashConvergedRow {
287
296
  /** 铸造时刻(键名照 `livePending[].ts`)。 */
288
297
  ts: number;
289
298
  expiresAtMs: number;
290
- /** 终态 DENIED 同码(稿 §2 (c) 施工形;SQL 车道收敛器的 fail-closed DENIED 同向)。 */
299
+ /**
300
+ * 收敛行的判别常量:这张卡**已不可再决**(wire 形不变)。⚠️ 协议上场的部署上,同一问在 ask 账里是 `VOID` + `deniedReason` = 本行 `cause`
301
+ * (决断口 410 `gone.ask` 读的是那一行)—— 「能不能决」两面一致,但终态的**名字**这里是 `denied`、ask 行是 `VOID`:这是登记在案的残留,
302
+ * 不是语义(改这个值会让把它当闭集判别式读的消费端整行丢弃,须与消费端同窗改)。
303
+ */
291
304
  decision: "denied";
292
305
  /** 成因注(闭集 {@link CrashConvergedCause}):`crashed_before_park` = 进程崩了;`shutdown_before_park`(S-929)= 腿被**有序关停**截断
293
306
  * (判据见头注「收敛行的 `cause`」)。两者的 `orphanState` / `resumeSafe` 判据相同。 */
@@ -420,7 +433,7 @@ export declare class FileApprovalAskAuditStore implements ApprovalAskAuditSink,
420
433
  */
421
434
  recordLateSettle(input: AuditLateSettleInput): void;
422
435
  /**
423
- * boot 崩溃收敛器(装配层在构造后、协调器开始服务前调一次;幂等跨 boot)。
436
+ * boot 崩溃收敛器的**本店半场**(由 `approval-crash-convergence.ts` 的启动收敛在任何卡开始之前调一次;幂等跨 boot)。
424
437
  * 判据 = 头注「boot 收敛判据」段(一处成文,不复述)。返回两分臂计数 + 坏行数供 boot 日志。
425
438
  */
426
439
  convergeCrashedAsks(): {
@@ -429,6 +442,12 @@ export declare class FileApprovalAskAuditStore implements ApprovalAskAuditSink,
429
442
  inconsistent: number;
430
443
  corruptLines: number;
431
444
  };
445
+ /**
446
+ * 一问的**成因词**(启动收敛的唯一判点,`approval-crash-convergence.ts` 读它写 ask 行的 `deniedReason`;本店收敛行的 `cause` 走同一只
447
+ * {@link causeOf})。前世账本里没有这只 `approvalId` 的铸造行(崩在 ask 账落行之后、本店铸造行之前,或本店那时不在)⇒ `crashed_before_park`:
448
+ * 没有有序关停的证据,就不说它是有序关停。判据读的是构造时冻结的前世账本 ⇒ 同一份账本跨几次启动恒得同一个词。
449
+ */
450
+ causeFor(approvalId: string): CrashConvergedCause;
432
451
  /** S-929 收敛行的成因注(判据见头注「收敛行的 `cause`」):组的最后一个事件行之后、收敛行(若已收敛)之前,有本 taskId 的
433
452
  * 有序关停记号 ⇒ `shutdown_before_park`;否则 `crashed_before_park`。 */
434
453
  private causeOf;
@@ -4,6 +4,7 @@ import { AppendLog, ensureDir } from "@sema-agent/core";
4
4
  import { recordFailOpen } from "./observability/fail-open.js";
5
5
  import { replayJsonlSkipping } from "./observability/corrupt-read-seat.js";
6
6
  export const APPROVAL_ASK_AUDIT_DIR = "approval-ask-audit";
7
+ export const APPROVAL_ASK_AUDIT_LEDGER = "ledger.jsonl";
7
8
  export const CRASH_CONVERGED_LIST_MAX = 200;
8
9
  export const AUDIT_SHUTDOWN_CAUSE_WORDS = { shutdown_before_park: true };
9
10
  export function buildCheckpointAnchorHash(token) {
@@ -190,7 +191,7 @@ export class FileApprovalAskAuditStore {
190
191
  this.openLog = internals?.appendLog ?? ((p) => new AppendLog(p));
191
192
  const dir = join(root, APPROVAL_ASK_AUDIT_DIR);
192
193
  ensureDir(dir);
193
- this.ledgerPath = join(dir, "ledger.jsonl");
194
+ this.ledgerPath = join(dir, APPROVAL_ASK_AUDIT_LEDGER);
194
195
  this.replay();
195
196
  }
196
197
  corruptLines = 0;
@@ -375,6 +376,10 @@ export class FileApprovalAskAuditStore {
375
376
  this.convergedRows.length = Math.min(this.convergedRows.length, CRASH_CONVERGED_LIST_MAX);
376
377
  return { pending, decided, inconsistent, corruptLines: this.corruptLines };
377
378
  }
379
+ causeFor(approvalId) {
380
+ const g = this.replayed.get(approvalId);
381
+ return g?.ask === undefined ? "crashed_before_park" : this.causeOf(g);
382
+ }
378
383
  causeOf(g) {
379
384
  const after = g.settleLine ?? g.askLine ?? 0;
380
385
  const before = g.convergedLine ?? Number.POSITIVE_INFINITY;
@@ -269,6 +269,23 @@ export type ProbeCauseProjection = z.infer<typeof ProbeCauseSchema>;
269
269
  * (截出来的是另一个机器码);`shown` 超限 ⇒ **截条目**,`total` 逐字保真。
270
270
  */
271
271
  export declare function readProbeCause(req: unknown): ProbeCauseProjection | undefined;
272
+ /**
273
+ * `AskRequest.probeReason`(core 5.33.0 [ref]:`probeCause` 的**散文**兄弟 —— 探针对「为什么没清掉这一问」的一句话)的
274
+ * **边界窄读**,活卡帧(`ToolApprovalFrame.probeReason`)的**唯一**铸造点。
275
+ *
276
+ * 🔴 **只有活卡帧**:`card_json` / 呈卡帧不携本键(`ApprovalCardSchema` 是 `.strict()` 的耐久信封,加键 = 回滚窗内旧二进制读不动新行,
277
+ * 要另行裁定)。耐久面的对偶 = `gate.riskDescriptor.probeReason`(park 行整体透传,出门前过 `redactRiskDescriptor` 的 `text` 叶,
278
+ * 与本函数同一只脱敏规则 ⇒ 对同一句话两面同值)。
279
+ *
280
+ * 姿势逐条(与 {@link readProbeCause} / {@link readRuleEvidence} 同款分工):
281
+ * · `safeParse`,非串 ⇒ 按缺席处置(宪法 [ref]);空串 / 纯空白 ⇒ 缺席(core 自己也只在 `trim() !== ""` 时铸)。
282
+ * · 长度:超过 {@link MAX_PROBE_REASON_CODEPOINTS} ⇒ **整只丢,不截**(判据与 `probeCause.code` 超限同形);
283
+ * 先判长度再脱敏 ⇒ 喂给脱敏器的永远是个短串(脱敏器的 RangeError 面是超大输入)。
284
+ * · 内容族(部署自写的探针散文,人眼信任边界上的文本)⇒ `redactSecrets`。脱敏后**不再截**:耐久面同样不截,
285
+ * 两面逐字节同值比「帧上短一截」重要(`persistedRuleShadowed` R2-[medium] 的反面:那里有卡面要对齐,这里没有)。
286
+ * · 缺席 ≠「没有原因」:只有 maybe 档收紧且探针真给了 reason 的 ask 才有它。
287
+ */
288
+ export declare function readProbeReason(req: unknown): string | undefined;
272
289
  /**
273
290
  * [ref](core 7.19.0)—— `readRootCandidate.dir` 的**上限**。
274
291
  *
@@ -512,26 +529,6 @@ declare const DenialLimitFallbackSchema: z.ZodObject<{
512
529
  autoDenyAfterMs: z.ZodNumber;
513
530
  }, z.core.$strict>;
514
531
  export type DenialLimitFallback = z.infer<typeof DenialLimitFallbackSchema>;
515
- /**
516
- * ⚰️ **退役成员的形(server 7.69.0–7.71.0 铸过,7.72.0 起永不再铸)** —— core 7.14.0 [ref] 把
517
- * `classifierUnavailable` 从 ask 侧**整族**删掉(事实改骑 `GateDisposition.denied.cause`),所以本仓
518
- * 的**铸造**腿(`buildApprovalCard` / 活卡帧 / inbox 行)本批全部摘掉;窄读函数
519
- * `readClassifierUnavailable` 与对 core 型面的合规钉连同它们的素材一起删(素材没了,钉的两端只剩一端)。
520
- *
521
- * 🔴 **这只 schema 本身留着,而且是承重的,不是没删干净**:`ApprovalCardSchema` 是 `.strict()`,
522
- * 而它同时是 `approval_ask.card_json` 这张**耐久列**的读契约(读侧三个 `safeParse`:重放腿、幂等重入、
523
- * 对账扫描)。把成员从 strict schema 上删掉 ⇒ 7.69.0–7.71.0 铸下、升级时**仍然 pending** 的那些行
524
- * 当场判假 ⇒ 「跳过 + 一次 warn」:人手上那张卡在重连后变不回来、幂等重入退成 park。
525
- * ⇒ 一条耐久投影的 schema **必须读得懂它自己写过的每一版**;写侧只写当前引擎供得出的键,读侧接受
526
- * 全部历史成员。这不是给退役键开特例,这是 strict + durable 这一组合的内生义务(同样的处置将适用于
527
- * 未来任何一次键退役)。
528
- *
529
- * 形保持原样(单成员 `cause` 必填、`.strict()`):它描述的是**已经写在库里的字节**,不许再动。
530
- */
531
- declare const ClassifierUnavailableSchema: z.ZodObject<{
532
- cause: z.ZodString;
533
- }, z.core.$strict>;
534
- export type ClassifierUnavailable = z.infer<typeof ClassifierUnavailableSchema>;
535
532
  /**
536
533
  * `AskRequest.denialLimitFallback`(core 7.4.0 [ref])的**边界窄读** —— 活卡帧与 `card_json` 的唯一铸造点
537
534
  * (与 {@link readRuleOffersAbsence} / {@link readProbeCause} 同款分工),两面结构性同值。
@@ -569,6 +566,7 @@ export declare const ApprovalCardSchema: z.ZodObject<{
569
566
  requiresRealApproval: z.ZodBoolean;
570
567
  }, z.core.$strict>;
571
568
  governanceForced: z.ZodOptional<z.ZodLiteral<true>>;
569
+ rememberRefused: z.ZodOptional<z.ZodLiteral<true>>;
572
570
  inputHasBidi: z.ZodOptional<z.ZodLiteral<true>>;
573
571
  persistedRuleShadowed: z.ZodOptional<z.ZodString>;
574
572
  ruleOffers: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
@@ -651,8 +649,8 @@ export declare const ApprovalCardSchema: z.ZodObject<{
651
649
  }, z.core.$strict>>>;
652
650
  personalRuleDotsAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
653
651
  }, z.core.$strict>>;
654
- askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary">>;
655
- classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary">>>;
652
+ askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>;
653
+ classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>>;
656
654
  fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
657
655
  sourceTaskId: z.ZodOptional<z.ZodString>;
658
656
  sourceAgentName: z.ZodOptional<z.ZodString>;
@@ -684,6 +682,7 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
684
682
  requiresRealApproval: z.ZodBoolean;
685
683
  }, z.core.$strict>;
686
684
  governanceForced: z.ZodOptional<z.ZodLiteral<true>>;
685
+ rememberRefused: z.ZodOptional<z.ZodLiteral<true>>;
687
686
  inputHasBidi: z.ZodOptional<z.ZodLiteral<true>>;
688
687
  persistedRuleShadowed: z.ZodOptional<z.ZodString>;
689
688
  ruleOffers: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
@@ -766,8 +765,8 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
766
765
  }, z.core.$strict>>>;
767
766
  personalRuleDotsAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
768
767
  }, z.core.$strict>>;
769
- askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary">>;
770
- classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "ordinary">>>;
768
+ askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>;
769
+ classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>>;
771
770
  fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
772
771
  sourceTaskId: z.ZodOptional<z.ZodString>;
773
772
  sourceAgentName: z.ZodOptional<z.ZodString>;
@@ -803,6 +802,8 @@ export interface ApprovalCardSource {
803
802
  /** [ref]/[ref]:治理来源标 —— 与 wire 帧**同一份素材**(`ToolApprovalFrame` 结构上满足本接口),
804
803
  * 于是 live 帧 / `card_json` / 重放帧三面同源,不是三处各判一遍。 */
805
804
  governanceForced?: true;
805
+ /** 7.116.0 S4:「本会话放行」不会被记住(帧上同名键,见 `ApprovalCardSchema.rememberRefused`)。 */
806
+ rememberRefused?: true;
806
807
  /** E-14:bidi 在场位 —— 与 wire 帧**同一份素材**(`ToolApprovalFrame` 结构上满足本接口)⇒ live 帧 /
807
808
  * `approval_request` / `card_json` / 重放帧四面同源,不是四处各扫一遍。 */
808
809
  inputHasBidi?: true;
@@ -160,6 +160,18 @@ export function readProbeCause(req) {
160
160
  ...(raw.further !== undefined ? { further: family(raw.further) } : {}),
161
161
  };
162
162
  }
163
+ const MAX_PROBE_REASON_CODEPOINTS = 201;
164
+ const ProbeReasonEnvelopeSchema = z.object({ probeReason: z.string().optional() });
165
+ export function readProbeReason(req) {
166
+ const parsed = ProbeReasonEnvelopeSchema.safeParse(req);
167
+ const raw = parsed.success ? parsed.data.probeReason : undefined;
168
+ if (raw === undefined || raw.trim() === "")
169
+ return undefined;
170
+ if ([...raw].length > MAX_PROBE_REASON_CODEPOINTS)
171
+ return undefined;
172
+ const redacted = redactSecrets(raw);
173
+ return redacted === "" ? undefined : redacted;
174
+ }
163
175
  export { READ_ROOT_CANDIDATE_DIR_MAX };
164
176
  const ReadRootCandidateEnvelopeSchema = z.object({
165
177
  readRootCandidate: z.object({ dir: z.string().min(1), clearsThisAsk: z.literal(true), covers: z.literal("exact").optional() }).optional(),
@@ -342,6 +354,7 @@ export const ApprovalCardSchema = z
342
354
  })
343
355
  .strict(),
344
356
  governanceForced: z.literal(true).optional(),
357
+ rememberRefused: z.literal(true).optional(),
345
358
  inputHasBidi: z.literal(true).optional(),
346
359
  persistedRuleShadowed: z.string().max(MAX_RULE_TEXT_CHARS).optional(),
347
360
  ruleOffers: z.array(RuleOfferSchema).max(MAX_RULE_OFFERS).optional(),
@@ -415,6 +428,7 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
415
428
  requiresRealApproval,
416
429
  },
417
430
  ...(source.governanceForced === true ? { governanceForced: true } : {}),
431
+ ...(source.rememberRefused === true ? { rememberRefused: true } : {}),
418
432
  ...(source.inputHasBidi === true ? { inputHasBidi: true } : {}),
419
433
  ...(shadowedRule !== undefined && shadowedRule !== "" ? { persistedRuleShadowed: shadowedRule } : {}),
420
434
  ...(probeCause !== undefined ? { probeCause } : {}),
@@ -0,0 +1,60 @@
1
+ import type { ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
2
+ import { FileApprovalAskAuditStore } from "./approval-ask-audit-store.js";
3
+ import { type HolderGoneReason } from "./approval-deny-reasons.js";
4
+ import { type RunStoreReader } from "./user-input-ledger.js";
5
+ import type { Logger } from "./observability/logger.js";
6
+ /** 启动收敛的读数(进启动日志)。 */
7
+ export interface CrashConvergenceReport {
8
+ /** 审计半场:pending 臂(崩时人还没答)/ decided 臂(批了、执行窗内崩)/ 审计断档组 / 坏行 —— 语义见审计店头注。 */
9
+ readonly pending: number;
10
+ readonly decided: number;
11
+ readonly inconsistent: number;
12
+ readonly corruptLines: number;
13
+ /** ask 账半场:按成因词计的、本次收成 VOID 的前世 `STREAM_PENDING` 行数。 */
14
+ readonly asksVoided: Readonly<Record<HolderGoneReason, number>>;
15
+ /** 其中卡信封读不出、成因词是回落(`crashed_before_park`)的行数(同时记 fail-open tag)。 */
16
+ readonly causeUnreadable: number;
17
+ }
18
+ /** 本次收成 VOID 的行数合计 —— 由成因闭集派生(对 `asksVoided` 的每个词求和;闭集加词不需要改这里)。 */
19
+ export declare function totalAsksVoided(report: Pick<CrashConvergenceReport, "asksVoided">): number;
20
+ /** 本函数读写的那几口(窄形:测试可以直给一只内存店)。 */
21
+ export type CrashConvergenceAskPort = Pick<ApprovalAskStore, "listByState" | "transitionAsk">;
22
+ /** 审计店的两口(收敛半场 + 成因判点)。 */
23
+ export type CrashConvergenceAuditPort = Pick<FileApprovalAskAuditStore, "convergeCrashedAsks" | "causeFor">;
24
+ /**
25
+ * 启动收敛读哪一份**审计证据**:活的审计店在场就用它;不在场(这一世 `TOOL_APPROVAL_ENABLED` 关着 ⇒ 装配三合取不成立)而盘上有前世的审计账本
26
+ * ⇒ 开一只读它 —— 成因是盘上证据的函数,不随这一世的开关变(否则前世有序关停截断的问在这一世被写成 `crashed_before_park`,审批重新打开那天审计读面
27
+ * 对同一问列 `shutdown_before_park`,两本账永久分家)。盘上没有账本(审批从没开过)⇒ 不铸店、不建目录,证据缺席。
28
+ * 开出来的这一只只供收敛:不进协调器、不进 `crashConverged` 读面(那一面在审批关着时本就缺席)。
29
+ */
30
+ export declare function openAuditEvidenceForConvergence(live: CrashConvergenceAuditPort | undefined, localDataRoot: string | undefined): {
31
+ audit: CrashConvergenceAuditPort | undefined;
32
+ release: () => void;
33
+ };
34
+ /**
35
+ * 启动收敛(见文件头)。只在数据根独占的车道上调,且必须在本进程铸出第一问之前。幂等跨启动:收过的行已不是 `STREAM_PENDING`,
36
+ * 审计半场有自己的收敛锚;成因读冻结的前世审计账 ⇒ 半途崩溃再启,剩下的行得到与第一次相同的词。
37
+ */
38
+ export declare function convergeCrashedAsksAtBoot(input: {
39
+ askStore: CrashConvergenceAskPort | undefined;
40
+ audit: CrashConvergenceAuditPort | undefined;
41
+ now?: () => number;
42
+ }): Promise<CrashConvergenceReport>;
43
+ /**
44
+ * **启动装配的这一步**(stage-07 调一次,孤儿 run 回收之后):车道判据 → 审计证据 → 收敛 → 启动日志。
45
+ * · 只在数据根独占的车道上跑 —— 车道事实与用户输入受理账本同一只 `writerGoneEvidenceFor`(local ⇒ `exclusive_data_root`);SQL 车道整步不做。
46
+ * · ask 店取 `backend.approvalAsk()`,**不看协议开关**:前世开着协议留下的行,今世关着也照收(否则重新打开协议那天它们又变得可决)。
47
+ * · 审计证据经 {@link openAuditEvidenceForConvergence}(这一世的审批开关不改变成因)。
48
+ * · 有任何收敛 / 坏行 ⇒ 一行 warn `approval_ask_audit_crash_converged`(零动作不打)。收敛写入抛错 ⇒ 抛出 ⇒ 拒启。
49
+ */
50
+ export declare function runBootCrashConvergence(ctx: {
51
+ backend: {
52
+ readonly kind: "mysql" | "pg" | "local";
53
+ approvalAsk(): CrashConvergenceAskPort;
54
+ } | undefined;
55
+ runStore: RunStoreReader | undefined;
56
+ liveAudit: CrashConvergenceAuditPort | undefined;
57
+ localDataRoot: string | undefined;
58
+ logger: Pick<Logger, "warn">;
59
+ }): Promise<CrashConvergenceReport | undefined>;
60
+ //# sourceMappingURL=approval-crash-convergence.d.ts.map
@@ -0,0 +1,75 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { APPROVAL_ASK_AUDIT_DIR, APPROVAL_ASK_AUDIT_LEDGER, FileApprovalAskAuditStore } from "./approval-ask-audit-store.js";
4
+ import { ApprovalCardEnvelopeSchema } from "./approval-card.js";
5
+ import { VOID_REASONS } from "./approval-deny-reasons.js";
6
+ import { writerGoneEvidenceFor } from "./user-input-ledger.js";
7
+ import { recordFailOpen } from "./observability/fail-open.js";
8
+ const CONVERGE_PAGE = 500;
9
+ export function totalAsksVoided(report) {
10
+ return Object.values(report.asksVoided).reduce((sum, n) => sum + n, 0);
11
+ }
12
+ export function openAuditEvidenceForConvergence(live, localDataRoot) {
13
+ if (live !== undefined)
14
+ return { audit: live, release: () => { } };
15
+ if (localDataRoot === undefined || !existsSync(join(localDataRoot, APPROVAL_ASK_AUDIT_DIR, APPROVAL_ASK_AUDIT_LEDGER)))
16
+ return { audit: undefined, release: () => { } };
17
+ const opened = new FileApprovalAskAuditStore(localDataRoot);
18
+ return { audit: opened, release: () => opened.close() };
19
+ }
20
+ export async function convergeCrashedAsksAtBoot(input) {
21
+ const now = input.now ?? Date.now;
22
+ const audited = input.audit?.convergeCrashedAsks() ?? { pending: 0, decided: 0, inconsistent: 0, corruptLines: 0 };
23
+ const asksVoided = { [VOID_REASONS.HOLDER_CRASHED]: 0, [VOID_REASONS.HOLDER_SHUTDOWN]: 0 };
24
+ let causeUnreadable = 0;
25
+ const store = input.askStore;
26
+ if (store !== undefined) {
27
+ const seen = new Set();
28
+ for (;;) {
29
+ const page = (await store.listByState("STREAM_PENDING", CONVERGE_PAGE)).filter((r) => !seen.has(r.askId));
30
+ if (page.length === 0)
31
+ break;
32
+ for (const row of page) {
33
+ seen.add(row.askId);
34
+ const envelope = ApprovalCardEnvelopeSchema.safeParse(row.cardJson);
35
+ if (!envelope.success) {
36
+ causeUnreadable += 1;
37
+ recordFailOpen("server.approval-crash-convergence.cause-unreadable", `ask ${row.askId} (task ${row.taskId}): card envelope unreadable — cause defaulted to ${VOID_REASONS.HOLDER_CRASHED}`);
38
+ }
39
+ const reason = envelope.success && input.audit !== undefined ? input.audit.causeFor(envelope.data.approvalId) : VOID_REASONS.HOLDER_CRASHED;
40
+ let won;
41
+ try {
42
+ won = await store.transitionAsk(row.askId, "STREAM_PENDING", "VOID", { deniedReason: reason, updatedAtMs: now() });
43
+ }
44
+ catch (err) {
45
+ throw new Error(`approval crash convergence: could not close the previous process's pending approval ask ${row.askId} (task ${row.taskId}): ` +
46
+ `${err instanceof Error ? err.message : String(err)} — refusing to start, because serving now would leave a card that nothing waits for ` +
47
+ "decidable with a 200. Fix the data root (disk full / read-only?) and start again.");
48
+ }
49
+ if (won)
50
+ asksVoided[reason] += 1;
51
+ }
52
+ }
53
+ }
54
+ return { ...audited, asksVoided, causeUnreadable };
55
+ }
56
+ export async function runBootCrashConvergence(ctx) {
57
+ const { backend, runStore, logger } = ctx;
58
+ if (backend === undefined || runStore === undefined || writerGoneEvidenceFor(backend.kind, runStore).kind !== "exclusive_data_root")
59
+ return undefined;
60
+ const evidence = openAuditEvidenceForConvergence(ctx.liveAudit, ctx.localDataRoot);
61
+ const converged = await convergeCrashedAsksAtBoot({ askStore: backend.approvalAsk(), audit: evidence.audit }).finally(evidence.release);
62
+ if (converged.pending > 0 || converged.decided > 0 || converged.inconsistent > 0 || converged.corruptLines > 0 || totalAsksVoided(converged) > 0) {
63
+ logger.warn("approval_ask_audit_crash_converged", {
64
+ pending: converged.pending,
65
+ decided: converged.decided,
66
+ inconsistent: converged.inconsistent,
67
+ corruptLines: converged.corruptLines,
68
+ asksVoided: converged.asksVoided,
69
+ causeUnreadable: converged.causeUnreadable,
70
+ note: "approval asks left pending by the previous process were converged: each such ask is now VOID in the ask ledger (the decision endpoint answers 410 gone.ask with this cause as deniedReason) and is listed under GET /v1/approvals crashConverged when the audit ledger saw it (cause crashed_before_park = the process crashed; cause shutdown_before_park = an orderly shutdown cut the leg off before it closed or before its park committed)",
71
+ });
72
+ }
73
+ return converged;
74
+ }
75
+ //# sourceMappingURL=approval-crash-convergence.js.map
@@ -16,6 +16,8 @@
16
16
  * 值域不相交这条纪律的用处(`ALL_TERMINAL_REASONS` 的机器判据钉住不相交性)。
17
17
  *
18
18
  * 写点纪律(grep 钉):全仓 `DENIED` 只有**一个**写点 = 收敛器判据 2 的非取消臂(`approval-reconciler.ts`)。
19
+ * 启动收敛(进程没了)落的是 VOID 不是 DENIED:状态机上 `STREAM_PENDING` 没有到 DENIED 的边(DENIED 只在「已路由去 park」之后),
20
+ * 而承载它的进程没了 = 这条腿死了 = abort 语义。
19
21
  */
20
22
  /** `DENIED` 终态的归因闭集(v1 单员)。 */
21
23
  export declare const DENY_REASONS: {
@@ -49,8 +51,19 @@ export declare const VOID_REASONS: {
49
51
  /** 判据 5:任何在 `STREAM_APPROVAL_ORPHAN_TTL_MS` 内仍未收敛的 `PARKING` 行(量 immutable 的
50
52
  * `createdAtMs`,§9 C5)—— 约束①(遗孤最终可判)的最后兜底,带一条 warn。 */
51
53
  readonly ORPHAN_TTL_EXCEEDED: "orphan_ttl_exceeded";
54
+ /**
55
+ * 启动收敛(`approval-crash-convergence.ts`,local 车道):**承载这一问的进程没了** —— 前世进程崩溃 / 被杀,留下的
56
+ * `STREAM_PENDING` 行在下一次启动、任何卡能开始之前收成 VOID。证据是数据根 `LOCK`(前世写者全死),不是「等够久了」。
57
+ * 🔴 值与 `GET /v1/approvals` `crashConverged[].cause` 是**同一个词**(审计店的 `CrashConvergedCause` 从本表取型):
58
+ * 同一问在两个读面上只有一个成因词。
59
+ */
60
+ readonly HOLDER_CRASHED: "crashed_before_park";
61
+ /** 同上,但承载它的腿是被**有序关停**截断的(审计账本上有本腿的关停记号;关停收尾没能在宽限窗内把这张卡结清)。 */
62
+ readonly HOLDER_SHUTDOWN: "shutdown_before_park";
52
63
  };
53
64
  export type VoidReason = (typeof VOID_REASONS)[keyof typeof VOID_REASONS];
65
+ /** 「承载这一问的进程没了」的两个成因词(启动收敛写进问账行 `deniedReason`,审计收敛行 `cause` 读同一型)。 */
66
+ export type HolderGoneReason = typeof VOID_REASONS.HOLDER_CRASHED | typeof VOID_REASONS.HOLDER_SHUTDOWN;
54
67
  /** 两组的并集(机器判据用:值域不相交 + 列宽 ≤ 64 的钉子在 test/approval-reconciler.test.ts)。 */
55
68
  export declare const ALL_TERMINAL_REASONS: readonly string[];
56
69
  //# sourceMappingURL=approval-deny-reasons.d.ts.map
@@ -7,6 +7,8 @@ export const VOID_REASONS = {
7
7
  BATCH_BOUND_ELSEWHERE: "batch_bound_elsewhere",
8
8
  ADHOC_LEG_NO_DURABLE_DOMAIN: "adhoc_leg_no_durable_domain",
9
9
  ORPHAN_TTL_EXCEEDED: "orphan_ttl_exceeded",
10
+ HOLDER_CRASHED: "crashed_before_park",
11
+ HOLDER_SHUTDOWN: "shutdown_before_park",
10
12
  };
11
13
  export const ALL_TERMINAL_REASONS = [...Object.values(DENY_REASONS), ...Object.values(VOID_REASONS)];
12
14
  //# sourceMappingURL=approval-deny-reasons.js.map
package/dist/approval.js CHANGED
@@ -4,6 +4,7 @@ import { ASK_USER_QUESTION_TOOL_NAME } from "./approval-content-kind.js";
4
4
  import { isExplicitPosture } from "./posture-source.js";
5
5
  import { handsOnWorkerFilePlane } from "./execution-lane-caps.js";
6
6
  import { heldRuleName } from "./tool-rule-names.js";
7
+ import { answerGrantsBeyondCall } from "./standing-answer-grant.js";
7
8
  export function hasApprovalListIntent(config) {
8
9
  return (config.approvalRequire.length > 0 ||
9
10
  config.approvalDeny.length > 0 ||
@@ -74,6 +75,9 @@ function createNameSetMatcher(names) {
74
75
  return (toolName) => exact.has(toolName);
75
76
  return (toolName) => exact.has(toolName) || covering.some((rule) => namespacedRuleNameCovers(rule, toolName));
76
77
  }
78
+ function grantedByRule() {
79
+ return { action: "allow", decisionReason: "rule" };
80
+ }
77
81
  export function createDurableAskPolicy(opts) {
78
82
  const require = createNameSetMatcher(opts.requireApproval);
79
83
  const deny = createNameSetMatcher(opts.deny ?? []);
@@ -94,7 +98,7 @@ export function createDurableAskPolicy(opts) {
94
98
  if (remEarly !== undefined && remEarly <= 0) {
95
99
  return operatorAlwaysHumanAsk("RESOURCE_SUSPEND budget exhausted", `durable resource budget exhausted — human approval required to run "${req.toolName}"`);
96
100
  }
97
- if (opts.exempt && heldRuleName(req, isQuestionTool) === undefined) {
101
+ if (opts.exempt && heldRuleName(req, isQuestionTool) === undefined && !answerGrantsBeyondCall(req)) {
98
102
  return (async () => {
99
103
  let exempted = false;
100
104
  try {
@@ -105,7 +109,7 @@ export function createDurableAskPolicy(opts) {
105
109
  }
106
110
  if (exempted) {
107
111
  opts.onExempted?.(req.toolName, req.toolName);
108
- return { action: "allow" };
112
+ return grantedByRule();
109
113
  }
110
114
  return decideRequired(req, requireHit);
111
115
  })();
@@ -122,7 +126,7 @@ export function createDurableAskPolicy(opts) {
122
126
  }
123
127
  if (heldRuleName(req, isQuestionTool) === undefined && budget > 0 && autoApproved < budget) {
124
128
  autoApproved++;
125
- return { action: "allow" };
129
+ return grantedByRule();
126
130
  }
127
131
  return operatorAsk(`APPROVAL_REQUIRE(${requireHit})`, `approval required to run "${req.toolName}"`);
128
132
  }
@@ -43,18 +43,6 @@ export function createLiveCoordinators(ctx) {
43
43
  const approvalAskAudit = config.toolApprovalEnabled && backend?.kind === "local" && config.localDataRoot !== undefined
44
44
  ? new FileApprovalAskAuditStore(config.localDataRoot)
45
45
  : undefined;
46
- if (approvalAskAudit) {
47
- const converged = approvalAskAudit.convergeCrashedAsks();
48
- if (converged.pending > 0 || converged.decided > 0 || converged.inconsistent > 0 || converged.corruptLines > 0) {
49
- logger.warn("approval_ask_audit_crash_converged", {
50
- pending: converged.pending,
51
- decided: converged.decided,
52
- inconsistent: converged.inconsistent,
53
- corruptLines: converged.corruptLines,
54
- note: "orphan approval asks from the previous process were converged (cause crashed_before_park = it crashed; cause shutdown_before_park = an orderly shutdown cut the leg off before it closed or before its park committed); see GET /v1/approvals crashConverged",
55
- });
56
- }
57
- }
58
46
  const approvalExemptionStore = backend ? backend.approvalExemption() : undefined;
59
47
  const toolApproval = config.toolApprovalEnabled
60
48
  ? new ToolApprovalCoordinator({
@@ -15,6 +15,7 @@ import { localDockerExecutionEnvFactory } from "../plugins/remote-env-local-dock
15
15
  import { sshExecutionEnvFactory } from "../plugins/remote-env-ssh.js";
16
16
  import { isRemoteScratchpadLane, withRemoteScratchpad } from "../plugins/remote-scratchpad.js";
17
17
  import { FileSchedulerBackend, defaultSchedulerStorePath } from "../plugins/scheduler-support.js";
18
+ import * as facts from "../capability-facts.js";
18
19
  import { materializeAttachmentsInto } from "../plugins/task-attachment-store.js";
19
20
  import { reapOrphanWorktrees, withWorktreeIsolation } from "../plugins/worktree-isolation.js";
20
21
  import { customPkgSourceFromEnv, derivePkgSourceEnv } from "../sandbox-pkg-source.js";
@@ -204,7 +205,7 @@ export function createExecutionEnv(ctx) {
204
205
  }
205
206
  else if (config.remoteExec?.provider === "host") {
206
207
  {
207
- const hostScheduler = config.schedulerEnabled && config.requirePrincipal !== true
208
+ const hostScheduler = facts.hostScheduler(config).available
208
209
  ? new FileSchedulerBackend({
209
210
  ...(config.schedulerStorePath ? { storePath: config.schedulerStorePath } : {}),
210
211
  ...(config.schedulerSessionWakeup === false || config.schedulerSessionLifetime === false
@@ -3,10 +3,11 @@ import type { Logger } from "../observability/logger.js";
3
3
  export interface ProcessExitBoundary {
4
4
  /**
5
5
  * **优化点**,不承担不变量:受控退的收尾链在它自己的终点早按一次子进程腿(理由逐字写在
6
- * `boot/shutdown.ts` 的调用点上 —— 那之后进程还活十秒量级,后台腿的软→硬梯子跑得完)。
6
+ * `boot/shutdown.ts` 的调用点上 —— 调用方随后在退出前等一次后台宽限,后台腿的软→硬梯子跑得完)。
7
7
  * 工作区腿**刻意不给**这条早按:停机中段 `rm -rf` 会把在飞 turn 正在写的目录从它脚下抽掉。
8
+ * 返回本次软停的后台行数(core `reapAllSessionBackground` 的返回;腿抛错 ⇒ 0,warn 已记)—— 调用方据此决定欠不欠宽限。
8
9
  */
9
- reapChildProcessesEarly(): void;
10
+ reapChildProcessesEarly(): number;
10
11
  /** 摘掉本次装配的 `exit` 钩。幂等(同进程内反复装配的调用方=测试必须用它收尾)。 */
11
12
  dispose(): void;
12
13
  }
@@ -5,8 +5,10 @@ import { liveEphemeralHostWorkspaceCount, reapEphemeralHostWorkspaces } from "..
5
5
  const PROCESS_EXIT_INCLUDES_RETAINED = false;
6
6
  export function installProcessExitBoundary({ logger }) {
7
7
  const reapOwnChildProcesses = () => {
8
+ let backgroundSoftStopped = 0;
8
9
  try {
9
10
  const reaped = defaultTaskRegistry.reapAllSessionBackground();
11
+ backgroundSoftStopped = reaped;
10
12
  if (reaped > 0)
11
13
  logger.info("shutdown_background_reaped", { reaped });
12
14
  }
@@ -23,6 +25,7 @@ export function installProcessExitBoundary({ logger }) {
23
25
  catch (err) {
24
26
  logger.warn("shutdown_foreground_reap_failed", { err: String(err) });
25
27
  }
28
+ return backgroundSoftStopped;
26
29
  };
27
30
  const reapBackgroundHard = () => {
28
31
  try {
@@ -9,7 +9,7 @@ import { gateScenarioRequest, mergeUserSkills, selectScenario } from "../capabil
9
9
  import { stampHandsLane, toolPolicyForHands } from "../capabilities/hands-lane.js";
10
10
  import { buildNoticeLaneFacts, shouldDiscloseNoPersistentMemory, withNoPersistentMemoryNotice } from "../capabilities/memory-notice.js";
11
11
  import { applyLongtailDefer } from "../capabilities/tool-defer.js";
12
- import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs } from "../deployment-governance.js";
12
+ import { assertGuardPatternsUsable, buildApprovalGrantNotice, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs } from "../deployment-governance.js";
13
13
  import { singleUserAutoAcceptBaselineApplies } from "../approval.js";
14
14
  import { acceptShellScratchpadDir, buildEnvFacts, egressForRemoteExec, ensureScratchpadDir, resumeFactsForLane, scratchpadCoveringTaskRoot } from "../env-facts.js";
15
15
  import { FleetEventBus } from "../fleet/fleet-bus.js";
@@ -59,6 +59,9 @@ export function createResolveSpec(ctx) {
59
59
  };
60
60
  if (onlySensitiveBaseline)
61
61
  logger.warn(onlySensitiveBaseline.event, onlySensitiveBaseline.fields);
62
+ const approvalGrantNotice = buildApprovalGrantNotice(config);
63
+ if (approvalGrantNotice)
64
+ logger.info(approvalGrantNotice.event, approvalGrantNotice.fields);
62
65
  const resolveSpec = async (body, req, auth, opts) => {
63
66
  assertRequestMcpUnlocked(body.mcpServers, lockedKeys);
64
67
  if (opts?.leg === "fresh")
@@ -439,6 +442,7 @@ export function createResolveSpec(ctx) {
439
442
  return merged && config.toolDeferLongtail ? applyLongtailDefer(merged, true) : merged;
440
443
  })(cap.tools),
441
444
  skills: body.disableSkills === true ? [] : mergeUserSkills(cap.skills, body.skills, logger),
445
+ ...(() => { const v = toolNameListFromBody(body.userInvokedSkills); return v ? { userInvokedSkills: v } : {}; })(),
442
446
  ...(parsedSettings.settings?.skillListingBudgetFraction !== undefined ? { skillListingBudgetFraction: parsedSettings.settings.skillListingBudgetFraction } : {}),
443
447
  ...(parsedSettings.settings?.skillListingMaxDescChars !== undefined ? { skillListingMaxDescChars: parsedSettings.settings.skillListingMaxDescChars } : {}),
444
448
  mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger, opts?.onNotice !== undefined ? { sessionId: auth?.sessionId, emit: opts.onNotice } : undefined),
@@ -96,6 +96,25 @@ export interface ShutdownCtx {
96
96
  * `backend.close()` 之前等它 quiesce —— 在飞的 `put`(审批 park / 资源挂起…,与审批协调器在不在场无关)不许被抽掉连接池。 */
97
97
  parkCommits: ParkCommitWatch | undefined;
98
98
  }
99
+ /**
100
+ * 受控退的**后台宽限**还要等多久(ms)—— hardShutdown 软停后台 shell 之后、`process.exit` 之前,给软停过的行**一次**
101
+ * `HOST_BG_KILL_GRACE_MS` 收完合作清理(host 梯子:SIGTERM → 宽限 → 组 SIGKILL)。
102
+ *
103
+ * 没有这一步时,**无活连接**的干净停机里 `server.close` 回调立刻落地、链上其余步骤都是毫秒级,`exit` 几乎紧跟软停,退出边界钩同拍一枪
104
+ * 组 SIGKILL ⇒ 收到 SIGTERM 后要一点时间停容器 / 刷盘的后台 shell 被腰斩。**只缩不延**的两条:
105
+ * · 起点是软停时刻(不是「别的步骤做完之后」)—— 慢 close / 审批面收尾已经耗掉的时间算进宽限里,链上别的等待不与它相加,取较晚者;
106
+ * · 封顶在 hardShutdown 起点算的**同一个到期点**(`deadlineMs`,与审批面收尾 / 店 quiesce 共读)—— 运维把宽限调得再长,
107
+ * 退出也不晚于到期点,一定早于 15 s 的 `finalExit`,不延长停机总上限。
108
+ * `softStopped` = 本次软停的后台行数(core `reapAllSessionBackground` 的返回;0 ⇒ 没有梯子在跑、不欠宽限,空闲引擎的退出不变慢)。
109
+ * 该数里含后台 agent / workflow 行(core 不拆开数),它们没有 shell 梯子 ⇒ 只有这一形会**多**等(≤ 一个宽限),不会少等。
110
+ */
111
+ export declare function backgroundGraceRemainingMs(a: {
112
+ softStopped: number;
113
+ softStoppedAt: number;
114
+ now: number;
115
+ graceMs: number;
116
+ deadlineMs: number;
117
+ }): number;
99
118
  /**
100
119
  * `installShutdownHandlers` 的**摘除把手**([ref] 二轮重扫件4)。
101
120
  *