@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.
@@ -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";
@@ -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.decision === "approve", row.updatedInput);
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.decision === "approve", confirmed.updatedInput).settled;
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.decision === "approve", outcome.row.updatedInput);
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.decision === "approve", outcome.row.updatedInput).settled ?? 0;
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)}, updated_input=${ph(dialect, 4)}, decided_at_ms=${ph(dialect, 5)}, idempotency_key=${ph(dialect, 6)}, rev=rev+1, updated_at_ms=${ph(dialect, 7)} WHERE ${pendingClaimWhere(dialect, 8)}`, [
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,
@@ -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, allowed: boolean, updatedInput?: unknown): {
1203
+ notifyExternalDecision(askId: string, outcome: DecidedAskOutcome): {
1198
1204
  settled: number;
1199
1205
  };
1200
1206
  /**
@@ -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
- if (row.decision !== "approve")
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 settleArgsOf(outcome) {
235
- if (outcome === "unavailable")
236
- return "unavailable";
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 { allowed: outcome.allow, ...(outcome.updatedInput !== undefined ? { updatedInput: outcome.updatedInput } : {}) };
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 replayed = settleArgsOf(replayTerminalAskRow(current));
620
- if (replayed === "unavailable")
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: "denied" };
634
+ return { kind: "decided", outcome: replayedOutcome };
624
635
  if (replayed.updatedInput !== undefined)
625
- return { kind: "approved", updatedInput: replayed.updatedInput };
636
+ return { kind: "decided", outcome: replayedOutcome };
626
637
  if (this.editedDecisionInFlight(askId))
627
638
  return { kind: "edit-in-flight" };
628
- return { kind: "approved" };
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, allowed, updatedInput) {
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 "approved":
1095
- this.notifyExternalDecision(askId, true, read.updatedInput);
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 "approved":
1115
- settle(true, "allowed", read.updatedInput);
1116
- broadcastClaimTruth(read);
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.decision === "approve", row.updatedInput);
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 decidedWon = false;
1899
- let confirmedRowPayload;
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, parsed.value !== "deny", parsed.updatedInput);
1916
+ this.notifyExternalDecision(askId, outcomeOfDecidedRow(late.row));
1909
1917
  });
1910
- decidedWon = decided.ok;
1911
- if (decided.ok)
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
- decidedWon = true;
1924
- confirmedRowPayload = { updatedInput: row.updatedInput };
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 (decidedWon)
1935
- this.notifyExternalDecision(askId, wantAllowed, confirmedRowPayload !== undefined ? confirmedRowPayload.updatedInput : parsed.updatedInput);
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 = confirmedRowPayload === undefined ||
1943
- approvalPayloadFingerprint({ updatedInput: confirmedRowPayload.updatedInput }) === approvalPayloadFingerprint({ updatedInput: parsed.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
- ...(confirmedRowPayload !== undefined ? { settlePayload: confirmedRowPayload, updatedInputDelivered: payloadIsMine } : {}),
1956
+ ...(winnerRow !== undefined ? { settleOutcome: outcomeOfDecidedRow(winnerRow) } : {}),
1957
+ ...(ambiguousConfirmedRow !== undefined ? { updatedInputDelivered: payloadIsMine } : {}),
1947
1958
  });
1948
- this.notifyExternalDecision(askId, wantAllowed, confirmedRowPayload !== undefined ? confirmedRowPayload.updatedInput : parsed.updatedInput);
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
- entry.settle(allowed, allowed ? "allowed" : "denied", opts?.settlePayload !== undefined ? opts.settlePayload.updatedInput : parsed.updatedInput, parsed.settledBy, allowed ? undefined : parsed.note);
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 = settleArgsOf(replayTerminalAskRow(row));
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.allowed, outcome.updatedInput).settled;
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",
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.2.1",
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",