@sema-agent/client-core 0.67.1 → 0.68.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 (39) hide show
  1. package/CHANGELOG.md +211 -0
  2. package/README.md +67 -3
  3. package/dist/adapt/arms.js +27 -2
  4. package/dist/adapt/turnFlags.d.ts +14 -0
  5. package/dist/adapt/turnFlags.js +4 -1
  6. package/dist/adapter/activeRunSelfHeal.d.ts +53 -6
  7. package/dist/adapter/activeRunSelfHeal.js +79 -8
  8. package/dist/adapter/downstream/eventToSdkMessage.d.ts +18 -1
  9. package/dist/adapter/downstream/eventToSdkMessage.js +33 -2
  10. package/dist/adapter/downstream/terminalToSdkResult.d.ts +75 -6
  11. package/dist/adapter/downstream/terminalToSdkResult.js +144 -44
  12. package/dist/adapter/runStream.d.ts +22 -2
  13. package/dist/adapter/runStream.js +190 -52
  14. package/dist/adapter/types.d.ts +4 -28
  15. package/dist/autoModeUnavailable.d.ts +17 -9
  16. package/dist/autoModeUnavailable.js +26 -8
  17. package/dist/classifierStatus.d.ts +32 -4
  18. package/dist/classifierStatus.js +5 -3
  19. package/dist/engineErrorCodes.d.ts +52 -0
  20. package/dist/engineErrorCodes.js +117 -0
  21. package/dist/engineNoticeCodes.d.ts +95 -1
  22. package/dist/engineNoticeCodes.js +124 -1
  23. package/dist/gateVocabulary.d.ts +18 -7
  24. package/dist/gateVocabulary.js +21 -8
  25. package/dist/hitl/parkResolver.d.ts +0 -14
  26. package/dist/hitl/parkResolver.js +22 -9
  27. package/dist/hitl/toolApprovalWire.d.ts +2 -1
  28. package/dist/hitl/toolApprovalWire.js +1 -0
  29. package/dist/ownKey.d.ts +34 -0
  30. package/dist/ownKey.js +36 -0
  31. package/dist/retryStatus.d.ts +13 -2
  32. package/dist/retryStatus.js +4 -1
  33. package/dist/runTerminal.d.ts +87 -14
  34. package/dist/runTerminal.js +89 -15
  35. package/dist/toolResult.js +8 -0
  36. package/dist/workflowClient.d.ts +22 -0
  37. package/dist/workflowClient.js +37 -0
  38. package/docs/INTEGRATION-CLIENTS.md +469 -10
  39. package/package.json +2 -2
@@ -1,3 +1,18 @@
1
+ /**
2
+ * parkResolver — HITL gate 桥**外环**的 park 决断(REF-CC-032 / SPLIT-12 第三刀,2026-08-02)。
3
+ *
4
+ * 内环(`frameRouter.ts`)带出一个 `GatePark` 之后,这里回答唯一一个问题:**接着读流,还是
5
+ * fail-soft 收场**。三条出路:
6
+ * · 决断成功 ⇒ `reattach`(驱动 attach `runs.events`,`lastEventId` 续传);
7
+ * · 「这个 gate 早就被解决了」(durable re-attach 必然重放已决断的 park)⇒ 同样 `reattach`;
8
+ * · 其余(overlay 缺席 / pending 蒸发 / decide 409/404 / turn 中断 / hop 预算耗尽)⇒ `failsoft`:
9
+ * flush HOLD 的毒化帧 + 原样吐 suspended 终帧(durable 腿无终帧则合成一条可见的 failed)。
10
+ *
11
+ * 🔴 本文件不碰流、不 yield:events 以数组交回驱动。`resolvePark` 的**返回值**就是它的全部对外
12
+ * 效果(台账写入除外,且台账写入全经 `GateLedger` 的动词)—— 于是「禁掉某条真臂」这种变异
13
+ * 在驱动的消费点上必然显形,而不是被本文件自己吞掉。
14
+ */
15
+ import { putOwnKey } from '../ownKey.js';
1
16
  import { DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, findPendingForTask, readDecideCurrentPending, readWireErrorCode, } from './hitlBridge.js';
2
17
  import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverlay, } from '../liveQuestionStore.js';
3
18
  import { hostLog } from '../host.js';
@@ -146,16 +161,14 @@ export function toAnsweredOutput(questions, answer) {
146
161
  for (const entry of answer.answers ?? []) {
147
162
  const q = questions.find(qq => qq.header === entry.header);
148
163
  const key = typeof q?.question === 'string' ? q.question : entry.header;
149
- // 🔴 0.67.1 同形族(与 `adapter/downstream/terminalToSdkResult.ts` 的 `putOwn` 同一条):
150
- // `key` 来自 wire(问题正文 / header),字面等于 `"__proto__"` 时裸赋值走的是
151
- // `Object.prototype` 上那只 **accessor** —— `answers` 那一格(串值)整条静默丢失,
152
- // `annotations` 那一格(对象值)还顺手改了表自己的原型。落键一律 `defineProperty`。
153
- const put = (t, v) => {
154
- Object.defineProperty(t, key, { value: v, enumerable: true, writable: true, configurable: true });
155
- };
156
- put(answers, (entry.selected ?? []).join(', '));
164
+ // 🔴 0.68.0 / L-246 B2:落键走**单源** `putOwnKey`(`src/ownKey.ts`)。修前这里是一只内联的
165
+ // 同形实现,与 `adapter/downstream/terminalToSdkResult.ts` 的那一份各写各的 —— 陷阱与取舍
166
+ // 的完整说明现在只在那个模块的头注里一份。`key` 来自 wire(问题正文 / header),字面等于
167
+ // `"__proto__"` 时裸赋值走的是 `Object.prototype` 上那只 accessor:`answers` 那一格(串值)
168
+ // 整条静默丢失,`annotations` 那一格(对象值)还顺手改了表自己的原型。
169
+ putOwnKey(answers, key, (entry.selected ?? []).join(', '));
157
170
  if (entry.note)
158
- put(annotations, { notes: entry.note });
171
+ putOwnKey(annotations, key, { notes: entry.note });
159
172
  }
160
173
  return {
161
174
  type: 'ask-user-question',
@@ -78,6 +78,7 @@
78
78
  * 🔴 方向纪律:reason/note 只做归因,绝不参与裁决;缺席 ⇒ 现状字节不变。
79
79
  */
80
80
  import { type GateCurrentPending, type HitlFailureStage, type HitlSafetyCode, type HitlClientLike } from './hitlBridge.js';
81
+ import { type RuleStoreUnreadableKind } from '../gateVocabulary.js';
81
82
  import { type DecideReceiptView } from '../decideReceipt.js';
82
83
  import { RULE_OFFER_MATCHES, RULE_OFFER_BATCH_MEMBER_KINDS, RULE_OFFER_UNCOVERED_REASONS, RULE_OFFERS_ABSENCE_REASONS, DENIAL_LIMIT_KINDS } from '@sema-agent/sdk';
83
84
  import type { RuleSuggestion, ToolApprovalRespondAck, PersistedRuleAnchor as SdkPersistedRuleAnchor, RuleOfferMatch as SdkRuleOfferMatch, RuleOfferBatchMember as SdkRuleOfferBatchMember, RuleOfferUncoveredDetail as SdkRuleOfferUncoveredDetail, RuleOfferUncoveredReason as SdkRuleOfferUncoveredReason, AskOrigin as SdkAskOrigin, RuleOffersAbsence as SdkRuleOffersAbsence, DenialLimitKind as SdkDenialLimitKind, DenialLimitFallback as SdkDenialLimitFallback } from '@sema-agent/sdk';
@@ -708,7 +709,7 @@ export interface ApprovalCardRequest {
708
709
  * value is removed at the stamp」)⇒ 表外值到不了消费端,真读到就是坏形,渲一句成因就是编事实。
709
710
  * 人话两句走**唯一铸点** `ruleStoreUnreadableDetail`(三端零自拼)。
710
711
  */
711
- ruleStoreUnreadable?: string;
712
+ ruleStoreUnreadable?: RuleStoreUnreadableKind;
712
713
  }
713
714
  /**
714
715
  * 🔴 **拆缝口** —— 弹「三选卡」并等人的决断。壳 = vendored CC `PermissionRequest`;
@@ -1016,6 +1016,7 @@ export function readDenialLimitFallback(v) {
1016
1016
  * 入参签名(typeshape 门的 unknown-出境棘轮 +1),而它的行为由两条腿的端到端素材覆盖。
1017
1017
  */
1018
1018
  function readRuleStoreUnreadable(v) {
1019
+ // 🔴 0.68.0 / L-245:谓词已是**型守卫** ⇒ 这里的 `as string` 退役(一处 cast 少一处)。
1019
1020
  return isRuleStoreUnreadableKind(v) ? v : undefined;
1020
1021
  }
1021
1022
  /**
@@ -0,0 +1,34 @@
1
+ /**
2
+ * src/ownKey.ts — 「拿 **wire 给的串**当对象键」时的**唯一落键姿势**(0.68.0 / L-246 B2 单源化)。
3
+ *
4
+ * ── 病形(0.67.1 在 `terminalToSdkResult.ts` 上定谳,同批在 `hitl/parkResolver.ts` 上又长了一份)──
5
+ * `Object.prototype.__proto__` 是一个 **accessor**:在一只普通对象上写 `o["__proto__"] = v` 走的是
6
+ * 那只 setter ——
7
+ * · **不产生自有属性** ⇒ 那一行在 `Object.keys` / `JSON.stringify` 里**整条消失**,连行数都少一;
8
+ * · `v` 是对象时还**顺手改了 `o` 的原型**。
9
+ * 而这几张表的键全都来自 wire(taskId / modelId / core 开集的 costBreakdown 键名 / 一条问题正文),
10
+ * 没有任何一条保证它们不等于这个字面。⇒ 落键一律走 `defineProperty`。
11
+ *
12
+ * ── 为什么要单源(本文件存在的理由)────────────────────────────────────────────────────────
13
+ * 修前同一条处置有**两份实现**(`terminalToSdkResult.ts` 的 `putOwn` 与 `parkResolver.ts` 里那只
14
+ * 内联 `put`),而且两份都带着各自的一段说明。两份同形实现的代价不是重复几行,是**下一次只修一处**:
15
+ * 这条落键姿势将来若要再收紧(例如连 `constructor` 一类也要拦),漏掉的那一份会静默地把老病留住。
16
+ * 本仓「同形存量」纪律的直接落点。
17
+ *
18
+ * 🔴 **不改成 null 原型对象交付**:端拿到的仍是一只正常对象(`hasOwnProperty` / `toString` 都在),
19
+ * 本模块只管「落键」这一步,不改交付形 —— 换原型会在宿主侧造出一类新的 `TypeError`。
20
+ * 描述符与普通赋值**逐位相同**(`writable` / `enumerable` / `configurable` 三真),所以除了
21
+ * `__proto__` 这一个字面,其余每一个键的行为一个字节都没变。
22
+ *
23
+ * 🔴 **零 import**(纯叶):它进 runStream 内核闭包与 index 闭包两条传递闭包,自己一条 import 都没有
24
+ * ⇒ 不可能把任何东西拖进去(portability 门真正守的三件 —— 零 Node 内建 / 零 react·ink /
25
+ * 外部包等值集 —— 一件都不动)。
26
+ */
27
+ /**
28
+ * 把 `value` 落在 `table[key]` 上,**保证产生一个自有属性**(`key` 可以是任何 wire 串,含 `__proto__`)。
29
+ *
30
+ * @param table 目标表(交付形不变:仍是一只普通对象)
31
+ * @param key 来自 wire 的键串
32
+ * @param value 要落的值
33
+ */
34
+ export declare function putOwnKey<T>(table: Record<string, T>, key: string, value: T): void;
package/dist/ownKey.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * src/ownKey.ts — 「拿 **wire 给的串**当对象键」时的**唯一落键姿势**(0.68.0 / L-246 B2 单源化)。
3
+ *
4
+ * ── 病形(0.67.1 在 `terminalToSdkResult.ts` 上定谳,同批在 `hitl/parkResolver.ts` 上又长了一份)──
5
+ * `Object.prototype.__proto__` 是一个 **accessor**:在一只普通对象上写 `o["__proto__"] = v` 走的是
6
+ * 那只 setter ——
7
+ * · **不产生自有属性** ⇒ 那一行在 `Object.keys` / `JSON.stringify` 里**整条消失**,连行数都少一;
8
+ * · `v` 是对象时还**顺手改了 `o` 的原型**。
9
+ * 而这几张表的键全都来自 wire(taskId / modelId / core 开集的 costBreakdown 键名 / 一条问题正文),
10
+ * 没有任何一条保证它们不等于这个字面。⇒ 落键一律走 `defineProperty`。
11
+ *
12
+ * ── 为什么要单源(本文件存在的理由)────────────────────────────────────────────────────────
13
+ * 修前同一条处置有**两份实现**(`terminalToSdkResult.ts` 的 `putOwn` 与 `parkResolver.ts` 里那只
14
+ * 内联 `put`),而且两份都带着各自的一段说明。两份同形实现的代价不是重复几行,是**下一次只修一处**:
15
+ * 这条落键姿势将来若要再收紧(例如连 `constructor` 一类也要拦),漏掉的那一份会静默地把老病留住。
16
+ * 本仓「同形存量」纪律的直接落点。
17
+ *
18
+ * 🔴 **不改成 null 原型对象交付**:端拿到的仍是一只正常对象(`hasOwnProperty` / `toString` 都在),
19
+ * 本模块只管「落键」这一步,不改交付形 —— 换原型会在宿主侧造出一类新的 `TypeError`。
20
+ * 描述符与普通赋值**逐位相同**(`writable` / `enumerable` / `configurable` 三真),所以除了
21
+ * `__proto__` 这一个字面,其余每一个键的行为一个字节都没变。
22
+ *
23
+ * 🔴 **零 import**(纯叶):它进 runStream 内核闭包与 index 闭包两条传递闭包,自己一条 import 都没有
24
+ * ⇒ 不可能把任何东西拖进去(portability 门真正守的三件 —— 零 Node 内建 / 零 react·ink /
25
+ * 外部包等值集 —— 一件都不动)。
26
+ */
27
+ /**
28
+ * 把 `value` 落在 `table[key]` 上,**保证产生一个自有属性**(`key` 可以是任何 wire 串,含 `__proto__`)。
29
+ *
30
+ * @param table 目标表(交付形不变:仍是一只普通对象)
31
+ * @param key 来自 wire 的键串
32
+ * @param value 要落的值
33
+ */
34
+ export function putOwnKey(table, key, value) {
35
+ Object.defineProperty(table, key, { value, enumerable: true, writable: true, configurable: true });
36
+ }
@@ -48,6 +48,16 @@ export type RetryStatus =
48
48
  elapsedMs?: number;
49
49
  /** 见 {@link BrainStatusPayload.timeoutMs}(引擎给了才在场;缺席禁渲成 0)。 */
50
50
  timeoutMs?: number;
51
+ /**
52
+ * 🔴 0.68.0(L-246)—— 引擎给的**中性人话提示**(`BrainStatus.detail`;server 已脱敏 + 限 300 字)。
53
+ *
54
+ * 修前这一位只在两条 `error` 臂上被读(`circuit_open` / `gave_up` 各拿它当 `error.formatted`),
55
+ * `stalled` 臂**整只丢掉**它 —— 于是「为什么还在等」这句唯一由引擎产出的解释,恰恰在**最长的
56
+ * 那一段等待**上看不见,端只好自己按 `errClass` 拼一句泛泛的话(而那正是措辞该由产生者单铸的
57
+ * 反例)。⇒ 原样透传,**一个字不改写、不截断**(显示封顶归端,它才知道自己的行宽)。
58
+ * 🔴 缺席 ⇒ 键不铸(空串同理):一格空白的解释比没有解释更坏。
59
+ */
60
+ detail?: string;
51
61
  } | {
52
62
  kind: 'error';
53
63
  deadline: number;
@@ -163,7 +173,8 @@ export type BrainStatusPhase = (typeof BRAIN_STATUS_PHASES)[number];
163
173
  * core `BrainRetryErrClass`(5.43.0)的**类型面镜像** —— 一次重试等待的**原因分桶**,
164
174
  * provider 中立(绝不是 HTTP 状态码 / syscall code / provider 分类法;那些不许过这条通道):
165
175
  * · `connect_refused` 目标本身给了确定否定(该地址没人 accept / 名字无地址)—— 短梯队服务的那一类;
166
- * · `transport` 其余传输层失败(connect 超时 / reset / 流中断 / 流停滞)—— 全梯队;
176
+ * · `transport` 其余传输层失败(connect 超时 / reset / 流中断)—— 全梯队;
177
+ * · `stall` 连上了、也没断,但**不出字节**(流停滞)—— 与 `transport` 分家的理由见下;
167
178
  * · `rate_limit` provider 要求放慢;
168
179
  * · `server` provider 自报自己这边出错;
169
180
  * · `http` 状态谓词判终态、但 provider 自己的显式重试裁定说重试;
@@ -174,7 +185,7 @@ export type BrainStatusPhase = (typeof BRAIN_STATUS_PHASES)[number];
174
185
  * 分支的,少一相会落 default 臂被渲成错话,所以那张表才需要运行期镜像 + engine-vocab 等值门。
175
186
  * 判据锚在「真正决定结果的量」上:决定结果的是有没有分支,不是有没有一张表。
176
187
  */
177
- export type BrainRetryErrClass = 'connect_refused' | 'transport' | 'rate_limit' | 'server' | 'http' | 'output_cap';
188
+ export type BrainRetryErrClass = 'connect_refused' | 'transport' | 'stall' | 'rate_limit' | 'server' | 'http' | 'output_cap';
178
189
  /** wire 上 `status` 臂的载荷(= core `BrainStatus`;server 两腿白名单原样转发这 **11** 键(7.3.0 起含 `elapsedMs`/`timeoutMs`)——
179
190
  * 真源 = server 7.58.0 `dist/trace/project.js` 的 `brainStatusEventData`,逐条条件拷贝)。 */
180
191
  export interface BrainStatusPayload {
@@ -128,13 +128,16 @@ export function mapBrainStatusToRetry(p, nowMs) {
128
128
  ...(elapsedMs !== undefined ? { elapsedMs } : {}),
129
129
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
130
130
  };
131
+ // 🔴 0.68.0(L-246):引擎那句中性提示。**只进 `stalled` 臂** —— 两条 error 臂上它已经是
132
+ // `error.formatted` 的来源,同一句话在同一只读数上出现两次会让端不知道该渲哪一份。
133
+ const stalledDetail = typeof p.detail === 'string' && p.detail.length > 0 ? { detail: p.detail } : {};
131
134
  const extra = { ...counts, ...cause, ...producerTiming, ...failureStatus, ...waitProgress };
132
135
  switch (p.phase) {
133
136
  // 🔴 引擎直报「恢复」:摘行。绝不落 error 臂 —— 那是把成功渲成失败。
134
137
  case 'recovered':
135
138
  return null;
136
139
  case 'reconnecting':
137
- return { kind: 'stalled', deadline, ...extra };
140
+ return { kind: 'stalled', deadline, ...extra, ...stalledDetail };
138
141
  case 'rate_limited':
139
142
  return { kind: 'error', deadline, ...extra, error: { formatted: '', rateLimits: {} } };
140
143
  case 'circuit_open':
@@ -193,25 +193,93 @@ export declare function runTerminalCode(read: RunTerminalRead | null): string |
193
193
  */
194
194
  export declare function isReviewPark(read: RunTerminalRead | null): boolean;
195
195
  /**
196
- * **非成功终局**的状态词(闭集;0.65.0 新铸,L-215② / core [6908])——「这条 run **不会再自己动了**,
197
- * 而且它没有成功」。
196
+ * ══════════════════════════════════════════════════════════════════════════════════════════════
197
+ * 0.68.0 🔴 BREAKING —— 终态词**两表分源**(L-247;core [7067] @cli 行顺答)
198
+ * ══════════════════════════════════════════════════════════════════════════════════════════════
199
+ * 修前本模块只有一张 `TERMINAL_STATUSES = ['completed','failed','killed','blocked']`,把**两个不同
200
+ * 属主**的词表合成了一张:
201
+ * · `completed` / `failed` / `blocked` 属 **core 的终局因由闭集**(`TerminalCause["kind"]`,
202
+ * `terminal-cause.ts`;core 那边第四员是 `paused`);
203
+ * · `killed` 属 **server 的 run 行状态面**(core 的因由集里**根本没有这个词** —— core [7067] 逐字:
204
+ * 「`killed` 不是 core 词 —— 壳手抄四词里的 `killed` 来自 server run status 面」)。
205
+ * 合成一张的后果是两边加员时都读不出该改哪儿:core 加第五个因由(闭集加员会在 wire 清单的
206
+ * `closedSetMembers` 上具名)与 server 加一个行状态词,在一张表上长得一模一样,于是消费方要么
207
+ * 把新词折进已知词(不安全侧,B-079① 同形),要么两边都漏。
208
+ *
209
+ * ⇒ 两张表**按属主分开**,各自带自己的对账物:
210
+ * · {@link TERMINAL_CAUSE_KINDS} —— core 闭集的镜像,门对**实装 devDep core** 双向对账;
211
+ * · {@link RUN_TERMINAL_STATUSES} —— server run 行状态面的终态词,门对 sdk 真字节钉 + 反向钉
212
+ * 「`killed` 不在 core 因由集里」。
213
+ *
214
+ * 🔴 **为什么是镜像而不是 `export … from '@sema-agent/core'`**(与 `engineNoticeCodes.ts` /
215
+ * `autoModeUnavailable.ts` / `toolResult.ts` 三处逐字同一条理由,再加两条本表独有的):
216
+ * ① `@sema-agent/core` 既不是本包的 peer 也不是 runtime dep —— 本包的 `.d.ts` 一旦引用它,
217
+ * 装了本包却没装 core 的下游当场编译不过;`portability` 门的 `EXPECTED_PACKAGES_INDEX` 是
218
+ * **等值门**(闭包外部包恒等于 `{diff, @sema-agent/sdk}`)且 ③ 段拿 esbuild
219
+ * `--platform=browser` 真打一次包,一条值级边会把整台引擎焊进 web/desktop 的产物;
220
+ * ② core **没有导出**任何名为「终局因由闭集」的元组:它只导出 `TERMINAL_CAUSE_IS_REPLAYABLE`
221
+ * (一张 `Record<kind, boolean>` 的处置表)与谓词 `isTerminalCauseKind`,而且这两件**都不在
222
+ * core 的 barrel 上**(`dist/index.d.ts` 只 re-export 了**类型** `TerminalCause`)⇒
223
+ * 「直接 re-export 那个闭集」在今天的上游字节上不存在可 re-export 的符号。
224
+ * ⇒ 镜像 + 机器对账(本仓对「不自抄」的既定机制):这张表是一份**抄件**不是意见,
225
+ * `scripts/run-terminal-word-source-test.mjs` 拿 core 那张处置表的**键集**逐词双向钉,
226
+ * core 一动这里就先红。
227
+ */
228
+ /**
229
+ * **core 终局因由**的 kind 闭集(core `TerminalCause["kind"]` 逐词镜像;顺序同源 =
230
+ * `dist/core/terminal-cause.js` 的 `TERMINAL_CAUSE_IS_REPLAYABLE` 键序)。
231
+ *
232
+ * 四员各答「这条 run 为什么结束」:
233
+ * · `completed` —— 跑完了(用户干净 halt 也算,见 `TaskResult.haltedByUser`);
234
+ * · `failed` —— 限额 / provider 失败 / abort / 产出不合法……;
235
+ * · `blocked` —— **agent 自报**走不下去(`report_blocked`)。上游逐字:它是 TERMINAL 的,
236
+ * 「the leg is over and it waits for nobody」;
237
+ * · `paused` —— 一次耐久暂停提交了 checkpoint,**run 可以被恢复**。🔴 它在因由面上是一个
238
+ * 合法的 kind,但它**不是**「run 结束了」——所以它**不在** {@link RUN_TERMINAL_STATUSES} 里。
239
+ * 这正是两张表必须分源的最硬那一格:同一个词在两张面上答的是两个问题。
240
+ *
241
+ * 🔴 **集外词的读法**:core 加第五个因由词时,wire 清单的 `closedSetMembers` 必须列、发车帖必须
242
+ * 具名(core [7067] 逐字规矩)。消费方按闭集读 —— **集外词整键缺席,绝不折成已知词**。
243
+ * 🔴 形制:`Object.freeze` 的**字面元组**(`as const`),不是 `readonly string[]` —— 后者在类型面
244
+ * 交不出成员字面量,端就只能自己再抄一遍四个词(L-247 的病根)。冻结是为了让公面消费者
245
+ * `.push()` 改不动判定源(同 `RESUME_RETRY_LATER_CODES` 的已定谳病形)。
246
+ */
247
+ export declare const TERMINAL_CAUSE_KINDS: readonly ["completed", "failed", "blocked", "paused"];
248
+ /** {@link TERMINAL_CAUSE_KINDS} 的成员型(端的型面改**派生**,不再手抄词)。 */
249
+ export type TerminalCauseKind = (typeof TERMINAL_CAUSE_KINDS)[number];
250
+ /**
251
+ * 一个词是不是 core 的终局**因由** kind({@link TERMINAL_CAUSE_KINDS} 的成员)。
252
+ *
253
+ * 🔴 **它不答「这条 run 结束了没有」** —— `paused` 是合法成员而 run 还能被恢复。要那一问请读
254
+ * {@link isTerminalStatus}(server 行状态面)或 {@link readRunTerminal} 的因由臂。
255
+ * 🔴 非串 / 空串 / 集外词 ⇒ `false` = 「这不是本端认得的因由词」,**不是**「成功」。
256
+ */
257
+ export declare function isTerminalCauseKind(kind: unknown): kind is TerminalCauseKind;
258
+ /**
259
+ * **server run 行状态面**的「非成功终局」词(闭集;0.65.0 新铸,L-215② / core [6908];
260
+ * 0.68.0 随 L-247 更名 `TERMINAL_NOT_SUCCESS_STATUSES` → 本名,理由见本段顶注的分源说明)——
261
+ * 「这条 run **不会再自己动了**,而且它没有成功」。
198
262
  *
199
263
  * 🔴 三员各有出处,且 **`blocked` 与 `suspended`/`needs_review` 的分界是本表存在的全部理由**
200
264
  * (core [6908] 定谳):
201
265
  * · `failed` —— 引擎自报失败;
202
- * · `killed` —— 外力终止;
266
+ * · `killed` —— 外力终止。🔴 **这一员是本表与 {@link TERMINAL_CAUSE_KINDS} 的分水岭**:
267
+ * core 的因由闭集里没有它,它是 server run 行状态面自己的词(sdk `FleetTaskStatus` /
268
+ * `SessionNotifyStatus` 上都在,`RunStatus` 上今天还没有 —— 见 §33 的登记);
203
269
  * · `blocked` —— **agent 自报的终态**:它自己判定这条 run 走不下去了(core `terminal.blocked`
204
270
  * 的 `reason` 是它的说明)。🔴 它**不是「等人」** —— 这正是修前被漏掉的那一格;
205
271
  * · 🔴 **`suspended` / `needs_review` 刻意不在表里**:那两个词是**等一次人的决定**,run 还活着、
206
272
  * 决定给了就继续跑。把它们读成「非成功终局」会把一条**正等着你**的 run 在面板上判死,
207
273
  * 而用户从此不知道有一张卡在等他 —— 与本表要修的方向相反的同一类错。
208
274
  *
209
- * 🔴 形制:`Object.freeze` 的数组,不是 `ReadonlySet`(同 `RESUME_RETRY_LATER_CODES` 的已定谳病形:
210
- * `ReadonlySet` 只在类型面只读,而判定查的就是公面上这同一个实例)。
275
+ * 🔴 形制:`Object.freeze` 的**字面元组**,不是 `ReadonlySet`(同 `RESUME_RETRY_LATER_CODES` 的
276
+ * 已定谳病形:`ReadonlySet` 只在类型面只读,而判定查的就是公面上这同一个实例)。
211
277
  */
212
- export declare const TERMINAL_NOT_SUCCESS_STATUSES: readonly string[];
278
+ export declare const RUN_TERMINAL_NOT_SUCCESS_STATUSES: readonly ["failed", "killed", "blocked"];
279
+ /** {@link RUN_TERMINAL_NOT_SUCCESS_STATUSES} 的成员型(端的型面改派生,不再手抄三词)。 */
280
+ export type RunTerminalNotSuccessStatus = (typeof RUN_TERMINAL_NOT_SUCCESS_STATUSES)[number];
213
281
  /**
214
- * 一个状态词是不是**非成功终局**({@link TERMINAL_NOT_SUCCESS_STATUSES} 的成员)。
282
+ * 一个状态词是不是**非成功终局**({@link RUN_TERMINAL_NOT_SUCCESS_STATUSES} 的成员)。
215
283
  *
216
284
  * **单铸谓词**(L-215②):本包的通知链(面板 settle 的 `isError` 位)与三端各自的后台白名单
217
285
  * 读的必须是**同一个判据** —— 修前包里是一处内联的 `status === 'failed' || status === 'killed'`,
@@ -224,19 +292,24 @@ export declare const TERMINAL_NOT_SUCCESS_STATUSES: readonly string[];
224
292
  * 已定谳的事故形。
225
293
  * 🔴 非串 / 空串 ⇒ `false`(同上:是「答不出」,不是「成功」)。
226
294
  */
227
- export declare function isTerminalNotSuccess(status: unknown): boolean;
295
+ export declare function isTerminalNotSuccess(status: unknown): status is RunTerminalNotSuccessStatus;
228
296
  /**
229
- * **终局**的状态词(闭集;0.65.0 新铸)—— 成功那一员加上 {@link TERMINAL_NOT_SUCCESS_STATUSES}
230
- * 的三员。答的是「这条 run 还会不会自己动」,与上一张表答的「它成没成功」是**两个问题**,所以
231
- * 两张表分开、后者由前者派生(加员只有一处要改)。
297
+ * **server run 行状态面**的终局状态词(闭集;0.65.0 新铸,0.68.0 随 L-247 更名 `TERMINAL_STATUSES`
298
+ * → 本名)—— 成功那一员加上 {@link RUN_TERMINAL_NOT_SUCCESS_STATUSES} 的三员。答的是「这条 run
299
+ * 还会不会自己动」,与上一张表答的「它成没成功」是**两个问题**,所以两张表分开、后者由前者派生
300
+ * (加员只有一处要改)。
232
301
  *
233
302
  * 🔴 同样**不含** `suspended` / `needs_review`:那两个词是「等一次人的决定」——run 没有结束。
303
+ * 🔴 也**不含** core 因由面的 `paused`:那是另一张表的词,而且它在语义上恰好与本表相反
304
+ * ({@link TERMINAL_CAUSE_KINDS} 顶注)。
234
305
  */
235
- export declare const TERMINAL_STATUSES: readonly string[];
306
+ export declare const RUN_TERMINAL_STATUSES: readonly ["completed", "failed", "killed", "blocked"];
307
+ /** {@link RUN_TERMINAL_STATUSES} 的成员型(端的型面改派生,不再手抄四词 —— L-247 的修形)。 */
308
+ export type RunTerminalStatus = (typeof RUN_TERMINAL_STATUSES)[number];
236
309
  /**
237
- * 一个状态词是不是**终局**({@link TERMINAL_STATUSES} 的成员)。
310
+ * 一个状态词是不是**终局**({@link RUN_TERMINAL_STATUSES} 的成员)。
238
311
  *
239
312
  * 用在「这条通知/这条行是不是可以收摊了」这一问上(通知去重的 seed 扫描、后台行 settle 门)。
240
313
  * 🔴 表外词 ⇒ `false` = 「本端认不出这是个终局」,**不是**「它还在跑」;要判「在跑」请读正向证据。
241
314
  */
242
- export declare function isTerminalStatus(status: unknown): boolean;
315
+ export declare function isTerminalStatus(status: unknown): status is RunTerminalStatus;
@@ -188,29 +188,100 @@ export function isReviewPark(read) {
188
188
  return read !== null && read.kind === 'paused' && read.flatStatus === FLAT_REVIEW_STATUS;
189
189
  }
190
190
  /**
191
- * **非成功终局**的状态词(闭集;0.65.0 新铸,L-215② / core [6908])——「这条 run **不会再自己动了**,
192
- * 而且它没有成功」。
191
+ * ══════════════════════════════════════════════════════════════════════════════════════════════
192
+ * 0.68.0 🔴 BREAKING —— 终态词**两表分源**(L-247;core [7067] @cli 行顺答)
193
+ * ══════════════════════════════════════════════════════════════════════════════════════════════
194
+ * 修前本模块只有一张 `TERMINAL_STATUSES = ['completed','failed','killed','blocked']`,把**两个不同
195
+ * 属主**的词表合成了一张:
196
+ * · `completed` / `failed` / `blocked` 属 **core 的终局因由闭集**(`TerminalCause["kind"]`,
197
+ * `terminal-cause.ts`;core 那边第四员是 `paused`);
198
+ * · `killed` 属 **server 的 run 行状态面**(core 的因由集里**根本没有这个词** —— core [7067] 逐字:
199
+ * 「`killed` 不是 core 词 —— 壳手抄四词里的 `killed` 来自 server run status 面」)。
200
+ * 合成一张的后果是两边加员时都读不出该改哪儿:core 加第五个因由(闭集加员会在 wire 清单的
201
+ * `closedSetMembers` 上具名)与 server 加一个行状态词,在一张表上长得一模一样,于是消费方要么
202
+ * 把新词折进已知词(不安全侧,B-079① 同形),要么两边都漏。
203
+ *
204
+ * ⇒ 两张表**按属主分开**,各自带自己的对账物:
205
+ * · {@link TERMINAL_CAUSE_KINDS} —— core 闭集的镜像,门对**实装 devDep core** 双向对账;
206
+ * · {@link RUN_TERMINAL_STATUSES} —— server run 行状态面的终态词,门对 sdk 真字节钉 + 反向钉
207
+ * 「`killed` 不在 core 因由集里」。
208
+ *
209
+ * 🔴 **为什么是镜像而不是 `export … from '@sema-agent/core'`**(与 `engineNoticeCodes.ts` /
210
+ * `autoModeUnavailable.ts` / `toolResult.ts` 三处逐字同一条理由,再加两条本表独有的):
211
+ * ① `@sema-agent/core` 既不是本包的 peer 也不是 runtime dep —— 本包的 `.d.ts` 一旦引用它,
212
+ * 装了本包却没装 core 的下游当场编译不过;`portability` 门的 `EXPECTED_PACKAGES_INDEX` 是
213
+ * **等值门**(闭包外部包恒等于 `{diff, @sema-agent/sdk}`)且 ③ 段拿 esbuild
214
+ * `--platform=browser` 真打一次包,一条值级边会把整台引擎焊进 web/desktop 的产物;
215
+ * ② core **没有导出**任何名为「终局因由闭集」的元组:它只导出 `TERMINAL_CAUSE_IS_REPLAYABLE`
216
+ * (一张 `Record<kind, boolean>` 的处置表)与谓词 `isTerminalCauseKind`,而且这两件**都不在
217
+ * core 的 barrel 上**(`dist/index.d.ts` 只 re-export 了**类型** `TerminalCause`)⇒
218
+ * 「直接 re-export 那个闭集」在今天的上游字节上不存在可 re-export 的符号。
219
+ * ⇒ 镜像 + 机器对账(本仓对「不自抄」的既定机制):这张表是一份**抄件**不是意见,
220
+ * `scripts/run-terminal-word-source-test.mjs` 拿 core 那张处置表的**键集**逐词双向钉,
221
+ * core 一动这里就先红。
222
+ */
223
+ /**
224
+ * **core 终局因由**的 kind 闭集(core `TerminalCause["kind"]` 逐词镜像;顺序同源 =
225
+ * `dist/core/terminal-cause.js` 的 `TERMINAL_CAUSE_IS_REPLAYABLE` 键序)。
226
+ *
227
+ * 四员各答「这条 run 为什么结束」:
228
+ * · `completed` —— 跑完了(用户干净 halt 也算,见 `TaskResult.haltedByUser`);
229
+ * · `failed` —— 限额 / provider 失败 / abort / 产出不合法……;
230
+ * · `blocked` —— **agent 自报**走不下去(`report_blocked`)。上游逐字:它是 TERMINAL 的,
231
+ * 「the leg is over and it waits for nobody」;
232
+ * · `paused` —— 一次耐久暂停提交了 checkpoint,**run 可以被恢复**。🔴 它在因由面上是一个
233
+ * 合法的 kind,但它**不是**「run 结束了」——所以它**不在** {@link RUN_TERMINAL_STATUSES} 里。
234
+ * 这正是两张表必须分源的最硬那一格:同一个词在两张面上答的是两个问题。
235
+ *
236
+ * 🔴 **集外词的读法**:core 加第五个因由词时,wire 清单的 `closedSetMembers` 必须列、发车帖必须
237
+ * 具名(core [7067] 逐字规矩)。消费方按闭集读 —— **集外词整键缺席,绝不折成已知词**。
238
+ * 🔴 形制:`Object.freeze` 的**字面元组**(`as const`),不是 `readonly string[]` —— 后者在类型面
239
+ * 交不出成员字面量,端就只能自己再抄一遍四个词(L-247 的病根)。冻结是为了让公面消费者
240
+ * `.push()` 改不动判定源(同 `RESUME_RETRY_LATER_CODES` 的已定谳病形)。
241
+ */
242
+ export const TERMINAL_CAUSE_KINDS = Object.freeze([
243
+ 'completed',
244
+ 'failed',
245
+ 'blocked',
246
+ 'paused',
247
+ ]);
248
+ /**
249
+ * 一个词是不是 core 的终局**因由** kind({@link TERMINAL_CAUSE_KINDS} 的成员)。
250
+ *
251
+ * 🔴 **它不答「这条 run 结束了没有」** —— `paused` 是合法成员而 run 还能被恢复。要那一问请读
252
+ * {@link isTerminalStatus}(server 行状态面)或 {@link readRunTerminal} 的因由臂。
253
+ * 🔴 非串 / 空串 / 集外词 ⇒ `false` = 「这不是本端认得的因由词」,**不是**「成功」。
254
+ */
255
+ export function isTerminalCauseKind(kind) {
256
+ return typeof kind === 'string' && TERMINAL_CAUSE_KINDS.includes(kind);
257
+ }
258
+ /**
259
+ * **server run 行状态面**的「非成功终局」词(闭集;0.65.0 新铸,L-215② / core [6908];
260
+ * 0.68.0 随 L-247 更名 `TERMINAL_NOT_SUCCESS_STATUSES` → 本名,理由见本段顶注的分源说明)——
261
+ * 「这条 run **不会再自己动了**,而且它没有成功」。
193
262
  *
194
263
  * 🔴 三员各有出处,且 **`blocked` 与 `suspended`/`needs_review` 的分界是本表存在的全部理由**
195
264
  * (core [6908] 定谳):
196
265
  * · `failed` —— 引擎自报失败;
197
- * · `killed` —— 外力终止;
266
+ * · `killed` —— 外力终止。🔴 **这一员是本表与 {@link TERMINAL_CAUSE_KINDS} 的分水岭**:
267
+ * core 的因由闭集里没有它,它是 server run 行状态面自己的词(sdk `FleetTaskStatus` /
268
+ * `SessionNotifyStatus` 上都在,`RunStatus` 上今天还没有 —— 见 §33 的登记);
198
269
  * · `blocked` —— **agent 自报的终态**:它自己判定这条 run 走不下去了(core `terminal.blocked`
199
270
  * 的 `reason` 是它的说明)。🔴 它**不是「等人」** —— 这正是修前被漏掉的那一格;
200
271
  * · 🔴 **`suspended` / `needs_review` 刻意不在表里**:那两个词是**等一次人的决定**,run 还活着、
201
272
  * 决定给了就继续跑。把它们读成「非成功终局」会把一条**正等着你**的 run 在面板上判死,
202
273
  * 而用户从此不知道有一张卡在等他 —— 与本表要修的方向相反的同一类错。
203
274
  *
204
- * 🔴 形制:`Object.freeze` 的数组,不是 `ReadonlySet`(同 `RESUME_RETRY_LATER_CODES` 的已定谳病形:
205
- * `ReadonlySet` 只在类型面只读,而判定查的就是公面上这同一个实例)。
275
+ * 🔴 形制:`Object.freeze` 的**字面元组**,不是 `ReadonlySet`(同 `RESUME_RETRY_LATER_CODES` 的
276
+ * 已定谳病形:`ReadonlySet` 只在类型面只读,而判定查的就是公面上这同一个实例)。
206
277
  */
207
- export const TERMINAL_NOT_SUCCESS_STATUSES = Object.freeze([
278
+ export const RUN_TERMINAL_NOT_SUCCESS_STATUSES = Object.freeze([
208
279
  'failed',
209
280
  'killed',
210
281
  'blocked',
211
282
  ]);
212
283
  /**
213
- * 一个状态词是不是**非成功终局**({@link TERMINAL_NOT_SUCCESS_STATUSES} 的成员)。
284
+ * 一个状态词是不是**非成功终局**({@link RUN_TERMINAL_NOT_SUCCESS_STATUSES} 的成员)。
214
285
  *
215
286
  * **单铸谓词**(L-215②):本包的通知链(面板 settle 的 `isError` 位)与三端各自的后台白名单
216
287
  * 读的必须是**同一个判据** —— 修前包里是一处内联的 `status === 'failed' || status === 'killed'`,
@@ -224,25 +295,28 @@ export const TERMINAL_NOT_SUCCESS_STATUSES = Object.freeze([
224
295
  * 🔴 非串 / 空串 ⇒ `false`(同上:是「答不出」,不是「成功」)。
225
296
  */
226
297
  export function isTerminalNotSuccess(status) {
227
- return typeof status === 'string' && TERMINAL_NOT_SUCCESS_STATUSES.includes(status);
298
+ return typeof status === 'string' && RUN_TERMINAL_NOT_SUCCESS_STATUSES.includes(status);
228
299
  }
229
300
  /**
230
- * **终局**的状态词(闭集;0.65.0 新铸)—— 成功那一员加上 {@link TERMINAL_NOT_SUCCESS_STATUSES}
231
- * 的三员。答的是「这条 run 还会不会自己动」,与上一张表答的「它成没成功」是**两个问题**,所以
232
- * 两张表分开、后者由前者派生(加员只有一处要改)。
301
+ * **server run 行状态面**的终局状态词(闭集;0.65.0 新铸,0.68.0 随 L-247 更名 `TERMINAL_STATUSES`
302
+ * → 本名)—— 成功那一员加上 {@link RUN_TERMINAL_NOT_SUCCESS_STATUSES} 的三员。答的是「这条 run
303
+ * 还会不会自己动」,与上一张表答的「它成没成功」是**两个问题**,所以两张表分开、后者由前者派生
304
+ * (加员只有一处要改)。
233
305
  *
234
306
  * 🔴 同样**不含** `suspended` / `needs_review`:那两个词是「等一次人的决定」——run 没有结束。
307
+ * 🔴 也**不含** core 因由面的 `paused`:那是另一张表的词,而且它在语义上恰好与本表相反
308
+ * ({@link TERMINAL_CAUSE_KINDS} 顶注)。
235
309
  */
236
- export const TERMINAL_STATUSES = Object.freeze([
310
+ export const RUN_TERMINAL_STATUSES = Object.freeze([
237
311
  'completed',
238
- ...TERMINAL_NOT_SUCCESS_STATUSES,
312
+ ...RUN_TERMINAL_NOT_SUCCESS_STATUSES,
239
313
  ]);
240
314
  /**
241
- * 一个状态词是不是**终局**({@link TERMINAL_STATUSES} 的成员)。
315
+ * 一个状态词是不是**终局**({@link RUN_TERMINAL_STATUSES} 的成员)。
242
316
  *
243
317
  * 用在「这条通知/这条行是不是可以收摊了」这一问上(通知去重的 seed 扫描、后台行 settle 门)。
244
318
  * 🔴 表外词 ⇒ `false` = 「本端认不出这是个终局」,**不是**「它还在跑」;要判「在跑」请读正向证据。
245
319
  */
246
320
  export function isTerminalStatus(status) {
247
- return typeof status === 'string' && TERMINAL_STATUSES.includes(status);
321
+ return typeof status === 'string' && RUN_TERMINAL_STATUSES.includes(status);
248
322
  }
@@ -125,6 +125,14 @@ export const STRUCTURED_DETAIL_TYPES = new Set([
125
125
  // 并已登记在 `dist/core/runner/tool-output-projection.js:67` 的 `CC_DETAIL_TYPES` 里)。
126
126
  // 病根与 5.20 / 5.43 两次逐字同族:词漏了不会响,只会让那一类卡**永远退回正则解模型面文本**。
127
127
  'list-agents',
128
+ // ── core 7.17.0 跟车一词(0.68.0 提货批;engine-vocab ⑤ 段对 7.17.1 实装物直证,红文逐字:
129
+ // 「core 7.17.1:44 项 / 本包 43 项;漏:artifact」)────────────────────────────────────────
130
+ // `artifact` = Artifact 工具的结构化卡(core `dist/tools/artifact/artifact-tool.js` 的
131
+ // `details: { type: "artifact", action: … }` 铸点,七种 action 各一形 + 三条错误形)。
132
+ // 🔴 **可达性有前提**:只有宿主真接了 artifact 席(`RunnerDeps.artifactHost`)那只工具才挂载 ⇒
133
+ // 没接的部署上零帧差异。补它的理由与历史五次逐字同族:词漏了不会响,只会让那一类卡**永远
134
+ // 退回正则解模型面文本**,而任何一层都不会说话。
135
+ 'artifact',
128
136
  ]);
129
137
  // 🔴 同批**删三词**(core 5.10.0 BREAKING「幽灵卡」清仓):`multiedit` / `memory-saved` /
130
138
  // `memory-recall` —— 全树零铸点(MultiEdit/批量重放铸的是 `type:"edit"` 带 `edits[]`;core
@@ -63,6 +63,28 @@ export interface LiveWorkflowConfig {
63
63
  * tolerates absent/permissive fields and never throws (the graceful-degrade contract lives above, in the
64
64
  * source loop; this just maps a well-formed run). Exported for the mock-parity unit cross-check. */
65
65
  export declare function projectWorkflowRun(run: WorkflowRun): WorkflowRunState;
66
+ /** `parks[]` 的一行 —— **恰好三键**,凭据席位在本型上不存在(见上段 ①)。 */
67
+ export interface WorkflowParkRowView {
68
+ /** 这只 park 属于哪一次调用(与 `agents[].callKey` join 得上)。 */
69
+ callKey: string;
70
+ /** 停着的那只审批属于哪个会话(宿主按它路由)。 */
71
+ sessionId: string;
72
+ /** 最初跑出这只 park 的那条 run。 */
73
+ originRunId: string;
74
+ }
75
+ /**
76
+ * `GET /v1/workflows/:id` 的 `parks[]` → 三键行;**整键缺席 ⇒ `undefined`**(与空数组逐字可分)。
77
+ *
78
+ * 🔴 **`undefined` 与 `[]` 不是一回事**,这是本读器最承重的一格:
79
+ * · `undefined` —— 这条记录上没有 `parks` 这一键(旧引擎写的)⇒ **证不出**有没有 park;
80
+ * · `[]` —— 这条记录有这一键且是空的 ⇒ 一句正面事实:这条 run 上没有 park。
81
+ * 🔴 **坏行只丢自己**(与本包 `wiring_manifest.mcp[]` 的处置同尺):三键缺一 / 非串 / 空串 的行
82
+ * 整条丢弃,其余行保序留下;**非空输入而全部坏行 ⇒ 返回 `[]`**?—— **不**:那会把「读不出」
83
+ * 伪装成「零 park」。全坏 ⇒ `undefined`(诚实缺席)。
84
+ * 🔴 `parks` 不是数组(对象 / 串 / null)⇒ 当整键缺席(非投影口喂进来的形)。
85
+ * 🔴 纯函数、零副作用、**绝不抛**。
86
+ */
87
+ export declare function readWorkflowParks(run: unknown): readonly WorkflowParkRowView[] | undefined;
66
88
  /** 台账里的一个 agent(读口产物,只读快照;缺席 = 不知道,绝不渲成 0/空)。 */
67
89
  export interface WorkflowActivityAgentView {
68
90
  /** `ctx.agent` 调用的稳定身份(SSE 主键);老引擎/无 callKey 的帧上缺席。 */
@@ -381,6 +381,43 @@ export function projectWorkflowRun(run) {
381
381
  state.description = run.description;
382
382
  return state;
383
383
  }
384
+ /**
385
+ * `GET /v1/workflows/:id` 的 `parks[]` → 三键行;**整键缺席 ⇒ `undefined`**(与空数组逐字可分)。
386
+ *
387
+ * 🔴 **`undefined` 与 `[]` 不是一回事**,这是本读器最承重的一格:
388
+ * · `undefined` —— 这条记录上没有 `parks` 这一键(旧引擎写的)⇒ **证不出**有没有 park;
389
+ * · `[]` —— 这条记录有这一键且是空的 ⇒ 一句正面事实:这条 run 上没有 park。
390
+ * 🔴 **坏行只丢自己**(与本包 `wiring_manifest.mcp[]` 的处置同尺):三键缺一 / 非串 / 空串 的行
391
+ * 整条丢弃,其余行保序留下;**非空输入而全部坏行 ⇒ 返回 `[]`**?—— **不**:那会把「读不出」
392
+ * 伪装成「零 park」。全坏 ⇒ `undefined`(诚实缺席)。
393
+ * 🔴 `parks` 不是数组(对象 / 串 / null)⇒ 当整键缺席(非投影口喂进来的形)。
394
+ * 🔴 纯函数、零副作用、**绝不抛**。
395
+ */
396
+ export function readWorkflowParks(run) {
397
+ if (typeof run !== 'object' || run === null || Array.isArray(run))
398
+ return undefined;
399
+ const raw = run.parks;
400
+ if (!Array.isArray(raw))
401
+ return undefined;
402
+ const rows = [];
403
+ for (const r of raw) {
404
+ if (typeof r !== 'object' || r === null || Array.isArray(r))
405
+ continue;
406
+ const o = r;
407
+ const callKey = typeof o.callKey === 'string' && o.callKey.length > 0 ? o.callKey : undefined;
408
+ const sessionId = typeof o.sessionId === 'string' && o.sessionId.length > 0 ? o.sessionId : undefined;
409
+ const originRunId = typeof o.originRunId === 'string' && o.originRunId.length > 0 ? o.originRunId : undefined;
410
+ if (callKey === undefined || sessionId === undefined || originRunId === undefined)
411
+ continue;
412
+ // 🔴 **逐键铸**,不 spread:上游在这张行上多放一个键(尤其是凭据形)时,它在结构上到不了
413
+ // 本包的产物。这不是风格选择 —— 它是本段 ① 那条红线的实现。
414
+ rows.push({ callKey, sessionId, originRunId });
415
+ }
416
+ // 非空输入而一行都没活下来 ⇒ 读不出,不是「零 park」(绝不铸 `[]` 冒充一句正面事实)。
417
+ if (raw.length > 0 && rows.length === 0)
418
+ return undefined;
419
+ return rows;
420
+ }
384
421
  // ── 轮询/退避节拍(REF-CC-131,域词表-15):三个 3000ms 常量语义各不相同,拆开命名——合并成一个
385
422
  // 会把三条独立的退避策略绑死在一起(其中任一策略调优都会误伤另外两条)。──────────────────────
386
423
  /** owner 名下暂无任何 run(`resolveRunId` 回 null)时的重试间隔:不是错误,只是「还没有可监的