@sema-agent/client-core 0.74.2 → 0.74.3

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.
@@ -373,6 +373,7 @@ function wireNumberKey(key, v) {
373
373
  export function projectWorkflows(rows) {
374
374
  return rows.map(r => {
375
375
  const startedCount = wireStartedCount(r);
376
+ const errorCode = wireErrorCode(r);
376
377
  return {
377
378
  id: r.id,
378
379
  // 187 workflow label = `e.summary ?? e.description`(短 label);折成单行,空 → 187 占位符。
@@ -385,9 +386,15 @@ export function projectWorkflows(rows) {
385
386
  ...wireNumberKey('tokens', wireCount(r.tokens)),
386
387
  ...wireNumberKey('failedCount', wireCount(r.failedCount)),
387
388
  ...(startedCount !== undefined ? { startedCount } : {}),
389
+ ...(errorCode !== undefined ? { errorCode } : {}),
388
390
  };
389
391
  });
390
392
  }
393
+ /** CC-72:sdk 未声明的 run 级码,按结构读 —— 非空串才算在场;空串 / 非串 / null 一律缺席(不折串)。 */
394
+ function wireErrorCode(r) {
395
+ const v = r.errorCode;
396
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
397
+ }
391
398
  // ── 键集契约(`run-fleet-view-keys-test.mjs` 的运行期锚)─────────────────────────────────────
392
399
  //
393
400
  // 两跳链条,一跳都断不了(形与 `seatContract.ts` 的座位门同款):
@@ -436,6 +443,7 @@ const FLEET_WORKFLOW_VIEW_KEY_TUPLE = [
436
443
  'tokens',
437
444
  'failedCount',
438
445
  'startedCount',
446
+ 'errorCode',
439
447
  ];
440
448
  /** `FleetWorkflowView` 的键名清单(同上)。 */
441
449
  export const FLEET_WORKFLOW_VIEW_KEYS = FLEET_WORKFLOW_VIEW_KEY_TUPLE;
@@ -38,6 +38,8 @@ export interface FleetAgentRowLike {
38
38
  startedAt?: number;
39
39
  /** 0.74.0 —— 行的代际号(fleet 视图 `FleetTaskView.cycleSeq` 原样交)。缺席 = 这一行没有代际概念,不是第一代。 */
40
40
  cycleSeq?: number;
41
+ /** 0.74.3 CC-70:行帧上的委派卡 id(sdk `FleetTaskRow.parentToolCallId`);身份归一的退路键,在场才投影。 */
42
+ parentToolCallId?: string;
41
43
  }
42
44
  /** 行从投影集消失多久之后上报 `absent`(0.72.12 起不再 settle;重连空窗 / snapshot 清表远小于此)。名字沿用(公面导出)。 */
43
45
  export declare const ABSENT_SETTLE_MS = 30000;
@@ -141,6 +141,7 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
141
141
  // 🔴 三态直取:`row.toolUses` 缺席就是 undefined,这里**不许**兜 0(见 FleetAgentRowLike 注)。
142
142
  const toolUses = typeof row.toolUses === 'number' ? row.toolUses : undefined;
143
143
  const transcriptId = row.transcriptId || undefined;
144
+ const parentToolCallId = typeof row.parentToolCallId === 'string' && row.parentToolCallId.length > 0 ? row.parentToolCallId : undefined;
144
145
  const startedAt = typeof row.startedAt === 'number' && row.startedAt > 0 ? row.startedAt : undefined;
145
146
  // 0.74.0:代际号(正整数才算;坏形 / 缺席 ⇒ undefined,绝不补 1)。
146
147
  const cycleSeq = typeof row.cycleSeq === 'number' && Number.isInteger(row.cycleSeq) && row.cycleSeq >= 1 ? row.cycleSeq : undefined;
@@ -191,6 +192,7 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
191
192
  (tokens !== undefined && prev?.tokens !== tokens) ||
192
193
  (toolUses !== undefined && prev?.toolUses !== toolUses) ||
193
194
  (transcriptId !== undefined && prev?.transcriptId !== transcriptId) ||
195
+ (parentToolCallId !== undefined && prev?.parentToolCallId !== parentToolCallId) ||
194
196
  (startedAt !== undefined && prev?.startedAt !== startedAt)) {
195
197
  publishEngineAgentPanelEvent({
196
198
  kind: 'fleet-row',
@@ -200,6 +202,7 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
200
202
  ...(knownTokens !== undefined ? { totalTokens: knownTokens } : {}),
201
203
  ...(toolUses !== undefined ? { toolUses } : {}),
202
204
  ...(transcriptId !== undefined ? { transcriptId } : {}),
205
+ ...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
203
206
  ...(knownStartedAt !== undefined ? { startedAt: knownStartedAt } : {}),
204
207
  ...(knownCycleSeq !== undefined ? { cycleSeq: knownCycleSeq } : {}),
205
208
  });
@@ -221,6 +224,7 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
221
224
  tokens: knownTokens,
222
225
  toolUses: toolUses !== undefined ? toolUses : prev?.toolUses,
223
226
  transcriptId: transcriptId !== undefined ? transcriptId : prev?.transcriptId,
227
+ parentToolCallId: parentToolCallId !== undefined ? parentToolCallId : prev?.parentToolCallId,
224
228
  startedAt: knownStartedAt,
225
229
  cycleSeq: knownCycleSeq,
226
230
  currentToolKey: currentToolKey !== undefined ? currentToolKey : prev?.currentToolKey,
@@ -243,6 +247,7 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
243
247
  prev.tokens !== knownTokens ||
244
248
  (toolUses !== undefined && prev.toolUses !== toolUses) ||
245
249
  (transcriptId !== undefined && prev.transcriptId !== transcriptId) ||
250
+ (parentToolCallId !== undefined && prev.parentToolCallId !== parentToolCallId) ||
246
251
  (startedAt !== undefined && prev.startedAt !== startedAt) ||
247
252
  (cycleSeq !== undefined && prev.cycleSeq !== cycleSeq) ||
248
253
  (currentToolKey !== undefined && prev.currentToolKey !== currentToolKey)) {
@@ -255,6 +260,7 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
255
260
  // 🔴 缺席即不落键(消费端据「键在不在」判三态)。
256
261
  ...(toolUses !== undefined ? { toolUses } : {}),
257
262
  ...(transcriptId !== undefined ? { transcriptId } : {}),
263
+ ...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
258
264
  ...(knownStartedAt !== undefined ? { startedAt: knownStartedAt } : {}),
259
265
  ...(knownCycleSeq !== undefined ? { cycleSeq: knownCycleSeq } : {}),
260
266
  ...(currentTool !== undefined ? { currentTool } : {}),
@@ -266,6 +272,7 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
266
272
  tokens: knownTokens,
267
273
  toolUses: toolUses !== undefined ? toolUses : prev?.toolUses,
268
274
  transcriptId: transcriptId !== undefined ? transcriptId : prev?.transcriptId,
275
+ parentToolCallId: parentToolCallId !== undefined ? parentToolCallId : prev?.parentToolCallId,
269
276
  startedAt: knownStartedAt,
270
277
  cycleSeq: knownCycleSeq,
271
278
  currentToolKey: currentToolKey !== undefined ? currentToolKey : prev?.currentToolKey,
@@ -67,6 +67,7 @@
67
67
  */
68
68
  import type { AgentEvent, ApprovalDecision, ApprovalStaleError, PendingCheckpoint, CheckpointGate, PlanReviewRequest, AssistantTaskStatus } from '@sema-agent/sdk';
69
69
  import type { ApprovalsListEnvelope } from './crashConverged.js';
70
+ import { type SuspendedReopenReading } from './suspendedReopen.js';
70
71
  /** durable `/decide` 腿的既有缺省拒因(不带归因时逐字不变 —— 0.27.0 及之前的 wire 字节)。 */
71
72
  export declare const DEFAULT_DENY_REASON = "The user rejected this tool use";
72
73
  /**
@@ -300,6 +301,8 @@ export interface ActiveGate {
300
301
  gate: CheckpointGate;
301
302
  /** The durable seq of the `suspended` event, for rewind/dedup correlation (types.ts:48 eventSeq). */
302
303
  seq?: string | undefined;
304
+ /** 0.74.3 CC-73:这条挂起是不是一次 reopen(`suspended.reopened` 三态,见 {@link suspendedReopenOf});每条 suspended 重读,不粘上一条。 */
305
+ reopened: SuspendedReopenReading;
303
306
  }
304
307
  /** Outcome of {@link findPendingForTask}. */
305
308
  export type FindPendingOutcome = {
@@ -394,6 +397,12 @@ export declare class HitlBridge {
394
397
  observe(ev: AgentEvent): void;
395
398
  /** The gate currently awaiting a human, if any. The shell branches on `gate.kind` to pick the chrome. */
396
399
  currentGate(): CheckpointGate | null;
400
+ /**
401
+ * 0.74.3 CC-73:当前挂起是不是一次 reopen(`suspended.reopened` 三态);没挂起 ⇒ `null`。
402
+ * 消费方据此把「真 reopen 成功、重试会重放」那半句说出来:`reopened` 在场码 ⇒ 卡仍 pending、决定没被消费、可再决;
403
+ * `not_reopened` / `unstated` ⇒ 普通挂起,一个字都不多说。
404
+ */
405
+ currentGateReopen(): SuspendedReopenReading | null;
397
406
  /**
398
407
  * Fetch the `PendingCheckpoint` this decide/answer is about to resolve, for the callers that do NOT
399
408
  * already have one in hand (see `decideTool`/`answerQuestion`'s `preResolvedPending` param — the wire
@@ -2,6 +2,7 @@ import { askParkForeignGateKind } from '../adapter/activeRunSelfHeal.js';
2
2
  import { eventSeq } from '../adapter/types.js';
3
3
  import { hostLog } from '../host.js';
4
4
  import { abortableSleep } from '../abortableSleep.js';
5
+ import { suspendedReopenOf } from './suspendedReopen.js';
5
6
  // ── deny/plan-review 归因的 wire 窄化(0.28.0 发版扫描 F1/F2/F3 收编;单源,三条腿共用)──────
6
7
  //
7
8
  // server 对 `/decide` 与 plan-review 两条腿的 `reason` 同限 `MAX_APPROVAL_REASON_CHARS`(4096 字符,
@@ -417,10 +418,10 @@ export class HitlBridge {
417
418
  case 'suspended':
418
419
  // gate may be null (Event_suspended 'null' arm) → a generic "needs you" pause (contract/08 CS-9).
419
420
  if (ev.gate) {
420
- this.active = { gate: ev.gate, seq: eventSeq(ev) };
421
+ this.active = { gate: ev.gate, seq: eventSeq(ev), reopened: suspendedReopenOf(ev) };
421
422
  }
422
423
  else {
423
- this.active = { gate: { kind: 'human' }, seq: eventSeq(ev) };
424
+ this.active = { gate: { kind: 'human' }, seq: eventSeq(ev), reopened: suspendedReopenOf(ev) };
424
425
  }
425
426
  break;
426
427
  case 'done':
@@ -439,6 +440,14 @@ export class HitlBridge {
439
440
  currentGate() {
440
441
  return this.active?.gate ?? null;
441
442
  }
443
+ /**
444
+ * 0.74.3 CC-73:当前挂起是不是一次 reopen(`suspended.reopened` 三态);没挂起 ⇒ `null`。
445
+ * 消费方据此把「真 reopen 成功、重试会重放」那半句说出来:`reopened` 在场码 ⇒ 卡仍 pending、决定没被消费、可再决;
446
+ * `not_reopened` / `unstated` ⇒ 普通挂起,一个字都不多说。
447
+ */
448
+ currentGateReopen() {
449
+ return this.active?.reopened ?? null;
450
+ }
442
451
  /**
443
452
  * Fetch the `PendingCheckpoint` this decide/answer is about to resolve, for the callers that do NOT
444
453
  * already have one in hand (see `decideTool`/`answerQuestion`'s `preResolvedPending` param — the wire
@@ -0,0 +1,30 @@
1
+ /**
2
+ * suspendedReopen — durable `suspended` 事件上 server 的**重开侧记键** `reopened` 的三态读口(0.74.3 CC-73;
3
+ * 契约 reopen 族段:`GET /v1/runs/{taskId}/events` 的 `suspended` 事件带 `reopened: <errorCode>`,「缺席 / null =
4
+ * 这次 park 不是重开」;sdk `events.d.ts` `reopened?: string | null`,普通挂起帧不带此键)。
5
+ *
6
+ * ── 🔴 为什么是三态而不是「是 / 不是」两态 ─────────────────────────────────────────────────
7
+ * 契约把「缺席」和「null」判成同一句话,但两者的**说话人不同**:`null` 是新 server 明说「不是重开」;键缺席既可能是
8
+ * 新 server 的普通挂起帧,也可能是根本没有这个键的老 server。把两者折成一个值,消费方就再也分不出「server 说了不是」
9
+ * 与「server 什么都没说」。读口把它们分开带出,消费方按契约把 `unstated` **当作**不是重开即可,但不替 server 说那句话。
10
+ *
11
+ * 码是**开集原词**(词表属主在 server:今天三员 `resume.env_failed` / `resume.tool_unavailable` /
12
+ * `resume.tool_contract_mismatch`,见 `engineErrorCodes.ts` `RESUME_REOPEN_CODES`),本读口不过滤;要文案的消费方拿码去查表。
13
+ */
14
+ /** `suspended.reopened` 的读数。 */
15
+ export type SuspendedReopenReading =
16
+ /** 键在场且是非空串:这条挂起是一次**重开**,`code` = server 给的 errorCode 原词。 */
17
+ {
18
+ readonly kind: 'reopened';
19
+ readonly code: string;
20
+ }
21
+ /** 键在场且显式 `null`:server 明说这次 park 不是重开。 */
22
+ | {
23
+ readonly kind: 'not_reopened';
24
+ }
25
+ /** 键缺席(或值读不出):server 没说;按契约当作不是重开,但不冒充 `null`。 */
26
+ | {
27
+ readonly kind: 'unstated';
28
+ };
29
+ /** 任意事件体 → 三态读数;非对象 / 坏形一律 `unstated`,永不抛。 */
30
+ export declare function suspendedReopenOf(ev: unknown): SuspendedReopenReading;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * suspendedReopen — durable `suspended` 事件上 server 的**重开侧记键** `reopened` 的三态读口(0.74.3 CC-73;
3
+ * 契约 reopen 族段:`GET /v1/runs/{taskId}/events` 的 `suspended` 事件带 `reopened: <errorCode>`,「缺席 / null =
4
+ * 这次 park 不是重开」;sdk `events.d.ts` `reopened?: string | null`,普通挂起帧不带此键)。
5
+ *
6
+ * ── 🔴 为什么是三态而不是「是 / 不是」两态 ─────────────────────────────────────────────────
7
+ * 契约把「缺席」和「null」判成同一句话,但两者的**说话人不同**:`null` 是新 server 明说「不是重开」;键缺席既可能是
8
+ * 新 server 的普通挂起帧,也可能是根本没有这个键的老 server。把两者折成一个值,消费方就再也分不出「server 说了不是」
9
+ * 与「server 什么都没说」。读口把它们分开带出,消费方按契约把 `unstated` **当作**不是重开即可,但不替 server 说那句话。
10
+ *
11
+ * 码是**开集原词**(词表属主在 server:今天三员 `resume.env_failed` / `resume.tool_unavailable` /
12
+ * `resume.tool_contract_mismatch`,见 `engineErrorCodes.ts` `RESUME_REOPEN_CODES`),本读口不过滤;要文案的消费方拿码去查表。
13
+ */
14
+ const UNSTATED = Object.freeze({ kind: 'unstated' });
15
+ const NOT_REOPENED = Object.freeze({ kind: 'not_reopened' });
16
+ /** 任意事件体 → 三态读数;非对象 / 坏形一律 `unstated`,永不抛。 */
17
+ export function suspendedReopenOf(ev) {
18
+ if (ev === null || typeof ev !== 'object' || Array.isArray(ev))
19
+ return UNSTATED;
20
+ const o = ev;
21
+ if (!Object.hasOwn(o, 'reopened'))
22
+ return UNSTATED;
23
+ const v = o.reopened;
24
+ if (v === null)
25
+ return NOT_REOPENED;
26
+ return typeof v === 'string' && v.length > 0 ? { kind: 'reopened', code: v } : UNSTATED;
27
+ }
package/dist/index.d.ts CHANGED
@@ -161,6 +161,8 @@ export * from './deviceExecutorManagementCapability.js';
161
161
  export * from './mcpReconnect.js';
162
162
  export * from './leaderConflict.js';
163
163
  export * from './runTerminal.js';
164
+ export * from './runCancelContext.js';
165
+ export * from './argvFlagValue.js';
164
166
  export * from './readFacePosture.js';
165
167
  export * from './mcpPanel.js';
166
168
  export * from './effectiveFacts.js';
@@ -253,6 +255,7 @@ export * from './hooksWireCaps.js';
253
255
  export * from './liveInitToolFace.js';
254
256
  export * from './model/providerPresets.js';
255
257
  export * from './hitl/hitlBridge.js';
258
+ export * from './hitl/suspendedReopen.js';
256
259
  export { toolEndOutputText, ENGINE_ABORT_TOOL_RESULT } from './hitl/frameRouter.js';
257
260
  export { isAskTool } from './hitl/frameRouter.js';
258
261
  export * from './hitl/hitlHostSurface.js';
package/dist/index.js CHANGED
@@ -202,6 +202,10 @@ export * from './mcpReconnect.js';
202
202
  export * from './leaderConflict.js';
203
203
  // 0.60.0(engine ≥7.64.0 / sdk 8.4.0):终局因由与门记录两只**判定归包**的读器。
204
204
  export * from './runTerminal.js';
205
+ // 0.74.3 CC-71:run 记录 cancelContext 读口 + turn_aborted{engine_error} 后的成因闭集四词(cli L-212 (a) 消费半场归包)
206
+ export * from './runCancelContext.js';
207
+ // 0.74.3 CC-80(core 交叉扫 X-5 / cli L-R1):argv 末次旗值扫描原语上公面 —— 包内四消费者早已收编,端还在造第 N 份手写循环
208
+ export * from './argvFlagValue.js';
205
209
  // 0.61.0(engine ≥7.65.0 / sdk 8.5.0 / S-167):READ 容纳面档位 + 来源的 operator 面窄读器
206
210
  // (`WiringDiagnostics.readFace`)。与 `writeProtectionCapability` / `gateOutcome` 同构同纪律
207
211
  // (防御读 / 唯一措辞铸点 / UNTRUSTED-for-display);与租户面 `capabilities.readFace` 刻意不合流,
@@ -438,6 +442,8 @@ export * from './model/providerPresets.js';
438
442
  // 共享宿主通知面 + cancel-by-deny 有界观察 —— `askGateWire.ts` 已 import `toolApprovalWire.ts`,
439
443
  // 反向 import 会成环,所以两条腿共用的那一组挪进一个两边都能到达的独立模块。
440
444
  export * from './hitl/hitlBridge.js';
445
+ // 0.74.3 CC-73:durable suspended.reopened 三态读口(cli L-56 ②)
446
+ export * from './hitl/suspendedReopen.js';
441
447
  // [C93] P0 直讨(0.17.1):toolEndOutputText 单一真源导出——web/desktop 被点名「两端务必接」的
442
448
  // 6.0.0 output content-block 归一,此前 frameRouter 内私有,两端手抄=漂移温床。frameRouter 只
443
449
  // 挑名导出这一件(整星导出会把路由内部面全泄进公面)。
@@ -0,0 +1,49 @@
1
+ /**
2
+ * runCancelContext — run 记录上 server 自铸的**取消现场**侧记键 `cancelContext`(server ≥7.87.3 `PersistedTaskResult =
3
+ * TaskResult & { cancelContext? }`,`dist/plugins/store-contracts.d.ts` `RunCancelContext` 四座)的三端共用读面,与
4
+ * `turn_aborted{reason:'engine_error'}` 之后「到底是谁把这条 run 结掉的」的**成因判定**(0.74.3 CC-71;cli L-212 (a) 消费半场归包)。
5
+ * sdk 9.7.1 对该键无型 ⇒ 按契约结构读。
6
+ *
7
+ * ── 🔴 读法 ─────────────────────────────────────────────────────────────────────────────────
8
+ * ① `cancelContext` 缺席 = 老 server / 这条 run 不是被取消的 / 记录形不带 —— **不折成「未取消」**,读数是 `not_reported`。
9
+ * ② 四座各自可缺席、各自防御读:`lastEventKind`(串)/ `lastEventAgeMs`(≥0 有限数)/ `elapsedMs`(≥0 有限数;绝不折 0)/
10
+ * `lastBrainStatus`(对象,原样带出不窄读)。「有 age 无 kind」是合法读数。
11
+ * ③ 成因判定 {@link classifyRunAbortCause}:只由 run 记录上的**机读证据**得出 —— `errorCode` ∈ 取消码 ⇒ `cancelled_by_user`;
12
+ * `cancelContext` 在场而码不是取消码 ⇒ 仍按码判(现场只是附注);码缺席 / 认不出 ⇒ `unknown`。**缺席不猜**。
13
+ */
14
+ /** 四座读数(在场才带)。 */
15
+ export interface RunCancelContextView {
16
+ readonly lastEventKind?: string;
17
+ readonly lastEventAgeMs?: number;
18
+ readonly elapsedMs?: number;
19
+ readonly lastBrainStatus?: Readonly<Record<string, unknown>>;
20
+ }
21
+ export type RunCancelContextReading = {
22
+ kind: 'not_reported';
23
+ } | {
24
+ kind: 'present';
25
+ context: RunCancelContextView;
26
+ };
27
+ /** run 记录(`runs.get` 回体 / `done.result`)→ 取消现场读数;记录非对象 ⇒ `undefined`(判不出,与 not_reported 分开)。 */
28
+ export declare function readRunCancelContext(record: unknown): RunCancelContextReading | undefined;
29
+ import { type RunTerminalRead } from './runTerminal.js';
30
+ export type RunAbortCause = {
31
+ kind: 'cancelled';
32
+ context?: RunCancelContextView;
33
+ } | {
34
+ kind: 'engine_error';
35
+ code?: string;
36
+ message?: string;
37
+ }
38
+ /** run 记录仍非终态:流断了而 run 还在(壳这一端的事实,不是 run 的成因)。`status` 原词。 */
39
+ | {
40
+ kind: 'run_still_live';
41
+ status?: string;
42
+ } | {
43
+ kind: 'unknown';
44
+ terminal?: RunTerminalRead['kind'];
45
+ };
46
+ /** 取消码常量(server `CANCELLED_CODE` / sdk runs.d.ts 逐字)。 */
47
+ export declare const RUN_CANCELLED_CODE: "cancelled";
48
+ /** 壳带 `engineTaskId` 去 `runs.get` 拿回的记录 ⇒ 成因。只由机读证据得出;缺席不猜。 */
49
+ export declare function classifyRunAbortCause(record: unknown): RunAbortCause;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * runCancelContext — run 记录上 server 自铸的**取消现场**侧记键 `cancelContext`(server ≥7.87.3 `PersistedTaskResult =
3
+ * TaskResult & { cancelContext? }`,`dist/plugins/store-contracts.d.ts` `RunCancelContext` 四座)的三端共用读面,与
4
+ * `turn_aborted{reason:'engine_error'}` 之后「到底是谁把这条 run 结掉的」的**成因判定**(0.74.3 CC-71;cli L-212 (a) 消费半场归包)。
5
+ * sdk 9.7.1 对该键无型 ⇒ 按契约结构读。
6
+ *
7
+ * ── 🔴 读法 ─────────────────────────────────────────────────────────────────────────────────
8
+ * ① `cancelContext` 缺席 = 老 server / 这条 run 不是被取消的 / 记录形不带 —— **不折成「未取消」**,读数是 `not_reported`。
9
+ * ② 四座各自可缺席、各自防御读:`lastEventKind`(串)/ `lastEventAgeMs`(≥0 有限数)/ `elapsedMs`(≥0 有限数;绝不折 0)/
10
+ * `lastBrainStatus`(对象,原样带出不窄读)。「有 age 无 kind」是合法读数。
11
+ * ③ 成因判定 {@link classifyRunAbortCause}:只由 run 记录上的**机读证据**得出 —— `errorCode` ∈ 取消码 ⇒ `cancelled_by_user`;
12
+ * `cancelContext` 在场而码不是取消码 ⇒ 仍按码判(现场只是附注);码缺席 / 认不出 ⇒ `unknown`。**缺席不猜**。
13
+ */
14
+ function nonNegFinite(v) {
15
+ return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : undefined;
16
+ }
17
+ /** run 记录(`runs.get` 回体 / `done.result`)→ 取消现场读数;记录非对象 ⇒ `undefined`(判不出,与 not_reported 分开)。 */
18
+ export function readRunCancelContext(record) {
19
+ if (record === null || typeof record !== 'object')
20
+ return undefined;
21
+ const r = record;
22
+ // 记录形两种:顶层 `result.cancelContext`(PersistedTaskResult)或已经是 result 本身。
23
+ const holder = r.result !== null && typeof r.result === 'object' ? r.result : r;
24
+ if (!Object.hasOwn(holder, 'cancelContext') || holder.cancelContext === undefined)
25
+ return { kind: 'not_reported' };
26
+ const c = holder.cancelContext;
27
+ if (c === null || typeof c !== 'object' || Array.isArray(c))
28
+ return undefined;
29
+ const o = c;
30
+ const kind = typeof o.lastEventKind === 'string' && o.lastEventKind.length > 0 ? o.lastEventKind : undefined;
31
+ const age = nonNegFinite(o.lastEventAgeMs);
32
+ const elapsed = nonNegFinite(o.elapsedMs);
33
+ const brain = o.lastBrainStatus !== null && typeof o.lastBrainStatus === 'object' && !Array.isArray(o.lastBrainStatus) ? o.lastBrainStatus : undefined;
34
+ return {
35
+ kind: 'present',
36
+ context: {
37
+ ...(kind !== undefined ? { lastEventKind: kind } : {}),
38
+ ...(age !== undefined ? { lastEventAgeMs: age } : {}),
39
+ ...(elapsed !== undefined ? { elapsedMs: elapsed } : {}),
40
+ ...(brain !== undefined ? { lastBrainStatus: brain } : {}),
41
+ },
42
+ };
43
+ }
44
+ // ── 成因判定(闭集四词;cli L-212 (a) 对齐)────────────────────────────────────────────────
45
+ //
46
+ // 字节出处(cli 亲 grep server 7.87.3 dist):
47
+ // · 被取消 = 唯一铸点 `run-cancel-context.js` `mintCancelledResult` ⇒ `terminal:{kind:"failed", code:"cancelled"}`,
48
+ // 且**只有这条路**挂 `cancelContext`;
49
+ // · 引擎错 = `terminal.kind === "failed"` 且 code !== "cancelled";code 是 core 的**开集**(network / brain.* / limits.* …)⇒ 只渲原码,不枚举;
50
+ // · 客户端断连**没有独立字节**:server 走的是 detach / park(durable run 继续跑、在等的 ask 停驻),run 记录不会因此失败 ⇒
51
+ // 它不是 run 的成因,是壳这一端的事实 —— 判法 = run 记录仍非终态(running / queued / suspended / needs_review)⇒ `run_still_live`。
52
+ // · 别的(完成 / 挡住 / 读不出终态词)⇒ `unknown`,不猜。
53
+ import { readRunTerminal } from './runTerminal.js';
54
+ /** 取消码常量(server `CANCELLED_CODE` / sdk runs.d.ts 逐字)。 */
55
+ export const RUN_CANCELLED_CODE = 'cancelled';
56
+ const LIVE_STATUSES = new Set(['running', 'queued', 'suspended', 'needs_review']);
57
+ /** 壳带 `engineTaskId` 去 `runs.get` 拿回的记录 ⇒ 成因。只由机读证据得出;缺席不猜。 */
58
+ export function classifyRunAbortCause(record) {
59
+ if (record === null || typeof record !== 'object')
60
+ return { kind: 'unknown' };
61
+ const r = record;
62
+ const status = typeof r.status === 'string' && r.status.length > 0 ? r.status : undefined;
63
+ const holder = r.result !== null && typeof r.result === 'object' ? r.result : record;
64
+ const read = readRunTerminal(holder);
65
+ if (read !== null && read.kind === 'failed') {
66
+ if (read.code === RUN_CANCELLED_CODE) {
67
+ const cc = readRunCancelContext(record);
68
+ return { kind: 'cancelled', ...(cc?.kind === 'present' ? { context: cc.context } : {}) };
69
+ }
70
+ return { kind: 'engine_error', ...(read.code !== undefined ? { code: read.code } : {}), ...(read.message !== undefined ? { message: read.message } : {}) };
71
+ }
72
+ // 非终态 = run 还活着:嵌套 paused 臂 / 平面 paused 词 / 行上 status 仍是在飞词(平面读器对 running / queued 答 unknown{word},
73
+ // 那不是「读不出」,是「还没结」—— 所以先看 status 再落 unknown)。
74
+ if (read !== null && read.kind === 'paused')
75
+ return { kind: 'run_still_live', ...(status !== undefined ? { status } : {}) };
76
+ // 异源复审 R2 [medium]:result 已是终态(completed / blocked)而行上 status 还是在飞词 = 两处证据矛盾 ⇒ unknown(带观测到的终态词),
77
+ // 不按外层 status 说「还活着」——result 是终局记录,行状态可能滞后;矛盾不是 liveness 的证据。
78
+ if ((read === null || read.kind === 'unknown') && status !== undefined && LIVE_STATUSES.has(status))
79
+ return { kind: 'run_still_live', status };
80
+ return { kind: 'unknown', ...(read !== null ? { terminal: read.kind } : {}) };
81
+ }
@@ -173,8 +173,8 @@ export function sqlEngineDoctorDetail(reading) {
173
173
  switch (reading.kind) {
174
174
  case 'unobserved':
175
175
  // 0.59.2(cli B-038):不点名任何端的命令 —— 「去哪里看」是端的事,包只说事实。
176
- return ('not observed — the engine reports it on /v1/capabilities; this process has not received ' +
177
- 'a capabilities response from the engine yet');
176
+ return ('not observed — the engine reports it on /v1/capabilities; this process has no usable ' +
177
+ 'capabilities reading cached for it (none received, or the last one was unreadable)');
178
178
  case 'not_reported':
179
179
  return 'not reported by this engine — the capability position needs a newer engine';
180
180
  case 'none':
@@ -126,7 +126,7 @@ export function webSearchBackendDoctorDetail(reading) {
126
126
  switch (reading.kind) {
127
127
  case 'unobserved':
128
128
  return ('deployment default backend not observed — the engine reports it on /v1/capabilities; ' +
129
- 'this process has not received a capabilities response from the engine yet');
129
+ 'this process has no usable capabilities reading cached for it (none received, or the last one was unreadable)');
130
130
  case 'not_reported':
131
131
  return ('deployment default backend not reported by this engine — only newer engines advertise it; ' +
132
132
  'this does not say whether the deployment has a search backend');
@@ -176,8 +176,8 @@ const WP_DETAIL_MAX = 40;
176
176
  export function writeProtectionDoctorDetail(reading) {
177
177
  switch (reading.kind) {
178
178
  case 'unobserved':
179
- return ('not observed — the engine reports it on /v1/capabilities; this process has not received ' +
180
- 'a capabilities response from the engine yet');
179
+ return ('not observed — the engine reports it on /v1/capabilities; this process has no usable ' +
180
+ 'capabilities reading cached for it (none received, or the last one was unreadable)');
181
181
  case 'not_reported':
182
182
  return ('not reported by this engine — the capability position needs a newer engine; this says ' +
183
183
  'nothing about whether a protected-name table is in effect (an absent seat is the engine ' +