@sema-agent/client-core 0.73.2 → 0.73.4

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.
@@ -69,7 +69,7 @@ export function startApprovalsFeed(client, onSnapshot, opts) {
69
69
  let takeSeq = 0;
70
70
  let lastObservedSeq = 0;
71
71
  const stats = {
72
- pushEvents: 0, heartbeats: 0, polls: 0, listErrors: 0, streamFailures: 0, snapshots: 0, reconciles: 0,
72
+ pushEvents: 0, heartbeats: 0, polls: 0, listErrors: 0, streamFailures: 0, snapshots: 0, reconciles: 0, pushTakeRetries: 0,
73
73
  };
74
74
  /** 域词表-14 收编:「怎么调 unref」的单一实现见 unrefTimer.ts,本函数只是就地起个短名。 */
75
75
  function arm(t) {
@@ -101,6 +101,56 @@ export function startApprovalsFeed(client, onSnapshot, opts) {
101
101
  }
102
102
  return verdict;
103
103
  }
104
+ // ── 0.73.4 欠一次看(见 `pushTakeRetryMs` 头注)──
105
+ const pushTakeRetryBaseMs = opts?.pushTakeRetryMs ?? 1000;
106
+ const pushTakeRetryMaxMs = Math.max(pushTakeRetryBaseMs, 30_000);
107
+ let pushTakeRetryTimer;
108
+ let pushTakeRetryDelayMs = pushTakeRetryBaseMs;
109
+ /** 欠账锚在**发起序号**上:最近一次没看成的 push 侧取件是第几号(0 = 不欠)。 */
110
+ let owedSinceSeq = 0;
111
+ /** 已经看成的最晚发起序号(与欠账独立记):比它更早发起的失败是迟到的旧账,不记(对偶的乱序形)。 */
112
+ let lastLookedSeq = 0;
113
+ /**
114
+ * 销账只认**比欠账更晚发起**的取件:更早发起的那一次看的是失败之前的世界,它迟到的成功说明不了
115
+ * 「引擎说变了的那一下」已经有人看过(乱序是真的会发生的:慢 list / `refresh()` 与 push 重取撞车)。
116
+ */
117
+ function noteTakeLooked(seq) {
118
+ if (seq > lastLookedSeq)
119
+ lastLookedSeq = seq;
120
+ if (seq <= owedSinceSeq)
121
+ return;
122
+ owedSinceSeq = 0;
123
+ if (pushTakeRetryTimer !== undefined) {
124
+ clearTimeout(pushTakeRetryTimer);
125
+ pushTakeRetryTimer = undefined;
126
+ }
127
+ pushTakeRetryDelayMs = pushTakeRetryBaseMs;
128
+ }
129
+ function noteTakeMissed(via, seq) {
130
+ if (via !== 'push' || pushTakeRetryBaseMs <= 0)
131
+ return;
132
+ if (seq < lastLookedSeq)
133
+ return; // 更晚发起的一眼已经看成 ⇒ 这次迟到的失败不欠
134
+ if (seq > owedSinceSeq)
135
+ owedSinceSeq = seq;
136
+ if (stopped || ac.signal.aborted)
137
+ return;
138
+ if (pushTakeRetryTimer !== undefined)
139
+ return; // 幂等:已经有一只在排
140
+ const delay = pushTakeRetryDelayMs;
141
+ pushTakeRetryDelayMs = Math.min(pushTakeRetryDelayMs * 2, pushTakeRetryMaxMs);
142
+ pushTakeRetryTimer = arm(setTimeout(() => {
143
+ pushTakeRetryTimer = undefined;
144
+ if (stopped || ac.signal.aborted)
145
+ return;
146
+ stats.pushTakeRetries++;
147
+ // 欠账不随模式走:期间若降级到轮询,这一次照取(轮询腿可能被 shouldPoll 关着)。
148
+ void take('push').catch(e => {
149
+ // 重试自己逃逸(回体坏形):欠账已在 take() 内续记并再排,这里只接住 rejection。
150
+ hostLog('debug', `approvalsFeed: push-take retry escaped (unreadable body?) — still owed: ${String(e)}`);
151
+ });
152
+ }, delay));
153
+ }
104
154
  async function take(via,
105
155
  /** [F4957-1 二审] 发起这次取件时的轮询代际(只有轮询腿传;push/refresh 不受代际管)。 */
106
156
  leg) {
@@ -118,6 +168,7 @@ export function startApprovalsFeed(client, onSnapshot, opts) {
118
168
  if (reading.kind === 'malformed') {
119
169
  stats.listErrors++;
120
170
  hostLog('debug', `approvalsFeed: list() body has an unreadable livePending section (${via} leg) — snapshot not committed`);
171
+ noteTakeMissed(via, seq);
121
172
  return;
122
173
  }
123
174
  if (reading.kind === 'present') {
@@ -129,11 +180,25 @@ export function startApprovalsFeed(client, onSnapshot, opts) {
129
180
  catch (e) {
130
181
  stats.listErrors++;
131
182
  hostLog('debug', `approvalsFeed: list() failed (${via} leg): ${String(e)}`);
183
+ noteTakeMissed(via, seq);
132
184
  return;
133
185
  }
134
186
  if (stopped)
135
187
  return;
136
- const d = digestOf(rows, live);
188
+ // 🔴 摘要排在销账**之前**:回体坏形(例:`pending` 不是数组)会让它抛出、整次取件逃逸(各调用点自己接盘,
189
+ // 轮询腿的连续逃逸计数靠的就是这条路)。先销账再抛 = 一份读不懂的回体把欠账销了,之后再没人来看。
190
+ // 逃逸本身也是「没看成」:在**这里**统一记欠账再原样抛出,四个调用点(起手 / 事件 / refresh / 重试)不必各记各的。
191
+ let d;
192
+ try {
193
+ d = digestOf(rows, live);
194
+ }
195
+ catch (e) {
196
+ noteTakeMissed(via, seq);
197
+ throw e;
198
+ }
199
+ // 看成了(回体读得懂)⇒ 销掉**更早发起**的欠账(判据在发起序号上,见 noteTakeLooked;与下面两道提交闸无关:
200
+ // 销账问的是「失败之后有没有人再看过」,提交闸问的是「这份结论还新不新」)。
201
+ noteTakeLooked(seq);
137
202
  // 🔴 [F4957-1 二审 finding②] 代际闸必须排在**提交之前**:下面四行(lastDigest / revision /
138
203
  // last / onSnapshot)一落,视图就已经被改了 —— 守卫排在 take() 之外只挡得住计数与续排,挡不住
139
204
  // 回滚。乱序是真的会发生的:旧 poll 在途 → push 重连并发布新快照 → 旧 poll 才带着**更老**的
@@ -407,6 +472,8 @@ export function startApprovalsFeed(client, onSnapshot, opts) {
407
472
  clearTimeout(retryTimer);
408
473
  if (reconcileTimer !== undefined)
409
474
  clearTimeout(reconcileTimer);
475
+ if (pushTakeRetryTimer !== undefined)
476
+ clearTimeout(pushTakeRetryTimer);
410
477
  opts?.signal?.removeEventListener('abort', onOuterAbort);
411
478
  ac.abort();
412
479
  },
package/dist/index.d.ts CHANGED
@@ -156,6 +156,7 @@ export * from './sqlEngineCapability.js';
156
156
  export * from './writeProtectionCapability.js';
157
157
  export * from './webSearchBackendCapability.js';
158
158
  export * from './executionLaneCapability.js';
159
+ export * from './approvalsStreamLiveCapability.js';
159
160
  export * from './mcpReconnect.js';
160
161
  export * from './leaderConflict.js';
161
162
  export * from './runTerminal.js';
package/dist/index.js CHANGED
@@ -190,6 +190,8 @@ export * from './webSearchBackendCapability.js';
190
190
  // 0.72.13 CC-54:`capabilities.executionLane`(server ≥7.86.0 S-426)四态读面 —— 键缺席 = 老 server 判不了(≠ 不是本机车道);
191
191
  // 「工具是否跑在本机」判据归包(读到位用位,读不到原样用调用方旁证)。
192
192
  export * from './executionLaneCapability.js';
193
+ // · approvalsStreamLiveCapability(0.73.3 CC-61):capabilities.approvalsStreamLive 四态读面 + feed 消费方「要不要靠对账拉」的判据单源。
194
+ export * from './approvalsStreamLiveCapability.js';
193
195
  // S-381 / core 7.22.0 #857(0.72.9 CC-30;server ≥7.85.0,sdk 尚未声明):会话内 MCP re-dial 的消费口 —— 200 三态按 outcome 分形读、
194
196
  // unsupported 恒五键、名册按 toolNames 在场性、失败按 errorCode 分诊、「重连 = 一次交易」措辞单源;能力位 mcpReconnect 三态读。
195
197
  export * from './mcpReconnect.js';
@@ -174,7 +174,10 @@ export function parseTranscriptNotificationSeeds(messages) {
174
174
  renderedKeys.push(`${external ? 'external:' : ''}${taskId}:${status}`);
175
175
  // L-215② 族扫(0.65.0):终局判据走单铸谓词 —— 修前这里手抄三词,core [6908] 的 `blocked`
176
176
  // 漏在外面 ⇒ 一条已经渲过的 `blocked` 通知不进 `notifiedKeys`,重开会话后**再投一次**。
177
- if (!external && isTerminalStatus(status)) {
177
+ // 0.73.3(CC-60 A-D11):这条 XML 有**两个生产者**、两张终局词表 —— core task-notification(带 cancelled)与本包
178
+ // enqueueBgChildNotification(status 来自 run 行状态面,带 blocked)⇒ 按**并集**判终局;`event` 是唯一非终局。
179
+ // 两张冻结元组各自不动(动任一张 = 把另一个生产者的词丢掉)。
180
+ if (!external && (isTerminalStatus(status) || isTaskNotificationTerminalStatus(status))) {
178
181
  notifiedKeys.push(taskId);
179
182
  }
180
183
  seeded++;
@@ -45,7 +45,8 @@ export interface EngineTaskStatusAttachment {
45
45
  type: 'task_status';
46
46
  taskId: string;
47
47
  taskType: 'local_bash';
48
- status: 'pending' | 'running' | 'completed' | 'failed' | 'killed';
48
+ /** 0.73.3(CC-60 A-D13):+`parked`(core 第四个 vbl 臂:停在待批检查点 —— 恰是最需要用户看见的状态)。 */
49
+ status: 'pending' | 'running' | 'completed' | 'failed' | 'killed' | 'parked';
49
50
  description: string;
50
51
  deltaSummary: null;
51
52
  }
package/dist/steering.js CHANGED
@@ -62,6 +62,8 @@ const PHRASE_TO_STATUS = [
62
62
  ['completed in background', 'completed'],
63
63
  ['still running in background', 'running'],
64
64
  ['stopped', 'killed'],
65
+ // 0.73.3(A-D13):core 第四个 vbl 臂,排在 raw-status 兜底组之前(此前恒不命中 ⇒ compaction 后停在待批检查点的行整行消失)。
66
+ ['parked awaiting an out-of-band approval — do NOT re-issue the gated call', 'parked'],
65
67
  // core's raw-status fallback arm for the non-vbl states (announce snapshots pending/running, but
66
68
  // parse defensively — a settle can race the boundary).
67
69
  ['pending', 'pending'],
@@ -23,7 +23,9 @@ export declare function fetchEngineTaskOutput(handle: string, opts?: {
23
23
  * 曾经各写一份 `.includes()` 判读,同一个协议标记有两处互不知情的判读点——现在两处判读结果
24
24
  * 对同一输入恒一致,因为已经是同一个函数)。
25
25
  */
26
- export declare function mergeTaskOutput(prev: string, fresh: string): string;
26
+ /** 0.73.3(CC-60 A-D3):`cursorSemantics` 机读位(server ≥7.77.0 `taskOutput` 面):"cursor" = 读了就消费 ⇒ append;"full" = 全量重读安全 ⇒ 替换。
27
+ * **缺席 / 认不出 ⇒ 现行文案标记臂兜底**(老 server 不发这个键;sdk 头注逐字「别把缺席当 full」)。 */
28
+ export declare function mergeTaskOutput(prev: string, fresh: string, cursorSemantics?: string): string;
27
29
  export type EngineTaskStopOutcome =
28
30
  /** 200:停止动作完成(status = wire 终态照实;重复 stop 幂等)。 */
29
31
  {
@@ -121,7 +121,10 @@ export async function fetchEngineTaskOutput(handle, opts) {
121
121
  });
122
122
  const fresh = typeof r?.content === 'string' ? r.content : '';
123
123
  const prev = accumulated.get(handle) ?? '';
124
- const merged = mergeTaskOutput(prev, fresh);
124
+ // A-D3:机读位优先 —— sdk 9.7.1 `SubagentOutputResult.cursorSemantics` 是**回体顶层**键(与 `output` 并列;generic taskOutput 面才铸,
125
+ // 窄面 subagentOutput 不带 ⇒ 缺席走文案臂)。异源对抗复审:首版读成 `output.cursorSemantics`,正常回体恒回落文案臂。
126
+ const cursorSemantics = r?.cursorSemantics;
127
+ const merged = mergeTaskOutput(prev, fresh, typeof cursorSemantics === 'string' ? cursorSemantics : undefined);
125
128
  accumulated.set(handle, merged);
126
129
  const out = r?.output ?? {};
127
130
  if (engineWireDebugEnabled()) {
@@ -148,9 +151,15 @@ export async function fetchEngineTaskOutput(handle, opts) {
148
151
  * 曾经各写一份 `.includes()` 判读,同一个协议标记有两处互不知情的判读点——现在两处判读结果
149
152
  * 对同一输入恒一致,因为已经是同一个函数)。
150
153
  */
151
- export function mergeTaskOutput(prev, fresh) {
154
+ /** 0.73.3(CC-60 A-D3):`cursorSemantics` 机读位(server ≥7.77.0 `taskOutput` 面):"cursor" = 读了就消费 ⇒ append;"full" = 全量重读安全 ⇒ 替换。
155
+ * **缺席 / 认不出 ⇒ 现行文案标记臂兜底**(老 server 不发这个键;sdk 头注逐字「别把缺席当 full」)。 */
156
+ export function mergeTaskOutput(prev, fresh, cursorSemantics) {
152
157
  if (fresh.length === 0)
153
158
  return prev;
159
+ if (cursorSemantics === 'full')
160
+ return fresh;
161
+ if (cursorSemantics === 'cursor')
162
+ return prev + fresh;
154
163
  const marker = spoolMarkerOf(fresh);
155
164
  if (marker === 'full')
156
165
  return fresh;
@@ -22,6 +22,8 @@ export declare const STRUCTURED_DETAIL_TYPES: ReadonlySet<string>;
22
22
  export declare function structuredDetailType(structured: unknown): string | undefined;
23
23
  export declare const PROTOCOL_MARKERS: {
24
24
  /** T11 bash 框架:退出码行头(core tools/fs/index.ts runShell)。 */
25
+ /** 0.73.3(CC-60 A-D5):认 core 两个发货变体 —— exit1 注记形 `exit code: 1 (<note>)` 与图片形(首行后**没有** `--- stdout ---` 行,
26
+ * 紧跟 `[Image data detected …]`)。第 2 组 = 括号注记(缺席 = undefined),第 3 组 = 老形的 stdout 分隔行(图片形缺席)。 */
25
27
  readonly bashHead: RegExp;
26
28
  /** T11 bash 框架:stderr 分隔行。 */
27
29
  readonly bashStderrSep: "\n--- stderr ---\n";
@@ -42,6 +44,7 @@ export declare const PROTOCOL_MARKERS: {
42
44
  /** T13 TaskOutput:stderr 分隔行。 */
43
45
  readonly taskOutputStderrLine: RegExp;
44
46
  /** T13 TaskOutput:agent 结果分隔行。 */
47
+ /** 0.73.3(CC-60 A-D4):认可选括号注记 `--- result (partial) ---`(core 被 stop 打断的子代任务此前正文整段切不出)。 */
45
48
  readonly taskOutputResultLine: RegExp;
46
49
  /** [1505] spool 协议:全量可重读形(`engineTaskHandleWire.ts` mergeTaskOutput 单源消费点)。 */
47
50
  readonly spoolStdoutFull: "--- stdout (full, re-readable) ---";
@@ -65,6 +68,7 @@ export declare function parseModelFacingBash(text: string): {
65
68
  stdout: string;
66
69
  stderr: string;
67
70
  exitCode: number | null;
71
+ note?: string;
68
72
  } | null;
69
73
  /**
70
74
  * T12 —— 剥引擎的 `delimitUntrusted` 围栏(core untrusted-text.ts:118)**仅供渲染**。
@@ -171,7 +171,9 @@ const WF_TERMINAL_STATUSES = new Set([
171
171
  // 这里只是把「谁在判读」收敛到具名常量 + 单一函数,每域一个 pattern home。
172
172
  export const PROTOCOL_MARKERS = {
173
173
  /** T11 bash 框架:退出码行头(core tools/fs/index.ts runShell)。 */
174
- bashHead: /^exit code: (-?\d+)\n--- stdout ---\n/,
174
+ /** 0.73.3(CC-60 A-D5):认 core 两个发货变体 —— exit1 注记形 `exit code: 1 (<note>)` 与图片形(首行后**没有** `--- stdout ---` 行,
175
+ * 紧跟 `[Image data detected …]`)。第 2 组 = 括号注记(缺席 = undefined),第 3 组 = 老形的 stdout 分隔行(图片形缺席)。 */
176
+ bashHead: /^exit code: (-?\d+)(?: \(([^)\n]*)\))?\n(--- stdout ---\n)?/,
175
177
  /** T11 bash 框架:stderr 分隔行。 */
176
178
  bashStderrSep: '\n--- stderr ---\n',
177
179
  /** T11 bash 框架:空 stdout 哨兵。 */
@@ -191,7 +193,8 @@ export const PROTOCOL_MARKERS = {
191
193
  /** T13 TaskOutput:stderr 分隔行。 */
192
194
  taskOutputStderrLine: /^--- (?:new )?stderr[^\n]*---\n?/m,
193
195
  /** T13 TaskOutput:agent 结果分隔行。 */
194
- taskOutputResultLine: /^--- result ---\n?/m,
196
+ /** 0.73.3(CC-60 A-D4):认可选括号注记 `--- result (partial) ---`(core 被 stop 打断的子代任务此前正文整段切不出)。 */
197
+ taskOutputResultLine: /^--- result(?: \([^)\n]*\))? ---\n?/m,
195
198
  /** [1505] spool 协议:全量可重读形(`engineTaskHandleWire.ts` mergeTaskOutput 单源消费点)。 */
196
199
  spoolStdoutFull: '--- stdout (full, re-readable) ---',
197
200
  /** [1505] spool 协议:增量新字节形。 */
@@ -238,7 +241,12 @@ export function parseModelFacingBash(text) {
238
241
  const head = PROTOCOL_MARKERS.bashHead.exec(text);
239
242
  if (!head)
240
243
  return null;
244
+ // 图片形(无 stdout 分隔行)只在首行后紧跟图片提示时才算命中;别的「首行后不是分隔行」的文本不是 bash 框架
245
+ if (head[3] === undefined && !text.slice(head[0].length).startsWith('[Image data detected'))
246
+ return null;
241
247
  const exitCode = Number(head[1]);
248
+ // 0.73.3(A-D5):首行括号注记 = 引擎对这次退出码的解释(良性非零退出);structured 缺席时它是唯一能喂给同一个良性槽的来源
249
+ const note = typeof head[2] === 'string' && head[2] !== '' ? head[2] : undefined;
242
250
  let rest = text.slice(head[0].length);
243
251
  let stderr = '';
244
252
  // 直写 `PROTOCOL_MARKERS.bashStderrSep`(不再中转 `const sep`):⑬ 段的角色普查按**用法形**
@@ -250,7 +258,7 @@ export function parseModelFacingBash(text) {
250
258
  rest = rest.slice(0, si);
251
259
  }
252
260
  const stdout = rest === PROTOCOL_MARKERS.bashEmptySentinel ? '' : rest;
253
- return { stdout, stderr, exitCode: Number.isFinite(exitCode) ? exitCode : null };
261
+ return { stdout, stderr, exitCode: Number.isFinite(exitCode) ? exitCode : null, ...(note !== undefined ? { note } : {}) };
254
262
  }
255
263
  /**
256
264
  * T12 —— 剥引擎的 `delimitUntrusted` 围栏(core untrusted-text.ts:118)**仅供渲染**。
@@ -406,9 +414,13 @@ export function wireOutputToBody(toolName, output, isError, truncated, flatten,
406
414
  // isError:false + structured.returnCodeInterpretation('No matches found')。注记在场 ⇒
407
415
  // 非零退出不再翻 failed(CC 同款:interpretation 形走 data 路径非 error 路径,
408
416
  // pretty223:394251/815147 直证)。[1948] 裁定不动:无注记的非零退出照旧按退出码补位。
417
+ // 0.73.3(A-D5):structured 缺席(core boundedCard 超 64 KiB 整只丢)时,首行括号注记是同一件事 ⇒ 喂同一个良性槽;
418
+ // 只放宽正则不接这一位,会把「正文丢失」换成「红色错误卡」。structured 在场仍以它为准。
409
419
  const benignInterp = typeof s?.returnCodeInterpretation === 'string' && s.returnCodeInterpretation.length > 0
410
420
  ? s.returnCodeInterpretation
411
- : undefined;
421
+ : 'note' in slots && typeof slots.note === 'string' && slots.note.length > 0
422
+ ? slots.note
423
+ : undefined;
412
424
  const failed = isError || (slots.exitCode != null && slots.exitCode !== 0 && benignInterp === undefined);
413
425
  if (failed) {
414
426
  // 失败卡走 UserToolErrorMessage,它读 BLOCK content 且非字符串会被吞成
@@ -884,6 +896,10 @@ modelText) {
884
896
  };
885
897
  }
886
898
  const wfName = typeof inner?.name === 'string' && inner.name.length > 0 ? inner.name : undefined;
899
+ // 0.73.4(CC-63):**run 级**错误码。引擎在这张结构卡的**顶层**铸 `errorCode`(与 `completionId` 同层;有值才带,
900
+ // 缺席 = 这条 run 没有码),今天只有 workflow 形带。它不是 `agent_runs[].errorCode`(per-agent,住在模型面 JSON 里),
901
+ // 也不在内层 `details` 上。开集原词透传;缺席 / 空串 / 非字符串 ⇒ 键不铸,绝不编码。CC 的 task 形没有这一格 ⇒ 超集键。
902
+ const runErrorCode = typeof s.errorCode === 'string' && s.errorCode.length > 0 ? s.errorCode : undefined;
887
903
  let output = bodyText ?? '';
888
904
  if (taskType === 'workflow') {
889
905
  // 终态轮询卡经过投影 = 模型 in-band 已收到结局 ⇒ 预 seed runId 去重(补发通道对同 runId 丢弃)。
@@ -904,6 +920,7 @@ modelText) {
904
920
  description: wfName ?? taskId,
905
921
  output,
906
922
  ...(error !== undefined ? { error } : {}),
923
+ ...(runErrorCode !== undefined ? { _sema_error_code: runErrorCode } : {}),
907
924
  },
908
925
  },
909
926
  };