@sema-agent/client-core 0.29.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 -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 +264 -48
- package/dist/adapter/activeRunSelfHeal.js +501 -17
- 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/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/parkRowBirthWait.d.ts +63 -0
- package/dist/hitl/parkRowBirthWait.js +192 -0
- package/dist/hitl/resumeRunningCard.d.ts +134 -0
- package/dist/hitl/resumeRunningCard.js +177 -0
- package/dist/hitl/toolApprovalWire.d.ts +46 -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,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>>;
|
|
@@ -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
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
// ── 逐字契约面(答案按 label 原样回声 —— 引擎的 `selected ⊆ options` 围栏,漂一个字节即失灵)──
|
|
38
|
+
/** ① 接管观看。 */
|
|
39
|
+
export const RESUME_CHOICE_ATTACH_LABEL = 'Attach and watch';
|
|
40
|
+
/** ② 用户显式取消(自动 cancel 那个动作的降级形)。 */
|
|
41
|
+
export const RESUME_CHOICE_CANCEL_LABEL = 'Cancel it';
|
|
42
|
+
/** ③ 后台留着 —— 🔴 **默认焦点**,所以它必须排在 options[0](见 {@link resumeRunningOptions})。 */
|
|
43
|
+
export const RESUME_CHOICE_BACKGROUND_LABEL = 'Leave it running in the background';
|
|
44
|
+
/** 卡的 header(答案按 header 归键;本卡只有一问)。与 409 三选卡的入口刻意不同词 —— 同一张卡的
|
|
45
|
+
* 两个入口,题面说的不是一件事。 */
|
|
46
|
+
export const RESUME_CHOICE_HEADER = 'Earlier run still in flight';
|
|
47
|
+
/**
|
|
48
|
+
* §3.2a 活性证据行。**缺席 ⇒ null(整行不渲)**;绝不渲 0、绝不渲「已死」。
|
|
49
|
+
* 负数 / 非有限值同样按缺席处理(wire 位 UNTRUSTED,类型说是 number 也要真判)。
|
|
50
|
+
*/
|
|
51
|
+
export function resumeRunningLivenessRow(msSinceLastActivity) {
|
|
52
|
+
const ms = msSinceLastActivity;
|
|
53
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0)
|
|
54
|
+
return null;
|
|
55
|
+
return `The engine last recorded activity on it ${String(Math.round(ms / 1000))}s ago.`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 卡的题面。第二行 = 活性证据(§3.2a 缺席时**整行消失**,不留占位、不留「unknown」字样 ——
|
|
59
|
+
* 一行「last activity: unknown」和一行 0 一样是在替引擎回答一个它没回答的问题)。
|
|
60
|
+
*/
|
|
61
|
+
export function resumeRunningQuestionText(i) {
|
|
62
|
+
const head = `This conversation was resumed while an earlier turn is still executing on the engine ` +
|
|
63
|
+
`(run ${i.taskId}, status ${i.status}). sema did not touch it. What should it do?`;
|
|
64
|
+
const liveness = resumeRunningLivenessRow(i.msSinceLastActivity);
|
|
65
|
+
return liveness === null ? head : `${head}\n${liveness}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 选项集(同一张卡两个入口,选项集**按入口分形**)。
|
|
69
|
+
*
|
|
70
|
+
* 🔴 **顺序即默认焦点**:卡的渲染面 `focusedIndex` 初值恒 0,而 wire 的 `QuestionFrame` 里
|
|
71
|
+
* **没有**任何「默认焦点」位可传。所以「默认焦点 = ③ 后台留着」在结构上只有一个落法:把 ③ 排在
|
|
72
|
+
* options[0]。谁哪天为了「读起来顺」把 ①②③ 按序号排回去,默认焦点就漂到了 `Attach and watch`
|
|
73
|
+
* —— 一个手滑回车就把一条后台 run 接管到前台。常驻门逐位反钉这个顺序。
|
|
74
|
+
*/
|
|
75
|
+
export function resumeRunningOptions(i) {
|
|
76
|
+
const options = [
|
|
77
|
+
{
|
|
78
|
+
// 🔴 默认焦点 —— 三条路里唯一零副作用的一条,所以它是手滑回车的落点。
|
|
79
|
+
// 🔴 措辞:这一项**不许**承诺「它会留在某个任务面板里」—— 这张卡的前提恰恰是「那条 run 活得
|
|
80
|
+
// 比上一个客户端进程久」,新进程里没有任何腿会为它建行(③ 的处置本身也是显式零动作)。
|
|
81
|
+
// 默认焦点上的一句假 affordance 是最坏的一格:手滑回车的人拿到一条既看不见、也停不掉、
|
|
82
|
+
// 还占着会话锁的 run。换成两句**可证的**真话:本屏不会列出它;它还占着会话,所以下一条
|
|
83
|
+
// 消息会再撞上它(那时 409 三选卡会带着 steer/cancel 两条真出路出现)。
|
|
84
|
+
label: RESUME_CHOICE_BACKGROUND_LABEL,
|
|
85
|
+
description: 'Leaves the run alone. It keeps executing on the engine, but this screen will not show it and it will not be listed here — your next message runs into it again, and sema asks you then.',
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
if (i.canAttach) {
|
|
89
|
+
options.push({
|
|
90
|
+
label: RESUME_CHOICE_ATTACH_LABEL,
|
|
91
|
+
// 🔴 第二句是**知情同意**那一半(见 {@link resumeAttachReplayDisclosure}):resume 入口没有
|
|
92
|
+
// 事件锚可用,durable 尾只能从这条 run 的第一帧读起,所以崩溃前已经落盘并被 resume 还原到
|
|
93
|
+
// 屏上的那一段会再出现一次。用户按下之前就得看见这句 —— 一个会做出用户没预料到的事的
|
|
94
|
+
// 选项,和一个按了没用的选项同样是假 affordance。
|
|
95
|
+
description: 'Follows that run from here: sema replays it from the engine ledger and keeps streaming it into this conversation. ' +
|
|
96
|
+
'Anything from that turn already shown above will appear a second time — sema has no resume marker for it.',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (i.canCancel) {
|
|
100
|
+
options.push({
|
|
101
|
+
label: RESUME_CHOICE_CANCEL_LABEL,
|
|
102
|
+
description: 'Stops that run on the engine (whatever it has been doing is discarded) and frees this session for a new turn.',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return options;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 答案 → 决定。🔴 只认**逐字命中**的标签;空答(overlay 的 Esc/dismiss 形)、多选、认不出的
|
|
109
|
+
* 自由文本一律回落 ③ —— 破坏性动作只认显式选择(与 409 三选卡同律)。
|
|
110
|
+
*/
|
|
111
|
+
export function resumeChoiceFromLabels(selected) {
|
|
112
|
+
if (!Array.isArray(selected) || selected.length !== 1)
|
|
113
|
+
return 'background';
|
|
114
|
+
const label = selected[0];
|
|
115
|
+
if (label === RESUME_CHOICE_ATTACH_LABEL)
|
|
116
|
+
return 'attach';
|
|
117
|
+
if (label === RESUME_CHOICE_CANCEL_LABEL)
|
|
118
|
+
return 'cancel';
|
|
119
|
+
return 'background';
|
|
120
|
+
}
|
|
121
|
+
// ── §3.2b 无 UI 臂 ───────────────────────────────────────────────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* headless 车道撞 resume-running 的**专码**退出码。
|
|
124
|
+
*
|
|
125
|
+
* 为什么非 0:这一趟 resume **没有**把会话交到一个可用状态 —— 那条 run 还占着会话,脚本接下来
|
|
126
|
+
* 提交什么都会撞 409。静默 0 = 谎报成功。
|
|
127
|
+
* 为什么是 75:sysexits.h 的 `EX_TEMPFAIL`(「暂时性失败,请稍后重试」)语义正对 —— run 跑完
|
|
128
|
+
* 或被取消之后同一条命令就能过;与 cli 既用的 0/1/2/129/143 零撞(脚本消费者据此可以把「会话被
|
|
129
|
+
* 在飞 run 占住」与「真失败(1)」「用法错(2)」分臂)。
|
|
130
|
+
*/
|
|
131
|
+
export const RESUME_RUNNING_NO_UI_EXIT_CODE = 75;
|
|
132
|
+
/** 机读指路的行首标识 —— 脚本按它 grep;UI 改版不许动它。 */
|
|
133
|
+
export const RESUME_RUNNING_GUIDANCE_TAG = 'sema: resume-running:';
|
|
134
|
+
/**
|
|
135
|
+
* §3.2b 的**机读三选指路**。三条路各给真实命令或 API 路径,一条都不许省 ——
|
|
136
|
+
* 少写一条 = 那条路在这个车道上被客户端单方面关掉了。
|
|
137
|
+
*
|
|
138
|
+
* 🔴 `attach=` 那一行只有在**真知道 session id** 时才给具体命令:不知道还硬拼一条
|
|
139
|
+
* `sema --resume <sessionId>` 字面量,就是印一条按了没用的命令(假 affordance 的文本形)。
|
|
140
|
+
*/
|
|
141
|
+
export function resumeRunningNoUiGuidance(i) {
|
|
142
|
+
const sid = typeof i.sessionId === 'string' && i.sessionId.length > 0 ? i.sessionId : null;
|
|
143
|
+
const attach = sid === null ? 'sema --resume (pick this session from the list)' : `sema --resume ${sid}`;
|
|
144
|
+
const T = RESUME_RUNNING_GUIDANCE_TAG;
|
|
145
|
+
return [
|
|
146
|
+
`${T} run ${i.taskId} from an earlier turn is still executing on the engine, and this lane has no card to ask you on. sema did NOT cancel it and did NOT attach to it.`,
|
|
147
|
+
`${T} attach=${attach}`,
|
|
148
|
+
`${T} cancel=POST /v1/runs/${i.taskId}/cancel`,
|
|
149
|
+
// 🔴 这条车道连 REPL 都不挂载,更没有任何本地面板可看。给的是引擎侧真读面。
|
|
150
|
+
`${T} background=do nothing — run ${i.taskId} keeps executing on the engine; this client does not list it (read it with GET /v1/runs/${i.taskId})`,
|
|
151
|
+
].join('\n');
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* ① 接管之前的**显式披露**。
|
|
155
|
+
*
|
|
156
|
+
* resume 入口没有 `lastEventId` 可用(端今天不持久化引擎事件锚),所以 durable 尾从这条 run 的
|
|
157
|
+
* 第一帧读起 —— 崩溃前已落盘、已被 resume 还原到屏上的那一段会再渲一遍。续听腿那道「无锚 × 已上屏
|
|
158
|
+
* ⇒ 不许从头续读」的门在这一格判不出来(它数的是**本条流**吐过几帧,resume 入口恒 0),所以不能
|
|
159
|
+
* 拿它当安全证明。处置 = 说在前面:静默复读会让用户以为模型把同一段活干了两遍。
|
|
160
|
+
*
|
|
161
|
+
* 🔴 **账目面同源**:同一次重放也会把这条 run 已经执行过的每一个 `turn_end` 再喂一次成本累计器。
|
|
162
|
+
* 客户端这一端证明不了哪一枚已经在恢复出来的总额里(那需要一本按 turn 落盘的账 —— 与缺席的
|
|
163
|
+
* 事件锚是同一件移交残件),所以接管腿声明**不计账**。既然不计,就必须在**同一行**说清楚:
|
|
164
|
+
* 只披露视觉复读、不披露账目缺口,用户拿成本面跟引擎账本对不上时只会怀疑客户端算错了。
|
|
165
|
+
*/
|
|
166
|
+
export function resumeAttachReplayDisclosure(taskId) {
|
|
167
|
+
return (`sema: attaching to run ${taskId} — sema is replaying it from the engine's ledger, so anything from that ` +
|
|
168
|
+
`turn already shown above will appear again below. There is no resume marker for a run that outlived its shell. ` +
|
|
169
|
+
`The replayed turns are not billed into this session's cost again either — the engine's own record ` +
|
|
170
|
+
`(GET /v1/runs/${taskId}) is the authority on what that run really spent.`);
|
|
171
|
+
}
|
|
172
|
+
/** ② 打完 cancel 但引擎没确认落地时的如实一行(绝不谎报「已停止」)。 */
|
|
173
|
+
export function resumeRunningCancelUnconfirmedRow(taskId) {
|
|
174
|
+
return (`sema: the cancel for run ${taskId} did not come back confirmed, so that run may still be executing. ` +
|
|
175
|
+
`This screen does not list it — read its real state on the engine (GET /v1/runs/${taskId}) before ` +
|
|
176
|
+
`assuming this session is free.`);
|
|
177
|
+
}
|