@sema-agent/server 7.140.0 → 7.142.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 (296) hide show
  1. package/MIGRATION.md +505 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +142 -25
  5. package/dist/approval-ask-audit-store.d.ts +54 -38
  6. package/dist/approval-ask-audit-store.js +58 -39
  7. package/dist/approval-batch-census.d.ts +27 -0
  8. package/dist/approval-batch-census.js +27 -0
  9. package/dist/approval-card.d.ts +39 -24
  10. package/dist/approval-card.js +6 -3
  11. package/dist/approval-crash-convergence.d.ts +17 -7
  12. package/dist/approval-crash-convergence.js +94 -22
  13. package/dist/approval-deny-reasons.d.ts +34 -30
  14. package/dist/approval-deny-reasons.js +21 -6
  15. package/dist/approval-policy-settlement.d.ts +1 -1
  16. package/dist/approval-reconciler.d.ts +33 -1
  17. package/dist/approval-reconciler.js +43 -40
  18. package/dist/approval.d.ts +15 -0
  19. package/dist/approval.js +10 -0
  20. package/dist/audit.d.ts +4 -2
  21. package/dist/audit.js +2 -2
  22. package/dist/auth-bridge.d.ts +9 -6
  23. package/dist/auth-bridge.js +9 -3
  24. package/dist/boot/adoption.js +2 -1
  25. package/dist/boot/budget-tracing.d.ts +16 -0
  26. package/dist/boot/budget-tracing.js +10 -6
  27. package/dist/boot/device-lane.d.ts +11 -0
  28. package/dist/boot/device-lane.js +16 -0
  29. package/dist/boot/engine-lease.d.ts +24 -3
  30. package/dist/boot/engine-lease.js +9 -2
  31. package/dist/boot/execution-env.d.ts +24 -4
  32. package/dist/boot/execution-env.js +34 -27
  33. package/dist/boot/execution-lane.js +2 -1
  34. package/dist/boot/exit-boundary.js +5 -4
  35. package/dist/boot/memory-boundary.d.ts +2 -2
  36. package/dist/boot/memory-consolidation.d.ts +1 -1
  37. package/dist/boot/memory-consolidation.js +3 -2
  38. package/dist/boot/parked-revive-gate.js +2 -1
  39. package/dist/boot/permission-rules-audit.d.ts +0 -28
  40. package/dist/boot/permission-rules-audit.js +2 -1
  41. package/dist/boot/read-deny-anchor-audit.js +2 -1
  42. package/dist/boot/reapers.d.ts +2 -2
  43. package/dist/boot/reapers.js +4 -3
  44. package/dist/boot/resolve-spec.js +18 -6
  45. package/dist/boot/retention-lane.js +5 -3
  46. package/dist/boot/runner-deps.js +16 -13
  47. package/dist/boot/runtime-caps.js +3 -2
  48. package/dist/boot/session-faces.d.ts +1 -1
  49. package/dist/boot/session-faces.js +8 -6
  50. package/dist/boot/shutdown.js +8 -2
  51. package/dist/boot/stage-04-stores.js +3 -2
  52. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  53. package/dist/boot/stage-05-execution-env.js +11 -7
  54. package/dist/boot/stage-06-runners.d.ts +1 -3
  55. package/dist/boot/stage-06-runners.js +3 -2
  56. package/dist/boot/stage-07-capability-layer.d.ts +1 -2
  57. package/dist/boot/stage-07-capability-layer.js +10 -11
  58. package/dist/boot/stage-08-reapers.d.ts +1 -2
  59. package/dist/boot/stage-08-reapers.js +3 -2
  60. package/dist/boot/stage-09-leader.d.ts +1 -2
  61. package/dist/boot/stage-10-http-server.d.ts +1 -2
  62. package/dist/boot/stage-10-http-server.js +6 -4
  63. package/dist/boot/stores.js +13 -6
  64. package/dist/boot/web-admin.js +18 -17
  65. package/dist/boot/webfetch-summarize-lane.js +2 -1
  66. package/dist/boot/workflow-orchestration.js +3 -2
  67. package/dist/capabilities/builtin-tools.d.ts +2 -1
  68. package/dist/capabilities/builtin-tools.js +2 -4
  69. package/dist/capabilities/composite-members.d.ts +2 -1
  70. package/dist/capabilities/composite-members.js +2 -1
  71. package/dist/capabilities/scenarios.d.ts +1 -1
  72. package/dist/capability-facts.d.ts +33 -0
  73. package/dist/capability-facts.js +32 -0
  74. package/dist/card-void.d.ts +38 -0
  75. package/dist/card-void.js +23 -0
  76. package/dist/config-catalog.js +12 -8
  77. package/dist/config-provider.d.ts +13 -13
  78. package/dist/config-provider.js +19 -33
  79. package/dist/config-types.d.ts +39 -10
  80. package/dist/config.d.ts +6 -0
  81. package/dist/config.js +72 -49
  82. package/dist/delegated-session-owner.d.ts +21 -19
  83. package/dist/delegated-session-owner.js +1 -1
  84. package/dist/device-store.d.ts +7 -0
  85. package/dist/device-store.js +3 -0
  86. package/dist/execution-config-root.d.ts +22 -0
  87. package/dist/execution-config-root.js +35 -0
  88. package/dist/execution-lane-caps.d.ts +50 -2
  89. package/dist/execution-lane-caps.js +90 -13
  90. package/dist/execution-placement.d.ts +3 -0
  91. package/dist/execution-placement.js +7 -0
  92. package/dist/fleet/drain-entry.d.ts +3 -0
  93. package/dist/fleet/fleet-bus.d.ts +6 -3
  94. package/dist/hooks/hook-runner.d.ts +6 -0
  95. package/dist/hooks/hook-runner.js +30 -1
  96. package/dist/hosted-posture.d.ts +8 -0
  97. package/dist/hosted-posture.js +4 -0
  98. package/dist/http/admission.js +3 -3
  99. package/dist/http/caller-trust.d.ts +3 -2
  100. package/dist/http/caller-trust.js +2 -1
  101. package/dist/http/dispatch.js +8 -2
  102. package/dist/http/execution-context.d.ts +2 -2
  103. package/dist/http/idempotency.d.ts +1 -1
  104. package/dist/http/live-frame.d.ts +24 -0
  105. package/dist/http/live-frame.js +16 -0
  106. package/dist/http/principal-gate.d.ts +3 -1
  107. package/dist/http/principal-gate.js +17 -8
  108. package/dist/http/resume-legs.js +42 -29
  109. package/dist/http/route-table.js +4 -0
  110. package/dist/http/routes/approvals-assistant.js +1 -0
  111. package/dist/http/routes/attachments.js +18 -0
  112. package/dist/http/routes/capabilities.js +10 -6
  113. package/dist/http/routes/diagnostics.d.ts +0 -19
  114. package/dist/http/routes/diagnostics.js +16 -2
  115. package/dist/http/routes/memory-bundle.d.ts +2 -2
  116. package/dist/http/routes/memory-bundle.js +1 -1
  117. package/dist/http/routes/memory-compliance.js +1 -1
  118. package/dist/http/routes/memory-policy.js +27 -7
  119. package/dist/http/routes/run-permission-mode.d.ts +75 -0
  120. package/dist/http/routes/run-permission-mode.js +170 -0
  121. package/dist/http/routes/runs.js +3 -3
  122. package/dist/http/routes/session-memory-sync.d.ts +63 -0
  123. package/dist/http/routes/session-memory-sync.js +157 -0
  124. package/dist/http/routes/session-sync.d.ts +3 -6
  125. package/dist/http/routes/session-sync.js +7 -2
  126. package/dist/http/routes/sessions-list.d.ts +2 -1
  127. package/dist/http/routes/sessions.js +5 -4
  128. package/dist/http/routes/tasks.js +70 -71
  129. package/dist/http/run-read-face.d.ts +7 -0
  130. package/dist/http/run-read-face.js +10 -4
  131. package/dist/http/send.d.ts +6 -1
  132. package/dist/http/send.js +10 -3
  133. package/dist/http/server.d.ts +20 -7
  134. package/dist/http/server.js +2 -4
  135. package/dist/http/sse-log.d.ts +3 -8
  136. package/dist/http/stored-body-replay.d.ts +3 -3
  137. package/dist/http/stored-body-replay.js +3 -1
  138. package/dist/http/submit-replay.d.ts +4 -3
  139. package/dist/http/submit-replay.js +2 -2
  140. package/dist/http/sync-submit-body.d.ts +21 -4
  141. package/dist/http/sync-submit-body.js +13 -0
  142. package/dist/identity-source.d.ts +21 -2
  143. package/dist/identity-source.js +6 -0
  144. package/dist/instruction-source-notice.d.ts +7 -0
  145. package/dist/instruction-source-notice.js +6 -6
  146. package/dist/mcp-assertion-header.d.ts +4 -0
  147. package/dist/mcp-assertion-header.js +5 -0
  148. package/dist/memory-incident-sink.d.ts +19 -6
  149. package/dist/memory-incident-sink.js +8 -4
  150. package/dist/memory-posture.d.ts +1 -1
  151. package/dist/memory-scope.d.ts +4 -4
  152. package/dist/memory-scope.js +1 -1
  153. package/dist/memory-sync-client.d.ts +35 -23
  154. package/dist/memory-sync-client.js +122 -52
  155. package/dist/memory-sync-identity.d.ts +24 -0
  156. package/dist/memory-sync-identity.js +57 -0
  157. package/dist/memory-sync.d.ts +7 -1
  158. package/dist/memory-sync.js +6 -3
  159. package/dist/observability/fail-open.d.ts +57 -12
  160. package/dist/observability/fail-open.js +50 -9
  161. package/dist/observability/metrics.js +0 -1
  162. package/dist/observability/run-terminal.d.ts +93 -31
  163. package/dist/observability/run-terminal.js +175 -171
  164. package/dist/observability/secret-env-scrub.d.ts +20 -1
  165. package/dist/observability/secret-env-scrub.js +14 -0
  166. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -2
  167. package/dist/orchestration/workflow-completion-inbox.js +13 -5
  168. package/dist/orchestration/workflow-notify-journal.d.ts +5 -3
  169. package/dist/orchestration/workflow-notify-journal.js +2 -1
  170. package/dist/permission-mode-admission.d.ts +68 -0
  171. package/dist/permission-mode-admission.js +23 -0
  172. package/dist/permission-rule-list.d.ts +55 -6
  173. package/dist/permission-rule-list.js +52 -5
  174. package/dist/plugins/approval-ask-store-file.d.ts +14 -4
  175. package/dist/plugins/approval-ask-store-file.js +48 -7
  176. package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
  177. package/dist/plugins/approval-ask-store-memory.js +9 -8
  178. package/dist/plugins/approval-ask-store-sql.d.ts +42 -10
  179. package/dist/plugins/approval-ask-store-sql.js +49 -19
  180. package/dist/plugins/caching-session-store.d.ts +6 -0
  181. package/dist/plugins/caching-session-store.js +3 -0
  182. package/dist/plugins/checkpoint-store-sql.d.ts +65 -17
  183. package/dist/plugins/checkpoint-store-sql.js +74 -3
  184. package/dist/plugins/device-store-sql.d.ts +5 -2
  185. package/dist/plugins/device-store-sql.js +4 -1
  186. package/dist/plugins/execution-placement-sql.d.ts +11 -1
  187. package/dist/plugins/execution-placement-sql.js +12 -1
  188. package/dist/plugins/file-run-store.d.ts +7 -2
  189. package/dist/plugins/file-run-store.js +12 -2
  190. package/dist/plugins/fork-routing-session-store.d.ts +10 -32
  191. package/dist/plugins/fork-routing-session-store.js +2 -61
  192. package/dist/plugins/k8s-exec-protocol.d.ts +4 -1
  193. package/dist/plugins/k8s-exec-protocol.js +6 -1
  194. package/dist/plugins/local-checkpoint-store.d.ts +10 -1
  195. package/dist/plugins/local-checkpoint-store.js +22 -0
  196. package/dist/plugins/local-session-store.d.ts +260 -39
  197. package/dist/plugins/local-session-store.js +582 -57
  198. package/dist/plugins/local-session-window.d.ts +17 -3
  199. package/dist/plugins/local-session-window.js +7 -13
  200. package/dist/plugins/local-task-attachment-store.d.ts +12 -2
  201. package/dist/plugins/local-task-attachment-store.js +67 -43
  202. package/dist/plugins/memory-key-guards.d.ts +3 -3
  203. package/dist/plugins/memory-run-store.d.ts +7 -2
  204. package/dist/plugins/memory-run-store.js +12 -2
  205. package/dist/plugins/one-time-migrations.d.ts +68 -2
  206. package/dist/plugins/one-time-migrations.js +120 -4
  207. package/dist/plugins/pg-pool.js +3 -2
  208. package/dist/plugins/pg-session-storage.d.ts +2 -0
  209. package/dist/plugins/pg-session-storage.js +3 -2
  210. package/dist/plugins/remote-env-e2b.d.ts +47 -7
  211. package/dist/plugins/remote-env-e2b.js +242 -46
  212. package/dist/plugins/remote-env-k8s.d.ts +51 -6
  213. package/dist/plugins/remote-env-k8s.js +234 -44
  214. package/dist/plugins/remote-env-local-docker.d.ts +40 -14
  215. package/dist/plugins/remote-env-local-docker.js +127 -33
  216. package/dist/plugins/run-store-sql.d.ts +11 -3
  217. package/dist/plugins/run-store-sql.js +13 -4
  218. package/dist/plugins/sandbox-read-floor.d.ts +273 -0
  219. package/dist/plugins/sandbox-read-floor.js +608 -0
  220. package/dist/plugins/session-ownership.d.ts +2 -1
  221. package/dist/plugins/session-placement.d.ts +17 -4
  222. package/dist/plugins/session-placement.js +2 -1
  223. package/dist/plugins/session-store.d.ts +18 -20
  224. package/dist/plugins/session-store.js +4 -12
  225. package/dist/plugins/store-backend.d.ts +41 -7
  226. package/dist/plugins/store-backend.js +22 -4
  227. package/dist/plugins/store-contracts.d.ts +35 -3
  228. package/dist/plugins/store-contracts.js +3 -0
  229. package/dist/plugins/task-attachment-store.d.ts +112 -14
  230. package/dist/plugins/task-attachment-store.js +221 -28
  231. package/dist/plugins/tidb-pool.js +2 -1
  232. package/dist/plugins/tidb-session-store.d.ts +4 -1
  233. package/dist/plugins/tidb-session-store.js +3 -2
  234. package/dist/plugins/workspace-fill.d.ts +60 -0
  235. package/dist/plugins/workspace-fill.js +106 -0
  236. package/dist/project-bundle.d.ts +38 -0
  237. package/dist/project-bundle.js +609 -0
  238. package/dist/run-cancel-context.d.ts +34 -0
  239. package/dist/run-cancel-context.js +7 -0
  240. package/dist/run-local.js +2 -8
  241. package/dist/runs.d.ts +39 -12
  242. package/dist/runs.js +69 -55
  243. package/dist/sandbox-egress-policy.d.ts +4 -1
  244. package/dist/sandbox-egress-policy.js +3 -1
  245. package/dist/sandbox-git-plane.d.ts +31 -12
  246. package/dist/sandbox-git-plane.js +24 -4
  247. package/dist/security.d.ts +11 -2
  248. package/dist/security.js +8 -0
  249. package/dist/server-secret-env.d.ts +2 -1
  250. package/dist/server-secret-env.js +2 -4
  251. package/dist/session-error-wire.d.ts +46 -4
  252. package/dist/session-error-wire.js +23 -0
  253. package/dist/session-incarnation.d.ts +9 -3
  254. package/dist/session-sync-kernel.d.ts +42 -1
  255. package/dist/session-sync-kernel.js +17 -0
  256. package/dist/session-sync.js +16 -4
  257. package/dist/sql-engine-coordinates.d.ts +49 -0
  258. package/dist/sql-engine-coordinates.js +27 -0
  259. package/dist/standing-answer-grant.d.ts +31 -2
  260. package/dist/standing-answer-grant.js +14 -2
  261. package/dist/store-posture.d.ts +1 -1
  262. package/dist/task-a2a.d.ts +1 -1
  263. package/dist/task-mcp.d.ts +1 -1
  264. package/dist/task-settings.d.ts +62 -27
  265. package/dist/task-settings.js +105 -29
  266. package/dist/terminal.d.ts +97 -20
  267. package/dist/terminal.js +107 -18
  268. package/dist/tool-approval.d.ts +4 -3
  269. package/dist/tool-approval.js +12 -14
  270. package/dist/trace/core-keyset-guard.d.ts +196 -2
  271. package/dist/trace/engine-notice-wire.d.ts +8 -2
  272. package/dist/trace/engine-notice-wire.js +4 -2
  273. package/dist/trace/exit-seat.d.ts +9 -1
  274. package/dist/trace/exit-seat.js +1 -0
  275. package/dist/trace/ledger-sink.d.ts +8 -4
  276. package/dist/trace/ledger-sink.js +2 -2
  277. package/dist/trace/model-segment.d.ts +2 -2
  278. package/dist/trace/project.d.ts +18 -1
  279. package/dist/trace/project.js +16 -0
  280. package/dist/trace/redact.js +323 -90
  281. package/dist/trace/self-minted-wire.d.ts +419 -4
  282. package/dist/trace/self-minted-wire.js +145 -0
  283. package/dist/trace/wire-projection-faces.d.ts +1 -1
  284. package/dist/trace/wire-projection-faces.js +1 -1
  285. package/dist/verifier-pause-card.d.ts +8 -0
  286. package/dist/verifier-pause-card.js +19 -0
  287. package/dist/web-admin-client/apply-effective.d.ts +5 -7
  288. package/dist/web-admin-client/apply-effective.js +22 -2
  289. package/dist/web-admin-client/candidate-gate.d.ts +1 -1
  290. package/dist/web-admin-client/effective-keys.js +2 -0
  291. package/dist/web-admin-client/read-warnings.d.ts +2 -2
  292. package/dist/web-admin-client/types.d.ts +4 -0
  293. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -2
  294. package/package.json +2 -2
  295. package/wire-changes/server@7.141.0.json +367 -0
  296. package/wire-changes/server@7.142.0.json +639 -0
@@ -86,9 +86,9 @@ export declare const RuleOfferSchema: z.ZodUnion<readonly [z.ZodObject<{
86
86
  host: "host";
87
87
  name: "name";
88
88
  path: "path";
89
+ pattern: "pattern";
89
90
  tool: "tool";
90
91
  exact: "exact";
91
- pattern: "pattern";
92
92
  prefix: "prefix";
93
93
  wildcard: "wildcard";
94
94
  subpath: "subpath";
@@ -105,9 +105,9 @@ export declare const RuleOfferSchema: z.ZodUnion<readonly [z.ZodObject<{
105
105
  host: "host";
106
106
  name: "name";
107
107
  path: "path";
108
+ pattern: "pattern";
108
109
  tool: "tool";
109
110
  exact: "exact";
110
- pattern: "pattern";
111
111
  prefix: "prefix";
112
112
  wildcard: "wildcard";
113
113
  subpath: "subpath";
@@ -149,6 +149,15 @@ export interface ReadRuleOffer {
149
149
  * 缺席(`undefined`)= 没有提议,答空、不计。展示帽(条数、成员前缀、扫描窗)、脱敏与截长归调用方 —— 它们是各腿的展示纪律,不是读法。
150
150
  */
151
151
  export declare function readRuleOfferList(raw: unknown): readonly ReadRuleOffer[];
152
+ /**
153
+ * 一份(已解码的)规则提议列表里,**带着无 `kind` 存量成员**的 batch offer 有几只 —— 判官是 core `readRuleOffers` 给每只读出的 offer 打的 `kindlessForm` 记号
154
+ * (「这一只 batch 里至少一个成员是经无 `kind` 的存量形读的」;与 {@link readRuleOfferList} 同一只读者,不是本仓自己再写一遍「成员没有 `kind` 键」)。
155
+ * 起服普查(`approval-batch-census.ts`)用它给存量行记数:读者那条存量臂退役的前提是「零行还靠它读」。
156
+ *
157
+ * **只数,不落任何痕迹**:读不出的元素(`dropped`)与整只值不是列表(`problem`)是 {@link readRuleOfferList} 的计数口径(`server.rule-offers.offer-dropped`),这里不重复记,
158
+ * 它们也不是「靠存量臂读出来」的 —— 一个读不出的无 `kind` 成员(缺文本键)退役前后同样被丢,不改变任何一行的读法,所以不计入。
159
+ */
160
+ export declare function countKindlessBatchOffers(raw: unknown): number;
152
161
  /**
153
162
  * 卡面/帧面共用的 offer **投影型**(= {@link RuleOfferSchema} 的输出形)。判别位 `kind` 上闭集,
154
163
  * 消费端(SDK/cli 渲染)按 core 契约分臂;`batch.rules` 成员是判别联合(`command` | `directoryRead`),
@@ -441,11 +450,13 @@ export interface AskClassPair {
441
450
  * bypass 姿态下按 `ask_rule` / `hook` 照问的那条壳规则无从落地。缺席 = 老行(core 7.5.0 之前 park 的)/ 集外词(计 [ref] tag)。
442
451
  * [ref] ②(core 7.37.0 [ref])第六 / 七位 `askClass` / `classes`:这一问的类对(经 {@link readAskClassPair};两位同在同缺,与活卡帧 / 呈卡帧的卡
443
452
  * 同名同义同值)—— durable 审批席按类答的依据。缺席 = 老行(core 7.37.0 之前 park 的,续跑不补)/ 读不出(计 [ref] tag)。
444
- * [ref](core 7.51.0 [ref])第八位 `boundGrantAbsence`:这张卡为什么没有授予绑定摘要(经 {@link readClosedWordSeat};与活卡帧 / inbox 行同名同义同值)——
445
- * `not_bindable` = 这一问不绑任何授予(「本会话放行」可按工具记)。缺席 = 技能卡(带摘要;摘要与清单、成因词三位走另一只投影 {@link skillCardFacts},见其顶注)/ 7.51.0 之前停驻的行(core inbox 投
446
- * `unreadable`,队列行如实缺席)/ 集外词(计 [ref] tag)。
453
+ * [ref](core 7.51.0 [ref])第八位 `boundGrantAbsence` / [ref] 起与 `boundGrantDigest` 合成**一臂**:这张卡的授予绑定 —— `tool_approval` 行**恰带**两者之一,
454
+ * 经本仓唯一读口 `grantBindingOf` 读停驻行、`grantBindingArm` 投回(`standing-answer-grant.ts`;与 core 投 inbox 行的那一臂逐形同值):摘要是串 ⇒ 摘要;
455
+ * 否则摘要键不在且行上的词是闭集成员 ⇒ 那个词;否则(7.51.0 之前停驻的行两者都不说 / 集外词 / 摘要在却不是串)⇒ `unreadable`。`not_bindable` = 这一问不绑任何授予
456
+ * (「本会话放行」可按工具记)。摘要读自停驻行本身 —— 不靠整行的检查点摘要,所以 SQL 腿那一次整行读失败也不丢这一臂;要 core 校验的清单与成因词走
457
+ * 另一只投影 {@link skillCardFacts}。
447
458
  */
448
- export type ParkRowFacts = Pick<ToolApprovalPark, "requiresRealApproval" | "ruleOffersAbsence" | "mandated" | "mandate" | "origin" | "askClass" | "classes" | "boundGrantAbsence">;
459
+ export type ParkRowFacts = Pick<ToolApprovalPark, "requiresRealApproval" | "ruleOffersAbsence" | "mandated" | "mandate" | "origin" | "askClass" | "classes" | "boundGrantDigest" | "boundGrantAbsence">;
449
460
  /**
450
461
  * [ref] —— park 行事实的**唯一投影**(`pendingAction` 是寄存行上 core 铸的那一只,读自持久层 ⇒ `unknown`)。
451
462
  *
@@ -465,7 +476,8 @@ export type ParkRowFacts = Pick<ToolApprovalPark, "requiresRealApproval" | "rule
465
476
  * park 孪生座与同步座是 core 同一个 factory 铸的同一闭集,不另起第二份词表;
466
477
  * · `mandate` / `origin` —— 经 {@link readClosedWordSeat}(一座一只 core 成员筛 `isPersistedRuleMandate` / `isAskOrigin`,与 core
467
478
  * `summarizeCheckpoint` 投 inbox 行时是同一只筛 ⇒ 两条耐久读面对同一张行同答;集外词读作缺席 + 计同一个集外 tag);
468
- * · 缺席 ≠ false:各位都只在「真 / 有词」时在场,消费端读**在场**。
479
+ * · 授予绑定([ref])—— 不是「在场才带」的一位,是恰一臂:`grantBindingArm(grantBindingOf(行))`,读法见 {@link ParkRowFacts} 顶注;
480
+ * · 缺席 ≠ false:其余各位都只在「真 / 有词」时在场,消费端读**在场**。
469
481
  *
470
482
  * 🔴 **这是展示/分诊投影,不是权限判据**(core 对两位的定性都是 display metadata:「the resume belts keep reading
471
483
  * the gate's own bit」)。本投影对「在场却读不出」的方向是**缺席**(卡上少一行 ≪ 卡上出现生词);一条要从
@@ -474,28 +486,31 @@ export type ParkRowFacts = Pick<ToolApprovalPark, "requiresRealApproval" | "rule
474
486
  */
475
487
  export declare function parkRowFacts(stored: unknown): ParkRowFacts;
476
488
  /**
477
- * 🔴 **技能卡三位的唯一投影**:授予绑定摘要 `boundGrantDigest`、它哈希的清单 `boundGrants`、分类器没给判词的成因 `classifierNoRuling` ——
489
+ * 🔴 **技能卡两位的唯一投影**:授予绑定摘要哈希的那一份清单 `boundGrants`、分类器没给判词的成因 `classifierNoRuling` ——
478
490
  * 助手收件箱行(`GET /v1/assistant/inbox`)、运维队列行(`GET /v1/approvals` 的 `pending[]`)、`/v1/approvals/stream` 的 `pending` 帧三张停驻读面
479
491
  * **经这一只函数**投它们。同一张停驻的技能卡于是三面同一份字节:运维从队列决这张卡时,看得到批准它还授予了什么。
492
+ * (摘要本身 `boundGrantDigest` 不在这里:它是授予绑定那一臂的一半,队列两面经 {@link parkRowFacts} 从停驻行读 —— core 对串摘要也是原样回显,值相同;
493
+ * [ref] 起这样分,是为了那一臂不依赖整行的检查点摘要读得到读不到。)
480
494
  *
481
- * **输入恒是 core 的检查点摘要**({@link CoreCheckpointSummary},`summarizeCheckpoint` 的产物)—— 三位的校验点单点在 core:摘要是串才回显;清单**只在哈希等于摘要时**才发布
495
+ * **输入恒是 core 的检查点摘要**({@link CoreCheckpointSummary},`summarizeCheckpoint` 的产物)—— 两位的校验点单点在 core:清单**只在哈希等于摘要时**才发布
482
496
  * (「never a list beside a digest that binds another」—— 一份与摘要对不上的清单会让批准人看着一份清单、批的却是另一份绑定);成因词只在闭集成员时回显。
483
- * 本仓**不重算哈希**(core 的 `boundInputHashOf` 不在公开导出面上,清单校验读口 `readBoundBinding` 同样不导出),所以**不许**拿寄存行 `pendingAction` 上的原值直接投 ——
484
- * 原值上的清单是未经哈希校验的(这正是三位此前不上队列行的原因,见 `trace/core-keyset-guard.ts` ⑧b 记账的旧注)。两只店各自拿到「这一行的摘要」的办法不同
485
- * (LOCAL 孪生本来就握着 core 对同一行投的摘要;SQL 孪生对带三位之一的行读一次该行再调 core 的 `summarizeCheckpoint`,见 `plugins/checkpoint-store-sql.ts`),
497
+ * 本仓**不重算哈希**(core 的 `boundInputHashOf` 不在公开导出面上,清单校验读口 `readBoundBinding` 同样不导出),所以**不许**拿寄存行 `pendingAction` 上的原清单直接投 ——
498
+ * 原值上的清单是未经哈希校验的(这正是它此前不上队列行的原因,见 `trace/core-keyset-guard.ts` ⑧b 记账的旧注)。两只店各自拿到「这一行的摘要」的办法不同
499
+ * (LOCAL 孪生本来就握着 core 对同一行投的摘要;SQL 孪生对可能带两位之一的行读一次该行再调 core 的 `summarizeCheckpoint`,见 `plugins/checkpoint-store-sql.ts`),
486
500
  * 但投影只有这一只:OMIT 契约 —— 缺席即缺席,恒不铸 `undefined` 值的键。
487
501
  *
488
502
  * `boundGrants` 是**部署文本**(技能名、清单条目):本函数不脱敏、不转义,与收件箱行逐字同口径 —— 渲染按上下文转义是客户端的事(契约同句)。
489
- * 三位都不进 `card_json`(同步路铸点,另一条纪律)。
503
+ * 两位都不进 `card_json`(同步路铸点,另一条纪律)。
490
504
  *
491
- * 🚧 清单校验读口 `readBoundBinding` 若成为 core 的公开导出,SQL 孪生那一次整行读与 {@link parkRowCarriesSkillCardFacts} 一并退役:`parkRowFacts` 直接读寄存行原值经 core 的读口投三位。
505
+ * 🚧 清单校验读口 `readBoundBinding` 若成为 core 的公开导出,SQL 孪生那一次整行读与 {@link parkRowCarriesSkillCardFacts} 一并退役:`parkRowFacts` 直接读寄存行原值经 core 的读口投两位
506
+ * (`grantBindingOf` 也改成它的薄包装 —— 那时 core 是绑定读法的唯一属主)。
492
507
  */
493
- export type SkillCardFacts = Pick<CoreCheckpointSummary, "boundGrantDigest" | "boundGrants" | "classifierNoRuling">;
508
+ export type SkillCardFacts = Pick<CoreCheckpointSummary, "boundGrants" | "classifierNoRuling">;
494
509
  export declare function skillCardFacts(summary: SkillCardFacts): SkillCardFacts;
495
510
  /**
496
- * 这一行的寄存行(`pendingAction`,读自持久层 ⇒ `unknown`)**有没有可能**让 core 的摘要投出三位里的任何一位 —— 只回答「值不值得为它取一次摘要」,**不投值**:
497
- * `tool_approval` 臂上摘要字段是串(core 只在这时回显摘要与清单),或成因词键在场(是不是闭集成员由 core 的摘要判)。判错的方向只有一个 —— 多取一次摘要(摘要读数为空),
498
- * 绝不漏取:摘要字段不是串 ⇒ core 的摘要既无摘要也无清单;成因词键不在 ⇒ 无成因词 —— 两者都不满足的行,摘要里这三位恒空。普通卡(绝大多数行)因此零额外读。
511
+ * 这一行的寄存行(`pendingAction`,读自持久层 ⇒ `unknown`)**有没有可能**让 core 的摘要投出两位里的任何一位 —— 只回答「值不值得为它取一次摘要」,**不投值**:
512
+ * `tool_approval` 臂上摘要字段是串(core 只在这时回显清单),或成因词键在场(是不是闭集成员由 core 的摘要判)。判错的方向只有一个 —— 多取一次摘要(摘要读数为空),
513
+ * 绝不漏取:摘要字段不是串 ⇒ core 的摘要没有清单;成因词键不在 ⇒ 无成因词 —— 两者都不满足的行,摘要里这两位恒空。普通卡(绝大多数行)因此零额外读。
499
514
  * 只读键在不在 / 是不是串,不读值的内容,所以不需要 {@link readClosedWordSeat} 那样的计数。
500
515
  */
501
516
  export declare function parkRowCarriesSkillCardFacts(stored: unknown): boolean;
@@ -556,12 +571,12 @@ export declare const ApprovalCardSchema: z.ZodObject<{
556
571
  question: "question";
557
572
  plan: "plan";
558
573
  skill: "skill";
559
- workflow: "workflow";
560
574
  "file-edit": "file-edit";
561
575
  "file-write": "file-write";
562
576
  "notebook-edit": "notebook-edit";
563
577
  shell: "shell";
564
578
  fetch: "fetch";
579
+ workflow: "workflow";
565
580
  generic: "generic";
566
581
  }>>;
567
582
  inputHasBidi: z.ZodOptional<z.ZodLiteral<true>>;
@@ -583,9 +598,9 @@ export declare const ApprovalCardSchema: z.ZodObject<{
583
598
  host: "host";
584
599
  name: "name";
585
600
  path: "path";
601
+ pattern: "pattern";
586
602
  tool: "tool";
587
603
  exact: "exact";
588
- pattern: "pattern";
589
604
  prefix: "prefix";
590
605
  wildcard: "wildcard";
591
606
  subpath: "subpath";
@@ -602,9 +617,9 @@ export declare const ApprovalCardSchema: z.ZodObject<{
602
617
  host: "host";
603
618
  name: "name";
604
619
  path: "path";
620
+ pattern: "pattern";
605
621
  tool: "tool";
606
622
  exact: "exact";
607
- pattern: "pattern";
608
623
  prefix: "prefix";
609
624
  wildcard: "wildcard";
610
625
  subpath: "subpath";
@@ -710,12 +725,12 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
710
725
  question: "question";
711
726
  plan: "plan";
712
727
  skill: "skill";
713
- workflow: "workflow";
714
728
  "file-edit": "file-edit";
715
729
  "file-write": "file-write";
716
730
  "notebook-edit": "notebook-edit";
717
731
  shell: "shell";
718
732
  fetch: "fetch";
733
+ workflow: "workflow";
719
734
  generic: "generic";
720
735
  }>>;
721
736
  inputHasBidi: z.ZodOptional<z.ZodLiteral<true>>;
@@ -737,9 +752,9 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
737
752
  host: "host";
738
753
  name: "name";
739
754
  path: "path";
755
+ pattern: "pattern";
740
756
  tool: "tool";
741
757
  exact: "exact";
742
- pattern: "pattern";
743
758
  prefix: "prefix";
744
759
  wildcard: "wildcard";
745
760
  subpath: "subpath";
@@ -756,9 +771,9 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
756
771
  host: "host";
757
772
  name: "name";
758
773
  path: "path";
774
+ pattern: "pattern";
759
775
  tool: "tool";
760
776
  exact: "exact";
761
- pattern: "pattern";
762
777
  prefix: "prefix";
763
778
  wildcard: "wildcard";
764
779
  subpath: "subpath";
@@ -4,6 +4,7 @@ import { redactSecrets } from "./trace/redact.js";
4
4
  import { recordFailOpen } from "./observability/fail-open.js";
5
5
  import { readApprovalCard } from "./approval-content-kind.js";
6
6
  import { capText } from "./caller-text-echo.js";
7
+ import { grantBindingArm, grantBindingOf } from "./standing-answer-grant.js";
7
8
  import { retireParkArmWords, retireStoredCardWords } from "./retired-card-words.js";
8
9
  export const MAX_AGENT_NAME = 200;
9
10
  const MAX_IDENT = 200;
@@ -61,6 +62,10 @@ export function readRuleOfferList(raw) {
61
62
  reading.dropped.forEach((d) => recordOfferDropped(d.problem, d.index));
62
63
  return reading.offers.map(withDetailSeatBounded);
63
64
  }
65
+ export function countKindlessBatchOffers(raw) {
66
+ const reading = readRuleOffers(raw);
67
+ return "problem" in reading ? 0 : reading.offers.filter((o) => o.kindlessForm === true).length;
68
+ }
64
69
  function withoutOverCapDetailSeat(item, index) {
65
70
  if (typeof item !== "object" || item === null)
66
71
  return item;
@@ -335,7 +340,6 @@ export function parkRowFacts(stored) {
335
340
  const mandate = readClosedWordSeat(pendingAction, "mandate");
336
341
  const origin = readClosedWordSeat(pendingAction, "origin");
337
342
  const askClassPair = readAskClassPair(pendingAction);
338
- const boundGrantAbsence = readClosedWordSeat(pendingAction, "boundGrantAbsence");
339
343
  return {
340
344
  ...(readParkRowTrueSeat(pendingAction, "requiresRealApproval") ? { requiresRealApproval: true } : {}),
341
345
  ...(readParkRowTrueSeat(pendingAction, "mandated") ? { mandated: true } : {}),
@@ -343,12 +347,11 @@ export function parkRowFacts(stored) {
343
347
  ...(origin !== undefined ? { origin } : {}),
344
348
  ...(ruleOffersAbsence !== undefined ? { ruleOffersAbsence } : {}),
345
349
  ...(askClassPair ?? {}),
346
- ...(boundGrantAbsence !== undefined ? { boundGrantAbsence } : {}),
350
+ ...grantBindingArm(grantBindingOf(pendingAction)),
347
351
  };
348
352
  }
349
353
  export function skillCardFacts(summary) {
350
354
  return {
351
- ...(summary.boundGrantDigest !== undefined ? { boundGrantDigest: summary.boundGrantDigest } : {}),
352
355
  ...(summary.boundGrants !== undefined ? { boundGrants: summary.boundGrants } : {}),
353
356
  ...(summary.classifierNoRuling !== undefined ? { classifierNoRuling: summary.classifierNoRuling } : {}),
354
357
  };
@@ -1,6 +1,7 @@
1
1
  import type { ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
2
2
  import { FileApprovalAskAuditStore } from "./approval-ask-audit-store.js";
3
3
  import { type HolderGoneCause } from "./approval-deny-reasons.js";
4
+ import { type ReconcileCheckpointPort } from "./approval-reconciler.js";
4
5
  import { type RunStoreReader } from "./user-input-ledger.js";
5
6
  import type { Logger } from "./observability/logger.js";
6
7
  /** 启动收敛的读数(进启动日志)。 */
@@ -10,17 +11,19 @@ export interface CrashConvergenceReport {
10
11
  readonly decided: number;
11
12
  readonly inconsistent: number;
12
13
  readonly corruptLines: number;
13
- /** ask 账半场:按成因词计的、本次收成 VOID 的前世 `STREAM_PENDING` 行数。 */
14
+ /** ask 账半场:按成因词计的、本次收成 VOID 的前世 `STREAM_PENDING` 行与审计判孤儿的 `PARKING` 行数。 */
14
15
  readonly asksVoided: Readonly<Record<HolderGoneCause, number>>;
16
+ /** 审计判孤儿的 `PARKING` 行里判据 ① 命中、收成 `PARKED` 的行数(停车行其实已提交;每行另有一行 warn 点名)。 */
17
+ readonly parkingBound: number;
15
18
  /** 其中卡信封读不出、成因词是回落(`crashed_before_park`)的行数(同时记 fail-open tag)。 */
16
19
  readonly causeUnreadable: number;
17
20
  }
18
21
  /** 本次收成 VOID 的行数合计 —— 由成因闭集派生(对 `asksVoided` 的每个词求和;闭集加词不需要改这里)。 */
19
22
  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">;
23
+ /** 本函数读写的那几口(窄形:测试可以直给一只内存店)。`getBatch` / `bindBatch` 是判据 ① 执行体的两口。 */
24
+ export type CrashConvergenceAskPort = Pick<ApprovalAskStore, "listByState" | "transitionAsk" | "getBatch" | "bindBatch">;
25
+ /** 审计店的两口(收敛半场 + 持有者没了的判点)。 */
26
+ export type CrashConvergenceAuditPort = Pick<FileApprovalAskAuditStore, "convergeCrashedAsks" | "holderGoneVerdict">;
24
27
  /**
25
28
  * 启动收敛读哪一份**审计证据**:活的审计店在场就用它;不在场(这一世 `TOOL_APPROVAL_ENABLED` 关着 ⇒ 装配三合取不成立)而盘上有前世的审计账本
26
29
  * ⇒ 开一只读它 —— 成因是盘上证据的函数,不随这一世的开关变(否则前世有序关停截断的问在这一世被写成 `crashed_before_park`,审批重新打开那天审计读面
@@ -32,12 +35,16 @@ export declare function openAuditEvidenceForConvergence(live: CrashConvergenceAu
32
35
  release: () => void;
33
36
  };
34
37
  /**
35
- * 启动收敛(见文件头)。只在数据根独占的车道上调,且必须在本进程铸出第一问之前。幂等跨启动:收过的行已不是 `STREAM_PENDING`,
36
- * 审计半场有自己的收敛锚;成因读冻结的前世审计账 ⇒ 半途崩溃再启,剩下的行得到与第一次相同的词。
38
+ * 启动收敛(见文件头)。只在数据根独占的车道上调,且必须在本进程铸出第一问之前。幂等跨启动:收过的行已不在 `STREAM_PENDING` / `PARKING`,
39
+ * 审计半场有自己的收敛锚;判定与成因读冻结的前世审计账 ⇒ 半途崩溃再启,剩下的行得到与第一次相同的判定与词。
40
+ * `checkpoints` = 判据 ① 的候选读口,必填、不可缺席(装配点交后端的只读口 `checkpointReadView()`;没有 checkpoint 店的后端交恒空读口):缺席会让判据 ① 恒不命中、
41
+ * 把已提交的停车行收成 VOID,所以既不给缺省,也不收 `undefined`。
37
42
  */
38
43
  export declare function convergeCrashedAsksAtBoot(input: {
39
44
  askStore: CrashConvergenceAskPort | undefined;
40
45
  audit: CrashConvergenceAuditPort | undefined;
46
+ checkpoints: ReconcileCheckpointPort;
47
+ logger: Pick<Logger, "warn">;
41
48
  now?: () => number;
42
49
  }): Promise<CrashConvergenceReport>;
43
50
  /**
@@ -45,12 +52,15 @@ export declare function convergeCrashedAsksAtBoot(input: {
45
52
  * · 只在数据根独占的车道上跑 —— 车道事实与用户输入受理账本同一只 `writerGoneEvidenceFor`(local ⇒ `exclusive_data_root`);SQL 车道整步不做。
46
53
  * · ask 店取 `backend.approvalAsk()`,**不看协议开关**:前世开着协议留下的行,今世关着也照收(否则重新打开协议那天它们又变得可决)。
47
54
  * · 审计证据经 {@link openAuditEvidenceForConvergence}(这一世的审批开关不改变成因)。
55
+ * · 判据 ① 的候选读口 = 后端的只读口 `backend.checkpointReadView()`,**不看 `DURABLE_APPROVAL`**(前世已提交的停驻不因这一世的开关消失;local 车道它与 durable 装配的那只是同一只店,
56
+ * 只是不接 run 店的收割探针)。
48
57
  * · 有任何收敛 / 坏行 ⇒ 一行 warn `approval_ask_audit_crash_converged`(零动作不打)。收敛写入抛错 ⇒ 抛出 ⇒ 拒启。
49
58
  */
50
59
  export declare function runBootCrashConvergence(ctx: {
51
60
  backend: {
52
61
  readonly kind: "mysql" | "pg" | "local";
53
62
  approvalAsk(): CrashConvergenceAskPort;
63
+ checkpointReadView(): ReconcileCheckpointPort;
54
64
  } | undefined;
55
65
  runStore: RunStoreReader | undefined;
56
66
  liveAudit: CrashConvergenceAuditPort | undefined;
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { APPROVAL_ASK_AUDIT_DIR, APPROVAL_ASK_AUDIT_LEDGER, FileApprovalAskAuditStore } from "./approval-ask-audit-store.js";
4
4
  import { readStoredCardEnvelope } from "./approval-card.js";
5
5
  import { APPROVAL_ENDING_CAUSES } from "./approval-deny-reasons.js";
6
+ import { bindParkingAskAtGate } from "./approval-reconciler.js";
6
7
  import { writerGoneEvidenceFor } from "./user-input-ledger.js";
7
8
  import { recordFailOpen } from "./observability/fail-open.js";
8
9
  const CONVERGE_PAGE = 500;
@@ -17,57 +18,128 @@ export function openAuditEvidenceForConvergence(live, localDataRoot) {
17
18
  const opened = new FileApprovalAskAuditStore(localDataRoot);
18
19
  return { audit: opened, release: () => opened.close() };
19
20
  }
21
+ async function forEachRowInState(store, state, visit) {
22
+ const seen = new Set();
23
+ let stayed = 0;
24
+ for (;;) {
25
+ const page = (await store.listByState(state, stayed + CONVERGE_PAGE)).filter((r) => !seen.has(r.askId));
26
+ if (page.length === 0)
27
+ return;
28
+ for (const row of page) {
29
+ seen.add(row.askId);
30
+ if (!(await visit(row)))
31
+ stayed += 1;
32
+ }
33
+ }
34
+ }
35
+ function refusingToStart(state, row, err) {
36
+ return new Error(`approval crash convergence: could not close the previous process's ${state === "STREAM_PENDING" ? "pending" : "parking"} approval ask ${row.askId} (task ${row.taskId}): ` +
37
+ `${err instanceof Error ? err.message : String(err)} — refusing to start, because serving now would leave a card that nothing waits for ` +
38
+ "decidable with a 200. Fix the data root (disk full / read-only?) and start again.");
39
+ }
20
40
  export async function convergeCrashedAsksAtBoot(input) {
21
41
  const now = input.now ?? Date.now;
22
- const audited = input.audit?.convergeCrashedAsks() ?? { pending: 0, decided: 0, inconsistent: 0, corruptLines: 0 };
42
+ const audit = input.audit;
43
+ const audited = audit?.convergeCrashedAsks() ?? { pending: 0, decided: 0, inconsistent: 0, corruptLines: 0 };
23
44
  const asksVoided = { [APPROVAL_ENDING_CAUSES.CRASHED_BEFORE_PARK]: 0, [APPROVAL_ENDING_CAUSES.SHUTDOWN_BEFORE_PARK]: 0 };
24
45
  let causeUnreadable = 0;
46
+ let parkingBound = 0;
25
47
  const store = input.askStore;
26
48
  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);
49
+ await forEachRowInState(store, "STREAM_PENDING", async (row) => {
50
+ const envelope = readStoredCardEnvelope(row.cardJson);
51
+ if (envelope === undefined) {
52
+ causeUnreadable += 1;
53
+ recordFailOpen("server.approval-crash-convergence.cause-unreadable", `ask ${row.askId} (task ${row.taskId}): card envelope unreadable — cause defaulted to ${APPROVAL_ENDING_CAUSES.CRASHED_BEFORE_PARK}`);
54
+ }
55
+ const reason = envelope !== undefined && audit !== undefined ? audit.holderGoneVerdict(envelope.approvalId).cause : APPROVAL_ENDING_CAUSES.CRASHED_BEFORE_PARK;
56
+ let won;
57
+ try {
58
+ won = await store.transitionAsk(row.askId, "STREAM_PENDING", "VOID", { endingCause: reason, updatedAtMs: now() });
59
+ }
60
+ catch (err) {
61
+ throw refusingToStart("STREAM_PENDING", row, err);
62
+ }
63
+ if (won)
64
+ asksVoided[reason] += 1;
65
+ return true;
66
+ });
67
+ if (audit !== undefined) {
68
+ const orphanOf = (row) => {
34
69
  const envelope = readStoredCardEnvelope(row.cardJson);
35
- if (envelope === undefined) {
36
- causeUnreadable += 1;
37
- recordFailOpen("server.approval-crash-convergence.cause-unreadable", `ask ${row.askId} (task ${row.taskId}): card envelope unreadable — cause defaulted to ${APPROVAL_ENDING_CAUSES.CRASHED_BEFORE_PARK}`);
70
+ if (envelope === undefined)
71
+ return undefined;
72
+ const verdict = audit.holderGoneVerdict(envelope.approvalId);
73
+ return verdict.orphan ? { approvalId: envelope.approvalId, cause: verdict.cause } : undefined;
74
+ };
75
+ let voidedSiblings = [];
76
+ const gate = {
77
+ askStore: store,
78
+ checkpoints: input.checkpoints,
79
+ warn: (event, fields) => input.logger.warn(event, fields),
80
+ revoke: (_row, askIds) => {
81
+ voidedSiblings = askIds;
82
+ },
83
+ };
84
+ await forEachRowInState(store, "PARKING", async (row) => {
85
+ const orphan = orphanOf(row);
86
+ if (orphan === undefined)
87
+ return false;
88
+ voidedSiblings = [];
89
+ let bound;
90
+ try {
91
+ bound = (await bindParkingAskAtGate(gate, row, now())).bound;
38
92
  }
39
- const reason = envelope !== undefined && input.audit !== undefined ? input.audit.causeFor(envelope.approvalId) : APPROVAL_ENDING_CAUSES.CRASHED_BEFORE_PARK;
93
+ catch (err) {
94
+ throw refusingToStart("PARKING", row, err);
95
+ }
96
+ if (!bound)
97
+ return false;
98
+ parkingBound += 1;
99
+ input.logger.warn("approval_crash_convergence_park_committed", {
100
+ askId: row.askId,
101
+ taskId: row.taskId,
102
+ approvalId: orphan.approvalId,
103
+ voidedSiblings,
104
+ note: "the audit ledger saw no settlement for this approval ask (an orphan of the previous process), but its park row had committed — bound PARKED, decidable through the durable approval endpoint; the commit landed in the window between the park commit and its observation. Same-batch siblings still parking were voided as superseded by this park (batch_bound_elsewhere)",
105
+ });
106
+ return true;
107
+ });
108
+ await forEachRowInState(store, "PARKING", async (row) => {
109
+ const orphan = orphanOf(row);
110
+ if (orphan === undefined)
111
+ return false;
40
112
  let won;
41
113
  try {
42
- won = await store.transitionAsk(row.askId, "STREAM_PENDING", "VOID", { deniedReason: reason, updatedAtMs: now() });
114
+ won = await store.transitionAsk(row.askId, "PARKING", "VOID", { endingCause: orphan.cause, updatedAtMs: now() });
43
115
  }
44
116
  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.");
117
+ throw refusingToStart("PARKING", row, err);
48
118
  }
49
119
  if (won)
50
- asksVoided[reason] += 1;
51
- }
120
+ asksVoided[orphan.cause] += 1;
121
+ return true;
122
+ });
52
123
  }
53
124
  }
54
- return { ...audited, asksVoided, causeUnreadable };
125
+ return { ...audited, asksVoided, causeUnreadable, parkingBound };
55
126
  }
56
127
  export async function runBootCrashConvergence(ctx) {
57
128
  const { backend, runStore, logger } = ctx;
58
129
  if (backend === undefined || runStore === undefined || writerGoneEvidenceFor(backend.kind, runStore).kind !== "exclusive_data_root")
59
130
  return undefined;
60
131
  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) {
132
+ const converged = await convergeCrashedAsksAtBoot({ askStore: backend.approvalAsk(), audit: evidence.audit, checkpoints: backend.checkpointReadView(), logger }).finally(evidence.release);
133
+ if (converged.pending > 0 || converged.decided > 0 || converged.inconsistent > 0 || converged.corruptLines > 0 || totalAsksVoided(converged) > 0 || converged.parkingBound > 0) {
63
134
  logger.warn("approval_ask_audit_crash_converged", {
64
135
  pending: converged.pending,
65
136
  decided: converged.decided,
66
137
  inconsistent: converged.inconsistent,
67
138
  corruptLines: converged.corruptLines,
68
139
  asksVoided: converged.asksVoided,
140
+ parkingBound: converged.parkingBound,
69
141
  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) 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)",
142
+ note: "approval asks left by the previous process were converged: each pending ask, and each parking ask the audit ledger judged an orphan, is now VOID in the ask ledger (the decision endpoint answers 410 gone.ask with this cause) 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 = the previous process completed its orderly shutdown wind-down while the ask was still unsettled); a parking ask whose park row had committed is bound PARKED instead",
71
143
  });
72
144
  }
73
145
  return converged;
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * ## 它治的病(同一件事、四张词表、一词多义)
5
5
  * 一张卡没被人答就结束了,今天有四个读面各说各话:审计 settle 行(`expired` 一词盖 TTL 到期 / run 取消 / run 退出清理 / 广播全灭,批内兄弟被撤卡也是同一个词)、
6
- * 问账 VOID/DENIED 行(`denied_reason` 列)、晚答 404 的判别位、决断口 409 的码(多种成因借 `approval_binding_mismatch` 一个名)。壳读账写不出「已取消」,
6
+ * 问账 VOID/DENIED 行(成因列)、晚答 404 的判别位、决断口 409 的码(多种成因借 `approval_binding_mismatch` 一个名)。壳读账写不出「已取消」,
7
7
  * 也分不出「重取坐标再决」与「改答案再决」。收成**一张表、一个词、各面同读**:
8
8
  * · 审计 settle 行的 `cause`(非人结局必填,人答缺席;`settle(verdict)` 判别联合逼每个非人调用点点名一个)与活卡结局帧 `tool_approval_complete` 的 additive `cause`(同一个词,同一条律);
9
- * · 问账 VOID/DENIED 行的 `denied_reason` 列 → 决断口 410 `gone.ask` 体的 `cause` 与晚答 404 体的 `cause`(读同一列,经唯一读口 {@link causeOfAskRow});
9
+ * · 问账 VOID/DENIED 行的 `ending_cause` 列 → 决断口 410 `gone.ask` 体的 `cause` 与晚答 404 体的 `cause`(读同一列,经唯一读口 {@link askRowEndingCause});
10
10
  * · `GET /v1/approvals` `crashConverged[].cause`(本表的 {@link HolderGoneCause} 两词子集);
11
11
  * · 决断口 409 的码:`binding_changed` / `answer_invalid` / `outcome_cell_invalid` 三词各归一个码。`retriable` 不由成因说 —— 三词都出自 core 的
12
12
  * `checkpoint.invalid_outcome`,由 `http/resume-legs.ts` 的 core 码 → 处置一张表派生(一个意思:卡仍 pending、行重新 park)。
@@ -16,8 +16,9 @@
16
16
  * · 成因**只从事实读**:run 行 `errorCode:"cancelled"`、钟到、emit 零接收、停车行 / 问账行的列、`detail.field`;不读拒句散文。
17
17
  * · 「停驻行的格式比本引擎新」**不是**本表的词:它只有 core 一个来源(`checkpoint.unsupported_version` + `detail.reason:"version_newer"`,原码镜像);服务层不从续跑上下文的写者版本推断成因。
18
18
  * · **人答的结局(`allowed` / `denied`)没有 cause**:`cause` 在场 ⇔ 非人结局。
19
- * · 落列面:问账行的成因写进 `approval_ask.denied_reason`(VARCHAR(64)),一列按 `state` 承载两种意思 —— VOID / DENIED 行上是**终态成因**,PARKING / PARKED 行上是
20
- * **转 park 的触发成因**;读它只经 {@link causeOfAskRow}(按 state 穷举分派,两张读词表只被它读)。列名将在下一个 DDL 窗按意义改名。
19
+ * · 落列面:问账行的成因落**两列,一列一义**(各 VARCHAR(64)):`approval_ask.ending_cause` = **终态成因**(这一行为什么没被人答就结束了;到 `VOID` / `DENIED` 的转移带着它写),
20
+ * `approval_ask.park_trigger` = **转 park 的触发成因**(这一行为什么被转进投递面;只由那一次 `claimTerminal(expire, { cause })` 写,此后不再被写 —— 终态不覆盖它)。
21
+ * 两只读口各读一列:{@link askRowEndingCause} / {@link askRowParkTrigger}(两张读词表各只被一只读)。
21
22
  *
22
23
  * ## 为什么 `routing_failure_fail_closed` 也在表里(归因诚实性)
23
24
  * 审计面唯一能证明「超时永不产生 denial」的东西是**终态 + 成因**的组合:人拒恒为 `DECIDED(decision=deny)`,路由失败恒为 `DENIED(routing_failure_fail_closed)`,
@@ -71,11 +72,15 @@ export declare const APPROVAL_ENDING_CAUSES: {
71
72
  */
72
73
  readonly ROUTING_FAILURE: "routing_failure_fail_closed";
73
74
  /**
74
- * 启动收敛(`approval-crash-convergence.ts`,local 车道):**承载这一问的进程没了** —— 前世进程崩溃 / 被杀,留下的 `STREAM_PENDING` 行在下一次启动、任何卡能开始之前收成 VOID。
75
- * 证据是数据根 `LOCK`(前世写者全死),不是「等够久了」。值与 `GET /v1/approvals` `crashConverged[].cause` 是**同一个词**。
75
+ * 启动收敛(`approval-crash-convergence.ts`,local 车道):**承载这一问的进程没了** —— 前世进程崩溃 / 被杀,留下的 `STREAM_PENDING` 行与审计判为孤儿的 `PARKING` 行
76
+ * (停车行没提交)在下一次启动、任何卡能开始之前收成 VOID。证据是数据根 `LOCK`(前世写者全死)+ 审计账的孤儿判定,不是「等够久了」。
77
+ * 值与 `GET /v1/approvals` `crashConverged[].cause` 是**同一个词**(一处判:审计店的 `holderGoneVerdict`)。
76
78
  */
77
79
  readonly CRASHED_BEFORE_PARK: "crashed_before_park";
78
- /** 同上,但承载它的腿是被**有序关停**截断的(审计账本上有本腿的关停记号;关停收尾没能在宽限窗内把这张卡结清),或有序关停期间这张卡被改路停驻(settle 行的成因)。 */
80
+ /**
81
+ * 同上,但前世走完了**有序关停**的审批收尾(审计账本上那一世的进程级关停记录落在这张卡的末事件之后 —— 收尾没能在宽限窗内把它结清;是进程的事实,
82
+ * 不看承载它的腿在不在追踪视野里),或有序关停期间这张卡被改路停驻(settle 行 / 问账行 PARKING 的触发成因)。
83
+ */
79
84
  readonly SHUTDOWN_BEFORE_PARK: "shutdown_before_park";
80
85
  /** 判据 5:任何在 `STREAM_APPROVAL_ORPHAN_TTL_MS` 内仍未收敛的 `PARKING` 行(量 immutable 的 `createdAtMs`,§9 C5)—— 遗孤最终可判的最后兜底,带一条 warn。 */
81
86
  readonly ORPHAN_TTL_EXCEEDED: "orphan_ttl_exceeded";
@@ -117,7 +122,7 @@ export declare const SETTLE_CAUSE_WORDS: {
117
122
  };
118
123
  /** 一个值是不是非人结局的成因词(审计账读盘用这一只判词)。 */
119
124
  export declare function isSettleCause(v: unknown): v is SettleCause;
120
- /** 「承载这一问的进程没了」的两个成因词(启动收敛写进问账行 `denied_reason`,审计收敛行 `cause` 读同一型)。 */
125
+ /** 「承载这一问的进程没了」的两个成因词(启动收敛写进问账行 `ending_cause`(前世的 `STREAM_PENDING` 与审计判孤儿的 `PARKING`),审计收敛行 `cause` 读同一型)。 */
121
126
  export type HolderGoneCause = typeof APPROVAL_ENDING_CAUSES.CRASHED_BEFORE_PARK | typeof APPROVAL_ENDING_CAUSES.SHUTDOWN_BEFORE_PARK;
122
127
  /** 对账器 `DENIED` 终态的成因(单员:全仓 DENIED 只有一个写点)。 */
123
128
  export type ReconcileDenyCause = typeof APPROVAL_ENDING_CAUSES.ROUTING_FAILURE;
@@ -128,7 +133,7 @@ export type ReconcileVoidCause = typeof APPROVAL_ENDING_CAUSES.RUN_CANCELLED | t
128
133
  * (取消赢下 VOID ⇒ `run_cancelled` / `run_exited`,兄弟被撤卡 ⇒ `batch_sibling_withdrawn`)。窗到期 / 广播全灭 / 决断类的词只落审计 settle 行或 409 码,不落这一列。
129
134
  */
130
135
  export type AskRowEndingCause = ReconcileDenyCause | ReconcileVoidCause | HolderGoneCause | typeof APPROVAL_ENDING_CAUSES.RUN_EXITED | typeof APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN;
131
- /** 问账行 `denied_reason` 列在 VOID / DENIED 行上的读词表({@link AskRowEndingCause} 的穷举,`satisfies` ⇒ 子集加词而本表没跟 = 编译红)。唯一读者 = {@link causeOfAskRow}。 */
136
+ /** 问账行 `ending_cause` 列的读词表({@link AskRowEndingCause} 的穷举,`satisfies` ⇒ 子集加词而本表没跟 = 编译红)。唯一读者 = {@link askRowEndingCause}。 */
132
137
  export declare const ASK_ROW_ENDING_CAUSE_WORDS: {
133
138
  readonly routing_failure_fail_closed: true;
134
139
  readonly run_cancelled: true;
@@ -142,12 +147,12 @@ export declare const ASK_ROW_ENDING_CAUSE_WORDS: {
142
147
  readonly shutdown_before_park: true;
143
148
  };
144
149
  /**
145
- * `expire` 终局 claim(窗到期 / 广播全灭 / 有序关停把一张卡转投递面 = `PARKING`)赢下时写进这一行 `denied_reason` 的**触发成因**:这一行此后若走到 `DENIED` / `VOID`,终态成因会覆盖它
146
- * ({@link AskRowEndingCause});`PARKED` 行的这一格就是「它为什么被转 park」。跨副本的读者(另一进程的悬挂腿轮询、CAS 败方读数)读到 `PARKING` / `PARKED` 时靠它归因,
147
- * 不替没写的行猜一个词。
150
+ * `expire` 终局 claim(窗到期 / 广播全灭 / 断连 / 呈不出 / 有序关停把一张卡转投递面 = `PARKING`)赢下时写进这一行 `park_trigger` 的**触发成因**:此后不再被写 ——
151
+ * 这一行走到 `PARKED` / `DENIED` / `VOID` 之后它仍是「这一行为什么被转 park」(终态成因在另一列,{@link AskRowEndingCause})。跨副本的读者(另一进程的悬挂腿轮询、
152
+ * CAS 败方读数)读到 `PARKING` / `PARKED` 时靠它归因,不替没写的行猜一个词。
148
153
  */
149
154
  export type ParkTriggerCause = typeof APPROVAL_ENDING_CAUSES.TTL_EXPIRED | typeof APPROVAL_ENDING_CAUSES.BROADCAST_LOST | typeof APPROVAL_ENDING_CAUSES.STREAM_DISCONNECTED | typeof APPROVAL_ENDING_CAUSES.UNDELIVERABLE | typeof APPROVAL_ENDING_CAUSES.SHUTDOWN_BEFORE_PARK;
150
- /** 问账行 `denied_reason` 在 `PARKING` / `PARKED` 行上的读词表({@link ParkTriggerCause} 的穷举)。唯一读者 = {@link causeOfAskRow}。 */
155
+ /** 问账行 `park_trigger` 列的读词表({@link ParkTriggerCause} 的穷举)。唯一读者 = {@link askRowParkTrigger}。 */
151
156
  export declare const PARK_TRIGGER_CAUSE_WORDS: {
152
157
  readonly ttl_expired: true;
153
158
  readonly broadcast_lost: true;
@@ -155,25 +160,24 @@ export declare const PARK_TRIGGER_CAUSE_WORDS: {
155
160
  readonly undeliverable: true;
156
161
  readonly shutdown_before_park: true;
157
162
  };
158
- /** 问账行成因的读数:`kind` 说这一个词在这一行上是哪种意思(终态成因 / 转 park 的触发成因)。 */
159
- export type AskRowCause = {
160
- readonly kind: "ending";
161
- readonly cause: AskRowEndingCause;
162
- } | {
163
- readonly kind: "park_trigger";
164
- readonly cause: ParkTriggerCause;
165
- };
166
163
  /**
167
- * 🔴 **问账行成因的唯一读口**:`denied_reason` 一列按 `state` 承载两种意思,本函数按 `state` 穷举分派(加状态 ⇒ 这里编译红),读者不自己先判 state 再查表:
168
- * · `VOID` / `DENIED` ⇒ 终态成因({@link AskRowEndingCause};决断口 410、晚答 404、CAS 败方读数、悬挂腿的轮询读这一种);
169
- * · `PARKING` / `PARKED` ⇒ 转 park 的触发成因({@link ParkTriggerCause};跨副本的悬挂腿与 CAS 败方读这一种);
170
- * · `DECIDED` / `STREAM_PENDING` / `absent`(败方读数的「行不在」)⇒ 无成因。
171
- * 列值 `null` / 键缺席(没写归因的存量行)或与 state 不配的词(手改 / 将来版本写的词)⇒ `undefined`(读面缺席即缺席,不替没写的行编一个成因)。返回纯数据。
164
+ * 🔴 **终态成因的唯一读口**(决断口 410、晚答 404、`GET` 读面、CAS 败方读数、悬挂腿的轮询读这一只):只读 `ending_cause` 一列,不看别的列。
165
+ * 行不变式由本口守:终态成因只在 `VOID` / `DENIED` 上作数(写者只在到这两态的转移上写它;别的态上读到 ⇒ 不是这一行的事实)—— 按 `state` 穷举(加状态 ⇒ 这里编译红)。
166
+ * 列值 `null` / 键缺席(没写归因的行)、词表外的词(手改 / 将来版本写的词)或与 state 不配 ⇒ `undefined`(读面缺席即缺席,不替没写的行编一个成因)。返回纯数据。
167
+ */
168
+ export declare function askRowEndingCause(row: {
169
+ readonly state: AskState;
170
+ readonly endingCause?: string | null;
171
+ }): AskRowEndingCause | undefined;
172
+ /**
173
+ * 🔴 **转 park 触发成因的唯一读口**(跨副本的悬挂腿、CAS 败方读数读这一只):只读 `park_trigger` 一列,不看别的列。
174
+ * 行不变式由本口守:触发成因只在**走过 `PARKING`** 的行上作数 —— `PARKING` / `PARKED`,以及从 `PARKING` 收成的 `DENIED` / `VOID`(终态不覆盖它);
175
+ * `STREAM_PENDING` / `DECIDED`(状态机上没有经 `PARKING` 到它们的边)上读到 ⇒ `undefined`。其余与 {@link askRowEndingCause} 同律。
172
176
  */
173
- export declare function causeOfAskRow(row: {
174
- readonly state: AskState | "absent";
175
- readonly deniedReason?: string | null;
176
- }): AskRowCause | undefined;
177
+ export declare function askRowParkTrigger(row: {
178
+ readonly state: AskState;
179
+ readonly parkTrigger?: string | null;
180
+ }): ParkTriggerCause | undefined;
177
181
  /** 非人**结局**的成因(审计 settle 行 `cause`、活卡结局帧 `tool_approval_complete.cause`):闭集去掉决断口 409 的三个词({@link DecisionRefusalCause})—— 那三个词不结束一张卡,只拒一次决议。 */
178
182
  export type SettleCause = Exclude<ApprovalEndingCause, DecisionRefusalCause>;
179
183
  /** 决断口 409 的成因子集(core 的 `detail.field` → 码;每词一个码,见 `http/resume-legs.ts`)。 */
@@ -63,22 +63,37 @@ export const PARK_TRIGGER_CAUSE_WORDS = {
63
63
  function isParkTriggerCause(v) {
64
64
  return Object.hasOwn(PARK_TRIGGER_CAUSE_WORDS, v);
65
65
  }
66
- export function causeOfAskRow(row) {
67
- const stored = row.deniedReason;
66
+ export function askRowEndingCause(row) {
67
+ const stored = row.endingCause;
68
68
  switch (row.state) {
69
69
  case "VOID":
70
70
  case "DENIED":
71
- return stored !== null && stored !== undefined && isAskRowEndingCause(stored) ? { kind: "ending", cause: stored } : undefined;
71
+ return stored !== null && stored !== undefined && isAskRowEndingCause(stored) ? stored : undefined;
72
72
  case "PARKING":
73
73
  case "PARKED":
74
- return stored !== null && stored !== undefined && isParkTriggerCause(stored) ? { kind: "park_trigger", cause: stored } : undefined;
75
74
  case "DECIDED":
76
75
  case "STREAM_PENDING":
77
- case "absent":
78
76
  return undefined;
79
77
  default: {
80
78
  const unhandled = row.state;
81
- throw new Error(`causeOfAskRow: unhandled ask row state ${String(unhandled)}`);
79
+ throw new Error(`askRowEndingCause: unhandled ask row state ${String(unhandled)}`);
80
+ }
81
+ }
82
+ }
83
+ export function askRowParkTrigger(row) {
84
+ const stored = row.parkTrigger;
85
+ switch (row.state) {
86
+ case "PARKING":
87
+ case "PARKED":
88
+ case "DENIED":
89
+ case "VOID":
90
+ return stored !== null && stored !== undefined && isParkTriggerCause(stored) ? stored : undefined;
91
+ case "DECIDED":
92
+ case "STREAM_PENDING":
93
+ return undefined;
94
+ default: {
95
+ const unhandled = row.state;
96
+ throw new Error(`askRowParkTrigger: unhandled ask row state ${String(unhandled)}`);
82
97
  }
83
98
  }
84
99
  }