@sema-agent/client-core 0.46.0 → 0.48.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 +188 -0
- package/LICENSE +21 -0
- package/README.md +5 -2
- package/dist/adapt/arms.js +10 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +96 -26
- package/dist/hitl/gateLedger.d.ts +21 -0
- package/dist/hitl/gateLedger.js +17 -0
- package/dist/hitl/hitlBridge.d.ts +61 -1
- package/dist/hitl/hitlBridge.js +49 -0
- package/dist/hitl/parkResolver.d.ts +2 -0
- package/dist/hitl/parkResolver.js +192 -30
- package/dist/hitl/toolApprovalWire.d.ts +36 -2
- package/dist/hitl/toolApprovalWire.js +29 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/interactiveHalt.d.ts +150 -0
- package/dist/interactiveHalt.js +131 -0
- package/dist/retryStatus.d.ts +50 -3
- package/dist/retryStatus.js +31 -7
- package/dist/sessionMemoryStatus.d.ts +138 -0
- package/dist/sessionMemoryStatus.js +181 -0
- package/dist/toolResult.js +9 -0
- package/dist/typePins.d.ts +40 -0
- package/docs/INTEGRATION-CLIENTS.md +353 -26
- package/package.json +6 -6
- package/docs/REFACTOR-LEDGER.md +0 -392
package/dist/index.js
CHANGED
|
@@ -138,6 +138,12 @@ export * from './notifications.js';
|
|
|
138
138
|
export * from './steering.js';
|
|
139
139
|
export * from './diagnostics.js';
|
|
140
140
|
export * from './retryStatus.js';
|
|
141
|
+
// ── S-53(0.48.0):会话记忆姿态的三端公共读面 ────────────────────────────────────────────────
|
|
142
|
+
// 收在库里的理由是**两处判定**(见文件头):① 同 status 不同码 —— 本路由的 404 有两个互不相干的
|
|
143
|
+
// 含义(`not_found.session` 会话未知 / `not_found.route` 老 server 没这条路由),按 status 分诊必然
|
|
144
|
+
// 把「部署没这个面」说成「你这个会话不存在」;② 五键缺席语义**逐键不同**,健康会话上就有两键
|
|
145
|
+
// 合法缺席,一律读成「没有/关着/0」就是对用户下一个证不出的断言。
|
|
146
|
+
export * from './sessionMemoryStatus.js';
|
|
141
147
|
export * from './adapt.js';
|
|
142
148
|
// ── B1 批:纯函数 / 侧信道台账 / 投影闸(2026-07-27)──────────────────────────────────────────
|
|
143
149
|
export * from './subagentContentStore.js';
|
|
@@ -377,6 +383,13 @@ export * from './hitl/localAllowRule.js';
|
|
|
377
383
|
// B7 ③(census G20,**行为改动**不是搬迁):pending-approvals 推送 feed(stream 优先 / 断流回落
|
|
378
384
|
// 轮询 / 定期再试)。🔴 它**不替换** D-1 的取件 —— 那三处必须继续走权威 `list()`(见文件头)。
|
|
379
385
|
export * from './hitl/approvalsFeed.js';
|
|
386
|
+
// ── #363 件③(0.47.0):交互 Esc 的**停止判定**三端公共上收 ──────────────────────────────────
|
|
387
|
+
// 「Esc ⇒ 先发 turn 级 halt;只有那一发连判决都拿不到、而屏上又确实挂着审批卡时,才升级成 run 级
|
|
388
|
+
// cancel」——TUI/desktop/web 三端都会 Esc、都会撞同一个 parked 格,判定本该在库里。此前整条住在
|
|
389
|
+
// cli 壳(`seamQuery.bestEffortInteractiveHalt` + `interruptWire.needsRunLevelStop`),那也是壳侧
|
|
390
|
+
// 「裸 fetch 直拨 interrupt」那条网络面豁免**唯一**的退役条件。本模块只出**判据**(纯函数 + 引擎
|
|
391
|
+
// 升级码闭集);发射/台账/留痕/UI 反馈仍归各端。🔴 零 IO、零 import、零 module 级状态。
|
|
392
|
+
export * from './interactiveHalt.js';
|
|
380
393
|
// ── B6 余项③(P5):补偿层登记表 —— 把「哪条补偿拆了、拆缝对面是谁、什么时候能退休」做成数据。
|
|
381
394
|
// 🔴 它**不是** `ADAPTER_DIVERGENCES`(那张表说的是 adapt 与 cli 行为不同的地方;本表里的东西
|
|
382
395
|
// 两侧行为相同)。自检口 `compensationSplitViolations()` 恒应为空。
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* interactiveHalt — 交互 **Esc** 的停止判定层(#363 件③,0.47.0;三端公共判定上收)。
|
|
3
|
+
*
|
|
4
|
+
* ── 收的是哪一件事 ────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* 「用户按 Esc ⇒ 先发 **turn 级 halt**;升级成 **run 级 cancel** 恰有**两格**:
|
|
6
|
+
* ① 引擎自己回了升级闭集里的 **409**(它在说「这里没有在飞 turn 可切,run 级停止请用 cancel」);
|
|
7
|
+
* ② 这一发**连判决都没拿到**(传输失败/超时/未武装)**且**屏上确实挂着审批卡(`parked`)。
|
|
8
|
+
* 其余一律不升级。」—— 这条判定 TUI / desktop / web **三端都要**(三端都会 Esc、都会撞
|
|
9
|
+
* 同一个 parked 格),此前整条住在 cli 壳里(`src/sema/seamQuery.ts` 的 `bestEffortInteractiveHalt`
|
|
10
|
+
* + `src/sema/interruptWire.ts` 的 `needsRunLevelStop`)。本模块把**判据**搬进来;
|
|
11
|
+
* **发射**(裸 fetch / SDK verb)、台账、留痕、UI 反馈仍归各端。
|
|
12
|
+
* ⚠️ 别把①漏掉(异源复审 [medium] 采纳的原文歧义):只写「拿不到判决且 parked 才升级」的话,
|
|
13
|
+
* 端会漏接**引擎明确指路**那一格 —— parked run 的会话锁不放,下一条消息照样撞「Session busy」。
|
|
14
|
+
*
|
|
15
|
+
* ── 两个动词各自唯一能做到的格(**cancel 绝不删**,本模块存在的前提)──────────────────────
|
|
16
|
+
* · `interrupt`(turn 级,bare 形 = core `stream.halt()`「切 + 停」)—— 需要一条**活的、在飞的
|
|
17
|
+
* turn**。这是 Esc 的正题:切掉这一轮,run 在边界上终局、同 session 下一 submit 照常续。
|
|
18
|
+
* · `cancel`(run 级)—— 显式整 run 终局手势,而且它是**唯一**能把一条停在审批门上的
|
|
19
|
+
* `suspended`/`needs_review` run 就地终态化、把会话锁放开的动词(server [868] 起的语义)。
|
|
20
|
+
* ⇒ 无条件把 cancel 换成 interrupt = 审批卡挂着按 Esc 的主场景当场回归成「Session busy」病;
|
|
21
|
+
* 无条件补 cancel = 同一条 run 上别的在飞工具(后台 bash 等)被连坐拆掉(#324 的病形)。
|
|
22
|
+
* 本判定就是这两条之间那道**不对称**的闸。
|
|
23
|
+
*
|
|
24
|
+
* ── 🔴 不对称是刻意的(方向安全,两条代价不等价)────────────────────────────────────────
|
|
25
|
+
* 判**不**升级的代价 = 用户退回既有的「Session busy」卡,自己再选一次(**可恢复**);
|
|
26
|
+
* 判**错**升级的代价 = 拆掉一条其实还活着的 run,连坐它身上所有在飞工具(**不可恢复**)。
|
|
27
|
+
* ⇒ 闸往严的一侧设,宁可少升一次;凡「证不出来」一律落**不升级**侧。
|
|
28
|
+
*
|
|
29
|
+
* ── 判据锚在哪 ────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
* · 升级的**第一判据 = 引擎自己的机器码**({@link RUN_LEVEL_STOP_ERROR_CODES} × 409),
|
|
31
|
+
* 不是壳对 UI 状态的猜测([anchor-on-the-deciding-quantity]);
|
|
32
|
+
* · 只有当**引擎连判决都没给**(传输失败/超时/根本没武装)时,才轮到壳自己独立知道的那个
|
|
33
|
+
* 事实(`parked` = 这一拍屏上确实挂着审批卡 ⇒ 那条 run 停在 pending 决断上 ⇒ 本来就没有
|
|
34
|
+
* 在飞 turn 可切 ⇒ 补 cancel 不构成连坐)。
|
|
35
|
+
*
|
|
36
|
+
* 🔴 **纯判定**:零 IO、零 import、零 module 级状态,同一入参恒同一出参。
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* server 在 interrupt 腿上表达「**这条 run 已经 parked、没有在飞 turn 可切**」的 409 机器码
|
|
40
|
+
* (engine 7.52.0 `dist/http/routes/runs.js` 真字节,`sendNoLiveTurn` 的 `isParkedRunStatus` 分支)。
|
|
41
|
+
* · `interrupt.nothing_in_flight` —— run 停在 pending 决断上。**审批卡挂着按 Esc 的主场景就是
|
|
42
|
+
* 这一格**,server 的原话直接指路 steer/cancel。
|
|
43
|
+
*
|
|
44
|
+
* 🔴 **闭集只收这一员**,另外两个 409 都刻意在外:
|
|
45
|
+
* · `interrupt.not_held` —— **不收**。语义是「**本副本**手上没有可切的 live turn face」,而 server
|
|
46
|
+
* 自己的两条原话把它拆得很清楚:一条是「run is live on **another replica**」,另一条是
|
|
47
|
+
* verify/cascade 车道不暴露 live stream。**两条都不证明「全局没有在飞 turn」** —— 尤其第一条,
|
|
48
|
+
* 那条 run 正在别的副本上跑得好好的。对它升级 = 把 **turn 级** Esc 放大成**整 run 终止**。
|
|
49
|
+
* server 提示「run-level stop 可用 cancel」是在告诉你**有这个动词**,不等于用户授权了 run 级停止。
|
|
50
|
+
* · `steering.not_running` —— 不收。run 已终局,没有任何东西要停,补一发 cancel 是纯噪声。
|
|
51
|
+
* ⇒ 只有「引擎结构化地证明了 run 已 parked」这一格才允许升级。等上游给出 owner/parked 判别位
|
|
52
|
+
* (或跨副本路由)之后,`not_held` 才谈得上有安全的处置。
|
|
53
|
+
*
|
|
54
|
+
* 🔴 **为什么是 `Object.freeze` 的数组而不是 `ReadonlySet`**(异源复审 [high] 采纳,真病):
|
|
55
|
+
* `ReadonlySet<string>` 只在**类型面**只读 —— 运行期它就是一只普通 `Set`,而判定查的是**同一个
|
|
56
|
+
* 实例**。任何 JS 消费者(或本包将来某处的一行手滑)`.add('interrupt.not_held')` 之后,同一份入参
|
|
57
|
+
* 就会从 `none` 变成 `escalate-cancel`,把一条**还活着**的 run 不可恢复地拆掉 —— 这正是本模块整段
|
|
58
|
+
* 头注在防的那个方向,却被自己的导出形留了后门;而「纯判定、零 module 级可变态」那句承诺也当场
|
|
59
|
+
* 变成假话。冻结数组在**运行期**真的改不动(ESM 恒 strict:`push`/下标赋值直接抛),于是「公开
|
|
60
|
+
* 面」与「判定源」可以安全地是同一个物,不必铸第二份(两份才会漂)。
|
|
61
|
+
* ⚠️ 判据形随之从 `.has()` 改成 `.includes()` —— 与同仓 `parkResolver.GATE_FAILURE_CODES` 的
|
|
62
|
+
* `as const` 数组 + `includes` 逐字同姿势。闭集只有一员,查找成本不是这里的量。
|
|
63
|
+
*/
|
|
64
|
+
export declare const RUN_LEVEL_STOP_ERROR_CODES: readonly string[];
|
|
65
|
+
/**
|
|
66
|
+
* 一发 bare interrupt 的归一结局(端把自己的发射结果折成这个形喂进来;**绝不是**异常)。
|
|
67
|
+
* 形与 cli `src/sema/interruptWire.ts` 的 `InterruptOutcome` 逐字同形 —— 壳换装时直接对接。
|
|
68
|
+
*/
|
|
69
|
+
export type InteractiveHaltInterruptOutcome =
|
|
70
|
+
/** 2xx:切 + 停受理。`turnCut` = 这一发有没有真切到一轮(重复调用第二次诚实 `false`;
|
|
71
|
+
* 读不出 ⇒ `undefined`,**不硬造 false**)。本判定不读它 —— 受理即受理。 */
|
|
72
|
+
{
|
|
73
|
+
kind: 'halted';
|
|
74
|
+
turnCut?: boolean | undefined;
|
|
75
|
+
}
|
|
76
|
+
/** 非 2xx:引擎**给了判决**。带机器码与状态码,升级闸只认这一类。 */
|
|
77
|
+
| {
|
|
78
|
+
kind: 'refused';
|
|
79
|
+
errorCode?: string | undefined;
|
|
80
|
+
status: number;
|
|
81
|
+
}
|
|
82
|
+
/** 没武装(mock 车道 / live client 还没建)⇒ 一枪没打,**没有判决**。 */
|
|
83
|
+
| {
|
|
84
|
+
kind: 'unarmed';
|
|
85
|
+
}
|
|
86
|
+
/** 传输层失败(超时、连接断)⇒ 一样是**没拿到服务端判决**。 */
|
|
87
|
+
| {
|
|
88
|
+
kind: 'transport';
|
|
89
|
+
detail?: string | undefined;
|
|
90
|
+
};
|
|
91
|
+
/** 本判定的入参。 */
|
|
92
|
+
export interface InteractiveHaltInput {
|
|
93
|
+
/**
|
|
94
|
+
* **这一拍屏上是否挂着审批卡**(壳自己独立知道的事实:cli = `focusedInputDialog === 'tool-permission'`)。
|
|
95
|
+
* 🔴 它只在「interrupt 连判决都没拿到」那一格起作用 —— 见 {@link planInteractiveHalt} 的分支表。
|
|
96
|
+
* 🔴 缺席 / 非 `true` 一律按**非 parked** 处理(fail-closed:证不出 parked 就不给升级资格)。
|
|
97
|
+
*/
|
|
98
|
+
readonly parked?: boolean | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* 首发 interrupt 的结局。**缺席 = 这一发还没打** ⇒ 本函数判 `interrupt`(首发恒行,无条件)。
|
|
101
|
+
* 在场 ⇒ 本函数回答的是「接着还要不要升级」。
|
|
102
|
+
*/
|
|
103
|
+
readonly interruptOutcome?: InteractiveHaltInterruptOutcome | undefined;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 判决的**机读因由**(闭集;人话文案归端,别拿它当展示串 —— [machine-readable-signal-not-visual-anchor])。
|
|
107
|
+
*/
|
|
108
|
+
export type InteractiveHaltReason =
|
|
109
|
+
/** 还没发过 interrupt ⇒ 首发恒行(这一步不看 `parked`,不看任何东西)。 */
|
|
110
|
+
'first-shot'
|
|
111
|
+
/** 202 受理:这一轮已被切掉,run 在边界上终局 ⇒ 无事可做。 */
|
|
112
|
+
| 'halted'
|
|
113
|
+
/** 引擎**自己**回了升级闭集里的 409 码 ⇒ 它在说「run 级停止请用 cancel」。 */
|
|
114
|
+
| 'engine-says-run-level'
|
|
115
|
+
/** 连判决都没拿到(传输失败/超时/未武装),但壳自证屏上挂着审批卡 ⇒ 这一格本来就没有在飞 turn。 */
|
|
116
|
+
| 'no-verdict-on-parked-card'
|
|
117
|
+
/** 连判决都没拿到且**非** parked ⇒ 那里真可能有在飞工具,凭一次超时拆整条 run 正是要消除的病。 */
|
|
118
|
+
| 'no-verdict-not-parked'
|
|
119
|
+
/** 引擎给了判决,但不在升级闭集里(404 / 400 / 非 409 / `steering.not_running` / 码读不出)。 */
|
|
120
|
+
| 'refused-no-escalation'
|
|
121
|
+
/** 入参不是本模块认得的任何一种结局形 ⇒ fail-closed 不升级(宿主传了新形/坏形)。 */
|
|
122
|
+
| 'unknown-outcome';
|
|
123
|
+
/** 判决值:做什么 + 为什么(机读)。 */
|
|
124
|
+
export interface InteractiveHaltPlan {
|
|
125
|
+
readonly action: 'interrupt' | 'escalate-cancel' | 'none';
|
|
126
|
+
readonly reason: InteractiveHaltReason;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Esc 停止弧的**唯一判定口**。
|
|
130
|
+
*
|
|
131
|
+
* ── 分支表(逐条 = 一条行为承诺)──────────────────────────────────────────────────────────
|
|
132
|
+
* | `interruptOutcome` | `parked` | 判决 | reason |
|
|
133
|
+
* |--------------------------------------------|----------|-------------------|--------|
|
|
134
|
+
* | 缺席(还没打) | 任意 | `interrupt` | `first-shot` |
|
|
135
|
+
* | `halted` | 任意 | `none` | `halted` |
|
|
136
|
+
* | `refused` + 409 + 升级闭集码 | 任意 | `escalate-cancel` | `engine-says-run-level` |
|
|
137
|
+
* | `refused`(其余:非 409 / 码不在闭集 / 码缺席)| 任意 | `none` | `refused-no-escalation` |
|
|
138
|
+
* | `transport` / `unarmed` | `true` | `escalate-cancel` | `no-verdict-on-parked-card` |
|
|
139
|
+
* | `transport` / `unarmed` | 其余 | `none` | `no-verdict-not-parked` |
|
|
140
|
+
* | 认不得的形 | 任意 | `none` | `unknown-outcome` |
|
|
141
|
+
*
|
|
142
|
+
* 🔴 **`parked` 只在「没有判决」那两格被读**:引擎给了判决时,判决说了算 —— 壳的 UI 状态不许覆盖
|
|
143
|
+
* 引擎的结构化答复(反过来也一样:引擎说 parked 时,`parked=false` 不阻止升级)。
|
|
144
|
+
* 🔴 **首发无条件**:`first-shot` 那一格刻意不看 `parked`。审批卡挂着时首发 interrupt 会吃一个
|
|
145
|
+
* 409,那正是升级闸要的**判决**;为了省一次往返而直接跳到 cancel,等于把判据从引擎搬回壳里猜。
|
|
146
|
+
* 🔴 **顺序契约(端必读,不是本函数能保证的那半)**:这一发必须排在「撕 SSE」**之前**。交互车道零
|
|
147
|
+
* `x-detach-on-disconnect`,先撕流 = server 按断连语义当场收尾那条 run,随后落地的 interrupt
|
|
148
|
+
* 只会拿到 409 `steering.not_running`(cli L-11 真机实测:同步撕流形每一轮都是它)。
|
|
149
|
+
*/
|
|
150
|
+
export declare function planInteractiveHalt(input: InteractiveHaltInput): InteractiveHaltPlan;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* interactiveHalt — 交互 **Esc** 的停止判定层(#363 件③,0.47.0;三端公共判定上收)。
|
|
3
|
+
*
|
|
4
|
+
* ── 收的是哪一件事 ────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* 「用户按 Esc ⇒ 先发 **turn 级 halt**;升级成 **run 级 cancel** 恰有**两格**:
|
|
6
|
+
* ① 引擎自己回了升级闭集里的 **409**(它在说「这里没有在飞 turn 可切,run 级停止请用 cancel」);
|
|
7
|
+
* ② 这一发**连判决都没拿到**(传输失败/超时/未武装)**且**屏上确实挂着审批卡(`parked`)。
|
|
8
|
+
* 其余一律不升级。」—— 这条判定 TUI / desktop / web **三端都要**(三端都会 Esc、都会撞
|
|
9
|
+
* 同一个 parked 格),此前整条住在 cli 壳里(`src/sema/seamQuery.ts` 的 `bestEffortInteractiveHalt`
|
|
10
|
+
* + `src/sema/interruptWire.ts` 的 `needsRunLevelStop`)。本模块把**判据**搬进来;
|
|
11
|
+
* **发射**(裸 fetch / SDK verb)、台账、留痕、UI 反馈仍归各端。
|
|
12
|
+
* ⚠️ 别把①漏掉(异源复审 [medium] 采纳的原文歧义):只写「拿不到判决且 parked 才升级」的话,
|
|
13
|
+
* 端会漏接**引擎明确指路**那一格 —— parked run 的会话锁不放,下一条消息照样撞「Session busy」。
|
|
14
|
+
*
|
|
15
|
+
* ── 两个动词各自唯一能做到的格(**cancel 绝不删**,本模块存在的前提)──────────────────────
|
|
16
|
+
* · `interrupt`(turn 级,bare 形 = core `stream.halt()`「切 + 停」)—— 需要一条**活的、在飞的
|
|
17
|
+
* turn**。这是 Esc 的正题:切掉这一轮,run 在边界上终局、同 session 下一 submit 照常续。
|
|
18
|
+
* · `cancel`(run 级)—— 显式整 run 终局手势,而且它是**唯一**能把一条停在审批门上的
|
|
19
|
+
* `suspended`/`needs_review` run 就地终态化、把会话锁放开的动词(server [868] 起的语义)。
|
|
20
|
+
* ⇒ 无条件把 cancel 换成 interrupt = 审批卡挂着按 Esc 的主场景当场回归成「Session busy」病;
|
|
21
|
+
* 无条件补 cancel = 同一条 run 上别的在飞工具(后台 bash 等)被连坐拆掉(#324 的病形)。
|
|
22
|
+
* 本判定就是这两条之间那道**不对称**的闸。
|
|
23
|
+
*
|
|
24
|
+
* ── 🔴 不对称是刻意的(方向安全,两条代价不等价)────────────────────────────────────────
|
|
25
|
+
* 判**不**升级的代价 = 用户退回既有的「Session busy」卡,自己再选一次(**可恢复**);
|
|
26
|
+
* 判**错**升级的代价 = 拆掉一条其实还活着的 run,连坐它身上所有在飞工具(**不可恢复**)。
|
|
27
|
+
* ⇒ 闸往严的一侧设,宁可少升一次;凡「证不出来」一律落**不升级**侧。
|
|
28
|
+
*
|
|
29
|
+
* ── 判据锚在哪 ────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
* · 升级的**第一判据 = 引擎自己的机器码**({@link RUN_LEVEL_STOP_ERROR_CODES} × 409),
|
|
31
|
+
* 不是壳对 UI 状态的猜测([anchor-on-the-deciding-quantity]);
|
|
32
|
+
* · 只有当**引擎连判决都没给**(传输失败/超时/根本没武装)时,才轮到壳自己独立知道的那个
|
|
33
|
+
* 事实(`parked` = 这一拍屏上确实挂着审批卡 ⇒ 那条 run 停在 pending 决断上 ⇒ 本来就没有
|
|
34
|
+
* 在飞 turn 可切 ⇒ 补 cancel 不构成连坐)。
|
|
35
|
+
*
|
|
36
|
+
* 🔴 **纯判定**:零 IO、零 import、零 module 级状态,同一入参恒同一出参。
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* server 在 interrupt 腿上表达「**这条 run 已经 parked、没有在飞 turn 可切**」的 409 机器码
|
|
40
|
+
* (engine 7.52.0 `dist/http/routes/runs.js` 真字节,`sendNoLiveTurn` 的 `isParkedRunStatus` 分支)。
|
|
41
|
+
* · `interrupt.nothing_in_flight` —— run 停在 pending 决断上。**审批卡挂着按 Esc 的主场景就是
|
|
42
|
+
* 这一格**,server 的原话直接指路 steer/cancel。
|
|
43
|
+
*
|
|
44
|
+
* 🔴 **闭集只收这一员**,另外两个 409 都刻意在外:
|
|
45
|
+
* · `interrupt.not_held` —— **不收**。语义是「**本副本**手上没有可切的 live turn face」,而 server
|
|
46
|
+
* 自己的两条原话把它拆得很清楚:一条是「run is live on **another replica**」,另一条是
|
|
47
|
+
* verify/cascade 车道不暴露 live stream。**两条都不证明「全局没有在飞 turn」** —— 尤其第一条,
|
|
48
|
+
* 那条 run 正在别的副本上跑得好好的。对它升级 = 把 **turn 级** Esc 放大成**整 run 终止**。
|
|
49
|
+
* server 提示「run-level stop 可用 cancel」是在告诉你**有这个动词**,不等于用户授权了 run 级停止。
|
|
50
|
+
* · `steering.not_running` —— 不收。run 已终局,没有任何东西要停,补一发 cancel 是纯噪声。
|
|
51
|
+
* ⇒ 只有「引擎结构化地证明了 run 已 parked」这一格才允许升级。等上游给出 owner/parked 判别位
|
|
52
|
+
* (或跨副本路由)之后,`not_held` 才谈得上有安全的处置。
|
|
53
|
+
*
|
|
54
|
+
* 🔴 **为什么是 `Object.freeze` 的数组而不是 `ReadonlySet`**(异源复审 [high] 采纳,真病):
|
|
55
|
+
* `ReadonlySet<string>` 只在**类型面**只读 —— 运行期它就是一只普通 `Set`,而判定查的是**同一个
|
|
56
|
+
* 实例**。任何 JS 消费者(或本包将来某处的一行手滑)`.add('interrupt.not_held')` 之后,同一份入参
|
|
57
|
+
* 就会从 `none` 变成 `escalate-cancel`,把一条**还活着**的 run 不可恢复地拆掉 —— 这正是本模块整段
|
|
58
|
+
* 头注在防的那个方向,却被自己的导出形留了后门;而「纯判定、零 module 级可变态」那句承诺也当场
|
|
59
|
+
* 变成假话。冻结数组在**运行期**真的改不动(ESM 恒 strict:`push`/下标赋值直接抛),于是「公开
|
|
60
|
+
* 面」与「判定源」可以安全地是同一个物,不必铸第二份(两份才会漂)。
|
|
61
|
+
* ⚠️ 判据形随之从 `.has()` 改成 `.includes()` —— 与同仓 `parkResolver.GATE_FAILURE_CODES` 的
|
|
62
|
+
* `as const` 数组 + `includes` 逐字同姿势。闭集只有一员,查找成本不是这里的量。
|
|
63
|
+
*/
|
|
64
|
+
export const RUN_LEVEL_STOP_ERROR_CODES = Object.freeze([
|
|
65
|
+
'interrupt.nothing_in_flight',
|
|
66
|
+
]);
|
|
67
|
+
/** 升级闭集的码在 wire 上**只以 409 出现**(engine 7.52.0 `sendNoLiveTurn` 两分支逐字)。 */
|
|
68
|
+
const RUN_LEVEL_STOP_STATUS = 409;
|
|
69
|
+
/**
|
|
70
|
+
* 这一发 interrupt 的结局是不是在说「**改用 run 级停止**」。
|
|
71
|
+
*
|
|
72
|
+
* 判据 = **码闭集({@link RUN_LEVEL_STOP_ERROR_CODES})与 409 状态的合取**,两个条件都必要:
|
|
73
|
+
* · 只认码不认状态 ⇒ 一只 5xx(引擎内部错、代理改写体、老版本复用同名码)只要正文里带上那个码,
|
|
74
|
+
* 就能把我们骗去打一发 **run 级 cancel** —— 那是**破坏性**动作,而它当时其实没有任何判决依据。
|
|
75
|
+
* · 只认 409 不认码 ⇒ `steering.not_running`(run 已终局)也会被升级,对着一条已经结束的 run 补枪。
|
|
76
|
+
*/
|
|
77
|
+
function enginePointsToRunLevelStop(outcome) {
|
|
78
|
+
const o = outcome;
|
|
79
|
+
return (o?.kind === 'refused' &&
|
|
80
|
+
o.status === RUN_LEVEL_STOP_STATUS &&
|
|
81
|
+
typeof o.errorCode === 'string' &&
|
|
82
|
+
RUN_LEVEL_STOP_ERROR_CODES.includes(o.errorCode));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Esc 停止弧的**唯一判定口**。
|
|
86
|
+
*
|
|
87
|
+
* ── 分支表(逐条 = 一条行为承诺)──────────────────────────────────────────────────────────
|
|
88
|
+
* | `interruptOutcome` | `parked` | 判决 | reason |
|
|
89
|
+
* |--------------------------------------------|----------|-------------------|--------|
|
|
90
|
+
* | 缺席(还没打) | 任意 | `interrupt` | `first-shot` |
|
|
91
|
+
* | `halted` | 任意 | `none` | `halted` |
|
|
92
|
+
* | `refused` + 409 + 升级闭集码 | 任意 | `escalate-cancel` | `engine-says-run-level` |
|
|
93
|
+
* | `refused`(其余:非 409 / 码不在闭集 / 码缺席)| 任意 | `none` | `refused-no-escalation` |
|
|
94
|
+
* | `transport` / `unarmed` | `true` | `escalate-cancel` | `no-verdict-on-parked-card` |
|
|
95
|
+
* | `transport` / `unarmed` | 其余 | `none` | `no-verdict-not-parked` |
|
|
96
|
+
* | 认不得的形 | 任意 | `none` | `unknown-outcome` |
|
|
97
|
+
*
|
|
98
|
+
* 🔴 **`parked` 只在「没有判决」那两格被读**:引擎给了判决时,判决说了算 —— 壳的 UI 状态不许覆盖
|
|
99
|
+
* 引擎的结构化答复(反过来也一样:引擎说 parked 时,`parked=false` 不阻止升级)。
|
|
100
|
+
* 🔴 **首发无条件**:`first-shot` 那一格刻意不看 `parked`。审批卡挂着时首发 interrupt 会吃一个
|
|
101
|
+
* 409,那正是升级闸要的**判决**;为了省一次往返而直接跳到 cancel,等于把判据从引擎搬回壳里猜。
|
|
102
|
+
* 🔴 **顺序契约(端必读,不是本函数能保证的那半)**:这一发必须排在「撕 SSE」**之前**。交互车道零
|
|
103
|
+
* `x-detach-on-disconnect`,先撕流 = server 按断连语义当场收尾那条 run,随后落地的 interrupt
|
|
104
|
+
* 只会拿到 409 `steering.not_running`(cli L-11 真机实测:同步撕流形每一轮都是它)。
|
|
105
|
+
*/
|
|
106
|
+
export function planInteractiveHalt(input) {
|
|
107
|
+
const outcome = input.interruptOutcome;
|
|
108
|
+
// 首发恒行:一发都还没打的时候,没有任何东西需要判。
|
|
109
|
+
// 🔴 `null` 与 `undefined` **同判「还没打」**(两种缺席形;宿主用哪一种写「没有结局」都算数)——
|
|
110
|
+
// 而不是落 fail-closed 的 `unknown-outcome`:那会让 Esc 一发都不打(interrupt 是**非破坏性**
|
|
111
|
+
// 动词,把它扣下来比多打一发更坏)。fail-closed 守的是**升级**那一侧,不是首发。
|
|
112
|
+
if (outcome === undefined || outcome === null)
|
|
113
|
+
return { action: 'interrupt', reason: 'first-shot' };
|
|
114
|
+
// 防御读:wire/宿主形在运行期不存在类型(M0/M1),`kind` 可能是新词、可能压根不是对象。
|
|
115
|
+
const kind = outcome.kind;
|
|
116
|
+
if (kind === 'halted')
|
|
117
|
+
return { action: 'none', reason: 'halted' };
|
|
118
|
+
if (kind === 'refused') {
|
|
119
|
+
return enginePointsToRunLevelStop(outcome)
|
|
120
|
+
? { action: 'escalate-cancel', reason: 'engine-says-run-level' }
|
|
121
|
+
: { action: 'none', reason: 'refused-no-escalation' };
|
|
122
|
+
}
|
|
123
|
+
if (kind === 'transport' || kind === 'unarmed') {
|
|
124
|
+
// 没拿到判决。只有壳能**独立证明** parked 的那一格才升级(理由见顶注的不对称段)。
|
|
125
|
+
return input.parked === true
|
|
126
|
+
? { action: 'escalate-cancel', reason: 'no-verdict-on-parked-card' }
|
|
127
|
+
: { action: 'none', reason: 'no-verdict-not-parked' };
|
|
128
|
+
}
|
|
129
|
+
// 宿主传了本模块认不得的形(新 kind / 坏对象)。fail-closed:不对一个读不懂的结局动手。
|
|
130
|
+
return { action: 'none', reason: 'unknown-outcome' };
|
|
131
|
+
}
|
package/dist/retryStatus.d.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* gave_up → error+terminal → '✻ <detail>'(重试用尽的终态;与 recovered 反向。**打 terminal 位**,
|
|
11
11
|
* 渲染面据此不得再接「· Retrying in Ns」——已经没有下一次了)
|
|
12
12
|
* 绝不捏造 attempt 计数——只用引擎真给的 phase / detail / retryInSec / retryInMs / attempt /
|
|
13
|
-
* maxRetries / errClass(core
|
|
13
|
+
* maxRetries / errClass / retryAtMs / errorStatus(core 7.0.x 起**九键**;0.48.0 补齐后两位,
|
|
14
|
+
* 族扫账见 {@link BRAIN_STATUS_PAYLOAD_KEYS} 末段)。
|
|
14
15
|
*
|
|
15
16
|
* 🔴 员数与字段补全(2026-08-08,#3004 跟修批)。此前本文件只列 4 相 + 3 字段,而引擎侧
|
|
16
17
|
* (core `BrainStatusPhase` / `BrainStatus`,dist/core/types.d.ts)是 **6 相 + 6 字段**,server 两腿的
|
|
@@ -33,6 +34,12 @@ export type RetryStatus =
|
|
|
33
34
|
maxRetries?: number;
|
|
34
35
|
/** 见 {@link BrainStatusPayload.errClass}(引擎给了才在场;本层只透传,措辞是壳半场)。 */
|
|
35
36
|
errClass?: BrainRetryErrClass | (string & {});
|
|
37
|
+
/** 见 {@link BrainStatusPayload.retryAtMs}。🔴 **产生者铸的墙钟截止点**;在场时端应当拿它
|
|
38
|
+
* 渲倒计时,而不是拿 {@link deadline} —— 后者是**本层**按 `nowMs + 剩余量` 现算的,跨进程跳
|
|
39
|
+
* 的传输耗时已经把它推后了(core 顶注点名的正是这个病)。缺席 ⇒ 退回 `deadline`。 */
|
|
40
|
+
retryAtMs?: number;
|
|
41
|
+
/** 见 {@link BrainStatusPayload.errorStatus}(引擎给了才在场;缺席禁渲成 0/未知码)。 */
|
|
42
|
+
errorStatus?: number;
|
|
36
43
|
} | {
|
|
37
44
|
kind: 'error';
|
|
38
45
|
deadline: number;
|
|
@@ -40,6 +47,11 @@ export type RetryStatus =
|
|
|
40
47
|
maxRetries?: number;
|
|
41
48
|
/** 见 {@link BrainStatusPayload.errClass}(引擎给了才在场;本层只透传,措辞是壳半场)。 */
|
|
42
49
|
errClass?: BrainRetryErrClass | (string & {});
|
|
50
|
+
/** 见 {@link BrainStatusPayload.retryAtMs}(与 `stalled` 臂同义同纪律:在场优先于 `deadline`)。 */
|
|
51
|
+
retryAtMs?: number;
|
|
52
|
+
/** 见 {@link BrainStatusPayload.errorStatus}。CC parity:`system/api_retry.error_status`
|
|
53
|
+
* 就是这个数,端可据它渲「API Error 529 · Retrying」这类**点名失败方**的行。 */
|
|
54
|
+
errorStatus?: number;
|
|
43
55
|
/**
|
|
44
56
|
* 🔴 **终态位**(2026-08-08 对抗复审命中):`true` ⇔ 引擎**不会再重试了**(`gave_up` 相)。
|
|
45
57
|
* 缺席 = 仍在重试循环里(retrying / rate_limited / circuit_open)。
|
|
@@ -86,7 +98,8 @@ export type BrainStatusPhase = (typeof BRAIN_STATUS_PHASES)[number];
|
|
|
86
98
|
* 判据锚在「真正决定结果的量」上:决定结果的是有没有分支,不是有没有一张表。
|
|
87
99
|
*/
|
|
88
100
|
export type BrainRetryErrClass = 'connect_refused' | 'transport' | 'rate_limit' | 'server' | 'http' | 'output_cap';
|
|
89
|
-
/** wire 上 `status` 臂的载荷(= core `BrainStatus`;server 两腿白名单原样转发这
|
|
101
|
+
/** wire 上 `status` 臂的载荷(= core `BrainStatus`;server 两腿白名单原样转发这 **9** 键 ——
|
|
102
|
+
* 真源 = server 7.54.0 `dist/trace/project.js` 的 `brainStatusEventData`,逐条条件拷贝)。 */
|
|
90
103
|
export interface BrainStatusPayload {
|
|
91
104
|
/** 闭集 + `(string & {})`:未知相仍可携带(开集读),不必先改类型再解析。 */
|
|
92
105
|
phase: BrainStatusPhase | (string & {});
|
|
@@ -100,6 +113,40 @@ export interface BrainStatusPayload {
|
|
|
100
113
|
attempt?: number;
|
|
101
114
|
/** 引擎这一轮的重试上限。与 `attempt` 一起才能渲「2/5」。 */
|
|
102
115
|
maxRetries?: number;
|
|
116
|
+
/**
|
|
117
|
+
* core 7.0.x(#506 ㋑;server ≥7.53 `brainStatusEventData` 真发;ADDITIVE,0.48.0 补)——
|
|
118
|
+
* 本次退避**预计结束的墙钟时刻**(epoch ms)= 发帧那一刻的 `Date.now() + retryInMs`,
|
|
119
|
+
* **由产生者铸**。不变式(core 顶注逐字):`retryInMs` 在场时它必在场,不宣告等待的帧
|
|
120
|
+
* (`recovered` / `gave_up` / output-cap 立即重发)上必缺席。
|
|
121
|
+
*
|
|
122
|
+
* 🔴 **为什么这一位必须由上游给、消费端不许自己算**(core 顶注的原话,也是本包接它的理由):
|
|
123
|
+
* `Date.now() + retryInMs` 只对**瞬时收到帧**的读者成立。中间隔了若干进程跳(core → server →
|
|
124
|
+
* 本包 → 端)之后再自己加,得到的截止点已经被传输耗时推后了;而 core 对 >30s 的等待会每 30s
|
|
125
|
+
* **重播一帧并递减**,于是「自己算」的倒计时在每个重播片上**重新起跳**而不是收敛。
|
|
126
|
+
* 🔴 **时钟域,明说以消除歧义**:墙钟(`Date.now()`),**不是**单调钟。端不得拿它与自己的
|
|
127
|
+
* 单调计时器比;跨机器/跨授时校正时它是**近似值** —— 权威的**相对**量始终是 `retryInMs`,
|
|
128
|
+
* 本位是由它派生的绝对便利位。
|
|
129
|
+
*/
|
|
130
|
+
retryAtMs?: number;
|
|
131
|
+
/**
|
|
132
|
+
* core 7.0.x(#506 ㋑;server ≥7.53 `brainStatusEventData` 真发;ADDITIVE,0.48.0 补)——
|
|
133
|
+
* **刚刚失败的那次尝试**的 HTTP 状态码。这条通道上**唯一**一个 provider 自报的数字
|
|
134
|
+
* (其余一切仍走引擎自己的中性分桶 `phase` / `errClass`)。
|
|
135
|
+
*
|
|
136
|
+
* 🔴 **为什么这条一贯拒绝 HTTP 细节的通道要收它**(core 顶注的原话):要让操作者判断
|
|
137
|
+
* 「该等,还是该去修点什么」,两个中性桶**供不出**这个信息 —— `rate_limit` 同时盖住
|
|
138
|
+
* 「provider 自己会清掉的 429」与「账号配额耗尽的 429」,`server` 同时盖住 500 与
|
|
139
|
+
* 负载均衡器后面的 503。CC 在**同一场合**报同一个数(`system/api_retry.error_status`,
|
|
140
|
+
* 读自 `APIError.status`)⇒ 接它同时满足 CC parity 与本通道的中立契约。
|
|
141
|
+
* 🔴 **在场面是封闭的,缺席不许反推**:只在「这次尝试拿到了一个**点名了失败**的应答」时在场
|
|
142
|
+
* (连接梯的 retry 等待 + output-cap 的 400)。**恒缺席**于:传输层失败(压根没有应答)、
|
|
143
|
+
* 流中断(那条应答自己的状态是**成功**,一个什么都没失败的状态不许被当成重试的原因)、
|
|
144
|
+
* `circuit_open`(本地快失败,没发出去)、`recovered`/`gave_up`(不宣告任何尝试)。
|
|
145
|
+
* ⇒ 端**禁**把缺席渲成 0 或「未知错误码」;那两类的因由由 `errClass` 按构造点名。
|
|
146
|
+
* 🔴 **与 CC 的可空必填不同**:CC 有一整条 `system/api_retry` 消息专供重试场合,能把键设成
|
|
147
|
+
* 必填可空;本形是**所有 phase 共用的一个形**,所以是可选位(core 顶注同款理由)。
|
|
148
|
+
*/
|
|
149
|
+
errorStatus?: number;
|
|
103
150
|
/**
|
|
104
151
|
* core 5.43.0(#307 双扫 S44,2026-08-19;ADDITIVE)——**这次等待的原因分桶**
|
|
105
152
|
* (core `BrainRetryErrClass`,provider 中立闭集)。与 `phase`(引擎正在**做什么**)互补:
|
|
@@ -120,7 +167,7 @@ export interface BrainStatusPayload {
|
|
|
120
167
|
* engine-vocab 门 G2-c 拿它与 core `BrainStatus` 的键集逐元素比 ⇒ 引擎 additive 增键当天红。
|
|
121
168
|
* 下面两个类型钉保证镜像与 interface 之间不可能漂移(少键/多键都是编译错)。
|
|
122
169
|
*/
|
|
123
|
-
export declare const BRAIN_STATUS_PAYLOAD_KEYS: readonly ["phase", "detail", "retryInSec", "retryInMs", "attempt", "maxRetries", "errClass"];
|
|
170
|
+
export declare const BRAIN_STATUS_PAYLOAD_KEYS: readonly ["phase", "detail", "retryInSec", "retryInMs", "attempt", "maxRetries", "errClass", "retryAtMs", "errorStatus"];
|
|
124
171
|
/**
|
|
125
172
|
* BrainStatus 载荷 → spinner 行状态。
|
|
126
173
|
*
|
package/dist/retryStatus.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* gave_up → error+terminal → '✻ <detail>'(重试用尽的终态;与 recovered 反向。**打 terminal 位**,
|
|
11
11
|
* 渲染面据此不得再接「· Retrying in Ns」——已经没有下一次了)
|
|
12
12
|
* 绝不捏造 attempt 计数——只用引擎真给的 phase / detail / retryInSec / retryInMs / attempt /
|
|
13
|
-
* maxRetries / errClass(core
|
|
13
|
+
* maxRetries / errClass / retryAtMs / errorStatus(core 7.0.x 起**九键**;0.48.0 补齐后两位,
|
|
14
|
+
* 族扫账见 {@link BRAIN_STATUS_PAYLOAD_KEYS} 末段)。
|
|
14
15
|
*
|
|
15
16
|
* 🔴 员数与字段补全(2026-08-08,#3004 跟修批)。此前本文件只列 4 相 + 3 字段,而引擎侧
|
|
16
17
|
* (core `BrainStatusPhase` / `BrainStatus`,dist/core/types.d.ts)是 **6 相 + 6 字段**,server 两腿的
|
|
@@ -49,6 +50,15 @@ export const BRAIN_STATUS_PAYLOAD_KEYS = [
|
|
|
49
50
|
'maxRetries',
|
|
50
51
|
// core 5.43.0 跟车一键(#307 双扫 S44,2026-08-19):engine-vocab G2-c 对实装 core 逐键对账。
|
|
51
52
|
'errClass',
|
|
53
|
+
// ── core 7.0.x 跟车**两键**(0.48.0;#506 ㋑)────────────────────────────────────────────
|
|
54
|
+
// 🔴 **族扫的产物,不是只补触发本批的那一个**([same-shape-residue-constitution]):本批的
|
|
55
|
+
// 派工单只点名了 `errorStatus`。族扫 = 把实装 core 的 `BrainStatus` **整个键集**与本镜像逐一
|
|
56
|
+
// 对表(而不是只补被点名的那一位),当场捞出**存量**漏键 `retryAtMs` —— 它与 errorStatus 同批
|
|
57
|
+
// 进 core、同在 server `brainStatusEventData` 的白名单里真发,只是没人提。
|
|
58
|
+
// 病形与 0.47.0 的 `task_progress.model` 逐字同族:上游真发、本层闭形镜像剥掉、两边代码看着都对。
|
|
59
|
+
// engine-vocab G2-c 的等值门本批**先红后绿**,红文逐字:「漏:retryAtMs,errorStatus」。
|
|
60
|
+
'retryAtMs',
|
|
61
|
+
'errorStatus',
|
|
52
62
|
];
|
|
53
63
|
const _brainStatusKeyPin = [true, true];
|
|
54
64
|
void _brainStatusKeyPin;
|
|
@@ -73,20 +83,34 @@ export function mapBrainStatusToRetry(p, nowMs) {
|
|
|
73
83
|
* (本层零分支,认不得也没有可落错的臂)。
|
|
74
84
|
*/
|
|
75
85
|
const cause = typeof p.errClass === 'string' && p.errClass.length > 0 ? { errClass: p.errClass } : {};
|
|
86
|
+
/**
|
|
87
|
+
* core 7.0.x 跟车两位(0.48.0),**原样透传、零重算**:
|
|
88
|
+
* · `retryAtMs` —— 产生者铸的墙钟截止点。本层**刻意不拿它去改写** `deadline`:那一位是已发布的
|
|
89
|
+
* 行为面(0.29.0 起端就在读),换算法 = 一次静默的行为改动。两位并存、端自己选(在场优先),
|
|
90
|
+
* 是 additive 的唯一诚实形。
|
|
91
|
+
* · `errorStatus` —— 刚失败那次尝试的 HTTP 状态。
|
|
92
|
+
* 🔴 **两位在终态帧(`recovered`/`gave_up`)与 `circuit_open` 上按 core 的不变式本就缺席**,
|
|
93
|
+
* 所以这里不写「终态就不透」的特判:该由 `terminal` 位管的事(渲染面不得再接倒计时)已经
|
|
94
|
+
* 有位管了,再加一道按键在场性的特判,等于让本层替上游重述一遍它自己的不变式 —— 上游哪天
|
|
95
|
+
* 改了不变式,特判就成了**本层单方面剥键**。判据锚在真正决定渲染的量(`terminal`)上,
|
|
96
|
+
* 不锚一个恰好同时成立的第二事实([anchor-on-the-deciding-quantity])。
|
|
97
|
+
*/
|
|
98
|
+
const producerTiming = typeof p.retryAtMs === 'number' ? { retryAtMs: p.retryAtMs } : {};
|
|
99
|
+
const failureStatus = typeof p.errorStatus === 'number' ? { errorStatus: p.errorStatus } : {};
|
|
100
|
+
const extra = { ...counts, ...cause, ...producerTiming, ...failureStatus };
|
|
76
101
|
switch (p.phase) {
|
|
77
102
|
// 🔴 引擎直报「恢复」:摘行。绝不落 error 臂 —— 那是把成功渲成失败。
|
|
78
103
|
case 'recovered':
|
|
79
104
|
return null;
|
|
80
105
|
case 'reconnecting':
|
|
81
|
-
return { kind: 'stalled', deadline, ...
|
|
106
|
+
return { kind: 'stalled', deadline, ...extra };
|
|
82
107
|
case 'rate_limited':
|
|
83
|
-
return { kind: 'error', deadline, ...
|
|
108
|
+
return { kind: 'error', deadline, ...extra, error: { formatted: '', rateLimits: {} } };
|
|
84
109
|
case 'circuit_open':
|
|
85
110
|
return {
|
|
86
111
|
kind: 'error',
|
|
87
112
|
deadline,
|
|
88
|
-
...
|
|
89
|
-
...cause,
|
|
113
|
+
...extra,
|
|
90
114
|
error: { formatted: p.detail ?? 'Service temporarily unavailable', isNetworkDown: true },
|
|
91
115
|
};
|
|
92
116
|
// 重试用尽:仍是错误行(与 recovered 反向,绝不许一起归成「结束了 ⇒ 清行」),但**打终态位** ——
|
|
@@ -94,9 +118,9 @@ export function mapBrainStatusToRetry(p, nowMs) {
|
|
|
94
118
|
// retrying'),既没有计数也没有 retryIn*,所以「不会再重试了」这件事**只能**由本位表达;
|
|
95
119
|
// 靠 attempt===maxRetries 去推是错的(供给方根本不发那两位)。
|
|
96
120
|
case 'gave_up':
|
|
97
|
-
return { kind: 'error', deadline, terminal: true, ...
|
|
121
|
+
return { kind: 'error', deadline, terminal: true, ...extra, error: { formatted: p.detail ?? '' } };
|
|
98
122
|
case 'retrying':
|
|
99
123
|
default:
|
|
100
|
-
return { kind: 'error', deadline, ...
|
|
124
|
+
return { kind: 'error', deadline, ...extra, error: { formatted: '' } };
|
|
101
125
|
}
|
|
102
126
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sessionMemoryStatus.ts — 会话记忆姿态的**三端公共读面**(S-53;server ≥7.53 / core 7.0.2 #511 件1;
|
|
3
|
+
* sdk 7.4.0 `sessions.memoryStatus` + `SessionMemoryStatus`)。
|
|
4
|
+
*
|
|
5
|
+
* ## 本件为什么在库里而不是在壳里
|
|
6
|
+
* 这条读面上有**两处判定**,三端(TUI / web / desktop)各写一遍必然各错一遍:
|
|
7
|
+
* ① **同 status 不同码**:`GET /v1/sessions/:id/memory-status` 的 404 有**两个**互不相干的含义 ——
|
|
8
|
+
* `not_found.session`(会话未知/非属主)与 `not_found.route`(<7.53 的老 server 压根没这条路由,
|
|
9
|
+
* 答通用回退)。按 **status** 分诊必然把「你的部署没这个面」说成「你这个会话不存在」。
|
|
10
|
+
* 判据只能锚 `errorCode`([anchor-on-the-deciding-quantity]:决定处置的量是码,不是 status)。
|
|
11
|
+
* ② **五键缺席语义逐键不同**(sdk `SessionMemoryStatus` 顶注逐字):`optOutSource` / `lastCaptureAt`
|
|
12
|
+
* 在**健康会话**上就合法缺席,其余三键只在源不可读时缺席。零历史会话的真形 =
|
|
13
|
+
* `{captureOptedOut:false, committedCount:0, foldedCount:0}`,**没有任何降级**。
|
|
14
|
+
* 把「缺席」一律读成「没有/关着/0」就是对用户下一个证不出的断言
|
|
15
|
+
* ([honest-absence-not-fabricated-zero])。
|
|
16
|
+
*
|
|
17
|
+
* ## 分工(与仓内既有形同款)
|
|
18
|
+
* **纯判定 + 薄封装**:IO 归宿主注入(`MemoryStatusClientLike`,与 `HitlClientLike` 同款 duck-type),
|
|
19
|
+
* 本件不构造 client、不认 baseUrl、不碰凭据。**一句面向用户的话都不铸** —— 措辞与是否上屏归端。
|
|
20
|
+
*/
|
|
21
|
+
import type { SessionMemoryStatus } from '@sema-agent/sdk';
|
|
22
|
+
/** 宿主注入的读口切片(duck-typed,与 {@link HitlClientLike} 同因:宿主可以注入自己的传输层)。 */
|
|
23
|
+
export interface MemoryStatusClientLike {
|
|
24
|
+
sessions: {
|
|
25
|
+
memoryStatus(sessionId: string, opts?: {
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
sessionId: string;
|
|
29
|
+
} & SessionMemoryStatus>;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export type MemoryStatusVerdict =
|
|
33
|
+
/** 200:读到了。`facts` 的**每一键**都按 wire 原样(合形才在场),缺席语义见 {@link readCaptureOptOut} 等。 */
|
|
34
|
+
{
|
|
35
|
+
readonly kind: 'ok';
|
|
36
|
+
readonly sessionId: string;
|
|
37
|
+
readonly facts: SessionMemoryStatus;
|
|
38
|
+
}
|
|
39
|
+
/** 本部署**没有这个面** —— 端应当**别提供这个入口**(不是报错,是诚实的能力缺席)。 */
|
|
40
|
+
| {
|
|
41
|
+
readonly kind: 'unsupported';
|
|
42
|
+
readonly reason: MemoryStatusUnsupportedReason;
|
|
43
|
+
}
|
|
44
|
+
/** 会话未知**或非本 principal 所有**(server 反枚举:两者同码同串,判不出更细的,别猜)。 */
|
|
45
|
+
| {
|
|
46
|
+
readonly kind: 'not_found';
|
|
47
|
+
}
|
|
48
|
+
/** 分类不明**如实说** —— 绝不编一个具体原因(`classifyTurnWireError` ④ 臂同款纪律)。 */
|
|
49
|
+
| {
|
|
50
|
+
readonly kind: 'failed';
|
|
51
|
+
readonly error: unknown;
|
|
52
|
+
};
|
|
53
|
+
export type MemoryStatusUnsupportedReason =
|
|
54
|
+
/** 501 `capability.*` —— 引擎未接线 / pg+tidb 记忆后端不自带控制面。**换部署形态**才可能有。 */
|
|
55
|
+
'capability'
|
|
56
|
+
/** 404 `not_found.route` —— 支持区间内 <7.53 的老 server 无本路由(通用回退)。**升 server**。 */
|
|
57
|
+
| 'route'
|
|
58
|
+
/** 501 `feature.*` —— 面在、开关关着。**防御臂**:今天这条路由的门序里没有 feature 臂
|
|
59
|
+
* (sdk 顶注的门序 401→501 capability→404→501→200),留着是因为 `capability.*` 与 `feature.*`
|
|
60
|
+
* 同为 501 而**处置相反**(SDK `CapabilityUnavailableError` / `FeatureDisabledError` 顶注逐字),
|
|
61
|
+
* 合流会把「叫管理员开开关」说成「换部署形态」。今天不可达 ⇒ 不是缺口,是不合流的登记。 */
|
|
62
|
+
| 'feature';
|
|
63
|
+
/**
|
|
64
|
+
* 一次 `memoryStatus` 失败(任意抛出物)→ 处置分型。**永不抛**。
|
|
65
|
+
*
|
|
66
|
+
* 🔴 **结构视图读,不 `instanceof`**(与 `readDecideCurrentPending` 逐字同因):抛出来的是不是 SDK 的
|
|
67
|
+
* `APIError` 由**宿主**决定 —— 跨 realm / 双 SDK 实例下 `instanceof` 会把一个读得懂的错判成读不懂。
|
|
68
|
+
* 🔴 **码优先、status 只作兜底**,且兜底只敢兜 501:
|
|
69
|
+
* · 501 在本路由上无歧义(两条 501 臂都是「面不在/没开」)⇒ 无码时按 `capability` 兜底是安全的;
|
|
70
|
+
* · 404 **恰恰相反**:两个码的处置相反,无码的 404 判不出 ⇒ 落 `failed`(如实说不知道),
|
|
71
|
+
* **绝不**挑一个猜 —— 猜错任一向都是对用户的一句假断言。
|
|
72
|
+
*/
|
|
73
|
+
export declare function classifyMemoryStatusFailure(e: unknown): MemoryStatusVerdict;
|
|
74
|
+
export type CaptureOptOutReading =
|
|
75
|
+
/** 存在一条单向 capture opt-out 记录(`captureOptedOut:true` + `optOutSource:"record"`)。 */
|
|
76
|
+
{
|
|
77
|
+
readonly state: 'opted_out';
|
|
78
|
+
}
|
|
79
|
+
/** 记录店可读且无记录 ⇒ capture 开着(`captureOptedOut:false`,`optOutSource` 合法缺席=健康默认态)。 */
|
|
80
|
+
| {
|
|
81
|
+
readonly state: 'active';
|
|
82
|
+
}
|
|
83
|
+
/** 记录店失败 ⇒ **判不了**(`optOutSource:"fault"` 伴 `captureOptedOut` 缺席)。
|
|
84
|
+
* 🔴 端**禁**把它渲成「开着」或「关着」—— 那是把一次读取失败说成一个姿态。 */
|
|
85
|
+
| {
|
|
86
|
+
readonly state: 'indeterminate';
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* `captureOptedOut` × `optOutSource` 的**合读**。两键必须一起读:单读 `captureOptedOut` 的话,
|
|
90
|
+
* 「缺席」既可能是 fault(判不了)也可能是老 server 没给,读成 `false`(capture 开着)是最坏的方向 ——
|
|
91
|
+
* 它会让端向用户断言「你的对话正在被记忆」,而真相是**不知道**。
|
|
92
|
+
*
|
|
93
|
+
* 🔴 **`fault` 一票否决,先于任何布尔位判**(对抗复审 [medium] 采纳,0.48.0):
|
|
94
|
+
* 上游契约里 `optOutSource:"fault"` 与 `captureOptedOut` **缺席**同行,所以
|
|
95
|
+
* `{captureOptedOut:false, optOutSource:"fault"}` 是一个**自相矛盾**的形。首版按「布尔位优先」写,
|
|
96
|
+
* 于是这个形被判成 `active` —— 一个带着故障标记的载荷被读成「记忆确定开着」,正是本模块存在要防的
|
|
97
|
+
* 那件事。矛盾形**不该产出确定判决**:版本斜差 / 畸形 200 体 / 中间层改写都能造出它,而端拿到
|
|
98
|
+
* `active` 之后不会再问第二遍。⇒ 见到 `fault` 一律 `indeterminate`,布尔位说什么都不算数。
|
|
99
|
+
*/
|
|
100
|
+
export declare function readCaptureOptOut(s: SessionMemoryStatus): CaptureOptOutReading;
|
|
101
|
+
export type LastCaptureReading =
|
|
102
|
+
/** 有已提交贡献,且拿到了最新一次的时刻。 */
|
|
103
|
+
{
|
|
104
|
+
readonly state: 'known';
|
|
105
|
+
readonly atMs: number;
|
|
106
|
+
}
|
|
107
|
+
/** **真的一次贡献都没有**(`committedCount === 0` 且 `lastCaptureAt` 缺席)。 */
|
|
108
|
+
| {
|
|
109
|
+
readonly state: 'none';
|
|
110
|
+
}
|
|
111
|
+
/** 台账不可读(`committedCount` 缺席)⇒ `lastCaptureAt` 的缺席推不出任何东西。 */
|
|
112
|
+
| {
|
|
113
|
+
readonly state: 'indeterminate';
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* `lastCaptureAt` 的**三态**读法 —— 本件存在的理由就是这一格:
|
|
117
|
+
* sdk 顶注逐字说 `lastCaptureAt` 缺席**同时**覆盖「台账不可读」与「根本没有已提交贡献」两形,
|
|
118
|
+
* 所以**单读这一键判不出任何东西**。判别材料是**另一键**:`committedCount` 与它同一次台账读 ⇒
|
|
119
|
+
* · `committedCount === 0`(台账可读、真零)+ 本键缺席 ⇒ 真的没有 → `none`;
|
|
120
|
+
* · `committedCount` 缺席(台账不可读)⇒ 推不出 → `indeterminate`;
|
|
121
|
+
* · `committedCount > 0` 却本键缺席 ⇒ 上游说不会发生;**防御性落 `indeterminate`**,
|
|
122
|
+
* 绝不折成 `none`(那会把「有贡献」渲成「从没有过」)。
|
|
123
|
+
*/
|
|
124
|
+
export declare function readLastCapture(s: SessionMemoryStatus): LastCaptureReading;
|
|
125
|
+
/**
|
|
126
|
+
* 读一次会话记忆姿态。**永不抛**:一切抛出物过 {@link classifyMemoryStatusFailure} 成判决。
|
|
127
|
+
*
|
|
128
|
+
* 🔴 `sessionId` 必须是**引擎捕获值**(壳从 wire 上拿到的那个 id),不是宿主自铸/自选的串 ——
|
|
129
|
+
* server 侧记录与台账按**裸 sessionId** 键控且**活过会话**,喂一个被回收的 id 会读到**上一代**
|
|
130
|
+
* 的计数/opt-out(元数据,无内容字节;server 侧成文的跨代注意)。空串在本层直接落
|
|
131
|
+
* `failed` 而不发请求:那一发必然是对某个不属于本会话的东西提问。
|
|
132
|
+
* 🔴 200 体**畸形键不铸**(非 number 的计数 / 非 boolean 的 opt-out 一律降键缺席):降键的后果是
|
|
133
|
+
* 上面两个读法答 `indeterminate`(「不知道」),而**采信**一个坏形的后果是拿它当真值渲 ——
|
|
134
|
+
* 两害相权,如实不知道。
|
|
135
|
+
*/
|
|
136
|
+
export declare function readSessionMemoryStatus(client: MemoryStatusClientLike, sessionId: string, opts?: {
|
|
137
|
+
signal?: AbortSignal;
|
|
138
|
+
}): Promise<MemoryStatusVerdict>;
|