@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.
@@ -88,7 +88,227 @@ export function isStaleEngineAgentPanelEnd(end, current) {
88
88
  return es < cs;
89
89
  return false;
90
90
  }
91
+ // ── 0.74.3 CC-70:子代身份归一(所有 lane 的唯一漏斗)────────────────────────────────────────
92
+ //
93
+ // 病(cli L-407 / test 定谳,三端同撞):同一个后台子代在 wire 上有**两把身份** —— fleet 行 id 尾段(17 位,= 注册表 task_id
94
+ // = 壳本地 agentId,core #948 按构造)与主流 `task_progress.taskId`(引擎 UUIDv7 = transcriptId = childSessionId)。本包的
95
+ // `fleet-row` / 通知 `end` 用前者,tick / 流内 close 臂的 `end` 用后者 ⇒ 壳面板按 `ev.taskId` 落 Map 长成两行;包内常驻标也是
96
+ // 「UUID 记、17 位清」永不清。修法 = 在这一只漏斗里把 tick / end 的 `taskId` 归一到 fleet 尾段(消费端零改):
97
+ // · 钥匙两把,fleet-row 一到就登记:`transcriptId → 尾段`(主)、`parentToolCallId → 尾段`(退路);
98
+ // · 查得到 ⇒ 改写 `taskId`,原词进 `wireTaskId`,`taskIdOrigin:'fleet-row'`;常驻标若记在 UUID 下随之迁移;
99
+ // · 查不到:绑卡的 tick(同步委派,`cardBound`)直发;未绑的 tick **缓一拍**等 fleet-row(常态 fleet-row 先到几十秒,但非保证)——
100
+ // 触发 = 同 UUID 的 fleet-row 到(归一发)/ 同 UUID 第二条 tick(新的原样发,旧的丢)/ 同 UUID `end`(先 tick 后 end 原样发)/
101
+ // 缓冲满(最旧的原样发);原样发的带 `taskIdOrigin:'wire'`。🔴 `sweep` **不放**缓着的 tick:那条 tick 还没有行,sweep 没有东西可结;
102
+ // 在「tick 先到、父 turn 先结、行帧后到」的窗里放出去反而把 UUID 行造出来、接着行帧再造第二行 —— 正是要修的病。
103
+ // · 两张钥匙表与缓一拍都有界(常量),溢出先进先出 —— 上限是内存约束不是语义,超出只退化成「原样发」;
104
+ // · 缓一拍还按**节拍**有界(异源复审 R1 [medium]):别的 fleet-row / end / sweep 每过一条算一拍(别的 tick 不算 —— 它说明不了行帧车道
105
+ // 活不活,一阵子代同时起步时也不该互相挤),过 MAX_HELD_WIRE_TICK_BEATS 拍还没等到自己的行帧 ⇒ 原样放(store 无计时器,拍是唯一可数的时间);
106
+ // · 钥匙带**周期身份**(复审 R1 [high]):行帧登记时连 cycleSeq / startedAt 一起记,归一后的 end 若自己不带周期就借钥匙上的
107
+ // (流内 close 臂的 end 恒不带)—— 复活后旧 transcriptId 的钥匙仍在,迟到的 end{旧 UUID} 归一后带着旧代际,
108
+ // isStaleEngineAgentPanelEnd 才判得出它是前代;
109
+ // · tick 经退路键(parentToolCallId)归一时,顺手把 UUID → 尾段 记进主表(复审 R1 [high]):随后的 end{UUID} 不带 parentToolCallId,
110
+ // 没有这一步它永远归不了一。
111
+ /** 钥匙表上限(LRU:命中即续命,真在飞的子代不会被一阵新登记挤掉;超出只退化成「原样发」)。 */
112
+ const MAX_IDENTITY_KEYS = 2048;
113
+ const MAX_HELD_WIRE_TICKS = 64;
114
+ /** 缓一拍的节拍上限:别的 fleet-row / end / sweep 每过一条算一拍(别的 tick 不算)。 */
115
+ export const MAX_HELD_WIRE_TICK_BEATS = 16;
116
+ const fleetIdByTranscriptId = new Map();
117
+ const fleetIdByParentToolCallId = new Map();
118
+ const heldWireTicks = new Map();
119
+ /** 每个 fleet 尾段最近一条行帧带的周期身份(在场才记):清常驻标时用它判「这条终态是不是前代的」(异源复审 R3 [high])。 */
120
+ const latestCycleByFleetId = new Map();
121
+ function remember(map, key, alias) {
122
+ map.delete(key);
123
+ map.set(key, alias);
124
+ if (map.size > MAX_IDENTITY_KEYS) {
125
+ const oldest = map.keys().next().value;
126
+ if (oldest !== undefined)
127
+ map.delete(oldest);
128
+ }
129
+ }
130
+ /**
131
+ * 0.74.3 CC-70:把 wire 上的子代 id(task_progress 的 UUID)反查成 fleet 行 id 尾段;`transcriptId` 表优先,退 `parentToolCallId` 表;
132
+ * 都查不到 ⇒ 原样(`origin:'wire'`)。fleet 尾段本身传进来也原样(它就是目标键)。
133
+ */
134
+ export function resolveEnginePanelTaskId(wireTaskId, parentToolCallId) {
135
+ const byTranscript = fleetIdByTranscriptId.get(wireTaskId);
136
+ if (byTranscript !== undefined) {
137
+ remember(fleetIdByTranscriptId, wireTaskId, byTranscript); // LRU 续命(异源复审 R2 [high]:FIFO 会把仍在飞的子代挤掉)
138
+ return identityOf(byTranscript);
139
+ }
140
+ const byParent = parentToolCallId !== undefined ? fleetIdByParentToolCallId.get(parentToolCallId) : undefined;
141
+ if (byParent !== undefined) {
142
+ remember(fleetIdByParentToolCallId, parentToolCallId, byParent);
143
+ return identityOf(byParent);
144
+ }
145
+ return { taskId: wireTaskId, origin: 'wire' };
146
+ }
147
+ function identityOf(alias) {
148
+ return {
149
+ taskId: alias.fleetId,
150
+ origin: 'fleet-row',
151
+ ...(alias.cycleSeq !== undefined ? { cycleSeq: alias.cycleSeq } : {}),
152
+ ...(alias.startedAt !== undefined ? { startedAt: alias.startedAt } : {}),
153
+ };
154
+ }
155
+ /** 经退路键归一的 tick:把 UUID → 尾段 记进主表,随后不带 parentToolCallId 的 end{UUID} 才归得了一。 */
156
+ function rememberResolvedWire(wireTaskId, id) {
157
+ if (fleetIdByTranscriptId.has(wireTaskId))
158
+ return;
159
+ remember(fleetIdByTranscriptId, wireTaskId, {
160
+ fleetId: id.taskId,
161
+ ...(id.cycleSeq !== undefined ? { cycleSeq: id.cycleSeq } : {}),
162
+ ...(id.startedAt !== undefined ? { startedAt: id.startedAt } : {}),
163
+ });
164
+ }
165
+ /** 别的事件过了一拍:缓着的 tick 各记一拍,过上限的原样放出(先进先出)。 */
166
+ function ageHeld(except) {
167
+ for (const [wireId, held] of [...heldWireTicks]) {
168
+ if (wireId === except)
169
+ continue;
170
+ held.beats += 1;
171
+ if (held.beats >= MAX_HELD_WIRE_TICK_BEATS) {
172
+ heldWireTicks.delete(wireId);
173
+ deliverEngineAgentPanelEvent(asIsTick(held.ev));
174
+ }
175
+ }
176
+ }
177
+ function migrateResidency(wireTaskId, fleetId) {
178
+ if (wireTaskId !== fleetId && residentTaskIds.has(wireTaskId)) {
179
+ residentTaskIds.delete(wireTaskId);
180
+ residentTaskIds.add(fleetId);
181
+ }
182
+ }
183
+ function normalizedTick(ev, fleetId) {
184
+ const { cardBound: _cardBound, ...rest } = ev;
185
+ void _cardBound;
186
+ migrateResidency(ev.taskId, fleetId);
187
+ return { ...rest, taskId: fleetId, wireTaskId: ev.taskId, taskIdOrigin: 'fleet-row' };
188
+ }
189
+ function asIsTick(ev) {
190
+ const { cardBound: _cardBound, ...rest } = ev;
191
+ void _cardBound;
192
+ return { ...rest, taskIdOrigin: 'wire' };
193
+ }
194
+ function flushHeld(wireTaskId) {
195
+ const held = heldWireTicks.get(wireTaskId);
196
+ if (held === undefined)
197
+ return;
198
+ heldWireTicks.delete(wireTaskId);
199
+ const id = resolveEnginePanelTaskId(held.ev.taskId, held.ev.parentToolCallId);
200
+ if (id.origin === 'fleet-row')
201
+ rememberResolvedWire(held.ev.taskId, id);
202
+ deliverEngineAgentPanelEvent(id.origin === 'fleet-row' ? normalizedTick(held.ev, id.taskId) : asIsTick(held.ev));
203
+ }
204
+ /**
205
+ * 0.74.3 CC-70(异源复审 R3 [high]):按 wire 上的子代 id 清常驻标 —— 原键照清;归一到 fleet 尾段的那把键**只在这条终态属当代时**才清:
206
+ * 复活后迟到的前代终态(旧 UUID 的流内 close / 终态 tick)归一后带旧代际,清了尾段键 = 撤掉复活行的 sweep 保护 ⇒ 下一次 sweep 假结它。
207
+ * 判据与 `end` 的陈旧判据同一只(`isStaleEngineAgentPanelEnd`,拿钥匙上的周期对最近一条行帧的周期);判不出 ⇒ 照清(不因缺席留下永久常驻)。
208
+ */
209
+ export function clearEnginePanelTaskResidentByWire(wireTaskId) {
210
+ residentTaskIds.delete(wireTaskId);
211
+ const id = resolveEnginePanelTaskId(wireTaskId);
212
+ if (id.taskId === wireTaskId)
213
+ return;
214
+ if (isStaleEngineAgentPanelEnd(id, latestCycleByFleetId.get(id.taskId)))
215
+ return;
216
+ residentTaskIds.delete(id.taskId);
217
+ }
218
+ /** 测试钩:清两张钥匙表、缓一拍与周期水位。 */
219
+ export function __resetEngineAgentPanelIdentityForTests() {
220
+ fleetIdByTranscriptId.clear();
221
+ fleetIdByParentToolCallId.clear();
222
+ heldWireTicks.clear();
223
+ latestCycleByFleetId.clear();
224
+ }
225
+ /** 宿主的唯一发布口(三条 lane 都经这里):先归一身份,再进主通道(订阅者 / 缓冲)。 */
91
226
  export function publishEngineAgentPanelEvent(ev) {
227
+ // 行帧先登记、先放自己匹配的 tick,再给别的缓着的 tick 记拍(异源复审 R2 [high]:先记拍会让第 16 拍那条正好是自己行帧的 tick 过期);
228
+ // end / sweep 没有「自己的」匹配 ⇒ 先记拍。
229
+ if (ev.kind === 'end' || ev.kind === 'sweep')
230
+ ageHeld(ev.kind === 'sweep' ? undefined : ev.taskId);
231
+ switch (ev.kind) {
232
+ case 'fleet-row': {
233
+ const alias = {
234
+ fleetId: ev.taskId,
235
+ ...(ev.cycleSeq !== undefined ? { cycleSeq: ev.cycleSeq } : {}),
236
+ ...(ev.startedAt !== undefined ? { startedAt: ev.startedAt } : {}),
237
+ };
238
+ if (alias.cycleSeq !== undefined || alias.startedAt !== undefined) {
239
+ latestCycleByFleetId.delete(ev.taskId);
240
+ latestCycleByFleetId.set(ev.taskId, { ...(alias.cycleSeq !== undefined ? { cycleSeq: alias.cycleSeq } : {}), ...(alias.startedAt !== undefined ? { startedAt: alias.startedAt } : {}) });
241
+ if (latestCycleByFleetId.size > MAX_IDENTITY_KEYS) {
242
+ const oldest = latestCycleByFleetId.keys().next().value;
243
+ if (oldest !== undefined)
244
+ latestCycleByFleetId.delete(oldest);
245
+ }
246
+ }
247
+ if (ev.transcriptId !== undefined && ev.transcriptId.length > 0)
248
+ remember(fleetIdByTranscriptId, ev.transcriptId, alias);
249
+ if (ev.parentToolCallId !== undefined && ev.parentToolCallId.length > 0)
250
+ remember(fleetIdByParentToolCallId, ev.parentToolCallId, alias);
251
+ // 先发行帧再放缓着的 tick:tick 一到 fleet lane 就永久让位,行帧说的话(transcriptId / startedAt)得先落在行上。
252
+ deliverEngineAgentPanelEvent(ev);
253
+ for (const [wireId, held] of [...heldWireTicks]) {
254
+ if (wireId === ev.transcriptId || (held.ev.parentToolCallId !== undefined && held.ev.parentToolCallId === ev.parentToolCallId))
255
+ flushHeld(wireId);
256
+ }
257
+ ageHeld(undefined);
258
+ return;
259
+ }
260
+ case 'tick': {
261
+ const id = resolveEnginePanelTaskId(ev.taskId, ev.parentToolCallId);
262
+ if (id.origin === 'fleet-row') {
263
+ heldWireTicks.delete(ev.taskId);
264
+ rememberResolvedWire(ev.taskId, id);
265
+ deliverEngineAgentPanelEvent(normalizedTick(ev, id.taskId));
266
+ return;
267
+ }
268
+ if (ev.cardBound === true) {
269
+ deliverEngineAgentPanelEvent(asIsTick(ev));
270
+ return;
271
+ }
272
+ if (heldWireTicks.has(ev.taskId)) {
273
+ // 第二拍还没等到 fleet-row ⇒ 不再等:新的原样发,旧的丢(progress 合并语义,旧拍没有信息)。
274
+ heldWireTicks.delete(ev.taskId);
275
+ deliverEngineAgentPanelEvent(asIsTick(ev));
276
+ return;
277
+ }
278
+ if (heldWireTicks.size >= MAX_HELD_WIRE_TICKS) {
279
+ const oldest = heldWireTicks.keys().next().value;
280
+ if (oldest !== undefined)
281
+ flushHeld(oldest);
282
+ }
283
+ heldWireTicks.set(ev.taskId, { ev, beats: 0 });
284
+ return;
285
+ }
286
+ case 'end': {
287
+ const id = resolveEnginePanelTaskId(ev.taskId);
288
+ if (id.origin === 'fleet-row' && id.taskId !== ev.taskId) {
289
+ flushHeld(ev.taskId); // 同 UUID 还缓着一条 tick ⇒ 先归一放出(行先有最后一拍的数字与名字),再结
290
+ migrateResidency(ev.taskId, id.taskId);
291
+ deliverEngineAgentPanelEvent({
292
+ ...ev,
293
+ taskId: id.taskId,
294
+ wireTaskId: ev.taskId,
295
+ taskIdOrigin: 'fleet-row',
296
+ // end 自己不带周期身份(流内 close 臂恒不带)⇒ 借钥匙上的:复活后迟到的 end{旧 UUID} 由此带着旧代际,消费端按 isStaleEngineAgentPanelEnd 挡掉
297
+ ...(ev.cycleSeq === undefined && id.cycleSeq !== undefined ? { cycleSeq: id.cycleSeq } : {}),
298
+ ...(ev.startedAt === undefined && id.startedAt !== undefined ? { startedAt: id.startedAt } : {}),
299
+ });
300
+ return;
301
+ }
302
+ flushHeld(ev.taskId); // 缓着的 tick 先落行,终态才有行可结
303
+ deliverEngineAgentPanelEvent(ev);
304
+ return;
305
+ }
306
+ default:
307
+ // sweep:缓着的 tick 不放(见头注)—— 原样进主通道。
308
+ deliverEngineAgentPanelEvent(ev);
309
+ }
310
+ }
311
+ function deliverEngineAgentPanelEvent(ev) {
92
312
  // 同 taskId 的任何主通道事实(tick / fleet-row = 行在场;end = 真终态)都让**缓冲里**那条缺席过期 —— 迟订阅的消费者
93
313
  // 不许先恢复了行、再被一条旧缺席标成 unknown(已递达订阅者的缺席由订阅者按契约在这些事件上撤标)。
94
314
  if (ev.kind !== 'sweep')
@@ -0,0 +1,6 @@
1
+ /** 调用方 `opts.generation` 的一次性快照;`null` = 读不出(供给坏了)。 */
2
+ export declare function snapshotCapsGeneration(opts: {
3
+ generation?: number;
4
+ } | undefined): number | undefined | null;
5
+ /** 入口快照的代际是否仍是当代;`gen` 缺席 = 调用方没供代际 = 恒当代;读代际抛出 = 不是。 */
6
+ export declare function capsGenerationStillCurrent(baseUrl: string, gen: number | undefined): boolean;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * engineCapsGenerationGuard — 六只能力位读器 tee 共用的**代际两问**(0.74.2;不出公面,只供包内 import)。
3
+ *
4
+ * · {@link snapshotCapsGeneration}:从调用方 `opts` 上**一次性**读代际。`opts` 是调用方的对象,回体上的 getter 能在投影中途改它、
5
+ * 也能让这一次读抛出 ⇒ 读不出答 `null`(tee 按「不动格」处置),别把调用方对象再读第二次。
6
+ * · {@link capsGenerationStillCurrent}:入口快照的代际现在还是不是当代。读代际抛出 ⇒ `false`(不动格,与陈旧代际同一处置)。
7
+ *
8
+ * 两只都**带值返回**而不是吞掉:tee 里因此没有一处「只求不抛」的空 catch(failloud 门 §C1)。
9
+ */
10
+ import { engineCapsGeneration } from './engineCapsCache.js';
11
+ /** 调用方 `opts.generation` 的一次性快照;`null` = 读不出(供给坏了)。 */
12
+ export function snapshotCapsGeneration(opts) {
13
+ try {
14
+ return opts?.generation;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ /** 入口快照的代际是否仍是当代;`gen` 缺席 = 调用方没供代际 = 恒当代;读代际抛出 = 不是。 */
21
+ export function capsGenerationStillCurrent(baseUrl, gen) {
22
+ if (gen === undefined)
23
+ return true;
24
+ try {
25
+ return gen === engineCapsGeneration(baseUrl);
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
@@ -25,7 +25,7 @@
25
25
  */
26
26
  import { capForDisplay } from './fleetTaskDesc.js';
27
27
  import { engineWireTarget } from './engineWireTarget.js';
28
- import { engineCapsGeneration } from './engineCapsCache.js';
28
+ import { snapshotCapsGeneration, capsGenerationStillCurrent } from './engineCapsGenerationGuard.js';
29
29
  /**
30
30
  * caps 回体 → 本格读数;**畸形一律 `undefined`**(= 这一格不写 ⇒ 读口答 `unobserved`)。
31
31
  * 键不在 / `undefined` ⇒ `not_reported`(老 server);`null` / 数组 / 非对象 / 缺任一座 / 座坏形 ⇒ 畸形。
@@ -54,21 +54,32 @@ const readingByBase = new Map();
54
54
  * 投影 `undefined`(畸形)⇒ 这一格**被删**而不是留着上一台引擎的旧值;`opts.generation` 关掉「旧探测覆盖新读数」竞态。
55
55
  */
56
56
  export function noteEngineCapsForExecutionLane(baseUrl, caps, opts) {
57
+ if (typeof baseUrl !== 'string' || baseUrl === '')
58
+ return;
59
+ // 🔴 代际只在入口**快照一次**(异源对抗复审 R3):opts 是调用方的对象,回体上的 getter 能在投影中途改它;首末两次核对都用这份快照,
60
+ // 读 opts / 读代际抛出 ⇒ 不动格(与陈旧代际同一处置)。
61
+ const gen = snapshotCapsGeneration(opts);
62
+ if (gen === null)
63
+ return; // 调用方 opts 的 getter 抛出 = 供给坏了 ⇒ 不动格
64
+ if (!capsGenerationStillCurrent(baseUrl, gen))
65
+ return;
66
+ let reading;
57
67
  try {
58
- if (typeof baseUrl !== 'string' || baseUrl === '')
59
- return;
60
- if (opts?.generation !== undefined && opts.generation !== engineCapsGeneration(baseUrl))
61
- return;
62
- const reading = projectExecutionLaneCapability(caps);
63
- if (reading === undefined) {
64
- readingByBase.delete(baseUrl);
65
- return;
66
- }
67
- readingByBase.set(baseUrl, reading);
68
+ reading = projectExecutionLaneCapability(caps);
68
69
  }
69
70
  catch {
70
- /* fail-soft:本 tee 任何分支都不许打断 caps 探测链 */
71
+ // 🔴 0.74.2 同形存量(CC-69 复审 R1 族扫):抛 getter / Proxy 的回体 = 畸形,与「投影答 undefined」同一处置 —— 删格,不许留着上一台的读数。
72
+ reading = undefined;
73
+ }
74
+ // 🔴 投影之后**再核一次代际**(异源对抗复审 R2):投影会执行回体上不可信的 getter,它可以在中途让这一格换代
75
+ // (invalidate + 新代际的 tee 已写入);此时无论投影返回还是抛出,都不许动新代际的格。
76
+ if (!capsGenerationStillCurrent(baseUrl, gen))
77
+ return;
78
+ if (reading === undefined) {
79
+ readingByBase.delete(baseUrl);
80
+ return;
71
81
  }
82
+ readingByBase.set(baseUrl, reading);
72
83
  }
73
84
  /** 本进程观测到的执行车道;这一格空缺 ⇒ `{kind:'unobserved'}`(**绝不**编一条车道)。 */
74
85
  export function observedExecutionLane(baseUrl = engineWireTarget()?.baseUrl) {
@@ -100,7 +111,7 @@ export function executionLaneDoctorDetail(reading) {
100
111
  switch (reading.kind) {
101
112
  case 'unobserved':
102
113
  return ('execution lane not observed — the engine reports it on /v1/capabilities; ' +
103
- 'this process has not received a capabilities response from the engine yet');
114
+ 'this process has no usable capabilities reading cached for it (none received, or the last one was unreadable)');
104
115
  case 'not_reported':
105
116
  return ('execution lane not reported by this engine — only newer engines advertise it; ' +
106
117
  'this does not say where tools run, so the client keeps inferring it the way it did before');
@@ -140,6 +140,11 @@ export interface FleetWorkflowView {
140
140
  failedCount?: number;
141
141
  /** 🔴 缺席 ≠ 0:0 = 引擎说「一个都没起」,undefined = 老引擎「不知道」(退 done+failed 下界)。 */
142
142
  startedCount?: number;
143
+ /**
144
+ * 0.74.3 CC-72(core 7.23.5 #932;cli L-235 ②):run 级 errorCode **原词逐字**(开集,不枚举)。缺席 = 无码 ——
145
+ * **不是成功也不是失败**,终态仍看 `status`;server 透传 / sdk 声明之前按结构读(sdk 9.7.1 `FleetWorkflowRow` 未声明)。
146
+ */
147
+ errorCode?: string;
143
148
  }
144
149
  /**
145
150
  * SDK `FleetTaskStatus` 的终态词汇 —— 非终态即在飞(未知未来值按在飞算,保守)。
@@ -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
@@ -157,9 +157,12 @@ export * from './writeProtectionCapability.js';
157
157
  export * from './webSearchBackendCapability.js';
158
158
  export * from './executionLaneCapability.js';
159
159
  export * from './approvalsStreamLiveCapability.js';
160
+ export * from './deviceExecutorManagementCapability.js';
160
161
  export * from './mcpReconnect.js';
161
162
  export * from './leaderConflict.js';
162
163
  export * from './runTerminal.js';
164
+ export * from './runCancelContext.js';
165
+ export * from './argvFlagValue.js';
163
166
  export * from './readFacePosture.js';
164
167
  export * from './mcpPanel.js';
165
168
  export * from './effectiveFacts.js';
@@ -252,6 +255,7 @@ export * from './hooksWireCaps.js';
252
255
  export * from './liveInitToolFace.js';
253
256
  export * from './model/providerPresets.js';
254
257
  export * from './hitl/hitlBridge.js';
258
+ export * from './hitl/suspendedReopen.js';
255
259
  export { toolEndOutputText, ENGINE_ABORT_TOOL_RESULT } from './hitl/frameRouter.js';
256
260
  export { isAskTool } from './hitl/frameRouter.js';
257
261
  export * from './hitl/hitlHostSurface.js';
package/dist/index.js CHANGED
@@ -192,6 +192,8 @@ export * from './webSearchBackendCapability.js';
192
192
  export * from './executionLaneCapability.js';
193
193
  // · approvalsStreamLiveCapability(0.73.3 CC-61):capabilities.approvalsStreamLive 四态读面 + feed 消费方「要不要靠对账拉」的判据单源。
194
194
  export * from './approvalsStreamLiveCapability.js';
195
+ // · deviceExecutorManagementCapability(0.74.2 CC-69):capabilities.deviceExecutor.management 四态读面 + 管理动词可用性判据单源。
196
+ export * from './deviceExecutorManagementCapability.js';
195
197
  // S-381 / core 7.22.0 #857(0.72.9 CC-30;server ≥7.85.0,sdk 尚未声明):会话内 MCP re-dial 的消费口 —— 200 三态按 outcome 分形读、
196
198
  // unsupported 恒五键、名册按 toolNames 在场性、失败按 errorCode 分诊、「重连 = 一次交易」措辞单源;能力位 mcpReconnect 三态读。
197
199
  export * from './mcpReconnect.js';
@@ -200,6 +202,10 @@ export * from './mcpReconnect.js';
200
202
  export * from './leaderConflict.js';
201
203
  // 0.60.0(engine ≥7.64.0 / sdk 8.4.0):终局因由与门记录两只**判定归包**的读器。
202
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';
203
209
  // 0.61.0(engine ≥7.65.0 / sdk 8.5.0 / S-167):READ 容纳面档位 + 来源的 operator 面窄读器
204
210
  // (`WiringDiagnostics.readFace`)。与 `writeProtectionCapability` / `gateOutcome` 同构同纪律
205
211
  // (防御读 / 唯一措辞铸点 / UNTRUSTED-for-display);与租户面 `capabilities.readFace` 刻意不合流,
@@ -436,6 +442,8 @@ export * from './model/providerPresets.js';
436
442
  // 共享宿主通知面 + cancel-by-deny 有界观察 —— `askGateWire.ts` 已 import `toolApprovalWire.ts`,
437
443
  // 反向 import 会成环,所以两条腿共用的那一组挪进一个两边都能到达的独立模块。
438
444
  export * from './hitl/hitlBridge.js';
445
+ // 0.74.3 CC-73:durable suspended.reopened 三态读口(cli L-56 ②)
446
+ export * from './hitl/suspendedReopen.js';
439
447
  // [C93] P0 直讨(0.17.1):toolEndOutputText 单一真源导出——web/desktop 被点名「两端务必接」的
440
448
  // 6.0.0 output content-block 归一,此前 frameRouter 内私有,两端手抄=漂移温床。frameRouter 只
441
449
  // 挑名导出这一件(整星导出会把路由内部面全泄进公面)。