@sema-agent/server 7.93.3 → 7.93.4
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/host-decision.d.ts +28 -1
- package/dist/http/routes/runs.js +5 -5
- package/dist/plugins/approval-ask-store-memory.js +5 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +56 -0
- package/dist/plugins/approval-ask-store-sql.js +29 -5
- package/dist/tool-approval.d.ts +30 -24
- package/dist/tool-approval.js +57 -44
- package/package.json +2 -2
package/dist/host-decision.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* ⇒ 本仓的规则从「记住一张词×决定的相容表」变成「说出你知道的那两件事」。相容性也不再靠记忆:一次
|
|
15
15
|
* sweep 不可能 `allow`,core pre-CAS 拒。
|
|
16
16
|
*/
|
|
17
|
-
import { type ResumeOutcome } from "@sema-agent/core";
|
|
17
|
+
import { type AskOutcome, type ResumeOutcome } from "@sema-agent/core";
|
|
18
18
|
/**
|
|
19
19
|
* 谁结束了这次等待,以及通道自报的归属。
|
|
20
20
|
* · `decidedBy: "person"` —— 运维经审批通道做的决定(两条 HTTP `/decide` 腿);
|
|
@@ -25,6 +25,33 @@ import { type ResumeOutcome } from "@sema-agent/core";
|
|
|
25
25
|
* 并把结算记成 `policy_refused`(`who.party:"none"`)。`allow` 侧结构上不可(core pre-CAS 拒:只有人批准)。
|
|
26
26
|
* · `approver` —— 通道自报的结算方标识串。**缺席永不读成**「没人批」或「有人批」,只意味着通道没报名字。
|
|
27
27
|
*/
|
|
28
|
+
/**
|
|
29
|
+
* core 5.23.0([ref]/[ref]②)—— **宿主自报**的结算来源,`AskOutcome` 对象臂那一格的型。本仓在这一格上有
|
|
30
|
+
* **两种身份**,合成一只座是刻意的(两只座 = 同一个 wire 位的两个写者):
|
|
31
|
+
* · **铸**(本仓自己观察到的事实):只有 `"timeout"` —— 同步 `onAsk` 腿的等待窗归**宿主**所有(引擎不为
|
|
32
|
+
* 一个它没排程的回调起表),所以「本仓的 TTL 窗走完了、没人答」这件事引擎结构上观察不到,不自报就
|
|
33
|
+
* 永远被记成「一个人拒绝了」;
|
|
34
|
+
* · **转**(调用方在回决体里自报的事实,core 7.26.2 / #1016 起):`settledBy:"policy"` = 答这张卡的是
|
|
35
|
+
* **这台部署自己的策略**,不是人。本仓**零判据、原样过境**:词的拒绝逻辑归引擎(`resolveAsk` 对
|
|
36
|
+
* 自报词表外的值是响亮 fail-closed deny),本仓抄一份词表的那天 core 加词就会被静默 400 掉。
|
|
37
|
+
* 逐词(词表属主恒是 core):
|
|
38
|
+
* · `"human"` —— 与「不铸这个键」逐字同义(core 缺席即按人处理);
|
|
39
|
+
* · `"aborted"` —— core **明令拒收**宿主自报(那是引擎自家 fail-closed 端的词,宿主自报等于把自己的
|
|
40
|
+
* 拒绝改标成引擎故障),所以它**不在** `AskOutcome` 的这一格上,本型也就够不着它。
|
|
41
|
+
* 类型从 core 的**同步 outcome 面**推导(不手抄):`AskOutcome` 的对象臂上 `settledBy` 是**宿主报的事实**,
|
|
42
|
+
* core 7.6.0([ref] S6-A)删掉的是那张**结算词表**(`ApprovalSettledBy`),不是这一格 —— 词由 core 从
|
|
43
|
+
* 这个事实铸(`human_refused` / `approval_window_expired` / `policy_refused`)。core 若改这一格的词表,
|
|
44
|
+
* 读它的每一处编译期先红。
|
|
45
|
+
*
|
|
46
|
+
* 🔴 **S-558:为什么这一格住在本叶子模块**(此前是 `tool-approval.ts` 的模块私有型):来源位随决议
|
|
47
|
+
* **落持久行**之后,店(`src/plugins/approval-ask-store-sql.ts` 的 `AskRow.settledBy` /
|
|
48
|
+
* `DecideAskInput.settledBy`)与协调器都要说同一个型 —— 而店不能反向依赖协调器。本模块只依赖
|
|
49
|
+
* `@sema-agent/core`,与同住的 {@link HostDecidedBy}(耐久腿的同位事实)恰好是一对:一条决议的来源
|
|
50
|
+
* 在两条腿上是同一件事,型也该在同一处。
|
|
51
|
+
*/
|
|
52
|
+
export type HostSettledBy = NonNullable<Extract<AskOutcome, {
|
|
53
|
+
allow: boolean;
|
|
54
|
+
}>["settledBy"]>;
|
|
28
55
|
/** 直接取 core `ResumeOutcome` policy_ask 臂的 `hostDecision` 型 —— 本仓不复述那几个词(第二铸点)。 */
|
|
29
56
|
export type HostDecision = Extract<ResumeOutcome, {
|
|
30
57
|
gate: "policy_ask";
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -16,7 +16,7 @@ import { scopedIdempotencyKey } from "../idempotency.js";
|
|
|
16
16
|
import { streamSseLog } from "../sse-log.js";
|
|
17
17
|
import { bindSseLifecycle } from "../sse-lifecycle.js";
|
|
18
18
|
import { buildApprovalPreamble, buildApprovalPreambleSseFrames } from "../../approval-card.js";
|
|
19
|
-
import { ASK_DECISION_CONSUMER_ABSENT, resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
19
|
+
import { ASK_DECISION_CONSUMER_ABSENT, outcomeOfDecidedRow, resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
20
20
|
import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
|
|
21
21
|
import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
|
|
22
22
|
import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
|
|
@@ -1321,7 +1321,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1321
1321
|
return true;
|
|
1322
1322
|
if (deps.toolApproval?.editedDecisionInFlight(row.askId) === true)
|
|
1323
1323
|
return true;
|
|
1324
|
-
const settled = deps.toolApproval?.notifyExternalDecision(row.askId, row
|
|
1324
|
+
const settled = deps.toolApproval?.notifyExternalDecision(row.askId, outcomeOfDecidedRow(row));
|
|
1325
1325
|
observedSettled.set(row.askId, settled?.settled ?? 0);
|
|
1326
1326
|
return true;
|
|
1327
1327
|
};
|
|
@@ -1431,7 +1431,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1431
1431
|
confirmed.decision === body.decision &&
|
|
1432
1432
|
confirmed.idempotencyKey === body.idempotencyKey &&
|
|
1433
1433
|
!confirmed.provisional) {
|
|
1434
|
-
const settledNow = deps.toolApproval.notifyExternalDecision(askId, confirmed
|
|
1434
|
+
const settledNow = deps.toolApproval.notifyExternalDecision(askId, outcomeOfDecidedRow(confirmed)).settled;
|
|
1435
1435
|
deps.logger?.warn?.("ask_decision_edit_settled_after_ambiguous_commit", { taskId, askId, settled: settledNow });
|
|
1436
1436
|
}
|
|
1437
1437
|
}
|
|
@@ -1470,11 +1470,11 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1470
1470
|
return;
|
|
1471
1471
|
if (outcome.row.decision !== body.decision) {
|
|
1472
1472
|
deps.logger?.error?.("ask_decision_row_diverged", { taskId, askId, requested: body.decision, stored: outcome.row.decision });
|
|
1473
|
-
deps.toolApproval?.notifyExternalDecision(outcome.row.askId, outcome.row
|
|
1473
|
+
deps.toolApproval?.notifyExternalDecision(outcome.row.askId, outcomeOfDecidedRow(outcome.row));
|
|
1474
1474
|
sendError(res, 500, "internal.error", "the ask row was rewritten between commit and read-back — the decision that stands is not the one this request submitted; re-read the ask before acting on it");
|
|
1475
1475
|
return;
|
|
1476
1476
|
}
|
|
1477
|
-
const settled = deps.toolApproval?.notifyExternalDecision(outcome.row.askId, outcome.row
|
|
1477
|
+
const settled = deps.toolApproval?.notifyExternalDecision(outcome.row.askId, outcomeOfDecidedRow(outcome.row)).settled ?? 0;
|
|
1478
1478
|
const forwarded = outcome.row.decision === "approve" && outcome.row.updatedInput !== undefined && settled > 0;
|
|
1479
1479
|
sendJson(res, 200, {
|
|
1480
1480
|
askId: outcome.row.askId,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { canAskTransition } from "../approval-ask-machine.js";
|
|
2
|
-
import { assertIdempotencyKeyShape, assertIntegralEpochMs, ApprovalAskReadAbortedError, projectAskTerminalClaimTruth } from "./approval-ask-store-sql.js";
|
|
2
|
+
import { assertDecideSettledBy, assertIdempotencyKeyShape, assertIntegralEpochMs, ApprovalAskReadAbortedError, projectAskTerminalClaimTruth } from "./approval-ask-store-sql.js";
|
|
3
3
|
function applyPatch(row, patch) {
|
|
4
4
|
if (patch.decision !== undefined)
|
|
5
5
|
row.decision = patch.decision;
|
|
@@ -42,7 +42,7 @@ export class InMemoryApprovalAskStore {
|
|
|
42
42
|
}
|
|
43
43
|
importRows(snapshot) {
|
|
44
44
|
for (const r of snapshot.asks ?? [])
|
|
45
|
-
this.asks.set(r.askId, { ...r });
|
|
45
|
+
this.asks.set(r.askId, { ...r, settledBy: r.settledBy ?? null });
|
|
46
46
|
for (const b of snapshot.batches ?? [])
|
|
47
47
|
this.batches.set(b.batchId, { ...b });
|
|
48
48
|
}
|
|
@@ -80,6 +80,7 @@ export class InMemoryApprovalAskStore {
|
|
|
80
80
|
decision: null,
|
|
81
81
|
decisionActor: null,
|
|
82
82
|
decisionNote: null,
|
|
83
|
+
settledBy: null,
|
|
83
84
|
decidedAtMs: null,
|
|
84
85
|
deniedReason: null,
|
|
85
86
|
gateToken: null,
|
|
@@ -113,6 +114,7 @@ export class InMemoryApprovalAskStore {
|
|
|
113
114
|
async decideAsk(askId, batchId, decision) {
|
|
114
115
|
if (decision.idempotencyKey != null)
|
|
115
116
|
assertIdempotencyKeyShape(decision.idempotencyKey);
|
|
117
|
+
assertDecideSettledBy(decision);
|
|
116
118
|
const now = this.now();
|
|
117
119
|
const batch = this.batches.get(batchId);
|
|
118
120
|
if (!batch || batch.state !== "OPEN") {
|
|
@@ -139,6 +141,7 @@ export class InMemoryApprovalAskStore {
|
|
|
139
141
|
ask.decision = decision.decision;
|
|
140
142
|
ask.decisionActor = decision.decisionActor ?? null;
|
|
141
143
|
ask.decisionNote = decision.decisionNote ?? null;
|
|
144
|
+
ask.settledBy = decision.settledBy ?? null;
|
|
142
145
|
if (decision.updatedInput === undefined)
|
|
143
146
|
delete ask.updatedInput;
|
|
144
147
|
else
|
|
@@ -41,6 +41,7 @@ import type { Pool as PgPool } from "pg";
|
|
|
41
41
|
import type { PgQueryFn } from "./pg-query.js";
|
|
42
42
|
import { type SqlDriver, type SqlDialect } from "./sql-driver.js";
|
|
43
43
|
import { type AskState, type BatchState } from "../approval-ask-machine.js";
|
|
44
|
+
import type { HostSettledBy } from "../host-decision.js";
|
|
44
45
|
import { type IndexSpec } from "./ensure-index.js";
|
|
45
46
|
export type AskDecision = "approve" | "deny";
|
|
46
47
|
export interface AskRow {
|
|
@@ -79,6 +80,32 @@ export interface AskRow {
|
|
|
79
80
|
/** 171 ActorAssertion JSON(车4 才消费,列先在——JSON.parse 后的裸值,类型未知)。 */
|
|
80
81
|
decisionActor: unknown | null;
|
|
81
82
|
decisionNote: string | null;
|
|
83
|
+
/**
|
|
84
|
+
* 🔴 S-558(#1016 的持久半场)—— 这条决议的**结算来源**:`null` = 一个人答的(= 缺席律,core 对
|
|
85
|
+
* 不带这一格的 outcome 逐字按 `"human"` 读),非 null = 宿主自报的那个词(今天 wire 上只开
|
|
86
|
+
* `"policy"`,词表属主恒是 core —— 见 {@link HostSettledBy})。
|
|
87
|
+
*
|
|
88
|
+
* **为什么必须是行上的一格**(病灶,7.93.3 合并树 codex ① 验真):`settledBy` 此前只到达**当次**回决的
|
|
89
|
+
* 即时回调,行上不留痕 ⇒ 重放 / 再入 / 同 askId 的兄弟等待者 / 歧义提交恢复读回的是裸 `false`,
|
|
90
|
+
* 而裸 `false` 在 core 那边逐字是「**一个人**拒绝了」—— 模型于是又读到那句「用户不想继续,停下来等他」,
|
|
91
|
+
* 在一条根本没有人的 run 里。不变量:**一条决议的来源在所有读回路径上是同一个事实**;唯一读回铸点 =
|
|
92
|
+
* `tool-approval.ts` 的 `outcomeOfDecidedRow`。
|
|
93
|
+
*
|
|
94
|
+
* 🔴 **为什么是自己一列,不是塞进 `decision_actor` 那只袋**(S-558 取舍,亲读后定):
|
|
95
|
+
* · 那只袋是**自由载荷**——型是 `unknown`(见上一格),读回不过任何 schema(`parseJsonColumn` 返回
|
|
96
|
+
* `unknown`),schema 属主在别处([ref] ActorAssertion),`adoption/plan.ts` 的裁定 D2 把它连同
|
|
97
|
+
* `decision_note` 一起定为**史实字段,永不改写**。往一只没有属主、没有类型化读回的袋里塞一个
|
|
98
|
+
* **执行面**的判别位 = 让一格决定「工具跑不跑、模型读哪一句」的事实寄生在一个只做审计的载荷上。
|
|
99
|
+
* · 更硬的一条:**live 回决腿根本不写那只袋**(`respondWithCas` 的 `decideAsk` 入参里没有
|
|
100
|
+
* `decisionActor`,列恒 NULL)。来源位进袋 ⇒ 那条腿得为了捎一个词现铸一只它压根没有的 actor
|
|
101
|
+
* ——凭空发明「谁按的」,正是 `hostDecisionFacts` 顶注禁的那件事。
|
|
102
|
+
* · 一列的代价已经由本仓的 SCHEMA POLICY 定死且**与既有两批加列同形**([ref] 两列、[ref] 一列):
|
|
103
|
+
* 改 CREATE + 启动期响亮拒(见 {@link assertApprovalAskRuleMaterialSchema}),零 ALTER、零双读、
|
|
104
|
+
* 老行 NULL 恒读成「人决」。
|
|
105
|
+
*
|
|
106
|
+
* 三态收成两态是刻意的:`"human"` 与缺席在 core 那边逐字同义,故店只存「非人」的那些词,`null` 即人。
|
|
107
|
+
*/
|
|
108
|
+
settledBy: HostSettledBy | null;
|
|
82
109
|
/**
|
|
83
110
|
* [ref]([ref] / [ref] 收口):人批准时带的 **ctrl+g 编辑实参**(wire `updatedInput`),与决议**同一条
|
|
84
111
|
* UPDATE** 原子落列 `updated_input`。语义三态,读法**严格**:
|
|
@@ -199,6 +226,14 @@ export interface DecideAskInput {
|
|
|
199
226
|
decisionNote?: string | null;
|
|
200
227
|
/** 车4 §12-E:回决幂等键,赢 CAS 时随决议一起落列(见 `AskRow.idempotencyKey`)。缺席 ⇒ 列保持 NULL。 */
|
|
201
228
|
idempotencyKey?: string | null;
|
|
229
|
+
/**
|
|
230
|
+
* 🔴 S-558:本次回决的**结算来源**,与决议同一条 UPDATE 落列 `settled_by`(语义/两态读法/为什么是
|
|
231
|
+
* 自己一列,全在 {@link AskRow.settledBy})。缺席 ⇒ 列 NULL ⇒ 读回是「一个人答的」。
|
|
232
|
+
*
|
|
233
|
+
* 店**不判词**(词表属主是 core):写什么存什么。店只判**形** —— `assertDecideSettledBy` 的两条
|
|
234
|
+
* (长度 ≤ 列宽、只与 `deny` 同行),理由见那只门的顶注;三 twin 同判。
|
|
235
|
+
*/
|
|
236
|
+
settledBy?: HostSettledBy;
|
|
202
237
|
/** [ref]:ctrl+g 编辑实参,与决议同一条 UPDATE 落列(语义三态见 {@link AskRow.updatedInput})。
|
|
203
238
|
* 缺席 / `undefined` ⇒ 列 NULL(没带编辑);**`null` 是值**(编辑成 null),照存 JSON `"null"`。
|
|
204
239
|
* 调用方纪律:deny 不传(店不替调用方判决议方向,写什么存什么)。 */
|
|
@@ -233,6 +268,24 @@ export type DecideResult = {
|
|
|
233
268
|
* 的 deadline 铸点)负责给整数,本守卫只拦漏网。 */
|
|
234
269
|
export declare function assertIntegralEpochMs(field: string, value: number): void;
|
|
235
270
|
export declare function assertIdempotencyKeyShape(key: string): void;
|
|
271
|
+
/** `settled_by` 列宽 = 这一格的唯一尺(两方言 DDL 与下面的形门读同一个常量,不手抄第二份)。 */
|
|
272
|
+
export declare const MAX_SETTLED_BY_CHARS = 16;
|
|
273
|
+
/**
|
|
274
|
+
* S-558 的**入店卫生门**(三 twin 同判,与 {@link assertIdempotencyKeyShape} / {@link assertIntegralEpochMs} 同族)。
|
|
275
|
+
* 判的是**形**,不是**词** —— 词表属主恒是 core(`resolveAsk` 对自报词表外的值是响亮 fail-closed deny),
|
|
276
|
+
* 本层一个词都不认识。两条:
|
|
277
|
+
*
|
|
278
|
+
* ① **长度 ≤ 列宽**。`settled_by` 是 `VARCHAR(16)` 的闭集词列,而值来自 wire(两条回决腿只证得出「它是个串」)。
|
|
279
|
+
* 不拦的话:MySQL 协议侧非严格模式**静默截断**、PG 侧直接报错 —— 同一份调用方代码在两个后端拿到两种
|
|
280
|
+
* 答案(`assertIdempotencyKeyShape` 的 PAD SPACE 论证逐字同源),而**被截断过的词与原词是两个词**:
|
|
281
|
+
* 一条决议的来源在库里变成了另一件事。空串同拒(core 那边它不是词,存进去只会读成一个假的「非人」)。
|
|
282
|
+
* ② **只与 `deny` 同行**。core 的契约明写两个非人词只在 `allow:false` 上合法(`{allow:true,settledBy}` 是
|
|
283
|
+
* fail-closed 拒),两条回决腿也都在**裁决之前**照此响亮拒(`tool-approval.ts` 的
|
|
284
|
+
* `SETTLED_BY_DENY_ONLY`,两腿共用一份文案)。在店门口再执行一次不是「第二个判官」:判的不是词而是**这一对列能不能共存**。
|
|
285
|
+
* 收益是结构性的 —— 「approve 行带来源位」从此在写面不可表示,读回铸点(`outcomeOfDecidedRow`)的 allow 臂
|
|
286
|
+
* 因此**证得出**不会丢掉任何真事实,而不是靠记性。
|
|
287
|
+
*/
|
|
288
|
+
export declare function assertDecideSettledBy(decision: Pick<DecideAskInput, "decision" | "settledBy">): void;
|
|
236
289
|
/**
|
|
237
290
|
* [ref] 车3 刀 3b(§14.2 遗留记账):两条**重放读口**被调用方的 deadline 掐断时抛的具名错误。
|
|
238
291
|
*
|
|
@@ -277,6 +330,9 @@ export type AskTerminalClaimCurrent = {
|
|
|
277
330
|
decision: AskDecision | null;
|
|
278
331
|
/** [ref] 三态读法照 {@link AskRow.updatedInput}:键缺席 = 没带编辑;`null` 是编辑成 null 的值。 */
|
|
279
332
|
updatedInput?: unknown;
|
|
333
|
+
/** S-558:赢家决议的**结算来源**(语义与两态读法见 {@link AskRow.settledBy})。**必填**(不是可选)——
|
|
334
|
+
* 可选会让投影漏挑这一列时静默编译过,而漏挑的后果恰是本批要修的病:CAS 输家把赢家的策略拒读成人拒。 */
|
|
335
|
+
settledBy: HostSettledBy | null;
|
|
280
336
|
};
|
|
281
337
|
/**
|
|
282
338
|
* 单次原子往返的答案(语义律的唯一规范陈述点 = core `checkpoint-store.ts` `claimTerminal` 契约 JSDoc,
|
|
@@ -12,6 +12,18 @@ export function assertIdempotencyKeyShape(key) {
|
|
|
12
12
|
throw new Error(`invalid idempotency key: must be 1-255 chars with no leading/trailing whitespace (got ${JSON.stringify(key.slice(0, 32))}${key.length > 32 ? "…" : ""}, length ${key.length})`);
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
export const MAX_SETTLED_BY_CHARS = 16;
|
|
16
|
+
export function assertDecideSettledBy(decision) {
|
|
17
|
+
const v = decision.settledBy;
|
|
18
|
+
if (v === undefined)
|
|
19
|
+
return;
|
|
20
|
+
if (v.length === 0 || v.length > MAX_SETTLED_BY_CHARS) {
|
|
21
|
+
throw new Error(`invalid settledBy: the settlement source is one word from the engine's closed set and lands in a VARCHAR(${MAX_SETTLED_BY_CHARS}) column — must be 1-${MAX_SETTLED_BY_CHARS} chars (got ${JSON.stringify(v.slice(0, 32))}${v.length > 32 ? "…" : ""}, length ${v.length})`);
|
|
22
|
+
}
|
|
23
|
+
if (decision.decision !== "deny") {
|
|
24
|
+
throw new Error(`invalid settledBy: a settlement source is only representable on a deny row (got decision="${decision.decision}") — only a person approves an action that EXECUTES, so an approve row carrying a non-human settlement source is a row no replay could honestly hand back to the engine`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
15
27
|
export class ApprovalAskReadAbortedError extends Error {
|
|
16
28
|
constructor(where) {
|
|
17
29
|
super(`approval-ask read ${where} aborted by caller`);
|
|
@@ -47,6 +59,7 @@ export function projectAskTerminalClaimTruth(row) {
|
|
|
47
59
|
rev: row.rev,
|
|
48
60
|
decision: row.decision,
|
|
49
61
|
...(row.updatedInput !== undefined ? { updatedInput: row.updatedInput } : {}),
|
|
62
|
+
settledBy: row.settledBy,
|
|
50
63
|
};
|
|
51
64
|
}
|
|
52
65
|
export const APPROVAL_ASK_TABLE = "approval_ask";
|
|
@@ -87,6 +100,12 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
|
|
|
87
100
|
decision VARCHAR(16) NULL,
|
|
88
101
|
decision_actor TEXT NULL,
|
|
89
102
|
decision_note TEXT NULL,
|
|
103
|
+
-- settled_by(S-558 / #1016 持久半场):这条决议的结算来源词(NULL = 人答的)。语义、两态读法与
|
|
104
|
+
-- 「为什么是自己一列而不是塞进 decision_actor」的全文见 AskRow.settledBy 顶注。
|
|
105
|
+
-- VARCHAR(16):词表属主是 core(AskOutcome.settledBy,今天最长 7 字符 "timeout"),宽度与同表
|
|
106
|
+
-- decision 同尺同理由 —— 闭集词列,不是自由文本。截断面由入店形门堵死(assertDecideSettledBy),
|
|
107
|
+
-- 不靠「列够宽就不会截」这种运气(MySQL 非严格模式静默截、PG 直接报错 = 两方言不同答)。
|
|
108
|
+
settled_by VARCHAR(16) NULL,
|
|
90
109
|
-- updated_input(S-26 / #304,A-054.15 收口):人批准时带的 ctrl+g 编辑实参,JSON 文本,与决议同一条
|
|
91
110
|
-- UPDATE 原子落列;NULL = 没带编辑(与 JSON "null" = 编辑成 null 分家)。LONGTEXT 照 card_json 先例:
|
|
92
111
|
-- 编辑实参是整只工具入参,VARCHAR/TEXT 会造静默截断面。语义三态与读法见 AskRow.updatedInput 顶注。
|
|
@@ -166,6 +185,8 @@ export async function ensurePgApprovalAskSchema(q) {
|
|
|
166
185
|
decision VARCHAR(16) COLLATE "C",
|
|
167
186
|
decision_actor TEXT COLLATE "C",
|
|
168
187
|
decision_note TEXT COLLATE "C",
|
|
188
|
+
-- settled_by(S-558)—— MySQL 孪生逐字同形(宽度/语义/形门的理由写在那侧的行内注)。
|
|
189
|
+
settled_by VARCHAR(16) COLLATE "C",
|
|
169
190
|
-- updated_input(S-26 / #304)—— MySQL 孪生逐字同形(语义/类型理由见那侧行内注;PG TEXT 无长度上限)。
|
|
170
191
|
updated_input TEXT COLLATE "C",
|
|
171
192
|
decided_at_ms BIGINT,
|
|
@@ -211,7 +232,7 @@ export async function assertApprovalAskRuleMaterialSchema(query, dialect) {
|
|
|
211
232
|
}
|
|
212
233
|
}
|
|
213
234
|
const ASK_COLS = "ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, " +
|
|
214
|
-
"provisional, rev, decision, decision_actor, decision_note, updated_input, decided_at_ms, denied_reason, gate_token, " +
|
|
235
|
+
"provisional, rev, decision, decision_actor, decision_note, settled_by, updated_input, decided_at_ms, denied_reason, gate_token, " +
|
|
215
236
|
"gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, " +
|
|
216
237
|
"created_at_ms, updated_at_ms";
|
|
217
238
|
const BATCH_COLS = "batch_id, task_id, state, bound_ask_id, rev, created_at_ms, updated_at_ms";
|
|
@@ -245,6 +266,7 @@ function mapAskRow(r) {
|
|
|
245
266
|
decision: strOrNull(r.decision) ?? null,
|
|
246
267
|
decisionActor: r.decision_actor == null ? null : parseJsonColumn(r.decision_actor),
|
|
247
268
|
decisionNote: strOrNull(r.decision_note),
|
|
269
|
+
settledBy: strOrNull(r.settled_by),
|
|
248
270
|
...(r.updated_input == null ? {} : { updatedInput: parseJsonColumn(r.updated_input) }),
|
|
249
271
|
decidedAtMs: numOrNull(r.decided_at_ms),
|
|
250
272
|
deniedReason: strOrNull(r.denied_reason),
|
|
@@ -331,9 +353,9 @@ export class SqlApprovalAskStore {
|
|
|
331
353
|
try {
|
|
332
354
|
await conn.begin();
|
|
333
355
|
await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_BATCH_TABLE} (batch_id, task_id, state, rev, created_at_ms, updated_at_ms) VALUES (?, ?, 'OPEN', 0, ?, ?)`, `INSERT INTO ${APPROVAL_BATCH_TABLE} (batch_id, task_id, state, rev, created_at_ms, updated_at_ms) VALUES ($1, $2, 'OPEN', 0, $3, $4) ON CONFLICT (batch_id) DO NOTHING`), [row.batchId, row.taskId, now, now]);
|
|
334
|
-
const askInsert = await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
|
|
335
|
-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?, ?, ?)", `INSERT INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
|
|
336
|
-
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $11, $12, $13, $14, $15, $16, $17) ON CONFLICT (ask_id) DO NOTHING"), [
|
|
356
|
+
const askInsert = await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, settled_by, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
|
|
357
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?, ?, ?)", `INSERT INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, settled_by, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
|
|
358
|
+
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $11, $12, $13, $14, $15, $16, $17) ON CONFLICT (ask_id) DO NOTHING"), [
|
|
337
359
|
row.askId,
|
|
338
360
|
row.taskId,
|
|
339
361
|
row.sourceTaskId,
|
|
@@ -389,6 +411,7 @@ export class SqlApprovalAskStore {
|
|
|
389
411
|
async decideAsk(askId, batchId, decision) {
|
|
390
412
|
if (decision.idempotencyKey != null)
|
|
391
413
|
assertIdempotencyKeyShape(decision.idempotencyKey);
|
|
414
|
+
assertDecideSettledBy(decision);
|
|
392
415
|
const now = Date.now();
|
|
393
416
|
const dialect = this.db.dialect;
|
|
394
417
|
const conn = await this.db.connect();
|
|
@@ -401,10 +424,11 @@ export class SqlApprovalAskStore {
|
|
|
401
424
|
}
|
|
402
425
|
let askRes;
|
|
403
426
|
try {
|
|
404
|
-
askRes = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=${ph(dialect, 1)}, decision_actor=${ph(dialect, 2)}, decision_note=${ph(dialect, 3)},
|
|
427
|
+
askRes = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=${ph(dialect, 1)}, decision_actor=${ph(dialect, 2)}, decision_note=${ph(dialect, 3)}, settled_by=${ph(dialect, 4)}, updated_input=${ph(dialect, 5)}, decided_at_ms=${ph(dialect, 6)}, idempotency_key=${ph(dialect, 7)}, rev=rev+1, updated_at_ms=${ph(dialect, 8)} WHERE ${pendingClaimWhere(dialect, 9)}`, [
|
|
405
428
|
decision.decision,
|
|
406
429
|
decision.decisionActor == null ? null : this.json(decision.decisionActor),
|
|
407
430
|
decision.decisionNote ?? null,
|
|
431
|
+
decision.settledBy ?? null,
|
|
408
432
|
decision.updatedInput === undefined ? null : this.json(decision.updatedInput),
|
|
409
433
|
now,
|
|
410
434
|
decision.idempotencyKey ?? null,
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { HostDecidedBy } from "./host-decision.js";
|
|
1
|
+
import type { HostDecidedBy, HostSettledBy } from "./host-decision.js";
|
|
2
2
|
import type { IncomingMessage } from "node:http";
|
|
3
3
|
import { type AskRequest, type AskOutcome, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
|
|
4
4
|
import { type CardRulePersisted, type RuleConsentLane } from "./rules-consent.js";
|
|
5
5
|
import { type DenialLimitFallback, type RuleOffersAbsence, type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js";
|
|
6
|
-
import { type ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
|
|
6
|
+
import { type ApprovalAskStore, type AskRow } from "./plugins/approval-ask-store-sql.js";
|
|
7
7
|
import type { ApprovalAskAuditSink } from "./approval-ask-audit-store.js";
|
|
8
8
|
import { type GovernanceAskMarks } from "./governance-ask-marks.js";
|
|
9
9
|
/** [ref]:审批 gate kind 闭集(core gateMatch 的 `human`/`irreversible_ask` ↔ outcome.gate "policy_ask")。
|
|
@@ -613,27 +613,6 @@ export declare function parseToolApprovalResponse(body: unknown): {
|
|
|
613
613
|
ok: false;
|
|
614
614
|
error: string;
|
|
615
615
|
};
|
|
616
|
-
/**
|
|
617
|
-
* core 5.23.0([ref]/[ref]②)—— **宿主自报**的结算来源,`AskOutcome` 对象臂新开的口。本仓在这一格上有
|
|
618
|
-
* **两种身份**,合成一只座是刻意的(两只座 = 同一个 wire 位的两个写者):
|
|
619
|
-
* · **铸**(本仓自己观察到的事实):只有 `"timeout"` —— 同步 `onAsk` 腿的等待窗归**宿主**所有(引擎不为
|
|
620
|
-
* 一个它没排程的回调起表),所以「本仓的 TTL 窗走完了、没人答」这件事引擎结构上观察不到,不自报就
|
|
621
|
-
* 永远被记成「一个人拒绝了」;
|
|
622
|
-
* · **转**(调用方在回决体里自报的事实,core 7.26.2 / #1016 起):`settledBy:"policy"` = 答这张卡的是
|
|
623
|
-
* **这台部署自己的策略**,不是人。本仓**零判据、原样过境**:词的拒绝逻辑归引擎(`resolveAsk` 对
|
|
624
|
-
* 自报词表外的值是响亮 fail-closed deny),本仓抄一份词表的那天 core 加词就会被静默 400 掉。
|
|
625
|
-
* 逐词(词表属主恒是 core):
|
|
626
|
-
* · `"human"` —— 与「不铸这个键」逐字同义(core 缺席即按人处理);
|
|
627
|
-
* · `"aborted"` —— core **明令拒收**宿主自报(那是引擎自家 fail-closed 端的词,宿主自报等于把自己的
|
|
628
|
-
* 拒绝改标成引擎故障),所以它**不在** `AskOutcome` 的这一格上,本型也就够不着它。
|
|
629
|
-
* 类型从 core 的**同步 outcome 面**推导(不手抄):`AskOutcome` 的对象臂上 `settledBy` 是**宿主报的事实**,
|
|
630
|
-
* core 7.6.0([ref] S6-A)删掉的是那张**结算词表**(`ApprovalSettledBy`),不是这一格 —— 词由 core 从
|
|
631
|
-
* 这个事实铸(`human_refused` / `approval_window_expired` / `policy_refused`)。core 若改这一格的词表,
|
|
632
|
-
* 这里编译期先红。
|
|
633
|
-
*/
|
|
634
|
-
type HostSettledBy = NonNullable<Extract<AskOutcome, {
|
|
635
|
-
allow: boolean;
|
|
636
|
-
}>["settledBy"]>;
|
|
637
616
|
/**
|
|
638
617
|
* #1016 —— wire 上**开给调用方**的那一个词,从 core 的两只闭集里取**交集**。
|
|
639
618
|
*
|
|
@@ -802,6 +781,33 @@ export declare function resolveApprovalLeg(input: {
|
|
|
802
781
|
* 「窗偏短」(安全侧,§7.3 的误差方向论证)。 */
|
|
803
782
|
nowMonotonicMs: number;
|
|
804
783
|
}): ApprovalLegAssembly;
|
|
784
|
+
/** 一条 **DECIDED 行**能交还给引擎的全部形(`AskOutcome` 减掉 park 路由那一臂 —— 一条有决议的行
|
|
785
|
+
* 从定义上不是「投递面拿走了它」)。{@link ToolApprovalCoordinator.notifyExternalDecision} 收的就是它。 */
|
|
786
|
+
export type DecidedAskOutcome = Exclude<AskOutcome, "unavailable">;
|
|
787
|
+
/** 铸点的入参形 = 行上与**这条决议本身**有关的三格。整行(`AskRow`)与 claim 败方读数都满足。 */
|
|
788
|
+
type DecidedRowFacts = Pick<AskRow, "decision" | "settledBy"> & {
|
|
789
|
+
updatedInput?: unknown;
|
|
790
|
+
};
|
|
791
|
+
/**
|
|
792
|
+
* 🔴 **一条 DECIDED 行 → 交还给引擎的那个 outcome:全仓唯一铸点**(S-558)。
|
|
793
|
+
*
|
|
794
|
+
* 存在理由 = 这条投影此前被**手抄**在每个消费点上(`row.decision === "approve"` + `row.updatedInput`,
|
|
795
|
+
* 六处),于是给行加一格新事实就得记得改六处 —— `settledBy` 正是那个没人改到的格:一次**策略拒**在
|
|
796
|
+
* 重放 / 兄弟广播 / 歧义提交恢复三条路上全都退化成裸 `false`,而裸 `false` 在 core 那边逐字是
|
|
797
|
+
* 「**一个人**拒绝了」。手抄投影全面下线([ref]):消费点交行,形由这里铸。
|
|
798
|
+
*
|
|
799
|
+
* 三条读法,每条的属主注在别处,这里只落笔:
|
|
800
|
+
* · **方向** —— `decision !== "approve"` 即拒(列的二值由店的 CAS 保证);
|
|
801
|
+
* · **编辑载荷**([ref])—— `updatedInput` 键缺席 = 没带编辑 ⇒ 裸 `true`;在场(**含 `null`**)= 编辑的
|
|
802
|
+
* 字面值 ⇒ 对象臂(语义三态的唯一属主 = `AskRow.updatedInput` 顶注);
|
|
803
|
+
* · **结算来源**(S-558)—— 行上非 null 即宿主自报的那个词(`null` = 人决,与「不铸这个键」同义;
|
|
804
|
+
* 语义属主 = `AskRow.settledBy` 顶注)。
|
|
805
|
+
*
|
|
806
|
+
* 🔴 approve 臂**不读**来源位,而且那不是「静默丢」:store 的入店形门(`assertDecideSettledBy`)让
|
|
807
|
+
* 「approve 行带来源位」在**写面不可表示** —— core 对 `{allow:true, settledBy}` 是 fail-closed 拒,
|
|
808
|
+
* 一条那样的行没有任何读回路径能诚实地交还给引擎,所以它在落库那一刻就被拒掉了,而不是在这里被吞掉。
|
|
809
|
+
*/
|
|
810
|
+
export declare function outcomeOfDecidedRow(row: DecidedRowFacts): DecidedAskOutcome;
|
|
805
811
|
/**
|
|
806
812
|
* Coordinates the live tool-approval HITL for the singleton runner. Process-local + same-replica (the pending map is
|
|
807
813
|
* in memory, like QuestionCoordinator): a respond that lands on another replica finds nothing → 404. Present (passed
|
|
@@ -1194,7 +1200,7 @@ export declare class ToolApprovalCoordinator {
|
|
|
1194
1200
|
* 真决议」,重复注册必须拿到**同一个**决议(approve 就是 approve)——合并会把外部批准的重复条目错
|
|
1195
1201
|
* 结算成拒绝。已被结算过的条目再调 settle 天然无操作(幂等),所以本口对「赢家自己已 settle」安全。
|
|
1196
1202
|
*/
|
|
1197
|
-
notifyExternalDecision(askId: string,
|
|
1203
|
+
notifyExternalDecision(askId: string, outcome: DecidedAskOutcome): {
|
|
1198
1204
|
settled: number;
|
|
1199
1205
|
};
|
|
1200
1206
|
/**
|
package/dist/tool-approval.js
CHANGED
|
@@ -84,6 +84,9 @@ export const PERSIST_RULE_EDITED_FLAG_ERROR = "persistRule.edited must be a bool
|
|
|
84
84
|
export const PERSIST_RULE_BATCH_INDEX_ERROR = "persistRule.batchOfferIndex must be a non-negative integer when present";
|
|
85
85
|
export const PERSIST_RULE_BATCH_EXCLUSIVE_ERROR = "persistRule.batchOfferIndex is mutually exclusive with persistRule.rule / persistRule.edited — send exactly one arm";
|
|
86
86
|
export const SETTLED_BY_MUST_BE_STRING = "settledBy must be a string — the settlement source is one word from the engine's closed set, and this layer judges only the SHAPE (which word it is, the engine decides)";
|
|
87
|
+
function respondSettledBy(parsed) {
|
|
88
|
+
return parsed.settledBy;
|
|
89
|
+
}
|
|
87
90
|
export const SETTLED_BY_DENY_ONLY = 'settledBy is only meaningful with decision "deny" — only a person approves an action that EXECUTES, so a self-reported settlement source on an allow is a contradiction the engine refuses; the refusal would arrive too late to undo this door\'s own side effects (a session allow-all grant, a persisted rule, a DECIDED row that replays as a plain approval), so it is refused here, before the decision';
|
|
88
91
|
export const PERSIST_RULE_SCOPE_REFUSED = "this endpoint does not accept a rule scope — the server mints it from where the approval happened (send neither `scope` nor `persistRule.scope`)";
|
|
89
92
|
export function parseToolApprovalResponse(body) {
|
|
@@ -222,21 +225,28 @@ function effectiveAskWindowMs(ttlMs, windowMarginMs, legDeadlineMonotonic, nowMo
|
|
|
222
225
|
return Math.min(ttlMs, Math.max(0, legRemainingMs - windowMarginMs));
|
|
223
226
|
}
|
|
224
227
|
function replayTerminalAskRow(row) {
|
|
225
|
-
if (row.state === "DECIDED")
|
|
226
|
-
|
|
227
|
-
return false;
|
|
228
|
-
return row.updatedInput !== undefined ? { allow: true, updatedInput: row.updatedInput } : true;
|
|
229
|
-
}
|
|
228
|
+
if (row.state === "DECIDED")
|
|
229
|
+
return outcomeOfDecidedRow(row);
|
|
230
230
|
if (row.state === "PARKING" || row.state === "PARKED")
|
|
231
231
|
return "unavailable";
|
|
232
232
|
return false;
|
|
233
233
|
}
|
|
234
|
-
function
|
|
235
|
-
if (
|
|
236
|
-
return
|
|
234
|
+
export function outcomeOfDecidedRow(row) {
|
|
235
|
+
if (row.decision !== "approve")
|
|
236
|
+
return row.settledBy !== null ? { allow: false, settledBy: row.settledBy } : false;
|
|
237
|
+
return row.updatedInput !== undefined ? { allow: true, updatedInput: row.updatedInput } : true;
|
|
238
|
+
}
|
|
239
|
+
function settleArgsOfDecided(outcome) {
|
|
237
240
|
if (typeof outcome === "boolean")
|
|
238
241
|
return { allowed: outcome };
|
|
239
|
-
return {
|
|
242
|
+
return {
|
|
243
|
+
allowed: outcome.allow,
|
|
244
|
+
...(outcome.updatedInput !== undefined ? { updatedInput: outcome.updatedInput } : {}),
|
|
245
|
+
...(outcome.settledBy !== undefined ? { settledBy: outcome.settledBy } : {}),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function settleArgsOf(outcome) {
|
|
249
|
+
return outcome === "unavailable" ? "unavailable" : settleArgsOfDecided(outcome);
|
|
240
250
|
}
|
|
241
251
|
export class ToolApprovalCoordinator {
|
|
242
252
|
als = new AsyncLocalStorage();
|
|
@@ -616,16 +626,17 @@ export class ToolApprovalCoordinator {
|
|
|
616
626
|
classifyClaimLoss(askId, current, _where) {
|
|
617
627
|
if (current.state === "absent")
|
|
618
628
|
return { kind: "absent" };
|
|
619
|
-
const
|
|
620
|
-
if (
|
|
629
|
+
const replayedOutcome = replayTerminalAskRow(current);
|
|
630
|
+
if (replayedOutcome === "unavailable")
|
|
621
631
|
return { kind: "parked" };
|
|
632
|
+
const replayed = settleArgsOfDecided(replayedOutcome);
|
|
622
633
|
if (!replayed.allowed)
|
|
623
|
-
return { kind: "
|
|
634
|
+
return { kind: "decided", outcome: replayedOutcome };
|
|
624
635
|
if (replayed.updatedInput !== undefined)
|
|
625
|
-
return { kind: "
|
|
636
|
+
return { kind: "decided", outcome: replayedOutcome };
|
|
626
637
|
if (this.editedDecisionInFlight(askId))
|
|
627
638
|
return { kind: "edit-in-flight" };
|
|
628
|
-
return { kind: "
|
|
639
|
+
return { kind: "decided", outcome: replayedOutcome };
|
|
629
640
|
}
|
|
630
641
|
hasOtherSuspendedOwner(askId, self) {
|
|
631
642
|
if (askId === undefined)
|
|
@@ -713,13 +724,14 @@ export class ToolApprovalCoordinator {
|
|
|
713
724
|
}
|
|
714
725
|
return tomb;
|
|
715
726
|
}
|
|
716
|
-
notifyExternalDecision(askId,
|
|
727
|
+
notifyExternalDecision(askId, outcome) {
|
|
717
728
|
const entries = this.pendingByAskId.get(askId);
|
|
718
729
|
if (!entries)
|
|
719
730
|
return { settled: 0 };
|
|
731
|
+
const args = settleArgsOfDecided(outcome);
|
|
720
732
|
let settled = 0;
|
|
721
733
|
for (const entry of [...entries]) {
|
|
722
|
-
entry.settle(allowed, allowed ? "allowed" : "denied", updatedInput);
|
|
734
|
+
entry.settle(args.allowed, args.allowed ? "allowed" : "denied", args.updatedInput, args.settledBy);
|
|
723
735
|
settled += 1;
|
|
724
736
|
}
|
|
725
737
|
return { settled };
|
|
@@ -1091,11 +1103,8 @@ export class ToolApprovalCoordinator {
|
|
|
1091
1103
|
if (askId === undefined)
|
|
1092
1104
|
return;
|
|
1093
1105
|
switch (read.kind) {
|
|
1094
|
-
case "
|
|
1095
|
-
this.notifyExternalDecision(askId,
|
|
1096
|
-
return;
|
|
1097
|
-
case "denied":
|
|
1098
|
-
this.notifyExternalDecision(askId, false);
|
|
1106
|
+
case "decided":
|
|
1107
|
+
this.notifyExternalDecision(askId, read.outcome);
|
|
1099
1108
|
return;
|
|
1100
1109
|
case "parked":
|
|
1101
1110
|
this.settleSameAskIdParkRoute(askId);
|
|
@@ -1111,14 +1120,12 @@ export class ToolApprovalCoordinator {
|
|
|
1111
1120
|
};
|
|
1112
1121
|
const settleFromClaimLoss = (read) => {
|
|
1113
1122
|
switch (read.kind) {
|
|
1114
|
-
case "
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
return true;
|
|
1118
|
-
case "denied":
|
|
1119
|
-
settle(false, "denied");
|
|
1123
|
+
case "decided": {
|
|
1124
|
+
const args = settleArgsOfDecided(read.outcome);
|
|
1125
|
+
settle(args.allowed, args.allowed ? "allowed" : "denied", args.updatedInput, args.settledBy);
|
|
1120
1126
|
broadcastClaimTruth(read);
|
|
1121
1127
|
return true;
|
|
1128
|
+
}
|
|
1122
1129
|
case "parked":
|
|
1123
1130
|
windowRouteUnavailable = true;
|
|
1124
1131
|
settle(false, "expired");
|
|
@@ -1622,7 +1629,7 @@ export class ToolApprovalCoordinator {
|
|
|
1622
1629
|
});
|
|
1623
1630
|
return this.notPending();
|
|
1624
1631
|
}
|
|
1625
|
-
this.notifyExternalDecision(row.askId, row
|
|
1632
|
+
this.notifyExternalDecision(row.askId, outcomeOfDecidedRow(row));
|
|
1626
1633
|
return {
|
|
1627
1634
|
status: 200,
|
|
1628
1635
|
body: {
|
|
@@ -1895,21 +1902,23 @@ export class ToolApprovalCoordinator {
|
|
|
1895
1902
|
}
|
|
1896
1903
|
}
|
|
1897
1904
|
async respondWithCas(askStore, askId, batchId, id, entry, parsed, ruleEligibility) {
|
|
1898
|
-
let
|
|
1899
|
-
let
|
|
1905
|
+
let winnerRow;
|
|
1906
|
+
let ambiguousConfirmedRow;
|
|
1900
1907
|
let noteLanded = false;
|
|
1901
1908
|
try {
|
|
1902
1909
|
const decided = await this.withStoreDeadline(askStore.decideAsk(askId, batchId, {
|
|
1903
1910
|
decision: parsed.value === "deny" ? "deny" : "approve",
|
|
1904
1911
|
...(parsed.note !== undefined ? { decisionNote: parsed.note } : {}),
|
|
1905
1912
|
...(parsed.value !== "deny" && parsed.updatedInput !== undefined ? { updatedInput: parsed.updatedInput } : {}),
|
|
1913
|
+
...(parsed.settledBy !== undefined ? { settledBy: respondSettledBy(parsed) } : {}),
|
|
1906
1914
|
}), "decideAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
|
|
1907
1915
|
if (late.ok)
|
|
1908
|
-
this.notifyExternalDecision(askId,
|
|
1916
|
+
this.notifyExternalDecision(askId, outcomeOfDecidedRow(late.row));
|
|
1909
1917
|
});
|
|
1910
|
-
|
|
1911
|
-
|
|
1918
|
+
if (decided.ok) {
|
|
1919
|
+
winnerRow = decided.row;
|
|
1912
1920
|
noteLanded = true;
|
|
1921
|
+
}
|
|
1913
1922
|
if (!decided.ok) {
|
|
1914
1923
|
return this.notPending();
|
|
1915
1924
|
}
|
|
@@ -1920,8 +1929,8 @@ export class ToolApprovalCoordinator {
|
|
|
1920
1929
|
try {
|
|
1921
1930
|
const row = await this.withStoreDeadline(askStore.getAsk(askId), "getAsk(decideAsk-confirm)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
|
|
1922
1931
|
if (row?.state === "DECIDED" && row.decision === (parsed.value === "deny" ? "deny" : "approve")) {
|
|
1923
|
-
|
|
1924
|
-
|
|
1932
|
+
winnerRow = row;
|
|
1933
|
+
ambiguousConfirmedRow = row;
|
|
1925
1934
|
noteLanded = parsed.note === undefined || row.decisionNote === parsed.note;
|
|
1926
1935
|
}
|
|
1927
1936
|
}
|
|
@@ -1930,22 +1939,25 @@ export class ToolApprovalCoordinator {
|
|
|
1930
1939
|
}
|
|
1931
1940
|
}
|
|
1932
1941
|
const wantAllowed = parsed.value !== "deny";
|
|
1942
|
+
const decidedWon = winnerRow !== undefined;
|
|
1933
1943
|
if (this.pending.get(id) !== entry) {
|
|
1934
|
-
if (
|
|
1935
|
-
this.notifyExternalDecision(askId,
|
|
1944
|
+
if (winnerRow !== undefined)
|
|
1945
|
+
this.notifyExternalDecision(askId, outcomeOfDecidedRow(winnerRow));
|
|
1936
1946
|
const converged = decidedWon && entry.settledOutcome === (wantAllowed ? "allowed" : "denied");
|
|
1937
1947
|
if (!converged) {
|
|
1938
1948
|
return this.notPending();
|
|
1939
1949
|
}
|
|
1940
1950
|
return this.finishRespond(id, entry, parsed, { updatedInputDelivered: false, noteRecorded: noteLanded });
|
|
1941
1951
|
}
|
|
1942
|
-
const payloadIsMine =
|
|
1943
|
-
approvalPayloadFingerprint({ updatedInput:
|
|
1952
|
+
const payloadIsMine = ambiguousConfirmedRow === undefined ||
|
|
1953
|
+
approvalPayloadFingerprint({ updatedInput: ambiguousConfirmedRow.updatedInput }) === approvalPayloadFingerprint({ updatedInput: parsed.updatedInput });
|
|
1944
1954
|
const result = this.finishRespond(id, entry, parsed, {
|
|
1945
1955
|
noteRecorded: noteLanded,
|
|
1946
|
-
...(
|
|
1956
|
+
...(winnerRow !== undefined ? { settleOutcome: outcomeOfDecidedRow(winnerRow) } : {}),
|
|
1957
|
+
...(ambiguousConfirmedRow !== undefined ? { updatedInputDelivered: payloadIsMine } : {}),
|
|
1947
1958
|
});
|
|
1948
|
-
|
|
1959
|
+
if (winnerRow !== undefined)
|
|
1960
|
+
this.notifyExternalDecision(askId, outcomeOfDecidedRow(winnerRow));
|
|
1949
1961
|
return result;
|
|
1950
1962
|
}
|
|
1951
1963
|
finishRespond(id, entry, parsed, opts) {
|
|
@@ -1962,7 +1974,8 @@ export class ToolApprovalCoordinator {
|
|
|
1962
1974
|
this.allowAllSessions.add(sessionAllowKey(entry.owner, entry.sessionId, sessionAllowCategory(entry.toolName)));
|
|
1963
1975
|
}
|
|
1964
1976
|
const allowed = parsed.value !== "deny";
|
|
1965
|
-
|
|
1977
|
+
const settleArgs = opts?.settleOutcome !== undefined ? settleArgsOfDecided(opts.settleOutcome) : { allowed, updatedInput: parsed.updatedInput, settledBy: respondSettledBy(parsed) };
|
|
1978
|
+
entry.settle(settleArgs.allowed, settleArgs.allowed ? "allowed" : "denied", settleArgs.updatedInput, settleArgs.settledBy, settleArgs.allowed ? undefined : parsed.note);
|
|
1966
1979
|
const updatedInputDelivered = opts?.updatedInputDelivered ?? true;
|
|
1967
1980
|
const noteRecorded = opts?.noteRecorded ?? false;
|
|
1968
1981
|
return {
|
|
@@ -2035,14 +2048,14 @@ export class ToolApprovalCoordinator {
|
|
|
2035
2048
|
continue;
|
|
2036
2049
|
if (this.editedDecisionInFlight(askId))
|
|
2037
2050
|
continue;
|
|
2038
|
-
const outcome =
|
|
2051
|
+
const outcome = replayTerminalAskRow(row);
|
|
2039
2052
|
if (outcome === "unavailable") {
|
|
2040
2053
|
const before = this.pendingByAskId.get(askId)?.size ?? 0;
|
|
2041
2054
|
this.settleSameAskIdParkRoute(askId);
|
|
2042
2055
|
settled += before;
|
|
2043
2056
|
continue;
|
|
2044
2057
|
}
|
|
2045
|
-
const delivered = this.notifyExternalDecision(askId, outcome
|
|
2058
|
+
const delivered = this.notifyExternalDecision(askId, outcome).settled;
|
|
2046
2059
|
settled += delivered;
|
|
2047
2060
|
}
|
|
2048
2061
|
const backlog = Math.max(0, candidates.length - polled);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.93.
|
|
3
|
+
"version": "7.93.4",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"sharp": "^0.35.3"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@sema-agent/sdk": "11.
|
|
77
|
+
"@sema-agent/sdk": "11.3.0",
|
|
78
78
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
79
79
|
"@types/node": "22.10.2",
|
|
80
80
|
"@types/pg": "^8.20.0",
|