@sema-agent/client-core 0.74.1 → 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.
@@ -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
+ }
@@ -43,7 +43,7 @@
43
43
  */
44
44
  import { capForDisplay } from './fleetTaskDesc.js';
45
45
  import { engineWireTarget } from './engineWireTarget.js';
46
- import { engineCapsGeneration } from './engineCapsCache.js';
46
+ import { snapshotCapsGeneration, capsGenerationStillCurrent } from './engineCapsGenerationGuard.js';
47
47
  /**
48
48
  * caps 回体 → 本格读数;**畸形一律 `undefined`**(= 这一格不写 ⇒ 读口答 `unobserved`)。
49
49
  *
@@ -66,15 +66,19 @@ export function projectSqlEngineCapability(caps) {
66
66
  if (typeof sql !== 'object')
67
67
  return undefined;
68
68
  const s = sql;
69
- if (typeof s.engine !== 'string' || s.engine === '')
69
+ // 🔴 只读一次(0.74.2 CC-69 复审 R1 族扫):校验与返回各读一次,变化的 getter 会让校验读到合形、返回读到别的。
70
+ const engine = s.engine;
71
+ const isolation = s.isolation;
72
+ const txnMode = s.txnMode;
73
+ if (typeof engine !== 'string' || engine === '')
70
74
  return undefined;
71
- if (typeof s.isolation !== 'string' || s.isolation === '')
75
+ if (typeof isolation !== 'string' || isolation === '')
72
76
  return undefined;
73
77
  // 🔴 `txnMode`:`null` 是**铸点域内的合法值**(见 SqlEngineCapabilityView 顶注),不是畸形。
74
78
  // 但这一位在铸点上**恒在场** ⇒ 键缺席(`undefined`)与非串非 null 仍判载体坏了。
75
- if (s.txnMode !== null && (typeof s.txnMode !== 'string' || s.txnMode === ''))
79
+ if (txnMode !== null && (typeof txnMode !== 'string' || txnMode === ''))
76
80
  return undefined;
77
- return { kind: 'present', view: { engine: s.engine, isolation: s.isolation, txnMode: s.txnMode } };
81
+ return { kind: 'present', view: { engine, isolation, txnMode } };
78
82
  }
79
83
  const readingByBase = new Map();
80
84
  /**
@@ -99,22 +103,32 @@ const readingByBase = new Map();
99
103
  * 有任何东西变坏。🔴 **tee 自己抓号是假判据**(陈旧那一发在这里读到的是新代号,永远像当代的)。
100
104
  */
101
105
  export function noteEngineCapsForSqlEngine(baseUrl, caps, opts) {
106
+ if (typeof baseUrl !== 'string' || baseUrl === '')
107
+ return;
108
+ // 🔴 代际只在入口**快照一次**(异源对抗复审 R3):opts 是调用方的对象,回体上的 getter 能在投影中途改它;首末两次核对都用这份快照,
109
+ // 读 opts / 读代际抛出 ⇒ 不动格(与陈旧代际同一处置)。
110
+ const gen = snapshotCapsGeneration(opts);
111
+ if (gen === null)
112
+ return; // 调用方 opts 的 getter 抛出 = 供给坏了 ⇒ 不动格
113
+ if (!capsGenerationStillCurrent(baseUrl, gen))
114
+ return;
115
+ let reading;
102
116
  try {
103
- if (typeof baseUrl !== 'string' || baseUrl === '')
104
- return;
105
- // 陈旧代际的那一发整只丢掉(见本函数顶注的 `opts.generation` 段);不传 ⇒ 照写,零行为变化。
106
- if (opts?.generation !== undefined && opts.generation !== engineCapsGeneration(baseUrl))
107
- return;
108
- const reading = projectSqlEngineCapability(caps);
109
- if (reading === undefined) {
110
- readingByBase.delete(baseUrl);
111
- return;
112
- }
113
- readingByBase.set(baseUrl, reading);
117
+ reading = projectSqlEngineCapability(caps);
114
118
  }
115
119
  catch {
116
- /* fail-soft:本 tee 任何分支都不许打断 caps 探测链 */
120
+ // 🔴 0.74.2 同形存量(CC-69 复审 R1 族扫):抛 getter / Proxy 的回体 = 畸形,与「投影答 undefined」同一处置 —— 删格,不许留着上一台的读数。
121
+ reading = undefined;
122
+ }
123
+ // 🔴 投影之后**再核一次代际**(异源对抗复审 R2):投影会执行回体上不可信的 getter,它可以在中途让这一格换代
124
+ // (invalidate + 新代际的 tee 已写入);此时无论投影返回还是抛出,都不许动新代际的格。
125
+ if (!capsGenerationStillCurrent(baseUrl, gen))
126
+ return;
127
+ if (reading === undefined) {
128
+ readingByBase.delete(baseUrl);
129
+ return;
117
130
  }
131
+ readingByBase.set(baseUrl, reading);
118
132
  }
119
133
  /**
120
134
  * 本进程观测到的 SQL 姿态;这一格空缺 ⇒ `{kind:'unobserved'}`(**绝不**折成 `none`)。
@@ -159,8 +173,8 @@ export function sqlEngineDoctorDetail(reading) {
159
173
  switch (reading.kind) {
160
174
  case 'unobserved':
161
175
  // 0.59.2(cli B-038):不点名任何端的命令 —— 「去哪里看」是端的事,包只说事实。
162
- return ('not observed — the engine reports it on /v1/capabilities; this process has not received ' +
163
- '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)');
164
178
  case 'not_reported':
165
179
  return 'not reported by this engine — the capability position needs a newer engine';
166
180
  case 'none':
@@ -38,7 +38,7 @@
38
38
  */
39
39
  import { capForDisplay } from './fleetTaskDesc.js';
40
40
  import { engineWireTarget } from './engineWireTarget.js';
41
- import { engineCapsGeneration } from './engineCapsCache.js';
41
+ import { snapshotCapsGeneration, capsGenerationStillCurrent } from './engineCapsGenerationGuard.js';
42
42
  /** `backend: "none"` —— 唯一带独立语义的词(见文件头);其余词按开集读。 */
43
43
  export const WEB_SEARCH_BACKEND_NONE = 'none';
44
44
  /**
@@ -77,21 +77,32 @@ const readingByBase = new Map();
77
77
  * 那一刻 `engineCapsGeneration(baseUrl)` 抓号、在这里原样交回,陈旧那一发整只丢掉;不传 ⇒ 照写。
78
78
  */
79
79
  export function noteEngineCapsForWebSearchBackend(baseUrl, caps, opts) {
80
+ if (typeof baseUrl !== 'string' || baseUrl === '')
81
+ return;
82
+ // 🔴 代际只在入口**快照一次**(异源对抗复审 R3):opts 是调用方的对象,回体上的 getter 能在投影中途改它;首末两次核对都用这份快照,
83
+ // 读 opts / 读代际抛出 ⇒ 不动格(与陈旧代际同一处置)。
84
+ const gen = snapshotCapsGeneration(opts);
85
+ if (gen === null)
86
+ return; // 调用方 opts 的 getter 抛出 = 供给坏了 ⇒ 不动格
87
+ if (!capsGenerationStillCurrent(baseUrl, gen))
88
+ return;
89
+ let reading;
80
90
  try {
81
- if (typeof baseUrl !== 'string' || baseUrl === '')
82
- return;
83
- if (opts?.generation !== undefined && opts.generation !== engineCapsGeneration(baseUrl))
84
- return;
85
- const reading = projectWebSearchBackendCapability(caps);
86
- if (reading === undefined) {
87
- readingByBase.delete(baseUrl);
88
- return;
89
- }
90
- readingByBase.set(baseUrl, reading);
91
+ reading = projectWebSearchBackendCapability(caps);
91
92
  }
92
93
  catch {
93
- /* fail-soft:本 tee 任何分支都不许打断 caps 探测链 */
94
+ // 🔴 0.74.2 同形存量(CC-69 复审 R1 族扫):抛 getter / Proxy 的回体 = 畸形,与「投影答 undefined」同一处置 —— 删格,不许留着上一台的读数。
95
+ reading = undefined;
96
+ }
97
+ // 🔴 投影之后**再核一次代际**(异源对抗复审 R2):投影会执行回体上不可信的 getter,它可以在中途让这一格换代
98
+ // (invalidate + 新代际的 tee 已写入);此时无论投影返回还是抛出,都不许动新代际的格。
99
+ if (!capsGenerationStillCurrent(baseUrl, gen))
100
+ return;
101
+ if (reading === undefined) {
102
+ readingByBase.delete(baseUrl);
103
+ return;
94
104
  }
105
+ readingByBase.set(baseUrl, reading);
95
106
  }
96
107
  /** 本进程观测到的部署默认后端;这一格空缺 ⇒ `{kind:'unobserved'}`(**绝不**折成 `none`)。 */
97
108
  export function observedWebSearchBackend(baseUrl = engineWireTarget()?.baseUrl) {
@@ -115,7 +126,7 @@ export function webSearchBackendDoctorDetail(reading) {
115
126
  switch (reading.kind) {
116
127
  case 'unobserved':
117
128
  return ('deployment default backend not observed — the engine reports it on /v1/capabilities; ' +
118
- '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)');
119
130
  case 'not_reported':
120
131
  return ('deployment default backend not reported by this engine — only newer engines advertise it; ' +
121
132
  'this does not say whether the deployment has a search backend');
@@ -31,7 +31,7 @@
31
31
  */
32
32
  import { capForDisplay } from './fleetTaskDesc.js';
33
33
  import { engineWireTarget } from './engineWireTarget.js';
34
- import { engineCapsGeneration } from './engineCapsCache.js';
34
+ import { snapshotCapsGeneration, capsGenerationStillCurrent } from './engineCapsGenerationGuard.js';
35
35
  /**
36
36
  * caps 回体 → 本格读数;**畸形一律 `undefined`**(= 这一格不写 ⇒ 读口答 `unobserved`)。
37
37
  *
@@ -55,13 +55,17 @@ export function projectWriteProtectionCapability(caps) {
55
55
  if (typeof wp !== 'object' || Array.isArray(wp))
56
56
  return undefined;
57
57
  const w = wp;
58
- if (typeof w.armed !== 'boolean')
58
+ // 🔴 只读一次(0.74.2 CC-69 复审 R1 族扫):校验与返回各读一次,变化的 getter 会让校验读到合形、返回读到别的。
59
+ const armed = w.armed;
60
+ const replaced = w.replaced;
61
+ const rows = w.rows;
62
+ if (typeof armed !== 'boolean')
59
63
  return undefined;
60
- if (typeof w.replaced !== 'boolean')
64
+ if (typeof replaced !== 'boolean')
61
65
  return undefined;
62
- if (typeof w.rows !== 'number' || !Number.isInteger(w.rows) || w.rows < 0)
66
+ if (typeof rows !== 'number' || !Number.isInteger(rows) || rows < 0)
63
67
  return undefined;
64
- return { kind: 'present', view: { armed: w.armed, rows: w.rows, replaced: w.replaced } };
68
+ return { kind: 'present', view: { armed, rows, replaced } };
65
69
  }
66
70
  /**
67
71
  * operator 面回体的 `writeProtection` 段 → 读数;不成形 ⇒ `undefined`。
@@ -124,22 +128,32 @@ const readingByBase = new Map();
124
128
  * 有任何东西变坏。🔴 **tee 自己抓号是假判据**(陈旧那一发在这里读到的是新代号,永远像当代的)。
125
129
  */
126
130
  export function noteEngineCapsForWriteProtection(baseUrl, caps, opts) {
131
+ if (typeof baseUrl !== 'string' || baseUrl === '')
132
+ return;
133
+ // 🔴 代际只在入口**快照一次**(异源对抗复审 R3):opts 是调用方的对象,回体上的 getter 能在投影中途改它;首末两次核对都用这份快照,
134
+ // 读 opts / 读代际抛出 ⇒ 不动格(与陈旧代际同一处置)。
135
+ const gen = snapshotCapsGeneration(opts);
136
+ if (gen === null)
137
+ return; // 调用方 opts 的 getter 抛出 = 供给坏了 ⇒ 不动格
138
+ if (!capsGenerationStillCurrent(baseUrl, gen))
139
+ return;
140
+ let reading;
127
141
  try {
128
- if (typeof baseUrl !== 'string' || baseUrl === '')
129
- return;
130
- // 陈旧代际的那一发整只丢掉(见本函数顶注的 `opts.generation` 段);不传 ⇒ 照写,零行为变化。
131
- if (opts?.generation !== undefined && opts.generation !== engineCapsGeneration(baseUrl))
132
- return;
133
- const reading = projectWriteProtectionCapability(caps);
134
- if (reading === undefined) {
135
- readingByBase.delete(baseUrl);
136
- return;
137
- }
138
- readingByBase.set(baseUrl, reading);
142
+ reading = projectWriteProtectionCapability(caps);
139
143
  }
140
144
  catch {
141
- /* fail-soft:本 tee 任何分支都不许打断 caps 探测链 */
145
+ // 🔴 0.74.2 同形存量(CC-69 复审 R1 族扫):抛 getter / Proxy 的回体 = 畸形,与「投影答 undefined」同一处置 —— 删格,不许留着上一台的读数。
146
+ reading = undefined;
147
+ }
148
+ // 🔴 投影之后**再核一次代际**(异源对抗复审 R2):投影会执行回体上不可信的 getter,它可以在中途让这一格换代
149
+ // (invalidate + 新代际的 tee 已写入);此时无论投影返回还是抛出,都不许动新代际的格。
150
+ if (!capsGenerationStillCurrent(baseUrl, gen))
151
+ return;
152
+ if (reading === undefined) {
153
+ readingByBase.delete(baseUrl);
154
+ return;
142
155
  }
156
+ readingByBase.set(baseUrl, reading);
143
157
  }
144
158
  /** 本进程观测到的写保护姿态;这一格空缺 ⇒ `{kind:'unobserved'}`(**绝不**折成 `none`)。 */
145
159
  export function observedWriteProtection(baseUrl = engineWireTarget()?.baseUrl) {
@@ -162,8 +176,8 @@ const WP_DETAIL_MAX = 40;
162
176
  export function writeProtectionDoctorDetail(reading) {
163
177
  switch (reading.kind) {
164
178
  case 'unobserved':
165
- return ('not observed — the engine reports it on /v1/capabilities; this process has not received ' +
166
- '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)');
167
181
  case 'not_reported':
168
182
  return ('not reported by this engine — the capability position needs a newer engine; this says ' +
169
183
  'nothing about whether a protected-name table is in effect (an absent seat is the engine ' +