@sema-agent/client-core 0.29.0 → 0.30.1
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 +455 -0
- package/README.md +19 -3
- 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 +285 -48
- package/dist/adapter/activeRunSelfHeal.js +553 -19
- package/dist/adapter/runStream.js +13 -3
- 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 +62 -4
- package/dist/hitl/armedGateRegistry.js +226 -14
- package/dist/hitl/askParkRowRouting.d.ts +150 -0
- package/dist/hitl/askParkRowRouting.js +183 -0
- package/dist/hitl/gateIdentity.d.ts +8 -0
- package/dist/hitl/gateIdentity.js +8 -0
- package/dist/hitl/hitlBridge.d.ts +7 -0
- package/dist/hitl/hitlBridge.js +11 -2
- package/dist/hitl/parkOwnership.d.ts +2 -1
- package/dist/hitl/parkOwnership.js +11 -3
- package/dist/hitl/parkRowBirthWait.d.ts +63 -0
- package/dist/hitl/parkRowBirthWait.js +192 -0
- package/dist/hitl/planReviewWire.d.ts +31 -1
- package/dist/hitl/planReviewWire.js +69 -30
- package/dist/hitl/resumeRunningCard.d.ts +134 -0
- package/dist/hitl/resumeRunningCard.js +177 -0
- package/dist/hitl/toolApprovalWire.d.ts +49 -9
- package/dist/hitl/toolApprovalWire.js +9 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +14 -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,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hitl/parkRowBirthWait.ts — 「park 的待决行**还没出生**」这个第三态的有界等待(#265 上收件,
|
|
3
|
+
* 2026-08-14;源形 = cli `src/sema/parkRowBirthWait.ts`,该文件自设计起就按候上收形写成零依赖)。
|
|
4
|
+
*
|
|
5
|
+
* ── 病(cli #269,plan 批准 → write 卡永不呈现)──────────────────────────────────────────────
|
|
6
|
+
* `/v1/approvals` 的 pending 表里**没有**某个 park 的行,过去被单义化成「这件事已经决了」。
|
|
7
|
+
* 真实世界有第三态:引擎刚被推进到下一只门,而门的行要等一次真实模型调用(实测 ~3s:plan
|
|
8
|
+
* 批准 → 模型算出 Write 的参数 → 才铸审批行)才落库。在那个窗里读到的空表既不是「已决」
|
|
9
|
+
* 也不是「没有这件事」,而是**还没出生**。
|
|
10
|
+
* 把「未出生」当「已决」的代价 = 客户端按成功收口 → 呈现回执永远等不到 → 上一句「没能重开
|
|
11
|
+
* 这张卡」的降级话,而那张卡三秒之后真的出生了却再没人去接 —— 用户看到的是会话死了。
|
|
12
|
+
*
|
|
13
|
+
* ── 这个模块是什么 ──────────────────────────────────────────────────────────────────────────
|
|
14
|
+
* 把「行不在表」拆成**三态**并给一个有界、可中断的重查环:
|
|
15
|
+
* · `row` —— 拿到一行可决断的行(唯一可以往下走的态);
|
|
16
|
+
* · `unborn` —— 此刻没有可决断的行,但**没有任何正面证据**说明这件事已经了结 ⇒ 值得再看一眼;
|
|
17
|
+
* · `settled` —— 有正面证据说明这里没有本臂该决的事(如行在但门种不属本臂)⇒ 等待零意义,立即收口。
|
|
18
|
+
* 判据纪律:`settled` 必须由**正面证据**得出;缺席本身永远只能得 `unborn`
|
|
19
|
+
* (「不知道」不许渲成「已完成」)。
|
|
20
|
+
*
|
|
21
|
+
* ── 边界(承重,别削)────────────────────────────────────────────────────────────────────────
|
|
22
|
+
* · **恒至少探一次**:`budgetMs <= 0` ⇒ 恰一次 probe,行为与「不等」逐字相同(存量判决不漂,
|
|
23
|
+
* 也让测试能把等待窗调成 0 拿回旧语义)。
|
|
24
|
+
* · **有界**:总窗 `budgetMs` 封顶,轮询间隔 `intervalMs`;窗尽如实回 `unborn`(不是回
|
|
25
|
+
* `settled` —— 等不到不等于已决,降级话在这一刻才是真话)。
|
|
26
|
+
* · **真可中断**:每一轮 probe 前、每一次 sleep 后都读 `signal.aborted`;缺省 sleep 自身也吃
|
|
27
|
+
* signal(包内共享叶 `abortableSleep`,不另铸第二份等待原语)。
|
|
28
|
+
* · **绝不裸固定拍当判据**:sleep 只是轮询节奏,判决恒来自下一次 probe 的真实读数。
|
|
29
|
+
* · probe 抛错按 `unborn` 处置(瞬时读面故障不该把有界重查提前判死),错误摘要进 reason ——
|
|
30
|
+
* 调用方只在 debug 档消费,绝不上屏(凭证外溢面纪律)。
|
|
31
|
+
*
|
|
32
|
+
* ── 归层 ────────────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
* 「一条 park 的待决行到底出生了没有」是 wire 语义,三端(cli / desktop / web)都要问同一句;
|
|
34
|
+
* 各端只注入自己的读面(cli = `approvals.list` + 归属判据),环本身共用这一份。行形走结构泛型,
|
|
35
|
+
* 读面/时钟/sleep 全部注入 —— 本文件值级 import 只有包内 `abortableSleep`,零 SDK 零宿主。
|
|
36
|
+
*/
|
|
37
|
+
import { abortableSleep } from '../abortableSleep.js';
|
|
38
|
+
/**
|
|
39
|
+
* 缺省轮询等待:signal 在场时直接借包内共享叶 {@link abortableSleep}(中止即刻 resolve 并清
|
|
40
|
+
* timer),缺席时退化成纯定时。
|
|
41
|
+
*
|
|
42
|
+
* 🔴 **不另铸第二份等待原语**:仓内「怎么安全地睡一觉」只此一份(`abortableSleep.ts` 的头注
|
|
43
|
+
* 写死了这条 —— D 族循环各自保留自己的节拍策略,但等待原语统一)。两份并存的代价不是重复代码,
|
|
44
|
+
* 是两条会各自漂的中止语义。
|
|
45
|
+
* 🔴 timer **不 unref**(与 abortableSleep 同判据):调用方正 await 本环拿判决,轮询窗里这只
|
|
46
|
+
* timer 可能是事件循环里唯一的活,unref 会让进程在等待中途直接退出、判决连同它要说的那句真话
|
|
47
|
+
* 一起蒸发。代价上限 = 一个 intervalMs 的进程存活延长。
|
|
48
|
+
*/
|
|
49
|
+
function parkRowPollDelay(ms, signal) {
|
|
50
|
+
// 无 signal 的调用方(端没接中断口)也走同一个叶:给它一只**永不 abort** 的 signal,而不是就地
|
|
51
|
+
// 再写一个裸 setTimeout —— 那就是第二份等待原语的起点。
|
|
52
|
+
return abortableSleep(ms, signal ?? new AbortController().signal);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 有界、可中断地等一条 park 的待决行出生。语义与边界见文件头;绝不抛。
|
|
56
|
+
*
|
|
57
|
+
* 🔴 判决只有 `row` 一态可以往下走 —— `unborn`(窗尽)与 `aborted` 都必须由调用方翻成**如实的
|
|
58
|
+
* 失败**,绝不许翻成「已决/已成功」(那正是 cli #269 的病)。
|
|
59
|
+
*/
|
|
60
|
+
export async function waitForParkRowBirth(deps) {
|
|
61
|
+
const now = typeof deps.now === 'function' ? deps.now : () => Date.now();
|
|
62
|
+
const sleep = typeof deps.sleep === 'function' ? deps.sleep : parkRowPollDelay;
|
|
63
|
+
const budgetMs = Number.isFinite(deps.budgetMs) && deps.budgetMs > 0 ? deps.budgetMs : 0;
|
|
64
|
+
const intervalMs = Number.isFinite(deps.intervalMs) && deps.intervalMs > 0 ? deps.intervalMs : 1;
|
|
65
|
+
const startedAt = now();
|
|
66
|
+
let probes = 0;
|
|
67
|
+
let lastReason = 'no decidable pending row for this park';
|
|
68
|
+
// 🔴 经函数读,别直接读 `deps.signal?.aborted`:tsc 的控制流分析会拿环顶那一次判断把它窄成
|
|
69
|
+
// `false | undefined`,sleep 之后的第二次判断当场被判「不可能为 true」而报错 —— 而真实世界里
|
|
70
|
+
// 正是 sleep 期间它会翻。函数返回值走声明类型,不参与那条窄化链。
|
|
71
|
+
const aborted = () => deps.signal?.aborted === true;
|
|
72
|
+
/** 一拍探测(读面故障按 unborn,绝不外溢)。 */
|
|
73
|
+
const safeProbe = async (attempt, signal) => {
|
|
74
|
+
try {
|
|
75
|
+
return await deps.probe(attempt, signal);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
// 读面瞬时故障 ≠ 已决:按未出生继续轮询(有界,窗尽自然如实收口)。
|
|
79
|
+
return { kind: 'unborn', reason: `probe threw: ${String(e)}` };
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* 🔴 把**剩余预算**套在每一拍 probe 上:deadline 只在 `await probe` **之后**判,等于什么都没
|
|
84
|
+
* 界住 —— 一次挂死的 `approvals.list` 能把「15s 有界」拖到 SDK 请求超时那么长,而中断也要等
|
|
85
|
+
* 那个 promise 落地才被看见。这里给每一拍配一个内部 controller:窗尽即 abort(读面真被掐,
|
|
86
|
+
* 不只是被放弃)并当拍判窗尽;外部 signal 与它合流,Esc 同样当拍生效。
|
|
87
|
+
* 零窗形(budgetMs<=0)**不设**内部 deadline —— 那一拍就是「修前那一次 list」,不许被新机制改语义。
|
|
88
|
+
*/
|
|
89
|
+
const probeWithinRemaining = async (attempt, remainingMs) => {
|
|
90
|
+
if (budgetMs <= 0)
|
|
91
|
+
return safeProbe(attempt, deps.signal);
|
|
92
|
+
const ctl = new AbortController();
|
|
93
|
+
let timer;
|
|
94
|
+
const deadline = new Promise(resolve => {
|
|
95
|
+
timer = setTimeout(() => {
|
|
96
|
+
ctl.abort();
|
|
97
|
+
resolve('deadline');
|
|
98
|
+
}, Math.max(1, remainingMs));
|
|
99
|
+
});
|
|
100
|
+
// 🔴 外部中断**自己也是竞速的一臂**,不能只靠「signal 传下去 ⇒ 读面自己会拒」:那是对 probe
|
|
101
|
+
// 实现的一个假设,而假设不成立时(probe 吞了 signal / 读面 abort 得慢)Esc 之后这一拍要一直
|
|
102
|
+
// 等到窗尽才被看见 —— 「UI 说停了、后台还在等」正是本模块承诺不会发生的那件事。
|
|
103
|
+
const caller = deps.signal;
|
|
104
|
+
/**
|
|
105
|
+
* 把中止**传导到真正交给 probe 的那只 signal**(清理动作:它自己的异常绝不挡住中止判决)。
|
|
106
|
+
* 🔴 承重理由:`AbortSignal.any` 缺席的宿主上 `merged` 就是 `ctl.signal`,而外部中断只赢竞速、
|
|
107
|
+
* 不会传导到它 —— finally 又把 deadline timer 清了,于是那一拍挂死的读面**永远**不会被 abort
|
|
108
|
+
* (判决说「停了」,后台请求活到进程结束)。两个中止入口(进拍即已中断 / 等待期间翻)共用这一份。
|
|
109
|
+
*/
|
|
110
|
+
const abortProbeLane = () => {
|
|
111
|
+
try {
|
|
112
|
+
ctl.abort();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* abort 面异常绝不挡住中止判决(唯一后果是那一拍的读面少收一次 abort) */
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
let onAbort;
|
|
119
|
+
const externalAbort = new Promise(resolve => {
|
|
120
|
+
if (caller === undefined)
|
|
121
|
+
return; // 永不 settle:race 另有两臂,无害
|
|
122
|
+
if (caller.aborted) {
|
|
123
|
+
// 进这一拍时就已中断 ⇒ probe 拿到的那只 signal 必须是**已中止**的(一个不查 `aborted`、
|
|
124
|
+
// 只挂 'abort' 事件的读面否则会一直挂着)。
|
|
125
|
+
abortProbeLane();
|
|
126
|
+
resolve('aborted');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// 🔴 先传导中止**再** resolve(见 {@link abortProbeLane}):否则「可中断」只是判决面的说法。
|
|
130
|
+
onAbort = () => {
|
|
131
|
+
abortProbeLane();
|
|
132
|
+
resolve('aborted');
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
caller.addEventListener('abort', onAbort, { once: true });
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
onAbort = undefined; // 没有事件面的 signal 形:退化成环顶那次 aborted() 读
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
// 合流(Node ≥20.3 / 现代浏览器原生);缺席时退化成只吃内部 deadline —— 中止仍由上面那一臂
|
|
142
|
+
// 当拍生效。`typeof` 探的是**运行期**在场,不是类型面缺口。
|
|
143
|
+
const merged = caller !== undefined && typeof AbortSignal.any === 'function' ? AbortSignal.any([caller, ctl.signal]) : ctl.signal;
|
|
144
|
+
try {
|
|
145
|
+
return await Promise.race([safeProbe(attempt, merged), deadline, externalAbort]);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
// timer 必清:竞速由 probe 赢时它还挂着,不清会把进程按 remainingMs 拖活一拍。
|
|
149
|
+
if (timer !== undefined)
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
// 监听器必摘:环可以跑几十拍,不摘就是几十个挂在调用方 signal 上的闭包。
|
|
152
|
+
if (onAbort !== undefined && caller !== undefined) {
|
|
153
|
+
try {
|
|
154
|
+
caller.removeEventListener('abort', onAbort);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* 监听面异常绝不反噬等待环 */
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
for (;;) {
|
|
163
|
+
if (aborted())
|
|
164
|
+
return { kind: 'aborted', waitedMs: now() - startedAt, probes };
|
|
165
|
+
probes += 1;
|
|
166
|
+
const outcome = await probeWithinRemaining(probes, budgetMs - (now() - startedAt));
|
|
167
|
+
// 读面挂着的时候用户按了 Esc ⇒ 当拍停(判决优先于窗尽,两者都成立时中断更准确)。
|
|
168
|
+
if (outcome === 'aborted' || aborted())
|
|
169
|
+
return { kind: 'aborted', waitedMs: now() - startedAt, probes };
|
|
170
|
+
if (outcome === 'deadline') {
|
|
171
|
+
return {
|
|
172
|
+
kind: 'unborn',
|
|
173
|
+
reason: `${lastReason} (the last read face did not answer within the remaining window)`,
|
|
174
|
+
waitedMs: now() - startedAt,
|
|
175
|
+
probes,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const probed = outcome;
|
|
179
|
+
if (probed.kind === 'row')
|
|
180
|
+
return { kind: 'row', row: probed.row, waitedMs: now() - startedAt, probes };
|
|
181
|
+
if (probed.kind === 'settled') {
|
|
182
|
+
return { kind: 'settled', reason: probed.reason, waitedMs: now() - startedAt, probes };
|
|
183
|
+
}
|
|
184
|
+
lastReason = probed.reason;
|
|
185
|
+
const remainingMs = budgetMs - (now() - startedAt);
|
|
186
|
+
if (remainingMs <= 0)
|
|
187
|
+
return { kind: 'unborn', reason: lastReason, waitedMs: now() - startedAt, probes };
|
|
188
|
+
await sleep(Math.min(intervalMs, remainingMs), deps.signal);
|
|
189
|
+
if (aborted())
|
|
190
|
+
return { kind: 'aborted', waitedMs: now() - startedAt, probes };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -54,6 +54,19 @@ export declare const PLAN_REVIEW_REJECT_LABEL = "No, reject it (keep planning)";
|
|
|
54
54
|
* 负责如实分类,不替调用方做选择。
|
|
55
55
|
*/
|
|
56
56
|
export declare function planReviewDecisionFromAnswer(answer: QuestionAnswer | null | undefined): 'approve' | 'reject' | 'dismissed';
|
|
57
|
+
/**
|
|
58
|
+
* 推代的**门控口**(A-024.4;#244 F1 上收,语义照壳现实现):只有**决断性**作答(approve/
|
|
59
|
+
* reject 标签命中 —— {@link planReviewDecisionFromAnswer} 的官方三值分类)才消费当代 plan 门。
|
|
60
|
+
* Esc/空答对原卡是 dismissal(arm 臂的 dismissed 分支不驱动 decidePlanReview,checkpoint 仍
|
|
61
|
+
* pending)—— 那时推代会把「被关过」翻成「首见」,恰是 A-024.4 在最常见路径(Esc 关卡)上要
|
|
62
|
+
* 保住的区分。消费方 = 宿主 overlay 的 answerAndRelease(respond 成功后调 —— 它是 responder
|
|
63
|
+
* 之外**第二条**决断可见点,原卡与重开卡两条通路对同一张卡各报一次,`notePlanReviewAnswered`
|
|
64
|
+
* 按完整 questionId 去重恰推一代);包内 responder 走 {@link notePlanReviewAnswered} 直报
|
|
65
|
+
* (它们没有 dismissed 臂会到达记账行)。
|
|
66
|
+
*/
|
|
67
|
+
export declare function notePlanReviewAnsweredIfDecisive(questionId: unknown, answer: QuestionAnswer | null | undefined): void;
|
|
68
|
+
/** W1 带 key 变体。 */
|
|
69
|
+
export declare function notePlanReviewAnsweredIfDecisiveFor(sessionKey: string, questionId: unknown, answer: QuestionAnswer | null | undefined): void;
|
|
57
70
|
/** done 帧的 plan_review park 形状(结构性读,别的终态一律 false)。 */
|
|
58
71
|
export declare function isPlanReviewPark(result: unknown): result is {
|
|
59
72
|
taskId: string;
|
|
@@ -108,6 +121,17 @@ export interface ReopenPlanReviewOpts {
|
|
|
108
121
|
* 或候 armPlanReviewApproval 补 sessionKey 位。
|
|
109
122
|
*/
|
|
110
123
|
sessionKey?: string;
|
|
124
|
+
/**
|
|
125
|
+
* **呈现回执模式**(#250 件1 语义;#244 F1 换装位,cli 装配用):在场 ⇒ 本函数转异步 ——
|
|
126
|
+
* publish 之后等宿主呈现面的登记事件(`waitForGateArmed`,键 = 本次发布的**原始帧 id**,
|
|
127
|
+
* 尝试级回执)最多这么多毫秒;回执到手才 `{reopened:true}`,窗尽如实 `{reopened:false}`。
|
|
128
|
+
* 🔴 本模式下重开腿**零登记**:登记时点 = 宿主 overlay 真入队点(`registerArmedGate
|
|
129
|
+
* FromQuestionId`),零回执还登记 = 下一次重开谎称复见(毒化 firstSight)。responder 在
|
|
130
|
+
* 窗尽后**故意保留**:hook 窗超过看门狗才入队的迟到卡必须仍可答(答不了的死卡比迟到更坏)。
|
|
131
|
+
* 缺席 ⇒ 同步路径逐字不变(0.30.0 行为:发布即登记、即回 `{reopened:true}`,像素级回执归
|
|
132
|
+
* 宿主经 `presented` 位补充)。
|
|
133
|
+
*/
|
|
134
|
+
presentationReceiptMs?: number;
|
|
111
135
|
}
|
|
112
136
|
/** 测试钩:清空重开 responder 台账(跨用例状态)。 */
|
|
113
137
|
export declare function _resetActiveReopenRespondersForTest(): void;
|
|
@@ -123,4 +147,10 @@ export declare function _resetActiveReopenRespondersForTest(): void;
|
|
|
123
147
|
* firstSight 判据 = 呈现台账(`armedGateRegistry`,键 = canonical `plan-review:<taskId>`);
|
|
124
148
|
* 先查后记 —— 顺序决定判决正确性。
|
|
125
149
|
*/
|
|
126
|
-
export declare function reopenPlanReviewCard(taskId: string, opts
|
|
150
|
+
export declare function reopenPlanReviewCard(taskId: string, opts: ReopenPlanReviewOpts & {
|
|
151
|
+
presentationReceiptMs: number;
|
|
152
|
+
}): Promise<ReopenCardVerdict>;
|
|
153
|
+
export declare function reopenPlanReviewCard(taskId: string, opts?: ReopenPlanReviewOpts & {
|
|
154
|
+
presentationReceiptMs?: undefined;
|
|
155
|
+
}): ReopenCardVerdict;
|
|
156
|
+
export declare function reopenPlanReviewCard(taskId: string, opts?: ReopenPlanReviewOpts): ReopenCardVerdict | Promise<ReopenCardVerdict>;
|
|
@@ -42,7 +42,7 @@ import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
|
42
42
|
import { engineWireTarget } from '../engineWireTarget.js';
|
|
43
43
|
import { enqueuePlanReviewOutcome } from '../notifications.js';
|
|
44
44
|
import { planReviewQuestionId, REOPEN_ID_TAIL } from './gateIdentity.js';
|
|
45
|
-
import {
|
|
45
|
+
import { notePlanReviewAnswered, notePlanReviewAnsweredFor, planReviewArmedKeyFor, registerArmedGateFor, waitForGateArmedFor, wasGateArmedFor, } from './armedGateRegistry.js';
|
|
46
46
|
import { DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
47
47
|
/** resume 是 SYNC 驱动到终态的(可能分钟级)——原 raw fetch 无超时;SDK Transport 恒有每请求
|
|
48
48
|
* 超时,取 6h 宽上界(诚实预算:超过它基本是引擎僵死,超时文案与网络失败同走 outcome 通知)。 */
|
|
@@ -72,6 +72,25 @@ export function planReviewDecisionFromAnswer(answer) {
|
|
|
72
72
|
return 'reject';
|
|
73
73
|
return 'dismissed';
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* 推代的**门控口**(A-024.4;#244 F1 上收,语义照壳现实现):只有**决断性**作答(approve/
|
|
77
|
+
* reject 标签命中 —— {@link planReviewDecisionFromAnswer} 的官方三值分类)才消费当代 plan 门。
|
|
78
|
+
* Esc/空答对原卡是 dismissal(arm 臂的 dismissed 分支不驱动 decidePlanReview,checkpoint 仍
|
|
79
|
+
* pending)—— 那时推代会把「被关过」翻成「首见」,恰是 A-024.4 在最常见路径(Esc 关卡)上要
|
|
80
|
+
* 保住的区分。消费方 = 宿主 overlay 的 answerAndRelease(respond 成功后调 —— 它是 responder
|
|
81
|
+
* 之外**第二条**决断可见点,原卡与重开卡两条通路对同一张卡各报一次,`notePlanReviewAnswered`
|
|
82
|
+
* 按完整 questionId 去重恰推一代);包内 responder 走 {@link notePlanReviewAnswered} 直报
|
|
83
|
+
* (它们没有 dismissed 臂会到达记账行)。
|
|
84
|
+
*/
|
|
85
|
+
export function notePlanReviewAnsweredIfDecisive(questionId, answer) {
|
|
86
|
+
notePlanReviewAnsweredIfDecisiveFor(DEFAULT_SESSION_KEY, questionId, answer);
|
|
87
|
+
}
|
|
88
|
+
/** W1 带 key 变体。 */
|
|
89
|
+
export function notePlanReviewAnsweredIfDecisiveFor(sessionKey, questionId, answer) {
|
|
90
|
+
if (planReviewDecisionFromAnswer(answer ?? { answers: [] }) === 'dismissed')
|
|
91
|
+
return;
|
|
92
|
+
notePlanReviewAnsweredFor(sessionKey, questionId);
|
|
93
|
+
}
|
|
75
94
|
/** done 帧的 plan_review park 形状(结构性读,别的终态一律 false)。 */
|
|
76
95
|
export function isPlanReviewPark(result) {
|
|
77
96
|
const r = result;
|
|
@@ -182,12 +201,11 @@ export function armPlanReviewApproval(result) {
|
|
|
182
201
|
// 卡的重开路径把决定权还给用户。呈现台账的键**不清**:同一张卡再被重开就是真「reopened」。
|
|
183
202
|
return { ok: true };
|
|
184
203
|
}
|
|
185
|
-
// A-024.4(
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
clearArmedGate(planReviewQuestionId(taskId));
|
|
204
|
+
// A-024.4(#244 F1 起走分代):决断一经递交,这个门的呈现史就消费掉(当代键清 + 推代);
|
|
205
|
+
// 同 run 的**下一个** plan gate(approve 推进后引擎可再 park 一个新 plan)必须读回首见,
|
|
206
|
+
// 否则文案对一张从未呈现过的新卡说「reopened」。递交后决定未生效的形(RB-471 族)读回
|
|
207
|
+
// 首见只损失「reopened」一词 —— 首见文案零历史断言,诚实方向安全。
|
|
208
|
+
notePlanReviewAnswered(questionId);
|
|
191
209
|
// fire-and-forget:resume 同步驱动到终态可能分钟级,不能挂住 overlay;结果经 queue 通知回来
|
|
192
210
|
void decidePlanReview(taskId, decided);
|
|
193
211
|
return { ok: true };
|
|
@@ -340,31 +358,28 @@ function retireActiveReopen(sessionKey, canonicalId) {
|
|
|
340
358
|
export function _resetActiveReopenRespondersForTest() {
|
|
341
359
|
activeReopenResponders.clear();
|
|
342
360
|
}
|
|
343
|
-
/**
|
|
344
|
-
* 重开某个 parked plan_review 的审批卡(409 自愈分诊树 `attemptActiveRunSelfHeal` 的
|
|
345
|
-
* `reopenPlanReview` 注入口的包内生产实现)。判决形/成文语义见 {@link ReopenCardVerdict}
|
|
346
|
-
* (`adapter/activeRunSelfHeal.ts`);本函数不产 `presented` 位 —— 包看不到像素,回执机制归
|
|
347
|
-
* 宿主端包装。绝不抛(turn 收尾路径)。
|
|
348
|
-
*
|
|
349
|
-
* 与 arm 臂的合成关系(A-028.4):首呈(done 帧)= `armPlanReviewApproval`;重开(409 撞锁)=
|
|
350
|
-
* 本函数。两臂共用同一份题面构造点/标签单源/决断三态判决与同一条 `decidePlanReview` 投递管道;
|
|
351
|
-
* 「铸新 questionId 还是复用同 id」由宿主去重语义作参数(见 {@link ReopenPlanReviewOpts})。
|
|
352
|
-
* firstSight 判据 = 呈现台账(`armedGateRegistry`,键 = canonical `plan-review:<taskId>`);
|
|
353
|
-
* 先查后记 —— 顺序决定判决正确性。
|
|
354
|
-
*/
|
|
355
361
|
export function reopenPlanReviewCard(taskId, opts) {
|
|
362
|
+
const receiptMs = opts?.presentationReceiptMs;
|
|
363
|
+
/** 回执模式下发布之后的收口(订阅必须**先于** publish:总线 handler 同步跑,快 overlay 的
|
|
364
|
+
* 登记可能发生在 publish 返回之前 —— 后订阅会错过已发生的回执)。 */
|
|
365
|
+
const settleOnReceipt = (receipt, firstSight) => receipt.then((presented) => (presented ? { reopened: true, firstSight } : { reopened: false }), () => ({ reopened: false }));
|
|
356
366
|
try {
|
|
357
|
-
if (typeof taskId !== 'string' || taskId.length === 0)
|
|
358
|
-
return { reopened: false };
|
|
367
|
+
if (typeof taskId !== 'string' || taskId.length === 0) {
|
|
368
|
+
return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
|
|
369
|
+
}
|
|
359
370
|
const sessionKey = opts?.sessionKey ?? DEFAULT_SESSION_KEY;
|
|
360
371
|
// 诚实边界:请求方会话没有 overlay 订阅者(headless / 尚未挂载)⇒ 返回 false,调用方如实说
|
|
361
372
|
// 「没能重开」。overlay 在场答的是「有没有一个 UI 会收到这一帧」;检查按 sessionKey 走 ——
|
|
362
373
|
// 别的会话挂着 overlay 不等于本会话的卡有地方渲(复审 [high]:默认键硬走会把帧投进别人的面)。
|
|
363
|
-
if (!hasQuestionOverlayFor(sessionKey))
|
|
364
|
-
return { reopened: false };
|
|
374
|
+
if (!hasQuestionOverlayFor(sessionKey)) {
|
|
375
|
+
return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
|
|
376
|
+
}
|
|
365
377
|
const canonicalId = planReviewQuestionId(taskId);
|
|
366
|
-
// 先查(firstSight)后记
|
|
367
|
-
|
|
378
|
+
// 先查(firstSight)后记 —— 顺序决定判决正确性;台账按 sessionKey 分账,键 = **当代** plan 键
|
|
379
|
+
// (A-024.4 分代:0 代与 canonical 字节同形,决断消费后落新代 —— 恒定键会对同 run 的下一只
|
|
380
|
+
// plan 门谎报「被关过」)。
|
|
381
|
+
const armedKey = planReviewArmedKeyFor(sessionKey, taskId);
|
|
382
|
+
const firstSight = !wasGateArmedFor(sessionKey, armedKey);
|
|
368
383
|
const mintFresh = opts?.mintFreshQuestionId !== false;
|
|
369
384
|
if (!mintFresh && hasLocalQuestionResponder(canonicalId)) {
|
|
370
385
|
// 重呈短路(与 arm 臂的重放短路同形):同一个 canonical 身份、同一份题面、同一个 responder。
|
|
@@ -376,12 +391,21 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
376
391
|
}
|
|
377
392
|
// 先退役本会话名下还在场的**重开**卡(如此前走过 mintFresh 臂)—— 同一 gate 不留两张活卡。
|
|
378
393
|
retireActiveReopen(sessionKey, canonicalId);
|
|
394
|
+
if (receiptMs !== undefined) {
|
|
395
|
+
const receipt = waitForGateArmedFor(sessionKey, [canonicalId], receiptMs);
|
|
396
|
+
publishQuestionFrameFor(sessionKey, {
|
|
397
|
+
type: 'question',
|
|
398
|
+
questionId: canonicalId,
|
|
399
|
+
questions: planReviewQuestions(ARM_QUESTION),
|
|
400
|
+
});
|
|
401
|
+
return settleOnReceipt(receipt, firstSight);
|
|
402
|
+
}
|
|
379
403
|
publishQuestionFrameFor(sessionKey, {
|
|
380
404
|
type: 'question',
|
|
381
405
|
questionId: canonicalId,
|
|
382
406
|
questions: planReviewQuestions(ARM_QUESTION),
|
|
383
407
|
});
|
|
384
|
-
registerArmedGateFor(sessionKey,
|
|
408
|
+
registerArmedGateFor(sessionKey, armedKey);
|
|
385
409
|
return { reopened: true, firstSight };
|
|
386
410
|
}
|
|
387
411
|
// 复审 [high] 单活纪律(两轮收紧):铸新卡前退役**全部**旧作答入口 —— 同一个 gate 至多一张
|
|
@@ -417,8 +441,10 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
417
441
|
// 这张卡再被重开就是真「reopened」。
|
|
418
442
|
return { ok: true };
|
|
419
443
|
}
|
|
420
|
-
// A-024.4:决断递交即消费呈现史(与 arm 臂 responder
|
|
421
|
-
|
|
444
|
+
// A-024.4(#244 F1 起走分代):决断递交即消费呈现史(与 arm 臂 responder 同点;overlay 的
|
|
445
|
+
// answerAndRelease 对同一张卡也会经 notePlanReviewAnsweredIfDecisive 报一次,按完整
|
|
446
|
+
// questionId 去重,不双推)。
|
|
447
|
+
notePlanReviewAnsweredFor(sessionKey, questionId);
|
|
422
448
|
try {
|
|
423
449
|
await deliver(taskId, decided);
|
|
424
450
|
}
|
|
@@ -430,7 +456,19 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
430
456
|
return { ok: true };
|
|
431
457
|
});
|
|
432
458
|
activeReopenResponders.set(activeKey, { questionId, unregister });
|
|
433
|
-
|
|
459
|
+
if (receiptMs !== undefined) {
|
|
460
|
+
// 回执模式(#250 件1):零登记(登记时点 = 宿主 overlay 真入队点)、订阅先于 publish、
|
|
461
|
+
// 回执键 = 原始帧 id(尝试级回执 —— 同 gate 并发重开不同键互唤);窗尽如实 false,
|
|
462
|
+
// responder 故意保留(迟到卡必须仍可答,决断腿照走)。
|
|
463
|
+
const receipt = waitForGateArmedFor(sessionKey, [questionId], receiptMs);
|
|
464
|
+
publishQuestionFrameFor(sessionKey, {
|
|
465
|
+
type: 'question',
|
|
466
|
+
questionId,
|
|
467
|
+
questions: planReviewQuestions(REOPEN_QUESTION),
|
|
468
|
+
});
|
|
469
|
+
return settleOnReceipt(receipt, firstSight);
|
|
470
|
+
}
|
|
471
|
+
registerArmedGateFor(sessionKey, armedKey);
|
|
434
472
|
publishQuestionFrameFor(sessionKey, {
|
|
435
473
|
type: 'question',
|
|
436
474
|
questionId,
|
|
@@ -439,6 +477,7 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
439
477
|
return { reopened: true, firstSight };
|
|
440
478
|
}
|
|
441
479
|
catch {
|
|
442
|
-
|
|
480
|
+
// 重开失败绝不炸 turn —— 调用方会如实说「没能重开」(回执模式回同形的 resolved promise)。
|
|
481
|
+
return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
|
|
443
482
|
}
|
|
444
483
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hitl/resumeRunningCard.ts — **resume 撞上「上一轮还在跑」**时那张三选卡的判定 / 选项集 / 文案
|
|
3
|
+
* (#265 上收件,2026-08-14;源形 = cli `src/sema/resumeRunningArm.ts` 的纯判定半场,编排腿留在端上)。
|
|
4
|
+
*
|
|
5
|
+
* ── 病与改形 ────────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
* resume 时读到一条 `running` 的 run,旧形是**客户端自己**给它打一枪 cancel(动机 = 僵尸烧 token)。
|
|
7
|
+
* 矛盾:resume 时的 running run **不可判**是「用户要的在飞工作」还是「僵尸」—— 而实况里自动 cancel
|
|
8
|
+
* 恰好是错的方向(用户批准之后 run 该继续跑,resume 时 cancel = 把刚救活的 run 又杀了)。血统纪律:
|
|
9
|
+
* **破坏性动作恒不自动**。改形 = 把三条路交回用户:
|
|
10
|
+
* ① `Attach and watch` —— 接管为当前 turn 输出;
|
|
11
|
+
* ② `Cancel it` —— 那个动作降级为**用户按下**的选项;
|
|
12
|
+
* ③ `Leave it running in the background`(**默认焦点**)—— 零动作。
|
|
13
|
+
*
|
|
14
|
+
* ── §3.2a 活性证据的缺席纪律(不可削)──────────────────────────────────────────────────────
|
|
15
|
+
* 判活体的 `msSinceLastActivity`(相对毫秒)在场 ⇒ 卡上渲一行「last activity Ns ago」;
|
|
16
|
+
* **缺席 = 无法证明**(跨副本命中 / 副本重启后),不是「不活」—— 那一行**整行不渲**,绝不折成 0、
|
|
17
|
+
* 绝不写成「已死」。
|
|
18
|
+
*
|
|
19
|
+
* ── §3.2b 无 UI 臂:退役自动 cancel 之后不许留死墙 ──────────────────────────────────────────
|
|
20
|
+
* 「不自动 cancel + 没有卡可选」= 又一堵死墙。所以没有卡的形必须**显式定义臂**:恒不 cancel;
|
|
21
|
+
* 输出**机读三选指路**({@link resumeRunningNoUiGuidance},attach / cancel / 后台留着三条路各给
|
|
22
|
+
* 真实命令或 API 路径);headless 车道走**专码**退出({@link RESUME_RUNNING_NO_UI_EXIT_CODE});
|
|
23
|
+
* 绝不静默挂起、绝不谎报成功。
|
|
24
|
+
*
|
|
25
|
+
* ── 🔴 塌缩纪律 ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
* Esc / 空答 / 认不出的自由文本 / 作答看门狗到点 / 呈卡失败 —— 一律收口成 ③(零动作)。
|
|
27
|
+
* 破坏性动作只认**逐字命中**的显式选择({@link resumeChoiceFromLabels})。
|
|
28
|
+
*
|
|
29
|
+
* ── 🔴 假 affordance 禁令(按供给渲)───────────────────────────────────────────────────────
|
|
30
|
+
* 一个按了没用的选项比没有这个选项更坏:durable `runs.events` 动词缺席**或**没有接管消费口 ⇒
|
|
31
|
+
* ① 整个不渲;`runs.cancel` 缺席 ⇒ ② 整个不渲;两条都缺 ⇒ 一张卡都不铸,走 §3.2b 臂。
|
|
32
|
+
*
|
|
33
|
+
* ── 归层 ────────────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
* 本文件 = 纯判定 + 选项集 + 文案,**零 import**。呈卡口/动词切片/判活口/指路 sink/退出码应用口
|
|
35
|
+
* 全部由端在装配点注入;端只剩装配与呈现。
|
|
36
|
+
*/
|
|
37
|
+
/** ① 接管观看。 */
|
|
38
|
+
export declare const RESUME_CHOICE_ATTACH_LABEL = "Attach and watch";
|
|
39
|
+
/** ② 用户显式取消(自动 cancel 那个动作的降级形)。 */
|
|
40
|
+
export declare const RESUME_CHOICE_CANCEL_LABEL = "Cancel it";
|
|
41
|
+
/** ③ 后台留着 —— 🔴 **默认焦点**,所以它必须排在 options[0](见 {@link resumeRunningOptions})。 */
|
|
42
|
+
export declare const RESUME_CHOICE_BACKGROUND_LABEL = "Leave it running in the background";
|
|
43
|
+
/** 卡的 header(答案按 header 归键;本卡只有一问)。与 409 三选卡的入口刻意不同词 —— 同一张卡的
|
|
44
|
+
* 两个入口,题面说的不是一件事。 */
|
|
45
|
+
export declare const RESUME_CHOICE_HEADER = "Earlier run still in flight";
|
|
46
|
+
/** 用户在 resume 变体卡上的决定。 */
|
|
47
|
+
export type ResumeRunningChoice = 'attach' | 'cancel' | 'background';
|
|
48
|
+
/**
|
|
49
|
+
* 呈卡口的回执。🔴 `'unavailable'` 与 `'background'` **绝不合并**:前者 =「问都没问成」
|
|
50
|
+
* (§3.2b 臂的入口:用户还欠一次告知),后者 =「问过了,用户选了什么都不做」(已经告知过)。
|
|
51
|
+
* 合并的代价 = 一次呈现失败被静默当成用户的决定 —— 那正是 §3.2b 要封的死墙。
|
|
52
|
+
*/
|
|
53
|
+
export type ResumeChoiceOutcome = ResumeRunningChoice | 'unavailable';
|
|
54
|
+
/** 铸卡所需的全部材料(纯数据 —— 呈现层按它铸自己的卡/帧)。 */
|
|
55
|
+
export interface ResumeRunningCardInput {
|
|
56
|
+
/** 那条还在跑的 run。 */
|
|
57
|
+
taskId: string;
|
|
58
|
+
/** 引擎报的状态(如实转述,不加工)。 */
|
|
59
|
+
status: string;
|
|
60
|
+
/** ① 可渲吗(= durable events 动词在场 **且** 有接管消费口)。 */
|
|
61
|
+
canAttach: boolean;
|
|
62
|
+
/** ② 可渲吗(= `runs.cancel` 在场)。 */
|
|
63
|
+
canCancel: boolean;
|
|
64
|
+
/** §3.2a 活性证据(null = 引擎没给 ⇒ 无法证明 ⇒ 那一行整行不渲)。 */
|
|
65
|
+
msSinceLastActivity: number | null;
|
|
66
|
+
}
|
|
67
|
+
/** 一个选项(CC AskUserQuestionTool option 形)。 */
|
|
68
|
+
export interface ResumeRunningCardOption {
|
|
69
|
+
label: string;
|
|
70
|
+
description: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* §3.2a 活性证据行。**缺席 ⇒ null(整行不渲)**;绝不渲 0、绝不渲「已死」。
|
|
74
|
+
* 负数 / 非有限值同样按缺席处理(wire 位 UNTRUSTED,类型说是 number 也要真判)。
|
|
75
|
+
*/
|
|
76
|
+
export declare function resumeRunningLivenessRow(msSinceLastActivity: number | null | undefined): string | null;
|
|
77
|
+
/**
|
|
78
|
+
* 卡的题面。第二行 = 活性证据(§3.2a 缺席时**整行消失**,不留占位、不留「unknown」字样 ——
|
|
79
|
+
* 一行「last activity: unknown」和一行 0 一样是在替引擎回答一个它没回答的问题)。
|
|
80
|
+
*/
|
|
81
|
+
export declare function resumeRunningQuestionText(i: ResumeRunningCardInput): string;
|
|
82
|
+
/**
|
|
83
|
+
* 选项集(同一张卡两个入口,选项集**按入口分形**)。
|
|
84
|
+
*
|
|
85
|
+
* 🔴 **顺序即默认焦点**:卡的渲染面 `focusedIndex` 初值恒 0,而 wire 的 `QuestionFrame` 里
|
|
86
|
+
* **没有**任何「默认焦点」位可传。所以「默认焦点 = ③ 后台留着」在结构上只有一个落法:把 ③ 排在
|
|
87
|
+
* options[0]。谁哪天为了「读起来顺」把 ①②③ 按序号排回去,默认焦点就漂到了 `Attach and watch`
|
|
88
|
+
* —— 一个手滑回车就把一条后台 run 接管到前台。常驻门逐位反钉这个顺序。
|
|
89
|
+
*/
|
|
90
|
+
export declare function resumeRunningOptions(i: ResumeRunningCardInput): ResumeRunningCardOption[];
|
|
91
|
+
/**
|
|
92
|
+
* 答案 → 决定。🔴 只认**逐字命中**的标签;空答(overlay 的 Esc/dismiss 形)、多选、认不出的
|
|
93
|
+
* 自由文本一律回落 ③ —— 破坏性动作只认显式选择(与 409 三选卡同律)。
|
|
94
|
+
*/
|
|
95
|
+
export declare function resumeChoiceFromLabels(selected: readonly string[] | null | undefined): ResumeRunningChoice;
|
|
96
|
+
/**
|
|
97
|
+
* headless 车道撞 resume-running 的**专码**退出码。
|
|
98
|
+
*
|
|
99
|
+
* 为什么非 0:这一趟 resume **没有**把会话交到一个可用状态 —— 那条 run 还占着会话,脚本接下来
|
|
100
|
+
* 提交什么都会撞 409。静默 0 = 谎报成功。
|
|
101
|
+
* 为什么是 75:sysexits.h 的 `EX_TEMPFAIL`(「暂时性失败,请稍后重试」)语义正对 —— run 跑完
|
|
102
|
+
* 或被取消之后同一条命令就能过;与 cli 既用的 0/1/2/129/143 零撞(脚本消费者据此可以把「会话被
|
|
103
|
+
* 在飞 run 占住」与「真失败(1)」「用法错(2)」分臂)。
|
|
104
|
+
*/
|
|
105
|
+
export declare const RESUME_RUNNING_NO_UI_EXIT_CODE = 75;
|
|
106
|
+
/** 机读指路的行首标识 —— 脚本按它 grep;UI 改版不许动它。 */
|
|
107
|
+
export declare const RESUME_RUNNING_GUIDANCE_TAG = "sema: resume-running:";
|
|
108
|
+
/**
|
|
109
|
+
* §3.2b 的**机读三选指路**。三条路各给真实命令或 API 路径,一条都不许省 ——
|
|
110
|
+
* 少写一条 = 那条路在这个车道上被客户端单方面关掉了。
|
|
111
|
+
*
|
|
112
|
+
* 🔴 `attach=` 那一行只有在**真知道 session id** 时才给具体命令:不知道还硬拼一条
|
|
113
|
+
* `sema --resume <sessionId>` 字面量,就是印一条按了没用的命令(假 affordance 的文本形)。
|
|
114
|
+
*/
|
|
115
|
+
export declare function resumeRunningNoUiGuidance(i: {
|
|
116
|
+
taskId: string;
|
|
117
|
+
sessionId?: string | null;
|
|
118
|
+
}): string;
|
|
119
|
+
/**
|
|
120
|
+
* ① 接管之前的**显式披露**。
|
|
121
|
+
*
|
|
122
|
+
* resume 入口没有 `lastEventId` 可用(端今天不持久化引擎事件锚),所以 durable 尾从这条 run 的
|
|
123
|
+
* 第一帧读起 —— 崩溃前已落盘、已被 resume 还原到屏上的那一段会再渲一遍。续听腿那道「无锚 × 已上屏
|
|
124
|
+
* ⇒ 不许从头续读」的门在这一格判不出来(它数的是**本条流**吐过几帧,resume 入口恒 0),所以不能
|
|
125
|
+
* 拿它当安全证明。处置 = 说在前面:静默复读会让用户以为模型把同一段活干了两遍。
|
|
126
|
+
*
|
|
127
|
+
* 🔴 **账目面同源**:同一次重放也会把这条 run 已经执行过的每一个 `turn_end` 再喂一次成本累计器。
|
|
128
|
+
* 客户端这一端证明不了哪一枚已经在恢复出来的总额里(那需要一本按 turn 落盘的账 —— 与缺席的
|
|
129
|
+
* 事件锚是同一件移交残件),所以接管腿声明**不计账**。既然不计,就必须在**同一行**说清楚:
|
|
130
|
+
* 只披露视觉复读、不披露账目缺口,用户拿成本面跟引擎账本对不上时只会怀疑客户端算错了。
|
|
131
|
+
*/
|
|
132
|
+
export declare function resumeAttachReplayDisclosure(taskId: string): string;
|
|
133
|
+
/** ② 打完 cancel 但引擎没确认落地时的如实一行(绝不谎报「已停止」)。 */
|
|
134
|
+
export declare function resumeRunningCancelUnconfirmedRow(taskId: string): string;
|