@sema-agent/client-core 0.75.0 → 0.76.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +7 -2
  3. package/dist/adapt/arms.js +120 -6
  4. package/dist/adapt/panelTasks.d.ts +6 -2
  5. package/dist/adapt/panelTasks.js +6 -3
  6. package/dist/adapter/downstream/eventToSdkMessage.d.ts +45 -0
  7. package/dist/adapter/downstream/eventToSdkMessage.js +121 -6
  8. package/dist/agentSession/backgroundView.js +2 -1
  9. package/dist/agentsWireCaps.d.ts +15 -2
  10. package/dist/engineAgentPanelStore.d.ts +6 -0
  11. package/dist/engineAgentPanelStore.js +113 -10
  12. package/dist/engineErrorCodes.d.ts +4 -0
  13. package/dist/engineErrorCodes.js +14 -0
  14. package/dist/fleet/fleetLedger.js +6 -0
  15. package/dist/fleet/fleetProjection.js +7 -2
  16. package/dist/fleet/fleetRowAgentType.d.ts +9 -0
  17. package/dist/fleet/fleetRowAgentType.js +59 -0
  18. package/dist/fleetAgentPanelProjection.js +24 -3
  19. package/dist/hitl/approvalOutcomeNote.d.ts +0 -10
  20. package/dist/hitl/approvalOutcomeNote.js +35 -8
  21. package/dist/hitl/approvalResolution.d.ts +148 -0
  22. package/dist/hitl/approvalResolution.js +199 -0
  23. package/dist/hitl/approvalsFeed.d.ts +100 -2
  24. package/dist/hitl/approvalsFeed.js +234 -18
  25. package/dist/hitl/livePendingAsk.d.ts +26 -6
  26. package/dist/hitl/livePendingAsk.js +52 -12
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +3 -0
  29. package/dist/memorySpecWire.d.ts +175 -0
  30. package/dist/memorySpecWire.js +320 -0
  31. package/dist/panelRunningHistory.d.ts +21 -0
  32. package/dist/panelRunningHistory.js +25 -0
  33. package/dist/seam.d.ts +64 -5
  34. package/dist/seam.js +10 -1
  35. package/docs/INTEGRATION-CLIENTS.md +112 -9
  36. package/package.json +1 -1
@@ -0,0 +1,148 @@
1
+ /**
2
+ * approvalResolution — 「**一次审批决断最后怎么了**」的单源判别联合({@link ApprovalResolution})
3
+ * 与它的唯一映射口({@link approvalResolutionOf})。
4
+ *
5
+ * ── 这一件修的是什么 ────────────────────────────────────────────────────────────────────────
6
+ * 同一个问题今天有**两种结局形**,而且第二种把三件互斥的事挤进同一个词:
7
+ * · durable-park 腿 {@link import('./toolApprovalWire.js').FsApprovalOutcome} —— 四个 `kind`
8
+ * + `failed.stage` 五词;
9
+ * · 流内帧腿 / 悬挂 ask(盲卡)腿 {@link import('./toolApprovalWire.js').ToolApprovalFrameOutcome}
10
+ * —— `decision` 四词,其中 `'unresolved'` 有 **3 种互斥含义**(宿主撤卡 / 编辑被拒 / respond 没落定),
11
+ * 要再读兄弟布尔位(`retracted` / `editRefused`)才分得开。
12
+ * ⇒ 本模块给出**一个**判别联合,两条腿的每一种结局各落恰一个臂 + 一个 cause 词,`'unresolved'`
13
+ * 的三义各落一臂。消费端从此一次判别拿到答案,不再靠读旁挂布尔位。
14
+ *
15
+ * ── 🔴 本版是 **additive**:既有型面一字未动 ───────────────────────────────────────────────
16
+ * `FsApprovalOutcome` / `ToolApprovalFrameOutcome` / `ApprovalCardDecision` 的键、值、语义
17
+ * **逐字节不变**;本模块只是**读**它们。端可以立刻改读本联合,也可以照旧读原结局对象。
18
+ *
19
+ * ── 三个臂各自答什么(边界写清,别处不许重铸)────────────────────────────────────────────────
20
+ * · `decided` —— 决断**送到了引擎并拿到语义答复**;
21
+ * · `not_sent` —— 包侧**一个字节都没发**(引擎那头的 ask 照旧挂着,`'no_pending'` 这一 cause 例外:
22
+ * 那一行本来就已经不在了);
23
+ * · `unsettled` —— **没落定**。🔴 它**刻意不承诺**「字节已经出站」:见
24
+ * {@link ApprovalUnsettledCause} 的 `'safety_stop'` / `'orchestration'` 两词的头注 ——
25
+ * 那两格在今天的结局对象上**分不出**决断有没有出站,而一个「保证发了」的承诺在这里是编的。
26
+ */
27
+ import type { DecideReceiptView } from '../decideReceipt.js';
28
+ import type { HitlSafetyCode } from './hitlBridge.js';
29
+ import type { FsApprovalOutcome, ToolApprovalFrameOutcome, ToolApprovalRespondAckView, ToolApprovalRespondDecision } from './toolApprovalWire.js';
30
+ /**
31
+ * 「包侧一个字节都没发」的因由闭集(**唯一真源**;端零手抄)。
32
+ * · `'retracted'` —— 宿主撤掉了一张已经没有决断口的卡(两条腿都有这一形)。
33
+ * · `'edit_refused'` —— 入参不可得的卡(悬挂 ask)上收到「编辑后批准」:丢掉改写再批准、或把改写
34
+ * 转发出去,两条都不是人按下的那个决定 ⇒ 整次不发。
35
+ * · `'no_pending'` —— 取件那一拍队列里没有本 run 的可决行(结构化判别位,不是文案猜的)。
36
+ * · `'fetch_failed'` —— 取件这一步**自己**失败了(与上一词结构可分:那是「没有行」,这是「问不出来」)。
37
+ * · `'no_input'` —— 行上没有可呈现的工具入参,卡渲不出来。
38
+ * · `'card_unavailable'` —— 卡口那一段失败(没装卡口 / 卡口自报失败)。
39
+ */
40
+ export declare const APPROVAL_NOT_SENT_CAUSES: readonly ["retracted", "edit_refused", "no_pending", "fetch_failed", "no_input", "card_unavailable"];
41
+ /** {@link APPROVAL_NOT_SENT_CAUSES} 的成员型。 */
42
+ export type ApprovalNotSentCause = (typeof APPROVAL_NOT_SENT_CAUSES)[number];
43
+ /**
44
+ * 「没落定」的因由闭集(**唯一真源**;端零手抄)。
45
+ * · `'engine_refused'` —— 决断发出去了,引擎**真的答复并拒了**(卡通常仍 pending、决定没被消费)。
46
+ * 🔴 **只在有肯定证据时才给这个词**:结局对象上带着引擎的 wire 机器码,或带着引擎在拒体上铸的指路键。
47
+ * 没有证据的 decide 失败落 `'decide_failed'` —— 见下一词。
48
+ * · `'decide_failed'` —— decide 这一段失败了,而结局对象上**没有任何「引擎答复过」的证据**:
49
+ * 调用方中止(人按了 Esc,出站前就抛)、抛出物身上根本没有码、老引擎的无码失败,三形同落这里。
50
+ * 🔴 **不许**据此说「引擎拒了」:那三形里有两形连字节都没出去。要不要重呈看别的位,不看这个词。
51
+ * · `'transport_exhausted'` —— 出站瞬断类失败重试后仍未送达:**两发都没拿到语义答复**。
52
+ * · `'safety_stop'` —— 本包的安全停闭集({@link HitlSafetyCode})命中 ⇒ 要重新呈现给人,**绝不自动重发**。
53
+ * 🔴 **这一格不承诺字节已出站**:`'binding_mismatch'` 既可能是本地绑定守卫在发送**前**拦下的,
54
+ * 也可能是引擎 409 答复重抛的 —— 今天的结局对象上这两个出处**不可分**,所以这里只说「没落定」。
55
+ * · `'respond_failed'` —— 流内帧腿的 respond 抛错:这次决断没落定,**引擎按 TTL / abort 自决**。
56
+ * · `'interrupted'` —— turn 被中断:包侧**发起**了一次工具级拒绝来结算挂着的 ask,但那一发是不观察结果的,
57
+ * 而且它自己也可能在出站前本地失败(绑定键缺失一类)。⇒ 既不能说落定,也**不能**说「一个字节都没发」。
58
+ * 🔴 它**不是** `not_sent`:`not_sent` 是「零字节」的承诺,这一格给不出那个承诺。
59
+ * · `'orchestration'` —— 重挂环在硬上限 / 连续零进展上收口:那不属于决断链的任何一段,
60
+ * 结局对象上**分不出**决断有没有出站。
61
+ * · `'unreadable'` —— 这个结局对象读不出来(不是对象 / 两个判别位都不成词 / 段词不认识)。
62
+ * 🔴 读不出**绝不猜 `decided`**,也不敢说「零字节」——两个方向猜错的代价都是替人说一句他没说的话。
63
+ */
64
+ export declare const APPROVAL_UNSETTLED_CAUSES: readonly ["engine_refused", "decide_failed", "transport_exhausted", "safety_stop", "respond_failed", "interrupted", "orchestration", "unreadable"];
65
+ /** {@link APPROVAL_UNSETTLED_CAUSES} 的成员型。 */
66
+ export type ApprovalUnsettledCause = (typeof APPROVAL_UNSETTLED_CAUSES)[number];
67
+ /** 决断送到了引擎并拿到语义答复。 */
68
+ export interface ApprovalResolutionDecided {
69
+ kind: 'decided';
70
+ /**
71
+ * 落定的决断词,**原样透传**。
72
+ * 🔴 **不在这里复校闭集**:帧腿的词已经在它自己的发送段窄化过,边界上再校一遍等于装第二个判官
73
+ * (一个表外的词会被降成「读不出」,而它其实是引擎收下的那个决断)。
74
+ * 🔴 durable-park 腿只可能给 `'allow'` / `'deny'`:那一腿的结局对象上**没有**「remember 送成了」
75
+ * 这一位,所以这里**不铸** `'allow_session'`(铸一个就是替引擎说一句它没说的话)。
76
+ * 🔴 流内帧腿上 **fail-closed 的拒绝与人按的拒绝不可分**(卡口的 `aborted` / `failed` 与 `deny`
77
+ * 在发送段折成同一个词)—— 今天的真相如实说,本联合不发明新位。
78
+ */
79
+ decision: ToolApprovalRespondDecision;
80
+ /** 待批 call 身份(durable-park 腿在场;帧腿没有这一位 —— 身份在帧的 `approvalId` 上,**不是**「没有身份」)。 */
81
+ gatedCallId?: string;
82
+ /** decide 的 200 回执读数(缺席 = 老 server / 终局形 / 回体读不动,三形同缺);🔴 它**不是**「门已解决」的证据。 */
83
+ receipt?: DecideReceiptView;
84
+ /** respond 的 200 ack(缺席 = **未知**:注入面回 void / 旧 server / 相关性门丢弃,绝不当成 false)。 */
85
+ ack?: ToolApprovalRespondAckView;
86
+ }
87
+ /** 包侧一个字节都没发。 */
88
+ export interface ApprovalResolutionNotSent {
89
+ kind: 'not_sent';
90
+ cause: ApprovalNotSentCause;
91
+ gatedCallId?: string;
92
+ /** 本包铸的诊断文案(原样;读不出 / 空串 ⇒ 键缺席,绝不编一句)。 */
93
+ detail?: string;
94
+ }
95
+ /** 没落定(见模块顶注:**不承诺**字节已出站)。 */
96
+ export interface ApprovalResolutionUnsettled {
97
+ kind: 'unsettled';
98
+ cause: ApprovalUnsettledCause;
99
+ gatedCallId?: string;
100
+ /** 本包的安全停判别位(`'safety_stop'` 这一 cause 上在场)。 */
101
+ safetyCode?: HitlSafetyCode;
102
+ /** 引擎的 wire 机器码(开集,原样;缺席**不带语义** —— 缺席 ≠「不是引擎拒的」)。 */
103
+ errorCode?: string;
104
+ /** 应答的 HTTP 状态(在场 = 引擎真应答了;缺席 = 传输层失败 / 注入面自抛,**不许**反推成 0)。 */
105
+ status?: number;
106
+ /** 可呈现的原文(帧腿 = 引擎的拒句原文;durable 腿 = 本包铸的诊断文案)。读不出 ⇒ 键缺席。 */
107
+ detail?: string;
108
+ }
109
+ /**
110
+ * 一次审批决断的**结局**(判别联合;三臂穷举,`'unresolved'` 的三义各落一臂)。
111
+ *
112
+ * 与旧读法的对照(消费端迁移判据一句话):
113
+ * · `out.decision !== 'unresolved'`(帧腿)⇒ `approvalResolutionOf(out).kind === 'decided'`;
114
+ * · `out.retracted === true` ⇒ `approvalNotSentCause(r) === 'retracted'`;
115
+ * · `out.editRefused === true` ⇒ `approvalNotSentCause(r) === 'edit_refused'`;
116
+ * · 裸 `'unresolved'` ⇒ `approvalUnsettledCause(r) === 'respond_failed'`。
117
+ *
118
+ * 🔴 **不并表的位**:指路键(`currentPending`)、ack 的逐位解释、receipt 的逐位语义照旧读原结局
119
+ * 对象 —— 本联合只答「怎么了」这一个问题。
120
+ */
121
+ export type ApprovalResolution = ApprovalResolutionDecided | ApprovalResolutionNotSent | ApprovalResolutionUnsettled;
122
+ /**
123
+ * {@link approvalResolutionOf} 认的入参:两条腿今天的结局对象。
124
+ * 🔴 ask / plan-review 腿的 `GateOutcome` **刻意不在**这个联合里(它多一个 `answered` 位与一张
125
+ * 另外的失败码闭集),plan-review 的决断生效读数也不在(它自己是另一张六词表)。
126
+ * ⚠️ **这是分工边界,不是一道编译期围栏**,如实说:只有 `failed` 那一臂因为失败码闭集不同而赋不进来;
127
+ * `decided` / `aborted` 两臂在结构上仍然兼容,硬喂进来会被按本腿的口径读(`answered` 被忽略、
128
+ * 别的码表词读不出判别位)。把别的腿的结局喂进本口是调用方的错,本函数不替它兜。
129
+ */
130
+ export type ApprovalLegOutcome = FsApprovalOutcome | ToolApprovalFrameOutcome;
131
+ /**
132
+ * 今天各腿的结局对象 → {@link ApprovalResolution}。**纯函数,永不抛,永不造**。
133
+ *
134
+ * 判别序(固定,不随键序变):
135
+ * ① `kind` 是 durable-park 腿的闭四词 ⇒ 按那一腿读;
136
+ * ② 否则决断词是串 ⇒ 按流内帧腿 / 盲卡腿读;
137
+ * ③ 都不成立 ⇒ `unsettled{cause:'unreadable'}`(🔴 不猜 `decided`,也不说「零字节」)。
138
+ *
139
+ * 🔴 **旁挂位在场压过决断词**(帧腿):撤卡 / 编辑被拒这两形是「一个字节都没发」,而它们今天与
140
+ * `decision` 同在一只对象上;先读决断词就会把一张没发出去的卡说成「批过了」。
141
+ */
142
+ export declare function approvalResolutionOf(outcome: ApprovalLegOutcome): ApprovalResolution;
143
+ /** 这次决断**落定了吗**(唯一判据;`true` ⇒ 决断送到并拿到语义答复)。坏形入参 ⇒ `false`。 */
144
+ export declare function isApprovalDecided(r: ApprovalResolution): r is ApprovalResolutionDecided;
145
+ /** 「一个字节都没发」的因由;不是那一臂 ⇒ `undefined`(**不是**「发了」的证据,只是「不是这一臂」)。 */
146
+ export declare function approvalNotSentCause(r: ApprovalResolution): ApprovalNotSentCause | undefined;
147
+ /** 「没落定」的因由;不是那一臂 ⇒ `undefined`。 */
148
+ export declare function approvalUnsettledCause(r: ApprovalResolution): ApprovalUnsettledCause | undefined;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * 「包侧一个字节都没发」的因由闭集(**唯一真源**;端零手抄)。
3
+ * · `'retracted'` —— 宿主撤掉了一张已经没有决断口的卡(两条腿都有这一形)。
4
+ * · `'edit_refused'` —— 入参不可得的卡(悬挂 ask)上收到「编辑后批准」:丢掉改写再批准、或把改写
5
+ * 转发出去,两条都不是人按下的那个决定 ⇒ 整次不发。
6
+ * · `'no_pending'` —— 取件那一拍队列里没有本 run 的可决行(结构化判别位,不是文案猜的)。
7
+ * · `'fetch_failed'` —— 取件这一步**自己**失败了(与上一词结构可分:那是「没有行」,这是「问不出来」)。
8
+ * · `'no_input'` —— 行上没有可呈现的工具入参,卡渲不出来。
9
+ * · `'card_unavailable'` —— 卡口那一段失败(没装卡口 / 卡口自报失败)。
10
+ */
11
+ export const APPROVAL_NOT_SENT_CAUSES = Object.freeze([
12
+ 'retracted',
13
+ 'edit_refused',
14
+ 'no_pending',
15
+ 'fetch_failed',
16
+ 'no_input',
17
+ 'card_unavailable',
18
+ ]);
19
+ /**
20
+ * 「没落定」的因由闭集(**唯一真源**;端零手抄)。
21
+ * · `'engine_refused'` —— 决断发出去了,引擎**真的答复并拒了**(卡通常仍 pending、决定没被消费)。
22
+ * 🔴 **只在有肯定证据时才给这个词**:结局对象上带着引擎的 wire 机器码,或带着引擎在拒体上铸的指路键。
23
+ * 没有证据的 decide 失败落 `'decide_failed'` —— 见下一词。
24
+ * · `'decide_failed'` —— decide 这一段失败了,而结局对象上**没有任何「引擎答复过」的证据**:
25
+ * 调用方中止(人按了 Esc,出站前就抛)、抛出物身上根本没有码、老引擎的无码失败,三形同落这里。
26
+ * 🔴 **不许**据此说「引擎拒了」:那三形里有两形连字节都没出去。要不要重呈看别的位,不看这个词。
27
+ * · `'transport_exhausted'` —— 出站瞬断类失败重试后仍未送达:**两发都没拿到语义答复**。
28
+ * · `'safety_stop'` —— 本包的安全停闭集({@link HitlSafetyCode})命中 ⇒ 要重新呈现给人,**绝不自动重发**。
29
+ * 🔴 **这一格不承诺字节已出站**:`'binding_mismatch'` 既可能是本地绑定守卫在发送**前**拦下的,
30
+ * 也可能是引擎 409 答复重抛的 —— 今天的结局对象上这两个出处**不可分**,所以这里只说「没落定」。
31
+ * · `'respond_failed'` —— 流内帧腿的 respond 抛错:这次决断没落定,**引擎按 TTL / abort 自决**。
32
+ * · `'interrupted'` —— turn 被中断:包侧**发起**了一次工具级拒绝来结算挂着的 ask,但那一发是不观察结果的,
33
+ * 而且它自己也可能在出站前本地失败(绑定键缺失一类)。⇒ 既不能说落定,也**不能**说「一个字节都没发」。
34
+ * 🔴 它**不是** `not_sent`:`not_sent` 是「零字节」的承诺,这一格给不出那个承诺。
35
+ * · `'orchestration'` —— 重挂环在硬上限 / 连续零进展上收口:那不属于决断链的任何一段,
36
+ * 结局对象上**分不出**决断有没有出站。
37
+ * · `'unreadable'` —— 这个结局对象读不出来(不是对象 / 两个判别位都不成词 / 段词不认识)。
38
+ * 🔴 读不出**绝不猜 `decided`**,也不敢说「零字节」——两个方向猜错的代价都是替人说一句他没说的话。
39
+ */
40
+ export const APPROVAL_UNSETTLED_CAUSES = Object.freeze([
41
+ 'engine_refused',
42
+ 'decide_failed',
43
+ 'transport_exhausted',
44
+ 'safety_stop',
45
+ 'respond_failed',
46
+ 'interrupted',
47
+ 'orchestration',
48
+ 'unreadable',
49
+ ]);
50
+ /** durable-park 腿的 `kind` 闭四词(判别序的第一把;不是这四个词才去看决断词)。 */
51
+ const FS_OUTCOME_KINDS = Object.freeze(['decided', 'aborted', 'retracted', 'failed']);
52
+ /** 非空串窄读(与本包其余读面同一条:空串 = 没这一格)。 */
53
+ function str(v) {
54
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
55
+ }
56
+ /** 有限数窄读(在场 = 真答了;非有限数一律当缺席,**不许**反推成 0)。 */
57
+ function num(v) {
58
+ return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
59
+ }
60
+ /** 一只可能来自宿主 / 注入面的记录:读属性本身就可能抛(getter 钩子),逐位各自兜住。 */
61
+ function pick(o, key) {
62
+ try {
63
+ return o?.[key];
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
69
+ /** `{...(x !== undefined ? {k:x} : {})}` 的一处写法收口:缺席**不落键**(不折成空串 / 0 / false)。 */
70
+ function opt(key, v) {
71
+ return v === undefined ? {} : { [key]: v };
72
+ }
73
+ /**
74
+ * 今天各腿的结局对象 → {@link ApprovalResolution}。**纯函数,永不抛,永不造**。
75
+ *
76
+ * 判别序(固定,不随键序变):
77
+ * ① `kind` 是 durable-park 腿的闭四词 ⇒ 按那一腿读;
78
+ * ② 否则决断词是串 ⇒ 按流内帧腿 / 盲卡腿读;
79
+ * ③ 都不成立 ⇒ `unsettled{cause:'unreadable'}`(🔴 不猜 `decided`,也不说「零字节」)。
80
+ *
81
+ * 🔴 **旁挂位在场压过决断词**(帧腿):撤卡 / 编辑被拒这两形是「一个字节都没发」,而它们今天与
82
+ * `decision` 同在一只对象上;先读决断词就会把一张没发出去的卡说成「批过了」。
83
+ */
84
+ export function approvalResolutionOf(outcome) {
85
+ if (typeof outcome !== 'object' || outcome === null || Array.isArray(outcome)) {
86
+ return { kind: 'unsettled', cause: 'unreadable' };
87
+ }
88
+ const kind = pick(outcome, 'kind');
89
+ if (typeof kind === 'string' && FS_OUTCOME_KINDS.includes(kind)) {
90
+ return fsLegResolution(outcome, kind);
91
+ }
92
+ if (typeof pick(outcome, 'decision') === 'string') {
93
+ return frameLegResolution(outcome);
94
+ }
95
+ return { kind: 'unsettled', cause: 'unreadable' };
96
+ }
97
+ /** durable-park 腿(`surfaceFsApprovalAndDecide`)的十二形。 */
98
+ function fsLegResolution(outcome, kind) {
99
+ const gatedCallId = str(pick(outcome, 'gatedCallId'));
100
+ const call = opt('gatedCallId', gatedCallId);
101
+ if (kind === 'decided') {
102
+ // `denied` 在场即真(缺席 = 放行);`'allow_session'` 在这一腿上无位可读 ⇒ 不铸(见 decision 位顶注)。
103
+ return {
104
+ kind: 'decided',
105
+ decision: pick(outcome, 'denied') === true ? 'deny' : 'allow',
106
+ ...call,
107
+ ...opt('receipt', pick(outcome, 'receipt')),
108
+ };
109
+ }
110
+ if (kind === 'retracted')
111
+ return { kind: 'not_sent', cause: 'retracted', ...call };
112
+ // 🔴 中断臂**发起**一次工具级拒绝(不观察结果,也不保证真出了站)⇒ 给不出 `not_sent` 的零字节承诺。
113
+ if (kind === 'aborted')
114
+ return { kind: 'unsettled', cause: 'interrupted', ...call };
115
+ // 剩下的是 `failed`:**停在链条哪一段**决定了「有没有发出去」——
116
+ // 取件 / 入参 / 卡口三段一个字节都没送出,只有 decide 那一段代表决断已经离开本包。
117
+ const detail = opt('detail', str(pick(outcome, 'reason')));
118
+ const stage = pick(outcome, 'stage');
119
+ if (stage === 'fetch') {
120
+ // 取件腿的结构化判别位:「这一拍没有可决行」与「问不出来」结构可分(不靠文案子串猜)。
121
+ const cause = pick(outcome, 'code') === 'no_pending' ? 'no_pending' : 'fetch_failed';
122
+ return { kind: 'not_sent', cause, ...call, ...detail };
123
+ }
124
+ if (stage === 'input')
125
+ return { kind: 'not_sent', cause: 'no_input', ...call, ...detail };
126
+ if (stage === 'card')
127
+ return { kind: 'not_sent', cause: 'card_unavailable', ...call, ...detail };
128
+ if (stage === 'decide') {
129
+ const safetyCode = str(pick(outcome, 'safetyCode'));
130
+ const errorCode = str(pick(outcome, 'errorCode'));
131
+ // 🔴 **「引擎答复过」必须有肯定证据**:wire 机器码在场,或引擎在拒体上铸的指路键在场。
132
+ // 没有证据的 decide 失败(调用方中止 ⇒ 出站前原错上抛 / 抛出物身上没有码 / 老引擎无码失败)
133
+ // 一律落 `'decide_failed'` —— 把它们说成「引擎拒了」是替引擎编了一句它没说过的话,
134
+ // 而消费端会照这句话去渲拒绝原因、去决定还要不要重呈。
135
+ const engineAnswered = errorCode !== undefined || pick(outcome, 'currentPending') !== undefined;
136
+ // 优先序固定:安全停 > 瞬断耗尽 > 引擎语义拒绝 > 无证据的 decide 失败。前两者在实装里互斥
137
+ // (两个抛出物类型),写死优先序是为了「同场也只有一个答案」,不让键序决定结局。
138
+ const cause = safetyCode !== undefined
139
+ ? 'safety_stop'
140
+ : pick(outcome, 'retryExhausted') === true
141
+ ? 'transport_exhausted'
142
+ : engineAnswered
143
+ ? 'engine_refused'
144
+ : 'decide_failed';
145
+ return {
146
+ kind: 'unsettled',
147
+ cause,
148
+ ...call,
149
+ ...opt('safetyCode', safetyCode),
150
+ ...opt('errorCode', errorCode),
151
+ ...detail,
152
+ };
153
+ }
154
+ if (stage === 'orchestration')
155
+ return { kind: 'unsettled', cause: 'orchestration', ...call, ...detail };
156
+ // 段词读不出 ⇒ 分不出停在哪:**不许**说「零字节」(那是 not_sent 的承诺),只说没落定。
157
+ return { kind: 'unsettled', cause: 'unreadable', ...call, ...detail };
158
+ }
159
+ /** 流内帧腿 / 悬挂 ask(盲卡)腿(`surfaceToolApprovalFrameAndRespond`)的四形。 */
160
+ function frameLegResolution(outcome) {
161
+ // 🔴 两个旁挂位先判(见 approvalResolutionOf 顶注):它们说的是「一个字节都没发」。
162
+ // 两位同在 ⇒ 撤卡胜 —— 与结局便签的 detail 优先序同一条,不是另铸一份。
163
+ if (pick(outcome, 'retracted') === true)
164
+ return { kind: 'not_sent', cause: 'retracted' };
165
+ if (pick(outcome, 'editRefused') === true)
166
+ return { kind: 'not_sent', cause: 'edit_refused' };
167
+ if (pick(outcome, 'decision') === 'unresolved') {
168
+ const refusal = pick(outcome, 'respondRefusal');
169
+ return {
170
+ kind: 'unsettled',
171
+ cause: 'respond_failed',
172
+ ...opt('status', num(pick(refusal, 'status'))),
173
+ ...opt('errorCode', str(pick(refusal, 'errorCode'))),
174
+ ...opt('detail', str(pick(refusal, 'message'))),
175
+ };
176
+ }
177
+ const ack = pick(outcome, 'ack');
178
+ return {
179
+ kind: 'decided',
180
+ decision: pick(outcome, 'decision'),
181
+ ...(typeof ack === 'object' && ack !== null ? { ack: ack } : {}),
182
+ };
183
+ }
184
+ /** 这次决断**落定了吗**(唯一判据;`true` ⇒ 决断送到并拿到语义答复)。坏形入参 ⇒ `false`。 */
185
+ export function isApprovalDecided(r) {
186
+ return typeof r === 'object' && r !== null && r.kind === 'decided';
187
+ }
188
+ /** 「一个字节都没发」的因由;不是那一臂 ⇒ `undefined`(**不是**「发了」的证据,只是「不是这一臂」)。 */
189
+ export function approvalNotSentCause(r) {
190
+ if (typeof r !== 'object' || r === null || r.kind !== 'not_sent')
191
+ return undefined;
192
+ return r.cause;
193
+ }
194
+ /** 「没落定」的因由;不是那一臂 ⇒ `undefined`。 */
195
+ export function approvalUnsettledCause(r) {
196
+ if (typeof r !== 'object' || r === null || r.kind !== 'unsettled')
197
+ return undefined;
198
+ return r.cause;
199
+ }
@@ -42,6 +42,12 @@
42
42
  * 「一条快照都没有」既可能是「真的没有 pending」,也可能是「两条腿都挂了」。所以 feed 暴露
43
43
  * `stats()`(pushEvents / polls / listErrors / streamFailures / snapshots)与 `mode()`,
44
44
  * 端的自检可以断言「跑了 N 秒之后 mode 不是 idle 且 polls+pushEvents > 0」。
45
+ *
46
+ * 🔴 **CC-98 三态**:上面那两个量是**自检**面(端自己查健康),不是**渲染**面。渲染面要的是一句话 ——
47
+ * 「有 N 条」/「一条都没有」/「这次取不到,不知道有没有」。第三句此前在订阅口上根本不存在:
48
+ * `list()` 抛 / 回体读不懂 ⇒ 只 `listErrors++` 并静默返回 ⇒ 端手上停着上一张快照,它说 0 就一直
49
+ * 显示 0。⇒ 订阅口现在发**两臂**({@link ApprovalsFeedEmission}),另有拉面三态 `reading()`。
50
+ * 退避 / 重试 / 熔断语义**不变**:发了「不知道」照旧重试,取到就发真快照(含「内容与失败前一字不差」那一形)。
45
51
  */
46
52
  import type { PendingCheckpoint } from '@sema-agent/sdk';
47
53
  import { type LivePendingAskView } from './livePendingAsk.js';
@@ -64,6 +70,12 @@ export interface ApprovalsFeedClientLike {
64
70
  }
65
71
  export type ApprovalsFeedMode = 'push' | 'poll' | 'idle';
66
72
  export interface ApprovalsFeedSnapshot {
73
+ /**
74
+ * CC-98 判别位。订阅口发的**不只有**真快照 —— 取件没看成时发
75
+ * {@link ApprovalsFeedUnknownSnapshot}(`kind:'unknown'`)。
76
+ * 🔴 消费者先看这一位再读别的键:把「取不到」当成「一条都没有」是本包的头号病形。
77
+ */
78
+ kind: 'snapshot';
67
79
  /** 权威 pending 列表(**恒来自 `list()`**,不是 delta 拼出来的)。 */
68
80
  pending: PendingCheckpoint[];
69
81
  /**
@@ -71,11 +83,77 @@ export interface ApprovalsFeedSnapshot {
71
83
  * 🔴 **键缺席 = 引擎没报这一段**(老引擎),不是「没有」;引擎报了且为空 ⇒ `[]`。读法见 `livePendingAsk.ts`。
72
84
  */
73
85
  livePending?: LivePendingAskView[];
86
+ /**
87
+ * 🔴 CC-98(异源复审 R1 [high] 采修):这一段里**读不出来的行数**(>0 才在场)。
88
+ *
89
+ * 为什么必须上快照:读器对坏行是「逐条丢 + 计数」,丢掉的那几行**引擎照旧在报**。只把读得懂的行交出去
90
+ * 而不交这个数,消费端就会拿一份**自己不知道不完整**的列表去做两件不许做的事:
91
+ * ① 报精确总数(「0 条在等你」而其实有一条读不懂的在等人);
92
+ * ② **缺席对账**(「这一行不在列表里了 ⇒ 撤卡」,而它只是这一拍读不懂)。
93
+ * ⇒ 在场时:{@link countApprovalsAwaitingDecision} 的流内两格答 `null`(不知道确切数),
94
+ * {@link createSuspendedAskTracker} 的 `ingest` **不报 `gone`**(照报 `appeared` / `upgraded`)。
95
+ * 缺席 = 这一段**完整**(每一行都读出来了)—— 这时「一条都没有」才是一句可以说的话。
96
+ */
97
+ livePendingDropped?: number;
98
+ /**
99
+ * 🔴 CC-98(异源复审 R2 [medium] 采修):`pending` 段里**读不出来的行数**(>0 才在场)。
100
+ * 与 `livePendingDropped` 同一条纪律:在场 ⇒ 这一段**不完整** ⇒ {@link countApprovalsAwaitingDecision}
101
+ * 的 `durable` 格答 `null`(不知道确切数),缺席 = 每一行都读出来了。
102
+ */
103
+ pendingDropped?: number;
74
104
  /** 这次快照是被哪条腿触发的(对账节拍触发的取件记 `'poll'`:它就是一次定时 `list()`)。 */
75
105
  mode: 'push' | 'poll';
76
106
  /** 单调递增的修订号(端可用它判「我看到的是不是最新的」)。 */
77
107
  revision: number;
78
108
  }
109
+ /**
110
+ * CC-98:一次取件**没看成**的两个成因(闭集,不设「其它」)。
111
+ * · `list_threw` —— 取件本身抛出 / 返回 reject 的 promise(手上根本没有回体:无端点、传输层炸、上游 5xx …);
112
+ * · `unreadable_payload` —— 有回体但读不懂(回体不是对象 / `livePending` 键在场却不是数组 / `pending` 不是数组或行畸形)。
113
+ * 两者刻意不合并:前者是「没拿到」,后者是「拿到了但不敢信」,宿主的提示文案与自检口径都不同。
114
+ */
115
+ export type ApprovalsFeedUnknownWhy = 'list_threw' | 'unreadable_payload';
116
+ /**
117
+ * 🔴 CC-98 **「这次取不到,不知道有没有」** —— 订阅口的第二臂,与真快照并列的一句话。
118
+ *
119
+ * 为什么必须有这一臂(而不是「不发」、更不是「发一张空的」):`list()` 失败 / 回体读不懂时,此前本 feed
120
+ * **一张都不发** ⇒ 端手上停着上一张快照 —— 它说 0 就一直显示 0(而真相是「不知道」),说 1 就一直显示 1。
121
+ * 「取不到」渲成「没有」是本包的头号病形,所以这一臂让**每个**消费者必须表态:渲破折号 / 渲「暂时看不到」/
122
+ * 显式选择继续显示上一张并标过期 —— 任选其一,但不许当成 0。
123
+ *
124
+ * 🔴 本臂**不带** `pending` / `livePending` / `revision`:那三样都是「视图」,而本臂说的正是「视图现在不可信」。
125
+ * 要上一张真快照(**过去**的事实)问 `snapshot()`;要「现在可不可信」问 `reading()`。
126
+ */
127
+ export interface ApprovalsFeedUnknownSnapshot {
128
+ kind: 'unknown';
129
+ /** 成因(见 {@link ApprovalsFeedUnknownWhy})。 */
130
+ why: ApprovalsFeedUnknownWhy;
131
+ /**
132
+ * 这一段「不知道」的**起点**(epoch ms)。同一成因连着失败不重复发臂(与「内容没变不发快照」同一条纪律),
133
+ * 所以本位不随每次失败前移 —— 端可以拿它答「已经盲了多久」。
134
+ */
135
+ at: number;
136
+ /** 在哪条腿上取不到(与真快照的 `mode` 同义)。 */
137
+ mode: 'push' | 'poll';
138
+ }
139
+ /** 订阅口发的东西(两臂)。🔴 先看 `kind`,再读别的键。 */
140
+ export type ApprovalsFeedEmission = ApprovalsFeedSnapshot | ApprovalsFeedUnknownSnapshot;
141
+ /**
142
+ * CC-98 **拉面三态**(与本包能力位族的读数同一套词)。端在任何时刻问「我现在该渲什么」:
143
+ * · `unobserved` —— 本 feed 还没有过任何结果(首次取件还没回来)。**不是**「没有 pending」。
144
+ * · `present` —— 手上这张快照就是最近一次取件的结论(「一条都没有」只在这一态且列表为空时成立)。
145
+ * · `unknown` —— 最近一次取件没看成(`why` / `at` 与最近发出的 {@link ApprovalsFeedUnknownSnapshot} 同源)。
146
+ */
147
+ export type ApprovalsFeedReading = {
148
+ kind: 'unobserved';
149
+ } | {
150
+ kind: 'present';
151
+ snapshot: ApprovalsFeedSnapshot;
152
+ } | {
153
+ kind: 'unknown';
154
+ why: ApprovalsFeedUnknownWhy;
155
+ at: number;
156
+ };
79
157
  export interface ApprovalsFeedStats {
80
158
  /** 收到的**非 heartbeat** 推送事件数。 */
81
159
  pushEvents: number;
@@ -93,6 +171,12 @@ export interface ApprovalsFeedStats {
93
171
  reconciles: number;
94
172
  /** 0.73.4:「欠一次看」的重试取件次数(见 `ApprovalsFeedOptions.pushTakeRetryMs`)。 */
95
173
  pushTakeRetries: number;
174
+ /**
175
+ * CC-98:真发给订阅者的「不知道」张数。与 `listErrors` 刻意分开:后者数**失败次数**,本位数
176
+ * **发布次数**(同一段「不知道」只发一张)⇒ `unknowns <= listErrors` 恒成立,端的自检据此
177
+ * 区分「一直在失败」与「失败了但端被通知过几次」。
178
+ */
179
+ unknowns: number;
96
180
  }
97
181
  /**
98
182
  * 0.72.14 **对账节拍**:悬挂的流内 ask 出现 / 结算时,较老的引擎不在 `approvals.stream` 上发任何事件 ⇒ push 腿连着也
@@ -152,13 +236,27 @@ export interface ApprovalsFeedHandle {
152
236
  stop(): void;
153
237
  mode(): ApprovalsFeedMode;
154
238
  stats(): ApprovalsFeedStats;
155
- /** 最近一次快照(还没取过 ⇒ null)。 */
239
+ /**
240
+ * 最近一次**真**快照(还没有过 ⇒ null)。
241
+ * 🔴 CC-98:它是**过去**的事实 —— 取件正失败时它仍是上一张(内容不会被改写成空)。
242
+ * 「现在可不可信」问 {@link ApprovalsFeedHandle.reading}。
243
+ */
156
244
  snapshot(): ApprovalsFeedSnapshot | null;
245
+ /**
246
+ * CC-98 三态:**现在**该渲什么。
247
+ * 🔴 「一条都没有」只在 `present` 且列表为空时成立;`unobserved` / `unknown` 一律渲「不知道」
248
+ * (破折号 / 「暂时看不到」),绝不渲 0。
249
+ */
250
+ reading(): ApprovalsFeedReading;
157
251
  /** 立刻取一次权威 list 并(有变化时)发快照。端在「用户点了刷新」之类的场景用。 */
158
252
  refresh(): Promise<void>;
159
253
  }
160
254
  /**
161
255
  * 起一条 pending-approvals feed。**幂等性归调用方**:一个 client 起一条就够了,起两条 =
162
256
  * 两倍取件(不是错误,但没意义)。
257
+ *
258
+ * 🔴 CC-98 订阅口收**两臂**({@link ApprovalsFeedEmission}):真快照(`kind:'snapshot'`)与
259
+ * 「这次取不到」({@link ApprovalsFeedUnknownSnapshot},`kind:'unknown'`)。先判 `kind` 再读别的键 ——
260
+ * 把 unknown 臂当成一张空列表就是把「不知道」渲成「没有」。三态的拉面读口是 `reading()`。
163
261
  */
164
- export declare function startApprovalsFeed(client: ApprovalsFeedClientLike, onSnapshot: (snap: ApprovalsFeedSnapshot) => void, opts?: ApprovalsFeedOptions): ApprovalsFeedHandle;
262
+ export declare function startApprovalsFeed(client: ApprovalsFeedClientLike, onSnapshot: (emission: ApprovalsFeedEmission) => void, opts?: ApprovalsFeedOptions): ApprovalsFeedHandle;