@sema-agent/server 7.50.0 → 7.52.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 (222) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +83 -20
  4. package/dist/adoption/plan.d.ts +4 -4
  5. package/dist/adoption/plan.js +11 -0
  6. package/dist/approval-card.d.ts +26 -2
  7. package/dist/approval-card.js +9 -0
  8. package/dist/approval-hmac.d.ts +1 -1
  9. package/dist/approval-reconciler.d.ts +2 -2
  10. package/dist/approval-reconciler.js +1 -1
  11. package/dist/bench/l8/artifact.d.ts +1 -1
  12. package/dist/bench/s1/arms.d.ts +1 -1
  13. package/dist/bench/s1/live-deps.d.ts +1 -1
  14. package/dist/bench/s1/oracle.d.ts +1 -1
  15. package/dist/bench/s1/row.d.ts +1 -1
  16. package/dist/boot/budget-tracing.d.ts +1 -1
  17. package/dist/boot/budget-tracing.js +2 -5
  18. package/dist/boot/coordinators.js +3 -1
  19. package/dist/boot/deferred-sandbox-path-env.d.ts +1 -1
  20. package/dist/boot/device-lane.d.ts +79 -0
  21. package/dist/boot/device-lane.js +63 -0
  22. package/dist/boot/engine-lease.d.ts +1 -1
  23. package/dist/boot/execution-env.d.ts +48 -2
  24. package/dist/boot/execution-env.js +57 -5
  25. package/dist/boot/governance-seams.d.ts +2 -2
  26. package/dist/boot/leader.d.ts +1 -1
  27. package/dist/boot/memory-boundary.d.ts +2 -2
  28. package/dist/boot/memory-consolidation.d.ts +6 -6
  29. package/dist/boot/org-memory.d.ts +1 -1
  30. package/dist/boot/parked-revive-gate.d.ts +3 -3
  31. package/dist/boot/reapers.d.ts +2 -2
  32. package/dist/boot/reapers.js +17 -4
  33. package/dist/boot/resolve-spec.d.ts +15 -0
  34. package/dist/boot/resolve-spec.js +40 -15
  35. package/dist/boot/retention-lane.d.ts +2 -2
  36. package/dist/boot/runner-deps.d.ts +17 -7
  37. package/dist/boot/runner-deps.js +4 -2
  38. package/dist/boot/runtime-caps.d.ts +23 -0
  39. package/dist/boot/runtime-caps.js +24 -2
  40. package/dist/boot/session-faces.d.ts +6 -1
  41. package/dist/boot/session-faces.js +17 -3
  42. package/dist/boot/shutdown.d.ts +8 -0
  43. package/dist/boot/shutdown.js +3 -1
  44. package/dist/boot/stores.js +40 -1
  45. package/dist/boot/task-list-lane.d.ts +2 -2
  46. package/dist/brain.js +51 -31
  47. package/dist/budget.d.ts +2 -2
  48. package/dist/capabilities/center-plugins.d.ts +1 -1
  49. package/dist/capabilities/collab-workflows.d.ts +1 -1
  50. package/dist/capabilities/hands-lane.d.ts +3 -3
  51. package/dist/capabilities/scenarios.d.ts +3 -3
  52. package/dist/capabilities/skills.d.ts +1 -1
  53. package/dist/config-center/hot-keys-registry.js +5 -0
  54. package/dist/config-center/http-client.d.ts +2 -2
  55. package/dist/config-center/read-face.d.ts +1 -1
  56. package/dist/config-center/restart-signal.d.ts +1 -1
  57. package/dist/config-center/types.d.ts +10 -3
  58. package/dist/config-invariants.d.ts +2 -2
  59. package/dist/config-invariants.js +18 -0
  60. package/dist/config-types.d.ts +90 -18
  61. package/dist/config.js +91 -19
  62. package/dist/deployment-governance.d.ts +1 -1
  63. package/dist/device-enrollment.d.ts +125 -0
  64. package/dist/device-enrollment.js +156 -0
  65. package/dist/device-store.d.ts +385 -0
  66. package/dist/device-store.js +407 -0
  67. package/dist/device-ws-hub.d.ts +182 -0
  68. package/dist/device-ws-hub.js +1012 -0
  69. package/dist/device-ws-protocol.d.ts +429 -0
  70. package/dist/device-ws-protocol.js +464 -0
  71. package/dist/env-facts.d.ts +5 -2
  72. package/dist/env-facts.js +1 -0
  73. package/dist/execution-lane-caps.d.ts +152 -0
  74. package/dist/execution-lane-caps.js +166 -0
  75. package/dist/fleet/fleet-bus.d.ts +1 -1
  76. package/dist/fleet/fleet-reconciler.d.ts +1 -1
  77. package/dist/fleet/fleet-terminal-window.d.ts +1 -1
  78. package/dist/http/active-run-conflict.d.ts +2 -2
  79. package/dist/http/principal-gate.d.ts +1 -1
  80. package/dist/http/route-ctx.d.ts +24 -4
  81. package/dist/http/routes/admin-memory-optout.d.ts +48 -0
  82. package/dist/http/routes/admin-memory-optout.js +75 -0
  83. package/dist/http/routes/agents-roster.d.ts +1 -1
  84. package/dist/http/routes/approvals-assistant.js +21 -21
  85. package/dist/http/routes/capabilities.js +27 -11
  86. package/dist/http/routes/diagnostics.d.ts +1 -1
  87. package/dist/http/routes/leader.d.ts +1 -1
  88. package/dist/http/routes/memory-bundle.d.ts +1 -1
  89. package/dist/http/routes/memory-bundle.js +3 -1
  90. package/dist/http/routes/memory-compliance.d.ts +4 -4
  91. package/dist/http/routes/memory-origin.d.ts +4 -4
  92. package/dist/http/routes/memory-origin.js +3 -1
  93. package/dist/http/routes/memory-policy.js +6 -2
  94. package/dist/http/routes/retention-ops.d.ts +1 -1
  95. package/dist/http/routes/rules.d.ts +5 -2
  96. package/dist/http/routes/rules.js +84 -5
  97. package/dist/http/routes/run-memory-capture.d.ts +34 -0
  98. package/dist/http/routes/run-memory-capture.js +97 -0
  99. package/dist/http/routes/runs.d.ts +1 -0
  100. package/dist/http/routes/runs.js +212 -6
  101. package/dist/http/routes/session-sync.d.ts +4 -6
  102. package/dist/http/routes/session-sync.js +60 -47
  103. package/dist/http/routes/sessions.js +61 -188
  104. package/dist/http/routes/shared-memory.d.ts +1 -1
  105. package/dist/http/routes/tasks.js +1 -1
  106. package/dist/http/routes/workflows.js +19 -0
  107. package/dist/http/server.d.ts +49 -13
  108. package/dist/http/server.js +109 -13
  109. package/dist/http/verify-rounds.d.ts +1 -1
  110. package/dist/http/wire-types.d.ts +41 -8
  111. package/dist/leader/endpoint.d.ts +1 -1
  112. package/dist/leader/leader.d.ts +1 -1
  113. package/dist/leader/repair-oracle.d.ts +2 -2
  114. package/dist/leader/repair-oracle.js +1 -1
  115. package/dist/leader/wire.js +4 -2
  116. package/dist/main.js +23 -9
  117. package/dist/memory-bundle-engine.d.ts +1 -1
  118. package/dist/memory-operator-faces.d.ts +5 -5
  119. package/dist/memory-origin-fence.d.ts +50 -0
  120. package/dist/memory-origin-fence.js +15 -0
  121. package/dist/memory-posture.d.ts +13 -1
  122. package/dist/memory-posture.js +1 -0
  123. package/dist/memory-scope.d.ts +6 -1
  124. package/dist/memory-scope.js +3 -2
  125. package/dist/model-select.d.ts +2 -2
  126. package/dist/observability/fail-open.d.ts +17 -5
  127. package/dist/observability/fail-open.js +17 -5
  128. package/dist/orchestration/hardened-vm-runner.d.ts +9 -2
  129. package/dist/orchestration/hardened-vm-runner.js +11 -1
  130. package/dist/orchestration/hardened-vm-worker-runner.js +2 -2
  131. package/dist/orchestration/workflow-completion-inbox.d.ts +2 -2
  132. package/dist/plugins/background-shell-support.d.ts +2 -2
  133. package/dist/plugins/blob-backend.d.ts +7 -2
  134. package/dist/plugins/blob-backend.js +22 -18
  135. package/dist/plugins/caching-session-store.d.ts +7 -3
  136. package/dist/plugins/checkpoint-store-sql.d.ts +2 -2
  137. package/dist/plugins/device-store-sql.d.ts +130 -0
  138. package/dist/plugins/device-store-sql.js +574 -0
  139. package/dist/plugins/file-history-store-sql.d.ts +170 -0
  140. package/dist/plugins/file-history-store-sql.js +827 -0
  141. package/dist/plugins/file-run-store.d.ts +44 -5
  142. package/dist/plugins/file-run-store.js +85 -13
  143. package/dist/plugins/fork-routing-session-store.d.ts +1 -1
  144. package/dist/plugins/fork-routing-session-store.js +2 -2
  145. package/dist/plugins/leader-run-store-sql.d.ts +1 -1
  146. package/dist/plugins/local-session-store.d.ts +9 -1
  147. package/dist/plugins/memory-embedder-fingerprint.d.ts +1 -1
  148. package/dist/plugins/memory-optout-grant-store-sql.d.ts +139 -0
  149. package/dist/plugins/memory-optout-grant-store-sql.js +157 -0
  150. package/dist/plugins/memory-origin-law.d.ts +2 -2
  151. package/dist/plugins/memory-run-store.d.ts +4 -3
  152. package/dist/plugins/memory-run-store.js +2 -1
  153. package/dist/plugins/permission-rule-store-file.d.ts +16 -6
  154. package/dist/plugins/permission-rule-store-file.js +77 -13
  155. package/dist/plugins/permission-rule-store-sql.d.ts +156 -8
  156. package/dist/plugins/permission-rule-store-sql.js +123 -21
  157. package/dist/plugins/pg-pool.js +59 -0
  158. package/dist/plugins/pg-safe-json.d.ts +1 -1
  159. package/dist/plugins/remote-env-device.d.ts +271 -0
  160. package/dist/plugins/remote-env-device.js +727 -0
  161. package/dist/plugins/remote-env-k8s.d.ts +1 -1
  162. package/dist/plugins/remote-scratchpad.js +1 -1
  163. package/dist/plugins/retention-lane-store-sql.d.ts +3 -3
  164. package/dist/plugins/retention-store-sql.d.ts +7 -7
  165. package/dist/plugins/retention-store-sql.js +6 -0
  166. package/dist/plugins/run-store-sql.d.ts +9 -3
  167. package/dist/plugins/run-store-sql.js +2 -2
  168. package/dist/plugins/send-user-file.d.ts +1 -1
  169. package/dist/plugins/session-placement.d.ts +1 -1
  170. package/dist/plugins/sql-errors.d.ts +1 -1
  171. package/dist/plugins/store-backend.d.ts +68 -29
  172. package/dist/plugins/store-backend.js +78 -15
  173. package/dist/plugins/store-contracts.d.ts +5 -3
  174. package/dist/plugins/task-list-store-sql.d.ts +1 -1
  175. package/dist/plugins/tidb-pool.js +59 -0
  176. package/dist/plugins/web-search.d.ts +1 -1
  177. package/dist/prompts-domain-validate.js +1 -1
  178. package/dist/resource-suspend.d.ts +1 -1
  179. package/dist/resource-window.d.ts +3 -3
  180. package/dist/rules-consent.d.ts +231 -9
  181. package/dist/rules-consent.js +411 -13
  182. package/dist/run-local.js +1 -0
  183. package/dist/runs.d.ts +15 -1
  184. package/dist/runs.js +19 -0
  185. package/dist/runtime-caps-resolver.d.ts +36 -1
  186. package/dist/runtime-caps-resolver.js +25 -0
  187. package/dist/runtime-governance.d.ts +1 -1
  188. package/dist/security.d.ts +5 -5
  189. package/dist/security.js +1 -1
  190. package/dist/session-sync.d.ts +52 -37
  191. package/dist/session-sync.js +19 -16
  192. package/dist/shared-memory-scope-authorizer.d.ts +1 -1
  193. package/dist/sighup-idle.d.ts +1 -1
  194. package/dist/spec-fields.d.ts +22 -1
  195. package/dist/spec-fields.js +11 -0
  196. package/dist/store-live-probe.d.ts +2 -2
  197. package/dist/task-a2a.d.ts +1 -1
  198. package/dist/task-cwd.d.ts +25 -0
  199. package/dist/task-cwd.js +3 -0
  200. package/dist/task-mcp.d.ts +3 -3
  201. package/dist/task-settings.d.ts +3 -3
  202. package/dist/task-settings.js +3 -1
  203. package/dist/task-workflow.d.ts +1 -1
  204. package/dist/text-bidi.d.ts +2 -2
  205. package/dist/tool-approval.d.ts +191 -9
  206. package/dist/tool-approval.js +248 -29
  207. package/dist/trace/core-keyset-guard.d.ts +1 -1
  208. package/dist/trace/engine-notice-wire.d.ts +6 -4
  209. package/dist/trace/engine-notice-wire.js +2 -0
  210. package/dist/trace/ledger-events.d.ts +1 -1
  211. package/dist/trace/ledger-sink.d.ts +2 -2
  212. package/dist/trace/project.d.ts +3 -2
  213. package/dist/trace/project.js +1 -0
  214. package/dist/turn-activity.d.ts +1 -1
  215. package/dist/wall-clock-jump-guard.d.ts +3 -3
  216. package/package.json +2 -2
  217. package/dist/http/tar.d.ts +0 -17
  218. package/dist/http/tar.js +0 -53
  219. package/dist/http/workspace-content.d.ts +0 -11
  220. package/dist/http/workspace-content.js +0 -20
  221. package/dist/plugins/file-snapshot-store-sql.d.ts +0 -170
  222. package/dist/plugins/file-snapshot-store-sql.js +0 -213
@@ -71,8 +71,9 @@ export declare class MemoryRunStore {
71
71
  setSuspended(taskId: string): Promise<void>;
72
72
  /** design/80 D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review. */
73
73
  setNeedsReview(taskId: string): Promise<void>;
74
- /** Durable F4 resume: flip suspended/needs_review → running (CAS) + reset cancel/preempt flags. */
75
- markResuming(taskId: string): Promise<boolean>;
74
+ /** Durable F4 resume: flip suspended/needs_review → running (CAS) + reset cancel/preempt flags.
75
+ * S-51(codex R1 [medium]):认领 = 接管 `instanceId`(= 当前持有副本;SQL 孪生同注)。CAS 输不改。 */
76
+ markResuming(taskId: string, instanceId: string): Promise<boolean>;
76
77
  getActiveTaskId(sessionId: string): Promise<string | undefined>;
77
78
  getRun(taskId: string): Promise<RunRecord | undefined>;
78
79
  /** Runs newest-first, keyset on (createdAt, taskId) DESC; optional exact-match filters (owner is exact, like the SQL). */
@@ -140,7 +141,7 @@ export declare class MemoryRunStore {
140
141
  * probe instead of a SQL JOIN (formerly a NO-OP, which was one of the five closed doors in the "suspended run
141
142
  * locks the session forever" incident). Semantics verbatim from the SQL twin: suspended ∧ updatedAt < cutoff
142
143
  * ∧ NOT probe.hasPending(session) → failed 'approval expired before decision' / error_code 'approval.expired'
143
- * + release the task_active claim. The NOT-pending clause is the design/80 D-D adversarial-blocker guard
144
+ * + release the task_active claim. The NOT-pending clause is the design/80 D-D blocker guard
144
145
  * (SQL 孪生现在的真身:`run-store-sql.ts` 的 `reapSuspended`;`tidb-run-store.ts` 该壳文件已删(5.0.0)): a row whose checkpoint is STILL pending belongs to the checkpoint-state
145
146
  * sweeps (SLA deny-sweep / failSuspendedWithExpiredCheckpoint) — time-reaping it here would orphan a model
146
147
  * leg on a now-unlocked session. Probe absent ⇒ honest NO-OP (never kill a park without the guard).
@@ -133,11 +133,12 @@ export class MemoryRunStore {
133
133
  r.updatedAt = new Date();
134
134
  }
135
135
  }
136
- async markResuming(taskId) {
136
+ async markResuming(taskId, instanceId) {
137
137
  const r = this.runs.get(taskId);
138
138
  if (!r || (r.status !== "suspended" && r.status !== "needs_review"))
139
139
  return false;
140
140
  r.status = "running";
141
+ r.instanceId = instanceId;
141
142
  r.cancelRequested = false;
142
143
  r.preemptRequested = false;
143
144
  r.updatedAt = new Date();
@@ -1,5 +1,5 @@
1
1
  import { FilePermissionRuleStoreProvider, type RuleApprovalRecord, type RuleApprovalRecordStore, type RuleScope, type StaleRuleApprovalRecord } from "@sema-agent/core";
2
- import { type RuleImportTicket, type RuleTicketDecisionClock, type RuleTicketRedeemResult } from "./permission-rule-store-sql.js";
2
+ import { type RuleImportTicket, type RuleTicketDecisionClock, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./permission-rule-store-sql.js";
3
3
  import type { PermissionRuleStoreBundle } from "../rules-consent.js";
4
4
  /**
5
5
  * `RuleApprovalRecordStore` 的 File 形。
@@ -44,18 +44,28 @@ export declare class FileRuleImportTicketStore {
44
44
  ticketId: string;
45
45
  principal: string;
46
46
  approvalId: string;
47
+ purpose: RuleTicketPurpose;
47
48
  candidates: ReadonlyArray<{
48
49
  rule: string;
49
50
  scope: RuleScope;
50
51
  }>;
51
52
  ttlMs: number;
52
53
  }): Promise<RuleImportTicket>;
53
- /** 一次性**认领**。四条否定项的判序与 SQL 形逐字相同(unknown → wrong-principal → consumed expired),
54
- * 于是两形的服务端日志归因可比;wire 面把四类折成同一个 404(零存在性 oracle)。 */
55
- consume(ticketId: string, principal: string, opts?: RuleTicketDecisionClock): Promise<RuleTicketRedeemResult>;
54
+ /** 一次性**认领**。五条否定项的判序与 SQL 形逐字相同(unknown → wrong-principal → wrong-purpose
55
+ * consumed → expired),于是两形的服务端日志归因可比;wire 面把五类折成同一个 404(零存在性 oracle)。 */
56
+ consume(ticketId: string, principal: string, purpose: RuleTicketPurpose, opts?: RuleTicketDecisionClock): Promise<RuleTicketRedeemResult>;
56
57
  /** 把认领**放回去**。`mustRemainValidMs` = 放回之后至少还要能用多久 —— 撑不过就不算放回成功
57
- * (理由逐字见 SQL 侧 `release` 的头注:承诺一次必然兑现不了的重试比不承诺更坏)。 */
58
- release(ticketId: string, principal: string, mustRemainValidMs?: number, opts?: RuleTicketDecisionClock): Promise<boolean>;
58
+ * (理由逐字见 SQL 侧 `release` 的头注:承诺一次必然兑现不了的重试比不承诺更坏)。
59
+ * `claimId` = A7 的认领围栏(R1-F1):被顶替的旧持有者动不了现持有者的认领。 */
60
+ release(ticketId: string, principal: string, claimId: string, mustRemainValidMs?: number, opts?: RuleTicketDecisionClock): Promise<boolean>;
61
+ /** #265 A7 CLAIMED → **SETTLED**。判据与 SQL 孪生逐字同源(持有**这一代**认领 ∧ 未落定;过期
62
+ * **刻意不设门** —— 理由逐字见 SQL 侧 `settle` 的头注:拒绝记录一件已经发生的事只会把真相丢掉)。 */
63
+ settle(ticketId: string, principal: string, claimId: string, outcome: unknown): Promise<boolean>;
64
+ /** #265 A7 三态快照。属主门与车道门同源(别人的票 / 别条车道的票 / 不存在的票一律 `undefined`)。 */
65
+ peek(ticketId: string, principal: string, purpose: RuleTicketPurpose): Promise<RuleTicketSnapshot | undefined>;
66
+ /** #265 A7 崩溃窗恢复:抢过一次**可证陈旧**的认领(判据与 SQL 孪生逐字同源 —— 含"过期刻意不设门"
67
+ * 那一条:过期只挡开始一次兑付,不挡做完一次已经开始的)。抢到之后**换一代认领**。 */
68
+ reclaim(ticketId: string, principal: string, purpose: RuleTicketPurpose, minClaimAgeMs: number): Promise<RuleTicketReclaimResult>;
59
69
  }
60
70
  /** local 车道的三面束 + 生命周期。`dispose()` 释放 core File provider 的写锁(见 {@link createFilePermissionRuleStores})。 */
61
71
  export interface FilePermissionRuleStores extends PermissionRuleStoreBundle {
@@ -1,4 +1,5 @@
1
1
  import { join } from "node:path";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { readdirSync } from "node:fs";
3
4
  import { FilePermissionRuleStoreProvider, AppendLog, ensureDir, readJsonlRecords, } from "@sema-agent/core";
4
5
  import { z } from "zod";
@@ -92,11 +93,14 @@ const TicketLineSchema = z.union([
92
93
  approvalId: z.string().min(1),
93
94
  payloadHash: z.string().min(1),
94
95
  expiresAtMs: z.number().int().nonnegative().safe(),
96
+ purpose: z.enum(["cc-import", "local-import"]).optional(),
95
97
  })
96
98
  .strict(),
97
- z.object({ op: z.literal("consume"), ticketId: z.string().min(1), atMs: z.number().int().nonnegative().safe() }).strict(),
99
+ z.object({ op: z.literal("consume"), ticketId: z.string().min(1), atMs: z.number().int().nonnegative().safe(), claimId: z.string().min(1).optional() }).strict(),
98
100
  z.object({ op: z.literal("release"), ticketId: z.string().min(1) }).strict(),
101
+ z.object({ op: z.literal("settle"), ticketId: z.string().min(1), outcome: z.unknown() }).strict(),
99
102
  ]);
103
+ const LEGACY_TICKET_PURPOSE = "cc-import";
100
104
  class CorruptionGate {
101
105
  face;
102
106
  path;
@@ -204,16 +208,35 @@ export class FileRuleImportTicketStore {
204
208
  }
205
209
  const line = parsed.data;
206
210
  if (line.op === "mint") {
207
- this.rows.set(line.ticketId, { ownerKey: line.ownerKey, approvalId: line.approvalId, payloadHash: line.payloadHash, expiresAtMs: line.expiresAtMs });
211
+ this.rows.set(line.ticketId, {
212
+ ownerKey: line.ownerKey,
213
+ approvalId: line.approvalId,
214
+ payloadHash: line.payloadHash,
215
+ purpose: line.purpose ?? LEGACY_TICKET_PURPOSE,
216
+ expiresAtMs: line.expiresAtMs,
217
+ });
208
218
  continue;
209
219
  }
210
220
  const row = this.rows.get(line.ticketId);
211
221
  if (row === undefined)
212
222
  continue;
213
- if (line.op === "consume")
214
- row.consumedAtMs = line.atMs;
215
- else
216
- delete row.consumedAtMs;
223
+ switch (line.op) {
224
+ case "consume":
225
+ row.consumedAtMs = line.atMs;
226
+ row.claimId = line.claimId ?? `legacy-claim-${randomUUID()}`;
227
+ break;
228
+ case "release":
229
+ delete row.consumedAtMs;
230
+ delete row.claimId;
231
+ break;
232
+ case "settle":
233
+ row.outcome = line.outcome;
234
+ break;
235
+ default: {
236
+ const unknown = line;
237
+ this.gate.trip(`a replayed ticket line carries an unknown op: ${JSON.stringify(unknown)}`);
238
+ }
239
+ }
217
240
  }
218
241
  this.log = new AppendLog(path);
219
242
  }
@@ -228,37 +251,78 @@ export class FileRuleImportTicketStore {
228
251
  this.gate.assertUsable();
229
252
  const expiresAtMs = this.now() + input.ttlMs;
230
253
  const payloadHash = buildRulePayloadHash(input.candidates);
231
- this.log.append({ op: "mint", ticketId: input.ticketId, ownerKey: input.principal, approvalId: input.approvalId, payloadHash, expiresAtMs }, true);
232
- this.rows.set(input.ticketId, { ownerKey: input.principal, approvalId: input.approvalId, payloadHash, expiresAtMs });
254
+ this.log.append({ op: "mint", ticketId: input.ticketId, ownerKey: input.principal, approvalId: input.approvalId, payloadHash, purpose: input.purpose, expiresAtMs }, true);
255
+ this.rows.set(input.ticketId, { ownerKey: input.principal, approvalId: input.approvalId, payloadHash, purpose: input.purpose, expiresAtMs });
233
256
  return { ticketId: input.ticketId, approvalId: input.approvalId, payloadHash, expiresAtMs };
234
257
  }
235
- async consume(ticketId, principal, opts) {
258
+ async consume(ticketId, principal, purpose, opts) {
236
259
  this.gate.assertUsable();
237
260
  const row = this.rows.get(ticketId);
238
261
  if (row === undefined)
239
262
  return { ok: false, reason: "unknown" };
240
263
  if (row.ownerKey !== principal)
241
264
  return { ok: false, reason: "wrong-principal" };
265
+ if (row.purpose !== purpose)
266
+ return { ok: false, reason: "wrong-purpose" };
242
267
  if (row.consumedAtMs !== undefined)
243
268
  return { ok: false, reason: "consumed" };
244
269
  const stampMs = this.now();
245
270
  if (row.expiresAtMs <= (opts?.decisionNowMs ?? stampMs))
246
271
  return { ok: false, reason: "expired" };
247
- this.log.append({ op: "consume", ticketId, atMs: stampMs }, true);
272
+ const claimId = randomUUID();
273
+ this.log.append({ op: "consume", ticketId, atMs: stampMs, claimId }, true);
248
274
  row.consumedAtMs = stampMs;
249
- return { ok: true, approvalId: row.approvalId, payloadHash: row.payloadHash };
275
+ row.claimId = claimId;
276
+ return { ok: true, approvalId: row.approvalId, payloadHash: row.payloadHash, claimId };
250
277
  }
251
- async release(ticketId, principal, mustRemainValidMs = 0, opts) {
278
+ async release(ticketId, principal, claimId, mustRemainValidMs = 0, opts) {
252
279
  this.gate.assertUsable();
253
280
  const row = this.rows.get(ticketId);
254
- if (row === undefined || row.ownerKey !== principal || row.consumedAtMs === undefined)
281
+ if (row === undefined || row.ownerKey !== principal || row.consumedAtMs === undefined || row.claimId !== claimId || row.outcome !== undefined)
255
282
  return false;
256
283
  if (row.expiresAtMs <= (opts?.decisionNowMs ?? this.now()) + mustRemainValidMs)
257
284
  return false;
258
285
  this.log.append({ op: "release", ticketId }, true);
259
286
  delete row.consumedAtMs;
287
+ delete row.claimId;
288
+ return true;
289
+ }
290
+ async settle(ticketId, principal, claimId, outcome) {
291
+ this.gate.assertUsable();
292
+ const row = this.rows.get(ticketId);
293
+ if (row === undefined || row.ownerKey !== principal || row.consumedAtMs === undefined || row.claimId !== claimId || row.outcome !== undefined)
294
+ return false;
295
+ this.log.append({ op: "settle", ticketId, outcome }, true);
296
+ row.outcome = outcome;
260
297
  return true;
261
298
  }
299
+ async peek(ticketId, principal, purpose) {
300
+ this.gate.assertUsable();
301
+ const row = this.rows.get(ticketId);
302
+ if (row === undefined || row.ownerKey !== principal || row.purpose !== purpose)
303
+ return undefined;
304
+ if (row.outcome !== undefined)
305
+ return { state: "settled", outcome: row.outcome };
306
+ if (row.consumedAtMs === undefined)
307
+ return { state: "minted" };
308
+ return { state: "claimed", claimAgeMs: Math.max(0, this.now() - row.consumedAtMs) };
309
+ }
310
+ async reclaim(ticketId, principal, purpose, minClaimAgeMs) {
311
+ this.gate.assertUsable();
312
+ const row = this.rows.get(ticketId);
313
+ if (row === undefined || row.ownerKey !== principal || row.purpose !== purpose)
314
+ return { ok: false };
315
+ if (row.consumedAtMs === undefined || row.claimId === undefined || row.outcome !== undefined)
316
+ return { ok: false };
317
+ const stampMs = this.now();
318
+ if (stampMs - row.consumedAtMs < minClaimAgeMs)
319
+ return { ok: false };
320
+ const claimId = randomUUID();
321
+ this.log.append({ op: "consume", ticketId, atMs: stampMs, claimId }, true);
322
+ row.consumedAtMs = stampMs;
323
+ row.claimId = claimId;
324
+ return { ok: true, approvalId: row.approvalId, payloadHash: row.payloadHash, claimId };
325
+ }
262
326
  }
263
327
  export function createFilePermissionRuleStores(root, opts) {
264
328
  const dir = join(root, "permission-rules");
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  import { type PermissionRuleWriter, type PermissionRuleStore, type PermissionRuleStoreProvider, type RuleScope, type RuleSyncFrontier, type RuleApprovalRecord, type StaleRuleApprovalRecord, type RuleApprovalRecordStore, type RuleOwner } from "@sema-agent/core";
2
3
  import type { Pool as MySqlPool } from "mysql2/promise";
3
4
  import type { PgQueryFn } from "./pg-query.js";
@@ -13,6 +14,16 @@ import { type SqlDriver } from "./sql-driver.js";
13
14
  * 也是三份会各自漂的形状假设。
14
15
  */
15
16
  export declare function writerOfSqlRuleStore(store: PermissionRuleStore): PermissionRuleWriter | undefined;
17
+ /**
18
+ * 382P1(core 7.0.0 design/382 §4.3)—— 这是 **DURABLE 二员面**的 at-rest 形:`{global, project-with-root}`,
19
+ * 与 core `isValidDurableScope` 逐格等价(`test/permission-rule-durable-face-pin.test.ts` 探针表钉住)。
20
+ * `RuleScope` 联合自 v7 起有第三员 `session`,但它**结构上**不许进持久店(家在会话 overlay,随会话死):
21
+ * at-rest 解码不认它 ⇒ 一条被外部写进来的 session 行整桶拒读(fail-closed),写臂另由 core 的
22
+ * `assertWriteDeltaScopeDurable` 在任何 DB 往返之前以典型码拒(见 `apply`)。consent 面(三员)归
23
+ * engine/cli 半场;本仓 wire 入口 `parseRuleScope` 只拼二员,所以审批记录的候选也用这张表(下方
24
+ * `RuleCandidateSchema`)——那是本仓自己的窄型收口,不是把两张表合成一张。
25
+ */
26
+ export declare const DurableRuleScopeSchema: z.ZodType<RuleScope>;
16
27
  export declare const PERMISSION_RULE_TABLE = "permission_rule";
17
28
  export declare const PERMISSION_RULE_APPROVAL_TABLE = "permission_rule_approval";
18
29
  export declare const PERMISSION_RULE_TICKET_TABLE = "permission_rule_ticket";
@@ -35,7 +46,7 @@ export declare const TIDB_PERMISSION_RULE_STATEMENTS: readonly string[];
35
46
  * 本函数留给只需要这三张表的集成测试)。 */
36
47
  export declare function ensureTiDBPermissionRuleSchema(pool: MySqlPool): Promise<void>;
37
48
  /**
38
- * #340 升级前置断言 —— **拒启**,不是 warn(codex 对抗复审 [high],验真后修;形照 #119 的
49
+ * #340 升级前置断言 —— **拒启**,不是 warn(codex 交叉复审 [high],验真后修;形照 #119 的
39
50
  * `assertToolResultProvenanceSchema` 先例)。
40
51
  *
41
52
  * 病:本仓不发 `ALTER TABLE` 迁移(SCHEMA POLICY:改列就改 CREATE + 删库重建)。于是一台**没删表**就升
@@ -51,7 +62,7 @@ export declare function ensureTiDBPermissionRuleSchema(pool: MySqlPool): Promise
51
62
  * 判据用**能力探测**而不是版本号/information_schema:发一条恒空的 `WHERE 1=0` 读,列不在就报错。
52
63
  * 只判「两列在不在」,不判宽度(两列都是 TEXT 族,没有截断轴)。
53
64
  *
54
- * 🔴 **只有可证的缺列才给破坏性指路**(codex 对抗复审 round2/round3 [high],两轮收窄后的终形):
65
+ * 🔴 **只有可证的缺列才给破坏性指路**(codex 交叉复审 round2/round3 [high],两轮收窄后的终形):
55
66
  * 「探针失败」**不等于**「列不在」。超时、连接被重置、取消、资源不足、列级权限被拒、表整个不存在 ——
56
67
  * 每一种都会让这条读抛错,而把它们诊断成「去 DROP TABLE」是一条**会真的毁掉审计记录**的建议(比它要
57
68
  * 挡的缺陷更贵)。round2 的两步探(先读一列老列)也不够:两次读之间同样可以插进一次瞬时故障。
@@ -70,6 +81,29 @@ export declare function ensureTiDBPermissionRuleSchema(pool: MySqlPool): Promise
70
81
  export declare function assertPermissionRuleApprovalSchema(query: (sql: string) => Promise<{
71
82
  rows: Record<string, unknown>[];
72
83
  }>, dialect: "tidb" | "pg"): Promise<void>;
84
+ /**
85
+ * #265 A7 升级前置断言 —— **拒启**(形与理由逐字照 {@link assertPermissionRuleApprovalSchema} 的配方,
86
+ * 那条头注写了这道门为什么必须是拒启而不是 warn:本仓不发 `ALTER TABLE`,`CREATE TABLE IF NOT EXISTS`
87
+ * 对存量表是空操作,而新列被**无条件**引用)。
88
+ *
89
+ * 病:一台没删表就升上来的部署,`permission_rule_ticket` 上没有 `redeemed_outcome` 列,而 A7 的三态
90
+ * 协议(`settle` / `peek` / `reclaim`)三条语句全都引用它。后果不是"少一个新功能":
91
+ * · `POST /v1/rules/local-import/redeem` 的落定那一跳撞 unknown column ⇒ 票停在 CLAIMED 永不结算,
92
+ * 属主的重放只能拿到 409,一次人的确认就此卡死到票过期;
93
+ * · 更坏的是它**只在兑付走完之后**才炸 —— 规则已经落进店了,而客户端拿到的是失败:一次**真实生效
94
+ * 的同意**被报成没发生。
95
+ * 所以判据放在启动期、拒启,而不是等第一位用户去踩。
96
+ *
97
+ * 判据同款:发一条恒空的 `WHERE 1=0` 读;**只有可证的缺列**才给破坏性指路(其余错误原样 rethrow ——
98
+ * 超时/连接重置/权限/表不在都会让这条读抛,把它们诊断成"去 DROP TABLE"是一条比缺陷更贵的建议)。
99
+ *
100
+ * 🔴 破坏性指路的代价在本表上**比审批表小得多**,文案照实说:票是分钟级 TTL 的治理瞬态(过期即死),
101
+ * 删表最多让**正在预览、还没按确认**的那几个人重走一次 prepare;持久规则在 `permission_rule` 里,
102
+ * 一行都不动。
103
+ */
104
+ export declare function assertPermissionRuleTicketSchema(query: (sql: string) => Promise<{
105
+ rows: Record<string, unknown>[];
106
+ }>, dialect: "tidb" | "pg"): Promise<void>;
73
107
  export declare function ensurePgPermissionRuleSchema(q: PgQueryFn): Promise<void>;
74
108
  /**
75
109
  * 一只桶的**存储键**(纯数据 ⇒ `build*`)。
@@ -137,15 +171,28 @@ export interface RuleImportTicket {
137
171
  payloadHash: string;
138
172
  expiresAtMs: number;
139
173
  }
140
- /** 消费结果。**四种拒绝合并成一个 `refused` 形**是刻意的:调用点把它们全部翻成同一个 404,
141
- * 于是「这张票不存在」「它是别人的」「它过期了」「它已经用过了」在 wire 上不可区分(禁存在性 oracle)。 */
174
+ /**
175
+ * #265 A7 —— 一张票是**哪条导入车道**铸的(codex 交叉轮 R1-F2,验真后修)。
176
+ *
177
+ * 🔴 闭集且**不可变**:两条车道共用这张表,而它们的票语义刻意不同(cc-import 一次性 / local-import
178
+ * 可恢复三态)。票不带这一位时,两条车道的兑付口互相**可以吃对方的票** —— 那不是"多一条路",那是让
179
+ * 一条车道的语义在**另一条车道的代码里**被改掉。铸票时定,此后每一次读写都拿它当匹配项。
180
+ */
181
+ export type RuleTicketPurpose = "cc-import" | "local-import";
182
+ /** 消费结果。**五种拒绝合并成一个 `refused` 形**是刻意的:调用点把它们全部翻成同一个 404,
183
+ * 于是「这张票不存在」「它是别人的」「它是另一条车道的」「它过期了」「它已经用过了」在 wire 上
184
+ * 不可区分(禁存在性 oracle)。
185
+ *
186
+ * `claimId` = 本次认领的**代次**(A7 围栏,R1-F1):`release`/`settle` 必须原样递回来,于是一个被
187
+ * 顶替的旧持有者动不了现持有者的认领。 */
142
188
  export type RuleTicketRedeemResult = {
143
189
  ok: true;
144
190
  approvalId: string;
145
191
  payloadHash: string;
192
+ claimId: string;
146
193
  } | {
147
194
  ok: false;
148
- reason: "unknown" | "wrong-principal" | "expired" | "consumed";
195
+ reason: "unknown" | "wrong-principal" | "wrong-purpose" | "expired" | "consumed";
149
196
  };
150
197
  /**
151
198
  * [4232]③/[4235]③ —— 票的**判定钟**覆写(记账钟永远不覆写,见 `consume` 内注)。
@@ -157,6 +204,43 @@ export type RuleTicketRedeemResult = {
157
204
  export interface RuleTicketDecisionClock {
158
205
  decisionNowMs?: number;
159
206
  }
207
+ /**
208
+ * #265 A7 —— 一张票的**三态快照**(MINTED → CLAIMED → SETTLED)。
209
+ *
210
+ * 🔴 判别式联合而不是"一个态字符串 + 两个可选字段":三个态各自带的东西**互不相干**,而消费方对三者
211
+ * 要做的事完全不同(首兑 / 判租约 / 逐字回放)。塌成一个可选包会让"已落定却读不出终局"这种矛盾态
212
+ * 变得可表达 —— 而那正是本协议最不能出错的地方(回放不出终局 = 要么重跑一次已经跑过的兑付,要么
213
+ * 对属主说一次谎)。
214
+ *
215
+ * `claimAgeMs` 由**店自己**用它的记账钟算,不交给调用方去减:认领戳(`consumed_at_ms`)是真墙钟,
216
+ * 而车道手里那只是单调投影 —— 两只钟不在同一个域,跨域相减出来的"年龄"没有意义(本仓 [4232]③ 的
217
+ * 教训:两只钟必须分家,谁的问题谁的钟答)。
218
+ */
219
+ export type RuleTicketSnapshot = {
220
+ state: "minted";
221
+ } | {
222
+ state: "claimed";
223
+ claimAgeMs: number;
224
+ } | {
225
+ state: "settled";
226
+ outcome: unknown;
227
+ };
228
+ /**
229
+ * #265 A7 —— 抢认领的结果。赢家拿回**票上那两样不可变的东西**(`approval_id` / `payload_hash`),
230
+ * 与 {@link RuleTicketRedeemResult} 的成功臂同形:抢到认领之后要做的事与首次认领之后**一模一样**
231
+ * (读记录 → 载荷比对 → 走批),所以两条路必须交出同一份素材,否则恢复臂就得自己再发一条读 ——
232
+ * 而"认领之后还有一次可能失败的 I/O"正是本仓 round4 修掉的那个形。
233
+ *
234
+ * 拒绝臂**不分类**:没抢到只有一个处置(继续等/让客户端稍后再来),分类只会诱导调用方去猜。
235
+ */
236
+ export type RuleTicketReclaimResult = {
237
+ ok: true;
238
+ approvalId: string;
239
+ payloadHash: string;
240
+ claimId: string;
241
+ } | {
242
+ ok: false;
243
+ };
160
244
  /**
161
245
  * [4232]③ 同族的 **GC 宽限**:`reapExpired` 只收「按墙钟至少已死一个完整 TTL」的票行。
162
246
  *
@@ -169,6 +253,23 @@ export interface RuleTicketDecisionClock {
169
253
  * ——这正是 core 建议的姿态(「接受 + 票廉价重铸」)。
170
254
  */
171
255
  export declare const RULE_IMPORT_TICKET_GC_GRACE_MS: number;
256
+ /**
257
+ * #265 A7 —— **数据库自己的钟**(毫秒),两方言各一句(codex 交叉轮 R5-F1,验真后修)。
258
+ *
259
+ * 用处:`settled_at_ms` 的写入与清扫「已落定」那一支的截止**必须在同一个钟域**。它们此前各拿各副本的
260
+ * 墙钟:落定那台若比跑清扫那台慢过一个宽限(本仓 [4232]③ 记过一次真实的 11 分钟 NTP 纠正),一份**刚刚**
261
+ * 生出来的权威回执下一拍就被删,而"落定之后必有一个完整的回放窗"是本批新许下的承诺 —— 它不能建立在
262
+ * 「两台机器的钟差不多」这条没人声明过的假设上。数据库的钟是这两台副本**唯一**共享的那口钟。
263
+ *
264
+ * 🔴 射程**刻意只到这一支**:票的其余三列(`expires_at_ms` / `consumed_at_ms` / `created_at_ms`)仍在
265
+ * 既有的副本墙钟域里 —— 那是 cc-import 车道的**既存已登记残余**([4232]③,收口方向逐字见
266
+ * `rules-consent.ts` 的 anchors 头注),把它一并搬走会改动两条车道共用的认领谓词,不在本批射程。
267
+ * 这一句同时也是那次收口的**样板**:形已经在这里了,下一批照抄即可。
268
+ */
269
+ export declare const SQL_DB_NOW_MS: {
270
+ readonly tidb: "CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS SIGNED)";
271
+ readonly pg: "(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
272
+ };
172
273
  /** prepare 时刻候选集的规范摘要(载荷绑定 F1 ④ 的判据)。纯数据 ⇒ `build*`。 */
173
274
  export declare function buildRulePayloadHash(candidates: ReadonlyArray<{
174
275
  rule: string;
@@ -183,6 +284,7 @@ export declare class SqlRuleImportTicketStore {
183
284
  ticketId: string;
184
285
  principal: string;
185
286
  approvalId: string;
287
+ purpose: RuleTicketPurpose;
186
288
  candidates: ReadonlyArray<{
187
289
  rule: string;
188
290
  scope: RuleScope;
@@ -204,7 +306,7 @@ export declare class SqlRuleImportTicketStore {
204
306
  * expired/consumed 四类给服务端日志;wire 面把四类折成同一个 404)。预读说「可以」而 UPDATE 命中零行,
205
307
  * 只可能是我们在这两跳之间输给了一个并发认领者 —— 如实报 `consumed`,而不是把这次失败当成成功。
206
308
  */
207
- consume(ticketId: string, principal: string, opts?: RuleTicketDecisionClock): Promise<RuleTicketRedeemResult>;
309
+ consume(ticketId: string, principal: string, purpose: RuleTicketPurpose, opts?: RuleTicketDecisionClock): Promise<RuleTicketRedeemResult>;
208
310
  /**
209
311
  * **认领的释放**(codex 交叉复审 round1 [high] 一,验真后修)。
210
312
  *
@@ -212,7 +314,7 @@ export declare class SqlRuleImportTicketStore {
212
314
  * 载荷比对、confirm+批量兑付。旧形把认领当终局 ⇒ 一次数据库抖动就让票**永久**烧掉,而调用方拿到的
213
315
  * 是与「票不存在」同形的 404:重试无门、最终状态对客户端不可判。
214
316
  *
215
- * ⇒ 认领之后**只有裁定性拒绝**才留着消费位(载荷被篡改、记录不存在 —— 重试不改判);**不确定/瞬时**
317
+ * ⇒ 认领之后**只有裁定性拒绝**才留着消费位(载荷被改写、记录不存在 —— 重试不改判);**不确定/瞬时**
216
318
  * 的失败(store 抛错、CAS 冲突)把认领**放回去**,让属主重试。放回去不会打开双兑付的门:同一时刻
217
319
  * 只有一个持有者(认领本身仍是单条条件 UPDATE),而兑付腿(`redeemRuleBatch`)按 core 的设计是
218
320
  * **幂等**的(记录里存着每个候选已铸的 dot,重放只会重放同一个 dot)。
@@ -221,7 +323,53 @@ export declare class SqlRuleImportTicketStore {
221
323
  * 期间属主可以重新走 prepare 拿一张新票(导入是幂等的:同一条规则再兑付只是同一个 dot 的重放)。
222
324
  * 要彻底消灭它需要一条带租约到期的认领状态机,属协议改动,列为后续件。
223
325
  */
224
- release(ticketId: string, principal: string, mustRemainValidMs?: number, opts?: RuleTicketDecisionClock): Promise<boolean>;
326
+ release(ticketId: string, principal: string, claimId: string, mustRemainValidMs?: number, opts?: RuleTicketDecisionClock): Promise<boolean>;
327
+ /**
328
+ * #265 A7 —— CLAIMED → **SETTLED**:把这一次兑付的终局写进行里。
329
+ *
330
+ * 🔴 判据全写在 `WHERE` 里(与 `consume` 同一条纪律,**禁 read-then-write**):必须**持有认领**
331
+ * (`consumed_at_ms IS NOT NULL`)且**尚未落定**(`redeemed_outcome IS NULL`)。两个写者同时到达时
332
+ * 恰一条 `affected === 1` —— 输者据此知道"别人已经答过了",去读那份终局回放,而不是把自己的那份
333
+ * 覆盖上去(覆盖 = 同一张票对两个客户端说了两句不同的话)。
334
+ *
335
+ * 🔴 **过期刻意不进 WHERE**(与 `consume`/`release` 相反,理由方向也相反):走已经发生了,规则可能
336
+ * 已经落进店里 —— 因为票在记录的这一刻恰好过期就拒绝记录它,只会把一份**已知的真相**丢掉,把
337
+ * 属主推回"不知道到底成没成"。过期管的是"还能不能**开始**兑付",不是"能不能**记下**已经发生的事"。
338
+ */
339
+ settle(ticketId: string, principal: string, claimId: string, outcome: unknown): Promise<boolean>;
340
+ /**
341
+ * #265 A7 —— 读一张票的三态。**principal 绑定**与四类拒同源:别人的票 / 不存在的票一律 `undefined`
342
+ * (调用方把它折成同一个 404,零存在性 oracle)。
343
+ *
344
+ * `claimAgeMs` 在**这里**算(店的记账钟),不交给调用方:见 {@link RuleTicketSnapshot} 的头注。
345
+ */
346
+ peek(ticketId: string, principal: string, purpose: RuleTicketPurpose): Promise<RuleTicketSnapshot | undefined>;
347
+ /**
348
+ * #265 A7 —— **崩溃窗恢复**:把一次**可证陈旧**的认领抢过来。
349
+ *
350
+ * 病:`consume` 之后、`settle` 之前硬崩,票停在 CLAIMED 且永远不会自己走完。旧形(cc-import)对这
351
+ * 一格没有答案 —— 票随 TTL 消失,人的确认丢掉;而 `release` 帮不上忙,它要求那个死掉的进程**还活着
352
+ * 去调它**。
353
+ *
354
+ * 判据同样全在 `WHERE` 里(恰一赢者):**这一代**认领仍在场 ∧ 未落定 ∧ 认领戳早于 now − minClaimAgeMs。
355
+ * `minClaimAgeMs` 由调用方给(车道的租约常数),而"多久算陈旧"这件事本身**只能**用记账钟判 ——
356
+ * 所以减法在这里做,与 {@link peek} 同源。
357
+ *
358
+ * 🔴 抢过来之后**换一代认领**(新戳 + 新 `claim_id`):租约从此刻起算,于是"抢的人自己也死了"这一形
359
+ * 同样有下一个恢复者;而新代次让被顶替的那一位从此 `release`/`settle` 都动不了这张票(R1-F1)。
360
+ * 重驱本身安全的理由在车道那侧(core 的批走是成员级幂等的)。
361
+ *
362
+ * 🔴 **过期刻意不进 WHERE**(codex 交叉轮 R1-F3,验真后改判)。第一版照 `consume` 抄了
363
+ * `expires_at_ms > now`,而那让"临期崩溃"这一整族**永远救不回来**:租约(分钟级)比 TTL(10 分钟)短,
364
+ * 持有者若在距过期不足一个租约时崩掉,认领**成熟之时票已过期** ⇒ 谁都抢不动,而车道对这一态答的是
365
+ * 「稍后拿同一张票再来」—— 一句必然兑现不了的承诺(本仓 `release` 的保守门正是为了不说这种话)。
366
+ * 更实的伤:那次崩溃可能已经把一半规则写进店了,属主却永远拿不到回执。
367
+ * 改判的语义是**过期只挡「开始」一次兑付,不挡「做完」一次已经开始的**:`consume` 的过期门一个字节
368
+ * 不动(一张从未被认领的过期票照旧是死的,外泄的票不会因此复活),而一张**已被属主认领过**的票代表
369
+ * 一次已经开始的兑付 —— 它的载荷仍被摘要锚定,做完它不多给任何东西。窗仍然有界:行随
370
+ * {@link RULE_IMPORT_TICKET_GC_GRACE_MS} 之外的保留期腿被收走,那就是成文的"回放窗 = 票行存续期"。
371
+ */
372
+ reclaim(ticketId: string, principal: string, purpose: RuleTicketPurpose, minClaimAgeMs: number): Promise<RuleTicketReclaimResult>;
225
373
  private readRow;
226
374
  }
227
375
  /**