@sema-agent/server 7.55.0 → 7.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +3 -3
- package/README.md +12 -6
- package/README.zh-CN.md +9 -5
- package/USAGE.md +108 -52
- package/deploy/sema-up/chart/values.yaml +1 -1
- package/dist/approval-content-kind.d.ts +22 -0
- package/dist/approval-content-kind.js +5 -0
- package/dist/boot/leader.d.ts +21 -0
- package/dist/boot/leader.js +6 -0
- package/dist/boot/runtime-caps.d.ts +2 -1
- package/dist/boot/runtime-caps.js +3 -1
- package/dist/config-center/types.d.ts +2 -1
- package/dist/config-provider.js +1 -0
- package/dist/http/route-ctx.d.ts +15 -3
- package/dist/http/routes/approvals-assistant.js +2 -2
- package/dist/http/routes/leader.js +2 -2
- package/dist/http/routes/workflows.js +1 -1
- package/dist/http/server.js +17 -3
- package/dist/leader/endpoint.js +5 -4
- package/dist/leader/wire.d.ts +50 -0
- package/dist/leader/wire.js +19 -7
- package/dist/main.js +1 -1
- package/dist/plugins/checkpoint-store-sql.d.ts +12 -0
- package/dist/plugins/checkpoint-store-sql.js +4 -1
- package/dist/plugins/file-run-store.js +2 -0
- package/dist/plugins/local-checkpoint-store.js +2 -0
- package/dist/plugins/local-session-store.d.ts +9 -9
- package/dist/plugins/local-session-store.js +5 -3
- package/dist/plugins/memory-run-store.js +2 -0
- package/dist/plugins/permission-rule-store-file.d.ts +22 -4
- package/dist/plugins/permission-rule-store-file.js +17 -8
- package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
- package/dist/plugins/permission-rule-store-sql.js +23 -13
- package/dist/plugins/pg-session-storage.js +17 -13
- package/dist/plugins/run-store-sql.js +6 -4
- package/dist/plugins/store-contracts.d.ts +4 -0
- package/dist/plugins/tidb-session-store.js +18 -14
- package/dist/rules-consent.d.ts +5 -4
- package/dist/rules-consent.js +5 -33
- package/dist/runtime-caps-resolver.js +3 -1
- package/dist/security.d.ts +7 -0
- package/dist/tool-approval.d.ts +12 -60
- package/dist/tool-approval.js +6 -61
- package/package.json +2 -2
- package/skills/find-skills.md +1 -1
- package/skills/loop.md +1 -1
|
@@ -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
|
|
2
|
+
import { type RuleImportTicket, 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 形。
|
|
@@ -29,17 +29,34 @@ export declare class FileRuleApprovalRecordStore implements RuleApprovalRecordSt
|
|
|
29
29
|
/**
|
|
30
30
|
* CC 导入票的 File 形。四条信任边界(principal 绑定 / TTL / 一次性原子消费 / 载荷绑定)与 SQL 形
|
|
31
31
|
* **同语义**;唯一不同的是「原子」由谁保证 —— 那边是引擎行锁,这边是单进程 + 单写者(顶注)。
|
|
32
|
+
*
|
|
33
|
+
* 🔴 **每只店对自己那口钟负责**([ref]):SQL 形的 TTL 轴锚在数据库的钟上(多副本共享的唯一一口);
|
|
34
|
+
* local 形没有那样一口外部钟,它的钟就是本进程的 —— 而进程墙钟**不单调**,一次 NTP 回拨就能让一张
|
|
35
|
+
* 真实已经走满 TTL 的票复活(codex 对抗复审 r1 [high],验真后修)。所以本形自己带一枚**单调锚**:
|
|
36
|
+
* 本进程铸的票,判定钟 = `铸票墙钟 + 自铸票起单调钟走过的量`;重启后重放回来的行没有锚,如实退回墙钟。
|
|
32
37
|
*/
|
|
33
38
|
export declare class FileRuleImportTicketStore {
|
|
34
39
|
private readonly rows;
|
|
35
40
|
private readonly log;
|
|
36
41
|
private readonly gate;
|
|
37
|
-
|
|
42
|
+
/** 单调钟(判定用)。与 {@link now}(墙钟,记账用)是两只不同的钟,别混。 */
|
|
43
|
+
private readonly monotonic;
|
|
44
|
+
constructor(dir: string, now?: () => number, onError?: (message: string) => void, monotonic?: () => number);
|
|
38
45
|
private readonly now;
|
|
39
46
|
/** 取证/自证用:本面是否因内部损坏而 fail-closed。 */
|
|
40
47
|
get corrupt(): boolean;
|
|
41
48
|
/** 归还本面 eager 持有的日志描述符(理由与姊妹面 {@link FileRuleApprovalRecordStore.close} 逐字同源)。 */
|
|
42
49
|
close(): void;
|
|
50
|
+
/**
|
|
51
|
+
* 这一刻的**判定钟**([ref] r1-F1)。
|
|
52
|
+
*
|
|
53
|
+
* 本进程铸的票 ⇒ `铸票墙钟 + 自铸票起单调钟走过的量`:墙钟被前拨不会误杀(真实只过了几秒的票还活着),
|
|
54
|
+
* 被回拨也不会续命(单调钟照走)。重启后重放回来的行没有锚 ⇒ 退回墙钟(单调钟的零点跨进程无意义)。
|
|
55
|
+
*
|
|
56
|
+
* **给不了票额外寿命**:投影严格按单调钟前进,真实走满 TTL 必然越过 `expiresAtMs`(负控钉在
|
|
57
|
+
* `permission-rule-store-file.test.ts` 的 [ref] r1-F1 族)。
|
|
58
|
+
*/
|
|
59
|
+
private decisionNowMs;
|
|
43
60
|
mint(input: {
|
|
44
61
|
ticketId: string;
|
|
45
62
|
principal: string;
|
|
@@ -53,11 +70,11 @@ export declare class FileRuleImportTicketStore {
|
|
|
53
70
|
}): Promise<RuleImportTicket>;
|
|
54
71
|
/** 一次性**认领**。五条否定项的判序与 SQL 形逐字相同(unknown → wrong-principal → wrong-purpose →
|
|
55
72
|
* consumed → expired),于是两形的服务端日志归因可比;wire 面把五类折成同一个 404(零存在性 oracle)。 */
|
|
56
|
-
consume(ticketId: string, principal: string, purpose: RuleTicketPurpose
|
|
73
|
+
consume(ticketId: string, principal: string, purpose: RuleTicketPurpose): Promise<RuleTicketRedeemResult>;
|
|
57
74
|
/** 把认领**放回去**。`mustRemainValidMs` = 放回之后至少还要能用多久 —— 撑不过就不算放回成功
|
|
58
75
|
* (理由逐字见 SQL 侧 `release` 的头注:承诺一次必然兑现不了的重试比不承诺更坏)。
|
|
59
76
|
* `claimId` = A7 的认领围栏(R1-F1):被顶替的旧持有者动不了现持有者的认领。 */
|
|
60
|
-
release(ticketId: string, principal: string, claimId: string, mustRemainValidMs?: number
|
|
77
|
+
release(ticketId: string, principal: string, claimId: string, mustRemainValidMs?: number): Promise<boolean>;
|
|
61
78
|
/** [ref] A7 CLAIMED → **SETTLED**。判据与 SQL 孪生逐字同源(持有**这一代**认领 ∧ 未落定;过期
|
|
62
79
|
* **刻意不设门** —— 理由逐字见 SQL 侧 `settle` 的头注:拒绝记录一件已经发生的事只会把真相丢掉)。 */
|
|
63
80
|
settle(ticketId: string, principal: string, claimId: string, outcome: unknown): Promise<boolean>;
|
|
@@ -89,5 +106,6 @@ export interface FilePermissionRuleStores extends PermissionRuleStoreBundle {
|
|
|
89
106
|
export declare function createFilePermissionRuleStores(root: string, opts?: {
|
|
90
107
|
onError?: (message: string) => void;
|
|
91
108
|
now?: () => number;
|
|
109
|
+
monotonic?: () => number;
|
|
92
110
|
}): FilePermissionRuleStores;
|
|
93
111
|
//# sourceMappingURL=permission-rule-store-file.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { readdirSync } from "node:fs";
|
|
4
|
+
import { performance } from "node:perf_hooks";
|
|
4
5
|
import { FilePermissionRuleStoreProvider, AppendLog, ensureDir, readJsonlRecords, } from "@sema-agent/core";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
import { buildRulePayloadHash } from "./permission-rule-store-sql.js";
|
|
@@ -195,8 +196,10 @@ export class FileRuleImportTicketStore {
|
|
|
195
196
|
rows = new Map();
|
|
196
197
|
log;
|
|
197
198
|
gate;
|
|
198
|
-
|
|
199
|
+
monotonic;
|
|
200
|
+
constructor(dir, now, onError, monotonic) {
|
|
199
201
|
this.now = now ?? Date.now;
|
|
202
|
+
this.monotonic = monotonic ?? (() => performance.now());
|
|
200
203
|
ensureDir(dir);
|
|
201
204
|
const path = join(dir, "import-tickets.jsonl");
|
|
202
205
|
this.gate = new CorruptionGate("permission-rule import-ticket log", path, onError);
|
|
@@ -247,15 +250,21 @@ export class FileRuleImportTicketStore {
|
|
|
247
250
|
close() {
|
|
248
251
|
this.log.close();
|
|
249
252
|
}
|
|
253
|
+
decisionNowMs(row) {
|
|
254
|
+
const a = row.mintAnchor;
|
|
255
|
+
return a === undefined ? this.now() : a.wallMs + (this.monotonic() - a.monoMs);
|
|
256
|
+
}
|
|
250
257
|
async mint(input) {
|
|
251
258
|
this.gate.assertUsable();
|
|
252
|
-
const
|
|
259
|
+
const mintedAtWall = this.now();
|
|
260
|
+
const mintedAtMono = this.monotonic();
|
|
261
|
+
const expiresAtMs = mintedAtWall + input.ttlMs;
|
|
253
262
|
const payloadHash = buildRulePayloadHash(input.candidates);
|
|
254
263
|
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 });
|
|
264
|
+
this.rows.set(input.ticketId, { ownerKey: input.principal, approvalId: input.approvalId, payloadHash, purpose: input.purpose, expiresAtMs, mintAnchor: { wallMs: mintedAtWall, monoMs: mintedAtMono } });
|
|
256
265
|
return { ticketId: input.ticketId, approvalId: input.approvalId, payloadHash, expiresAtMs };
|
|
257
266
|
}
|
|
258
|
-
async consume(ticketId, principal, purpose
|
|
267
|
+
async consume(ticketId, principal, purpose) {
|
|
259
268
|
this.gate.assertUsable();
|
|
260
269
|
const row = this.rows.get(ticketId);
|
|
261
270
|
if (row === undefined)
|
|
@@ -267,7 +276,7 @@ export class FileRuleImportTicketStore {
|
|
|
267
276
|
if (row.consumedAtMs !== undefined)
|
|
268
277
|
return { ok: false, reason: "consumed" };
|
|
269
278
|
const stampMs = this.now();
|
|
270
|
-
if (row.expiresAtMs <= (
|
|
279
|
+
if (row.expiresAtMs <= this.decisionNowMs(row))
|
|
271
280
|
return { ok: false, reason: "expired" };
|
|
272
281
|
const claimId = randomUUID();
|
|
273
282
|
this.log.append({ op: "consume", ticketId, atMs: stampMs, claimId }, true);
|
|
@@ -275,12 +284,12 @@ export class FileRuleImportTicketStore {
|
|
|
275
284
|
row.claimId = claimId;
|
|
276
285
|
return { ok: true, approvalId: row.approvalId, payloadHash: row.payloadHash, claimId };
|
|
277
286
|
}
|
|
278
|
-
async release(ticketId, principal, claimId, mustRemainValidMs = 0
|
|
287
|
+
async release(ticketId, principal, claimId, mustRemainValidMs = 0) {
|
|
279
288
|
this.gate.assertUsable();
|
|
280
289
|
const row = this.rows.get(ticketId);
|
|
281
290
|
if (row === undefined || row.ownerKey !== principal || row.consumedAtMs === undefined || row.claimId !== claimId || row.outcome !== undefined)
|
|
282
291
|
return false;
|
|
283
|
-
if (row.expiresAtMs <=
|
|
292
|
+
if (row.expiresAtMs <= this.decisionNowMs(row) + mustRemainValidMs)
|
|
284
293
|
return false;
|
|
285
294
|
this.log.append({ op: "release", ticketId }, true);
|
|
286
295
|
delete row.consumedAtMs;
|
|
@@ -329,7 +338,7 @@ export function createFilePermissionRuleStores(root, opts) {
|
|
|
329
338
|
ensureDir(dir);
|
|
330
339
|
const provider = opts?.onError ? new FilePermissionRuleStoreProvider(dir, opts.onError) : new FilePermissionRuleStoreProvider(dir);
|
|
331
340
|
const approvals = new FileRuleApprovalRecordStore(dir, opts?.onError);
|
|
332
|
-
const tickets = new FileRuleImportTicketStore(dir, opts?.now, opts?.onError);
|
|
341
|
+
const tickets = new FileRuleImportTicketStore(dir, opts?.now, opts?.onError, opts?.monotonic);
|
|
333
342
|
return {
|
|
334
343
|
provider,
|
|
335
344
|
approvals,
|
|
@@ -195,16 +195,6 @@ export type RuleTicketRedeemResult = {
|
|
|
195
195
|
ok: false;
|
|
196
196
|
reason: "unknown" | "wrong-principal" | "wrong-purpose" | "expired" | "consumed";
|
|
197
197
|
};
|
|
198
|
-
/**
|
|
199
|
-
* [ref]③/[ref]③ —— 票的**判定钟**覆写(记账钟永远不覆写,见 `consume` 内注)。
|
|
200
|
-
*
|
|
201
|
-
* 缺席 = 墙钟,既有行为逐字不变。在场 = 调用方(同意车道)按**单调锚**投影出来的那个时刻:
|
|
202
|
-
* 铸票时刻的墙钟 + 自铸票起单调钟走过的量。它只能让判定更贴近**真实经过的时间**,给不了票额外寿命
|
|
203
|
-
* (投影按单调钟前进,走满 TTL 就必然越过 `expires_at_ms`)。
|
|
204
|
-
*/
|
|
205
|
-
export interface RuleTicketDecisionClock {
|
|
206
|
-
decisionNowMs?: number;
|
|
207
|
-
}
|
|
208
198
|
/**
|
|
209
199
|
* [ref] A7 —— 一张票的**三态快照**(MINTED → CLAIMED → SETTLED)。
|
|
210
200
|
*
|
|
@@ -243,15 +233,18 @@ export type RuleTicketReclaimResult = {
|
|
|
243
233
|
ok: false;
|
|
244
234
|
};
|
|
245
235
|
/**
|
|
246
|
-
* [ref]③ 同族的 **GC 宽限**:`reapExpired`
|
|
236
|
+
* [ref]③ 同族的 **GC 宽限**:`reapExpired` 只收「按**数据库的钟**至少已死一个完整 TTL」的票行。
|
|
247
237
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
238
|
+
* 🔴 立论已换([ref] / [ref]):它原本是给单调锚兜底的 —— 一张「按墙钟已过期、按单调钟还在窗内」的
|
|
239
|
+
* 票当年是可兑付的,GC 不能抢在判定前面把行收走。TTL 轴整条搬到数据库的钟之后,那一族**从根上没有了**
|
|
240
|
+
* (判定与清扫从此同一口钟、同一个不等号,不可能一个说活一个说死)。留着它的现由是另外两条,都成立:
|
|
241
|
+
* · **归因质量**:行还在 ⇒ 拒绝是 `expired`(服务端日志说得清「票过期了」);行被收走 ⇒ 塌成 `unknown`。
|
|
242
|
+
* 死票在库里多躺一个宽限,换来运维排查时一个准确的归因,而票表按设计是短命小表,代价可忽略。
|
|
243
|
+
* · **数据库自己的钟也会被拨**(DB 主机的 NTP 纠正、或一次主从切换换了台机器)。宽限一个 TTL 覆盖住
|
|
244
|
+
* 「DB 钟被往前拨了不到一个 TTL」这一族:那种时刻 `consume` 会暂时拒,但行还在,钟走回来票就活。
|
|
251
245
|
*
|
|
252
|
-
* 残余(如实登记,不假称已解)
|
|
253
|
-
*
|
|
254
|
-
* ——这正是 core 建议的姿态(「接受 + 票廉价重铸」)。
|
|
246
|
+
* 残余(如实登记,不假称已解):DB 钟被往前拨**超过**一个 TTL 时,行仍会被收。补偿是现成的:prepare
|
|
247
|
+
* 廉价,重铸一张即可(core [ref] 建议的姿态)。
|
|
255
248
|
*/
|
|
256
249
|
export declare const RULE_IMPORT_TICKET_GC_GRACE_MS: number;
|
|
257
250
|
/**
|
|
@@ -262,11 +255,17 @@ export declare const RULE_IMPORT_TICKET_GC_GRACE_MS: number;
|
|
|
262
255
|
* 生出来的权威回执下一拍就被删,而"落定之后必有一个完整的回放窗"是本批新许下的承诺 —— 它不能建立在
|
|
263
256
|
* 「两台机器的钟差不多」这条没人声明过的假设上。数据库的钟是这两台副本**唯一**共享的那口钟。
|
|
264
257
|
*
|
|
265
|
-
* 🔴
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* `
|
|
269
|
-
*
|
|
258
|
+
* 🔴 射程收官([ref] / [ref],2026-09-02):**这张票上的每一个时刻量都走本钟**,一个不剩 ——
|
|
259
|
+
* `consumed_at_ms` / `settled_at_ms`(R8-F2 起)、`expires_at_ms` / `created_at_ms`(本批)。
|
|
260
|
+
*
|
|
261
|
+
* 本批补上的是 **TTL 轴**,也是最后一条:铸票 `expires_at_ms = <本钟> + ttl`、判定
|
|
262
|
+
* `expires_at_ms > <本钟>`、清扫 `expires_at_ms <= <本钟> − 宽限`。修前那一形是**两台副本各出一半**的
|
|
263
|
+
* 比较(铸票副本的墙钟定到期时刻,兑付副本的墙钟做判定),于是 TTL 在集群上**不单调**:同一张票在钟慢
|
|
264
|
+
* 的那台上还活着、在钟快的那台上已经死了;极端形是「出生即过期」(铸票副本慢过一个 TTL ⇒ 票铸出来就
|
|
265
|
+
* 没人兑得动)。这不需要故障就能发作 —— 负载均衡把 prepare 与 redeem 分到两台副本是**常态**。
|
|
266
|
+
* 上一批的进程内单调锚只覆盖「同一副本铸、同一副本兑」那一格,连同它的台账在本批一并删除。
|
|
267
|
+
*
|
|
268
|
+
* 判定读不到本钟 ⇒ 语句抛错 ⇒ 调用方按不确定失败处置(fail-closed:绝不退回本地墙钟放行)。
|
|
270
269
|
*/
|
|
271
270
|
export declare const SQL_DB_NOW_MS: {
|
|
272
271
|
readonly tidb: "CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS SIGNED)";
|
|
@@ -277,11 +276,37 @@ export declare function buildRulePayloadHash(candidates: ReadonlyArray<{
|
|
|
277
276
|
rule: string;
|
|
278
277
|
scope: RuleScope;
|
|
279
278
|
}>): string;
|
|
279
|
+
/**
|
|
280
|
+
* 票面三态协议的 SQL 实现。
|
|
281
|
+
*
|
|
282
|
+
* 🔴 **这只店没有本地钟 seam**([ref]):它此前收一个 `now: () => number`,而票上的每一个时刻量现在都由
|
|
283
|
+
* 数据库自己出({@link SQL_DB_NOW_MS})。留着那只形参会让人以为「注一只假钟就能改票的判决」——
|
|
284
|
+
* 那正是本批要消灭的那条依赖。测试要制造过期,改行(把 `expires_at_ms` 相对 `<db now>` 挪)即可,
|
|
285
|
+
* 而那恰恰也是生产上唯一能让票过期的方式。
|
|
286
|
+
*/
|
|
280
287
|
export declare class SqlRuleImportTicketStore {
|
|
281
288
|
private readonly db;
|
|
282
|
-
|
|
283
|
-
constructor(db: SqlDriver, now?: () => number);
|
|
289
|
+
constructor(db: SqlDriver);
|
|
284
290
|
private q;
|
|
291
|
+
/**
|
|
292
|
+
* 问一次**数据库自己的钟**(毫秒)。铸票用它当基准,于是到期时刻与后续每一次判定同域。
|
|
293
|
+
*
|
|
294
|
+
* 读不出数 ⇒ **响亮抛**([ref] fail-closed):静默兜个本地 `Date.now()` 正是本批要删掉的那条腿,
|
|
295
|
+
* 而一次「钟读不出来」在授权面上是运维事件,不是一个可以猜的默认值。
|
|
296
|
+
*/
|
|
297
|
+
private dbNowMs;
|
|
298
|
+
/**
|
|
299
|
+
* 铸一张票。
|
|
300
|
+
*
|
|
301
|
+
* 🔴 到期时刻 = **数据库的钟** + TTL([ref] / [ref]),不是这台副本的墙钟 + TTL。理由逐字见
|
|
302
|
+
* {@link SQL_DB_NOW_MS} 的头注:铸造点与判定点必须同一口钟,否则 TTL 在集群上不单调。
|
|
303
|
+
*
|
|
304
|
+
* 为什么是「先问一次钟、再把绝对值写进去」而不是「INSERT 里直接写 `<db now> + ?`」:`expiresAtMs`
|
|
305
|
+
* 要**原样上 wire**(prepare 的响应体里有它,客户端拿它显示倒计时),而 MySQL 的 INSERT 交不回
|
|
306
|
+
* 计算列 —— 那一形要么再补一条 SELECT 读回来(失败时留一行孤儿),要么两方言各写一套(PG 有
|
|
307
|
+
* `RETURNING`、MySQL 没有,两份会各自漂)。先问钟的形只有一个失败方向:钟没问到 ⇒ 一行都没落,
|
|
308
|
+
* 调用方原样收错。两跳之间流逝的那点时间只会让票**略短命**,方向保守。
|
|
309
|
+
*/
|
|
285
310
|
mint(input: {
|
|
286
311
|
ticketId: string;
|
|
287
312
|
principal: string;
|
|
@@ -308,7 +333,7 @@ export declare class SqlRuleImportTicketStore {
|
|
|
308
333
|
* wrong-purpose/expired/consumed 五类给服务端日志;wire 面把五类折成同一个 404)。预读说「可以」而 UPDATE 命中零行,
|
|
309
334
|
* 只可能是我们在这两跳之间输给了一个并发认领者 —— 如实报 `consumed`,而不是把这次失败当成成功。
|
|
310
335
|
*/
|
|
311
|
-
consume(ticketId: string, principal: string, purpose: RuleTicketPurpose
|
|
336
|
+
consume(ticketId: string, principal: string, purpose: RuleTicketPurpose): Promise<RuleTicketRedeemResult>;
|
|
312
337
|
/**
|
|
313
338
|
* **认领的释放**(codex 交叉复审 round1 [high] 一,验真后修)。
|
|
314
339
|
*
|
|
@@ -325,7 +350,7 @@ export declare class SqlRuleImportTicketStore {
|
|
|
325
350
|
* 期间属主可以重新走 prepare 拿一张新票(导入是幂等的:同一条规则再兑付只是同一个 dot 的重放)。
|
|
326
351
|
* 要彻底消灭它需要一条带租约到期的认领状态机,属协议改动,列为后续件。
|
|
327
352
|
*/
|
|
328
|
-
release(ticketId: string, principal: string, claimId: string, mustRemainValidMs?: number
|
|
353
|
+
release(ticketId: string, principal: string, claimId: string, mustRemainValidMs?: number): Promise<boolean>;
|
|
329
354
|
/**
|
|
330
355
|
* [ref] A7 —— CLAIMED → **SETTLED**:把这一次兑付的终局写进行里。
|
|
331
356
|
*
|
|
@@ -427,11 +452,16 @@ export interface PermissionRuleStores {
|
|
|
427
452
|
* 「超帽当场拒」那一条路,不收「人改主意了」)。
|
|
428
453
|
*
|
|
429
454
|
* 两张表的谓词都只删**可证已死**的行,所以本腿不需要旋钮(没有可调的语义):
|
|
430
|
-
* · 票(三支 OR,A7 交叉复审定形,逐支论证见 reapExpired 实现内注)
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
* · 记录:`state = 'pending' AND created_at_ms <
|
|
455
|
+
* · 票(三支 OR,A7 交叉复审定形,逐支论证见 reapExpired 实现内注)**三支全走 DB 钟**([ref]):
|
|
456
|
+
* 甲=从未认领且过期已满 GC 宽限;乙=已落定且落定后过 GC 宽限(按 `settled_at_ms` 判);
|
|
457
|
+
* 丙=CLAIMED-未结且认领戳老过废弃期(按 `consumed_at_ms` 判);
|
|
458
|
+
* · 记录:`state = 'pending' AND created_at_ms < nowMs − {@link RULE_PENDING_APPROVAL_RETENTION_MS}`,
|
|
434
459
|
* 再加 `NOT EXISTS`(载荷必须陪着把手活,R6-F1:仍有票引用的 pending 记录不删)。
|
|
460
|
+
*
|
|
461
|
+
* 🔴 `nowMs` 形参**只剩记录那一支在用**,如实登记:`permission_rule_approval.created_at_ms` 是这台
|
|
462
|
+
* 副本自己写下的(见 {@link SqlRuleApprovalRecordStore}),按同一台副本的墙钟判"落盘满 24 小时了吗"
|
|
463
|
+
* 是**同域比较** —— 把它单独搬到 DB 钟反而会造出一次新的跨钟比较。那张表的钟域是另一条轴上的事,
|
|
464
|
+
* 不在本批射程(登记为后续件)。票那一支已经不看它了。
|
|
435
465
|
*/
|
|
436
466
|
reapExpired(nowMs: number): Promise<number>;
|
|
437
467
|
}
|
|
@@ -616,16 +616,21 @@ export function buildRulePayloadHash(candidates) {
|
|
|
616
616
|
}
|
|
617
617
|
export class SqlRuleImportTicketStore {
|
|
618
618
|
db;
|
|
619
|
-
|
|
620
|
-
constructor(db, now = Date.now) {
|
|
619
|
+
constructor(db) {
|
|
621
620
|
this.db = db;
|
|
622
|
-
this.now = now;
|
|
623
621
|
}
|
|
624
622
|
q(tidbSql, pgSql) {
|
|
625
623
|
return this.db.dialect === "tidb" ? tidbSql : pgSql;
|
|
626
624
|
}
|
|
625
|
+
async dbNowMs() {
|
|
626
|
+
const { rows } = await this.db.query(this.q(`SELECT ${SQL_DB_NOW_MS.tidb} AS db_now_ms`, `SELECT ${SQL_DB_NOW_MS.pg} AS db_now_ms`), []);
|
|
627
|
+
const n = Number(rows[0]?.["db_now_ms"] ?? Number.NaN);
|
|
628
|
+
if (!Number.isFinite(n))
|
|
629
|
+
throw new Error(`${PERMISSION_RULE_TICKET_TABLE}: the database clock came back unreadable (${String(rows[0]?.["db_now_ms"])})`);
|
|
630
|
+
return n;
|
|
631
|
+
}
|
|
627
632
|
async mint(input) {
|
|
628
|
-
const nowMs = this.
|
|
633
|
+
const nowMs = await this.dbNowMs();
|
|
629
634
|
const expiresAtMs = nowMs + input.ttlMs;
|
|
630
635
|
const payloadHash = buildRulePayloadHash(input.candidates);
|
|
631
636
|
await this.db.query(this.q(`INSERT INTO ${PERMISSION_RULE_TICKET_TABLE} (ticket_id, owner_key, principal, approval_id, payload_hash, payload_json, expires_at_ms, consumed_at_ms, claim_id, purpose, created_at_ms)
|
|
@@ -643,8 +648,7 @@ export class SqlRuleImportTicketStore {
|
|
|
643
648
|
]);
|
|
644
649
|
return { ticketId: input.ticketId, approvalId: input.approvalId, payloadHash, expiresAtMs };
|
|
645
650
|
}
|
|
646
|
-
async consume(ticketId, principal, purpose
|
|
647
|
-
const decisionNowMs = opts?.decisionNowMs ?? this.now();
|
|
651
|
+
async consume(ticketId, principal, purpose) {
|
|
648
652
|
const ownerKey = buildRuleOwnerKey({ kind: "principal", principal });
|
|
649
653
|
const row = await this.readRow(ticketId);
|
|
650
654
|
if (row === undefined)
|
|
@@ -655,17 +659,23 @@ export class SqlRuleImportTicketStore {
|
|
|
655
659
|
return { ok: false, reason: "wrong-purpose" };
|
|
656
660
|
if (row.consumedAtMs !== undefined)
|
|
657
661
|
return { ok: false, reason: "consumed" };
|
|
658
|
-
if (row.expiresAtMs <=
|
|
662
|
+
if (row.expiresAtMs <= row.dbNowMs)
|
|
659
663
|
return { ok: false, reason: "expired" };
|
|
660
664
|
const claimId = randomUUID();
|
|
661
|
-
const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.tidb}, claim_id = ? WHERE ticket_id = ? AND owner_key = ? AND purpose = ? AND consumed_at_ms IS NULL AND expires_at_ms >
|
|
665
|
+
const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.tidb}, claim_id = ? WHERE ticket_id = ? AND owner_key = ? AND purpose = ? AND consumed_at_ms IS NULL AND expires_at_ms > ${SQL_DB_NOW_MS.tidb}`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.pg}, claim_id = $1 WHERE ticket_id = $2 AND owner_key = $3 AND purpose = $4 AND consumed_at_ms IS NULL AND expires_at_ms > ${SQL_DB_NOW_MS.pg}`), [claimId, ticketId, ownerKey, purpose]);
|
|
662
666
|
if (affected === 1)
|
|
663
667
|
return { ok: true, approvalId: row.approvalId, payloadHash: row.payloadHash, claimId };
|
|
668
|
+
const after = await this.readRow(ticketId);
|
|
669
|
+
if (after === undefined)
|
|
670
|
+
return { ok: false, reason: "unknown" };
|
|
671
|
+
if (after.consumedAtMs !== undefined)
|
|
672
|
+
return { ok: false, reason: "consumed" };
|
|
673
|
+
if (after.expiresAtMs <= after.dbNowMs)
|
|
674
|
+
return { ok: false, reason: "expired" };
|
|
664
675
|
return { ok: false, reason: "consumed" };
|
|
665
676
|
}
|
|
666
|
-
async release(ticketId, principal, claimId, mustRemainValidMs = 0
|
|
667
|
-
const
|
|
668
|
-
const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = NULL, claim_id = NULL WHERE ticket_id = ? AND owner_key = ? AND claim_id = ? AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND expires_at_ms > ?`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = NULL, claim_id = NULL WHERE ticket_id = $1 AND owner_key = $2 AND claim_id = $3 AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND expires_at_ms > $4`), [ticketId, buildRuleOwnerKey({ kind: "principal", principal }), claimId, nowMs + mustRemainValidMs]);
|
|
677
|
+
async release(ticketId, principal, claimId, mustRemainValidMs = 0) {
|
|
678
|
+
const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = NULL, claim_id = NULL WHERE ticket_id = ? AND owner_key = ? AND claim_id = ? AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND expires_at_ms > ${SQL_DB_NOW_MS.tidb} + ?`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = NULL, claim_id = NULL WHERE ticket_id = $1 AND owner_key = $2 AND claim_id = $3 AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND expires_at_ms > ${SQL_DB_NOW_MS.pg} + $4`), [ticketId, buildRuleOwnerKey({ kind: "principal", principal }), claimId, mustRemainValidMs]);
|
|
669
679
|
return affected === 1;
|
|
670
680
|
}
|
|
671
681
|
async settle(ticketId, principal, claimId, outcome) {
|
|
@@ -730,9 +740,9 @@ export function createSqlPermissionRuleStores(db, now) {
|
|
|
730
740
|
return {
|
|
731
741
|
provider: new SqlPermissionRuleStoreProvider(db, now),
|
|
732
742
|
approvals: new SqlRuleApprovalRecordStore(db, now),
|
|
733
|
-
tickets: new SqlRuleImportTicketStore(db
|
|
743
|
+
tickets: new SqlRuleImportTicketStore(db),
|
|
734
744
|
reapExpired: async (nowMs) => {
|
|
735
|
-
const deadTickets = await db.query(q(`DELETE FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE (consumed_at_ms IS NULL AND expires_at_ms <=
|
|
745
|
+
const deadTickets = await db.query(q(`DELETE FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE (consumed_at_ms IS NULL AND expires_at_ms <= ${SQL_DB_NOW_MS.tidb} - ${RULE_IMPORT_TICKET_GC_GRACE_MS}) OR (redeemed_outcome IS NOT NULL AND settled_at_ms IS NOT NULL AND settled_at_ms <= ${SQL_DB_NOW_MS.tidb} - ${RULE_IMPORT_TICKET_GC_GRACE_MS}) OR (consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND consumed_at_ms < ${SQL_DB_NOW_MS.tidb} - ${RULE_PENDING_APPROVAL_RETENTION_MS}) LIMIT ${RULE_REAP_BATCH}`, `DELETE FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ctid IN (SELECT ctid FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE (consumed_at_ms IS NULL AND expires_at_ms <= ${SQL_DB_NOW_MS.pg} - ${RULE_IMPORT_TICKET_GC_GRACE_MS}) OR (redeemed_outcome IS NOT NULL AND settled_at_ms IS NOT NULL AND settled_at_ms <= ${SQL_DB_NOW_MS.pg} - ${RULE_IMPORT_TICKET_GC_GRACE_MS}) OR (consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND consumed_at_ms < ${SQL_DB_NOW_MS.pg} - ${RULE_PENDING_APPROVAL_RETENTION_MS}) LIMIT ${RULE_REAP_BATCH})`), []);
|
|
736
746
|
const orphanPending = await db.query(q(`DELETE FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE state = 'pending' AND created_at_ms < ? AND NOT EXISTS (SELECT 1 FROM ${PERMISSION_RULE_TICKET_TABLE} t WHERE t.approval_id = ${PERMISSION_RULE_APPROVAL_TABLE}.record_id) LIMIT ${RULE_REAP_BATCH}`, `DELETE FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE ctid IN (SELECT a.ctid FROM ${PERMISSION_RULE_APPROVAL_TABLE} a WHERE a.state = 'pending' AND a.created_at_ms < $1 AND NOT EXISTS (SELECT 1 FROM ${PERMISSION_RULE_TICKET_TABLE} t WHERE t.approval_id = a.record_id) LIMIT ${RULE_REAP_BATCH})`), [nowMs - RULE_PENDING_APPROVAL_RETENTION_MS]);
|
|
737
747
|
return deadTickets.affected + orphanPending.affected;
|
|
738
748
|
},
|
|
@@ -310,25 +310,29 @@ export class PgSessionStore {
|
|
|
310
310
|
where.push(`(sm.updated_at < ${a} OR (sm.updated_at = ${b} AND sm.session_id < ${sid}))`);
|
|
311
311
|
}
|
|
312
312
|
const sql = "SELECT sm.session_id, sm.owner, sm.updated_at AS last_activity, sm.created_at AS first_activity, sm.title, " +
|
|
313
|
-
" COALESCE(r.run_count, 0) AS run_count, latest.objective_preview, latest.status, latest.task_id AS last_run_id " +
|
|
313
|
+
" COALESCE(r.run_count, 0) AS run_count, latest.objective_preview, latest.status, latest.task_id AS last_run_id, latest.last_task_run_id " +
|
|
314
314
|
"FROM session_meta sm " +
|
|
315
315
|
"LEFT JOIN (SELECT session_id, COUNT(*) AS run_count FROM task_run GROUP BY session_id) r ON r.session_id = sm.session_id " +
|
|
316
|
-
"LEFT JOIN (SELECT DISTINCT ON (session_id) session_id, objective_preview, status, task_id FROM task_run " +
|
|
316
|
+
"LEFT JOIN (SELECT DISTINCT ON (session_id) session_id, objective_preview, status, task_id, result ->> 'runId' AS last_task_run_id FROM task_run " +
|
|
317
317
|
" ORDER BY session_id, created_at DESC, task_id DESC) latest ON latest.session_id = sm.session_id " +
|
|
318
318
|
(where.length ? "WHERE " + where.join(" AND ") + " " : "") +
|
|
319
319
|
`ORDER BY sm.updated_at DESC, sm.session_id DESC LIMIT ${ph(opts.limit)}`;
|
|
320
320
|
const res = await this.pool.query(sql, params);
|
|
321
|
-
return res.rows.map((r) =>
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
321
|
+
return res.rows.map((r) => {
|
|
322
|
+
const runId = r.last_task_run_id;
|
|
323
|
+
return {
|
|
324
|
+
sessionId: String(r.session_id),
|
|
325
|
+
owner: r.owner ?? null,
|
|
326
|
+
lastActivityAt: r.last_activity instanceof Date ? r.last_activity.toISOString() : String(r.last_activity),
|
|
327
|
+
firstActivityAt: r.first_activity instanceof Date ? r.first_activity.toISOString() : String(r.first_activity),
|
|
328
|
+
runCount: Number(r.run_count),
|
|
329
|
+
objectivePreview: r.objective_preview ?? null,
|
|
330
|
+
lastStatus: r.status == null ? "" : String(r.status),
|
|
331
|
+
lastRunId: r.last_run_id ?? null,
|
|
332
|
+
...(typeof runId === "string" && runId !== "" ? { lastTaskRunId: runId } : {}),
|
|
333
|
+
title: r.title ?? null,
|
|
334
|
+
};
|
|
335
|
+
});
|
|
332
336
|
}
|
|
333
337
|
async probeTitle(sessionId) {
|
|
334
338
|
const res = await this.pool.query("SELECT title FROM session_meta WHERE session_id = $1", [sessionId]);
|
|
@@ -237,8 +237,8 @@ export class SqlRunStore {
|
|
|
237
237
|
outer.push("(last_activity < ? OR (last_activity = ? AND session_id < ?))");
|
|
238
238
|
params.push(new Date(opts.cursor.lastActivityAt), new Date(opts.cursor.lastActivityAt), opts.cursor.sessionId);
|
|
239
239
|
}
|
|
240
|
-
const sql = "SELECT session_id, owner, task_id, last_activity, first_activity, run_count, objective_preview, status FROM (" +
|
|
241
|
-
"SELECT session_id, owner, task_id, objective_preview, status," +
|
|
240
|
+
const sql = "SELECT session_id, owner, task_id, last_activity, first_activity, run_count, objective_preview, status, last_task_run_id FROM (" +
|
|
241
|
+
"SELECT session_id, owner, task_id, objective_preview, status, JSON_UNQUOTE(JSON_EXTRACT(result, '$.runId')) AS last_task_run_id," +
|
|
242
242
|
" ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY created_at DESC, task_id DESC) AS rn," +
|
|
243
243
|
" MAX(created_at) OVER (PARTITION BY session_id) AS last_activity," +
|
|
244
244
|
" MIN(created_at) OVER (PARTITION BY session_id) AS first_activity," +
|
|
@@ -264,8 +264,8 @@ export class SqlRunStore {
|
|
|
264
264
|
const sid = p(opts.cursor.sessionId);
|
|
265
265
|
outer.push(`(last_activity < ${a} OR (last_activity = ${b} AND session_id < ${sid}))`);
|
|
266
266
|
}
|
|
267
|
-
const sql = "SELECT session_id, owner, task_id, last_activity, first_activity, run_count, objective_preview, status FROM (" +
|
|
268
|
-
"SELECT session_id, owner, task_id, objective_preview, status," +
|
|
267
|
+
const sql = "SELECT session_id, owner, task_id, last_activity, first_activity, run_count, objective_preview, status, last_task_run_id FROM (" +
|
|
268
|
+
"SELECT session_id, owner, task_id, objective_preview, status, result ->> 'runId' AS last_task_run_id," +
|
|
269
269
|
" ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY created_at DESC, task_id DESC) AS rn," +
|
|
270
270
|
" MAX(created_at) OVER (PARTITION BY session_id) AS last_activity," +
|
|
271
271
|
" MIN(created_at) OVER (PARTITION BY session_id) AS first_activity," +
|
|
@@ -276,6 +276,7 @@ export class SqlRunStore {
|
|
|
276
276
|
return rows.map((r) => this.rowToSession(r));
|
|
277
277
|
}
|
|
278
278
|
rowToSession(r) {
|
|
279
|
+
const lastTaskRunId = r.last_task_run_id;
|
|
279
280
|
return {
|
|
280
281
|
sessionId: String(r.session_id),
|
|
281
282
|
owner: r.owner ?? null,
|
|
@@ -285,6 +286,7 @@ export class SqlRunStore {
|
|
|
285
286
|
objectivePreview: r.objective_preview ?? null,
|
|
286
287
|
lastStatus: String(r.status),
|
|
287
288
|
lastRunId: r.task_id ?? null,
|
|
289
|
+
...(typeof lastTaskRunId === "string" && lastTaskRunId !== "" ? { lastTaskRunId } : {}),
|
|
288
290
|
title: null,
|
|
289
291
|
};
|
|
290
292
|
}
|
|
@@ -94,6 +94,10 @@ export interface SessionSummary {
|
|
|
94
94
|
/** K-5c (shell §K): the LATEST run's task_id — the anchor the shell uses to re-attach the live event tail in ONE
|
|
95
95
|
* hop (`GET /v1/runs/:lastRunId/events`) when resuming a picked session, instead of a second lookup. */
|
|
96
96
|
lastRunId: string | null;
|
|
97
|
+
/** [ref]([ref]/[ref]):latest 终局 run 的 engine runId(`TaskResult.runId`,latest.result JSON 提取的
|
|
98
|
+
* 零迁移路线 —— result 终局才写 ⇒ 在跑 run 键诚实缺席);OMIT-when-absent,恒不铸 null。轴辨析与
|
|
99
|
+
* wire 半场见 `security.ts SessionListItem.lastTaskRunId`(同名同义)。 */
|
|
100
|
+
lastTaskRunId?: string;
|
|
97
101
|
/** The auto-generated session title (cheap-model one-liner, write-once). null = not (yet)
|
|
98
102
|
* titled OR the lister face has no session_meta (the legacy task_run aggregate / memory twins — honest degrade). */
|
|
99
103
|
title: string | null;
|
|
@@ -144,28 +144,32 @@ export class TiDBSessionStore {
|
|
|
144
144
|
params.push(new Date(opts.cursor.lastActivityAt), new Date(opts.cursor.lastActivityAt), opts.cursor.sessionId);
|
|
145
145
|
}
|
|
146
146
|
const sql = "SELECT sm.session_id, sm.owner, sm.updated_at AS last_activity, sm.created_at AS first_activity, sm.title, " +
|
|
147
|
-
" COALESCE(r.run_count, 0) AS run_count, latest.objective_preview, latest.status, latest.task_id AS last_run_id " +
|
|
147
|
+
" COALESCE(r.run_count, 0) AS run_count, latest.objective_preview, latest.status, latest.task_id AS last_run_id, latest.last_task_run_id " +
|
|
148
148
|
"FROM session_meta sm " +
|
|
149
149
|
"LEFT JOIN (SELECT session_id, COUNT(*) AS run_count FROM task_run GROUP BY session_id) r ON r.session_id = sm.session_id " +
|
|
150
|
-
"LEFT JOIN (SELECT session_id, objective_preview, status, task_id FROM (" +
|
|
151
|
-
" SELECT session_id, objective_preview, status, task_id, " +
|
|
150
|
+
"LEFT JOIN (SELECT session_id, objective_preview, status, task_id, last_task_run_id FROM (" +
|
|
151
|
+
" SELECT session_id, objective_preview, status, task_id, JSON_UNQUOTE(JSON_EXTRACT(result, '$.runId')) AS last_task_run_id, " +
|
|
152
152
|
" ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY created_at DESC, task_id DESC) AS rn FROM task_run" +
|
|
153
153
|
" ) lr WHERE rn = 1) latest ON latest.session_id = sm.session_id " +
|
|
154
154
|
(where.length ? "WHERE " + where.join(" AND ") + " " : "") +
|
|
155
155
|
"ORDER BY sm.updated_at DESC, sm.session_id DESC LIMIT ?";
|
|
156
156
|
params.push(opts.limit);
|
|
157
157
|
const [rows] = await this.pool.query(sql, params);
|
|
158
|
-
return rows.map((r) =>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
158
|
+
return rows.map((r) => {
|
|
159
|
+
const runId = r.last_task_run_id;
|
|
160
|
+
return {
|
|
161
|
+
sessionId: String(r.session_id),
|
|
162
|
+
owner: r.owner ?? null,
|
|
163
|
+
lastActivityAt: r.last_activity instanceof Date ? r.last_activity.toISOString() : String(r.last_activity),
|
|
164
|
+
firstActivityAt: r.first_activity instanceof Date ? r.first_activity.toISOString() : String(r.first_activity),
|
|
165
|
+
runCount: Number(r.run_count),
|
|
166
|
+
objectivePreview: r.objective_preview ?? null,
|
|
167
|
+
lastStatus: r.status == null ? "" : String(r.status),
|
|
168
|
+
lastRunId: r.last_run_id ?? null,
|
|
169
|
+
...(typeof runId === "string" && runId !== "" ? { lastTaskRunId: runId } : {}),
|
|
170
|
+
title: r.title ?? null,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
169
173
|
}
|
|
170
174
|
async probeTitle(sessionId) {
|
|
171
175
|
const [rows] = await this.pool.query("SELECT title FROM session_meta WHERE session_id = ?", [sessionId]);
|
package/dist/rules-consent.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type PersistedAllowRule, type RemoveResult, type EditedRuleTextPrecheck, type RuleScope, type RuleOffer } from "@sema-agent/core";
|
|
2
2
|
import type { PermissionRuleStoreProvider, RuleApprovalRecordStore } from "@sema-agent/core";
|
|
3
|
-
import { type RuleImportTicket, type
|
|
3
|
+
import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./plugins/permission-rule-store-sql.js";
|
|
4
4
|
/**
|
|
5
5
|
* 本车道**真正**依赖的三面(**结构**接口,不是 SQL 类)。
|
|
6
6
|
*
|
|
@@ -31,12 +31,13 @@ export interface RuleConsentStores {
|
|
|
31
31
|
}): Promise<RuleImportTicket>;
|
|
32
32
|
/** 一次**认领**(不是「已完成」)。语义与「认领之后怎么办」见 {@link RuleConsentStores.tickets.release}。
|
|
33
33
|
* `purpose` = 车道绑定(A7 R1-F2):一条车道的口只认它自己铸的票,不符与「票不存在」同形。
|
|
34
|
-
*
|
|
35
|
-
|
|
34
|
+
* 🔴 **不收任何时刻量**([ref]):过期判定是店自己的事,由店那口钟(SQL=数据库的钟 / local=本进程的
|
|
35
|
+
* 墙钟)在**语句执行的那一刻**答。车道递一个时刻进来,就等于让副本的钟参与授权判决。 */
|
|
36
|
+
consume(ticketId: string, principal: string, purpose: RuleTicketPurpose): Promise<RuleTicketRedeemResult>;
|
|
36
37
|
/** 把认领**放回去**(认领之后遇到不确定/瞬时失败时)。`claimId` = 本次认领的代次(A7 R1-F1 围栏:
|
|
37
38
|
* 被顶替的旧持有者动不了现持有者的认领);`mustRemainValidMs` = 放回之后这张票至少还要能用多久
|
|
38
39
|
* —— 撑不过就**不算放回成功**(理由逐字见 store 侧 `release` 的头注)。 */
|
|
39
|
-
release(ticketId: string, principal: string, claimId: string, mustRemainValidMs?: number
|
|
40
|
+
release(ticketId: string, principal: string, claimId: string, mustRemainValidMs?: number): Promise<boolean>;
|
|
40
41
|
/**
|
|
41
42
|
* [ref] A7 三态协议的三条口(CLAIMED → SETTLED / 三态快照 / 崩溃窗抢认领)。
|
|
42
43
|
*
|