@sema-agent/client-core 0.28.0 → 0.30.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 +400 -0
- package/README.md +19 -2
- package/dist/adapt/arms.js +24 -1
- package/dist/adapt/wireShapes.d.ts +7 -0
- package/dist/adapt/wireShapes.js +7 -0
- package/dist/adapter/activeRunSelfHeal.d.ts +407 -0
- package/dist/adapter/activeRunSelfHeal.js +771 -0
- package/dist/adapter/runStream.d.ts +23 -6
- package/dist/adapter/runStream.js +19 -5
- package/dist/engineWireSdk.d.ts +10 -2
- package/dist/engineWireSdk.js +7 -3
- package/dist/hitl/approvalDecisionNoteAudit.d.ts +58 -0
- package/dist/hitl/approvalDecisionNoteAudit.js +91 -0
- package/dist/hitl/armedGateRegistry.d.ts +19 -0
- package/dist/hitl/armedGateRegistry.js +92 -0
- package/dist/hitl/askParkRowRouting.d.ts +150 -0
- package/dist/hitl/askParkRowRouting.js +183 -0
- package/dist/hitl/frameRouter.d.ts +5 -1
- package/dist/hitl/frameRouter.js +1 -1
- package/dist/hitl/gateIdentity.d.ts +50 -0
- package/dist/hitl/gateIdentity.js +64 -0
- package/dist/hitl/hitlBridge.d.ts +7 -0
- package/dist/hitl/hitlBridge.js +11 -2
- package/dist/hitl/parkOwnership.d.ts +65 -0
- package/dist/hitl/parkOwnership.js +49 -0
- package/dist/hitl/parkResolver.js +3 -1
- package/dist/hitl/parkRowBirthWait.d.ts +63 -0
- package/dist/hitl/parkRowBirthWait.js +192 -0
- package/dist/hitl/planReviewWire.d.ts +51 -0
- package/dist/hitl/planReviewWire.js +172 -17
- package/dist/hitl/resumeRunningCard.d.ts +134 -0
- package/dist/hitl/resumeRunningCard.js +177 -0
- package/dist/hitl/toolApprovalWire.d.ts +69 -23
- package/dist/hitl/toolApprovalWire.js +60 -24
- package/dist/index.d.ts +9 -0
- package/dist/index.js +34 -0
- package/dist/liveQuestionStore.d.ts +11 -0
- package/dist/liveQuestionStore.js +13 -0
- package/dist/seatContract.d.ts +27 -0
- package/dist/seatContract.js +42 -0
- package/dist/subagent/engineSubagentTail.d.ts +0 -2
- package/dist/subagent/engineSubagentTail.js +7 -15
- package/dist/subagentContentStore.d.ts +58 -2
- package/dist/subagentContentStore.js +95 -6
- package/dist/toolResult.d.ts +26 -0
- package/dist/toolResult.js +38 -6
- package/dist/workflowClient.d.ts +6 -1
- package/docs/INTEGRATION-CLIENTS.md +844 -0
- package/docs/REFACTOR-LEDGER.md +392 -0
- package/package.json +7 -4
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { askParkForeignGateKind } from '../adapter/activeRunSelfHeal.js';
|
|
2
|
+
import { isAskTool } from './frameRouter.js';
|
|
3
|
+
import { approvalCallKey } from './gateIdentity.js';
|
|
4
|
+
import { askQuestionsFromPending, pendingRowIsOwnedByThisSession } from './parkOwnership.js';
|
|
5
|
+
/**
|
|
6
|
+
* 待决行有界等待窗的**建议上限**(端可覆盖;`waitForParkRowBirth` 自己不带缺省)。
|
|
7
|
+
* 实测锚:plan 批准之后引擎要跑**一次真实模型调用**才算得出 Write 的参数、才把审批行落库 ——
|
|
8
|
+
* t+2500ms 读到的仍是空表,t+3000ms 行才出生。15s = 实测 3s 的 5 倍余量(慢模型/负载下的头部
|
|
9
|
+
* 延迟);超限如实收口 —— 到那一刻降级话是真话,这正是有界的意义。
|
|
10
|
+
* 🔴 代价如实记:真孤儿 park(座位占着、确实没有任何待决行)要等满这个窗才说那句诚实的失败话。
|
|
11
|
+
* 方向取舍 = 让**常见且可修**的那一态(行未出生)真的被接住,代价是**罕见且本就已坏**的那一态
|
|
12
|
+
* 多等一个有界窗;等待全程可被中断(signal 直通重查环)。
|
|
13
|
+
*/
|
|
14
|
+
export const ASK_PARK_ROW_WAIT_MS = 15000;
|
|
15
|
+
/** 重查节奏建议值。500ms 级:比行出生的量级(秒)细一个数量级,又不至于把本地引擎打成轮询风暴。 */
|
|
16
|
+
export const ASK_PARK_ROW_POLL_MS = 500;
|
|
17
|
+
/**
|
|
18
|
+
* ① 「本臂此刻有没有一行可决断的行」——三态判决(见文件头)。**纯函数**:行由调用方读面取回,
|
|
19
|
+
* 本函数只判;绝不抛。
|
|
20
|
+
*
|
|
21
|
+
* @param rows `/v1/approvals` 全量 pending 行(scope 级单队列原样)
|
|
22
|
+
* @param taskId busy 信号点名的 run id(优先匹配它;匹配不到才谈兜底)
|
|
23
|
+
*/
|
|
24
|
+
export function classifyAskParkRows(rows, taskId, deps) {
|
|
25
|
+
const isOwned = deps?.isOwned ?? ((row) => pendingRowIsOwnedByThisSession(row, deps?.ownership));
|
|
26
|
+
const own = rows.find(r => r.taskId === taskId);
|
|
27
|
+
// 🔴 兜底纪律:只在**队列无歧义**(恰一行 pending)时才兜底 —— scope 级单队列上可能还躺着别的
|
|
28
|
+
// 会话/别的后台 lane 的行,多行并存时挑任何一行都证明不了归属,展示并决它 = 把无关审批递到这个
|
|
29
|
+
// 用户手上。歧义 ⇒ 本拍无行(交回有界重查环,窗尽才诚实失败)。
|
|
30
|
+
const sole = rows.length === 1 ? rows[0] : undefined;
|
|
31
|
+
const soleUnowned = sole !== undefined && !isOwned(sole);
|
|
32
|
+
const pending = own ?? (soleUnowned ? undefined : sole);
|
|
33
|
+
if (pending === undefined) {
|
|
34
|
+
return {
|
|
35
|
+
kind: 'unborn',
|
|
36
|
+
reason: rows.length === 0
|
|
37
|
+
? // 空表两义:要么真孤儿 park(座位占着却没有待决行 = 引擎态异常),要么下一只门的行还没
|
|
38
|
+
// 铸出来。两义分不开 ⇒ 按可修的那一义有界重查,窗尽才按前者如实收口。
|
|
39
|
+
`no pending row at all for task ${taskId} — either an orphan park (a seat held with no decision outstanding) or the next gate's row is not minted yet`
|
|
40
|
+
: soleUnowned
|
|
41
|
+
? `the only pending row (task ${String(sole?.taskId)} / session ${String(sole?.sessionId)}) could not be proven to belong to this client (not in its own-run ledger, session id absent or different) — refusing to surface an approval that may be another session's`
|
|
42
|
+
: `${String(rows.length)} pending rows but none for task ${taskId} — ambiguous queue, refusing to surface an unrelated row`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// 行路由的门种闸:kind 在场但不在 ask 门族(plan_review / resource_limit / 未来新词)⇒ 拒绝入臂
|
|
46
|
+
// —— 对语义不属本臂的门驱动 `/decide` 比不重开更坏。判为 `unborn` 而非 `settled`,理由见文件头。
|
|
47
|
+
// 判据 = 包内单源 {@link askParkForeignGateKind}(`findPendingForTask` 的行过滤同一只,P-30)。
|
|
48
|
+
const foreignKind = askParkForeignGateKind(pending);
|
|
49
|
+
if (foreignKind !== null) {
|
|
50
|
+
return {
|
|
51
|
+
kind: 'unborn',
|
|
52
|
+
reason: `the visible pending row (task ${String(pending.taskId)}) is parked on a '${foreignKind}' gate — not an ask/approval gate; this arm will not surface or decide it, and it may also be the previous gate's row still visible during a transition`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return { kind: 'row', row: pending };
|
|
56
|
+
}
|
|
57
|
+
/** 行上的门种(空串按缺席归一;缺席 = pre-`gate_kind` 的历史行,别当 `"human"`)。 */
|
|
58
|
+
function askParkRowGateKind(row) {
|
|
59
|
+
return typeof row.gateKind === 'string' && row.gateKind.length > 0 ? row.gateKind : null;
|
|
60
|
+
}
|
|
61
|
+
/** ② 算一行的身份三件套。`taskId` = 请求者点名的 id(行自己没带 taskId 时的回退)。 */
|
|
62
|
+
export function askParkRowIdentity(taskId, row) {
|
|
63
|
+
const rowTaskId = typeof row.taskId === 'string' && row.taskId.length > 0 ? row.taskId : taskId;
|
|
64
|
+
const gatedCallId = row.toolCallId ?? row.boundCallId ?? undefined;
|
|
65
|
+
return { rowTaskId, gatedCallId, armedKey: approvalCallKey(gatedCallId, rowTaskId) };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* ③ 这一行该走哪条臂。判据锚在**决定量**(这行到底有没有问句)上,不锚门种的名字 —— 理由见文件头。
|
|
69
|
+
* · kind 缺席(pre-3.0.0 旧行)⇒ 维持 toolName 判据(旧行没有别的信号);
|
|
70
|
+
* · kind 是 ask 门族里 `human` 之外的词(tool_approval / irreversible_ask / policy_ask)⇒ 工具臂;
|
|
71
|
+
* · kind = `human` ⇒ 三选一命中即问答臂:toolName 明说是 ask 工具 / 行真带问句 payload /
|
|
72
|
+
* 行**连 toolName 都没有**(没有工具可渲,问答臂是唯一可能的形,且它自己会对空 payload 诚实收口)。
|
|
73
|
+
* 🔴 `toolName` 是 UNTRUSTED wire 位且 `/v1/approvals` 行不经工具名归一 —— 用包的规范判据
|
|
74
|
+
* {@link isAskTool}(空白/下划线/大小写不敏感),严格串比会把小写形误路由进工具臂。
|
|
75
|
+
*/
|
|
76
|
+
export function askParkRowArm(row) {
|
|
77
|
+
const rowGateKind = askParkRowGateKind(row);
|
|
78
|
+
const toolNameIsAsk = isAskTool(row.toolName ?? undefined);
|
|
79
|
+
if (rowGateKind === null)
|
|
80
|
+
return toolNameIsAsk ? 'question' : 'tool-gate';
|
|
81
|
+
if (!GENERIC_DECISION_GATE_KINDS.includes(rowGateKind))
|
|
82
|
+
return 'tool-gate';
|
|
83
|
+
const rowHasToolName = typeof row.toolName === 'string' && row.toolName.trim().length > 0;
|
|
84
|
+
return toolNameIsAsk || askQuestionsFromPending(row) !== null || !rowHasToolName ? 'question' : 'tool-gate';
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 「泛化的**要人来决定**」那一族 kind —— 它们**不自带**「这是工具 / 这是问句」的答案,所以必须按
|
|
88
|
+
* 决定量(这行到底有没有问句)判臂;别的 ask 族 kind 自带答案,直接进工具臂。
|
|
89
|
+
*
|
|
90
|
+
* 逐条证据(engine 侧类型直读,不靠名字猜):
|
|
91
|
+
* · `human` —— core `CheckpointGate` 的 `{kind:'human', reason, toolName}`,而实测真 wire 上它同时
|
|
92
|
+
* 承载「Write 审批行」与「AskUserQuestion 行」两形(#269 的第二病就是把它单义化成问句)。
|
|
93
|
+
* · `policy_ask` —— 它是**决断信封**那一侧的名字:core `ResumeOutcome` 的
|
|
94
|
+
* `{gate:'policy_ask', boundCallId, boundInputHash, decision:'allow'|'deny', answer?: QuestionAnswer}`
|
|
95
|
+
* —— 那个 `answer?` 位就是**问句门也走这个信封**的直证。所以一行报 `policy_ask` 时同样两形皆可能,
|
|
96
|
+
* 按名字硬判工具臂 = `human` 那一病换个 kind 复发(二次评审 R3 [high] 命中)。
|
|
97
|
+
* · `irreversible_ask` 的 core 形是 `{kind, reason, toolName}`(toolName **必填**)、`tool_approval`
|
|
98
|
+
* 按定义就是工具审批 ⇒ 两者不进这张表,直接工具臂。
|
|
99
|
+
*/
|
|
100
|
+
const GENERIC_DECISION_GATE_KINDS = ['human', 'policy_ask'];
|
|
101
|
+
/**
|
|
102
|
+
* ④ 「这一行还在不在待决表里」——回读判据(键取法与 {@link askParkRowIdentity} 同源)。
|
|
103
|
+
* 🔴 调用方读面失败时**必须**当「还在」(保守:照走重试/上屏;重复 decide 由 server 侧 checkpoint
|
|
104
|
+
* 一次性消费挡住,二发只会得到 stale/no_pending 拒绝,不会 double-act)。
|
|
105
|
+
*/
|
|
106
|
+
export function askParkRowStillPending(rows, identity) {
|
|
107
|
+
return rows.some(r => identity.gatedCallId !== undefined
|
|
108
|
+
? (r.toolCallId ?? r.boundCallId ?? undefined) === identity.gatedCallId
|
|
109
|
+
: r.taskId === identity.rowTaskId);
|
|
110
|
+
}
|
|
111
|
+
export function classifyAskParkChainFailure(i) {
|
|
112
|
+
if (i.rowStillPending)
|
|
113
|
+
return 'retry';
|
|
114
|
+
return i.cardPresented ? 'row-gone-after-card' : 'row-unborn';
|
|
115
|
+
}
|
|
116
|
+
export function createRowArmSingleFlight() {
|
|
117
|
+
const inFlight = new Map();
|
|
118
|
+
/**
|
|
119
|
+
* 正在跑 `start()` 的键(只覆盖 start 的**同步执行窗**)。
|
|
120
|
+
* 🔴 它封的是最常见的那一形自依赖:`async () => { const v = await join(同键, …) ; … }` —— async
|
|
121
|
+
* 函数体在第一个 await 之前是**同步**跑的,那次 `join(同键)` 恰好落在这个窗里。不封它的话,外层
|
|
122
|
+
* run 会 settle 到一个反过来等 run 的 promise ⇒ 永不 settle,而这一行之后每一次重开都合流到它。
|
|
123
|
+
*/
|
|
124
|
+
const starting = new Set();
|
|
125
|
+
return {
|
|
126
|
+
join: (key, start) => {
|
|
127
|
+
if (starting.has(key)) {
|
|
128
|
+
// 前置条件被破(`start` 不许 join 自己的键)⇒ 当场拒绝,绝不交出一只会永久 pending 的 promise。
|
|
129
|
+
return Promise.reject(new Error(`row-arm single flight: start() re-entered join() for its own key while starting (${key})`));
|
|
130
|
+
}
|
|
131
|
+
const joined = inFlight.get(key);
|
|
132
|
+
if (joined !== undefined)
|
|
133
|
+
return joined;
|
|
134
|
+
// 🔴 **占位先于 start()**:直接 `const run = start()` 会让键在 start 跑完之前是空的,start
|
|
135
|
+
// 内部(或它同步触发的任何回调)再入同键就会另起第二条链 —— 同一个待决项两张卡,正是本闸
|
|
136
|
+
// 要封的那件事。先铸一只空 promise 占键,再把 start 的结果接进去。
|
|
137
|
+
let settle;
|
|
138
|
+
let fail;
|
|
139
|
+
const run = new Promise((resolve, reject) => {
|
|
140
|
+
settle = resolve;
|
|
141
|
+
fail = reject;
|
|
142
|
+
});
|
|
143
|
+
inFlight.set(key, run);
|
|
144
|
+
// 🔴 清账**认身份不认键**:`clear()` 之后同键可能已经起了新的一条,而旧链落地时那句无条件
|
|
145
|
+
// `delete(key)` 会把新链的登记抹掉 ⇒ 第三个调用方又能起一条,闸形同虚设。
|
|
146
|
+
const release = () => {
|
|
147
|
+
if (inFlight.get(key) === run)
|
|
148
|
+
inFlight.delete(key);
|
|
149
|
+
};
|
|
150
|
+
void run.then(release, release);
|
|
151
|
+
starting.add(key);
|
|
152
|
+
try {
|
|
153
|
+
const started = start();
|
|
154
|
+
if (started === run) {
|
|
155
|
+
// 🔴 自依赖 fail-fast(二次评审 R2 [high]):`start` 把占位 promise 原样交回来(典型形 = 它
|
|
156
|
+
// 自己又 join 了同一个键)⇒ run 会 resolve 到它自己,**永不 settle**,而这一行的后续每一次
|
|
157
|
+
// 重开都会合流到那只永久 pending 的 promise —— 死锁比多铸一张卡坏得多。
|
|
158
|
+
// 与它配对的另一半在 join 入口:`starting` 集合封住 `start` **同步执行窗**内的同键再入
|
|
159
|
+
// (async 函数体第一个 await 之前那段,正是 `await join(同键)` 的落点)。
|
|
160
|
+
// 残余(如实记):`start` 在**更晚的 tick**(已经 await 过别的东西之后)才 join 自己的键,
|
|
161
|
+
// 两道闸都判不出来 —— 那条只能靠调用协议:**`start` 不许 join 自己的键**。
|
|
162
|
+
fail?.(new Error('row-arm single flight: start() returned the in-flight placeholder for the same key'));
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
settle?.(started);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
// start 同步抛 ⇒ 判决走**拒绝的 promise**(签名承诺的就是 Promise<T>),不是同步抛给调用方
|
|
170
|
+
// ——后者会让合流者与首发者拿到两种不同的失败形。
|
|
171
|
+
fail?.(e);
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
starting.delete(key);
|
|
175
|
+
}
|
|
176
|
+
return run;
|
|
177
|
+
},
|
|
178
|
+
size: () => inFlight.size,
|
|
179
|
+
clear: () => {
|
|
180
|
+
inFlight.clear();
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { AgentEvent } from '@sema-agent/sdk';
|
|
18
18
|
import type { HitlClientLike } from './hitlBridge.js';
|
|
19
|
-
import { type RespondToolApprovalFn } from './toolApprovalWire.js';
|
|
19
|
+
import { type RespondToolApprovalFn, type ToolApprovalFrameLaneOpts } from './toolApprovalWire.js';
|
|
20
20
|
import type { GateLedger } from './gateLedger.js';
|
|
21
21
|
/**
|
|
22
22
|
* CC `utils/messages.ts` 的 `REJECT_MESSAGE` **逐字**(B7 搬迁差分 2)。deny 后重放 tool_end 的
|
|
@@ -50,6 +50,10 @@ export interface AskGateWireDeps {
|
|
|
50
50
|
/** POST /v1/tool-approvals/:id/respond(server 1.191 同步帧腿,[830]①)。缺省=不消费
|
|
51
51
|
* tool_approval 帧(帧被吞、引擎按自身 fail-closed TTL 自决)——mock/旧引擎路径零影响。 */
|
|
52
52
|
respondToolApproval?: RespondToolApprovalFn;
|
|
53
|
+
/** #229 respond-note 供给链(0.29.0 发包扫描门修,2026-08-12):帧腿车道参数(能力位读数),
|
|
54
|
+
* 宿主从自己的 caps 缓存供给。缺席 = note 门 fail-closed 到「不发」侧(决断照常,现状字节
|
|
55
|
+
* 不变)—— 此前包内唯一生产调用点(routeFrame 的 tool_approval 臂)无此位,note 恒不发。 */
|
|
56
|
+
approvalLane?: ToolApprovalFrameLaneOpts;
|
|
53
57
|
}
|
|
54
58
|
/** 两族 gate:AskUserQuestion 走问答 overlay(原路);其余(fs 写三件 / Bash / 一等
|
|
55
59
|
* `kind==='tool_approval'`)走 CC 三选卡。 */
|
package/dist/hitl/frameRouter.js
CHANGED
|
@@ -302,7 +302,7 @@ async function routeToolApprovalFrame(ev, ctx) {
|
|
|
302
302
|
const gatedCallId = !fromSubagent ? led.lastPendingFsCall() : undefined;
|
|
303
303
|
// FIX①(2026-08-07):产物是 `{decision, ack?}` —— ack 上的 `rememberApplied`/`updatedInputForwarded`
|
|
304
304
|
// 由 toolApprovalWire 就地收敛成宿主通知(本路由只认决断词,不重复解释 ack)。
|
|
305
|
-
const { decision } = await surfaceToolApprovalFrameAndRespond(ev, deps.respondToolApproval, argsOfGatedStart(led, gatedCallId), ctx.signal);
|
|
305
|
+
const { decision } = await surfaceToolApprovalFrameAndRespond(ev, deps.respondToolApproval, argsOfGatedStart(led, gatedCallId), ctx.signal, deps.approvalLane);
|
|
306
306
|
if (decision === 'deny' && !fromSubagent) {
|
|
307
307
|
// 该 call 的报错收口帧 stamp REJECT 文案(vendored `User rejected <op> to <path>` 卡)。
|
|
308
308
|
if (gatedCallId !== undefined)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gateIdentity.ts — HITL 决断卡**身份键**的唯一铸口(A-028.3,#244 族A 包半场,2026-08-12)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有它:三条身份键字面此前散在三处各铸各的 ——
|
|
5
|
+
* · `parkResolver.ts` 铸 `hitl-ask:${gatedCallId ?? taskId}`(overlay 问答帧 id);
|
|
6
|
+
* · `toolApprovalWire.ts` 铸 `gatedCallId ?? taskId`(durable 腿卡口 callKey)与
|
|
7
|
+
* `hitl-frame:${approvalId}`(live 帧腿 callKey);
|
|
8
|
+
* · `planReviewWire.ts` 铸 `plan-review:${taskId}`(module 私有,壳侧只能人肉抄字面对齐)。
|
|
9
|
+
* 消费方(呈现台账 `armedGateRegistry` 的键归一、壳侧重开腿)靠「与铸口逐字对齐」的注释纪律
|
|
10
|
+
* 活着 —— 漂一个字节,首见/复见判决就静默失真。本文件把全部字面收成单源:铸口 import 这里,
|
|
11
|
+
* 台账的键推导也 import 这里,两边在结构上不可能再漂。
|
|
12
|
+
*
|
|
13
|
+
* 🔴 **声明边界(web [C1] d3 拦截跟修,2026-08-12)**:上句「全部字面」只指 **HITL 决断卡的
|
|
14
|
+
* questionId / callKey 键空间**(本文件四常量两函数)。`seatContract.ts` 的
|
|
15
|
+
* `TOOL_PERMISSION_REQUEST_ID_DOMAINS`(`live:`/`durable:`/`question:`/`elicit:`/`plan:`)是
|
|
16
|
+
* **另一个键空间** —— 座位 `ToolPermissionRequest.requestId` 的路由域词表,出站校验器按它
|
|
17
|
+
* fail-close。两空间零重叠、各自单源,**绝不合并**:把 `plan-review:`(本文件)与 `plan:`
|
|
18
|
+
* (seat 域)字面归一会让座位校验器静默拒掉全部 plan-review 卡(web 按 [C161]-d3 原文施工时
|
|
19
|
+
* 当场拦下的真回归形)。
|
|
20
|
+
*
|
|
21
|
+
* 🔴 零 import 纯函数叶(portability:不进任何闭包新边;index 闭包 +1 文件已登记)。
|
|
22
|
+
*/
|
|
23
|
+
/** overlay 问答帧的 ask-gate 命名空间前缀(`parkResolver.surfaceGateAndDecide` 合成帧 id 用)。 */
|
|
24
|
+
export declare const HITL_ASK_QUESTION_ID_PREFIX = "hitl-ask:";
|
|
25
|
+
/** plan_review 审批卡的合成 questionId 前缀(`planReviewWire` 首次 arm 与重开腿共用)。 */
|
|
26
|
+
export declare const PLAN_REVIEW_QUESTION_ID_PREFIX = "plan-review:";
|
|
27
|
+
/** live `tool_approval` 帧腿的卡口 callKey 前缀(帧上无 gatedCallId 可对齐,以 approvalId 铸)。 */
|
|
28
|
+
export declare const HITL_FRAME_CALL_KEY_PREFIX = "hitl-frame:";
|
|
29
|
+
/** 重开腿铸新身份的尾分隔符(`…#reopen-<suffix>`);键归一时从这里剥尾,首呈与重开落同一键。 */
|
|
30
|
+
export declare const REOPEN_ID_TAIL = "#reopen-";
|
|
31
|
+
/**
|
|
32
|
+
* durable 审批腿的卡身份键(`toolApprovalWire` 卡口 / own-run 台账同域):
|
|
33
|
+
* gate 绑定的 callId 优先,行没带 callId 时退 taskId。
|
|
34
|
+
*/
|
|
35
|
+
export declare function approvalCallKey(gatedCallId: string | undefined, taskId: string): string;
|
|
36
|
+
/** ask-gate overlay 帧 id(入参 = {@link approvalCallKey} 的产出)。 */
|
|
37
|
+
export declare function askGateQuestionId(callKey: string): string;
|
|
38
|
+
/** plan_review 审批卡的合成 questionId(首次 arm 的 canonical 身份;重开腿在其后接 {@link REOPEN_ID_TAIL})。
|
|
39
|
+
* A-028.3:此前是 `planReviewWire.ts` 的 module 私有函数,壳侧重开腿只能手抄字面 —— 现转公面单源。 */
|
|
40
|
+
export declare function planReviewQuestionId(taskId: string): string;
|
|
41
|
+
/** live `tool_approval` 帧腿的卡口 callKey(`toolApprovalWire.surfaceToolApprovalFrameAndRespond`)。 */
|
|
42
|
+
export declare function liveFrameCallKey(approvalId: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* question 帧 id → 呈现台账键的归一(armedGateRegistry 的键推导):
|
|
45
|
+
* · `hitl-ask:X` → `X`(ask 帧的台账键 = 卡口 callKey,与 durable 腿卡的键同域);
|
|
46
|
+
* · `…#reopen-<suffix>` 尾剥掉(重开铸的新身份归一到原键,首呈与重开同键);
|
|
47
|
+
* · `plan-review:<taskId>` **保留前缀**(plan 键与「taskId 直接作 ask 键」永不撞域);
|
|
48
|
+
* · 不认识的形原样入册(未来新 id 形至多多占一个键,不误伤既有词汇)。
|
|
49
|
+
*/
|
|
50
|
+
export declare function armedKeyFromQuestionId(questionId: string): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gateIdentity.ts — HITL 决断卡**身份键**的唯一铸口(A-028.3,#244 族A 包半场,2026-08-12)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有它:三条身份键字面此前散在三处各铸各的 ——
|
|
5
|
+
* · `parkResolver.ts` 铸 `hitl-ask:${gatedCallId ?? taskId}`(overlay 问答帧 id);
|
|
6
|
+
* · `toolApprovalWire.ts` 铸 `gatedCallId ?? taskId`(durable 腿卡口 callKey)与
|
|
7
|
+
* `hitl-frame:${approvalId}`(live 帧腿 callKey);
|
|
8
|
+
* · `planReviewWire.ts` 铸 `plan-review:${taskId}`(module 私有,壳侧只能人肉抄字面对齐)。
|
|
9
|
+
* 消费方(呈现台账 `armedGateRegistry` 的键归一、壳侧重开腿)靠「与铸口逐字对齐」的注释纪律
|
|
10
|
+
* 活着 —— 漂一个字节,首见/复见判决就静默失真。本文件把全部字面收成单源:铸口 import 这里,
|
|
11
|
+
* 台账的键推导也 import 这里,两边在结构上不可能再漂。
|
|
12
|
+
*
|
|
13
|
+
* 🔴 **声明边界(web [C1] d3 拦截跟修,2026-08-12)**:上句「全部字面」只指 **HITL 决断卡的
|
|
14
|
+
* questionId / callKey 键空间**(本文件四常量两函数)。`seatContract.ts` 的
|
|
15
|
+
* `TOOL_PERMISSION_REQUEST_ID_DOMAINS`(`live:`/`durable:`/`question:`/`elicit:`/`plan:`)是
|
|
16
|
+
* **另一个键空间** —— 座位 `ToolPermissionRequest.requestId` 的路由域词表,出站校验器按它
|
|
17
|
+
* fail-close。两空间零重叠、各自单源,**绝不合并**:把 `plan-review:`(本文件)与 `plan:`
|
|
18
|
+
* (seat 域)字面归一会让座位校验器静默拒掉全部 plan-review 卡(web 按 [C161]-d3 原文施工时
|
|
19
|
+
* 当场拦下的真回归形)。
|
|
20
|
+
*
|
|
21
|
+
* 🔴 零 import 纯函数叶(portability:不进任何闭包新边;index 闭包 +1 文件已登记)。
|
|
22
|
+
*/
|
|
23
|
+
/** overlay 问答帧的 ask-gate 命名空间前缀(`parkResolver.surfaceGateAndDecide` 合成帧 id 用)。 */
|
|
24
|
+
export const HITL_ASK_QUESTION_ID_PREFIX = 'hitl-ask:';
|
|
25
|
+
/** plan_review 审批卡的合成 questionId 前缀(`planReviewWire` 首次 arm 与重开腿共用)。 */
|
|
26
|
+
export const PLAN_REVIEW_QUESTION_ID_PREFIX = 'plan-review:';
|
|
27
|
+
/** live `tool_approval` 帧腿的卡口 callKey 前缀(帧上无 gatedCallId 可对齐,以 approvalId 铸)。 */
|
|
28
|
+
export const HITL_FRAME_CALL_KEY_PREFIX = 'hitl-frame:';
|
|
29
|
+
/** 重开腿铸新身份的尾分隔符(`…#reopen-<suffix>`);键归一时从这里剥尾,首呈与重开落同一键。 */
|
|
30
|
+
export const REOPEN_ID_TAIL = '#reopen-';
|
|
31
|
+
/**
|
|
32
|
+
* durable 审批腿的卡身份键(`toolApprovalWire` 卡口 / own-run 台账同域):
|
|
33
|
+
* gate 绑定的 callId 优先,行没带 callId 时退 taskId。
|
|
34
|
+
*/
|
|
35
|
+
export function approvalCallKey(gatedCallId, taskId) {
|
|
36
|
+
return gatedCallId ?? taskId;
|
|
37
|
+
}
|
|
38
|
+
/** ask-gate overlay 帧 id(入参 = {@link approvalCallKey} 的产出)。 */
|
|
39
|
+
export function askGateQuestionId(callKey) {
|
|
40
|
+
return `${HITL_ASK_QUESTION_ID_PREFIX}${callKey}`;
|
|
41
|
+
}
|
|
42
|
+
/** plan_review 审批卡的合成 questionId(首次 arm 的 canonical 身份;重开腿在其后接 {@link REOPEN_ID_TAIL})。
|
|
43
|
+
* A-028.3:此前是 `planReviewWire.ts` 的 module 私有函数,壳侧重开腿只能手抄字面 —— 现转公面单源。 */
|
|
44
|
+
export function planReviewQuestionId(taskId) {
|
|
45
|
+
return `${PLAN_REVIEW_QUESTION_ID_PREFIX}${taskId}`;
|
|
46
|
+
}
|
|
47
|
+
/** live `tool_approval` 帧腿的卡口 callKey(`toolApprovalWire.surfaceToolApprovalFrameAndRespond`)。 */
|
|
48
|
+
export function liveFrameCallKey(approvalId) {
|
|
49
|
+
return `${HITL_FRAME_CALL_KEY_PREFIX}${approvalId}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* question 帧 id → 呈现台账键的归一(armedGateRegistry 的键推导):
|
|
53
|
+
* · `hitl-ask:X` → `X`(ask 帧的台账键 = 卡口 callKey,与 durable 腿卡的键同域);
|
|
54
|
+
* · `…#reopen-<suffix>` 尾剥掉(重开铸的新身份归一到原键,首呈与重开同键);
|
|
55
|
+
* · `plan-review:<taskId>` **保留前缀**(plan 键与「taskId 直接作 ask 键」永不撞域);
|
|
56
|
+
* · 不认识的形原样入册(未来新 id 形至多多占一个键,不误伤既有词汇)。
|
|
57
|
+
*/
|
|
58
|
+
export function armedKeyFromQuestionId(questionId) {
|
|
59
|
+
const base = questionId.startsWith(HITL_ASK_QUESTION_ID_PREFIX)
|
|
60
|
+
? questionId.slice(HITL_ASK_QUESTION_ID_PREFIX.length)
|
|
61
|
+
: questionId;
|
|
62
|
+
const cut = base.indexOf(REOPEN_ID_TAIL);
|
|
63
|
+
return cut >= 0 ? base.slice(0, cut) : base;
|
|
64
|
+
}
|
|
@@ -192,6 +192,13 @@ export type FindPendingOutcome = {
|
|
|
192
192
|
* cares about, e.g. fs-write vs AskUserQuestion) first; fall back to ANY row for this `taskId` (a gate
|
|
193
193
|
* whose toolName the predicate doesn't recognize, but which still belongs to this run). Never `pending[0]`
|
|
194
194
|
* — a typed failure when nothing for this taskId is queued.
|
|
195
|
+
*
|
|
196
|
+
* P-30(2026-08-14):both legs only consider rows whose `gateKind` belongs to the ask/approval family
|
|
197
|
+
* ({@link askParkForeignGateKind} — the same single-source guard `classifyAskParkRows` uses). Without it
|
|
198
|
+
* the ANY-row fallback would hand a `plan_review`/`resource_limit` row to the tool-approval wire — an
|
|
199
|
+
* empty-toolName card whose decide then 409s (`gate_not_tool_approval`). A foreign-kind row is filtered
|
|
200
|
+
* out, not a rejection of the whole queue: a genuine approval row for the same task still resolves.
|
|
201
|
+
* Rows with no `gateKind` (pre-`gate_kind` history) keep the old behaviour — no stronger signal exists.
|
|
195
202
|
*/
|
|
196
203
|
export declare function findPendingForTask(client: HitlClientLike, taskId: string, matches: (toolName: string | undefined) => boolean, opts?: {
|
|
197
204
|
signal?: AbortSignal;
|
package/dist/hitl/hitlBridge.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { askParkForeignGateKind } from '../adapter/activeRunSelfHeal.js';
|
|
1
2
|
import { eventSeq } from '../adapter/types.js';
|
|
2
3
|
import { hostLog } from '../host.js';
|
|
3
4
|
// ── deny/plan-review 归因的 wire 窄化(0.28.0 发版扫描 F1/F2/F3 收编;单源,三条腿共用)──────
|
|
@@ -70,6 +71,13 @@ export class HitlSafetyError extends Error {
|
|
|
70
71
|
* cares about, e.g. fs-write vs AskUserQuestion) first; fall back to ANY row for this `taskId` (a gate
|
|
71
72
|
* whose toolName the predicate doesn't recognize, but which still belongs to this run). Never `pending[0]`
|
|
72
73
|
* — a typed failure when nothing for this taskId is queued.
|
|
74
|
+
*
|
|
75
|
+
* P-30(2026-08-14):both legs only consider rows whose `gateKind` belongs to the ask/approval family
|
|
76
|
+
* ({@link askParkForeignGateKind} — the same single-source guard `classifyAskParkRows` uses). Without it
|
|
77
|
+
* the ANY-row fallback would hand a `plan_review`/`resource_limit` row to the tool-approval wire — an
|
|
78
|
+
* empty-toolName card whose decide then 409s (`gate_not_tool_approval`). A foreign-kind row is filtered
|
|
79
|
+
* out, not a rejection of the whole queue: a genuine approval row for the same task still resolves.
|
|
80
|
+
* Rows with no `gateKind` (pre-`gate_kind` history) keep the old behaviour — no stronger signal exists.
|
|
73
81
|
*/
|
|
74
82
|
export async function findPendingForTask(client, taskId, matches, opts) {
|
|
75
83
|
let rows;
|
|
@@ -79,8 +87,9 @@ export async function findPendingForTask(client, taskId, matches, opts) {
|
|
|
79
87
|
catch (e) {
|
|
80
88
|
return { ok: false, reason: `approvals.list failed: ${String(e)}` };
|
|
81
89
|
}
|
|
82
|
-
const
|
|
83
|
-
|
|
90
|
+
const decidable = rows.filter((r) => askParkForeignGateKind(r) === null);
|
|
91
|
+
const pending = decidable.find((r) => r.taskId === taskId && matches(typeof r.toolName === 'string' ? r.toolName : undefined)) ??
|
|
92
|
+
decidable.find((r) => r.taskId === taskId);
|
|
84
93
|
if (!pending)
|
|
85
94
|
return { ok: false, reason: 'no pending checkpoint for this run (resolved/expired?)', code: 'no_pending' };
|
|
86
95
|
return { ok: true, pending, gatedCallId: pending.toolCallId ?? pending.boundCallId ?? undefined };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parkOwnership.ts — ask park 重开腿的**纯判据**三件(A-028.2,#244 族A 包半场,2026-08-12;
|
|
3
|
+
* 源形 = cli `src/sema/askParkReopen.ts` 的发现面判据半场;`approvals.list` 编排与 stderr
|
|
4
|
+
* 留宿主 —— 本文件零网络、零渲染,只回答判决)。
|
|
5
|
+
*
|
|
6
|
+
* ── A-024.1:「队列恰一行」不是归属证明 ────────────────────────────────────────────────────
|
|
7
|
+
* `/v1/approvals` 是 **scope 级**单队列:恰一行只说明「此刻没有别的行在排队」,而孤儿 park、
|
|
8
|
+
* 行可见性延迟、另一会话独占队列三种形都能让**别人的**行落单。把这一行直接展示并代为决断,
|
|
9
|
+
* 是 REF-CC-029(`?? pending[0]`)那个病的换形复发 —— 后果不是渲染错,是**把别人的审批递到
|
|
10
|
+
* 这个用户手上并替他按了钮**。
|
|
11
|
+
*
|
|
12
|
+
* 宿主能拿出的归属硬证据只有两条(缺一不可地互补,不是二选一):
|
|
13
|
+
* ① 行的 `taskId` ∈ own-run 台账(`isOwnEngineRun`,只记本进程亲手驱动的 run + fleet 行帧
|
|
14
|
+
* 闭包进来的自家子代)⇒ 本宿主起的 run/子代。child 委派行走这条 —— 子代 park 行的
|
|
15
|
+
* `sessionId` 是子代自己的 session,天生 ≠ 本会话,只按 ② 判会把发现面拆掉。
|
|
16
|
+
* ② 行自带 `sessionId` === 本宿主当前引擎会话 ⇒ 本会话自己的 park。
|
|
17
|
+
*
|
|
18
|
+
* 🔴 判据是 **fail-CLOSED 的正向证明**:两条腿任一成立才采信,**都不成立就不采信** —— 包括
|
|
19
|
+
* 「本会话 id 还没有」「行没带 sessionId」这两种**判不出**的形。把「证明不了」当「是我的」,
|
|
20
|
+
* 在一个会替用户按钮的面上正好反了。判不出的代价只是一句诚实的 reopen-failed(用户重发消息
|
|
21
|
+
* 即重走本链),而放行的代价是替别人决断,不对等。
|
|
22
|
+
*/
|
|
23
|
+
import type { PendingCheckpoint } from '@sema-agent/sdk';
|
|
24
|
+
import type { AskQuestion } from '../liveQuestionStore.js';
|
|
25
|
+
/** 归属判据的两条腿的注入口(缺席走包内既有真源:SessionPort + own-run 台账)。 */
|
|
26
|
+
export interface ParkOwnershipDeps {
|
|
27
|
+
/**
|
|
28
|
+
* 多会话宿主的会话键(缺省 = DEFAULT_SESSION_KEY,单会话宿主零参装配)。
|
|
29
|
+
* 🔴 传了**非默认键**而没有注入 `isOwnRun` 时,own-run 缺省腿**整条跳过**(复审 [high] 收紧):
|
|
30
|
+
* 包内 own-run 台账是**进程级**的(记「本进程驱动过的 run」),在一个跑着 A/B 两个会话的宿主
|
|
31
|
+
* 进程里,它答不了「这行是 A 的还是 B 的」—— 拿进程级证据放行会把 A 的审批递到 B 手上。
|
|
32
|
+
* 多会话宿主要么注入自己会话粒度的 `isOwnRun`,要么只靠会话腿(fail-closed:少一条腿的代价
|
|
33
|
+
* 只是多一次诚实 reopen-failed)。
|
|
34
|
+
*/
|
|
35
|
+
sessionKey?: string;
|
|
36
|
+
/** 会话腿真源(缺省 = `hostSessionFor(sessionKey).currentSessionId()`;空串按缺席归一,
|
|
37
|
+
* 判不出 ≠ 命中)。 */
|
|
38
|
+
currentSessionId?: () => string | undefined;
|
|
39
|
+
/** own-run 台账腿(缺省 = 包 `isOwnEngineRun`,**仅默认会话键**下启用,理由见 sessionKey 注)。
|
|
40
|
+
* 🔴 键域 = `rowIdTail(行 taskId)`(fleet 台账登记时就过 rowIdTail,本判据同域取键后才调本口)。
|
|
41
|
+
* 🔴 成文局限(复审二轮裁定,不改行为):默认键下的缺省腿是**进程级**证据,不区分同一宿主
|
|
42
|
+
* 进程内的会话代际 —— `/clear` 前登记的 run 在新会话语境下仍判 owned。这是单会话宿主的
|
|
43
|
+
* 出货语义(owner = 本进程的这一位用户;child 委派行的 sessionId 天生 ≠ 本会话,发现面恰恰
|
|
44
|
+
* 依赖跨会话 id 的进程级证据),最坏形 = 用户看到**自己**旧会话的审批卡(卡面标注行归属)。
|
|
45
|
+
* 要会话代际粒度就注入自己的 `isOwnRun` —— 台账本体改代际级属 own-run 域独立议题。 */
|
|
46
|
+
isOwnRun?: (taskIdTail: string) => boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 这行 pending 能不能**正向证明**属于本宿主会话(fail-closed;两腿语义见文件头)。
|
|
50
|
+
* 腿序:own-run 先问(child 委派行不依赖会话 id),会话腿后问。
|
|
51
|
+
*/
|
|
52
|
+
export declare function pendingRowIsOwnedByThisSession(row: PendingCheckpoint, deps?: ParkOwnershipDeps): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* questions payload 从 pending.input 读 —— **结构判定**收窄到 AskQuestion 必需键(question
|
|
55
|
+
* 非 string / questions 非数组 / 空数组 ⇒ null,不 cast 硬塞;缺 header 的行照 CC 形容忍,
|
|
56
|
+
* overlay 自兜)。铸空卡 = 假 affordance,拿不到题就诚实返 null。
|
|
57
|
+
*/
|
|
58
|
+
export declare function askQuestionsFromPending(pending: PendingCheckpoint): AskQuestion[] | null;
|
|
59
|
+
/**
|
|
60
|
+
* HitlSafetyError 判型的 duck-check(按 `name`,不按 instanceof —— 宿主 bundle 里存在第二份包
|
|
61
|
+
* 实例时 instanceof 会分叉,sseIdleTriage 同款纪律)。安全信号(binding_mismatch 族)契约禁
|
|
62
|
+
* 盲重试,重开腿据此把「可重试的瞬时故障」与「必须交还人重决的安全停」分开。
|
|
63
|
+
* 入参是任意抛出值,本函数就是窄化动作本身(unknown 出境登记见 typeshape 门 RATCHET 注)。
|
|
64
|
+
*/
|
|
65
|
+
export declare function isHitlSafetyErrorLike(e: unknown): boolean;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { isOwnEngineRun } from '../subagentContentStore.js';
|
|
2
|
+
import { rowIdTail } from '../workflow.js';
|
|
3
|
+
import { hostSessionFor } from '../host.js';
|
|
4
|
+
import { DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
5
|
+
/**
|
|
6
|
+
* 这行 pending 能不能**正向证明**属于本宿主会话(fail-closed;两腿语义见文件头)。
|
|
7
|
+
* 腿序:own-run 先问(child 委派行不依赖会话 id),会话腿后问。
|
|
8
|
+
*/
|
|
9
|
+
export function pendingRowIsOwnedByThisSession(row, deps) {
|
|
10
|
+
const sessionKey = deps?.sessionKey ?? DEFAULT_SESSION_KEY;
|
|
11
|
+
// ① 本宿主亲手驱动的 run / 由它闭包进来的自家子代 —— 不依赖会话 id。
|
|
12
|
+
// 进程级缺省腿只在默认会话键下启用(多会话宿主必须注入会话粒度的口,见 deps.sessionKey 注)。
|
|
13
|
+
const rowTaskId = typeof row.taskId === 'string' ? row.taskId : '';
|
|
14
|
+
const ownRun = deps?.isOwnRun ?? (sessionKey === DEFAULT_SESSION_KEY ? isOwnEngineRun : undefined);
|
|
15
|
+
if (ownRun !== undefined && rowTaskId.length > 0 && ownRun(rowIdTail(rowTaskId)))
|
|
16
|
+
return true;
|
|
17
|
+
// ② 行自带 sessionId === 本会话当前引擎会话。任一侧缺席/空串 = 判不出,不是命中。
|
|
18
|
+
const currentSessionId = deps?.currentSessionId ?? (() => hostSessionFor(sessionKey)?.currentSessionId());
|
|
19
|
+
const sessionId = currentSessionId();
|
|
20
|
+
const rowSessionId = typeof row.sessionId === 'string' ? row.sessionId : '';
|
|
21
|
+
return (typeof sessionId === 'string' &&
|
|
22
|
+
sessionId.length > 0 &&
|
|
23
|
+
rowSessionId.length > 0 &&
|
|
24
|
+
rowSessionId === sessionId);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* questions payload 从 pending.input 读 —— **结构判定**收窄到 AskQuestion 必需键(question
|
|
28
|
+
* 非 string / questions 非数组 / 空数组 ⇒ null,不 cast 硬塞;缺 header 的行照 CC 形容忍,
|
|
29
|
+
* overlay 自兜)。铸空卡 = 假 affordance,拿不到题就诚实返 null。
|
|
30
|
+
*/
|
|
31
|
+
export function askQuestionsFromPending(pending) {
|
|
32
|
+
const input = pending.input;
|
|
33
|
+
if (typeof input !== 'object' || input === null)
|
|
34
|
+
return null;
|
|
35
|
+
const qs = input.questions;
|
|
36
|
+
if (!Array.isArray(qs) || qs.length === 0)
|
|
37
|
+
return null;
|
|
38
|
+
const ok = qs.every(q => typeof q === 'object' && q !== null && typeof q.question === 'string');
|
|
39
|
+
return ok ? qs : null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* HitlSafetyError 判型的 duck-check(按 `name`,不按 instanceof —— 宿主 bundle 里存在第二份包
|
|
43
|
+
* 实例时 instanceof 会分叉,sseIdleTriage 同款纪律)。安全信号(binding_mismatch 族)契约禁
|
|
44
|
+
* 盲重试,重开腿据此把「可重试的瞬时故障」与「必须交还人重决的安全停」分开。
|
|
45
|
+
* 入参是任意抛出值,本函数就是窄化动作本身(unknown 出境登记见 typeshape 门 RATCHET 注)。
|
|
46
|
+
*/
|
|
47
|
+
export function isHitlSafetyErrorLike(e) {
|
|
48
|
+
return typeof e === 'object' && e !== null && e.name === 'HitlSafetyError';
|
|
49
|
+
}
|
|
@@ -4,6 +4,7 @@ import { hostLog } from '../host.js';
|
|
|
4
4
|
import { surfaceFsApprovalAndDecide } from './toolApprovalWire.js';
|
|
5
5
|
import { observeCancelByDeny } from './hitlHostSurface.js';
|
|
6
6
|
import { isAskTool } from './frameRouter.js';
|
|
7
|
+
import { approvalCallKey, askGateQuestionId } from './gateIdentity.js';
|
|
7
8
|
/** 一 turn 内最多循环这么多次 park(防御:引擎/模型病态连环提问时不无限 attach)。 */
|
|
8
9
|
const MAX_GATE_HOPS = 24;
|
|
9
10
|
/**
|
|
@@ -121,7 +122,8 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal) {
|
|
|
121
122
|
};
|
|
122
123
|
}
|
|
123
124
|
// overlay 往返:合成 frame(id 独占命名空间 hitl-ask:)→ local responder 一次性收答。
|
|
124
|
-
|
|
125
|
+
// A-028.3:id 经 gateIdentity 唯一铸口(呈现台账/壳侧重开腿的键推导同源,字面不再各铸各的)。
|
|
126
|
+
const questionId = askGateQuestionId(approvalCallKey(gatedCallId, taskId));
|
|
125
127
|
const answer = await new Promise(resolve => {
|
|
126
128
|
const unregister = registerLocalQuestionResponder(questionId, async (_id, a) => {
|
|
127
129
|
cleanup();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** 一次读面探测的判决(三态;见文件头「这个模块是什么」)。 */
|
|
2
|
+
export type ParkRowProbe<Row> = {
|
|
3
|
+
kind: 'row';
|
|
4
|
+
row: Row;
|
|
5
|
+
}
|
|
6
|
+
/** 此刻没有可决断的行,且拿不出「已了结」的正面证据 ⇒ 可能只是还没出生。 */
|
|
7
|
+
| {
|
|
8
|
+
kind: 'unborn';
|
|
9
|
+
reason: string;
|
|
10
|
+
}
|
|
11
|
+
/** 有正面证据:这里没有本臂该决的事 ⇒ 再等也不会变(如行在但门种不属本臂)。 */
|
|
12
|
+
| {
|
|
13
|
+
kind: 'settled';
|
|
14
|
+
reason: string;
|
|
15
|
+
};
|
|
16
|
+
/** 有界重查环的判决。`waitedMs`/`probes` 是留证位(debug 档),不参与任何分支。 */
|
|
17
|
+
export type ParkRowWaitVerdict<Row> = {
|
|
18
|
+
kind: 'row';
|
|
19
|
+
row: Row;
|
|
20
|
+
waitedMs: number;
|
|
21
|
+
probes: number;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'unborn';
|
|
24
|
+
reason: string;
|
|
25
|
+
waitedMs: number;
|
|
26
|
+
probes: number;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'settled';
|
|
29
|
+
reason: string;
|
|
30
|
+
waitedMs: number;
|
|
31
|
+
probes: number;
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'aborted';
|
|
34
|
+
waitedMs: number;
|
|
35
|
+
probes: number;
|
|
36
|
+
};
|
|
37
|
+
export interface ParkRowBirthWaitDeps<Row> {
|
|
38
|
+
/**
|
|
39
|
+
* 读一次真状态(第 N 次探测,N 从 1 起)。
|
|
40
|
+
*
|
|
41
|
+
* 🔴 `signal` **必须**被透传进真实读面(HTTP 请求等):它是本环对 `budgetMs` 的**执行手段** ——
|
|
42
|
+
* 环在窗尽时 abort 它,读面才真的被掐断。不接这个 signal 的 probe 会让「有界」退化成一句
|
|
43
|
+
* 口号:一次挂死的读面能把总窗拖到任意长。
|
|
44
|
+
*/
|
|
45
|
+
probe: (attempt: number, signal?: AbortSignal) => Promise<ParkRowProbe<Row>>;
|
|
46
|
+
/** 总窗上限(ms)。`<=0` ⇒ 恰一次 probe,不等(那一拍不设内部 deadline,与修前逐字同拍)。 */
|
|
47
|
+
budgetMs: number;
|
|
48
|
+
/** 轮询间隔(ms);实际 sleep = min(intervalMs, 窗内剩余)。 */
|
|
49
|
+
intervalMs: number;
|
|
50
|
+
/** 时钟(测试注入;缺省 Date.now)。 */
|
|
51
|
+
now?: () => number;
|
|
52
|
+
/** sleep(测试注入;缺省 = 可中断等待,见 {@link parkRowPollDelay})。 */
|
|
53
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
54
|
+
/** 中断口(Esc/turn abort);每轮读,真停。 */
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 有界、可中断地等一条 park 的待决行出生。语义与边界见文件头;绝不抛。
|
|
59
|
+
*
|
|
60
|
+
* 🔴 判决只有 `row` 一态可以往下走 —— `unborn`(窗尽)与 `aborted` 都必须由调用方翻成**如实的
|
|
61
|
+
* 失败**,绝不许翻成「已决/已成功」(那正是 cli #269 的病)。
|
|
62
|
+
*/
|
|
63
|
+
export declare function waitForParkRowBirth<Row>(deps: ParkRowBirthWaitDeps<Row>): Promise<ParkRowWaitVerdict<Row>>;
|