@sema-agent/client-core 0.47.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 +105 -0
- package/README.md +3 -2
- package/dist/adapt/arms.js +10 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +62 -28
- package/dist/hitl/hitlBridge.d.ts +30 -20
- package/dist/hitl/hitlBridge.js +16 -2
- package/dist/hitl/toolApprovalWire.d.ts +20 -0
- package/dist/hitl/toolApprovalWire.js +7 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -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/typePins.d.ts +40 -0
- package/docs/INTEGRATION-CLIENTS.md +208 -21
- package/package.json +4 -4
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>;
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
/**
|
|
22
|
+
* 一次 `memoryStatus` 失败(任意抛出物)→ 处置分型。**永不抛**。
|
|
23
|
+
*
|
|
24
|
+
* 🔴 **结构视图读,不 `instanceof`**(与 `readDecideCurrentPending` 逐字同因):抛出来的是不是 SDK 的
|
|
25
|
+
* `APIError` 由**宿主**决定 —— 跨 realm / 双 SDK 实例下 `instanceof` 会把一个读得懂的错判成读不懂。
|
|
26
|
+
* 🔴 **码优先、status 只作兜底**,且兜底只敢兜 501:
|
|
27
|
+
* · 501 在本路由上无歧义(两条 501 臂都是「面不在/没开」)⇒ 无码时按 `capability` 兜底是安全的;
|
|
28
|
+
* · 404 **恰恰相反**:两个码的处置相反,无码的 404 判不出 ⇒ 落 `failed`(如实说不知道),
|
|
29
|
+
* **绝不**挑一个猜 —— 猜错任一向都是对用户的一句假断言。
|
|
30
|
+
*/
|
|
31
|
+
export function classifyMemoryStatusFailure(e) {
|
|
32
|
+
// 🔴 **取属性本身要设防**(对抗复审 R2 [medium] 采纳,0.48.0):`e` 是**任意抛出物** ——
|
|
33
|
+
// 它完全可以是一个带抛错 getter 的对象或敌意 `Proxy`,那样连 `e.errorCode` 这一下都会抛。
|
|
34
|
+
// 而本函数是被 {@link readSessionMemoryStatus} 在 **`catch` 块里**调用的:`catch` 内抛出的
|
|
35
|
+
// 异常**不会**再被同一个 `try` 接住 ⇒ 分类器一抛,包装器那句「永不抛」当场破功。
|
|
36
|
+
// (R1 那轮把归一化移进 try 只堵了正常返回路,这条是**失败路**上的同一个洞 —— 同形第二处。)
|
|
37
|
+
// 两键**各读一次**存下来,读不动就当「没有可判的机器码」落 `failed`,把原抛出物原样带出。
|
|
38
|
+
let code;
|
|
39
|
+
let status;
|
|
40
|
+
try {
|
|
41
|
+
const o = e;
|
|
42
|
+
const rawCode = o?.errorCode;
|
|
43
|
+
const rawStatus = o?.status;
|
|
44
|
+
code = typeof rawCode === 'string' ? rawCode : undefined;
|
|
45
|
+
status = typeof rawStatus === 'number' ? rawStatus : undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { kind: 'failed', error: e };
|
|
49
|
+
}
|
|
50
|
+
if (code !== undefined) {
|
|
51
|
+
if (code.startsWith('capability.'))
|
|
52
|
+
return { kind: 'unsupported', reason: 'capability' };
|
|
53
|
+
if (code.startsWith('feature.'))
|
|
54
|
+
return { kind: 'unsupported', reason: 'feature' };
|
|
55
|
+
if (code === 'not_found.route')
|
|
56
|
+
return { kind: 'unsupported', reason: 'route' };
|
|
57
|
+
if (code === 'not_found.session')
|
|
58
|
+
return { kind: 'not_found' };
|
|
59
|
+
return { kind: 'failed', error: e };
|
|
60
|
+
}
|
|
61
|
+
if (status === 501)
|
|
62
|
+
return { kind: 'unsupported', reason: 'capability' };
|
|
63
|
+
return { kind: 'failed', error: e };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* `captureOptedOut` × `optOutSource` 的**合读**。两键必须一起读:单读 `captureOptedOut` 的话,
|
|
67
|
+
* 「缺席」既可能是 fault(判不了)也可能是老 server 没给,读成 `false`(capture 开着)是最坏的方向 ——
|
|
68
|
+
* 它会让端向用户断言「你的对话正在被记忆」,而真相是**不知道**。
|
|
69
|
+
*
|
|
70
|
+
* 🔴 **`fault` 一票否决,先于任何布尔位判**(对抗复审 [medium] 采纳,0.48.0):
|
|
71
|
+
* 上游契约里 `optOutSource:"fault"` 与 `captureOptedOut` **缺席**同行,所以
|
|
72
|
+
* `{captureOptedOut:false, optOutSource:"fault"}` 是一个**自相矛盾**的形。首版按「布尔位优先」写,
|
|
73
|
+
* 于是这个形被判成 `active` —— 一个带着故障标记的载荷被读成「记忆确定开着」,正是本模块存在要防的
|
|
74
|
+
* 那件事。矛盾形**不该产出确定判决**:版本斜差 / 畸形 200 体 / 中间层改写都能造出它,而端拿到
|
|
75
|
+
* `active` 之后不会再问第二遍。⇒ 见到 `fault` 一律 `indeterminate`,布尔位说什么都不算数。
|
|
76
|
+
*/
|
|
77
|
+
export function readCaptureOptOut(s) {
|
|
78
|
+
// 🔴 **完整真值表,不是「fault 一票否决 + 看布尔位」**(对抗复审 R2 [medium] 采纳,0.48.0)。
|
|
79
|
+
// 上游契约把这两键**成对**定死,只有两个组合是有定义的:
|
|
80
|
+
// · `true` × `"record"` = 有一条单向 opt-out 记录 ⇒ opted_out
|
|
81
|
+
// · `false` × **缺席** = 记录店可读且无记录(健康默认态)⇒ active
|
|
82
|
+
// · 缺席 × `"fault"` = 记录店失败 ⇒ indeterminate
|
|
83
|
+
// 其余组合(`false`×`record` / `true`×缺席 / `true`×`fault` / …)在契约上**不存在** ——
|
|
84
|
+
// 它们只可能来自版本斜差、畸形 200 体或中间层改写。R1 那轮只挡了带 `fault` 的两格,
|
|
85
|
+
// 于是 `{captureOptedOut:false, optOutSource:"record"}`(记录明明在,布尔位却说没关)
|
|
86
|
+
// 仍被确定判成 `active`,`{captureOptedOut:true}`(没有来源佐证)仍被判成 `opted_out`。
|
|
87
|
+
// 这是**隐私姿态**的断言,方向两边都危险 ⇒ 凡不在真值表上的组合一律 `indeterminate`。
|
|
88
|
+
// ⚠️ 这里**不担心**版本斜差把已知答案降级成「不知道」:五键是同一批(S-53)进的面,
|
|
89
|
+
// 更老的 server 根本没有这条路由(答 404 `not_found.route` ⇒ 走 `unsupported`,到不了这里)。
|
|
90
|
+
if (s.captureOptedOut === true && s.optOutSource === 'record')
|
|
91
|
+
return { state: 'opted_out' };
|
|
92
|
+
if (s.captureOptedOut === false && s.optOutSource === undefined)
|
|
93
|
+
return { state: 'active' };
|
|
94
|
+
return { state: 'indeterminate' };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `lastCaptureAt` 的**三态**读法 —— 本件存在的理由就是这一格:
|
|
98
|
+
* sdk 顶注逐字说 `lastCaptureAt` 缺席**同时**覆盖「台账不可读」与「根本没有已提交贡献」两形,
|
|
99
|
+
* 所以**单读这一键判不出任何东西**。判别材料是**另一键**:`committedCount` 与它同一次台账读 ⇒
|
|
100
|
+
* · `committedCount === 0`(台账可读、真零)+ 本键缺席 ⇒ 真的没有 → `none`;
|
|
101
|
+
* · `committedCount` 缺席(台账不可读)⇒ 推不出 → `indeterminate`;
|
|
102
|
+
* · `committedCount > 0` 却本键缺席 ⇒ 上游说不会发生;**防御性落 `indeterminate`**,
|
|
103
|
+
* 绝不折成 `none`(那会把「有贡献」渲成「从没有过」)。
|
|
104
|
+
*/
|
|
105
|
+
export function readLastCapture(s) {
|
|
106
|
+
// 🔴 `known` 的**合取**条件(对抗复审 [medium] 采纳,0.48.0):时刻在场 **且** 台账可读
|
|
107
|
+
// **且** 真有贡献。首版只判「时刻在场」,于是 `{committedCount:0, lastCaptureAt:X}` 这个
|
|
108
|
+
// **自相矛盾**形(台账明说零贡献,却给得出「最近一次捕获」的时刻;两者本是**同一次台账读**)
|
|
109
|
+
// 被判成 `known` —— 端会据此确定地渲一个时间。同理 `committedCount` 缺席(台账不可读)时,
|
|
110
|
+
// 那个时刻是从哪来的也说不清 ⇒ 一并落 `indeterminate`。
|
|
111
|
+
// 判据锚在「真正决定这件事的量」上:决定「有没有捕获过」的是**台账计数**,时刻只是它的附属。
|
|
112
|
+
if (typeof s.lastCaptureAt === 'number') {
|
|
113
|
+
return typeof s.committedCount === 'number' && s.committedCount > 0
|
|
114
|
+
? { state: 'known', atMs: s.lastCaptureAt }
|
|
115
|
+
: { state: 'indeterminate' };
|
|
116
|
+
}
|
|
117
|
+
if (s.committedCount === 0)
|
|
118
|
+
return { state: 'none' };
|
|
119
|
+
return { state: 'indeterminate' };
|
|
120
|
+
}
|
|
121
|
+
// ── ③ 薄封装(唯一一处 IO) ───────────────────────────────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* 读一次会话记忆姿态。**永不抛**:一切抛出物过 {@link classifyMemoryStatusFailure} 成判决。
|
|
124
|
+
*
|
|
125
|
+
* 🔴 `sessionId` 必须是**引擎捕获值**(壳从 wire 上拿到的那个 id),不是宿主自铸/自选的串 ——
|
|
126
|
+
* server 侧记录与台账按**裸 sessionId** 键控且**活过会话**,喂一个被回收的 id 会读到**上一代**
|
|
127
|
+
* 的计数/opt-out(元数据,无内容字节;server 侧成文的跨代注意)。空串在本层直接落
|
|
128
|
+
* `failed` 而不发请求:那一发必然是对某个不属于本会话的东西提问。
|
|
129
|
+
* 🔴 200 体**畸形键不铸**(非 number 的计数 / 非 boolean 的 opt-out 一律降键缺席):降键的后果是
|
|
130
|
+
* 上面两个读法答 `indeterminate`(「不知道」),而**采信**一个坏形的后果是拿它当真值渲 ——
|
|
131
|
+
* 两害相权,如实不知道。
|
|
132
|
+
*/
|
|
133
|
+
export async function readSessionMemoryStatus(client, sessionId, opts) {
|
|
134
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) {
|
|
135
|
+
return { kind: 'failed', error: new Error('memoryStatus: empty sessionId') };
|
|
136
|
+
}
|
|
137
|
+
// 🔴 **归一化整段在 try 内**(对抗复审 [medium] 采纳,0.48.0):首版只把那一发 `await` 圈进
|
|
138
|
+
// try,归一化在 try 外读属性 —— 而本函数吃的是**宿主注入**的 duck-typed client,它完全可以回一个
|
|
139
|
+
// 带抛错 getter 的对象 / 一个 Proxy。那种载荷会让本函数**向外 reject**,当场毁掉顶注写死的
|
|
140
|
+
// 「**永不抛**」承诺,而所有调用点都是照着那句话写的(没人给它套 try)。
|
|
141
|
+
// 圈进来的代价是零,漏在外面的代价是一个只在畸形宿主上才现形的 unhandled rejection。
|
|
142
|
+
try {
|
|
143
|
+
const raw = await client.sessions.memoryStatus(sessionId, opts?.signal ? { signal: opts.signal } : {});
|
|
144
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
145
|
+
return { kind: 'failed', error: new Error('memoryStatus: malformed body') };
|
|
146
|
+
}
|
|
147
|
+
const o = raw;
|
|
148
|
+
// 🔴 **身份绑定:回声的 sessionId 必须与请求的**逐字相等**,否则整只落 `failed`**
|
|
149
|
+
// (对抗复审 [medium] 采纳)。首版对不相等的回声**照收**,并把**对方**的 id 当成
|
|
150
|
+
// 结果返回 ⇒ 一次缓存错配 / 中间层串台 / 依赖故障,就会把**另一条会话**的记忆元数据
|
|
151
|
+
// (计数、opt-out 姿态)呈现在当前会话的面板上。这条通道的上游对同一件事的纪律是
|
|
152
|
+
// **宁缺席不串台**(server 对缺 `sessionId` 的通告如实不投),本层照同一条办。
|
|
153
|
+
// ⚠️ 契约上 `sessionId` 是**必填回显**(`SessionMemoryStatusResponse`),所以缺席/非串
|
|
154
|
+
// 同样是坏形 —— 一并落 failed,**不**退回「就当是我请求的那个」。
|
|
155
|
+
// 🔴 **「严格相等会不会误杀合法场景」已对真 server 源码验真**(不是推断):server 的
|
|
156
|
+
// `GET /v1/sessions/:id/memory-status` 回显的是 `safeDecode(<路径段>)`,而 SDK 发的是
|
|
157
|
+
// `encodeURIComponent(sessionId)` ⇒ 往返是 `decodeURIComponent(encodeURIComponent(x)) === x`
|
|
158
|
+
// 的恒等,对任何合法串都字节相等;且 200 体里 `sessionId` 是**无条件**铸的(不在条件拷贝
|
|
159
|
+
// 那一组里)。⇒ 严格相等**杀不掉**任何合法回声,缺席也确实只可能是坏形。
|
|
160
|
+
// (坏 percent-encoding 由 server 在更早的门答 400 `request.path_malformed`,落 `failed`。)
|
|
161
|
+
if (typeof o.sessionId !== 'string' || o.sessionId !== sessionId) {
|
|
162
|
+
return { kind: 'failed', error: new Error('memoryStatus: response sessionId mismatch') };
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
kind: 'ok',
|
|
166
|
+
sessionId: o.sessionId,
|
|
167
|
+
facts: {
|
|
168
|
+
...(typeof o.captureOptedOut === 'boolean' ? { captureOptedOut: o.captureOptedOut } : {}),
|
|
169
|
+
...(typeof o.committedCount === 'number' ? { committedCount: o.committedCount } : {}),
|
|
170
|
+
...(typeof o.foldedCount === 'number' ? { foldedCount: o.foldedCount } : {}),
|
|
171
|
+
...(o.optOutSource === 'record' || o.optOutSource === 'fault'
|
|
172
|
+
? { optOutSource: o.optOutSource }
|
|
173
|
+
: {}),
|
|
174
|
+
...(typeof o.lastCaptureAt === 'number' ? { lastCaptureAt: o.lastCaptureAt } : {}),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
return classifyMemoryStatusFailure(e);
|
|
180
|
+
}
|
|
181
|
+
}
|
package/dist/typePins.d.ts
CHANGED
|
@@ -15,3 +15,43 @@
|
|
|
15
15
|
* run-client-core-portability-test.mjs 头注 "①源码级传递闭包…剥 type-only").
|
|
16
16
|
*/
|
|
17
17
|
export type Covers<A, B> = Exclude<A, B> extends never ? true : never;
|
|
18
|
+
/**
|
|
19
|
+
* 🔴 `AllReadonly<T>`(0.48.0,对抗复审 [medium] 采纳后铸的第二把钉)—— 「T 的**每一个**顶层
|
|
20
|
+
* 属性都是 `readonly`」的编译期判据。`true` = 全只读;`false` = 至少有一个可写位。
|
|
21
|
+
*
|
|
22
|
+
* 为什么需要它:把一个自铸形换成上游形的**别名**时,成员名与成员类型都能逐个对上,唯独
|
|
23
|
+
* **修饰符**会静默丢失 —— 而 `readonly` 是已发布的类型契约的一部分。本批实翻:0.47.0 的
|
|
24
|
+
* `GateCurrentPending` 三位全 `readonly`(那三位是 D-1 坐标,契约是**逐字回显、绝不本地重算**),
|
|
25
|
+
* 换成上游 `ApprovalStaleCurrentPending` 的别名之后三位都变成可写 —— 名字、字段、行为门全绿,
|
|
26
|
+
* 只有消费方手上的契约悄悄弱了一档。这类退化**没有任何行为门看得见**(它不改变任何运行期字节),
|
|
27
|
+
* 只有编译期判据抓得住。
|
|
28
|
+
*
|
|
29
|
+
* 实现锚 = TS 的「同一性」惯用法:两个泛型函数签名只有在类型**完全相同**(含修饰符)时才互相
|
|
30
|
+
* 赋值兼容,所以某个位与「剥掉它 readonly 的同一个位」相等 ⇔ 那个位本来就没有 readonly。
|
|
31
|
+
*
|
|
32
|
+
* 🔴 **逐位判,不是整形判**(本批自查命中,首版就是错的):整形写法
|
|
33
|
+
* `IdenticalTo<T, {-readonly [K in keyof T]: T[K]}, false, true>` 只要**有一位**是 readonly 就答
|
|
34
|
+
* `true` —— 它其实是 `SomeReadonly`,名字比它做的事强一档。实测反证:
|
|
35
|
+
* `{readonly a: string; b: number}`(一半只读)在整形写法下答 `true`,于是「三位里有两位被改成可写」
|
|
36
|
+
* 这类**部分退化**能整个溜过去。⇒ 改成把每一位单独拎出来判,全体无可写位才 `true`。
|
|
37
|
+
* (`[X] extends [never]` 的方括号是必须的:裸 `X extends never` 在 X 是联合时会分配,空联合恒真。)
|
|
38
|
+
*
|
|
39
|
+
* 🔴 **守到哪、没守到哪(明写,免得下一棒over-trust 这把钉)**:本判据是**浅层**的 —— 它只判 T 的
|
|
40
|
+
* **顶层**属性(可选位也进得来,`-?` 管这一格)。嵌套对象**内部**的成员是否 readonly、数组元素是否
|
|
41
|
+
* `readonly T[]`,它一概不看,`Readonly<T>` 本身也只做一层。所以它能抓的是「整形/部分位的顶层
|
|
42
|
+
* readonly 退化」,抓不到「某个嵌套成员从 readonly 变可写」。
|
|
43
|
+
* 今天的唯一消费者 `GateCurrentPending` 三位全是**扁平标量**(两 `string` + 一可选 `string`),
|
|
44
|
+
* 浅层判据对它是**完备**的;哪天有人拿它去钉一个带嵌套对象的形,先回来读这一段
|
|
45
|
+
* ——「一道只守住一半却被当成守住全部的门」比没有门更坏([surfacing-is-not-gating] 同族)。
|
|
46
|
+
*/
|
|
47
|
+
type IdenticalTo<X, Y, Yes, No> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? Yes : No;
|
|
48
|
+
/** T 里**可写**位的名字联合(全只读 ⇒ `never`)。`-?` 让可选位也进得来。 */
|
|
49
|
+
type WritableKeys<T> = {
|
|
50
|
+
[P in keyof T]-?: IdenticalTo<{
|
|
51
|
+
[Q in P]: T[P];
|
|
52
|
+
}, {
|
|
53
|
+
-readonly [Q in P]: T[P];
|
|
54
|
+
}, P, never>;
|
|
55
|
+
}[keyof T];
|
|
56
|
+
export type AllReadonly<T> = [WritableKeys<T>] extends [never] ? true : false;
|
|
57
|
+
export {};
|