@sema-agent/client-core 0.40.0 → 0.42.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/CHANGELOG.md +450 -0
- package/README.md +1 -1
- package/dist/adapter/downstream/eventToSdkMessage.js +24 -0
- package/dist/attachmentsWireCaps.d.ts +43 -8
- package/dist/attachmentsWireCaps.js +64 -14
- package/dist/classifierVerdictWire.d.ts +0 -25
- package/dist/classifierVerdictWire.js +27 -7
- package/dist/engineAgentPanelStore.js +102 -13
- package/dist/hitl/approvalsFeed.d.ts +2 -2
- package/dist/hitl/approvalsFeed.js +57 -2
- package/dist/hitl/askGateWire.d.ts +4 -2
- package/dist/hitl/askGateWire.js +2 -0
- package/dist/hitl/editedRuleTextPrecheck.d.ts +102 -0
- package/dist/hitl/editedRuleTextPrecheck.js +91 -0
- package/dist/hitl/frameRouter.d.ts +21 -2
- package/dist/hitl/frameRouter.js +114 -12
- package/dist/hitl/gateLedger.d.ts +27 -0
- package/dist/hitl/gateLedger.js +64 -9
- package/dist/hitl/hitlBridge.d.ts +70 -14
- package/dist/hitl/hitlBridge.js +117 -34
- package/dist/hitl/hitlHostSurface.d.ts +33 -3
- package/dist/hitl/hitlHostSurface.js +33 -3
- package/dist/hitl/parkResolver.js +39 -13
- package/dist/hitl/toolApprovalWire.d.ts +133 -3
- package/dist/hitl/toolApprovalWire.js +143 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/model/catalogLoader.js +151 -45
- package/dist/printToolResultFrame.d.ts +19 -0
- package/dist/seatContract.d.ts +25 -1
- package/dist/seatContract.js +29 -2
- package/dist/subagent/engineDelegatedPrompt.js +10 -3
- package/dist/systemReminderTag.d.ts +52 -0
- package/dist/systemReminderTag.js +73 -0
- package/docs/INTEGRATION-CLIENTS.md +123 -7
- package/package.json +2 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* editedRuleTextPrecheck.ts — 编辑臂的「边打字边校验」判官**转出口**(#225 / [5076] 自领件,0.42.0)。
|
|
3
|
+
*
|
|
4
|
+
* ## 这一格要解决的问题
|
|
5
|
+
* 卡上的规则编辑框想在人还在打字的时候就说「这条能不能提交」。唯一合法的判官是引擎自己那只 ——
|
|
6
|
+
* core 5.57.0 导出的纯函数 `precheckEditedRuleText(text, command)`,它与 `confirmRuleApproval` 的
|
|
7
|
+
* fresh-edit 臂**跑同一个函数体**(`checkEditedRuleText`),所以预检面与真裁判**结构性不可分歧**。
|
|
8
|
+
* 在边界上复读一遍 `parseAllowRuleText` 是**装第二个更严的判官**:`Bash(adb *)` 这类肌肉记忆形
|
|
9
|
+
* 会被它当场误杀,而编辑面本来先跑拼写归一(`normalizeEditedSpelling`)再接受
|
|
10
|
+
* ——[5071] 定界②的原话,core [5075] 复核确认。
|
|
11
|
+
*
|
|
12
|
+
* ## 🔴 为什么是**端口注入**而不是 `export { precheckEditedRuleText } from '@sema-agent/core'`
|
|
13
|
+
* [5076] 的自领件原话是「re-export + 类型面」。**逐字照做在本包里是不可能的**,而且不是口味问题,
|
|
14
|
+
* 是本仓门族的硬约束(0.42.0 施工时实测,证据两条):
|
|
15
|
+
* · `scripts/run-client-core-portability-test.mjs` 的 `EXPECTED_PACKAGES_INDEX` 是**等值门**
|
|
16
|
+
* ——包总入口闭包的外部包集合恒等于 `{diff, @sema-agent/sdk}`,多一个当场红;
|
|
17
|
+
* · 同门 ③ 段拿 esbuild `--platform=browser` **真打一次包**。`@sema-agent/core` 的 barrel 值级
|
|
18
|
+
* 拉进 `node:crypto`(`engine/session/log-digest.js`)、`node:fs`/`node:path`
|
|
19
|
+
* (`core/skills-directory.js`)…… 浏览器腿当场打不成。
|
|
20
|
+
* ⇒ 一个 value 级 re-export 会把**整台引擎**焊进每一个装 client-core 的端(web/desktop 首当其冲),
|
|
21
|
+
* 换来一只纯函数。本包的存在理由正是「三端共用的**客户端**运行时」,这条边不能连。
|
|
22
|
+
*
|
|
23
|
+
* 于是转出口取**同等效力的第二形**:
|
|
24
|
+
* ① **类型面**逐形转出({@link EditedRuleTextPrecheck} / {@link EditedRuleTextPrechecker}),
|
|
25
|
+
* 三端从此对着同一份形写代码,谁都不用自己抄一份返回型;
|
|
26
|
+
* ② **注入口**({@link installEditedRuleTextPrechecker}):Node 宿主(TUI / desktop 主进程 /
|
|
27
|
+
* server 侧渲染)在启动时把 core 那只函数原样装进来 —— **原样装,不许包一层判断**;
|
|
28
|
+
* ③ **读口**({@link precheckEditedRuleText}):没装 ⇒ 返 `undefined`(**诚实缺席**),
|
|
29
|
+
* 绝不返一个编出来的 `{ok:true}`。缺席时呈现面的正解是退「提交后才知道」的往返形
|
|
30
|
+
* ——[5076] 施工要点③ 已经把这两段写成互不阻塞的两腿。
|
|
31
|
+
* 🔴 浏览器 lane(web)结构上装不了这只函数(引擎不在那一侧),它**本就该**留缺席走往返形;
|
|
32
|
+
* 本口的缺席语义因此不是「宿主忘了装」的同义词,别拿它去判部署形态。
|
|
33
|
+
*
|
|
34
|
+
* ## 🔴 两点语义预披露(core [5075],消费侧写文案前必读;[5076] 已落账为 #225 施工要点)
|
|
35
|
+
* · `ok` = **可提交**,不是「confirm 必成」。记录级闸(binding 回声 / 记录 owner 与 state /
|
|
36
|
+
* scope 继承 / 部署开关)**不在**预检覆盖面,由 `confirmRuleApproval` 对着记录重判。
|
|
37
|
+
* ⇒ UI 文案只许写「可提交」,**绝不**写「将被批准」。
|
|
38
|
+
* · `canonicalRule` 可能与输入**字节不同**(拼写归一)。它是真正会落库的那一形,所以内联反馈
|
|
39
|
+
* 应当显示 **canonical 形**,而不是把人的输入原样回显。
|
|
40
|
+
*
|
|
41
|
+
* ## 调用面契约(core 逐字,注入口不许替它软化)
|
|
42
|
+
* · `text` 是**人**的输入 ⇒ 任何畸形拼写都是一个**答案**(refusal),永不抛;
|
|
43
|
+
* · `command` 是**调用方**的上下文(卡所依据的被裁决命令)⇒ 缺席/非串是**调用方 bug**,
|
|
44
|
+
* core 响亮抛(`config.invalid_argument`)。本包的读口**不吞这个抛**(见 {@link precheckEditedRuleText}
|
|
45
|
+
* 的 `@throws`):把它折成一个 refusal 会让人读到「你的规则不接受空命令」——把调用方的错
|
|
46
|
+
* 误归到人头上,正是 core 那段头注点名要避免的事。
|
|
47
|
+
* · 卡上**压根没有被裁决命令**(老记录 / 店丢了这一列)⇒ 那是**记录级**拒绝,不该问本函数;
|
|
48
|
+
* 宿主此时的正解是**根本不给编辑框**。
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* `text × command` 三步门(拼写归一 → 语法门 → coverage 闸)的判决 —— core 5.57.0
|
|
52
|
+
* `EditedRuleTextPrecheck` 的**逐形镜像**(结构等价,故 core 那只函数可直接装进
|
|
53
|
+
* {@link EditedRuleTextPrechecker} 而无需任何适配)。
|
|
54
|
+
*
|
|
55
|
+
* 🔴 `code` 的**在场规则**与提交面的 `edit_rejected.detail` 同源:验证器自己拒时**在场**,
|
|
56
|
+
* coverage 闸拒时**缺席** —— 同一具函数体出来的同一批值。缺席不许被读成「没有理由」。
|
|
57
|
+
* 🔴 `code` 是**开集串**:core 的 `RuleRejectCode` 词表属主是引擎,本包**刻意不镜像那张枚举**
|
|
58
|
+
* (镜像 = 引擎加员当天把一个合法值判没,#157 词表纪律的反面)。要分支就按串比,未知值原样呈现。
|
|
59
|
+
*/
|
|
60
|
+
export type EditedRuleTextPrecheck = {
|
|
61
|
+
ok: true;
|
|
62
|
+
/** 真正会落库的那一形(可能与输入字节不同 —— 拼写归一)。内联反馈显示**这一形**。 */
|
|
63
|
+
canonicalRule: string;
|
|
64
|
+
} | {
|
|
65
|
+
ok: false;
|
|
66
|
+
/** 验证器自己拒时在场;coverage 闸拒时缺席(开集串,见类型注)。 */
|
|
67
|
+
code?: string;
|
|
68
|
+
/** 拒绝措辞(与 `edit_rejected.detail` 同词汇)。原样呈现,包边界零加工。 */
|
|
69
|
+
message: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* 判官的形:`(text, command) => 判决`。**唯一合法的实参** = core 5.57.0 导出的
|
|
73
|
+
* `precheckEditedRuleText`,原样装,不许在外面包一层自己的判断
|
|
74
|
+
* ——包一层就是把「同一函数体」这条唯一的抗漂移保证亲手拆掉。
|
|
75
|
+
*/
|
|
76
|
+
export type EditedRuleTextPrechecker = (text: string, command: string) => EditedRuleTextPrecheck;
|
|
77
|
+
/**
|
|
78
|
+
* 装/卸判官。返回**还原函数**(装口族统一姿势:还原到装之前那一只,不是无脑清空 —— 嵌套装载
|
|
79
|
+
* 时无脑清空会把外层那只一起抹掉)。
|
|
80
|
+
*
|
|
81
|
+
* Node 宿主的装法(**逐字**,别加工):
|
|
82
|
+
* ```ts
|
|
83
|
+
* import { precheckEditedRuleText } from '@sema-agent/core'
|
|
84
|
+
* installEditedRuleTextPrechecker(precheckEditedRuleText)
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare function installEditedRuleTextPrechecker(fn: EditedRuleTextPrechecker | null): () => void;
|
|
88
|
+
/**
|
|
89
|
+
* 装没装(存在性读口哨兵:`boolean` 形 —— 见 `docs/INTEGRATION-CLIENTS.md` §5a 的三形分野)。
|
|
90
|
+
* 呈现面据它决定**给不给编辑框的内联反馈**;`false` 时编辑框本身仍然可用(走往返形)。
|
|
91
|
+
*/
|
|
92
|
+
export declare function hasEditedRuleTextPrechecker(): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* 预检一段编辑过的规则文本。**没装判官 ⇒ 返 `undefined`**(诚实缺席,不是 `{ok:true}`
|
|
95
|
+
* 也不是 `{ok:false}` —— 两者都是在替一只不在场的判官发言)。
|
|
96
|
+
*
|
|
97
|
+
* @throws core 判官对**调用方 bug**(`command` 缺席/非串/不可渲染)的响亮拒**原样上抛**
|
|
98
|
+
* ——本包不吞它:吞掉会让一个调用方错误变成一句对人的判决(见模块头注「调用面契约」)。
|
|
99
|
+
*/
|
|
100
|
+
export declare function precheckEditedRuleText(text: string, command: string): EditedRuleTextPrecheck | undefined;
|
|
101
|
+
/** 测试钩:卸口。 */
|
|
102
|
+
export declare function _resetEditedRuleTextPrecheckerForTest(): void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* editedRuleTextPrecheck.ts — 编辑臂的「边打字边校验」判官**转出口**(#225 / [5076] 自领件,0.42.0)。
|
|
3
|
+
*
|
|
4
|
+
* ## 这一格要解决的问题
|
|
5
|
+
* 卡上的规则编辑框想在人还在打字的时候就说「这条能不能提交」。唯一合法的判官是引擎自己那只 ——
|
|
6
|
+
* core 5.57.0 导出的纯函数 `precheckEditedRuleText(text, command)`,它与 `confirmRuleApproval` 的
|
|
7
|
+
* fresh-edit 臂**跑同一个函数体**(`checkEditedRuleText`),所以预检面与真裁判**结构性不可分歧**。
|
|
8
|
+
* 在边界上复读一遍 `parseAllowRuleText` 是**装第二个更严的判官**:`Bash(adb *)` 这类肌肉记忆形
|
|
9
|
+
* 会被它当场误杀,而编辑面本来先跑拼写归一(`normalizeEditedSpelling`)再接受
|
|
10
|
+
* ——[5071] 定界②的原话,core [5075] 复核确认。
|
|
11
|
+
*
|
|
12
|
+
* ## 🔴 为什么是**端口注入**而不是 `export { precheckEditedRuleText } from '@sema-agent/core'`
|
|
13
|
+
* [5076] 的自领件原话是「re-export + 类型面」。**逐字照做在本包里是不可能的**,而且不是口味问题,
|
|
14
|
+
* 是本仓门族的硬约束(0.42.0 施工时实测,证据两条):
|
|
15
|
+
* · `scripts/run-client-core-portability-test.mjs` 的 `EXPECTED_PACKAGES_INDEX` 是**等值门**
|
|
16
|
+
* ——包总入口闭包的外部包集合恒等于 `{diff, @sema-agent/sdk}`,多一个当场红;
|
|
17
|
+
* · 同门 ③ 段拿 esbuild `--platform=browser` **真打一次包**。`@sema-agent/core` 的 barrel 值级
|
|
18
|
+
* 拉进 `node:crypto`(`engine/session/log-digest.js`)、`node:fs`/`node:path`
|
|
19
|
+
* (`core/skills-directory.js`)…… 浏览器腿当场打不成。
|
|
20
|
+
* ⇒ 一个 value 级 re-export 会把**整台引擎**焊进每一个装 client-core 的端(web/desktop 首当其冲),
|
|
21
|
+
* 换来一只纯函数。本包的存在理由正是「三端共用的**客户端**运行时」,这条边不能连。
|
|
22
|
+
*
|
|
23
|
+
* 于是转出口取**同等效力的第二形**:
|
|
24
|
+
* ① **类型面**逐形转出({@link EditedRuleTextPrecheck} / {@link EditedRuleTextPrechecker}),
|
|
25
|
+
* 三端从此对着同一份形写代码,谁都不用自己抄一份返回型;
|
|
26
|
+
* ② **注入口**({@link installEditedRuleTextPrechecker}):Node 宿主(TUI / desktop 主进程 /
|
|
27
|
+
* server 侧渲染)在启动时把 core 那只函数原样装进来 —— **原样装,不许包一层判断**;
|
|
28
|
+
* ③ **读口**({@link precheckEditedRuleText}):没装 ⇒ 返 `undefined`(**诚实缺席**),
|
|
29
|
+
* 绝不返一个编出来的 `{ok:true}`。缺席时呈现面的正解是退「提交后才知道」的往返形
|
|
30
|
+
* ——[5076] 施工要点③ 已经把这两段写成互不阻塞的两腿。
|
|
31
|
+
* 🔴 浏览器 lane(web)结构上装不了这只函数(引擎不在那一侧),它**本就该**留缺席走往返形;
|
|
32
|
+
* 本口的缺席语义因此不是「宿主忘了装」的同义词,别拿它去判部署形态。
|
|
33
|
+
*
|
|
34
|
+
* ## 🔴 两点语义预披露(core [5075],消费侧写文案前必读;[5076] 已落账为 #225 施工要点)
|
|
35
|
+
* · `ok` = **可提交**,不是「confirm 必成」。记录级闸(binding 回声 / 记录 owner 与 state /
|
|
36
|
+
* scope 继承 / 部署开关)**不在**预检覆盖面,由 `confirmRuleApproval` 对着记录重判。
|
|
37
|
+
* ⇒ UI 文案只许写「可提交」,**绝不**写「将被批准」。
|
|
38
|
+
* · `canonicalRule` 可能与输入**字节不同**(拼写归一)。它是真正会落库的那一形,所以内联反馈
|
|
39
|
+
* 应当显示 **canonical 形**,而不是把人的输入原样回显。
|
|
40
|
+
*
|
|
41
|
+
* ## 调用面契约(core 逐字,注入口不许替它软化)
|
|
42
|
+
* · `text` 是**人**的输入 ⇒ 任何畸形拼写都是一个**答案**(refusal),永不抛;
|
|
43
|
+
* · `command` 是**调用方**的上下文(卡所依据的被裁决命令)⇒ 缺席/非串是**调用方 bug**,
|
|
44
|
+
* core 响亮抛(`config.invalid_argument`)。本包的读口**不吞这个抛**(见 {@link precheckEditedRuleText}
|
|
45
|
+
* 的 `@throws`):把它折成一个 refusal 会让人读到「你的规则不接受空命令」——把调用方的错
|
|
46
|
+
* 误归到人头上,正是 core 那段头注点名要避免的事。
|
|
47
|
+
* · 卡上**压根没有被裁决命令**(老记录 / 店丢了这一列)⇒ 那是**记录级**拒绝,不该问本函数;
|
|
48
|
+
* 宿主此时的正解是**根本不给编辑框**。
|
|
49
|
+
*/
|
|
50
|
+
let installed = null;
|
|
51
|
+
/**
|
|
52
|
+
* 装/卸判官。返回**还原函数**(装口族统一姿势:还原到装之前那一只,不是无脑清空 —— 嵌套装载
|
|
53
|
+
* 时无脑清空会把外层那只一起抹掉)。
|
|
54
|
+
*
|
|
55
|
+
* Node 宿主的装法(**逐字**,别加工):
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { precheckEditedRuleText } from '@sema-agent/core'
|
|
58
|
+
* installEditedRuleTextPrechecker(precheckEditedRuleText)
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function installEditedRuleTextPrechecker(fn) {
|
|
62
|
+
const prev = installed;
|
|
63
|
+
installed = fn;
|
|
64
|
+
return () => {
|
|
65
|
+
installed = prev;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 装没装(存在性读口哨兵:`boolean` 形 —— 见 `docs/INTEGRATION-CLIENTS.md` §5a 的三形分野)。
|
|
70
|
+
* 呈现面据它决定**给不给编辑框的内联反馈**;`false` 时编辑框本身仍然可用(走往返形)。
|
|
71
|
+
*/
|
|
72
|
+
export function hasEditedRuleTextPrechecker() {
|
|
73
|
+
return installed !== null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 预检一段编辑过的规则文本。**没装判官 ⇒ 返 `undefined`**(诚实缺席,不是 `{ok:true}`
|
|
77
|
+
* 也不是 `{ok:false}` —— 两者都是在替一只不在场的判官发言)。
|
|
78
|
+
*
|
|
79
|
+
* @throws core 判官对**调用方 bug**(`command` 缺席/非串/不可渲染)的响亮拒**原样上抛**
|
|
80
|
+
* ——本包不吞它:吞掉会让一个调用方错误变成一句对人的判决(见模块头注「调用面契约」)。
|
|
81
|
+
*/
|
|
82
|
+
export function precheckEditedRuleText(text, command) {
|
|
83
|
+
const fn = installed;
|
|
84
|
+
if (fn === null)
|
|
85
|
+
return undefined;
|
|
86
|
+
return fn(text, command);
|
|
87
|
+
}
|
|
88
|
+
/** 测试钩:卸口。 */
|
|
89
|
+
export function _resetEditedRuleTextPrecheckerForTest() {
|
|
90
|
+
installed = null;
|
|
91
|
+
}
|
|
@@ -24,8 +24,16 @@ import type { GateLedger } from './gateLedger.js';
|
|
|
24
24
|
* 🔴 pure 门 B7 段:包内冻结字面量(无条件)+ 壳树全树扫描「每一处声明都逐字节相同」(壳树缺席=跳过)。
|
|
25
25
|
*/
|
|
26
26
|
export declare const HITL_REJECT_MESSAGE = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
27
|
+
/**
|
|
28
|
+
* core 对**被 gate park 的那个 call**(gate 主角)铸的 tool_end 载体(逐字;desktop session-host
|
|
29
|
+
* 真引擎实测同款)。HOLD 谓词锚它做**精确等值** —— 普通工具错的输出是各自的错误文案,永不进 HOLD。
|
|
30
|
+
*
|
|
31
|
+
* 🔴 **勘误(件⑤,2026-08-22,[4970] 真机取证)**:此处原话是「core 对被 gate/**连坐** abort 的
|
|
32
|
+
* call **恒**铸这一串」——对连坐的旁观者**不成立**。core 5.52.0 dist 直证两个短路点两种文案:
|
|
33
|
+
* gate 主角走 abort 早退 = 本串;**执行前被连坐**的旁观者 = `"operation aborted before execution"`
|
|
34
|
+
* (小写 + 后缀)。所以本串只是判据的**兜底臂**(旧 core 唯一的信号),主锚是机读码
|
|
35
|
+
* `gate.parked`——见 {@link isEngineAbortToolEnd}。
|
|
36
|
+
*/
|
|
29
37
|
export declare const ENGINE_ABORT_TOOL_RESULT = "Operation aborted";
|
|
30
38
|
/**
|
|
31
39
|
* tool_end.output 的文本归一(P0 案B,2026-08-03,server 6.0.0 wire 实测):sync leg 的毒化帧
|
|
@@ -63,6 +71,16 @@ export interface GatePark {
|
|
|
63
71
|
gate: GateKind;
|
|
64
72
|
/** sync leg 的 `done{suspended}` 终帧 —— fail-soft 回退时原样吐回去(durable leg 的 park 无此帧)。 */
|
|
65
73
|
pendingDone?: AgentEvent | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* 这张 park 的**待批 call 身份**(server ≥7.41.0,[4913]/[4914] 请托① 到货件)。
|
|
76
|
+
* 供给见 {@link parkGatedCallId};消费者今天只有一个:`parkResolver` 决断成功臂的
|
|
77
|
+
* `dropHeldForDecidedPark` —— 它此前只能拿 `findPendingForTask` 那个**卡面渲染宽口**当身份,
|
|
78
|
+
* 同族多行排队时会摘错 call 的扣留帧([4933] 立案的存量)。
|
|
79
|
+
* 🔴 缺席是**常态**(旧引擎 / tool-less park / 键畸形),消费方必须自带无身份也能走的臂。
|
|
80
|
+
* 🔴 刻意**不**喂给 `#324` 的连坐判别子:那条判据的换锚是独立一件事(S3/S33 的「已知局限锁」
|
|
81
|
+
* 断言到时会翻红 = 点亮信号),两件事混在一起改就分不清谁引入了假阳。
|
|
82
|
+
*/
|
|
83
|
+
gatedCallId?: string | undefined;
|
|
66
84
|
}
|
|
67
85
|
/** 帧路由的产物:驱动只认这四种,自己不再判帧型。 */
|
|
68
86
|
export type FrameAction = {
|
|
@@ -74,6 +92,7 @@ export type FrameAction = {
|
|
|
74
92
|
kind: 'park';
|
|
75
93
|
gate: GateKind;
|
|
76
94
|
pendingDone?: AgentEvent | undefined;
|
|
95
|
+
gatedCallId?: string | undefined;
|
|
77
96
|
} | {
|
|
78
97
|
kind: 'end';
|
|
79
98
|
events: readonly AgentEvent[];
|
package/dist/hitl/frameRouter.js
CHANGED
|
@@ -11,8 +11,16 @@ export const HITL_REJECT_MESSAGE = "The user doesn't want to proceed with this t
|
|
|
11
11
|
function rejectMessageForRender() {
|
|
12
12
|
return HITL_REJECT_MESSAGE;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
14
|
+
/**
|
|
15
|
+
* core 对**被 gate park 的那个 call**(gate 主角)铸的 tool_end 载体(逐字;desktop session-host
|
|
16
|
+
* 真引擎实测同款)。HOLD 谓词锚它做**精确等值** —— 普通工具错的输出是各自的错误文案,永不进 HOLD。
|
|
17
|
+
*
|
|
18
|
+
* 🔴 **勘误(件⑤,2026-08-22,[4970] 真机取证)**:此处原话是「core 对被 gate/**连坐** abort 的
|
|
19
|
+
* call **恒**铸这一串」——对连坐的旁观者**不成立**。core 5.52.0 dist 直证两个短路点两种文案:
|
|
20
|
+
* gate 主角走 abort 早退 = 本串;**执行前被连坐**的旁观者 = `"operation aborted before execution"`
|
|
21
|
+
* (小写 + 后缀)。所以本串只是判据的**兜底臂**(旧 core 唯一的信号),主锚是机读码
|
|
22
|
+
* `gate.parked`——见 {@link isEngineAbortToolEnd}。
|
|
23
|
+
*/
|
|
16
24
|
export const ENGINE_ABORT_TOOL_RESULT = 'Operation aborted';
|
|
17
25
|
/**
|
|
18
26
|
* tool_end.output 的文本归一(P0 案B,2026-08-03,server 6.0.0 wire 实测):sync leg 的毒化帧
|
|
@@ -82,6 +90,56 @@ function isGatedToolName(name) {
|
|
|
82
90
|
function isFsOrShellToolName(name) {
|
|
83
91
|
return typeof name === 'string' && (toolNameIsFsWrite(name) || toolNameIsShellExec(name));
|
|
84
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* core 对**因 gate park 而中止**的 call 铸的机读短码(`tool_end.errorCode`,core ≥5.9.0)。
|
|
95
|
+
*
|
|
96
|
+
* 🔴 **契约,不是巧合**(core 官方答复 [4973]):两个 abort 短路点(gate 主角 / 执行前被连坐的
|
|
97
|
+
* 旁观者)**消费同一个分类席**,而 park 提交**先于** abort 发射 —— 铸点读那一席时无竞态,
|
|
98
|
+
* 于是主角与旁观者**恒**同席同码。所以本码可以当**主锚**用,而不是「我们在消费侧观察到它总在」
|
|
99
|
+
* 那种推断。SDK 的 `Event_tool_end.errorCode` 声明处也逐字点名这个码 =「门把这次调用 park 了」;
|
|
100
|
+
* 供给点见 core 5.52.0 dist `prepare-task` 的 `abortResultDetails: () => ({ code: "gate.parked" })`。
|
|
101
|
+
*
|
|
102
|
+
* 🔴 开集里的**具名一员**,不是前缀族:判据只认这一个码。放宽成「有 errorCode 就算」会把
|
|
103
|
+
* `path_not_in_root` 这类普通工具错拖进 HOLD —— 那正是 [2084]①-b 收窄掉的病(真错迟到上屏)。
|
|
104
|
+
* 常驻反证:hitl 门 F11-d(表外码的普通工具错必须**零滞后**上屏,判据锚在帧的**位置**上)。
|
|
105
|
+
*
|
|
106
|
+
* 🔴 **在场 ≠ 这次 park 有 call 身份**([4973]③):plan-review / resource-slice 这类不持有 call
|
|
107
|
+
* 的 park 照样带本码,但诚实缺席 call id。所以本判据只锚 errorCode,**不**顺手去锚任何 id —— 锚了
|
|
108
|
+
* 就等于把那几族 park 的连坐帧判成「认不出」。(core 5.55 的 `tool_end.gatedCallId` **顶层键**
|
|
109
|
+
* 到货后可用于「折叠归组」那一面,与本判据是两件事,跟车另批。)
|
|
110
|
+
*/
|
|
111
|
+
const ENGINE_GATE_PARKED_ERROR_CODE = 'gate.parked';
|
|
112
|
+
/**
|
|
113
|
+
* 「这一帧是**引擎因 gate 而中止**的毒化帧」——双判据 OR(件⑤,2026-08-22,[4970] G1 端到端 RED)。
|
|
114
|
+
*
|
|
115
|
+
* 🔴 病形(test 线真机取证:真 server 7.41.0 + 真 Postgres,1 Bash 待审批 + 2 并行 WebFetch):
|
|
116
|
+
* 判据原来只有下面第②条(精确串),而 core 的**两个短路点产两种串**——
|
|
117
|
+
* · gate 主角(`agent-loop` abort 早退,dist 两处)= `"Operation aborted"`(精确);
|
|
118
|
+
* · **同批被连坐的旁观者**(执行前短路)= `"operation aborted before execution"`(小写 + 后缀)。
|
|
119
|
+
* ⇒ 判据对旁观者**恰好全不命中**:它们的毒化帧根本没进 HOLD(当场上屏 + 被 generic-close 记
|
|
120
|
+
* `markEnded`,resume 后重放的真结果被静默去重),而 #324 的连坐机读位在真实现网**一次都没出现过**。
|
|
121
|
+
* 本文件旧注写的「core 对被 gate/连坐 abort 的 call **恒**铸这一串」对旁观者不成立,已按实证撤回。
|
|
122
|
+
*
|
|
123
|
+
* 两臂各自的适用域(都**不**放宽成模糊匹配):
|
|
124
|
+
* ① **主锚** `errorCode === "gate.parked"`([4973] 契约:双短路点同席恒铸)—— 机读位,
|
|
125
|
+
* 两种文案都覆盖,core ≥5.9.0 起在场;
|
|
126
|
+
* ② **兜底** 输出等于 {@link ENGINE_ABORT_TOOL_RESULT} —— 覆盖两类:老 core(还没有 errorCode
|
|
127
|
+
* 这个位)**以及** core 现役唯一**不铸**本码的那一条臂 —— **post-hook signal re-check**
|
|
128
|
+
* ([4973]②:工具**已经执行完**、hook 之后签核那一拍才发现 abort,历来铸的 abort 结果
|
|
129
|
+
* 不带 details ⇒ 无 errorCode)。这条臂正是双判据设计成立的理由:少了兜底臂,它整条落空。
|
|
130
|
+
* 🔴 缺席不反推:两臂都不命中 = 这一帧没有「因 gate 中止」的任何证据,照旧走普通收口
|
|
131
|
+
* (老 core + 变体串就落在这一格 —— 诚实缺席,不靠猜文案家族把普通工具错也收进来)。
|
|
132
|
+
*
|
|
133
|
+
* 🔴 **两串族不会被上游合并**([4973],core 显式拒绝改串向,**不是**「core 待修」):两条串各自
|
|
134
|
+
* 承的是真语义 —— `"Operation aborted"` = **执行中被中止**,`"operation aborted before execution"`
|
|
135
|
+
* = **从未执行**。消费端要的是「归不归 gate 管」这个判定,那件事归机读码;文案面归 core 的语义面,
|
|
136
|
+
* 两者不该互相迁就。
|
|
137
|
+
*/
|
|
138
|
+
function isEngineAbortToolEnd(ev) {
|
|
139
|
+
if (ev.errorCode === ENGINE_GATE_PARKED_ERROR_CODE)
|
|
140
|
+
return true;
|
|
141
|
+
return toolEndOutputText(ev.output) === ENGINE_ABORT_TOOL_RESULT;
|
|
142
|
+
}
|
|
85
143
|
/**
|
|
86
144
|
* 「这一帧 tool_end 归 gate 管」的判据 —— 上面那条**名字腿**加一条 kind 腿的替身([2393] hitl-F1,
|
|
87
145
|
* 2026-08-02)。
|
|
@@ -96,9 +154,10 @@ function isFsOrShellToolName(name) {
|
|
|
96
154
|
* 🔴 为什么不能在名字腿里补 kind:`tool_end` 帧上**根本没有 kind 位** —— gate 的 kind 只在
|
|
97
155
|
* `done{suspended}.checkpointGate` / `suspended.gate` 上,而那两帧**晚于**毒化 tool_end 到达
|
|
98
156
|
* (park 帧序:tool_start → tool_end(毒化)→ done{suspended})。「park 时按 callId 打标、
|
|
99
|
-
* tool_end
|
|
100
|
-
*
|
|
101
|
-
*
|
|
157
|
+
* tool_end 时读台账」这条路对**这一帧**来得太晚。帧面上唯一可读的信号是引擎铸的 abort 标记
|
|
158
|
+
* ({@link isEngineAbortToolEnd} 的**双判据**:机读码 `gate.parked` ∪ 那条确切串 —— 件⑤ 之前
|
|
159
|
+
* 这里只有后者,而 core 对**旁观者**铸的是另一种文案,见该函数头注),所以本判据 =
|
|
160
|
+
* 名字腿 ∪ abort 标记腿。
|
|
102
161
|
*
|
|
103
162
|
* 🔴 集合关系(说清楚,别让下一棒再以为两边逐字相等):本判据是 park 判定的**超集**(它还会盖到
|
|
104
163
|
* 被同 turn 连坐 abort 的非 gate call)。方向是有意的,且不对称成立 —— 多盖一帧的代价是那一帧
|
|
@@ -108,6 +167,26 @@ function isFsOrShellToolName(name) {
|
|
|
108
167
|
* ⚠️ 普通工具错(非 abort 标记)照旧**不进** HOLD —— [2084]①-b 的收窄没有被放宽,负控 F1-c 钉着。
|
|
109
168
|
*/
|
|
110
169
|
function isGatedToolEnd(ev) {
|
|
170
|
+
if (isGatedToolName(ev.toolName))
|
|
171
|
+
return true;
|
|
172
|
+
return isEngineAbortToolEnd(ev); // 件⑤:abort 标记腿单源(机读码 ∪ 确切串)
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 上者的**窄口**,只给 ②`deny-stamp-next` 那条**位置性**判据用(件⑤ 的臂间掩蔽面,异源复审四审
|
|
176
|
+
* finding①)。
|
|
177
|
+
*
|
|
178
|
+
* 🔴 为什么这一条不能用超集谓词:deny stamp 是**一次性的全局记号**(同步审批帧腿 deny 且 callId
|
|
179
|
+
* 关联不上时才武装),语义是「**下一张** gated 报错帧就是那张门的收口帧」——**谁先到谁消费**。
|
|
180
|
+
* 件⑤ 把 abort 标记腿放宽到认 `gate.parked` 之后,同批被连坐的**旁观者**(WebFetch 之流)也成了
|
|
181
|
+
* 「gated 报错帧」:它先到就把记号吃掉、被改写成「用户拒绝了」并记 ended(它 resume 后的真结果
|
|
182
|
+
* 还会被去重),而真正被拒的那个 call 拿到的是引擎原文 —— 正是 [2393] hitl-F1 要修的那个用户面,
|
|
183
|
+
* 方向还反了。连坐帧带的是「我被 gate 连累中止」,**不是**「我就是那张被拒的门」,所以它没资格
|
|
184
|
+
* 认领一个**位置性**的身份记号。
|
|
185
|
+
* ⇒ 本判据 = 件⑤ **之前**的那一版(名字腿 ∪ 确切串),逐字保留;新加的连坐码臂**刻意不进来**。
|
|
186
|
+
* (确切串腿理论上也可能是一条正在执行中被连坐的帧 —— 那是本记号自带的既有近似,与件⑤ 无关,
|
|
187
|
+
* 不在本批放宽也不在本批收紧。)
|
|
188
|
+
*/
|
|
189
|
+
function isDenyStampCandidate(ev) {
|
|
111
190
|
if (isGatedToolName(ev.toolName))
|
|
112
191
|
return true;
|
|
113
192
|
return toolEndOutputText(ev.output) === ENGINE_ABORT_TOOL_RESULT;
|
|
@@ -133,6 +212,19 @@ function isFsApprovalPark(result) {
|
|
|
133
212
|
!isAskToolLoose(r.checkpointGate?.toolName) &&
|
|
134
213
|
isToolApprovalGate(r.checkpointGate));
|
|
135
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* park 帧上的**待批 call 身份**(server ≥7.41.0;[4914] 施工帖逐字:数据源 = checkpoint 本体的
|
|
217
|
+
* `pendingAction.toolCallId`,投影到 **`done{suspended}` 的 result 顶层**与 durable **`suspended`
|
|
218
|
+
* 事件**的 `gate` **旁**,两处同键同义)。两个 park 腿各读各的载体,判据是同一只。
|
|
219
|
+
*
|
|
220
|
+
* 🔴 UNTRUSTED wire 值:只认**非空串**,其余(缺席 / 非串 / 空串)一律诚实缺席 —— tool-less park
|
|
221
|
+
* (resource_limit / plan_review / task_done)按契约本来就没有这个键,而一个坏值被当成身份用
|
|
222
|
+
* 会去摘一条不存在的扣留帧(等于什么都没摘,但把「判不出」的保守臂骗过去了)。
|
|
223
|
+
*/
|
|
224
|
+
function parkGatedCallId(src) {
|
|
225
|
+
const v = src?.toolCallId;
|
|
226
|
+
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
227
|
+
}
|
|
136
228
|
/** 台账里那份 gated `tool_start` 当拍的 args 快照(UNTRUSTED,只搬运不解释)。 */
|
|
137
229
|
function argsOfGatedStart(led, callId) {
|
|
138
230
|
if (callId === undefined)
|
|
@@ -197,7 +289,9 @@ const TOOL_END_ARMS = [
|
|
|
197
289
|
// 收口帧名字腿认不出,不盖它 = 用户按了 No 却看到引擎原文。
|
|
198
290
|
id: 'deny-stamp-next',
|
|
199
291
|
run(ev, callId, led) {
|
|
200
|
-
|
|
292
|
+
// 🔴 窄口(件⑤/四审 finding①):这条记号是**一次性 + 位置性**的,超集谓词会让连坐旁观者
|
|
293
|
+
// 抢走它 —— 理由见 `isDenyStampCandidate` 头注。
|
|
294
|
+
if (ev.isError !== true || !isDenyStampCandidate(ev) || led.isDenied(callId))
|
|
201
295
|
return undefined;
|
|
202
296
|
if (!led.takeDenyStamp())
|
|
203
297
|
return undefined;
|
|
@@ -229,8 +323,12 @@ const TOOL_END_ARMS = [
|
|
|
229
323
|
run(ev, callId, led) {
|
|
230
324
|
if (ev.isError !== true || !isGatedToolEnd(ev))
|
|
231
325
|
return undefined;
|
|
232
|
-
//
|
|
233
|
-
|
|
326
|
+
// 件⑤(2026-08-22,[4970] 真机 RED):判据 = `isEngineAbortToolEnd` 的**双判据**(机读码
|
|
327
|
+
// `gate.parked` ∪ 那条确切串)。此前只有后者,而 core 对**被连坐的旁观者**铸的是
|
|
328
|
+
// `"operation aborted before execution"` —— 于是连坐帧恰好全不进 HOLD(当场上屏 + 被 ⑤ 臂
|
|
329
|
+
// 记 ended 吃掉重放的真结果),#324 的机读位在现网一次都没出现过。适用域与不放宽的理由
|
|
330
|
+
// 见该函数头注;6.0.0 wire 的 content-block 数组形由 `toolEndOutputText` 归一后仍做精确等值。
|
|
331
|
+
if (!isEngineAbortToolEnd(ev))
|
|
234
332
|
return undefined;
|
|
235
333
|
// #324 换锚(2026-08-22):这一拍**不判出身**。durable park 腿的帧序是
|
|
236
334
|
// `tool_start → tool_end(毒化) → done{suspended}` —— gate 身份晚于本帧到达,此刻任何判词
|
|
@@ -364,16 +462,18 @@ function routeSuspended(ev, ctx) {
|
|
|
364
462
|
// 🔴 两条 park 臂都要登记(判别子的归属证据只认 park)—— ask park 自己用不到兜底腿,
|
|
365
463
|
// 但它必须把**上一张** gate 的准入证清掉,否则旧证会让兜底把一个 Bash 连坐帧反认成主角、
|
|
366
464
|
// 再把真正的 ask 主角 stamp 掉(对抗复审实测 `ask-main=STAMP`)。
|
|
465
|
+
// [4914]:durable `suspended` 事件的待批 call 身份就挂在 `gate` **旁**(不是 gate 里)。
|
|
466
|
+
const gatedCallId = parkGatedCallId(ev);
|
|
367
467
|
if (ev.gate && isAskToolLoose(ev.gate.toolName) && ctx.taskId.current) {
|
|
368
468
|
ctx.led.noteParkGate({ fsOrShellFamily: false });
|
|
369
|
-
return { kind: 'park', gate: 'ask' };
|
|
469
|
+
return { kind: 'park', gate: 'ask', ...(gatedCallId !== undefined ? { gatedCallId } : {}) };
|
|
370
470
|
}
|
|
371
471
|
if (ev.gate &&
|
|
372
472
|
isToolApprovalGate(ev.gate) &&
|
|
373
473
|
ctx.taskId.current) {
|
|
374
474
|
// 准入证:park 的 gate 本体属 fs/shell 族(kind-only gate 的 toolName 在族外 ⇒ 不发证)。
|
|
375
475
|
ctx.led.noteParkGate({ fsOrShellFamily: isFsOrShellToolName(ev.gate.toolName) });
|
|
376
|
-
return { kind: 'park', gate: 'fs' };
|
|
476
|
+
return { kind: 'park', gate: 'fs', ...(gatedCallId !== undefined ? { gatedCallId } : {}) };
|
|
377
477
|
}
|
|
378
478
|
// 其余 gate:透传。⚠️ ADAPTER-F8 注纠(2026-08-02):原文写「eventToSdkMessage 出 null」——
|
|
379
479
|
// 那个返回形已随 REF-CC-058 退役,今天它出的是 `EventProjection` 三态,永不是 null;本臂的
|
|
@@ -382,17 +482,19 @@ function routeSuspended(ev, ctx) {
|
|
|
382
482
|
}
|
|
383
483
|
function routeDone(ev, ctx) {
|
|
384
484
|
const { led } = ctx;
|
|
485
|
+
// [4914]:sync 腿的待批 call 身份在 `done{suspended}` 的 **result 顶层**(与 checkpointId 同位)。
|
|
486
|
+
const gatedCallId = parkGatedCallId(ev.result);
|
|
385
487
|
// 两条 park 臂都登记(理由同 routeSuspended)。
|
|
386
488
|
if (isAskGatePark(ev.result)) {
|
|
387
489
|
ctx.taskId.current = ctx.taskId.current ?? ev.result.taskId;
|
|
388
490
|
led.noteParkGate({ fsOrShellFamily: false });
|
|
389
|
-
return { kind: 'park', gate: 'ask', pendingDone: ev };
|
|
491
|
+
return { kind: 'park', gate: 'ask', pendingDone: ev, ...(gatedCallId !== undefined ? { gatedCallId } : {}) };
|
|
390
492
|
}
|
|
391
493
|
if (isFsApprovalPark(ev.result)) {
|
|
392
494
|
ctx.taskId.current = ctx.taskId.current ?? ev.result.taskId;
|
|
393
495
|
// 准入证:主语是 checkpointGate **本体**的 toolName,不是任何 tool_end 帧的。
|
|
394
496
|
led.noteParkGate({ fsOrShellFamily: isFsOrShellToolName(ev.result.checkpointGate?.toolName) });
|
|
395
|
-
return { kind: 'park', gate: 'fs', pendingDone: ev };
|
|
497
|
+
return { kind: 'park', gate: 'fs', pendingDone: ev, ...(gatedCallId !== undefined ? { gatedCallId } : {}) };
|
|
396
498
|
}
|
|
397
499
|
const events = [...led.flushHeld()]; // 真错(非 gate)的 Ask tool_end 此刻诚实渲染
|
|
398
500
|
// durable 终帧的 result.taskId === sessionId(引擎 durable quirk)会让 captureSessionId 跳过
|
|
@@ -74,6 +74,33 @@ export interface GateLedger {
|
|
|
74
74
|
*/
|
|
75
75
|
hold(callId: string, ev: AgentEvent): void;
|
|
76
76
|
dropHeld(callId: string): void;
|
|
77
|
+
/**
|
|
78
|
+
* 件④([4933] 立案的存量根修,0.41.0):**决断成功**之后摘掉这张 park 主角的扣留帧。
|
|
79
|
+
*
|
|
80
|
+
* 🔴 与裸 {@link dropHeld} 的差别只有一个:**入参可信度**。内环那四个调用点摘的是**帧自己的**
|
|
81
|
+
* `toolCallId`(帧在手,身份无疑);而决断成功那一下,外环手上只有 `findPendingForTask` 给的
|
|
82
|
+
* callId —— 那是给**卡面渲染**用的宽口(同 taskId + 族谓词命中的**第一行**,谓词不命中还退
|
|
83
|
+
* 「任意行」),不是身份。同族多行排队时它可能指向旁观者,于是被摘掉的是**另一条** call 的
|
|
84
|
+
* 扣留帧,而那一帧从此不再吐:{@link flushHeld} 的「一帧不丢」在这一格上不成立
|
|
85
|
+
* (用户面 = dim 圆点永远等不到结果行)。
|
|
86
|
+
*
|
|
87
|
+
* 🔴 摘的**永远是真被决断的那一行**(`candidateCallId`);`trustedCallId` 的角色是**确证**,
|
|
88
|
+
* 不是第二个摘取目标 —— 两边指向不同 call 时摘谁都是 split-brain(决断绑一行、摘另一行的帧)。
|
|
89
|
+
*
|
|
90
|
+
* 两条腿:
|
|
91
|
+
* ① `trustedCallId`(server ≥7.41.0 的 park 身份:`done{suspended}.result.toolCallId` /
|
|
92
|
+
* durable `suspended` 事件 `gate` 旁同键,[4914])与 `candidateCallId` **相等** ⇒ 身份确证,
|
|
93
|
+
* 点名摘。取件腿今天已优先按同一个身份选行,所以正常路径上两者恒等;
|
|
94
|
+
* ② 身份缺席(旧引擎 / tool-less park / 键畸形)或两者不等(身份在场但队列没有那一行 ⇒ 决断
|
|
95
|
+
* 落在别的行上)⇒ 只在 `candidateCallId` 于扣留表里**同族唯一**
|
|
96
|
+
* 时才摘;同族多条 = 判不出谁是主角 ⇒ **一条都不摘**,帧照走 flush 的连坐判别
|
|
97
|
+
* (与 {@link flushHeld}「判不出整表不标」同轴的 fail-safe:宁可多渲一行 abort,
|
|
98
|
+
* 也不许让一帧永久消失)。
|
|
99
|
+
*
|
|
100
|
+
* 🔴 两条腿都要走「摘完看表空不空」那一步(见 {@link dropHeld} 尾注的批级退休不变量),
|
|
101
|
+
* 包括**什么都没摘**那条路 —— 零 HOLD 批正是每次 delete 都落空的形状。
|
|
102
|
+
*/
|
|
103
|
+
dropHeldForDecidedPark(candidateCallId: string | undefined, trustedCallId?: string): void;
|
|
77
104
|
heldCount(): number;
|
|
78
105
|
/**
|
|
79
106
|
* drain:逐帧吐出并**同批记 ended**(fail-soft 回退路径与终帧路径共用的唯一出口)。
|
package/dist/hitl/gateLedger.js
CHANGED
|
@@ -62,6 +62,49 @@ export function createGateLedger() {
|
|
|
62
62
|
const family = [...new Set([...heldAskEnds.keys(), ...droppedFromBatch])].filter((id) => fsOrShellGatedCalls.has(id));
|
|
63
63
|
return family.length === 1 ? new Set(family) : undefined;
|
|
64
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* `dropHeld` / `dropHeldForDecidedPark` 的**唯一**实现(两个动词共用一份,批级退休那条不变量
|
|
67
|
+
* 只写一处 —— 两份写法各自漂正是本模块存在的理由)。
|
|
68
|
+
* `callId === undefined` = 「这次什么都不摘」:退休检查照跑,别的一律不动。
|
|
69
|
+
*/
|
|
70
|
+
const dropHeldImpl = (callId) => {
|
|
71
|
+
if (callId !== undefined && heldAskEnds.delete(callId))
|
|
72
|
+
droppedFromBatch.add(callId);
|
|
73
|
+
// 🔴 扣留表空 = 这一批没有(或不再有)任何待判的帧,批级判据当场退休 —— 不能等下一次
|
|
74
|
+
// flush:「决断成功但本批压根没有毒化帧」的路径上 `flushHeld` 一次都不会跑,判据会就这么
|
|
75
|
+
// 活到下一批去把那一批的**真主角**判成连坐(实测 `new-main=STAMP`)。
|
|
76
|
+
// 🔴 「摘的那个不在表里」也要走这一步(不能早退):零 HOLD 批正是**每次** delete 都落空的
|
|
77
|
+
// 形状,早退就等于那种批永远不退休 —— 上一版的漏洞就在这里。
|
|
78
|
+
if (heldAskEnds.size === 0)
|
|
79
|
+
retireBatchState();
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* 件④ 保守臂的判据:候选在扣留表里**同族唯一**吗。
|
|
83
|
+
*
|
|
84
|
+
* 「族」的口径与**取件谓词**同轴(那才是可能撞车的那一层 —— `findPendingForTask` 是按族选行的):
|
|
85
|
+
* · 候选属 fs 写 / shell 执行族(`fsOrShellGatedCalls`,tool_start 当拍记)⇒ 数扣留表里同属
|
|
86
|
+
* 该族的条数(两个 Bash / 一个 Write 一个 Edit 都算同族,因为取件谓词对它们不可分);
|
|
87
|
+
* · 否则(ask / kind-only 等)⇒ 数扣留表里 `toolName` 逐字相同的条数。
|
|
88
|
+
* 不在扣留表里 ⇒ `false`(没有可摘的帧,也就谈不上「唯一」)。
|
|
89
|
+
*/
|
|
90
|
+
const soleOfItsFamilyInHold = (callId) => {
|
|
91
|
+
const ev = heldAskEnds.get(callId);
|
|
92
|
+
if (ev === undefined)
|
|
93
|
+
return false;
|
|
94
|
+
let n = 0;
|
|
95
|
+
if (fsOrShellGatedCalls.has(callId)) {
|
|
96
|
+
for (const id of heldAskEnds.keys())
|
|
97
|
+
if (fsOrShellGatedCalls.has(id))
|
|
98
|
+
n++;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const name = ev.toolName;
|
|
102
|
+
for (const held of heldAskEnds.values())
|
|
103
|
+
if (held.toolName === name)
|
|
104
|
+
n++;
|
|
105
|
+
}
|
|
106
|
+
return n === 1;
|
|
107
|
+
};
|
|
65
108
|
/** 一批判完(或整批被摘空)⇒ 四样批级判据同批退休:下一批从零证起。 */
|
|
66
109
|
const retireBatchState = () => {
|
|
67
110
|
fsOrShellFamilyGate = false;
|
|
@@ -109,15 +152,27 @@ export function createGateLedger() {
|
|
|
109
152
|
heldAskEnds.set(callId, ev);
|
|
110
153
|
},
|
|
111
154
|
dropHeld(callId) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
155
|
+
dropHeldImpl(callId);
|
|
156
|
+
},
|
|
157
|
+
dropHeldForDecidedPark(candidateCallId, trustedCallId) {
|
|
158
|
+
// ① wire 身份**对上了这次真被决断的那一行** ⇒ 身份确证,点名摘。
|
|
159
|
+
// 🔴 摘的永远是「真的被决断的那一行」(`candidateCallId`),wire 身份在这里的角色是**确证**
|
|
160
|
+
// 而不是另一个独立的摘取目标 —— 异源复审 finding①:决断腿按队列首行选行、这里却按 wire
|
|
161
|
+
// 身份摘帧,两边指向不同 call 时就是 split-brain(用户批的是旁观者,被摘的是主角帧)。
|
|
162
|
+
// 取件腿今天已经优先按同一个身份选行(见 `surfaceFsApprovalAndDecide` 的 parkGatedCallId),
|
|
163
|
+
// 所以正常路径上两者恒相等;不相等只发生在「身份在场但队列里没有那一行」的回落形 ——
|
|
164
|
+
// 那时决断落在别的行上,任何一帧都证明不了自己已过时,走②/③ 的保守判。
|
|
165
|
+
if (trustedCallId !== undefined && trustedCallId === candidateCallId) {
|
|
166
|
+
dropHeldImpl(trustedCallId);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// ② 无可信身份:候选同族唯一才摘 —— 同族多条时任何选择都是猜,而猜错的代价是一帧永久消失。
|
|
170
|
+
if (candidateCallId !== undefined && soleOfItsFamilyInHold(candidateCallId)) {
|
|
171
|
+
dropHeldImpl(candidateCallId);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// ③ 判不出 ⇒ 一条都不摘(退休检查仍照跑,见 dropHeldImpl 头注)。
|
|
175
|
+
dropHeldImpl(undefined);
|
|
121
176
|
},
|
|
122
177
|
heldCount() {
|
|
123
178
|
return heldAskEnds.size;
|