@sema-agent/client-core 0.49.0 → 0.51.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 +167 -0
- package/README.md +3 -1
- package/dist/adapter/activeRunSelfHeal.d.ts +17 -1
- package/dist/adapter/activeRunSelfHeal.js +45 -7
- package/dist/engineErrorCodes.d.ts +16 -0
- package/dist/engineErrorCodes.js +17 -0
- package/dist/hitl/askGateWire.d.ts +3 -2
- package/dist/hitl/askGateWire.js +82 -6
- package/dist/hitl/frameRouter.d.ts +13 -0
- package/dist/hitl/parkResolver.d.ts +56 -1
- package/dist/hitl/parkResolver.js +167 -50
- package/dist/hitl/toolApprovalWire.d.ts +3 -0
- package/dist/hitl/toolApprovalWire.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/selfOrchestrationDenial.d.ts +210 -0
- package/dist/selfOrchestrationDenial.js +255 -0
- package/docs/INTEGRATION-CLIENTS.md +333 -15
- package/package.json +1 -1
|
@@ -61,6 +61,15 @@ export declare const ENGINE_ABORT_TOOL_RESULT = "Operation aborted";
|
|
|
61
61
|
*/
|
|
62
62
|
export declare function toolEndOutputText(output: unknown): string | undefined;
|
|
63
63
|
/** 本桥消费的 wire 面(@sema-agent/sdk AgentClient 的结构切片,mock 可注入)。 */
|
|
64
|
+
/** L-80:非进展 reattach 的一次用户面告知(命名形,typeshape B4 棘轮:导出签名不带内联匿名对象)。 */
|
|
65
|
+
export interface ParkReattachNotice {
|
|
66
|
+
/** 该臂同因连续第几次。 */
|
|
67
|
+
attempt: number;
|
|
68
|
+
/** 该臂上限(到了就诚实收场)。 */
|
|
69
|
+
max: number;
|
|
70
|
+
/** 真因 token(已解决码 / 传输错类词)。 */
|
|
71
|
+
reason: string;
|
|
72
|
+
}
|
|
64
73
|
export interface AskGateWireDeps {
|
|
65
74
|
/** approvals.list/decide + assistant(HitlBridge 的 client 切片)。 */
|
|
66
75
|
client: HitlClientLike;
|
|
@@ -72,6 +81,10 @@ export interface AskGateWireDeps {
|
|
|
72
81
|
/** POST /v1/tool-approvals/:id/respond(server 1.191 同步帧腿,[830]①)。缺省=不消费
|
|
73
82
|
* tool_approval 帧(帧被吞、引擎按自身 fail-closed TTL 自决)——mock/旧引擎路径零影响。 */
|
|
74
83
|
respondToolApproval?: RespondToolApprovalFn;
|
|
84
|
+
/** L-80(2026-09-03,[6215]):**非进展** reattach 的用户面告知口 —— 每一轮「引擎重放了同一张 park /
|
|
85
|
+
* decide 出站瞬断耗尽」而壳只能再附着一次时调一次(`attempt`=该臂同因连续第几次,`max`=该臂上限,
|
|
86
|
+
* `reason`=真因 token)。缺席 = 只留 debug 行(修前形:24 轮空转用户零告知)。进展轮**不**调。 */
|
|
87
|
+
onParkReattach?: (e: ParkReattachNotice) => void;
|
|
75
88
|
/** #229 respond-note 供给链(0.29.0 发包扫描门修,2026-08-12):帧腿车道参数(能力位读数),
|
|
76
89
|
* 宿主从自己的 caps 缓存供给。缺席 = note 门 fail-closed 到「不发」侧(决断照常,现状字节
|
|
77
90
|
* 不变)—— 此前包内唯一生产调用点(routeFrame 的 tool_approval 臂)无此位,note 恒不发。
|
|
@@ -17,6 +17,32 @@ import { type GateCurrentPending } from './hitlBridge.js';
|
|
|
17
17
|
import { type QuestionAnswer } from '../liveQuestionStore.js';
|
|
18
18
|
import type { AskAnsweredOutput, GateLedger } from './gateLedger.js';
|
|
19
19
|
import { type AskGateWireDeps, type GatePark } from './frameRouter.js';
|
|
20
|
+
/**
|
|
21
|
+
* L-80(2026-09-03,[6215];#357 复发):**连续非进展轮**上限 —— 数的是「壳再附着之后那段流里一个
|
|
22
|
+
* host 推进帧都没有」的轮次,**不是** park 次数。修前这里是 `24` 且按每次 park 递增
|
|
23
|
+
* (askGateWire `hops++` 无条件),于是一个 turn 里模型对同一失败 Edit 重试 24 次、每次真被门真被决,
|
|
24
|
+
* 也会在第 25 次撞成 `gate hop limit (24) exceeded`——一句与真因无关的预算话术。CC 没有这条预算:
|
|
25
|
+
* 模型问 N 次就答 N 次。本闸只留作最后防线:连续 N 轮零进展(坐标失配空转 / reopen 类 re-park /
|
|
26
|
+
* decide 出站恒断)才收场,收场前再呈一次收场卡、终帧带真因与出路。
|
|
27
|
+
*/
|
|
28
|
+
export declare const MAX_GATE_HOPS = 2;
|
|
29
|
+
/** 一轮 park 的进展判决(驱动侧按 `isHostProgressFrame` 观察,见 askGateWire)。 */
|
|
30
|
+
export type HopRound = 'progress' | 'stalled';
|
|
31
|
+
/** hop 预算状态:`stalled` = 连续非进展轮数(进展即清零);`total` = 本 turn park 总数(只用于日志)。 */
|
|
32
|
+
export interface HopBudget {
|
|
33
|
+
readonly stalled: number;
|
|
34
|
+
readonly total: number;
|
|
35
|
+
}
|
|
36
|
+
/** 纯函数:上一轮预算 + 本轮判决 ⇒ 新预算(不就地改入参;`undefined` = 本 turn 第一轮)。 */
|
|
37
|
+
export declare function nextHopBudget(prev: HopBudget | undefined, round: HopRound): HopBudget;
|
|
38
|
+
/**
|
|
39
|
+
* 硬兜底(对抗复审 r1 [high]①,r6 [high] 收窄):一 turn 内**解析不出身份**的 park 数上限。连续非进展计数靠
|
|
40
|
+
* 「解析到的 pending 行身份 + host 推进帧」判进展;解析不到行(tool-less park / approvals 恒空)时身份判不出 ⇒
|
|
41
|
+
* 只能靠推进帧,一个每轮先吐一帧文本再原样 park 的病态引擎在那一形下仍可能无限。**只数这一类**:每轮都解析到
|
|
42
|
+
* 新 call 且真推进的门(模型重试同一失败调用 N 次)不吃这格 —— CC 没有此闸,问 65 次就答 65 次。
|
|
43
|
+
* 命中即收场、终帧如实说「本 turn 有 N 次 park 解析不出身份」。
|
|
44
|
+
*/
|
|
45
|
+
export declare const MAX_TOTAL_PARKS = 64;
|
|
20
46
|
/**
|
|
21
47
|
* REF-CC-033:`GateOutcome` 的 `failed` 分支闭集判别码。来源两处:①本文件自己的
|
|
22
48
|
* 「pending 行取不到」早退(`surfaceGateAndDecide` 里 `!pending` 那条);②`HitlBridge` 抛出的
|
|
@@ -69,8 +95,19 @@ export declare function isAlreadyResolvedGateReason(reason: unknown): boolean;
|
|
|
69
95
|
* (multiSelect 与对话框同款 ", " lossy join;note → annotations.notes)。 */
|
|
70
96
|
export declare function toAnsweredOutput(questions: unknown[], answer: QuestionAnswer): AskAnsweredOutput;
|
|
71
97
|
/** 外环决断的两种出路(见文件头注)。 */
|
|
98
|
+
/** resolvePark 的续流判决:`progress` 位 = 这一轮决断真落地了一次(驱动侧不据此复位计数 —— 复位只看
|
|
99
|
+
* 下一段流有没有 host 推进帧);非进展轮携 `reason`(收场文案用)。 */
|
|
72
100
|
export type ParkResolution = {
|
|
73
101
|
kind: 'reattach';
|
|
102
|
+
progress: true;
|
|
103
|
+
presented: boolean;
|
|
104
|
+
gatedCallId?: string | undefined;
|
|
105
|
+
} | {
|
|
106
|
+
kind: 'reattach';
|
|
107
|
+
progress: false;
|
|
108
|
+
reason: string;
|
|
109
|
+
presented: boolean;
|
|
110
|
+
gatedCallId?: string | undefined;
|
|
74
111
|
} | {
|
|
75
112
|
kind: 'failsoft';
|
|
76
113
|
events: readonly AgentEvent[];
|
|
@@ -80,8 +117,26 @@ export interface ParkResolveCtx {
|
|
|
80
117
|
led: GateLedger;
|
|
81
118
|
/** sync leg 捕获的真 run handle(内环 `TaskIdRef` 的当前值)。 */
|
|
82
119
|
taskId: string;
|
|
83
|
-
/** 本 turn
|
|
120
|
+
/** 本 turn park 总数(日志用;**不再**是预算计量,见 {@link MAX_GATE_HOPS})。 */
|
|
84
121
|
hops: number;
|
|
122
|
+
/** 本 turn 内**解析不出身份**的 park 数(驱动侧累计;超过 {@link MAX_TOTAL_PARKS} 即硬兜底收场)。 */
|
|
123
|
+
unverifiableParks?: number;
|
|
124
|
+
/** 连续非进展轮数(驱动侧 {@link nextHopBudget} 算出;超过 {@link MAX_GATE_HOPS} 即收场)。 */
|
|
125
|
+
stalledRounds?: number;
|
|
126
|
+
/** 上一轮非进展的真因 token(收场文案用)。 */
|
|
127
|
+
lastStallReason?: string | undefined;
|
|
128
|
+
/** 上一轮有没有真呈过卡(收场卡只在**没呈过**时才呈 —— 呈过再呈 = 把同一张失效卡再问一遍,F15-e② 禁)。 */
|
|
129
|
+
lastRoundPresentedCard?: boolean | undefined;
|
|
85
130
|
signal?: AbortSignal | undefined;
|
|
86
131
|
}
|
|
87
132
|
export declare function resolvePark(park: GatePark, ctx: ParkResolveCtx): Promise<ParkResolution>;
|
|
133
|
+
/**
|
|
134
|
+
* 驱动侧在**预算提交之后**判定触顶时的收场(对抗复审 r4 [high]①:有推进帧的轮次触顶必须等权威身份解析完 ——
|
|
135
|
+
* 先呈 B、决 B,提交后仍是同一只 call 才收场;这一轮已经呈过卡,不再呈收场卡)。
|
|
136
|
+
*/
|
|
137
|
+
export declare function stalledTerminal(park: GatePark, ctx: ParkResolveCtx, stalledRounds: number, lastStallReason: string | undefined,
|
|
138
|
+
/** 这一轮的决断有没有落地(r5 [medium]③):落地了就**不许**说「could not be answered」「still parked」—— 那是谎报。 */
|
|
139
|
+
decidedThisRound: boolean,
|
|
140
|
+
/** 这一轮决断落在哪只 call(解析到的行身份):收场排水前先把**它**的扣留帧摘掉(r6 [medium]:两只同族 call 都在
|
|
141
|
+
* 扣留、park 帧无可信身份时,决断臂刻意保守不摘,排水会把刚批准的那只当 abort 吐出)。 */
|
|
142
|
+
decidedGatedCallId?: string | undefined): ParkResolution;
|
|
@@ -5,8 +5,30 @@ import { surfaceFsApprovalAndDecide } from './toolApprovalWire.js';
|
|
|
5
5
|
import { observeCancelByDeny } from './hitlHostSurface.js';
|
|
6
6
|
import { flushHeldWithInterruptRewrite, isAskTool } from './frameRouter.js';
|
|
7
7
|
import { approvalCallKey, askGateQuestionId } from './gateIdentity.js';
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* L-80(2026-09-03,[6215];#357 复发):**连续非进展轮**上限 —— 数的是「壳再附着之后那段流里一个
|
|
10
|
+
* host 推进帧都没有」的轮次,**不是** park 次数。修前这里是 `24` 且按每次 park 递增
|
|
11
|
+
* (askGateWire `hops++` 无条件),于是一个 turn 里模型对同一失败 Edit 重试 24 次、每次真被门真被决,
|
|
12
|
+
* 也会在第 25 次撞成 `gate hop limit (24) exceeded`——一句与真因无关的预算话术。CC 没有这条预算:
|
|
13
|
+
* 模型问 N 次就答 N 次。本闸只留作最后防线:连续 N 轮零进展(坐标失配空转 / reopen 类 re-park /
|
|
14
|
+
* decide 出站恒断)才收场,收场前再呈一次收场卡、终帧带真因与出路。
|
|
15
|
+
*/
|
|
16
|
+
export const MAX_GATE_HOPS = 2;
|
|
17
|
+
/** 纯函数:上一轮预算 + 本轮判决 ⇒ 新预算(不就地改入参;`undefined` = 本 turn 第一轮)。 */
|
|
18
|
+
export function nextHopBudget(prev, round) {
|
|
19
|
+
const total = (prev?.total ?? 0) + 1;
|
|
20
|
+
return round === 'progress' ? { stalled: 0, total } : { stalled: (prev?.stalled ?? 0) + 1, total };
|
|
21
|
+
}
|
|
22
|
+
/** decide 出站瞬断耗尽臂(retryExhausted)的同因连续上限:第 3 次同因即诚实收场(修前该臂**无闸**)。 */
|
|
23
|
+
const MAX_TRANSPORT_REATTACHES = 2;
|
|
24
|
+
/**
|
|
25
|
+
* 硬兜底(对抗复审 r1 [high]①,r6 [high] 收窄):一 turn 内**解析不出身份**的 park 数上限。连续非进展计数靠
|
|
26
|
+
* 「解析到的 pending 行身份 + host 推进帧」判进展;解析不到行(tool-less park / approvals 恒空)时身份判不出 ⇒
|
|
27
|
+
* 只能靠推进帧,一个每轮先吐一帧文本再原样 park 的病态引擎在那一形下仍可能无限。**只数这一类**:每轮都解析到
|
|
28
|
+
* 新 call 且真推进的门(模型重试同一失败调用 N 次)不吃这格 —— CC 没有此闸,问 65 次就答 65 次。
|
|
29
|
+
* 命中即收场、终帧如实说「本 turn 有 N 次 park 解析不出身份」。
|
|
30
|
+
*/
|
|
31
|
+
export const MAX_TOTAL_PARKS = 64;
|
|
10
32
|
/**
|
|
11
33
|
* 「已解决 ⇒ reattach」臂的**同因连续命中上限**(#357,现网 P0;[5522] 定谳 / [5525] server 答复)。
|
|
12
34
|
* 同一判据源连续第 `N+1` 次命中即**不再** reattach。
|
|
@@ -140,7 +162,7 @@ export function toAnsweredOutput(questions, answer) {
|
|
|
140
162
|
async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal,
|
|
141
163
|
/** 这张 park 的待批 call 身份(server ≥7.41.0;缺席是常态)。语义与不 fail-closed 的理由
|
|
142
164
|
* 与 fs 腿同一条,见 `toolApprovalWire.surfaceFsApprovalAndDecide` 的同名参数头注。 */
|
|
143
|
-
parkGatedCallId) {
|
|
165
|
+
parkGatedCallId, onPresented) {
|
|
144
166
|
if (!hasQuestionOverlay()) {
|
|
145
167
|
return { kind: 'failed', reason: 'no question overlay mounted (print/non-REPL mode)' };
|
|
146
168
|
}
|
|
@@ -197,6 +219,7 @@ parkGatedCallId) {
|
|
|
197
219
|
return;
|
|
198
220
|
}
|
|
199
221
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
222
|
+
onPresented?.(); // L-80:ask 腿的呈现回执与 fs 腿同一枚旗(对抗复审 r2 [high]②:此前只数卡口)
|
|
200
223
|
publishQuestionFrame({ type: 'question', questionId, questions });
|
|
201
224
|
});
|
|
202
225
|
const bridge = new HitlBridge(deps.client, taskId);
|
|
@@ -304,11 +327,11 @@ function isFetchStepNoPending(outcome) {
|
|
|
304
327
|
* @param gatedCallId 交给取件腿的**身份**(wire 给的 park 身份;缺席是常态)。首击与坐标重探
|
|
305
328
|
* 传的是**同一个值** —— 重探要的是读数新鲜,不是判据放宽,理由见调用点的红线注。
|
|
306
329
|
*/
|
|
307
|
-
async function surfaceParkGate(park, ctx, gatedCallId) {
|
|
330
|
+
async function surfaceParkGate(park, ctx, gatedCallId, onPresented) {
|
|
308
331
|
const { deps, led, taskId } = ctx;
|
|
309
332
|
if (park.gate === 'fs') {
|
|
310
333
|
// [816] 放宽腿:fs 写权限 gate → CC 三选卡(vendored PermissionRequest)→ decide。
|
|
311
|
-
const outcome = await surfaceFsApprovalAndDecide({ client: deps.client }, taskId, argsByCallOf(led), ctx.signal, gatedCallId);
|
|
334
|
+
const outcome = await surfaceFsApprovalAndDecide({ client: deps.client, ...(onPresented ? { onPresented } : {}) }, taskId, argsByCallOf(led), ctx.signal, gatedCallId);
|
|
312
335
|
// #51: `outcome`'s declared type is the wider `GateOutcome |
|
|
313
336
|
// FsApprovalOutcome`; both unions share a 'decided' kind with
|
|
314
337
|
// different optional fields (`answered` vs `denied`), so a plain
|
|
@@ -321,11 +344,11 @@ async function surfaceParkGate(park, ctx, gatedCallId) {
|
|
|
321
344
|
}
|
|
322
345
|
return outcome;
|
|
323
346
|
}
|
|
324
|
-
return surfaceGateAndDecide(deps, taskId, argsByCallOf(led), ctx.signal, gatedCallId);
|
|
347
|
+
return surfaceGateAndDecide(deps, taskId, argsByCallOf(led), ctx.signal, gatedCallId, onPresented);
|
|
325
348
|
}
|
|
326
349
|
export async function resolvePark(park, ctx) {
|
|
327
350
|
// `deps` 不在本函数直读:呈卡/决断两族腿统一经 `surfaceParkGate`(#357 单源化,见其头注)。
|
|
328
|
-
const { led, taskId
|
|
351
|
+
const { led, taskId } = ctx;
|
|
329
352
|
let outcome;
|
|
330
353
|
// REF-CC-034:这次 park 的候选 callId(仅 fs 分支填,已决断身份匹配用)——ask 分支不填,
|
|
331
354
|
// 因为 `surfaceGateAndDecide` 的 `!pending` 早退已经自带 `code:'no_pending'`(REF-CC-033),
|
|
@@ -333,13 +356,44 @@ export async function resolvePark(park, ctx) {
|
|
|
333
356
|
// re-attach 重放的 `suspended` park 没有配套的新 tool_start/tool_end,那个栈在这次重放之前
|
|
334
357
|
// 早被 tool_end 的 drop 清空了(见台账声明处注),取不到候选。
|
|
335
358
|
let candidateGatedCallId;
|
|
336
|
-
|
|
337
|
-
|
|
359
|
+
const stalledRounds = ctx.stalledRounds ?? 0;
|
|
360
|
+
let closingCardTried = false;
|
|
361
|
+
// 呈现回执(对抗复审 r2 [medium]②③):每次 resolvePark 各自一枚旗,fs 腿经 deps.onPresented、ask 腿经
|
|
362
|
+
// publishQuestionFrame 前的同一枚旗置位;并发会话互不串扰,规则直决/取件失败恒 false。
|
|
363
|
+
let presentedThisRound = false;
|
|
364
|
+
const witness = () => {
|
|
365
|
+
presentedThisRound = true;
|
|
366
|
+
};
|
|
367
|
+
if ((ctx.unverifiableParks ?? 0) > MAX_TOTAL_PARKS) {
|
|
368
|
+
// 硬兜底:见 MAX_TOTAL_PARKS 头注(只数解析不出身份的 park)。不呈收场卡,终帧如实。
|
|
369
|
+
closingCardTried = true;
|
|
370
|
+
outcome = {
|
|
371
|
+
kind: 'failed',
|
|
372
|
+
reason: `${ctx.unverifiableParks ?? 0} parks in this single turn resolved to no identifiable approval (hard cap ${MAX_TOTAL_PARKS}; last: ${ctx.lastStallReason ?? 'the engine kept parking'})`,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
else if (stalledRounds > MAX_GATE_HOPS) {
|
|
376
|
+
// 最后防线:连续 N 轮零进展。收场前**再呈一次收场卡**(现读队列的可决行,用户面能看见能答),
|
|
377
|
+
// 真呈出来且决了就按决断走;呈不出/仍失败才 failsoft,终帧带真因与出路(不再是预算话术)。
|
|
378
|
+
closingCardTried = true;
|
|
379
|
+
const stalledReason = `the run stayed parked across ${stalledRounds} consecutive re-attach rounds without any host progress ` +
|
|
380
|
+
`(last: ${ctx.lastStallReason ?? 'unknown'})`;
|
|
381
|
+
if (ctx.lastRoundPresentedCard === true) {
|
|
382
|
+
// 上一轮已经把卡呈给用户、用户也答了、引擎还是原样 park 回来 —— 再呈一次只是把同一张失效卡再问一遍。
|
|
383
|
+
outcome = { kind: 'failed', reason: stalledReason };
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
const closing = await surfaceParkGate(park, ctx, park.gatedCallId, witness);
|
|
387
|
+
outcome =
|
|
388
|
+
closing.kind === 'decided' || closing.kind === 'aborted'
|
|
389
|
+
? closing
|
|
390
|
+
: { kind: 'failed', reason: `${stalledReason}; closing re-read: ${closing.reason}` };
|
|
391
|
+
}
|
|
338
392
|
}
|
|
339
393
|
else {
|
|
340
394
|
if (park.gate === 'fs')
|
|
341
395
|
candidateGatedCallId = led.lastFsOrShellGatedCallId();
|
|
342
|
-
outcome = await surfaceParkGate(park, ctx, park.gatedCallId);
|
|
396
|
+
outcome = await surfaceParkGate(park, ctx, park.gatedCallId, witness);
|
|
343
397
|
}
|
|
344
398
|
// #110 缺陷② b/c —— **重放的、早已决断过的 park 不是失败**。
|
|
345
399
|
// durable re-attach 必然会把 park 帧再送一遍(`lastEventId` 再准也只能精确到帧,park 就在
|
|
@@ -371,7 +425,8 @@ export async function resolvePark(park, ctx) {
|
|
|
371
425
|
const alreadyDecidedById = outcome.kind === 'failed' &&
|
|
372
426
|
candidateGatedCallId !== undefined &&
|
|
373
427
|
led.takeDecided(candidateGatedCallId);
|
|
374
|
-
|
|
428
|
+
// L-80:触顶收场的合成失败**不进**「已解决」臂(它的文案判据会把 `(last: no pending …)` 认成同族再续一轮)。
|
|
429
|
+
if (!closingCardTried && outcome.kind === 'failed' && (isAlreadyResolvedFailure(outcome) || alreadyDecidedById)) {
|
|
375
430
|
const firstReason = outcome.reason;
|
|
376
431
|
// 键按语义分(见 `alreadyResolvedStreakKey` 头注);命中的那个文案词只进诊断串。
|
|
377
432
|
const streakKey = alreadyResolvedStreakKey(!isAlreadyResolvedFailure(outcome) && alreadyDecidedById);
|
|
@@ -386,8 +441,8 @@ export async function resolvePark(park, ctx) {
|
|
|
386
441
|
// 的那把闸。抹掉它就等于把 F10-j 钉住的那条绕道兜底重新打开 —— 身份指着一行反族行、队列里
|
|
387
442
|
// 又没有别的行时,「任意行」兜底会把那行反族行捞回来呈卡 + decide(卡面全错、决断打在另一个
|
|
388
443
|
// checkpoint 上)。重探要的是**读数新鲜**,不是**判据放宽**。
|
|
389
|
-
const rescan = isFetchStepNoPending(outcome) &&
|
|
390
|
-
? await surfaceParkGate(park, ctx, park.gatedCallId)
|
|
444
|
+
const rescan = isFetchStepNoPending(outcome) && stalledRounds <= MAX_GATE_HOPS // L-80:按连续非进展轮判,不按 park 总数
|
|
445
|
+
? await surfaceParkGate(park, ctx, park.gatedCallId, witness)
|
|
391
446
|
: undefined;
|
|
392
447
|
// 🔴 只有**真进展**才采信重探的结果:决断成功 / 用户 Esc / 瞬断耗尽(那一形有自己的重呈臂)。
|
|
393
448
|
// 重探自己失败(网络类、或队列确实没有可决行)⇒ **不采信**,走限次闸兜底 —— 采信的话,
|
|
@@ -397,7 +452,8 @@ export async function resolvePark(park, ctx) {
|
|
|
397
452
|
(rescan.kind === 'decided' ||
|
|
398
453
|
rescan.kind === 'aborted' ||
|
|
399
454
|
(rescan.kind === 'failed' && rescan.retryExhausted === true))) {
|
|
400
|
-
|
|
455
|
+
// L-80:**不在这里**复位同因计数 —— 复位只在驱动侧观察到 host 进展帧时发生(#357 的闸曾被这一行
|
|
456
|
+
// 每轮绕空:坐标失配序下每轮都「找到新行→决成功→引擎原样重放」,计数永远回不到 2)。
|
|
401
457
|
hostLog('debug', `liveHitlAskWire: gate reported already-resolved (${firstReason}) but a fresh approvals re-read for run ` +
|
|
402
458
|
`${taskId} surfaced a decidable row under the current coordinates — re-presented it (rescan: ${rescan.kind}) ` +
|
|
403
459
|
`instead of re-attaching on the stale park identity`);
|
|
@@ -407,10 +463,11 @@ export async function resolvePark(park, ctx) {
|
|
|
407
463
|
const streak = led.noteAlreadyResolvedGate(streakKey);
|
|
408
464
|
if (streak <= MAX_ALREADY_RESOLVED_REATTACHES) {
|
|
409
465
|
const seq = led.lastSeq();
|
|
466
|
+
notifyParkReattach(ctx, streak, MAX_ALREADY_RESOLVED_REATTACHES, reasonToken ?? streakKey);
|
|
410
467
|
hostLog('debug', `liveHitlAskWire: gate already resolved (${firstReason}) — replayed park, re-attaching runs.events(${taskId})${seq ? ` from seq ${seq}` : ''} instead of failing the turn` +
|
|
411
468
|
(alreadyDecidedById ? ` [decidedGates id match: ${candidateGatedCallId}]` : '') +
|
|
412
469
|
(led.decidedCount() > 0 ? ` [decided so far: ${led.decidedCount()}]` : ''));
|
|
413
|
-
return { kind: 'reattach' };
|
|
470
|
+
return { kind: 'reattach', progress: false, reason: reasonToken ?? streakKey, presented: presentedThisRound, gatedCallId: outcome.gatedCallId };
|
|
414
471
|
}
|
|
415
472
|
// ── 兜底:同因连续第 2 次 ⇒ 不再空转,诚实收场 ──────────────────────────────────────
|
|
416
473
|
// 件B(可见告知):真因走**终帧的 errorMessage**(下面 fail-soft 汇流合成的那一条),不是只落
|
|
@@ -437,42 +494,27 @@ export async function resolvePark(park, ctx) {
|
|
|
437
494
|
// 用户**(重呈的呈现就是卡本身,归端零新 UI);hop 预算照吃(每轮都要人再答一次,不会空转)。
|
|
438
495
|
// 引擎真死时失败也尽快显形:下一轮的 approvals.list / runs.events 对死引擎当场失败,walks 既有
|
|
439
496
|
// 诚实红(reason 是 approvals.list failed,不带 retryExhausted ⇒ 不再进本臂)。
|
|
440
|
-
if (outcome.kind === 'failed' && outcome.retryExhausted === true) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
497
|
+
if (outcome.kind === 'failed' && outcome.retryExhausted === true && !closingCardTried) {
|
|
498
|
+
// L-80:该臂修前**无次数闸**(头注自认「approvals.list 网络失败这类真失败会连吃 24 个 hop」)——
|
|
499
|
+
// 现进同一本同因账:连续第 3 次即收场(下方 failsoft),终帧带真因。
|
|
500
|
+
const token = transportReasonToken(outcome.reason);
|
|
501
|
+
const streak = led.noteAlreadyResolvedGate(`transport:${token}`);
|
|
502
|
+
if (streak <= MAX_TRANSPORT_REATTACHES) {
|
|
503
|
+
const seq = led.lastSeq();
|
|
504
|
+
notifyParkReattach(ctx, streak, MAX_TRANSPORT_REATTACHES, `decide transport failed: ${token}`);
|
|
505
|
+
hostLog('debug', `liveHitlAskWire: decide transport retries exhausted (${outcome.reason}) — re-presenting the gate via ` +
|
|
506
|
+
`re-attach runs.events(${taskId})${seq ? ` from seq ${seq}` : ''} instead of failing the turn (the run is still parked and the pending row is still decidable) [${streak}/${MAX_TRANSPORT_REATTACHES}]`);
|
|
507
|
+
return { kind: 'reattach', progress: false, reason: `decide transport failed: ${token}`, presented: presentedThisRound, gatedCallId: outcome.gatedCallId };
|
|
508
|
+
}
|
|
509
|
+
outcome = {
|
|
510
|
+
kind: 'failed',
|
|
511
|
+
...(outcome.gatedCallId !== undefined ? { gatedCallId: outcome.gatedCallId } : {}),
|
|
512
|
+
reason: `the decide call failed on transport ${streak} times in a row while the run stayed parked (${outcome.reason})`,
|
|
513
|
+
};
|
|
445
514
|
}
|
|
446
515
|
if (outcome.kind !== 'decided') {
|
|
447
516
|
hostLog('debug', `liveHitlAskWire: gate not decided (${outcome.kind}${'reason' in outcome ? `: ${outcome.reason}` : ''}) — fail-soft to suspended terminal`);
|
|
448
|
-
|
|
449
|
-
// 件 B(异源复审 finding 采纳):**五个排水出口一律走同一个中断感知出口** —— 本出口今天恒是
|
|
450
|
-
// park 批(走到这里的前提就是有一张 park),零-park 硬门必挡,所以是**语义等价的 no-op**;
|
|
451
|
-
// 写成统一形是为了「新开一个出口就绕过改写」这条病形从此在结构上不成立(常驻门:hitl F13-h
|
|
452
|
-
// 钉住 src 下 `flushHeld()` 的调用点恰好一处)。
|
|
453
|
-
const events = [
|
|
454
|
-
...flushHeldWithInterruptRewrite(led, {
|
|
455
|
-
terminal: park.pendingDone,
|
|
456
|
-
signal: ctx.signal,
|
|
457
|
-
// 模 B:`aborted` = 用户在门卡上按了 Esc(该 outcome 在包内的语义就是逐字这一条)。
|
|
458
|
-
gateAbortedByUser: outcome.kind === 'aborted',
|
|
459
|
-
}),
|
|
460
|
-
];
|
|
461
|
-
if (park.pendingDone) {
|
|
462
|
-
events.push(park.pendingDone);
|
|
463
|
-
}
|
|
464
|
-
else if (outcome.kind === 'failed') {
|
|
465
|
-
// 记案(#87 评审② minor,不修):durable re-attach 的第二问(park.pendingDone 无)走 `aborted`
|
|
466
|
-
// 时(仅用户主动 Esc/Ctrl+C 中断触发)不吐终帧——下游正在拆流,合成终帧也没人渲;flushHeld
|
|
467
|
-
// 已把毒化帧诚实吐出。真正的 failed 才合成下面的可见终帧。
|
|
468
|
-
// durable-leg park 无 done 可回吐 —— 合成 failed 让用户看得见为什么停了
|
|
469
|
-
events.push({
|
|
470
|
-
type: 'failed',
|
|
471
|
-
errorCode: 'hitl_unanswered',
|
|
472
|
-
errorMessage: `${park.gate === 'fs' ? 'Tool approval' : 'AskUserQuestion'} gate could not be answered: ${outcome.reason}`,
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
return { kind: 'failsoft', events };
|
|
517
|
+
return { kind: 'failsoft', events: failsoftEvents(park, ctx, outcome.kind === 'aborted', outcome.kind === 'failed' ? outcome.reason : undefined) };
|
|
476
518
|
}
|
|
477
519
|
// decide 成功:丢弃该 call 的毒化 HOLD(续流重放会带 isError:false 的解答帧收口卡片),
|
|
478
520
|
// 并记下真实答案供该解答帧 stamp `structured`(否则卡片渲成结果不可用)。
|
|
@@ -486,7 +528,8 @@ export async function resolvePark(park, ctx) {
|
|
|
486
528
|
// 行给没给出 callId 无关(park 有主角,decide 也成功了)。
|
|
487
529
|
// #357:决断成功 = 判据链上的**真进展**,「已解决」的同因连续计数归零(否则一个 turn 里两只门
|
|
488
530
|
// 各带一次正当重放,第二只的重放会被第一只的计数顶成「第 2 次」而被闸掉)。
|
|
489
|
-
|
|
531
|
+
// L-80:决断成功**不在这里**复位同因计数 —— 「决了」不等于「引擎动了」(reopen 类 re-park / 坐标
|
|
532
|
+
// 失配都是决了又原样 park 回来);复位归驱动侧的进展观察点。
|
|
490
533
|
led.dropHeldForDecidedPark(outcome.gatedCallId, park.gatedCallId);
|
|
491
534
|
if (outcome.gatedCallId) {
|
|
492
535
|
// 🔴 #324:**刻意不把 pending 行的 callId 当成连坐判别的主角身份**。durable `checkpointGate`
|
|
@@ -507,5 +550,79 @@ export async function resolvePark(park, ctx) {
|
|
|
507
550
|
}
|
|
508
551
|
const seq = led.lastSeq();
|
|
509
552
|
hostLog('debug', `liveHitlAskWire: gate decided (call ${outcome.gatedCallId ?? '?'}) — attaching runs.events(${taskId})${seq ? ` from seq ${seq}` : ''}`);
|
|
510
|
-
return { kind: 'reattach' };
|
|
553
|
+
return { kind: 'reattach', progress: true, presented: presentedThisRound, gatedCallId: outcome.gatedCallId };
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* fail-soft 收场的事件序列:扣留帧一律走中断感知出口(件 B:五个排水出口同一形,零-park 硬门必挡,语义等价 no-op;
|
|
557
|
+
* 常驻门 hitl F13-h 钉 src 下 `flushHeld()` 调用点恰一处)+ sync 终帧原样回吐 / durable 腿合成 `hitl_unanswered` 真因终帧
|
|
558
|
+
* (#87 评审② minor 记案:durable 第二问走 `aborted` 时不吐终帧 —— 下游正在拆流,合成了也没人渲)。
|
|
559
|
+
*/
|
|
560
|
+
function failsoftEvents(park, ctx, gateAbortedByUser, failedReason, terminal) {
|
|
561
|
+
const events = [
|
|
562
|
+
...flushHeldWithInterruptRewrite(ctx.led, {
|
|
563
|
+
terminal: park.pendingDone,
|
|
564
|
+
signal: ctx.signal,
|
|
565
|
+
gateAbortedByUser,
|
|
566
|
+
}),
|
|
567
|
+
];
|
|
568
|
+
if (park.pendingDone) {
|
|
569
|
+
events.push(park.pendingDone);
|
|
570
|
+
}
|
|
571
|
+
else if (terminal !== undefined) {
|
|
572
|
+
events.push({ type: 'failed', ...terminal });
|
|
573
|
+
}
|
|
574
|
+
else if (failedReason !== undefined) {
|
|
575
|
+
events.push({
|
|
576
|
+
type: 'failed',
|
|
577
|
+
errorCode: 'hitl_unanswered',
|
|
578
|
+
// 真因 + 出路(L-80:修前的终帧只有一句预算话术,用户拿不到「run 还 parked、卡还能决」这件事)。
|
|
579
|
+
errorMessage: `${park.gate === 'fs' ? 'Tool approval' : 'AskUserQuestion'} gate could not be answered: ${failedReason}` +
|
|
580
|
+
' — the run is still parked on this approval: decide it on the card when it is shown again, or cancel the run.',
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
return events;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* 驱动侧在**预算提交之后**判定触顶时的收场(对抗复审 r4 [high]①:有推进帧的轮次触顶必须等权威身份解析完 ——
|
|
587
|
+
* 先呈 B、决 B,提交后仍是同一只 call 才收场;这一轮已经呈过卡,不再呈收场卡)。
|
|
588
|
+
*/
|
|
589
|
+
export function stalledTerminal(park, ctx, stalledRounds, lastStallReason,
|
|
590
|
+
/** 这一轮的决断有没有落地(r5 [medium]③):落地了就**不许**说「could not be answered」「still parked」—— 那是谎报。 */
|
|
591
|
+
decidedThisRound,
|
|
592
|
+
/** 这一轮决断落在哪只 call(解析到的行身份):收场排水前先把**它**的扣留帧摘掉(r6 [medium]:两只同族 call 都在
|
|
593
|
+
* 扣留、park 帧无可信身份时,决断臂刻意保守不摘,排水会把刚批准的那只当 abort 吐出)。 */
|
|
594
|
+
decidedGatedCallId) {
|
|
595
|
+
const gate = park.gate === 'fs' ? 'Tool approval' : 'AskUserQuestion';
|
|
596
|
+
if (decidedThisRound) {
|
|
597
|
+
// r7 [high]:第二参必须是 **park 帧上的**可信身份(GateLedger 契约),不能拿候选身份自证 —— 无可信身份且同族
|
|
598
|
+
// 不唯一时保守不摘、由排水按中断语义吐出(一帧不丢),与决断臂同一条纪律。
|
|
599
|
+
if (decidedGatedCallId !== undefined)
|
|
600
|
+
ctx.led.dropHeldForDecidedPark(decidedGatedCallId, park.gatedCallId);
|
|
601
|
+
const errorMessage = `${gate} was decided, but the engine parked the same call again after each of the last ${stalledRounds} decisions, so sema ` +
|
|
602
|
+
`stopped re-attaching. Your last decision was accepted; sema did not observe the run move on after it — check the ` +
|
|
603
|
+
`transcript, decide the call again if the card comes back, or cancel the run.`;
|
|
604
|
+
hostLog('debug', `liveHitlAskWire: ${errorMessage}`);
|
|
605
|
+
return { kind: 'failsoft', events: failsoftEvents(park, ctx, false, undefined, { errorCode: 'hitl_stalled', errorMessage }) };
|
|
606
|
+
}
|
|
607
|
+
const reason = `the run stayed parked across ${stalledRounds} consecutive re-attach rounds without any host progress ` +
|
|
608
|
+
`(last: ${lastStallReason ?? 'the engine kept parking'})`;
|
|
609
|
+
hostLog('debug', `liveHitlAskWire: ${reason} — fail-soft after the round committed`);
|
|
610
|
+
return { kind: 'failsoft', events: failsoftEvents(park, ctx, false, reason) };
|
|
611
|
+
}
|
|
612
|
+
/** 非进展 reattach 的用户面告知(deps 口缺席 = 只留 debug;口抛错不许炸续流)。 */
|
|
613
|
+
function notifyParkReattach(ctx, attempt, max, reason) {
|
|
614
|
+
const cb = ctx.deps.onParkReattach;
|
|
615
|
+
if (typeof cb !== 'function')
|
|
616
|
+
return;
|
|
617
|
+
try {
|
|
618
|
+
cb({ attempt, max, reason });
|
|
619
|
+
}
|
|
620
|
+
catch (e) {
|
|
621
|
+
hostLog('debug', `liveHitlAskWire: onParkReattach threw (${String(e)}) — ignored`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/** 传输失败的同因键:只留错误类词(同一类连断才算同因;数字/id 每次都变,不能进键)。 */
|
|
625
|
+
function transportReasonToken(reason) {
|
|
626
|
+
const m = /(ECONNREFUSED|ECONNRESET|ETIMEDOUT|EPIPE|EAI_AGAIN|TimeoutError|AbortError|fetch failed|network)/i.exec(reason);
|
|
627
|
+
return (m?.[1] ?? reason.split(/\s+/).slice(0, 4).join(' ')).toLowerCase();
|
|
511
628
|
}
|
|
@@ -132,6 +132,9 @@ export type FsApprovalOutcome = {
|
|
|
132
132
|
/** 本桥消费的 wire 面(liveHitlAskWire 的 AskGateWireDeps 同形切片,mock 可注入)。 */
|
|
133
133
|
export interface FsApprovalWireDeps {
|
|
134
134
|
client: HitlClientLike;
|
|
135
|
+
/** L-80(对抗复审 r2 [medium]):**这一次调用**真把卡交给卡口时回调一次 —— 每次调用各自的回执,
|
|
136
|
+
* 不是进程级计数(那会把并发会话 A 的呈卡算到 B 头上)。缺席 = 不关心。 */
|
|
137
|
+
onPresented?: () => void;
|
|
135
138
|
}
|
|
136
139
|
/** 结构等值(键序无关深比较)——updatedInput「真编辑过」判定用。zod parse 会产新引用与重排,
|
|
137
140
|
* 引用比较/JSON.stringify 串比较都会假报「编辑过」。 */
|
|
@@ -289,6 +289,7 @@ parkGatedCallId) {
|
|
|
289
289
|
// (run-durable-card-display-keys-test.mjs 的 NOT_PROJECTED 账),上游补位后按 probeCause 的
|
|
290
290
|
// 双源合流形跟批。
|
|
291
291
|
const durableProbeCause = pending.riskDescriptor?.probeCause;
|
|
292
|
+
deps.onPresented?.(); // L-80:真要交给卡口了才算呈过(规则直决 / 取件失败都不走到这一行)
|
|
292
293
|
const card = await surfaceApprovalCard({
|
|
293
294
|
toolName,
|
|
294
295
|
args,
|
package/dist/index.d.ts
CHANGED
|
@@ -173,6 +173,7 @@ export * from './rewindWireCaps.js';
|
|
|
173
173
|
export * from './selfOrchestrationWireCaps.js';
|
|
174
174
|
export * from './skillsWireCaps.js';
|
|
175
175
|
export * from './ultracodeWireCaps.js';
|
|
176
|
+
export * from './selfOrchestrationDenial.js';
|
|
176
177
|
export * from './webSearchWireCaps.js';
|
|
177
178
|
export * from './liveModelCatalog.js';
|
|
178
179
|
export * from './modelBudgetRule.js';
|
package/dist/index.js
CHANGED
|
@@ -199,6 +199,13 @@ export * from './rewindWireCaps.js';
|
|
|
199
199
|
export * from './selfOrchestrationWireCaps.js';
|
|
200
200
|
export * from './skillsWireCaps.js';
|
|
201
201
|
export * from './ultracodeWireCaps.js';
|
|
202
|
+
// ── S-81(server 7.57.0):selfOrchestration **被拒**的三端公共判定 ────────────────────────────
|
|
203
|
+
// 上面两条 stamp 腿(selfOrchestrationWireCaps / ultracodeWireCaps)只管「怎么把意图发出去」;
|
|
204
|
+
// 半配置多租户形态下 server 会把带这两个键的提交 501 拒掉,而「这一发是不是被那两个键拒的 /
|
|
205
|
+
// 要去掉哪两个键 / caps 上的闸怎么读」三处都是判定不是文案 —— 三端各写一遍必然各错一遍
|
|
206
|
+
// (按 status 分诊会把别的 `capability.*` 501 拖进重发臂;手写 delete 必漏掉 `settings.ultracode`
|
|
207
|
+
// 那一处;把 `workflowsGate` 的缺席读成「引擎说不行」= 替一台什么都没说的 server 下断言)。
|
|
208
|
+
export * from './selfOrchestrationDenial.js';
|
|
202
209
|
export * from './webSearchWireCaps.js';
|
|
203
210
|
// 模型面纯逻辑族
|
|
204
211
|
export * from './liveModelCatalog.js';
|