@sema-agent/server 7.93.2 → 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/USAGE.md +12 -4
- package/dist/adoption/wire.d.ts +5 -5
- package/dist/approval-ask-audit-store.js +6 -1
- package/dist/boot/shutdown.d.ts +2 -1
- package/dist/boot/shutdown.js +40 -3
- package/dist/host-decision.d.ts +57 -9
- package/dist/host-decision.js +2 -2
- package/dist/http/approval-sweeps.js +2 -2
- package/dist/http/resume-legs.js +4 -1
- package/dist/http/routes/approvals-assistant.js +20 -7
- package/dist/http/routes/memory-origin.d.ts +2 -2
- 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/plugins/host-platform.d.ts +21 -6
- package/dist/plugins/host-platform.js +1 -1
- package/dist/plugins/store-backend.js +4 -2
- package/dist/plugins/store-open-refusal.d.ts +11 -0
- package/dist/plugins/store-open-refusal.js +23 -0
- package/dist/run-local.js +5 -4
- package/dist/tool-approval.d.ts +66 -2
- package/dist/tool-approval.js +67 -44
- package/package.json +3 -3
package/USAGE.md
CHANGED
|
@@ -375,12 +375,20 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
|
|
|
375
375
|
core 5.54.0 起拒启错误带**可分派 code**(`FileStoreLockError`,message 点名占用方 pid),运维按码
|
|
376
376
|
三动作:`store.dir_in_use`=另一个活进程持有该目录 → 停掉它(或换数据根/迁 SQL 后端);
|
|
377
377
|
`store.dir_claiming`=另一进程正在接管崩溃者留下的锁 → **瞬态,稍候重试**(接管方要么完成要么
|
|
378
|
-
自己也被收割);`store.lock_unreadable
|
|
379
|
-
|
|
378
|
+
自己也被收割);`store.lock_unreadable`=锁**读不出可解析的 owner**
|
|
379
|
+
(真坏形:无 owner / 嵌套接管残留;**或者**本进程读不动它 —— core 把一切非 ENOENT 的读失败都折进这一码)
|
|
380
|
+
→ 人工处置,message 会点名要删的东西(⚠️ 三条消息点名的**不是同一个**:`LOCK` / `LOCK.claim.*` / 本进程
|
|
381
|
+
自己那把删不掉的锁),且 core 的句子**带条件** ——「stale 才删」:读不动的那一形背后可能是个**活**写者
|
|
382
|
+
(权限/属主不对),盲删 = 拆掉围栏 ⇒ 两个写者共用一个数据根。**不要盲删数据根下其它东西**。
|
|
383
|
+
server 刻意不把这族错**吞掉**(fail-fast:
|
|
380
384
|
两进程共用 file 数据目录是禁止形,静默容忍比拒启更坏)。
|
|
385
|
+
⚠️ **拒启句本身就带这三条里对应的那一条出路**(不必再回来查这张表);拒启原因**不是**锁冲突时
|
|
386
|
+
(磁盘满 / 权限 / 只读挂载 …)拒启句**不加任何出路猜测** —— 此前它一律拼「另一个实例可能占着,停掉它」,
|
|
387
|
+
把运维送去找一个不存在的进程。HTTP 服务与 `run-local` 共用同一只派生,只有「换一个空目录」那半句按通道
|
|
388
|
+
措辞不同(`LOCAL_DATA_ROOT` / `--root`)。
|
|
381
389
|
⚠️ 自 7.93.1 起 `store.dir_in_use` **这一码**多一道有界等待(见下面 `STORE_LOCK_WAIT_MS` 那条):
|
|
382
|
-
|
|
383
|
-
与非锁类失败**一秒都不等**,原样抛。
|
|
390
|
+
**等待腿本身**不改拒启形、也不改上面那句出路(它只在拒之前先在窗内反复重取,并在拒句里多写一句
|
|
391
|
+
`waited Nms`);另两码(`dir_claiming` / `lock_unreadable`)与非锁类失败**一秒都不等**,原样抛。
|
|
384
392
|
`DB_BACKEND=memory` 并非全无盘:workflow 相关账本仍挂数据根下——多个 memory 形引擎**不要共用**
|
|
385
393
|
数据根/HOME(1.309 起并发 boot 不再拒启,但账本内容级共享仍不受支持)。
|
|
386
394
|
- **`STORE_LOCK_WAIT_MS`(缺省 `60000`,单位 ms)—— 撞上活持锁人时等一会儿再拒**:
|
package/dist/adoption/wire.d.ts
CHANGED
|
@@ -28,8 +28,8 @@ export declare const AdoptionSourceSchema: z.ZodObject<{
|
|
|
28
28
|
export type AdoptionSource = z.infer<typeof AdoptionSourceSchema>;
|
|
29
29
|
/** per-store 腿的动作词(183 §6 闭集;form b 只会产出前两个 + `none`)。 */
|
|
30
30
|
export declare const AdoptionLegActionSchema: z.ZodEnum<{
|
|
31
|
-
reset: "reset";
|
|
32
31
|
none: "none";
|
|
32
|
+
reset: "reset";
|
|
33
33
|
"bucket-rebind": "bucket-rebind";
|
|
34
34
|
"row-rewrite": "row-rewrite";
|
|
35
35
|
carried: "carried";
|
|
@@ -38,8 +38,8 @@ export type AdoptionLegAction = z.infer<typeof AdoptionLegActionSchema>;
|
|
|
38
38
|
export declare const AdoptionLegSchema: z.ZodObject<{
|
|
39
39
|
store: z.ZodString;
|
|
40
40
|
action: z.ZodEnum<{
|
|
41
|
-
reset: "reset";
|
|
42
41
|
none: "none";
|
|
42
|
+
reset: "reset";
|
|
43
43
|
"bucket-rebind": "bucket-rebind";
|
|
44
44
|
"row-rewrite": "row-rewrite";
|
|
45
45
|
carried: "carried";
|
|
@@ -109,8 +109,8 @@ export declare const AdoptionReportSchema: z.ZodObject<{
|
|
|
109
109
|
legs: z.ZodArray<z.ZodObject<{
|
|
110
110
|
store: z.ZodString;
|
|
111
111
|
action: z.ZodEnum<{
|
|
112
|
-
reset: "reset";
|
|
113
112
|
none: "none";
|
|
113
|
+
reset: "reset";
|
|
114
114
|
"bucket-rebind": "bucket-rebind";
|
|
115
115
|
"row-rewrite": "row-rewrite";
|
|
116
116
|
carried: "carried";
|
|
@@ -170,8 +170,8 @@ export declare const AdoptionReceiptSchema: z.ZodObject<{
|
|
|
170
170
|
legs: z.ZodArray<z.ZodObject<{
|
|
171
171
|
store: z.ZodString;
|
|
172
172
|
action: z.ZodEnum<{
|
|
173
|
-
reset: "reset";
|
|
174
173
|
none: "none";
|
|
174
|
+
reset: "reset";
|
|
175
175
|
"bucket-rebind": "bucket-rebind";
|
|
176
176
|
"row-rewrite": "row-rewrite";
|
|
177
177
|
carried: "carried";
|
|
@@ -214,8 +214,8 @@ export type AdoptionReceipt = z.infer<typeof AdoptionReceiptSchema>;
|
|
|
214
214
|
export declare const AdoptionLegsSchema: z.ZodArray<z.ZodObject<{
|
|
215
215
|
store: z.ZodString;
|
|
216
216
|
action: z.ZodEnum<{
|
|
217
|
-
reset: "reset";
|
|
218
217
|
none: "none";
|
|
218
|
+
reset: "reset";
|
|
219
219
|
"bucket-rebind": "bucket-rebind";
|
|
220
220
|
"row-rewrite": "row-rewrite";
|
|
221
221
|
carried: "carried";
|
|
@@ -7,6 +7,11 @@ export const CRASH_CONVERGED_LIST_MAX = 200;
|
|
|
7
7
|
export function buildCheckpointAnchorHash(token) {
|
|
8
8
|
return createHash("sha256").update(token, "utf8").digest("hex").slice(0, 16);
|
|
9
9
|
}
|
|
10
|
+
const HOST_DECIDED_BY_WORDS = ["person", "sla_timeout", "policy"];
|
|
11
|
+
const HOST_DECIDED_BY = new Set(HOST_DECIDED_BY_WORDS);
|
|
12
|
+
function isHostDecidedBy(v) {
|
|
13
|
+
return typeof v === "string" && HOST_DECIDED_BY.has(v);
|
|
14
|
+
}
|
|
10
15
|
function readLedgerRecord(u) {
|
|
11
16
|
if (u === null || typeof u !== "object" || Array.isArray(u))
|
|
12
17
|
return undefined;
|
|
@@ -87,7 +92,7 @@ function readLedgerRecord(u) {
|
|
|
87
92
|
const decidedByV = o.decidedBy;
|
|
88
93
|
const legacySettledByV = decidedByV === undefined ? o.settledBy : undefined;
|
|
89
94
|
const legacySettledBy = legacySettledByV === "human" || legacySettledByV === "timeout" || legacySettledByV === "aborted" ? legacySettledByV : undefined;
|
|
90
|
-
const decidedBy = decidedByV
|
|
95
|
+
const decidedBy = isHostDecidedBy(decidedByV) ? decidedByV : legacySettledBy !== undefined ? "person" : undefined;
|
|
91
96
|
const approver = s("approver");
|
|
92
97
|
const settledAtMs = num("settledAtMs");
|
|
93
98
|
const sessionId = s("sessionId");
|
package/dist/boot/shutdown.d.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* "第二个信号"的语义读数。
|
|
13
13
|
* 3. `clearInterval(reaper)` 必须是 hardShutdown 的第一件事(在 `server.close()` 之前),否则
|
|
14
14
|
* 收尾期还会有 reaper tick 打向正在关闭的池。
|
|
15
|
-
* 4. **进程级监听一律成对**([ref] 二轮件4)
|
|
15
|
+
* 4. **进程级监听一律成对**([ref] 二轮件4):本函数装的六只(三信号 + [ref]① 双钩 + S-557 退出边界钩)
|
|
16
|
+
* 只能经返回的
|
|
16
17
|
* {@link ShutdownHandle} 摘 —— 调用方手写「摘三信号」的收尾会把双钩漏在进程上,而陈旧的那只
|
|
17
18
|
* rejection 钩仍握着 `process.exit(1)`,会对后续无关的 rejection 把进程打掉。
|
|
18
19
|
*/
|
package/dist/boot/shutdown.js
CHANGED
|
@@ -2,7 +2,8 @@ import { Runner, defaultTaskRegistry } from "@sema-agent/core";
|
|
|
2
2
|
import { writeCrashLast, clearCrashLast, readCrashLast } from "./crash-last.js";
|
|
3
3
|
import { createSighupIdleHandler } from "../sighup-idle.js";
|
|
4
4
|
import { createParentWatch } from "../parent-watch.js";
|
|
5
|
-
import { reapHostForegroundChildren } from "../plugins/host-platform.js";
|
|
5
|
+
import { liveHostForegroundChildCount, reapHostForegroundChildren } from "../plugins/host-platform.js";
|
|
6
|
+
import { liveEphemeralHostWorkspaceCount, reapEphemeralHostWorkspaces } from "../plugins/host-workspace-registry.js";
|
|
6
7
|
import { createEngineLeaseTracker } from "./engine-lease.js";
|
|
7
8
|
import { writeExitLast, readExitLast, clearExitLast } from "./exit-last.js";
|
|
8
9
|
import { isScriptRealmRejection, describeRejectionReason } from "../orchestration/hardened-vm-runner.js";
|
|
@@ -51,9 +52,45 @@ export function installShutdownHandlers(ctx) {
|
|
|
51
52
|
logger.warn("shutdown_foreground_reap_failed", { err: String(err) });
|
|
52
53
|
}
|
|
53
54
|
};
|
|
54
|
-
const
|
|
55
|
+
const reapOwnHostWorkspaces = () => {
|
|
56
|
+
try {
|
|
57
|
+
const { removed, failed } = reapEphemeralHostWorkspaces();
|
|
58
|
+
if (removed.length > 0)
|
|
59
|
+
logger.info("shutdown_host_workspaces_reaped", { removed: removed.length });
|
|
60
|
+
if (failed.length > 0)
|
|
61
|
+
logger.warn("shutdown_host_workspace_reap_incomplete", { dirs: failed.map((f) => f.dir) });
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
logger.warn("shutdown_host_workspace_reap_failed", { err: String(err) });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const onProcessExit = () => {
|
|
68
|
+
const onBooks = { foregroundChildren: liveHostForegroundChildCount(), hostWorkspaces: liveEphemeralHostWorkspaceCount() };
|
|
55
69
|
try {
|
|
56
70
|
reapOwnChildProcesses();
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error("shutdown_exit_boundary_child_reap_threw", err);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
reapOwnHostWorkspaces();
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.error("shutdown_exit_boundary_workspace_reap_threw", err);
|
|
80
|
+
}
|
|
81
|
+
const left = { foregroundChildren: liveHostForegroundChildCount(), hostWorkspaces: liveEphemeralHostWorkspaceCount() };
|
|
82
|
+
try {
|
|
83
|
+
logger.info("shutdown_exit_boundary", { ...onBooks, leftForegroundChildren: left.foregroundChildren, leftHostWorkspaces: left.hostWorkspaces });
|
|
84
|
+
if (left.foregroundChildren > 0 || left.hostWorkspaces > 0)
|
|
85
|
+
logger.warn("shutdown_exit_boundary_incomplete", left);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
console.error("shutdown_exit_boundary_log_threw", err);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
process.once("exit", onProcessExit);
|
|
92
|
+
const onUncaughtException = (err) => {
|
|
93
|
+
try {
|
|
57
94
|
writeLastWords(err);
|
|
58
95
|
console.error(err);
|
|
59
96
|
}
|
|
@@ -72,7 +109,6 @@ export function installShutdownHandlers(ctx) {
|
|
|
72
109
|
return;
|
|
73
110
|
}
|
|
74
111
|
try {
|
|
75
|
-
reapOwnChildProcesses();
|
|
76
112
|
writeLastWords(err);
|
|
77
113
|
console.error(err);
|
|
78
114
|
}
|
|
@@ -212,6 +248,7 @@ export function installShutdownHandlers(ctx) {
|
|
|
212
248
|
if (disposed)
|
|
213
249
|
return;
|
|
214
250
|
disposed = true;
|
|
251
|
+
process.removeListener("exit", onProcessExit);
|
|
215
252
|
process.removeListener("uncaughtException", onUncaughtException);
|
|
216
253
|
process.removeListener("unhandledRejection", onUnhandledRejection);
|
|
217
254
|
process.removeListener("SIGTERM", drainThenShutdown);
|
package/dist/host-decision.d.ts
CHANGED
|
@@ -14,26 +14,74 @@
|
|
|
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` 腿);
|
|
21
21
|
* · `decidedBy: "sla_timeout"` —— 宿主自己的 SLA 扫在行的 deadline 上判的(FACET A;**只有** sweep 能这么说,
|
|
22
22
|
* 而 sweep 结构上不能 `allow`)。
|
|
23
|
+
* · `decidedBy: "policy"`(core 7.26.2 / #1016)—— **这台部署自己的策略**拒的:答话的是机器不是人。调用方
|
|
24
|
+
* 经决裁体的 `settledBy` 自报;core 据它给模型铸一句**策略句**(而不是「用户不同意,停下来等他」),
|
|
25
|
+
* 并把结算记成 `policy_refused`(`who.party:"none"`)。`allow` 侧结构上不可(core pre-CAS 拒:只有人批准)。
|
|
23
26
|
* · `approver` —— 通道自报的结算方标识串。**缺席永不读成**「没人批」或「有人批」,只意味着通道没报名字。
|
|
24
27
|
*/
|
|
25
|
-
/**
|
|
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"]>;
|
|
55
|
+
/** 直接取 core `ResumeOutcome` policy_ask 臂的 `hostDecision` 型 —— 本仓不复述那几个词(第二铸点)。 */
|
|
26
56
|
export type HostDecision = Extract<ResumeOutcome, {
|
|
27
57
|
gate: "policy_ask";
|
|
28
58
|
}>["hostDecision"];
|
|
59
|
+
/** **谁结束了这次等待**的闭集,型从 core 的那一格派生(本仓永不抄词表:core 加/退一个词,读它的每一处
|
|
60
|
+
* 编译期先红)。今天 = `"person"` | `"sla_timeout"` | `"policy"`。 */
|
|
61
|
+
export type HostDecidedBy = HostDecision["decidedBy"];
|
|
29
62
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
63
|
+
* 一次 decide 的事实,**一处成形**(两条 HTTP decide 腿 + 活卡 PARKED 赎回席共用)。
|
|
64
|
+
*
|
|
65
|
+
* `decidedBy` = 调用方**陈述**的决策者(缺席 ⇒ `"person"`,= 本面自古以来的形:一条不说话的 decide 就是
|
|
66
|
+
* 一个人按的)。🔴 **本仓零判据**:词表的属主是 core,不认识的词由它 pre-CAS 响亮拒
|
|
67
|
+
* (`checkpoint.invalid_outcome`,`hostDecision.decidedBy … is outside the closed set`)。在这里再判一次
|
|
68
|
+
* 就是同一语义面的第二个判官(源头修复纪律 [ref]),而且本仓抄一份词表的那天 core 加第四个词就会被
|
|
69
|
+
* 静默 400 掉 —— 那正是 #1016 的 `"policy"` 加进来时本可以发生的事。
|
|
70
|
+
*
|
|
71
|
+
* `principal` = 那条腿已经验过身份的决策者(缺席 = 匿名档的部署,通道确实没有名字可报)。
|
|
72
|
+
* 归属串走 core 的 `screenApproverAttribution` **先筛后带**:core 对形不合的归属是 pre-CAS 响亮拒,
|
|
73
|
+
* 而一个奇形怪状的 principal 把一次合法的决策变成 400 是纯回归。筛不过 ⇒ **不带这一格**(而不是带一个
|
|
74
|
+
* 被改造过的串):归属是转录,不是可以「修一修」的东西 —— 修出来的名字既不是通道报的那个,也不是
|
|
75
|
+
* 「通道没报」,两头都不真。
|
|
76
|
+
*
|
|
77
|
+
* 🔴 归属**不按词分岔**:core 的 `policy_refused` 结算记录上确实没有归属席(`who.party:"none"`),但那是
|
|
78
|
+
* **core 的记录**怎么写,不是**本服务观察到的事实**是什么 —— 送出这次请求的那把已验证身份是真的,本仓的
|
|
79
|
+
* 迟到结算账本(`approval-ask-audit-store.ts`)记的就是它。按词删一格 = 给账本挖一个只在某个词上出现的洞。
|
|
32
80
|
*
|
|
33
|
-
*
|
|
34
|
-
* (
|
|
35
|
-
*
|
|
36
|
-
*
|
|
81
|
+
* 🔴 **入参是 `string`,不是 `HostDecidedBy` —— 这一格刻意让类型说真话**(wire 边界强类型宪法):两条
|
|
82
|
+
* decide 腿能证得出的只有「调用方给了个串」(形门在各自的口上),**证不出**它是词表成员;把参数声明成
|
|
83
|
+
* 闭集就是让类型替一件本层从没做过的检查背书。⇒ 收 `string`,在**这一处**(本座的边界)做唯一一次 cast,
|
|
84
|
+
* 而那次 cast 说的是「我把一个未判词的串放到只收词的座上,判词的是座的属主(core,pre-CAS 响亮拒)」。
|
|
37
85
|
*/
|
|
38
|
-
export declare function
|
|
86
|
+
export declare function hostDecisionFacts(decidedBy: string | undefined, principal: string | undefined): HostDecision;
|
|
39
87
|
//# sourceMappingURL=host-decision.d.ts.map
|
package/dist/host-decision.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { screenApproverAttribution } from "@sema-agent/core";
|
|
2
|
-
export function
|
|
2
|
+
export function hostDecisionFacts(decidedBy, principal) {
|
|
3
3
|
const screened = screenApproverAttribution(principal);
|
|
4
|
-
return { decidedBy: "person", ...(screened.approver !== undefined ? { approver: screened.approver } : {}) };
|
|
4
|
+
return { decidedBy: (decidedBy ?? "person"), ...(screened.approver !== undefined ? { approver: screened.approver } : {}) };
|
|
5
5
|
}
|
|
6
6
|
//# sourceMappingURL=host-decision.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { redactHead } from "../observability/run-terminal.js";
|
|
2
2
|
import { findParkOrigin } from "../parked-decide.js";
|
|
3
3
|
import { decodeCheckpointScope } from "../security.js";
|
|
4
|
-
import {
|
|
4
|
+
import { hostDecisionFacts } from "../host-decision.js";
|
|
5
5
|
import { APPROVAL_GATE_KINDS } from "../tool-approval.js";
|
|
6
6
|
import { createWallClockJumpGuard } from "../wall-clock-jump-guard.js";
|
|
7
7
|
import { hasServiceAuth } from "./dispatch.js";
|
|
@@ -130,7 +130,7 @@ export function createApprovalSweeps(ctx) {
|
|
|
130
130
|
...(req.gateBoundInputHash !== null ? { boundInputHash: req.gateBoundInputHash } : {}),
|
|
131
131
|
...(req.updatedInput !== undefined ? { updatedInput: req.updatedInput } : {}),
|
|
132
132
|
};
|
|
133
|
-
const out = await resumeCheckpoint(cp.sessionId, req.decision, req.note,
|
|
133
|
+
const out = await resumeCheckpoint(cp.sessionId, req.decision, req.note, hostDecisionFacts(req.settledBy, req.principal), req.httpReq, undefined, binding, undefined, true, { principal: req.principal, explicitOperator: false });
|
|
134
134
|
return { status: out.status, body: out.body };
|
|
135
135
|
}
|
|
136
136
|
return { denyExpiredApprovals, redeemParkedAsk };
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -62,6 +62,7 @@ const WORKER_SWAP_REDEEMABLE = {
|
|
|
62
62
|
function checkpointRowRedeemableElsewhere(reason) {
|
|
63
63
|
return reason !== undefined && WORKER_SWAP_REDEEMABLE[reason] === true;
|
|
64
64
|
}
|
|
65
|
+
const NON_BINDING_INVALID_OUTCOME_FIELDS = new Set(["hostDecision", "approver"]);
|
|
65
66
|
export function createResumeLegs(ctx) {
|
|
66
67
|
const { deps, runnerFor, idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor } = ctx;
|
|
67
68
|
const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
|
|
@@ -908,7 +909,9 @@ export function createResumeLegs(ctx) {
|
|
|
908
909
|
...(taskId !== undefined ? { taskId } : {}),
|
|
909
910
|
error: e.message,
|
|
910
911
|
errorCode: e.code === "checkpoint.invalid_outcome"
|
|
911
|
-
? (outcome.gate === "policy_ask"
|
|
912
|
+
? (outcome.gate === "policy_ask" && !NON_BINDING_INVALID_OUTCOME_FIELDS.has(String(e.detail?.field ?? ""))
|
|
913
|
+
? "approval_binding_mismatch"
|
|
914
|
+
: "resume_outcome_invalid")
|
|
912
915
|
: e.code,
|
|
913
916
|
...(e.detail?.field ? { field: e.detail.field } : {}),
|
|
914
917
|
...((e.code === "resume.usage_window_exhausted" || e.code === "resume.preflight_rejected") && typeof e.detail?.retryAfterMs === "number" && Number.isFinite(e.detail.retryAfterMs) && e.detail.retryAfterMs > 0
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { screenedGate } from "../../trace/project.js";
|
|
2
|
-
import {
|
|
2
|
+
import { hostDecisionFacts } from "../../host-decision.js";
|
|
3
3
|
import { sameExecutionRecord } from "@sema-agent/core";
|
|
4
4
|
import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
|
|
5
|
-
import { APPROVAL_GATE_KINDS } from "../../tool-approval.js";
|
|
5
|
+
import { APPROVAL_GATE_KINDS, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING } from "../../tool-approval.js";
|
|
6
6
|
import { isParkedRunStatus } from "../../plugins/store-contracts.js";
|
|
7
7
|
import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
|
|
8
8
|
import { editedPlanBinding, isWellFormedUnicode, verifyDirectDoorProof } from "../../principal-jwt.js";
|
|
@@ -21,10 +21,10 @@ import { sendResumeOutcome } from "../route-ctx.js";
|
|
|
21
21
|
import { assertNever } from "../route-ctx.js";
|
|
22
22
|
import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
|
|
23
23
|
const DECIDE_BODY_DECLARED_KEYS = [
|
|
24
|
-
"answer", "boundCallId", "boundInputHash", "checkpointToken", "decision", "reason", "remember", "updatedInput",
|
|
24
|
+
"answer", "boundCallId", "boundInputHash", "checkpointToken", "decision", "reason", "remember", "settledBy", "updatedInput",
|
|
25
25
|
];
|
|
26
26
|
const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
|
|
27
|
-
const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "remember", "updatedInput"];
|
|
27
|
+
const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "remember", "settledBy", "updatedInput"];
|
|
28
28
|
const ACCEPTED_DECIDE_BODY = new Set(DECIDE_BODY_DECLARED_KEYS);
|
|
29
29
|
const UNSUPPORTED_DECIDE_BODY = new Set(["editedPlan", "sessionId", "taskId"]);
|
|
30
30
|
export function decideBodyKeyIssue(raw) {
|
|
@@ -71,7 +71,8 @@ function buildApprovalEnvelope(sessionId, covered, reading) {
|
|
|
71
71
|
}
|
|
72
72
|
const ACCEPTED_PLAN_REVIEW_BODY = new Set(PLAN_REVIEW_BODY_DECLARED_KEYS);
|
|
73
73
|
const UNSUPPORTED_PLAN_REVIEW_BODY = new Set([
|
|
74
|
-
|
|
74
|
+
...DECIDE_BODY_DECLARED_KEYS.filter((k) => !PLAN_REVIEW_BODY_DECLARED_KEYS.includes(k)),
|
|
75
|
+
"permissionMode", "sessionId", "taskId",
|
|
75
76
|
]);
|
|
76
77
|
export function planReviewBodyKeyIssue(raw) {
|
|
77
78
|
if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
|
|
@@ -553,7 +554,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
553
554
|
const body = (await readJson(req));
|
|
554
555
|
const decision = body?.decision === "approve" ? "approve" : body?.decision === "deny" ? "deny" : undefined;
|
|
555
556
|
if (!decision) {
|
|
556
|
-
sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember? }");
|
|
557
|
+
sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, settledBy? }");
|
|
557
558
|
return;
|
|
558
559
|
}
|
|
559
560
|
{
|
|
@@ -624,6 +625,18 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
624
625
|
sendError(res, 400, "checkpoint_token_not_in_proof", "checkpointToken is not supported on a direct-door worker (not covered by the decision proof); bind the decision with boundCallId and boundInputHash instead");
|
|
625
626
|
return;
|
|
626
627
|
}
|
|
628
|
+
if (deps.config.directDoorActive && body.settledBy !== undefined) {
|
|
629
|
+
sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (body.settledBy !== undefined && decision !== "deny") {
|
|
633
|
+
sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (body.settledBy !== undefined && typeof body.settledBy !== "string") {
|
|
637
|
+
sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
627
640
|
const binding = {
|
|
628
641
|
checkpointToken: body.checkpointToken,
|
|
629
642
|
boundCallId: body.boundCallId,
|
|
@@ -672,7 +685,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
672
685
|
pendingGrant = { sessionId: grant.overrideSessionId ?? sessionId, toolName: canonical };
|
|
673
686
|
}
|
|
674
687
|
: undefined;
|
|
675
|
-
const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined,
|
|
688
|
+
const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(body.settledBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
|
|
676
689
|
const outErrorCode = out.body.errorCode;
|
|
677
690
|
if (pendingGrant !== undefined && out.status === 200 && outErrorCode !== "cancelled" && deps.approvalExemptionStore) {
|
|
678
691
|
try {
|
|
@@ -97,7 +97,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
|
|
|
97
97
|
detail?: string | undefined;
|
|
98
98
|
eventId: string;
|
|
99
99
|
at: number;
|
|
100
|
-
to: "
|
|
100
|
+
to: "done" | "failed";
|
|
101
101
|
requestId: string;
|
|
102
102
|
}[];
|
|
103
103
|
custodyBytes: number;
|
|
@@ -105,7 +105,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
|
|
|
105
105
|
requestId: string;
|
|
106
106
|
reason: string;
|
|
107
107
|
at: number;
|
|
108
|
-
status: "
|
|
108
|
+
status: "done" | "failed" | "pending";
|
|
109
109
|
originUnknownKeys?: string[] | undefined;
|
|
110
110
|
clearanceId: string;
|
|
111
111
|
entryId: string;
|
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,
|