@sema-agent/server 7.7.0 → 7.8.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/dist/approval-card.d.ts +2 -2
- package/dist/approval-card.js +2 -2
- package/dist/approval-deny-reasons.d.ts +1 -1
- package/dist/approval-deny-reasons.js +1 -1
- package/dist/approval-reconciler.js +2 -2
- package/dist/capabilities/hands-lane.d.ts +9 -0
- package/dist/capabilities/hands-lane.js +11 -2
- package/dist/config-types.d.ts +1 -1
- package/dist/http/routes/runs.js +1 -1
- package/dist/http/server.js +5 -2
- package/dist/plugins/approval-ask-store-memory.d.ts +1 -1
- package/dist/plugins/approval-ask-store-memory.js +17 -17
- package/dist/plugins/approval-ask-store-sql.d.ts +12 -12
- package/dist/plugins/approval-ask-store-sql.js +58 -58
- package/dist/plugins/background-agent-store-sql.d.ts +1 -1
- package/dist/plugins/background-agent-store-sql.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +2 -2
- package/dist/plugins/checkpoint-store-sql.js +6 -6
- package/dist/plugins/mailbox-store-sql.d.ts +2 -2
- package/dist/plugins/mailbox-store-sql.js +2 -2
- package/dist/plugins/memory-engine-pg.d.ts +2 -2
- package/dist/plugins/memory-engine-pg.js +17 -17
- package/dist/plugins/memory-engine-tidb.d.ts +2 -2
- package/dist/plugins/memory-engine-tidb.js +16 -16
- package/dist/plugins/memory-sync-store-pg.d.ts +2 -2
- package/dist/plugins/memory-sync-store-pg.js +19 -19
- package/dist/plugins/memory-sync-store-tidb.d.ts +1 -1
- package/dist/plugins/memory-sync-store-tidb.js +21 -21
- package/dist/plugins/pg-pool.js +26 -26
- package/dist/plugins/task-list-store-sql.d.ts +1 -1
- package/dist/plugins/task-list-store-sql.js +11 -11
- package/dist/plugins/tidb-pool.js +26 -26
- package/dist/plugins/workflow-journal-store-sql.d.ts +1 -1
- package/dist/plugins/workflow-journal-store-sql.js +4 -4
- package/dist/plugins/workflow-run-store-sql.d.ts +1 -1
- package/dist/plugins/workflow-run-store-sql.js +9 -9
- package/dist/security.js +9 -1
- package/package.json +2 -2
package/dist/approval-card.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* #151 车3(design/172 流内审批协议)—— **审批卡的 schema 属主 + 重放腿的纯函数**。
|
|
3
3
|
*
|
|
4
4
|
* 本模块只出两样东西,**都不带 IO、不带定时器、不发帧**:
|
|
5
|
-
* 1. `ApprovalCardSchema` / `ApprovalCardEnvelopeSchema` —— 落库 `
|
|
5
|
+
* 1. `ApprovalCardSchema` / `ApprovalCardEnvelopeSchema` —— 落库 `approval_ask.card_json` 与一切读面
|
|
6
6
|
* (重放腿=刀 3c、回决端点=车4、对账扫描=车5)共用的**同一份** schema。写侧(刀 3b 的 `ensureAsk`)
|
|
7
7
|
* 与读侧走同一个符号,于是「存的形」与「读的形」不可能各自漂。
|
|
8
8
|
* 2. `buildReplayFrame` / `buildApprovalPreamble` —— 行 → 帧的**纯投影**(设计稿 §5.2)。
|
|
@@ -61,7 +61,7 @@ export declare const ApprovalCardSchema: z.ZodObject<{
|
|
|
61
61
|
}, z.core.$strict>;
|
|
62
62
|
export type ApprovalCard = z.infer<typeof ApprovalCardSchema>;
|
|
63
63
|
/**
|
|
64
|
-
* `
|
|
64
|
+
* `approval_ask.card_json` 里真正存的东西(设计稿 §6.2)。存**信封**而不是裸卡的理由:重放帧要回填
|
|
65
65
|
* 车2 铸的 `approvalId`(wire 面的回决通道桥 + 消费端与并行 `tool_approval` 帧的去重键),而它不属于
|
|
66
66
|
* 「卡的内容」——放进卡里会让 schema 的语义边界糊掉。`schemaVersion` 是形的版本闩(与行上的
|
|
67
67
|
* `schema_version` 列同值,列供 SQL 侧过滤,信封里这份供读面在解出内容**之前**判形)。
|
package/dist/approval-card.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* #151 车3(design/172 流内审批协议)—— **审批卡的 schema 属主 + 重放腿的纯函数**。
|
|
3
3
|
*
|
|
4
4
|
* 本模块只出两样东西,**都不带 IO、不带定时器、不发帧**:
|
|
5
|
-
* 1. `ApprovalCardSchema` / `ApprovalCardEnvelopeSchema` —— 落库 `
|
|
5
|
+
* 1. `ApprovalCardSchema` / `ApprovalCardEnvelopeSchema` —— 落库 `approval_ask.card_json` 与一切读面
|
|
6
6
|
* (重放腿=刀 3c、回决端点=车4、对账扫描=车5)共用的**同一份** schema。写侧(刀 3b 的 `ensureAsk`)
|
|
7
7
|
* 与读侧走同一个符号,于是「存的形」与「读的形」不可能各自漂。
|
|
8
8
|
* 2. `buildReplayFrame` / `buildApprovalPreamble` —— 行 → 帧的**纯投影**(设计稿 §5.2)。
|
|
@@ -92,7 +92,7 @@ export const ApprovalCardSchema = z
|
|
|
92
92
|
})
|
|
93
93
|
.strict();
|
|
94
94
|
/**
|
|
95
|
-
* `
|
|
95
|
+
* `approval_ask.card_json` 里真正存的东西(设计稿 §6.2)。存**信封**而不是裸卡的理由:重放帧要回填
|
|
96
96
|
* 车2 铸的 `approvalId`(wire 面的回决通道桥 + 消费端与并行 `tool_approval` 帧的去重键),而它不属于
|
|
97
97
|
* 「卡的内容」——放进卡里会让 schema 的语义边界糊掉。`schemaVersion` 是形的版本闩(与行上的
|
|
98
98
|
* `schema_version` 列同值,列供 SQL 侧过滤,信封里这份供读面在解出内容**之前**判形)。
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* wire 与审计上全程可分。若两组共用一个值域(或某个值同时可落两种终态),「这条 deny 是人做的、还是
|
|
12
12
|
* 路由失败、还是超时兜底」就再也无法从行上读出来,归因诚实性(§6.1)当场失效。
|
|
13
13
|
*
|
|
14
|
-
* 落列面:两组值都写进 `
|
|
14
|
+
* 落列面:两组值都写进 `approval_ask.denied_reason`(VARCHAR(64))——列名沿用车1 的 DDL(本车不改列),
|
|
15
15
|
* 但它承载的是**终态归因**而不只是「拒绝理由」。读者靠「值属于哪一组」判别是哪条臂写的,这正是两组
|
|
16
16
|
* 值域不相交这条纪律的用处(`ALL_TERMINAL_REASONS` 的机器判据钉住不相交性)。
|
|
17
17
|
*
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* wire 与审计上全程可分。若两组共用一个值域(或某个值同时可落两种终态),「这条 deny 是人做的、还是
|
|
12
12
|
* 路由失败、还是超时兜底」就再也无法从行上读出来,归因诚实性(§6.1)当场失效。
|
|
13
13
|
*
|
|
14
|
-
* 落列面:两组值都写进 `
|
|
14
|
+
* 落列面:两组值都写进 `approval_ask.denied_reason`(VARCHAR(64))——列名沿用车1 的 DDL(本车不改列),
|
|
15
15
|
* 但它承载的是**终态归因**而不只是「拒绝理由」。读者靠「值属于哪一组」判别是哪条臂写的,这正是两组
|
|
16
16
|
* 值域不相交这条纪律的用处(`ALL_TERMINAL_REASONS` 的机器判据钉住不相交性)。
|
|
17
17
|
*
|
|
@@ -254,7 +254,7 @@ export function createApprovalReconciler(deps) {
|
|
|
254
254
|
switch (action.kind) {
|
|
255
255
|
case "bind": {
|
|
256
256
|
// `allowBind: false` 下结构上不可达;留一条防御性 hold,绝不在这里第二次调 bindBatch。
|
|
257
|
-
await withDeadline(askStore.deferReconcile(ask.askId, "PARKING", ask.
|
|
257
|
+
await withDeadline(askStore.deferReconcile(ask.askId, "PARKING", ask.rev, nowMs), "deferReconcile");
|
|
258
258
|
return { outcome: "held", match: gateMatch };
|
|
259
259
|
}
|
|
260
260
|
case "deny": {
|
|
@@ -288,7 +288,7 @@ export function createApprovalReconciler(deps) {
|
|
|
288
288
|
case "hold": {
|
|
289
289
|
// 队列轮转(§8 D-4 / §9 C5):推 `updated_at_ms` 排到队尾。CAS 输(版本已被别人推进)= 正常,
|
|
290
290
|
// 本轮判定已过期,下轮重来。
|
|
291
|
-
await withDeadline(askStore.deferReconcile(ask.askId, "PARKING", ask.
|
|
291
|
+
await withDeadline(askStore.deferReconcile(ask.askId, "PARKING", ask.rev, nowMs), "deferReconcile");
|
|
292
292
|
return { outcome: "held", match: gateMatch };
|
|
293
293
|
}
|
|
294
294
|
}
|
|
@@ -43,6 +43,15 @@ export declare function pickHandsRunner(hands: ScenarioHands, pair: HandsRunnerP
|
|
|
43
43
|
*
|
|
44
44
|
* ⚠️ 本全集是**观测口径**(用于防漂移对账),**不是** deny 名单 —— deny 用下面的
|
|
45
45
|
* {@link HANDS_EXCLUSIVE_TOOL_NAMES},理由见那里。
|
|
46
|
+
*
|
|
47
|
+
* 🚨 [3248] P0(2026-08-09):派生时必须过滤 core 的 `RETIRED_TOOL_NAMES` —— core 5.21.0 的
|
|
48
|
+
* `HAND_TOOL_EFFECTS` 里残留已退役的 `MultiEdit`,而 core `prepare-task` 的策略名审计见退役名
|
|
49
|
+
* **无条件 throw**(`config.legacy_tool_name`);不过滤,deny 名单一进 `spec.toolPolicy`,
|
|
50
|
+
* `hands: none` 三场景(scan/code-review/team)在**所有部署**上零 turn 全灭。core 5.21.1 已删
|
|
51
|
+
* 那条死条目([3249]),这道 filter 是结构性防御:将来 core 再退役任何 band 名都不会重演
|
|
52
|
+
* (test/scenario-hands-lane.test.ts 格7/格8 双钉:真 Runner 过审计 + 零交集门)。
|
|
53
|
+
* filter 打在**整表**(effect 表 + 钉死补集)之后 —— caret floor 下装置会解析到更新的 core,
|
|
54
|
+
* 补集名哪天被退役,只滤前半就是同一次故障换个名字(codex 复审 finding,2026-08-09)。
|
|
46
55
|
*/
|
|
47
56
|
export declare const HANDS_BAND_TOOL_NAMES: readonly string[];
|
|
48
57
|
/**
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* 有手 Runner 上,手独占名单逐一 deny。**不是**主保证(主保证是根本不挂 factory ⇒ 工具面不可见)。
|
|
15
15
|
* ③ {@link createHandsLaneRegistry} —— resolveSpec 产出 spec 与执行点选 Runner 之间的 lane 传递。
|
|
16
16
|
*/
|
|
17
|
-
import { HAND_TOOL_EFFECTS, combinePolicies, createAllowDenyPolicy } from "@sema-agent/core";
|
|
17
|
+
import { HAND_TOOL_EFFECTS, RETIRED_TOOL_NAMES, combinePolicies, createAllowDenyPolicy } from "@sema-agent/core";
|
|
18
18
|
/**
|
|
19
19
|
* 无手孪生 Runner 的 deps 构造:把 `executionEnvFactory` **摘键**(不是置 undefined)。
|
|
20
20
|
* 摘键让「这只 Runner 没有手」在 deps 字面量上自证;置 undefined 留下一个值为 undefined 的键,
|
|
@@ -44,6 +44,15 @@ export function pickHandsRunner(hands, pair) {
|
|
|
44
44
|
*
|
|
45
45
|
* ⚠️ 本全集是**观测口径**(用于防漂移对账),**不是** deny 名单 —— deny 用下面的
|
|
46
46
|
* {@link HANDS_EXCLUSIVE_TOOL_NAMES},理由见那里。
|
|
47
|
+
*
|
|
48
|
+
* 🚨 [3248] P0(2026-08-09):派生时必须过滤 core 的 `RETIRED_TOOL_NAMES` —— core 5.21.0 的
|
|
49
|
+
* `HAND_TOOL_EFFECTS` 里残留已退役的 `MultiEdit`,而 core `prepare-task` 的策略名审计见退役名
|
|
50
|
+
* **无条件 throw**(`config.legacy_tool_name`);不过滤,deny 名单一进 `spec.toolPolicy`,
|
|
51
|
+
* `hands: none` 三场景(scan/code-review/team)在**所有部署**上零 turn 全灭。core 5.21.1 已删
|
|
52
|
+
* 那条死条目([3249]),这道 filter 是结构性防御:将来 core 再退役任何 band 名都不会重演
|
|
53
|
+
* (test/scenario-hands-lane.test.ts 格7/格8 双钉:真 Runner 过审计 + 零交集门)。
|
|
54
|
+
* filter 打在**整表**(effect 表 + 钉死补集)之后 —— caret floor 下装置会解析到更新的 core,
|
|
55
|
+
* 补集名哪天被退役,只滤前半就是同一次故障换个名字(codex 复审 finding,2026-08-09)。
|
|
47
56
|
*/
|
|
48
57
|
export const HANDS_BAND_TOOL_NAMES = Object.freeze([
|
|
49
58
|
...Object.keys(HAND_TOOL_EFFECTS),
|
|
@@ -52,7 +61,7 @@ export const HANDS_BAND_TOOL_NAMES = Object.freeze([
|
|
|
52
61
|
"Monitor",
|
|
53
62
|
"EnterWorktree",
|
|
54
63
|
"ExitWorktree",
|
|
55
|
-
]);
|
|
64
|
+
].filter((n) => !RETIRED_TOOL_NAMES.has(n)));
|
|
56
65
|
/**
|
|
57
66
|
* band 里**只有手才可能出现**的那批(= deny 名单)。
|
|
58
67
|
*
|
package/dist/config-types.d.ts
CHANGED
|
@@ -869,7 +869,7 @@ export interface ServiceConfigFlat {
|
|
|
869
869
|
* `approvalTimeoutSec` 族,本旋钮不新增第二个 TTL 概念。 */
|
|
870
870
|
streamAskWindowMarginMs: number;
|
|
871
871
|
/** #151 车3(design/172 流内审批协议)总开关 + 四个从属旋钮。**协议整体默认 OFF**——开关关闭时全链
|
|
872
|
-
* 逐字现行为:不发 `approval_request` 帧、不落 `
|
|
872
|
+
* 逐字现行为:不发 `approval_request` 帧、不落 `approval_ask` 行、窗 = 既有 `DEFAULT_APPROVAL_TTL_MS`
|
|
873
873
|
* (5min)、`askStore` 不注入协调器。理由(三问):
|
|
874
874
|
* - 谁需要 60s 窗:开了协议的部署——approver 就盯着流、秒级应答,窗短 ⇒ 少占一条活腿、更快落到可恢复
|
|
875
875
|
* 的 park 面。这是协议的设计意图,不是随手取的数。
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -46,7 +46,7 @@ export const TOOL_APPROVAL_RESPOND_RE = /^\/v1\/tool-approvals\/([^/]+)\/respond
|
|
|
46
46
|
// #151 车4(design/172 §3.2;稿 = design/151-172-stream-approval/151-car4-design.md,§12 折叠节为准)
|
|
47
47
|
// —— 流内审批卡的 **durable 回决口**。HITL 族第四条,但与上面三条 respond 的辖域正相反:
|
|
48
48
|
// · 三条 respond = LIVE-ONLY + 同副本(解一个内存里的悬挂 promise;跨副本/窗后一律 404);
|
|
49
|
-
// · 本条 = 打 `
|
|
49
|
+
// · 本条 = 打 `approval_ask` 行的 **CAS**(任何副本都能受理),本副本活体窗只是「顺带同步终局」。
|
|
50
50
|
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
51
51
|
export const TASK_ASK_DECISION_RE = /^\/v1\/tasks\/([^/]+)\/asks\/([^/]+)\/decision$/;
|
|
52
52
|
/** 425 的轮询提示秒数(头与体成对,§12-C F37)。 */
|
package/dist/http/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import { once } from "node:events";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, HAND_TOOL_EFFECTS, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs } from "@sema-agent/core";
|
|
4
|
+
import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, HAND_TOOL_EFFECTS, RETIRED_TOOL_NAMES, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs } from "@sema-agent/core";
|
|
5
5
|
import { decideParkedAgent, findParkedAgentForCheckpoint } from "../parked-decide.js";
|
|
6
6
|
import { matchCatalogModel } from "../model-select.js";
|
|
7
7
|
import {} from "../config-center/facade.js";
|
|
@@ -107,7 +107,10 @@ const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024; // a single inline image's base6
|
|
|
107
107
|
// Hand (executionEnv) tool names core mounts at the runner when an executionEnvFactory is configured — the
|
|
108
108
|
// authoritative set is core `HAND_TOOL_EFFECTS` (root-exported since 1.70). These are NEVER in spec.tools, so
|
|
109
109
|
// the durable-resume satisfiability pre-check must union them when remote exec is on.
|
|
110
|
-
|
|
110
|
+
// [3248]: filter RETIRED_TOOL_NAMES — core 5.21.0 left retired "MultiEdit" in HAND_TOOL_EFFECTS; a retired
|
|
111
|
+
// name is never mounted, so keeping it here would let the pre-check claim satisfiability for a tool that
|
|
112
|
+
// cannot exist (same derivation hygiene as hands-lane.ts HANDS_BAND_TOOL_NAMES).
|
|
113
|
+
const HAND_TOOL_NAMES = Object.keys(HAND_TOOL_EFFECTS).filter((n) => !RETIRED_TOOL_NAMES.has(n));
|
|
111
114
|
/** [1245] codex-3/4 — the REOPEN-class resume failures: core's runtask reopened the checkpoint (store.reopen,
|
|
112
115
|
* dist-read) BEFORE returning this failed result, so the park is STILL PENDING and decidable. Everything
|
|
113
116
|
* downstream must treat these as a re-park, never terminal: the durable event log writes `suspended` (not
|
|
@@ -21,7 +21,7 @@ export declare class InMemoryApprovalAskStore implements ApprovalAskStore {
|
|
|
21
21
|
decideAsk(askId: string, batchId: string, decision: DecideAskInput): Promise<DecideResult>;
|
|
22
22
|
expireAsk(askId: string, batchId: string): Promise<ExpireResult>;
|
|
23
23
|
bindBatch(batchId: string, askId: string, gate: BindGateInput): Promise<BindResult>;
|
|
24
|
-
deferReconcile(askId: string, expectedState: AskState,
|
|
24
|
+
deferReconcile(askId: string, expectedState: AskState, expectedRev: number, nowMs: number): Promise<boolean>;
|
|
25
25
|
abortBatch(batchId: string): Promise<string[]>;
|
|
26
26
|
listByState(state: AskState, limit: number): Promise<AskRow[]>;
|
|
27
27
|
/** `signal` 的 twin 语义(车3 刀 3b):同步实现里没有「在飞」这回事,所以能做也只需做**入口复核** ——
|
|
@@ -44,7 +44,7 @@ export class InMemoryApprovalAskStore {
|
|
|
44
44
|
taskId: row.taskId,
|
|
45
45
|
state: "OPEN",
|
|
46
46
|
boundAskId: null,
|
|
47
|
-
|
|
47
|
+
rev: 0,
|
|
48
48
|
createdAtMs: row.createdAtMs,
|
|
49
49
|
updatedAtMs: row.createdAtMs,
|
|
50
50
|
});
|
|
@@ -69,7 +69,7 @@ export class InMemoryApprovalAskStore {
|
|
|
69
69
|
boundInputHash: row.boundInputHash ?? null,
|
|
70
70
|
state: "STREAM_PENDING",
|
|
71
71
|
provisional: false,
|
|
72
|
-
|
|
72
|
+
rev: 0,
|
|
73
73
|
decision: null,
|
|
74
74
|
decisionActor: null,
|
|
75
75
|
decisionNote: null,
|
|
@@ -97,7 +97,7 @@ export class InMemoryApprovalAskStore {
|
|
|
97
97
|
return false;
|
|
98
98
|
applyPatch(row, patch);
|
|
99
99
|
row.state = to;
|
|
100
|
-
row.
|
|
100
|
+
row.rev += 1;
|
|
101
101
|
row.updatedAtMs = patch.updatedAtMs;
|
|
102
102
|
return true;
|
|
103
103
|
}
|
|
@@ -125,7 +125,7 @@ export class InMemoryApprovalAskStore {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
-
batch.
|
|
128
|
+
batch.rev += 1;
|
|
129
129
|
batch.updatedAtMs = now;
|
|
130
130
|
ask.state = "DECIDED";
|
|
131
131
|
ask.decision = decision.decision;
|
|
@@ -133,7 +133,7 @@ export class InMemoryApprovalAskStore {
|
|
|
133
133
|
ask.decisionNote = decision.decisionNote ?? null;
|
|
134
134
|
ask.decidedAtMs = now;
|
|
135
135
|
ask.idempotencyKey = decision.idempotencyKey ?? null;
|
|
136
|
-
ask.
|
|
136
|
+
ask.rev += 1;
|
|
137
137
|
ask.updatedAtMs = now;
|
|
138
138
|
return { ok: true, row: { ...ask } };
|
|
139
139
|
}
|
|
@@ -149,18 +149,18 @@ export class InMemoryApprovalAskStore {
|
|
|
149
149
|
if (!ask || ask.state !== "STREAM_PENDING")
|
|
150
150
|
return { won: false, voidedSiblings: [] };
|
|
151
151
|
ask.state = "PARKING";
|
|
152
|
-
ask.
|
|
152
|
+
ask.rev += 1;
|
|
153
153
|
ask.updatedAtMs = now;
|
|
154
154
|
if (batch.state === "OPEN") {
|
|
155
155
|
batch.state = "ROUTING_UNBOUND";
|
|
156
|
-
batch.
|
|
156
|
+
batch.rev += 1;
|
|
157
157
|
batch.updatedAtMs = now;
|
|
158
158
|
}
|
|
159
159
|
const voided = [];
|
|
160
160
|
for (const [id, r] of this.asks) {
|
|
161
161
|
if (r.batchId === batchId && r.state === "STREAM_PENDING" && id !== askId) {
|
|
162
162
|
r.state = "VOID";
|
|
163
|
-
r.
|
|
163
|
+
r.rev += 1;
|
|
164
164
|
r.updatedAtMs = now;
|
|
165
165
|
voided.push(id);
|
|
166
166
|
}
|
|
@@ -183,31 +183,31 @@ export class InMemoryApprovalAskStore {
|
|
|
183
183
|
return { ok: false, batchState: batch.state };
|
|
184
184
|
batch.state = "ROUTING_BOUND";
|
|
185
185
|
batch.boundAskId = askId;
|
|
186
|
-
batch.
|
|
186
|
+
batch.rev += 1;
|
|
187
187
|
batch.updatedAtMs = now;
|
|
188
188
|
ask.state = "PARKED";
|
|
189
189
|
ask.gateToken = gate.gateToken;
|
|
190
190
|
ask.gateBoundCallId = gate.gateBoundCallId ?? null;
|
|
191
191
|
ask.gateBoundInputHash = gate.gateBoundInputHash ?? null;
|
|
192
|
-
ask.
|
|
192
|
+
ask.rev += 1;
|
|
193
193
|
ask.updatedAtMs = now;
|
|
194
194
|
const voidedSiblings = [];
|
|
195
195
|
for (const [id, r] of this.asks) {
|
|
196
196
|
if (r.batchId === batchId && r.state === "PARKING" && id !== askId) {
|
|
197
197
|
r.state = "VOID";
|
|
198
|
-
r.
|
|
198
|
+
r.rev += 1;
|
|
199
199
|
r.updatedAtMs = now;
|
|
200
200
|
voidedSiblings.push(id);
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
return { ok: true, voidedSiblings };
|
|
204
204
|
}
|
|
205
|
-
async deferReconcile(askId, expectedState,
|
|
205
|
+
async deferReconcile(askId, expectedState, expectedRev, nowMs) {
|
|
206
206
|
// 纯 touch,**不碰 state**,故意不经 canAskTransition(同态转移在转移表上无出边——见接口注)。
|
|
207
207
|
const row = this.asks.get(askId);
|
|
208
|
-
if (!row || row.state !== expectedState || row.
|
|
208
|
+
if (!row || row.state !== expectedState || row.rev !== expectedRev)
|
|
209
209
|
return false;
|
|
210
|
-
row.
|
|
210
|
+
row.rev += 1;
|
|
211
211
|
row.updatedAtMs = nowMs;
|
|
212
212
|
return true;
|
|
213
213
|
}
|
|
@@ -217,13 +217,13 @@ export class InMemoryApprovalAskStore {
|
|
|
217
217
|
if (!batch || (batch.state !== "OPEN" && batch.state !== "ROUTING_UNBOUND"))
|
|
218
218
|
return [];
|
|
219
219
|
batch.state = "ABORTED";
|
|
220
|
-
batch.
|
|
220
|
+
batch.rev += 1;
|
|
221
221
|
batch.updatedAtMs = now;
|
|
222
222
|
const voided = [];
|
|
223
223
|
for (const [id, r] of this.asks) {
|
|
224
224
|
if (r.batchId === batchId && r.state === "STREAM_PENDING") {
|
|
225
225
|
r.state = "VOID";
|
|
226
|
-
r.
|
|
226
|
+
r.rev += 1;
|
|
227
227
|
r.updatedAtMs = now;
|
|
228
228
|
voided.push(id);
|
|
229
229
|
}
|
|
@@ -278,7 +278,7 @@ export class InMemoryApprovalAskStore {
|
|
|
278
278
|
applyPatch(row, patch);
|
|
279
279
|
row.state = to;
|
|
280
280
|
row.provisional = false;
|
|
281
|
-
row.
|
|
281
|
+
row.rev += 1;
|
|
282
282
|
row.updatedAtMs = patch.updatedAtMs;
|
|
283
283
|
return true;
|
|
284
284
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (`TiDBApprovalAskStore` / `PgApprovalAskStore`,SINGLE-FILE DUAL-DIALECT,design/158 A12 形——照
|
|
4
4
|
* checkpoint-store-sql.ts:一个 `SqlApprovalAskStore` 类接 `SqlDriver`,两个薄 ctor 子类落方言绑定)。
|
|
5
5
|
*
|
|
6
|
-
* 两张表:`
|
|
6
|
+
* 两张表:`approval_ask`(每个流内工具调用一行,ask 级 6 态状态机)+ `approval_batch`(同一决策点的
|
|
7
7
|
* 兄弟 ask 共享一行,批级 4 态状态机,承载「N 个候选里最终只有一个中选走 gate」的 bind-once 语义)。
|
|
8
8
|
* 状态机真源 = `../approval-ask-machine.js`(纯函数,零 IO)。
|
|
9
9
|
*
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* 1. 一切转移带 `WHERE state = <from>`,赢 = affected 恰为 1。禁读-改-写两步(TOCTOU 窗口)。
|
|
12
12
|
* 2. 🔴 MySQL/TiDB `affectedRows` 坑:同值 UPDATE(SET 的新值与当前列值逐字节相同)affectedRows=0——
|
|
13
13
|
* 这不是「行不存在」,是「驱动认为没有变化」。**所有** CAS UPDATE 因此必须带一个恒变列
|
|
14
|
-
* (`
|
|
15
|
-
* 误判成「CAS 输了」。本文件每一条 CAS UPDATE 都带 `
|
|
14
|
+
* (`rev = rev + 1`),这样只要「行在且谓词命中」就必然 affected=1,不会被这个驱动怪癖
|
|
15
|
+
* 误判成「CAS 输了」。本文件每一条 CAS UPDATE 都带 `rev = rev + 1`。
|
|
16
16
|
* 3. 事务:走 `SqlDriver.connect()` → `conn.begin()` → 一串 `conn.query()` → `conn.commit()`,失败
|
|
17
17
|
* `conn.rollback()`(形照 image-bake-store-sql.ts 的 try/catch/finally 骨架)。本店没有 image-bake
|
|
18
18
|
* 那种「NOT EXISTS 快照读」问题(每条 CAS 都是对已存在主键行的 `UPDATE … WHERE state=?`,MySQL/TiDB
|
|
@@ -73,7 +73,7 @@ export interface AskRow {
|
|
|
73
73
|
state: AskState;
|
|
74
74
|
/** §3.0 对账三约束②:超时不得发布终态 denial;provisional 终态可被 `resolveProvisional` 版本化收敛。 */
|
|
75
75
|
provisional: boolean;
|
|
76
|
-
|
|
76
|
+
rev: number;
|
|
77
77
|
decision: AskDecision | null;
|
|
78
78
|
/** 171 ActorAssertion JSON(车4 才消费,列先在——JSON.parse 后的裸值,类型未知)。 */
|
|
79
79
|
decisionActor: unknown | null;
|
|
@@ -100,7 +100,7 @@ export interface AskRow {
|
|
|
100
100
|
createdAtMs: number;
|
|
101
101
|
updatedAtMs: number;
|
|
102
102
|
}
|
|
103
|
-
/** `ensureAsk` 的入参——初始态恒 STREAM_PENDING/provisional=false/
|
|
103
|
+
/** `ensureAsk` 的入参——初始态恒 STREAM_PENDING/provisional=false/rev=0,故不在此列。 */
|
|
104
104
|
export interface NewAskRow {
|
|
105
105
|
askId: string;
|
|
106
106
|
taskId: string;
|
|
@@ -221,7 +221,7 @@ export interface BatchRow {
|
|
|
221
221
|
taskId: string;
|
|
222
222
|
state: BatchState;
|
|
223
223
|
boundAskId: string | null;
|
|
224
|
-
|
|
224
|
+
rev: number;
|
|
225
225
|
createdAtMs: number;
|
|
226
226
|
updatedAtMs: number;
|
|
227
227
|
}
|
|
@@ -235,7 +235,7 @@ export interface ApprovalAskStore {
|
|
|
235
235
|
bindBatch(batchId: string, askId: string, gate: BindGateInput): Promise<BindResult>;
|
|
236
236
|
abortBatch(batchId: string): Promise<string[]>;
|
|
237
237
|
/**
|
|
238
|
-
* 🔴 车5 §9 C5:**纯 touch** 的 CAS——推 `updated_at_ms` + `
|
|
238
|
+
* 🔴 车5 §9 C5:**纯 touch** 的 CAS——推 `updated_at_ms` + `rev`,**不改 state、不走状态机转移表**。
|
|
239
239
|
* 收敛器每轮扫过但未收敛的 PARKING 行靠它排到队尾(读口 `ORDER BY updated_at_ms ASC`),否则 200 条
|
|
240
240
|
* 长驻行会把批量上限占满、新行永远轮不到(队头堵塞)。
|
|
241
241
|
*
|
|
@@ -243,11 +243,11 @@ export interface ApprovalAskStore {
|
|
|
243
243
|
* 表里没有这条边(表把「同值自环」定义为非法,§6-11 钉住的正是这条)——所以队列轮转在状态机上无合法写口,
|
|
244
244
|
* 只能另开一扇不碰 state 的门。
|
|
245
245
|
*
|
|
246
|
-
* `
|
|
246
|
+
* `expectedRev` 必带:两副本同扫时只允许**一个**推进,输者原样返回 false(自己那轮的判定已过期)。
|
|
247
247
|
* 🔴 配套纪律(同 §9 C5):orphan/adhoc 的 TTL 一律量 immutable 的 `createdAtMs`/`expiresAtMs`,
|
|
248
248
|
* **绝不量 `updatedAtMs`**——它会被本方法每轮刷新,量它的 TTL 永不到期。
|
|
249
249
|
*/
|
|
250
|
-
deferReconcile(askId: string, expectedState: AskState,
|
|
250
|
+
deferReconcile(askId: string, expectedState: AskState, expectedRev: number, nowMs: number): Promise<boolean>;
|
|
251
251
|
listByState(state: AskState, limit: number): Promise<AskRow[]>;
|
|
252
252
|
/** `signal` 语义见 {@link ApprovalAskStore.listPendingBySession}(两条重放读口同款)。 */
|
|
253
253
|
listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
|
|
@@ -285,8 +285,8 @@ export interface ApprovalAskStore {
|
|
|
285
285
|
* 「核心接口之外、为了让分区可枚举而加的」扩展读口。 */
|
|
286
286
|
getBatch(batchId: string): Promise<BatchRow | null>;
|
|
287
287
|
}
|
|
288
|
-
export declare const
|
|
289
|
-
export declare const
|
|
288
|
+
export declare const APPROVAL_ASK_TABLE = "approval_ask";
|
|
289
|
+
export declare const APPROVAL_BATCH_TABLE = "approval_batch";
|
|
290
290
|
/**
|
|
291
291
|
* TiDB/MySQL-protocol 侧的两条建表语句,**抽成导出数组**(车3 刀 3a):`tidb-pool.ts` 的
|
|
292
292
|
* `SCHEMA_STATEMENTS` 用 `...TIDB_APPROVAL_ASK_STATEMENTS` 展开它们,于是这两张表随中央
|
|
@@ -380,7 +380,7 @@ export declare class SqlApprovalAskStore implements ApprovalAskStore {
|
|
|
380
380
|
listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
|
|
381
381
|
getAsk(askId: string): Promise<AskRow | null>;
|
|
382
382
|
getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
|
|
383
|
-
deferReconcile(askId: string, expectedState: AskState,
|
|
383
|
+
deferReconcile(askId: string, expectedState: AskState, expectedRev: number, nowMs: number): Promise<boolean>;
|
|
384
384
|
resolveProvisional(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
|
|
385
385
|
deleteByTask(taskId: string): Promise<void>;
|
|
386
386
|
getBatch(batchId: string): Promise<BatchRow | null>;
|