@sema-agent/server 7.1.0 → 7.3.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/README.md +3 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +6 -1
- package/dist/approval-ask-machine.d.ts +39 -0
- package/dist/approval-ask-machine.js +101 -0
- package/dist/approval-card.d.ts +244 -0
- package/dist/approval-card.js +237 -0
- package/dist/approval-deny-reasons.d.ts +56 -0
- package/dist/approval-deny-reasons.js +54 -0
- package/dist/approval-reconciler.d.ts +167 -0
- package/dist/approval-reconciler.js +307 -0
- package/dist/boot/coordinators.d.ts +1 -0
- package/dist/boot/coordinators.js +36 -3
- package/dist/boot/lexical-path-env.d.ts +14 -0
- package/dist/boot/lexical-path-env.js +116 -0
- package/dist/boot/org-memory.d.ts +8 -5
- package/dist/boot/org-memory.js +23 -12
- package/dist/boot/reapers.d.ts +34 -0
- package/dist/boot/reapers.js +198 -23
- package/dist/boot/resolve-spec.d.ts +16 -1
- package/dist/boot/resolve-spec.js +104 -46
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/http-client.d.ts +19 -0
- package/dist/config-center/http-client.js +81 -32
- package/dist/config-types.d.ts +76 -7
- package/dist/config.d.ts +1 -0
- package/dist/config.js +134 -4
- package/dist/elicitation.d.ts +4 -0
- package/dist/elicitation.js +2 -2
- package/dist/http/routes/capabilities.js +18 -2
- package/dist/http/routes/runs.d.ts +1 -0
- package/dist/http/routes/runs.js +548 -16
- package/dist/http/routes/tasks.js +190 -12
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +124 -8
- package/dist/http/sse-log.d.ts +51 -0
- package/dist/http/sse-log.js +64 -0
- package/dist/http/wire-types.d.ts +20 -5
- package/dist/leader/wire.js +4 -0
- package/dist/main.js +7 -4
- package/dist/org-memory-admission.d.ts +2 -1
- package/dist/org-memory-admission.js +10 -4
- package/dist/parked-decide.js +5 -2
- package/dist/plugins/approval-ask-store-memory.d.ts +38 -0
- package/dist/plugins/approval-ask-store-memory.js +299 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +341 -0
- package/dist/plugins/approval-ask-store-sql.js +705 -0
- package/dist/plugins/background-agent-store-sql.js +20 -1
- package/dist/plugins/checkpoint-store-sql.d.ts +84 -9
- package/dist/plugins/checkpoint-store-sql.js +297 -16
- package/dist/plugins/local-checkpoint-store.d.ts +6 -5
- package/dist/plugins/local-checkpoint-store.js +4 -0
- package/dist/plugins/pg-pool.js +11 -0
- package/dist/plugins/store-backend.d.ts +18 -0
- package/dist/plugins/store-backend.js +10 -0
- package/dist/plugins/tidb-pool.js +27 -4
- package/dist/question.d.ts +18 -3
- package/dist/question.js +20 -6
- package/dist/runs.d.ts +16 -1
- package/dist/runs.js +61 -3
- package/dist/runtime-caps-resolver.d.ts +7 -1
- package/dist/runtime-caps-resolver.js +65 -3
- package/dist/runtime-governance.d.ts +11 -4
- package/dist/runtime-governance.js +16 -5
- package/dist/spec-fields.d.ts +4 -0
- package/dist/spec-fields.js +6 -0
- package/dist/task-settings.d.ts +35 -15
- package/dist/task-settings.js +19 -5
- package/dist/tool-approval.d.ts +296 -3
- package/dist/tool-approval.js +1066 -50
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/ledger-sink.js +14 -1
- package/dist/trace/project.d.ts +55 -0
- package/dist/trace/project.js +135 -0
- package/package.json +4 -3
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 流内审批协议(design/172 v3.1 §3.0/§3.1/§3.2)持久层 —— #151 车1,`ApprovalAskStore` 双方言实现
|
|
3
|
+
* (`TiDBApprovalAskStore` / `PgApprovalAskStore`,SINGLE-FILE DUAL-DIALECT,design/158 A12 形——照
|
|
4
|
+
* checkpoint-store-sql.ts:一个 `SqlApprovalAskStore` 类接 `SqlDriver`,两个薄 ctor 子类落方言绑定)。
|
|
5
|
+
*
|
|
6
|
+
* 两张表:`approval_asks`(每个流内工具调用一行,ask 级 6 态状态机)+ `approval_batches`(同一决策点的
|
|
7
|
+
* 兄弟 ask 共享一行,批级 4 态状态机,承载「N 个候选里最终只有一个中选走 gate」的 bind-once 语义)。
|
|
8
|
+
* 状态机真源 = `../approval-ask-machine.js`(纯函数,零 IO)。
|
|
9
|
+
*
|
|
10
|
+
* ── CAS 铁则(design 定稿 §4,全店统一,写在这里防遗忘)────────────────────────────────────────────
|
|
11
|
+
* 1. 一切转移带 `WHERE state = <from>`,赢 = affected 恰为 1。禁读-改-写两步(TOCTOU 窗口)。
|
|
12
|
+
* 2. 🔴 MySQL/TiDB `affectedRows` 坑:同值 UPDATE(SET 的新值与当前列值逐字节相同)affectedRows=0——
|
|
13
|
+
* 这不是「行不存在」,是「驱动认为没有变化」。**所有** CAS UPDATE 因此必须带一个恒变列
|
|
14
|
+
* (`version = version + 1`),这样只要「行在且谓词命中」就必然 affected=1,不会被这个驱动怪癖
|
|
15
|
+
* 误判成「CAS 输了」。本文件每一条 CAS UPDATE 都带 `version = version + 1`。
|
|
16
|
+
* 3. 事务:走 `SqlDriver.connect()` → `conn.begin()` → 一串 `conn.query()` → `conn.commit()`,失败
|
|
17
|
+
* `conn.rollback()`(形照 image-bake-store-sql.ts 的 try/catch/finally 骨架)。本店没有 image-bake
|
|
18
|
+
* 那种「NOT EXISTS 快照读」问题(每条 CAS 都是对已存在主键行的 `UPDATE … WHERE state=?`,MySQL/TiDB
|
|
19
|
+
* 对被更新行本身恒是 current read,不受 REPEATABLE READ 快照影响),所以走普通 `begin()`,不需要
|
|
20
|
+
* `beginPessimistic()`。
|
|
21
|
+
*
|
|
22
|
+
* ── 方言差异(显式写在每个调用点,design/158 A12 doctrine)──────────────────────────────────────────
|
|
23
|
+
* - `?` 占位符(位置序)vs `$n`(显式编号)——动态列清单(`transitionAsk`/`resolveProvisional`,列集合
|
|
24
|
+
* 依 patch 内容变化)靠 `ph(dialect, n)` 生成两种占位符文本,与 background-agent-store-sql.ts 的
|
|
25
|
+
* `updateIf` guard 占位符构造同精神(那里也是「列集合动态、占位符文本仍显式按方言生成」)。
|
|
26
|
+
* - `INSERT IGNORE` vs `ON CONFLICT … DO NOTHING`——ensureAsk 的幂等 upsert。
|
|
27
|
+
* - 撤卡名单回读:PG 用 `UPDATE … RETURNING ask_id` 单语句拿到名单;TiDB 无 RETURNING,同事务内
|
|
28
|
+
* 先 `SELECT` 后 `UPDATE`(设计定稿 §4 原话)。
|
|
29
|
+
* - JSON 列(`card_json`/`decision_actor`)走 `dialectJsonEncoder`(TiDB verbatim `JSON.stringify`;
|
|
30
|
+
* PG 走 `pgSafeJsonStringify` 深度 sanitize——这两列是「已帽已洗后的投影」内容面 blob,不是像
|
|
31
|
+
* checkpoint 的 `tool_input` 那样跨信任边界做 hash 绑定的执行面字节,故用有损而非无损信封)。
|
|
32
|
+
*
|
|
33
|
+
* ── 施工偏离记录(设计定稿字面矛盾,未擅自改设计,如实记在这里 + 车1 最终汇报)──────────────────────
|
|
34
|
+
* - deriveAskId/deriveBatchId 的分隔符:设计定稿 §5 代码片段字面写 `.join(' ')`(单空格),但同一段
|
|
35
|
+
* prose 明确说「分隔用 NUL 防注入拼接歧义」,且 DDL 注释(§3 ask_id 行)又写「用 ':' join」——三处
|
|
36
|
+
* 互相矛盾。本实现(approval-ask-machine.ts)取 prose 的安全论证(NUL,唯一在这些不透明 id 部件里
|
|
37
|
+
* 不会自然出现的分隔字节)为准,未取字面的空格/冒号。
|
|
38
|
+
*/
|
|
39
|
+
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
40
|
+
import type { Pool as PgPool } from "pg";
|
|
41
|
+
import type { PgQueryFn } from "./pg-query.js";
|
|
42
|
+
import { type SqlDriver } from "./sql-driver.js";
|
|
43
|
+
import { type AskState, type BatchState } from "../approval-ask-machine.js";
|
|
44
|
+
export type AskDecision = "approve" | "deny";
|
|
45
|
+
export interface AskRow {
|
|
46
|
+
askId: string;
|
|
47
|
+
taskId: string;
|
|
48
|
+
sourceTaskId: string;
|
|
49
|
+
sessionId: string;
|
|
50
|
+
/** 租户 scope——哨兵归一在消费层(车2+),店里存原值(设计定稿 §3)。 */
|
|
51
|
+
owner: string | null;
|
|
52
|
+
batchId: string;
|
|
53
|
+
toolCallId: string;
|
|
54
|
+
/** 车3 §3.2′ 换轴:数值 `leg` → `legKey`(= `sha256(resume token)` 的 hex,首腿空串)。语义与派生
|
|
55
|
+
* 论证的唯一真源 = `approval-ask-machine.ts` 的 `deriveAskId` 头注,这里不复述。 */
|
|
56
|
+
legKey: string;
|
|
57
|
+
parentToolCallId: string | null;
|
|
58
|
+
/**
|
|
59
|
+
* 🔴 车5 §9 C2:铸卡时呈给人看的那份 input 的**服务端摘要**(`AskRequest.boundInputHash`,core 侧已在场)。
|
|
60
|
+
* 与下面 PARKED 坐标里的 `gateBoundInputHash` 是**两件不同的东西**,别混:
|
|
61
|
+
* - `boundInputHash`(本列)= ask **铸行时**的入参摘要,一次写定永不改;对账收敛器判据 1 用它跟
|
|
62
|
+
* checkpoint 行的同名列做**硬相等**(identity 四元组之外的第二道等式)。缺席 ⇒ 判据 1 不命中
|
|
63
|
+
* (禁「能取到时才比」的可选谓词——同 session 内 toolCallId 会被网关重用,只靠 identity 会把旧 ask
|
|
64
|
+
* PARK 到别人的 resume 坐标上,而 PARKED 是不可回滚的终态)。
|
|
65
|
+
* - `gateBoundInputHash`(下面)= 真 **PARK 成功那一刻**从 checkpoint 抄回来的坐标之一,bindBatch 才写。
|
|
66
|
+
* 本车只落店面承载(列 + 行形 + 读写),铸行调用点的供值归车2/3b。
|
|
67
|
+
*/
|
|
68
|
+
boundInputHash: string | null;
|
|
69
|
+
state: AskState;
|
|
70
|
+
/** §3.0 对账三约束②:超时不得发布终态 denial;provisional 终态可被 `resolveProvisional` 版本化收敛。 */
|
|
71
|
+
provisional: boolean;
|
|
72
|
+
version: number;
|
|
73
|
+
decision: AskDecision | null;
|
|
74
|
+
/** 171 ActorAssertion JSON(车4 才消费,列先在——JSON.parse 后的裸值,类型未知)。 */
|
|
75
|
+
decisionActor: unknown | null;
|
|
76
|
+
decisionNote: string | null;
|
|
77
|
+
decidedAtMs: number | null;
|
|
78
|
+
deniedReason: string | null;
|
|
79
|
+
/** PARKED 坐标三件。 */
|
|
80
|
+
gateToken: string | null;
|
|
81
|
+
gateBoundCallId: string | null;
|
|
82
|
+
gateBoundInputHash: string | null;
|
|
83
|
+
/**
|
|
84
|
+
* 🔴 车4 §12-E:**回决专用**幂等键(响应四则①),`decideAsk` 赢 CAS 时落列 —— **不是** ask 创建键。
|
|
85
|
+
* (车1 原注把它写成 ensureAsk 时铸的创建键,那是错的:现网 `ensureAsk` 调用点恒不传,列恒 NULL,
|
|
86
|
+
* 而端点的重试回放要认的是「同一次回决的重试」。双写者收口 ⇒ `NewAskRow` 的同名字段已摘除。)
|
|
87
|
+
* UNIQUE `(task_id, idempotency_key)`(NULL 可重复,双库皆然)——**带 task 维**是租户/存在性门:
|
|
88
|
+
* 全局唯一会让 A 租户用别人的 key 撞库探测到「这个 key 在别处存在」,也会让两 task 的正常键互撞。
|
|
89
|
+
*/
|
|
90
|
+
idempotencyKey: string | null;
|
|
91
|
+
/** 重放腿的帧载荷(args 已帽已洗后的投影)。 */
|
|
92
|
+
cardJson: unknown;
|
|
93
|
+
schemaVersion: number;
|
|
94
|
+
/** 一次铸定永不重算。 */
|
|
95
|
+
expiresAtMs: number;
|
|
96
|
+
createdAtMs: number;
|
|
97
|
+
updatedAtMs: number;
|
|
98
|
+
}
|
|
99
|
+
/** `ensureAsk` 的入参——初始态恒 STREAM_PENDING/provisional=false/version=0,故不在此列。 */
|
|
100
|
+
export interface NewAskRow {
|
|
101
|
+
askId: string;
|
|
102
|
+
taskId: string;
|
|
103
|
+
sourceTaskId: string;
|
|
104
|
+
sessionId: string;
|
|
105
|
+
owner?: string | null;
|
|
106
|
+
batchId: string;
|
|
107
|
+
toolCallId: string;
|
|
108
|
+
legKey: string;
|
|
109
|
+
parentToolCallId?: string | null;
|
|
110
|
+
/** 车5 §9 C2 的对账 join 键(语义见 `AskRow.boundInputHash`)。缺席 ⇒ 该行结构上永不满足判据 1。 */
|
|
111
|
+
boundInputHash?: string | null;
|
|
112
|
+
cardJson: unknown;
|
|
113
|
+
schemaVersion: number;
|
|
114
|
+
expiresAtMs: number;
|
|
115
|
+
createdAtMs: number;
|
|
116
|
+
}
|
|
117
|
+
/** `transitionAsk`/`resolveProvisional` 的可选补丁——只有出现的字段才落 SQL(未出现 = 该列不变),
|
|
118
|
+
* `updatedAtMs` 恒必填(调用方是未来的协调器,时间戳由它按事件时钟决定,店不偷偷用 `Date.now()`)。 */
|
|
119
|
+
export interface AskTransitionPatch {
|
|
120
|
+
decision?: AskDecision | null;
|
|
121
|
+
decisionActor?: unknown | null;
|
|
122
|
+
decisionNote?: string | null;
|
|
123
|
+
decidedAtMs?: number | null;
|
|
124
|
+
deniedReason?: string | null;
|
|
125
|
+
gateToken?: string | null;
|
|
126
|
+
gateBoundCallId?: string | null;
|
|
127
|
+
gateBoundInputHash?: string | null;
|
|
128
|
+
provisional?: boolean;
|
|
129
|
+
updatedAtMs: number;
|
|
130
|
+
}
|
|
131
|
+
export interface DecideAskInput {
|
|
132
|
+
decision: AskDecision;
|
|
133
|
+
decisionActor?: unknown;
|
|
134
|
+
decisionNote?: string | null;
|
|
135
|
+
/** 车4 §12-E:回决幂等键,赢 CAS 时随决议一起落列(见 `AskRow.idempotencyKey`)。缺席 ⇒ 列保持 NULL。 */
|
|
136
|
+
idempotencyKey?: string | null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* `idempotency_conflict`(车4 §12-E):同一 (task_id, idempotency_key) 已被**另一条** ask 的回决占用。
|
|
140
|
+
* 🔴 方言错误的判别**收在店内**(TiDB `ER_DUP_ENTRY`/errno 1062 与 PG SQLSTATE 23505),端点只认这个
|
|
141
|
+
* typed 臂 —— 否则每个消费层都得自己认两套驱动的错误对象形状,方言就漏出了持久层。
|
|
142
|
+
* `row` = 判定时刻读到的本 ask 行(可能为 null:并发删)。
|
|
143
|
+
*/
|
|
144
|
+
export type DecideResult = {
|
|
145
|
+
ok: true;
|
|
146
|
+
row: AskRow;
|
|
147
|
+
} | {
|
|
148
|
+
ok: false;
|
|
149
|
+
reason: "batch_closed" | "ask_not_pending" | "idempotency_conflict";
|
|
150
|
+
row: AskRow | null;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* 幂等键的入店卫生门(属主兜底轮 F2,三 twin 同判——SQL 双方言与 InMemory 都从这里过)。
|
|
154
|
+
*
|
|
155
|
+
* 🔴 为什么必须在店门口拒而不是「存什么比什么」:TiDB/MySQL 的 VARCHAR 等值与 UNIQUE 检查带
|
|
156
|
+
* PAD SPACE 语义(utf8mb4_bin 也一样)——`'retry'` 与 `'retry '` 在那侧是**同一个键**,而 PG 与
|
|
157
|
+
* InMemory 的严格字符串比较把它们当两个键 ⇒ 同一份调用方代码在不同后端拿到不同的
|
|
158
|
+
* `idempotency_conflict`/回放结果。键是调用方铸的 opaque 值,首尾空白没有任何合法语义,
|
|
159
|
+
* 在边界上 fail-loud 拒掉,方言分歧面整个消失(比把列改 VARBINARY 更窄、且对三形同时成立)。
|
|
160
|
+
*/
|
|
161
|
+
export declare function assertIdempotencyKeyShape(key: string): void;
|
|
162
|
+
/**
|
|
163
|
+
* #151 车3 刀 3b(§14.2 遗留记账):两条**重放读口**被调用方的 deadline 掐断时抛的具名错误。
|
|
164
|
+
*
|
|
165
|
+
* 具名类而不是「按 message 文本判别」:错误文案是给人看的,拿它当控制流 = 上游改一句人话下游静默失效
|
|
166
|
+
* (#90 工程规范 v3 门②)。调用方(开流 preamble)对它的处置与其余读错误一样 —— fail-open、零卡、一次
|
|
167
|
+
* warn —— 所以它今天没有专门的分支;类在这里是为了让**将来**想分「超时 vs 店真报错」的消费点有得分。
|
|
168
|
+
*/
|
|
169
|
+
export declare class ApprovalAskReadAbortedError extends Error {
|
|
170
|
+
constructor(where: string);
|
|
171
|
+
}
|
|
172
|
+
export interface ExpireResult {
|
|
173
|
+
won: boolean;
|
|
174
|
+
voidedSiblings: string[];
|
|
175
|
+
}
|
|
176
|
+
export interface BindGateInput {
|
|
177
|
+
gateToken: string;
|
|
178
|
+
gateBoundCallId?: string | null;
|
|
179
|
+
gateBoundInputHash?: string | null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* `bindBatch` 的判别式返回形(车5 §8 A-4 + §9 C3)。原先的 `Promise<boolean>` 把两件**处置完全相反**的
|
|
183
|
+
* 失败压成同一个 `false`:批 `ABORTED`(run 被取消 ⇒ 收敛器该走 VOID 臂)与批已 `ROUTING_BOUND`
|
|
184
|
+
* (兄弟先绑 ⇒ 本行该收 VOID/续判,绝不是失败)。调用方拿不到批态就只能猜,于是:
|
|
185
|
+
* - `ok: true` 带 `voidedSiblings` —— 被本次绑定连坐 VOID 的 PARKING 兄弟名单(撤卡帧的 `askIds`);
|
|
186
|
+
* 读法与 `expireAsk` 同形(PG `RETURNING` / TiDB 同事务先 `SELECT` 后 `UPDATE`)。
|
|
187
|
+
* - `ok: false` 带 `batchState` —— 批行的**真实**当前态;`"MISSING"` = 批行不存在(错配 batchId 的
|
|
188
|
+
* 编程错误形)。`boundAskId` 只在批已绑定时在场(中选者是谁)。
|
|
189
|
+
*/
|
|
190
|
+
export type BindResult = {
|
|
191
|
+
ok: true;
|
|
192
|
+
voidedSiblings: string[];
|
|
193
|
+
} | {
|
|
194
|
+
ok: false;
|
|
195
|
+
batchState: BatchState | "MISSING";
|
|
196
|
+
boundAskId?: string;
|
|
197
|
+
};
|
|
198
|
+
export interface BatchRow {
|
|
199
|
+
batchId: string;
|
|
200
|
+
taskId: string;
|
|
201
|
+
state: BatchState;
|
|
202
|
+
boundAskId: string | null;
|
|
203
|
+
version: number;
|
|
204
|
+
createdAtMs: number;
|
|
205
|
+
updatedAtMs: number;
|
|
206
|
+
}
|
|
207
|
+
/** design 定稿 §4 的持久层接口。协调器接线(车2)、恢复扫描消费(车5)不在本车范围——本车只落这些方法。 */
|
|
208
|
+
export interface ApprovalAskStore {
|
|
209
|
+
ensureAsk(row: NewAskRow): Promise<AskRow>;
|
|
210
|
+
transitionAsk(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
|
|
211
|
+
decideAsk(askId: string, batchId: string, decision: DecideAskInput): Promise<DecideResult>;
|
|
212
|
+
expireAsk(askId: string, batchId: string): Promise<ExpireResult>;
|
|
213
|
+
bindBatch(batchId: string, askId: string, gate: BindGateInput): Promise<BindResult>;
|
|
214
|
+
abortBatch(batchId: string): Promise<string[]>;
|
|
215
|
+
/**
|
|
216
|
+
* 🔴 车5 §9 C5:**纯 touch** 的 CAS——推 `updated_at_ms` + `version`,**不改 state、不走状态机转移表**。
|
|
217
|
+
* 收敛器每轮扫过但未收敛的 PARKING 行靠它排到队尾(读口 `ORDER BY updated_at_ms ASC`),否则 200 条
|
|
218
|
+
* 长驻行会把批量上限占满、新行永远轮不到(队头堵塞)。
|
|
219
|
+
*
|
|
220
|
+
* 为什么不能复用 `transitionAsk`:那条路先过 `canAskTransition`,而 `PARKING → PARKING` 是同态转移、
|
|
221
|
+
* 表里没有这条边(表把「同值自环」定义为非法,§6-11 钉住的正是这条)——所以队列轮转在状态机上无合法写口,
|
|
222
|
+
* 只能另开一扇不碰 state 的门。
|
|
223
|
+
*
|
|
224
|
+
* `expectedVersion` 必带:两副本同扫时只允许**一个**推进,输者原样返回 false(自己那轮的判定已过期)。
|
|
225
|
+
* 🔴 配套纪律(同 §9 C5):orphan/adhoc 的 TTL 一律量 immutable 的 `createdAtMs`/`expiresAtMs`,
|
|
226
|
+
* **绝不量 `updatedAtMs`**——它会被本方法每轮刷新,量它的 TTL 永不到期。
|
|
227
|
+
*/
|
|
228
|
+
deferReconcile(askId: string, expectedState: AskState, expectedVersion: number, nowMs: number): Promise<boolean>;
|
|
229
|
+
listByState(state: AskState, limit: number): Promise<AskRow[]>;
|
|
230
|
+
/** `signal` 语义见 {@link ApprovalAskStore.listPendingBySession}(两条重放读口同款)。 */
|
|
231
|
+
listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
|
|
232
|
+
/** 车3 §5.1 的第二个重放读口:sync 腿每次开流都是**新 taskId**,它要接的未决卡来自「同一
|
|
233
|
+
* (owner, session) 的上一条腿 / 长命 bg 子代」(正是 `boundAsk` broker 的 `["session", owner,
|
|
234
|
+
* sessionId]` 键域)——按 taskId 读永远读不到它们。
|
|
235
|
+
*
|
|
236
|
+
* `owner` **必填**(不是 optional):重放面是**跨腿投递**,必须带租户门,不能靠调用方记得过滤
|
|
237
|
+
* (与 `respond` 的 owner gate 同姿势)。`null` = 无租户身份的部署形,匹配 `owner IS NULL` 的行 ——
|
|
238
|
+
* SQL 的 `owner = NULL` 恒 UNKNOWN,所以两方言都按 owner 是否为 null 走两段不同的谓词文本,
|
|
239
|
+
* 而不是塞一个会静默匹配零行的绑定参数。
|
|
240
|
+
*
|
|
241
|
+
* 🔴 `signal`(车3 刀 3b,§14.2 遗留记账):**只有两条重放读口**带它 —— 它们是唯一一类「调用方有硬
|
|
242
|
+
* deadline、超时后结果彻底无用」的读(开流 preamble 的 2s 有界窗)。给写路径加 signal 是另一回事
|
|
243
|
+
* (中止一半的事务语义),不在此列。
|
|
244
|
+
*
|
|
245
|
+
* ⚠️ **可达到的语义与残留(如实)**:本仓的 `SqlDriver`/`PgQueryFn` 都**没有取消 seam**(mysql2 要
|
|
246
|
+
* `connection.destroy()`、pg 要另开连接发 cancel request),所以 abort 做到的是:①已 abort 的调用
|
|
247
|
+
* **一条 SQL 都不发**;②在飞的调用**立刻**以 abort 拒绝、调用方不再被吊住。**做不到**的是让已经发出去
|
|
248
|
+
* 的那条查询在库上停下来 —— 它仍会跑完并占着它那个池位。因此 §14.2 记的「DB 挂死时重连风暴累积连接池
|
|
249
|
+
* 等待」只被**部分**缓解(不再叠加调用方侧的等待,池侧仍会堆)。真正的取消要 driver 级 seam,登记为
|
|
250
|
+
* 后续件。 */
|
|
251
|
+
listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
|
|
252
|
+
getAsk(askId: string): Promise<AskRow | null>;
|
|
253
|
+
/** 回决幂等回放的读口。🔴 `taskId` **必填**且是键的第一维——UNIQUE 已改 `(task_id, idempotency_key)`
|
|
254
|
+
* (车4 §12-E),裸按 key 全局查在多租户库上既可能回**别人 task** 的行(存在性外泄),也可能在同 key
|
|
255
|
+
* 合法共存于两 task 时任选一行返回(不确定)。租户门是读口自己的责任,同 `listPendingBySession`。 */
|
|
256
|
+
getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
|
|
257
|
+
resolveProvisional(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
|
|
258
|
+
deleteByTask(taskId: string): Promise<void>;
|
|
259
|
+
/** 🔴 车1 追加(不在设计定稿 §4 的接口字面清单里):设计的 `ApprovalAskStore` 接口本身没有批读方法,
|
|
260
|
+
* 但 §6 测试钉(3/6/7 条)要求断言批行的 state/boundAskId 变化——没有读口就无法验证 store 真做对了
|
|
261
|
+
* 它自己承诺的事。加一个纯观测方法(不改变任何写路径行为),SQL 双方言 + InMemory 三形都实现,
|
|
262
|
+
* 供测试与未来的协调器/对账消费——同精神先例:background-agent-store-sql.ts 的 `listScopes` 也是
|
|
263
|
+
* 「核心接口之外、为了让分区可枚举而加的」扩展读口。 */
|
|
264
|
+
getBatch(batchId: string): Promise<BatchRow | null>;
|
|
265
|
+
}
|
|
266
|
+
export declare const APPROVAL_ASKS_TABLE = "approval_asks";
|
|
267
|
+
export declare const APPROVAL_BATCHES_TABLE = "approval_batches";
|
|
268
|
+
/**
|
|
269
|
+
* TiDB/MySQL-protocol 侧的两条建表语句,**抽成导出数组**(车3 刀 3a):`tidb-pool.ts` 的
|
|
270
|
+
* `SCHEMA_STATEMENTS` 用 `...TIDB_APPROVAL_ASK_STATEMENTS` 展开它们,于是这两张表随中央
|
|
271
|
+
* `ensureSchema` 一起在 **named-lock 的那条 `conn`** 上建 —— 把一个 pool 级 ensure 函数塞进
|
|
272
|
+
* `SCHEMA_STATEMENTS` 的执行循环里做不到这点(会绕开 DDL 串行化),所以真源必须是**语句文本**。
|
|
273
|
+
* 下面的 `ensureTiDBApprovalAskSchema` 保留为遍历这个数组的薄壳(集成测试直接调它建表,
|
|
274
|
+
* 不必为了建两张表去拉起整套中央 schema)。
|
|
275
|
+
*
|
|
276
|
+
* SCHEMA POLICY 同 `tidb-pool.ts` 头注:纯 CREATE,禁 ALTER 增量 seam,改列直接改这里 + 重建库
|
|
277
|
+
* (`docs/schema/baseline-mysql.sql` 是由这些文本机器录制出的**产物**,永不手改)。
|
|
278
|
+
*/
|
|
279
|
+
export declare const TIDB_APPROVAL_ASK_STATEMENTS: readonly string[];
|
|
280
|
+
/** {@link TIDB_APPROVAL_ASK_STATEMENTS} 的遍历壳——生产路径走 `tidb-pool.ts` 的中央 `ensureSchema`
|
|
281
|
+
* (那里展开了同一份数组),本函数留给只需要这两张表的集成测试。 */
|
|
282
|
+
export declare function ensureTiDBApprovalAskSchema(pool: MySqlPool): Promise<void>;
|
|
283
|
+
export declare function ensurePgApprovalAskSchema(q: PgQueryFn): Promise<void>;
|
|
284
|
+
/** 双方言 `ApprovalAskStore`。见文件头注的 CAS 铁则 + 方言差异清单。 */
|
|
285
|
+
export declare class SqlApprovalAskStore implements ApprovalAskStore {
|
|
286
|
+
private readonly db;
|
|
287
|
+
constructor(db: SqlDriver);
|
|
288
|
+
private q;
|
|
289
|
+
private json;
|
|
290
|
+
/** 唯一键冲突判别(方言差异显式,checkpoint-store-sql.ts 同形):mysql2 挂 `errno`,node-pg 挂 SQLSTATE `code`。 */
|
|
291
|
+
private isDupKey;
|
|
292
|
+
/**
|
|
293
|
+
* 🔴 失败臂的回读**必须走当前这条 `conn`**,不许调 `this.getAsk`/`this.getBatch`(codex 复审 F1,
|
|
294
|
+
* 2026-08-06 真缺陷,池大小=1 的钉上实测挂死 25s)。原因:那两个读口走**池级** `this.db.query`,而
|
|
295
|
+
* 此刻本方法还攥着自己的事务连接(要到 `finally` 才 release)——于是「回读在等一条新连接,而唯一
|
|
296
|
+
* 那条连接要等回读返回才释放」= 确定性死锁。池大于 1 时它退化成「同时走到失败臂的并发数 ≥ 池大小
|
|
297
|
+
* 就整池饿死」,是同一个缺陷的更难复现形,不是另一个问题。
|
|
298
|
+
*
|
|
299
|
+
* 调用点纪律:**先 `conn.rollback()` 再调这两个**。回滚之后连接回到 autocommit,这条 SELECT 自成
|
|
300
|
+
* 一个事务 ⇒ 读到的是最新已提交视图(正是失败臂要如实上报的东西),不受本事务快照的影响——两方言
|
|
301
|
+
* 在这一点上同义,不必再纠结 InnoDB 读视图与 TiDB start_ts 的差别。
|
|
302
|
+
*/
|
|
303
|
+
private getAskOn;
|
|
304
|
+
/** {@link getAskOn} 的批侧同形(同一条纪律:失败臂回读走本连接)。 */
|
|
305
|
+
private getBatchOn;
|
|
306
|
+
ensureAsk(row: NewAskRow): Promise<AskRow>;
|
|
307
|
+
transitionAsk(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
|
|
308
|
+
decideAsk(askId: string, batchId: string, decision: DecideAskInput): Promise<DecideResult>;
|
|
309
|
+
expireAsk(askId: string, batchId: string): Promise<ExpireResult>;
|
|
310
|
+
bindBatch(batchId: string, askId: string, gate: BindGateInput): Promise<BindResult>;
|
|
311
|
+
abortBatch(batchId: string): Promise<string[]>;
|
|
312
|
+
/**
|
|
313
|
+
* 🔴 排序键是 `updated_at_ms`(车5 §8 D-4;codex 三轮补抓:原先按 `created_at_ms` 排,`deferReconcile`
|
|
314
|
+
* 就是**空转**的)。这条读口与 `deferReconcile` 是一对:收敛器每轮取前 `limit` 条,判不出终局的行用
|
|
315
|
+
* `deferReconcile` 推 `updated_at_ms` 把自己排到队尾,下一轮新行才轮得到。若这里仍按建行时间排,
|
|
316
|
+
* 推 `updated_at_ms` 对顺序毫无影响 ⇒ 一旦 `limit` 被若干长驻 PARKING 行占满,每轮扫到的永远是同一批,
|
|
317
|
+
* 后来的审批被无限饿死(§5 旋钮那句「超出下轮接着扫」也就不成立)。
|
|
318
|
+
* 次级键 `created_at_ms` 只为**确定性**:同毫秒的行不至于在两次扫描间乱序(分页/限流下的稳定序)。
|
|
319
|
+
*
|
|
320
|
+
* 🔴 配套纪律(§9 C5):任何 TTL 判据一律量 immutable 的 `createdAtMs`/`expiresAtMs`,**绝不量
|
|
321
|
+
* `updatedAtMs`**——它被队列轮转每轮刷新,量它的 TTL 永不到期。
|
|
322
|
+
*/
|
|
323
|
+
listByState(state: AskState, limit: number): Promise<AskRow[]>;
|
|
324
|
+
listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
|
|
325
|
+
listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
|
|
326
|
+
getAsk(askId: string): Promise<AskRow | null>;
|
|
327
|
+
getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
|
|
328
|
+
deferReconcile(askId: string, expectedState: AskState, expectedVersion: number, nowMs: number): Promise<boolean>;
|
|
329
|
+
resolveProvisional(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
|
|
330
|
+
deleteByTask(taskId: string): Promise<void>;
|
|
331
|
+
getBatch(batchId: string): Promise<BatchRow | null>;
|
|
332
|
+
}
|
|
333
|
+
/** MySQL-protocol(TiDB)绑定。 */
|
|
334
|
+
export declare class TiDBApprovalAskStore extends SqlApprovalAskStore {
|
|
335
|
+
constructor(pool: MySqlPool);
|
|
336
|
+
}
|
|
337
|
+
/** PostgreSQL 绑定。 */
|
|
338
|
+
export declare class PgApprovalAskStore extends SqlApprovalAskStore {
|
|
339
|
+
constructor(pool: PgPool);
|
|
340
|
+
}
|
|
341
|
+
//# sourceMappingURL=approval-ask-store-sql.d.ts.map
|