@sema-agent/server 7.65.0 → 7.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +6 -3
- package/dist/approval-card.d.ts +8 -0
- package/dist/approval.d.ts +3 -0
- package/dist/approval.js +2 -1
- package/dist/boot/governance-seams.js +1 -1
- package/dist/boot/resolve-spec.js +12 -5
- package/dist/boot/stores.js +3 -3
- package/dist/capabilities/hands-lane.d.ts +3 -1
- package/dist/capabilities/hands-lane.js +3 -3
- package/dist/config-catalog.js +2 -2
- package/dist/config-center/apply-effective.js +1 -1
- package/dist/config-center/skills-mcp.d.ts +3 -3
- package/dist/config-center/skills-mcp.js +17 -4
- package/dist/config-center/types.d.ts +11 -0
- package/dist/config-provider.js +1 -0
- package/dist/config-types.d.ts +27 -4
- package/dist/config.d.ts +25 -3
- package/dist/config.js +58 -30
- package/dist/deployment-governance.d.ts +8 -0
- package/dist/http/routes/approvals-assistant.js +18 -0
- package/dist/http/routes/diagnostics.d.ts +27 -2
- package/dist/http/routes/diagnostics.js +21 -1
- package/dist/http/routes/rules.js +15 -10
- package/dist/http/routes/tasks.js +9 -9
- package/dist/http/server.js +4 -1
- package/dist/mcp-tool-faces.d.ts +64 -0
- package/dist/mcp-tool-faces.js +93 -0
- package/dist/observability/fail-open.d.ts +8 -0
- package/dist/observability/fail-open.js +8 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/permission-rule-vocab.d.ts +13 -21
- package/dist/permission-rule-vocab.js +6 -5
- package/dist/plugins/checkpoint-store-sql.d.ts +68 -7
- package/dist/plugins/checkpoint-store-sql.js +90 -25
- package/dist/plugins/local-checkpoint-store.d.ts +19 -1
- package/dist/plugins/local-checkpoint-store.js +6 -1
- package/dist/plugins/permission-rule-store-file.d.ts +3 -5
- package/dist/plugins/permission-rule-store-file.js +2 -1
- package/dist/plugins/permission-rule-store-sql.d.ts +10 -10
- package/dist/plugins/permission-rule-store-sql.js +35 -20
- package/dist/plugins/pg-pool.js +5 -0
- package/dist/plugins/tidb-pool.js +11 -0
- package/dist/posture-source.d.ts +57 -0
- package/dist/posture-source.js +27 -0
- package/dist/read-face-posture.d.ts +9 -4
- package/dist/read-face-posture.js +7 -11
- package/dist/rules-consent.d.ts +15 -10
- package/dist/rules-consent.js +41 -21
- package/dist/session-titler.js +3 -12
- package/dist/task-mcp.d.ts +18 -4
- package/dist/task-mcp.js +23 -4
- package/dist/task-settings.d.ts +30 -0
- package/dist/task-settings.js +39 -18
- package/dist/tool-approval.d.ts +5 -4
- package/dist/tool-approval.js +3 -3
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/project.d.ts +22 -0
- package/dist/trace/project.js +21 -1
- package/package.json +4 -4
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { TOOL_APPROVAL_CARDS, TOOL_FAMILIES, TOOL_PATH_ACCESSES } from "@sema-agent/core";
|
|
2
|
+
function isRecord(v) {
|
|
3
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4
|
+
}
|
|
5
|
+
function isStringArray(v) {
|
|
6
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
7
|
+
}
|
|
8
|
+
function memberOf(table, v) {
|
|
9
|
+
return typeof v === "string" && table.includes(v);
|
|
10
|
+
}
|
|
11
|
+
function onlyKeys(rec, allowed) {
|
|
12
|
+
return Object.keys(rec).every((k) => allowed.includes(k));
|
|
13
|
+
}
|
|
14
|
+
function readOneFace(raw) {
|
|
15
|
+
if (!isRecord(raw))
|
|
16
|
+
return undefined;
|
|
17
|
+
if (!onlyKeys(raw, ["family", "pathTarget", "renderHints", "ruleFace"]))
|
|
18
|
+
return undefined;
|
|
19
|
+
const out = {};
|
|
20
|
+
if (raw.family !== undefined) {
|
|
21
|
+
if (!memberOf(TOOL_FAMILIES, raw.family))
|
|
22
|
+
return undefined;
|
|
23
|
+
out.family = raw.family;
|
|
24
|
+
}
|
|
25
|
+
if (raw.pathTarget !== undefined) {
|
|
26
|
+
const pt = raw.pathTarget;
|
|
27
|
+
if (!isRecord(pt) || typeof pt.param !== "string" || !memberOf(TOOL_PATH_ACCESSES, pt.access))
|
|
28
|
+
return undefined;
|
|
29
|
+
if (!onlyKeys(pt, ["param", "access", "aliases", "skillScopeEligible"]))
|
|
30
|
+
return undefined;
|
|
31
|
+
if (pt.aliases !== undefined && !isStringArray(pt.aliases))
|
|
32
|
+
return undefined;
|
|
33
|
+
if (pt.skillScopeEligible !== undefined && typeof pt.skillScopeEligible !== "boolean")
|
|
34
|
+
return undefined;
|
|
35
|
+
out.pathTarget = {
|
|
36
|
+
param: pt.param,
|
|
37
|
+
access: pt.access,
|
|
38
|
+
...(pt.aliases !== undefined ? { aliases: pt.aliases } : {}),
|
|
39
|
+
...(pt.skillScopeEligible === true ? { skillScopeEligible: true } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (raw.renderHints !== undefined) {
|
|
43
|
+
const rh = raw.renderHints;
|
|
44
|
+
if (!isRecord(rh) || !onlyKeys(rh, ["userFacingName", "activity", "summaryParams", "resultCards", "approvalCard"]))
|
|
45
|
+
return undefined;
|
|
46
|
+
if (rh.userFacingName !== undefined && typeof rh.userFacingName !== "string")
|
|
47
|
+
return undefined;
|
|
48
|
+
if (rh.activity !== undefined && typeof rh.activity !== "string")
|
|
49
|
+
return undefined;
|
|
50
|
+
if (rh.summaryParams !== undefined && !isStringArray(rh.summaryParams))
|
|
51
|
+
return undefined;
|
|
52
|
+
if (rh.resultCards !== undefined && !isStringArray(rh.resultCards))
|
|
53
|
+
return undefined;
|
|
54
|
+
if (rh.approvalCard !== undefined && !memberOf(TOOL_APPROVAL_CARDS, rh.approvalCard))
|
|
55
|
+
return undefined;
|
|
56
|
+
out.renderHints = {
|
|
57
|
+
...(rh.userFacingName !== undefined ? { userFacingName: rh.userFacingName } : {}),
|
|
58
|
+
...(rh.activity !== undefined ? { activity: rh.activity } : {}),
|
|
59
|
+
...(rh.summaryParams !== undefined ? { summaryParams: rh.summaryParams } : {}),
|
|
60
|
+
...(rh.resultCards !== undefined ? { resultCards: rh.resultCards } : {}),
|
|
61
|
+
...(rh.approvalCard !== undefined ? { approvalCard: rh.approvalCard } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (raw.ruleFace !== undefined) {
|
|
65
|
+
const rf = raw.ruleFace;
|
|
66
|
+
if (!isRecord(rf) || !onlyKeys(rf, ["primaryParams", "params"]))
|
|
67
|
+
return undefined;
|
|
68
|
+
if (rf.primaryParams !== undefined && !isStringArray(rf.primaryParams))
|
|
69
|
+
return undefined;
|
|
70
|
+
if (rf.params !== undefined && !isStringArray(rf.params))
|
|
71
|
+
return undefined;
|
|
72
|
+
out.ruleFace = {
|
|
73
|
+
...(rf.primaryParams !== undefined ? { primaryParams: rf.primaryParams } : {}),
|
|
74
|
+
...(rf.params !== undefined ? { params: rf.params } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
export function readMcpToolFaces(v) {
|
|
80
|
+
if (v === undefined)
|
|
81
|
+
return { ok: true };
|
|
82
|
+
if (!isRecord(v))
|
|
83
|
+
return { ok: false, problem: `toolFaces is ${Array.isArray(v) ? "an array" : typeof v}, not an object of per-tool faces` };
|
|
84
|
+
const faces = {};
|
|
85
|
+
for (const [tool, raw] of Object.entries(v)) {
|
|
86
|
+
const face = readOneFace(raw);
|
|
87
|
+
if (face === undefined)
|
|
88
|
+
return { ok: false, problem: `the declared face for tool ${JSON.stringify(tool)} does not fit the tool-face shape` };
|
|
89
|
+
faces[tool] = face;
|
|
90
|
+
}
|
|
91
|
+
return Object.keys(faces).length > 0 ? { ok: true, faces } : { ok: true };
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=mcp-tool-faces.js.map
|
|
@@ -17,6 +17,10 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
17
17
|
readonly cls: "F";
|
|
18
18
|
readonly note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`,写腿与两条读腿共用):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。";
|
|
19
19
|
};
|
|
20
|
+
readonly "server.trace.tool-roster-malformed": {
|
|
21
|
+
readonly cls: "F";
|
|
22
|
+
readonly note: "⑪(core 7.8.0 design/388,`trace/project.ts` 的 `wiring_manifest.tools` 段与 `tool_roster_delta` 构造器共用):载荷过不了 core 自己的 typebox 判形器(`ToolRoster` / `ToolRosterDelta`)⇒ 那一段缺席 / 那一帧不发,**绝不半张脸上 wire**。放行的最坏后果=消费端这条腿看不到工具名册(壳退回按名字分组、用通用审批卡),不影响任何裁决——名册是观测/渲染面,门在引擎里早已判完。反方向(把一份判不了形的名册照发)更坏:消费端会把读不出的行当成「这只工具没挂」。计数让「某个世代的产生者一直在发坏名册」显形。";
|
|
23
|
+
};
|
|
20
24
|
readonly "server.task-mcp.source-label-dropped": {
|
|
21
25
|
readonly cls: "F";
|
|
22
26
|
readonly note: "B-036 / S-143(`mcp-source-label.ts readMcpSourceLabel`,请求腿 + center 腿共用):`mcpServers[].source`(分组标签,显示用、开集)坏形——空串 / 超 80 字 / 非字符串——**丢键留 server**。放行的最坏后果=这台 server 在 wiring_manifest.mcp[] 里没有分组标签(难看),不是门/权限/凭证;本地 config.d 腿由 settings-schema 1.7.0 在解析处拒,故本臂只在请求腿与远端 /effective 直读 JSON 腿可达。";
|
|
@@ -65,6 +69,10 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
65
69
|
readonly cls: "F";
|
|
66
70
|
readonly note: "elicitation_complete 面包屑持久化失败。同上——答案已经在壳手里,丢的是收尾渲染。";
|
|
67
71
|
};
|
|
72
|
+
readonly "server.session-titler.gateway-unreachable": {
|
|
73
|
+
readonly cls: "F";
|
|
74
|
+
readonly note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。";
|
|
75
|
+
};
|
|
68
76
|
readonly "server.prompts.artifact-cache-write-failed": {
|
|
69
77
|
readonly cls: "F";
|
|
70
78
|
readonly note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。";
|
|
@@ -8,6 +8,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
8
8
|
cls: "F",
|
|
9
9
|
note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`,写腿与两条读腿共用):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。",
|
|
10
10
|
},
|
|
11
|
+
"server.trace.tool-roster-malformed": {
|
|
12
|
+
cls: "F",
|
|
13
|
+
note: "⑪(core 7.8.0 design/388,`trace/project.ts` 的 `wiring_manifest.tools` 段与 `tool_roster_delta` 构造器共用):载荷过不了 core 自己的 typebox 判形器(`ToolRoster` / `ToolRosterDelta`)⇒ 那一段缺席 / 那一帧不发,**绝不半张脸上 wire**。放行的最坏后果=消费端这条腿看不到工具名册(壳退回按名字分组、用通用审批卡),不影响任何裁决——名册是观测/渲染面,门在引擎里早已判完。反方向(把一份判不了形的名册照发)更坏:消费端会把读不出的行当成「这只工具没挂」。计数让「某个世代的产生者一直在发坏名册」显形。",
|
|
14
|
+
},
|
|
11
15
|
"server.task-mcp.source-label-dropped": {
|
|
12
16
|
cls: "F",
|
|
13
17
|
note: "B-036 / S-143(`mcp-source-label.ts readMcpSourceLabel`,请求腿 + center 腿共用):`mcpServers[].source`(分组标签,显示用、开集)坏形——空串 / 超 80 字 / 非字符串——**丢键留 server**。放行的最坏后果=这台 server 在 wiring_manifest.mcp[] 里没有分组标签(难看),不是门/权限/凭证;本地 config.d 腿由 settings-schema 1.7.0 在解析处拒,故本臂只在请求腿与远端 /effective 直读 JSON 腿可达。",
|
|
@@ -56,6 +60,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
56
60
|
cls: "F",
|
|
57
61
|
note: "elicitation_complete 面包屑持久化失败。同上——答案已经在壳手里,丢的是收尾渲染。",
|
|
58
62
|
},
|
|
63
|
+
"server.session-titler.gateway-unreachable": {
|
|
64
|
+
cls: "F",
|
|
65
|
+
note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。",
|
|
66
|
+
},
|
|
59
67
|
"server.prompts.artifact-cache-write-failed": {
|
|
60
68
|
cls: "F",
|
|
61
69
|
note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。",
|
|
@@ -260,7 +260,7 @@ export function createMetrics() {
|
|
|
260
260
|
m.counter("stale_cache_conflict_total", "Warm session-cache entries evicted by a cross-instance write while cached (S25)");
|
|
261
261
|
m.counter("breaker_writethrough_failed_total", "Circuit-breaker cross-replica write-through failures (LOW), by backend");
|
|
262
262
|
m.counter("web_search_bad_payload_total", "Web-search provider responses whose results field was not an array (LOW), by provider");
|
|
263
|
-
m.counter("permission_denied_total", "Tool-gate denials by the LAYER whose verdict is the deny (core's DeniedBy: policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution; 'other' = this build cannot name the layer; label is `deniedBy` since server 7.64.0 — it was `source` over core's retired PermissionDeniedSource; goal B4 always-on deny meter)");
|
|
263
|
+
m.counter("permission_denied_total", "Tool-gate denials by the LAYER whose verdict is the deny (core's DeniedBy: policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution/persisted_rule; 'other' = this build cannot name the layer; label is `deniedBy` since server 7.64.0 — it was `source` over core's retired PermissionDeniedSource; goal B4 always-on deny meter)");
|
|
264
264
|
m.counter("permission_rule_events_total", "Persisted allow-rule seam events by event (persisted_rule_allowed = a rule authorized a call with nobody asked; rule_store_unreadable = the store failed to read so the call was adjudicated with ZERO rules)");
|
|
265
265
|
m.counter("compaction_events_total", "Compaction lifecycle events by outcome (started/completed/failed/skipped) and trigger");
|
|
266
266
|
m.counter("hook_llm_calls_total", "prompt/agent hook entries that completed a model call (hooks 3b), by type");
|
|
@@ -1,29 +1,21 @@
|
|
|
1
|
+
import type { RuleApprovalRecord } from "@sema-agent/core";
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `plugins/permission-rule-store-sql.ts` 的 `rules_json` 读/写形),core 给 `PersistedRuleMatch` 加两员
|
|
7
|
-
* (wildcard / subpath)、给 `PersistedRuleTool` 加 Read 时,四份抄件**没有一份**会编译红 —— zod 的 enum
|
|
8
|
-
* 只是运行期的一个 Set,静默把一次上游语义扩张翻成一整类合法行的拒收([ref] 词表纪律说的正是这形:
|
|
9
|
-
* 未知词必须是**编译**错,不是运行期一个安静的 false)。
|
|
3
|
+
* `PersistedRuleTool` **不再是闭词表**(core 7.9.0 / [ref]):工具名自 core 的**目录**派生
|
|
4
|
+
* (`pathTarget` 声明 ⇒ 路径文法;`Bash` ⇒ 命令文法),型是 `string`。于是本文件对它的处置从
|
|
5
|
+
* 「转录一份名单」换成「转 core 的**谓词**」——名单在 core 的目录里,抄一份的那天目录加一个带
|
|
6
|
+
* `pathTarget` 的工具,本仓会静默拒收一整类合法行(正是本文件顶注说的那个病)。
|
|
10
7
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* core 没有的词 ⇒ 同样红(词表属主是 core,本仓只转录)。
|
|
14
|
-
*
|
|
15
|
-
* ⚠️ 转录**不是**判据:一个词在表里只说「这是 core 定义的一员」,不说「本仓每条腿都接受它」。逐腿的收窄
|
|
16
|
-
* (例如卡编辑臂对 `subpath` 的拒)写在那条腿自己的穷举 `switch` 里,并各带理由。
|
|
8
|
+
* 谓词方向仍是 fail-CLOSED:`ruleToolGrammarOf` 认不出的名字**不是**一条可持久化规则的工具位
|
|
9
|
+
* (既非命令文法也非路径文法 ⇒ 这条行永远匹配不上任何调用),两只店与票口一律拒。
|
|
17
10
|
*/
|
|
18
|
-
|
|
19
|
-
/** `PersistedRuleTool`([ref] §2.2:`"Read"` 只说一种文法 —— `Read(//abs-dir/**)` 目录只读授权)。 */
|
|
20
|
-
export declare const PERSISTED_RULE_TOOLS: readonly ["Bash", "Read"];
|
|
11
|
+
export declare function isPersistedRuleTool(name: string): boolean;
|
|
21
12
|
/**
|
|
22
|
-
* `PersistedRuleMatch`
|
|
13
|
+
* `PersistedRuleMatch` 五员(core 顶注逐字):`exact` / `prefix`(历史 colon-star,永久兼容读、复合前缀形的
|
|
23
14
|
* 唯一拼写)/ `wildcard`([ref] 尾 space-star,单命令体上与 prefix **同一谓词**,建议面自此铸它)/
|
|
24
|
-
* `subpath`(Read 目录形,永不容纳任何命令)
|
|
15
|
+
* `subpath`(Read 目录形,永不容纳任何命令)/ `path`(core 7.9.0 [ref]:路径族 deny/ask/allow 的模式文法
|
|
16
|
+
* —— `//abs`、`~/`、`/root-relative`、cwd 相对,段内 `*`、跨段 `**`)。
|
|
25
17
|
*/
|
|
26
|
-
export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "subpath"];
|
|
18
|
+
export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "subpath", "path"];
|
|
27
19
|
/** [ref] §2.3 B3:batch offer 成员的判别位(`command` = 历史逐段 Bash 规则;`directoryRead` = cd 段铸的目录只读授权)。 */
|
|
28
20
|
export declare const RULE_OFFER_BATCH_MEMBER_KINDS: readonly ["command", "directoryRead"];
|
|
29
21
|
/** [ref] §3.5:一段「为什么仍未被覆盖」的闭三词集(`redirection` 永久逐次 / `no_rule_form` 无可覆盖形 / `cap_overflow` 去重+帽挤出)。 */
|
|
@@ -31,7 +23,7 @@ export declare const UNCOVERED_SEGMENT_REASONS: readonly ["redirection", "no_rul
|
|
|
31
23
|
/** [ref] §3.3-5:卡编辑面的 breadth 警告码(记录 `edited.warnings` 存的就是这两个字面)。 */
|
|
32
24
|
export declare const EDITED_RULE_BREADTH_WARNING_CODES: readonly ["broad_prefix", "compound_prefix"];
|
|
33
25
|
/**
|
|
34
|
-
* [ref] §4.5 / [ref] §3.5 —— 审批记录的**形版本号**(core `RuleApprovalRecord.schema`,本批
|
|
26
|
+
* [ref] §4.5 / [ref] §3.5 —— 审批记录的**形版本号**(core `RuleApprovalRecord.schema`,本批 3→4;[ref] 三态 behavior 进候选行)。
|
|
35
27
|
* 类型绑 core 的字面型:core 再升号 ⇒ 这里编译红,两只 durable 店(SQL 双方言 + File)的 stale 判别与写侧钉
|
|
36
28
|
* 同批随之;判别是**等式**(`!==`)不是下限 —— 旧号的行按 stale 信封交还(不迁移、不 widen、不丢行),
|
|
37
29
|
* pending 卡短命,重触发即取新卡(B7/B8 既诺)。
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { ruleToolGrammarOf } from "@sema-agent/core";
|
|
2
|
+
export function isPersistedRuleTool(name) {
|
|
3
|
+
return ruleToolGrammarOf(name) !== undefined;
|
|
4
|
+
}
|
|
5
|
+
export const PERSISTED_RULE_MATCHES = ["exact", "prefix", "wildcard", "subpath", "path"];
|
|
5
6
|
const MATCHES_PIN = true;
|
|
6
7
|
void MATCHES_PIN;
|
|
7
8
|
export const RULE_OFFER_BATCH_MEMBER_KINDS = ["command", "directoryRead"];
|
|
@@ -13,5 +14,5 @@ void REASONS_PIN;
|
|
|
13
14
|
export const EDITED_RULE_BREADTH_WARNING_CODES = ["broad_prefix", "compound_prefix"];
|
|
14
15
|
const WARNING_CODES_PIN = true;
|
|
15
16
|
void WARNING_CODES_PIN;
|
|
16
|
-
export const RULE_APPROVAL_RECORD_SCHEMA =
|
|
17
|
+
export const RULE_APPROVAL_RECORD_SCHEMA = 4;
|
|
17
18
|
//# sourceMappingURL=permission-rule-vocab.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
2
|
import type { Pool as PgPool } from "pg";
|
|
3
|
-
import { type Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolvedOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
|
|
3
|
+
import { type ExecutionOutcomeRecordWord, type GateOutcome, type Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolvedOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
|
|
4
4
|
import { type RuleOfferProjection } from "../approval-card.js";
|
|
5
5
|
import { type SqlDialect, type SqlDriver } from "./sql-driver.js";
|
|
6
6
|
export { approvalPayloadFingerprint } from "./approval-payload-fingerprint.js";
|
|
@@ -195,16 +195,31 @@ export declare function pendingRowPrecedes(a: {
|
|
|
195
195
|
createdAt: number;
|
|
196
196
|
token: string;
|
|
197
197
|
}, pick?: "newest" | "oldest"): boolean;
|
|
198
|
+
/**
|
|
199
|
+
* `checkpoint` 表上**本仓后加、且被无条件引用**的列册 —— 升级前置探针({@link assertCheckpointColumnSchema})
|
|
200
|
+
* 的唯一数据源。一行一列:列名 + 这一列不在时**运行期**先坏在哪(拒启文案逐字用它,所以运维读到的是
|
|
201
|
+
* 「我会先在哪里挂」,不是一句泛泛的 schema 不齐)。
|
|
202
|
+
*
|
|
203
|
+
* 🔴 为什么是一张表而不是每列一个 `assert*ColumnSchema` 函数(S-160 归一):[ref]① 那次是一列一函数,
|
|
204
|
+
* 本批 [ref] 又要加两列 —— 照抄就是第二、第三份三十行的同形代码,而它们**唯一的差别只有列名与那句话**。
|
|
205
|
+
* 探测法(恒空 `WHERE 1=0` 读 + 方言缺列码)、失败方向(只在**可证**缺列时才给删表指路)、恢复动作
|
|
206
|
+
* (DROP TABLE checkpoint 的代价交代)三者对整张表是同一条,所以规则只留一条,列变成数据:下一次加列
|
|
207
|
+
* 只加一行,不再多一条规则。
|
|
208
|
+
*/
|
|
209
|
+
export declare const CHECKPOINT_ADDED_COLUMNS: ReadonlyArray<{
|
|
210
|
+
column: string;
|
|
211
|
+
breaksAt: string;
|
|
212
|
+
}>;
|
|
198
213
|
/**
|
|
199
214
|
* [ref]① 升级前置断言 —— **拒启**,不是 warn(形照 [ref] `assertApprovalAskRuleMaterialSchema` / [ref] /
|
|
200
215
|
* [ref] 三条先例;codex 店面轮 R1-[high] 采纳 —— 其引证的「MIGRATION.md 常设策略」经亲验不存在,采纳的
|
|
201
|
-
* 依据是本仓 assert*Schema 探针族的真先例与失败方向本身)。
|
|
216
|
+
* 依据是本仓 assert*Schema 探针族的真先例与失败方向本身)。S-160 起判据对 {@link CHECKPOINT_ADDED_COLUMNS}
|
|
217
|
+
* **整张列册**跑(见那张表的头注:归一的理由)。
|
|
202
218
|
*
|
|
203
219
|
* 病:本仓不发 `ALTER TABLE`(SCHEMA POLICY:改列就改 CREATE + 删库重建)。一台**没删表**就升上来的
|
|
204
|
-
* 部署,`CREATE TABLE IF NOT EXISTS` 对存量 `checkpoint` 是空操作 ——
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* `GET /v1/approvals` 与 `/stream` 的每一次轮询 500 —— 运行期才响,且首撞点在用户的审批路径上。
|
|
220
|
+
* 部署,`CREATE TABLE IF NOT EXISTS` 对存量 `checkpoint` 是空操作 —— 后加的列不在,而写口与读口
|
|
221
|
+
* **无条件**引用它们。没有这道断言,服务照常过启动与健康检查,直到运行期才响,且首撞点在用户的
|
|
222
|
+
* 审批 / resume 路径上。
|
|
208
223
|
*
|
|
209
224
|
* 判据 = **能力探测**(恒空的 `WHERE 1=0` 读)+ **方言的缺列错误码**({@link isMissingColumnError},
|
|
210
225
|
* 判据属主 `sql-errors.ts`);其余错误**原样 rethrow**,一个字都不加工 —— 一句破坏性的「去 DROP TABLE」
|
|
@@ -214,7 +229,7 @@ export declare function pendingRowPrecedes(a: {
|
|
|
214
229
|
* store 装配段、路由装配之前;**无条件**跑(与 [ref] tool_result 探针同档)——checkpoint 表在两方言的
|
|
215
230
|
* 中央 DDL 里恒在,SQL 后端一开它就是 durable park/审批队列的承载面,不存在「协议不上场」的豁免形。
|
|
216
231
|
*/
|
|
217
|
-
export declare function
|
|
232
|
+
export declare function assertCheckpointColumnSchema(query: (sql: string) => Promise<{
|
|
218
233
|
rows: Record<string, unknown>[];
|
|
219
234
|
}>, dialect: SqlDialect): Promise<void>;
|
|
220
235
|
/**
|
|
@@ -321,6 +336,17 @@ export interface DecidedApprovalRecord {
|
|
|
321
336
|
* 铸法见 {@link approvalPayloadFingerprint}(两条车道共用同一只,免得指纹口径漂)。
|
|
322
337
|
*/
|
|
323
338
|
payloadFp: string | null;
|
|
339
|
+
/**
|
|
340
|
+
* S-160(core 7.7.0 [ref])—— 这条已决行上**已决动作最后被怎么处置**的结算记录,`null` = **未知**。
|
|
341
|
+
*
|
|
342
|
+
* 🔴 缺席只有一个含义:未知,**绝不是**「放行过」。`decision` 说的是决定(人批了没有),这一格说的是
|
|
343
|
+
* 那次决定在 resume 腿上执行时的处置(部署自己的复核可能在 post-CAS 把人批过的、改过参数的调用否掉,
|
|
344
|
+
* 于是 `decision: "approve"` 与 denied/policy 的处置**同时**在一行上成立)。[ref] 的病正是「流断之后
|
|
345
|
+
* 从行重建这条 run 的消费者只读得到前半句」——decide 幂等回放正是那种消费者。
|
|
346
|
+
*
|
|
347
|
+
* 铸法 = core 的 {@link executionVerdict} **单口**(两条车道共用),不许任何读点自己判列的在场性。
|
|
348
|
+
*/
|
|
349
|
+
executionOutcome: GateOutcome | null;
|
|
324
350
|
}
|
|
325
351
|
/**
|
|
326
352
|
* [ref] 件1 —— {@link SqlCheckpointStore.findDecidedApprovalsForBinding} 的扫描上界。
|
|
@@ -417,6 +443,17 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
417
443
|
readonly redecision: {
|
|
418
444
|
readonly reopen: true;
|
|
419
445
|
};
|
|
446
|
+
/**
|
|
447
|
+
* `CheckpointStore.execution` 声明(core 7.7.0 [ref];S-160)——**必填**,且必须恰是 `{ outcome: true }`:
|
|
448
|
+
* 店在此承诺 {@link SqlCheckpointStore.recordExecutionOutcome} 是契约描述的那条真 CAS(谓词、四词、
|
|
449
|
+
* 冲突抛),且 {@link SqlCheckpointStore.reopen} 认这条记录。与 `durability`/`fidelity`/`redecision`
|
|
450
|
+
* 三格同一条「声明而非鸭子判」姿势,区别只在这一格**不可缺席**:缺席的店会被 core 在 store 席的
|
|
451
|
+
* 第一次读(`resolveCheckpointStore`)以 `config.invalid_checkpoint_store` 拒启 —— 那是契约的正确行为,
|
|
452
|
+
* 因为一台记不下「已决动作最后被怎么处置」的部署,它手里每一条 resolved 行在这条轴上都永远读不出来。
|
|
453
|
+
*/
|
|
454
|
+
readonly execution: {
|
|
455
|
+
readonly outcome: true;
|
|
456
|
+
};
|
|
420
457
|
constructor(db: SqlDriver, logger?: {
|
|
421
458
|
info?(msg: string, meta?: unknown): void;
|
|
422
459
|
} | undefined);
|
|
@@ -485,6 +522,23 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
485
522
|
* is validated against it — clearing it would defeat the winner-replay invariant.
|
|
486
523
|
*/
|
|
487
524
|
reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
|
|
525
|
+
/**
|
|
526
|
+
* core 7.7.0 [ref](S-160)—— 记录**已决动作在 resume 腿上最后被怎么处置**:那一次调用的 `tool_end` 帧
|
|
527
|
+
* 携带的 {@link GateOutcome}。契约(答案闭集四词 / 幂等 / 冲突抛 / 一个字都不碰 status·rev·outcome·
|
|
528
|
+
* reopen_reason)的**判词逻辑一行都不自铸**:全部消费 core 导出的
|
|
529
|
+
* {@link executionRecordDisposition} —— 它是每个后端共用的那一份「CAS 谓词 + 幂等/冲突两臂」,
|
|
530
|
+
* 本店只留 commit 那一半(一条 `UPDATE … WHERE`)。
|
|
531
|
+
*
|
|
532
|
+
* 形 = **先读后 CAS**(与 {@link SqlCheckpointStore.setPendingSteer} 同款,理由同款):判词是**按行读**
|
|
533
|
+
* 出来的(要分辨 already_recorded / conflict / not_resolved / absent 四种,SQL 的 affected 只会告诉你
|
|
534
|
+
* 「没打中」),所以读一次交给 core 判,再把 commit 那一半压回一条带完整谓词的 UPDATE。两条语句之间那道窗
|
|
535
|
+
* 由 UPDATE 自己的谓词兜住:CAS 落空就重读重判(下一轮的读会分辨出到底是谁抢先了)。给足冗余仍全败 ⇒
|
|
536
|
+
* **抛**,绝不返回一个四词之一 —— 那等于替一次没发生的写编一个答案。
|
|
537
|
+
*
|
|
538
|
+
* 🔴 `execution_outcome` 坏 cell 在这里与 `get()` 同款 **抛**(strict parse):静默当缺席会让一份不同的
|
|
539
|
+
* 记录覆盖已经在案的那份,而记录物理不可变正是这条契约的全部意义。
|
|
540
|
+
*/
|
|
541
|
+
recordExecutionOutcome(token: CheckpointToken, scope: string, gate: GateOutcome): Promise<ExecutionOutcomeRecordWord>;
|
|
488
542
|
/**
|
|
489
543
|
* [ref] D-A (durable steering) —— **core 5.14.0([ref] BREAKING)起是「追加进一条有界有序队列」**,
|
|
490
544
|
* 不再是「盖掉单座」。两个人同时给一条挂起的 run 转向,旧语义会让第二条**静默销毁**第一条(一条没人
|
|
@@ -560,6 +614,13 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
560
614
|
/** Pending checkpoints for the operator queue — NON-secret fields only (never the token). `taskId` is
|
|
561
615
|
* joined from task_active (the JOIN KEY to the run/trace — a suspended run KEEPS its
|
|
562
616
|
* session claim, so the join is live for every pending row; null only in pathological windows). */
|
|
617
|
+
/**
|
|
618
|
+
* S-160 显式记账 —— **本口与 {@link SqlCheckpointStore.listByScope} 都不投执行判词,这是结构性的不是遗漏**:
|
|
619
|
+
* 两口的谓词都硬 `status='pending'`,而执行记录的 CAS 谓词是 `status='resolved' AND execution_outcome IS NULL`
|
|
620
|
+
* ⇒ 一条 pending 行**按构造**不可能带记录;反向也封死了(记过的行 `reopen` 拒,永远回不到 pending)。
|
|
621
|
+
* 所以这两条读面上「没有 `executionOutcome`」不是「缺席=未知」,而是「这一格在这条读面上不存在」。
|
|
622
|
+
* 真正需要它的读面是**已决行**的那一条:`findDecidedApprovalsForBinding`(decide 幂等回放),见那里。
|
|
623
|
+
*/
|
|
563
624
|
listPending(scope?: string): Promise<PendingCheckpoint[]>;
|
|
564
625
|
/**
|
|
565
626
|
* [ref] #2 (scheduler seam, core 1.105): a scope's PENDING checkpoints as lightweight
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { contentKindOf } from "../approval-content-kind.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { APPROVAL_GATE_KINDS_SQL_IN } from "../tool-approval.js";
|
|
4
|
-
import { CheckpointError, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, } from "@sema-agent/core";
|
|
4
|
+
import { CheckpointError, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, ExecutionOutcomeConflictError, executionRecordDisposition, executionVerdict, } from "@sema-agent/core";
|
|
5
5
|
import { redactDeep, redactSecrets } from "../trace/redact.js";
|
|
6
6
|
import { MAX_RULE_OFFERS, MAX_RULE_OFFER_BATCH_MEMBERS, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat } from "../approval-card.js";
|
|
7
7
|
import { parseJsonStrict as parseJson } from "./sql-row-helpers.js";
|
|
@@ -32,23 +32,41 @@ export function pendingRowPrecedes(a, b, pick) {
|
|
|
32
32
|
return a.createdAt < b.createdAt || (a.createdAt === b.createdAt && a.token < b.token);
|
|
33
33
|
return a.createdAt > b.createdAt || (a.createdAt === b.createdAt && a.token > b.token);
|
|
34
34
|
}
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
35
|
+
export const CHECKPOINT_ADDED_COLUMNS = [
|
|
36
|
+
{
|
|
37
|
+
column: "has_bidi_controls",
|
|
38
|
+
breaksAt: "the park-mint write (put) and the operator-queue reads (GET /v1/approvals, /v1/approvals/stream) name it " +
|
|
39
|
+
"unconditionally, so every durable suspend fails at first park and every queue poll 500s",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
column: "execution_outcome",
|
|
43
|
+
breaksAt: "the execution-outcome record (CheckpointStore.recordExecutionOutcome, core 7.7.0 #602) writes it under a CAS and " +
|
|
44
|
+
"`reopen` reads it as a predicate, so every resumed approval fails to file how the decided action was disposed and " +
|
|
45
|
+
"every reopen statement errors",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
column: "execution_at_ms",
|
|
49
|
+
breaksAt: "it is written in the same CAS as `execution_outcome` (the record and its stamp are one write group)",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
export async function assertCheckpointColumnSchema(query, dialect) {
|
|
53
|
+
for (const { column, breaksAt } of CHECKPOINT_ADDED_COLUMNS) {
|
|
54
|
+
try {
|
|
55
|
+
await query(`SELECT ${column} FROM checkpoint WHERE 1=0`);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (!isMissingColumnError(err, dialect))
|
|
59
|
+
throw err;
|
|
60
|
+
throw new Error(`checkpoint is missing the column ${column} — refusing to start. ` +
|
|
61
|
+
`On this table ${breaksAt} — at runtime, after a healthy-looking boot. ` +
|
|
62
|
+
"This repository ships no ALTER TABLE migrations, so the table must be recreated: run " +
|
|
63
|
+
"`DROP TABLE checkpoint;` on the " +
|
|
64
|
+
(dialect === "pg" ? "PostgreSQL" : "MySQL-protocol") +
|
|
65
|
+
" backend and restart — the schema is recreated at boot. The cost is bounded but real: the table holds IN-FLIGHT " +
|
|
66
|
+
"durable parks (suspended approvals/reviews awaiting a decision) — those cards are lost and their runs must be " +
|
|
67
|
+
"re-triggered; `checkpoint_ctx`, run rows and every other persistent face are untouched. " +
|
|
68
|
+
`Underlying probe error: ${err instanceof Error ? err.message : String(err)}`);
|
|
69
|
+
}
|
|
52
70
|
}
|
|
53
71
|
}
|
|
54
72
|
const MAX_RULE_OFFER_SCAN = 32;
|
|
@@ -266,6 +284,7 @@ export function readPendingSteerColumns(seatRaw, queueRaw) {
|
|
|
266
284
|
};
|
|
267
285
|
}
|
|
268
286
|
const STEER_CAS_ATTEMPTS = 8;
|
|
287
|
+
const EXECUTION_RECORD_CAS_ATTEMPTS = 8;
|
|
269
288
|
export class SqlCheckpointStore {
|
|
270
289
|
db;
|
|
271
290
|
logger;
|
|
@@ -273,6 +292,7 @@ export class SqlCheckpointStore {
|
|
|
273
292
|
durability = "durable";
|
|
274
293
|
fidelity = "json";
|
|
275
294
|
redecision = { reopen: true };
|
|
295
|
+
execution = { outcome: true };
|
|
276
296
|
constructor(db, logger) {
|
|
277
297
|
this.db = db;
|
|
278
298
|
this.logger = logger;
|
|
@@ -318,14 +338,17 @@ export class SqlCheckpointStore {
|
|
|
318
338
|
const toolInput = boundedToolInput(pa?.args);
|
|
319
339
|
const ruleOffers = boundedRuleOffers(pa?.ruleOffers);
|
|
320
340
|
const hasBidiControls = pa?.hasBidiControls === true ? 1 : null;
|
|
341
|
+
const executionOutcome = cp.executionOutcome === undefined ? null : this.json(cp.executionOutcome, "execution outcome");
|
|
342
|
+
const executionAtMs = cp.executionOutcome === undefined ? null : (cp.executionAtMs ?? null);
|
|
321
343
|
try {
|
|
322
|
-
await this.db.query(this.q("INSERT INTO checkpoint (token, scope, session_id, version, status, tool_name, tool_call_id, tool_input, checkpoint, deadline, created_at_ms, terminal_at_ms, gate_kind, bound_input_hash, risk_descriptor, rule_suggestions, has_bidi_controls) " +
|
|
323
|
-
"VALUES (
|
|
324
|
-
"VALUES ($1,$2,$3,$4
|
|
344
|
+
await this.db.query(this.q("INSERT INTO checkpoint (token, scope, session_id, version, status, tool_name, tool_call_id, tool_input, checkpoint, deadline, created_at_ms, terminal_at_ms, gate_kind, bound_input_hash, risk_descriptor, rule_suggestions, has_bidi_controls, rev, reopen_reason, execution_outcome, execution_at_ms) " +
|
|
345
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", "INSERT INTO checkpoint (token, scope, session_id, version, status, tool_name, tool_call_id, tool_input, checkpoint, deadline, created_at_ms, terminal_at_ms, gate_kind, bound_input_hash, risk_descriptor, rule_suggestions, has_bidi_controls, rev, reopen_reason, execution_outcome, execution_at_ms) " +
|
|
346
|
+
"VALUES ($1,$2,$3,$4,$5,$6,$7,$8::jsonb,$9::jsonb,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20::jsonb,$21)"), [
|
|
325
347
|
token,
|
|
326
348
|
cp.scope,
|
|
327
349
|
cp.sessionId,
|
|
328
350
|
version,
|
|
351
|
+
cp.status,
|
|
329
352
|
pa?.toolName ?? null,
|
|
330
353
|
pa?.toolCallId ?? null,
|
|
331
354
|
toolInput === null ? null : this.json(toolInput, "approval tool_input"),
|
|
@@ -338,6 +361,10 @@ export class SqlCheckpointStore {
|
|
|
338
361
|
((g) => (g?.riskDescriptor ? this.json(g.riskDescriptor, "risk descriptor") : null))(cp.gate),
|
|
339
362
|
ruleOffers === undefined ? null : this.json(ruleOffers, "rule offers"),
|
|
340
363
|
hasBidiControls,
|
|
364
|
+
cp.rev ?? 0,
|
|
365
|
+
cp.reopenReason ?? null,
|
|
366
|
+
executionOutcome,
|
|
367
|
+
executionAtMs,
|
|
341
368
|
]);
|
|
342
369
|
}
|
|
343
370
|
catch (e) {
|
|
@@ -348,7 +375,7 @@ export class SqlCheckpointStore {
|
|
|
348
375
|
}
|
|
349
376
|
}
|
|
350
377
|
async get(token) {
|
|
351
|
-
const { rows } = await this.db.query(this.q("SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer, pending_steer_queue FROM checkpoint WHERE token = ?", "SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer, pending_steer_queue FROM checkpoint WHERE token = $1"), [token]);
|
|
378
|
+
const { rows } = await this.db.query(this.q("SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer, pending_steer_queue, execution_outcome, execution_at_ms FROM checkpoint WHERE token = ?", "SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer, pending_steer_queue, execution_outcome, execution_at_ms FROM checkpoint WHERE token = $1"), [token]);
|
|
352
379
|
const r = rows[0];
|
|
353
380
|
if (!r)
|
|
354
381
|
return null;
|
|
@@ -363,6 +390,12 @@ export class SqlCheckpointStore {
|
|
|
363
390
|
cp.resolvedOutcome = winnerFromOutcome(outcomeRaw);
|
|
364
391
|
cp.reopenReason = r.reopen_reason ?? undefined;
|
|
365
392
|
cp.rev = r.rev == null ? 0 : Number(r.rev);
|
|
393
|
+
const executionOutcome = parseJson(r.execution_outcome);
|
|
394
|
+
if (executionOutcome !== null) {
|
|
395
|
+
cp.executionOutcome = executionOutcome;
|
|
396
|
+
if (r.execution_at_ms != null)
|
|
397
|
+
cp.executionAtMs = Number(r.execution_at_ms);
|
|
398
|
+
}
|
|
366
399
|
const steer = readPendingSteerColumns(r.pending_steer, r.pending_steer_queue);
|
|
367
400
|
if (steer.lossy) {
|
|
368
401
|
this.logger?.info?.("checkpoint_pending_steer_unreadable", { tokenFp: tokenFingerprint(token) });
|
|
@@ -392,9 +425,32 @@ export class SqlCheckpointStore {
|
|
|
392
425
|
return res.affected === 1;
|
|
393
426
|
}
|
|
394
427
|
async reopen(token, scope, reason) {
|
|
395
|
-
const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'pending', reopen_reason = ?, rev = rev + 1 WHERE token = ? AND scope = ? AND status = 'resolved'", "UPDATE checkpoint SET status = 'pending', reopen_reason = $1, rev = rev + 1 WHERE token = $2 AND scope = $3 AND status = 'resolved'"), [reason, token, scope]);
|
|
428
|
+
const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'pending', reopen_reason = ?, rev = rev + 1 WHERE token = ? AND scope = ? AND status = 'resolved' AND execution_outcome IS NULL", "UPDATE checkpoint SET status = 'pending', reopen_reason = $1, rev = rev + 1 WHERE token = $2 AND scope = $3 AND status = 'resolved' AND execution_outcome IS NULL"), [reason, token, scope]);
|
|
396
429
|
return res.affected === 1;
|
|
397
430
|
}
|
|
431
|
+
async recordExecutionOutcome(token, scope, gate) {
|
|
432
|
+
for (let attempt = 0; attempt < EXECUTION_RECORD_CAS_ATTEMPTS; attempt++) {
|
|
433
|
+
const { rows } = await this.db.query(this.q("SELECT scope, status, execution_outcome FROM checkpoint WHERE token = ?", "SELECT scope, status, execution_outcome FROM checkpoint WHERE token = $1"), [token]);
|
|
434
|
+
const row = rows[0];
|
|
435
|
+
const facts = row === undefined
|
|
436
|
+
? undefined
|
|
437
|
+
: {
|
|
438
|
+
scope: String(row.scope),
|
|
439
|
+
status: row.status,
|
|
440
|
+
...((v) => (v === null ? {} : { executionOutcome: v }))(parseJson(row.execution_outcome)),
|
|
441
|
+
};
|
|
442
|
+
const disposition = executionRecordDisposition(facts, scope, gate);
|
|
443
|
+
if (disposition === "conflict")
|
|
444
|
+
throw new ExecutionOutcomeConflictError(facts.executionOutcome, gate);
|
|
445
|
+
if (disposition !== "commit")
|
|
446
|
+
return disposition;
|
|
447
|
+
const res = await this.db.query(this.q("UPDATE checkpoint SET execution_outcome = ?, execution_at_ms = ? WHERE token = ? AND scope = ? AND status = 'resolved' AND execution_outcome IS NULL", "UPDATE checkpoint SET execution_outcome = $1::jsonb, execution_at_ms = $2 WHERE token = $3 AND scope = $4 AND status = 'resolved' AND execution_outcome IS NULL"), [this.json(gate, "execution outcome"), Date.now(), token, scope]);
|
|
448
|
+
if (res.affected === 1)
|
|
449
|
+
return "recorded";
|
|
450
|
+
}
|
|
451
|
+
throw new Error(`recordExecutionOutcome lost the execution-record CAS ${EXECUTION_RECORD_CAS_ATTEMPTS} times on checkpoint ${tokenFingerprint(token)} — ` +
|
|
452
|
+
"refusing to answer with one of the four contract words (that would invent an account of a write that did not happen)");
|
|
453
|
+
}
|
|
398
454
|
async setPendingSteer(token, scope, steer) {
|
|
399
455
|
const clean = validatePendingSteer(steer);
|
|
400
456
|
for (let attempt = 0; attempt < STEER_CAS_ATTEMPTS; attempt++) {
|
|
@@ -529,8 +585,8 @@ export class SqlCheckpointStore {
|
|
|
529
585
|
return out;
|
|
530
586
|
}
|
|
531
587
|
async findDecidedApprovalsForBinding(sessionId, boundCallId) {
|
|
532
|
-
const { rows } = await this.db.query(this.q("SELECT scope, bound_input_hash, outcome FROM checkpoint " +
|
|
533
|
-
`WHERE session_id=? AND tool_call_id=? AND status='resolved' AND decided_at_ms IS NOT NULL ORDER BY decided_at_ms DESC LIMIT ${DECIDED_REPLAY_SCAN_CAP}`, "SELECT scope, bound_input_hash, outcome FROM checkpoint " +
|
|
588
|
+
const { rows } = await this.db.query(this.q("SELECT scope, bound_input_hash, outcome, execution_outcome FROM checkpoint " +
|
|
589
|
+
`WHERE session_id=? AND tool_call_id=? AND status='resolved' AND decided_at_ms IS NOT NULL ORDER BY decided_at_ms DESC LIMIT ${DECIDED_REPLAY_SCAN_CAP}`, "SELECT scope, bound_input_hash, outcome, execution_outcome FROM checkpoint " +
|
|
534
590
|
`WHERE session_id=$1 AND tool_call_id=$2 AND status='resolved' AND decided_at_ms IS NOT NULL ORDER BY decided_at_ms DESC LIMIT ${DECIDED_REPLAY_SCAN_CAP}`), [sessionId, boundCallId]);
|
|
535
591
|
return rows.map((r) => {
|
|
536
592
|
let winner;
|
|
@@ -541,11 +597,20 @@ export class SqlCheckpointStore {
|
|
|
541
597
|
catch (e) {
|
|
542
598
|
this.logger?.info?.("checkpoint_decided_outcome_unreadable", { sessionId, error: e instanceof Error ? e.message : String(e) });
|
|
543
599
|
}
|
|
600
|
+
let executed = null;
|
|
601
|
+
try {
|
|
602
|
+
const verdict = executionVerdict({ ...((v) => (v === null ? {} : { executionOutcome: v }))(parseJson(r.execution_outcome)) });
|
|
603
|
+
executed = verdict.kind === "executed" ? verdict.gate : null;
|
|
604
|
+
}
|
|
605
|
+
catch (e) {
|
|
606
|
+
this.logger?.info?.("checkpoint_execution_outcome_unreadable", { sessionId, error: e instanceof Error ? e.message : String(e) });
|
|
607
|
+
}
|
|
544
608
|
return {
|
|
545
609
|
scope: String(r.scope),
|
|
546
610
|
boundInputHash: r.bound_input_hash == null ? null : String(r.bound_input_hash),
|
|
547
611
|
decision: approvalDecisionOfWinner(winner, boundCallId),
|
|
548
612
|
payloadFp: winner === undefined ? null : approvalPayloadFingerprint(winner),
|
|
613
|
+
executionOutcome: executed,
|
|
549
614
|
};
|
|
550
615
|
});
|
|
551
616
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Checkpoint, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ReopenReason, type ResolveExpectation, type ResumeOutcome, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
|
|
1
|
+
import { type Checkpoint, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ReopenReason, type ResolveExpectation, type ExecutionOutcomeRecordWord, type GateOutcome, type ResumeOutcome, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
|
|
2
2
|
import { type DecidedApprovalRecord, type PendingCheckpoint, type PendingRowBinding } from "./checkpoint-store-sql.js";
|
|
3
3
|
export interface LocalCheckpointStoreOptions {
|
|
4
4
|
/** taskId join for the pending card (the local FileRunStore's `getActiveTaskId`). Absent ⇒ `taskId: null`. */
|
|
@@ -40,6 +40,20 @@ export declare class LocalCheckpointStore {
|
|
|
40
40
|
readonly redecision: {
|
|
41
41
|
readonly reopen: true;
|
|
42
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* `CheckpointStore.execution` 声明(core 7.7.0 [ref];S-160)——**必填**,与上面三格同一条判据:**如实
|
|
45
|
+
* 按内核判**。内核 core `FileCheckpointStore` 自己声明 `{ outcome: true }`,并把执行记录落成 ledger 的
|
|
46
|
+
* 一个新事件种类(`execution`,与 resolve 同款「先 append+fsync 再翻内存」的崩溃序),
|
|
47
|
+
* {@link LocalCheckpointStore.recordExecutionOutcome} 与 {@link LocalCheckpointStore.reopen} 都是纯委派
|
|
48
|
+
* ⇒ 抄内核的读数是诚实的。包装类**不继承**被包装者的声明(委派不是子类),所以这一格必须自己写。
|
|
49
|
+
*
|
|
50
|
+
* ⚠️ 升级序的诚实披露(DEPLOY-PREREQS 同句):ledger 里一旦出现 `execution` 事件,**7.7.0 之前的
|
|
51
|
+
* binary 会拒绝重放这个目录**(响亮,不是静默跳过);而压缩把事件折进快照行之后,那个旧 binary 读到
|
|
52
|
+
* 一个它不认识的字段、且它的 `reopen` 不认这条记录 —— 跨记录行回滚不受支持。
|
|
53
|
+
*/
|
|
54
|
+
readonly execution: {
|
|
55
|
+
readonly outcome: true;
|
|
56
|
+
};
|
|
43
57
|
private readonly inner;
|
|
44
58
|
private readonly scopesPath;
|
|
45
59
|
private readonly sessionTokensPath;
|
|
@@ -63,6 +77,10 @@ export declare class LocalCheckpointStore {
|
|
|
63
77
|
get(token: CheckpointToken): Promise<Checkpoint | null>;
|
|
64
78
|
resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise<boolean>;
|
|
65
79
|
reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
|
|
80
|
+
/** core 7.7.0 [ref](S-160):执行判词的 CAS —— 语义、四词、幂等的结构相等与冲突抛全在被包的内核里
|
|
81
|
+
* (它自己走 core 的共享判词 `executionRecordDisposition`,ledger 事件 `execution` 是它的 commit 半场),
|
|
82
|
+
* 本 wrapper 只做转发。服务扩展面(运维队列/会话索引)与这条记录无关:它不改 status,也不新增可枚举行。 */
|
|
83
|
+
recordExecutionOutcome(token: CheckpointToken, scope: string, gate: GateOutcome): Promise<ExecutionOutcomeRecordWord>;
|
|
66
84
|
/** core 5.14.0([ref]):入参是 `PendingSteerInput`(text/trusted + 可选 actor/inputId/priority),不是
|
|
67
85
|
* 两键单座 —— 形参窄成 `{text,trusted}` 会**静默丢掉**署名与幂等键(TS 的方法参数是双变的,窄形不
|
|
68
86
|
* 报错,只是把三个可选键从类型上抹掉,调用点再也传不进来)。队列语义与边界全在被包的 core
|