@sema-agent/client-core 0.67.2 → 0.68.1

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 (50) hide show
  1. package/CHANGELOG.md +310 -0
  2. package/README.md +65 -1
  3. package/dist/adapt/arms.js +67 -9
  4. package/dist/adapt/textStream.d.ts +102 -1
  5. package/dist/adapt/textStream.js +169 -6
  6. package/dist/adapt/turnFlags.d.ts +14 -0
  7. package/dist/adapt/turnFlags.js +4 -1
  8. package/dist/adapt.js +4 -1
  9. package/dist/adapter/activeRunSelfHeal.d.ts +53 -6
  10. package/dist/adapter/activeRunSelfHeal.js +79 -8
  11. package/dist/adapter/downstream/eventToSdkMessage.d.ts +18 -1
  12. package/dist/adapter/downstream/eventToSdkMessage.js +50 -9
  13. package/dist/adapter/downstream/terminalToSdkResult.d.ts +29 -0
  14. package/dist/adapter/downstream/terminalToSdkResult.js +46 -15
  15. package/dist/adapter/runStream.d.ts +22 -2
  16. package/dist/adapter/runStream.js +139 -38
  17. package/dist/adapter/types.d.ts +4 -1
  18. package/dist/autoModeUnavailable.d.ts +17 -9
  19. package/dist/autoModeUnavailable.js +26 -8
  20. package/dist/classifierStatus.d.ts +32 -4
  21. package/dist/classifierStatus.js +5 -3
  22. package/dist/controlRouter.d.ts +16 -0
  23. package/dist/controlRouter.js +6 -0
  24. package/dist/engineErrorCodes.d.ts +52 -0
  25. package/dist/engineErrorCodes.js +117 -0
  26. package/dist/engineNoticeCodes.d.ts +95 -1
  27. package/dist/engineNoticeCodes.js +124 -1
  28. package/dist/gateVocabulary.d.ts +18 -7
  29. package/dist/gateVocabulary.js +21 -8
  30. package/dist/hitl/parkResolver.d.ts +0 -14
  31. package/dist/hitl/parkResolver.js +22 -9
  32. package/dist/hitl/toolApprovalWire.d.ts +2 -1
  33. package/dist/hitl/toolApprovalWire.js +1 -0
  34. package/dist/ownKey.d.ts +34 -0
  35. package/dist/ownKey.js +36 -0
  36. package/dist/request/taskRequest.d.ts +6 -6
  37. package/dist/request/taskRequest.js +45 -0
  38. package/dist/retryStatus.d.ts +13 -2
  39. package/dist/retryStatus.js +4 -1
  40. package/dist/runTerminal.d.ts +87 -14
  41. package/dist/runTerminal.js +89 -15
  42. package/dist/seam.d.ts +78 -8
  43. package/dist/seam.js +16 -2
  44. package/dist/toolResult.js +8 -0
  45. package/dist/toolRoster.d.ts +34 -2
  46. package/dist/toolRoster.js +16 -2
  47. package/dist/workflowClient.d.ts +22 -0
  48. package/dist/workflowClient.js +37 -0
  49. package/docs/INTEGRATION-CLIENTS.md +633 -9
  50. package/package.json +2 -2
@@ -173,6 +173,53 @@ function readStatus(record) {
173
173
  const st = record?.status;
174
174
  return typeof st === 'string' && st.length > 0 ? st : null;
175
175
  }
176
+ /**
177
+ * 「这条 run 还占着会话吗」的**直接读数**(0.68.0 / L-230;server 7.73.0 S-122 P-45:
178
+ * `GET /v1/runs/:id` 上的 `heldBy`)。
179
+ *
180
+ * 三态,**刻意不是布尔**:
181
+ * · `'held'` —— `heldBy` 是一个非空串 = **有一条 run 名字在上面**,它确实还占着;
182
+ * · `'released'` —— `heldBy` 显式是 `null` = 一句**正面事实**:会话已经交出来了;
183
+ * · `'unknown'` —— 这一位**整键缺席**(老引擎 / 不发这一位的部署),或者形不对
184
+ * (非串非 null)⇒ 读不出,**绝不**当成 released。
185
+ *
186
+ * 🔴 `null` 与「键不在」必须分开,这是本读器存在的全部理由:
187
+ * 把两者都读成「释放了」,老引擎上每一次探测都会当场答「已释放」并立刻重发那条消息 ——
188
+ * 而它可能一头撞回一个还锁着的会话(再吃一个 409,正是这条腿存在的理由);
189
+ * 把两者都读成「还占着」,新引擎上那句正面事实白给了,窗要一直等到点。
190
+ * 🔴 判据用 `Object.hasOwn`:`{heldBy: undefined}` 与 `{}` 在这里是**同一件事**(都读不出),
191
+ * 而 `null` 必须与它们分开 —— 靠 `?? ` / `!= null` 一类写法分不出来。
192
+ */
193
+ function readClaimHolder(record) {
194
+ if (record === null || typeof record !== 'object')
195
+ return 'unknown';
196
+ if (!Object.hasOwn(record, 'heldBy'))
197
+ return 'unknown';
198
+ const v = record.heldBy;
199
+ if (v === null)
200
+ return 'released';
201
+ if (typeof v === 'string' && v.length > 0)
202
+ return 'held';
203
+ // 空串 / 数 / 对象:形不对 ⇒ 读不出(失效方向在保守那一侧:不宣告释放)。
204
+ return 'unknown';
205
+ }
206
+ /**
207
+ * cancel 回执(202 体)/ run 行上的 `cancelRequested`(0.68.0 / L-230;server 7.73.0 S-122 P-45)。
208
+ *
209
+ * 🔴 **三态**同上:`true` / `false` 都是真读数,**读不出 ⇒ `undefined`**(老引擎不发这一位)。
210
+ * 绝不折 `false` —— 「没请求过取消」与「不知道有没有请求过」对下一步的含义不同:前者可以放心
211
+ * 再发一次 cancel,后者再发就可能是第二枪。
212
+ * 🔴 它**不是**「已经取消了」:上游逐字是 *requested*,一次**已受理的请求**,不是终局。
213
+ * 会话有没有交出来仍然只由 {@link readClaimHolder} 与 {@link CLAIM_RELEASED_STATES} 回答。
214
+ */
215
+ export function readCancelRequested(body) {
216
+ if (typeof body !== 'object' || body === null || Array.isArray(body))
217
+ return undefined;
218
+ if (!Object.hasOwn(body, 'cancelRequested'))
219
+ return undefined;
220
+ const v = body.cancelRequested;
221
+ return typeof v === 'boolean' ? v : undefined;
222
+ }
176
223
  /** 重开判决 → 结局(真呈现回执消费点:`presented === false` 时 reopened 断言不成立)。 */
177
224
  function reopenDelivered(verdict) {
178
225
  return verdict.reopened === true && verdict.presented !== false;
@@ -339,28 +386,33 @@ export async function waitForClaimRelease(taskId, deps) {
339
386
  const isAborted = () => deps.signal?.aborted === true;
340
387
  let delay = CANCEL_POLL_START_MS;
341
388
  let lastStatus = null;
389
+ // 🔴 0.68.0 / L-230:`heldBy` 的三态与 `lastStatus` **同律** —— 只记最近一次**有回答**的探测。
390
+ let lastHolder = 'unknown';
342
391
  for (;;) {
343
392
  if (isAborted())
344
- return { released: false, waitedMs: waited(), aborted: true, lastStatus };
393
+ return { released: false, waitedMs: waited(), aborted: true, lastStatus, lastHolder };
345
394
  const remaining = deadline - now();
346
395
  if (remaining <= 0)
347
- return { released: false, waitedMs: waited(), aborted: false, lastStatus };
396
+ return { released: false, waitedMs: waited(), aborted: false, lastStatus, lastHolder };
348
397
  await sleep(Math.min(delay, remaining), deps.signal);
349
398
  if (isAborted())
350
- return { released: false, waitedMs: waited(), aborted: true, lastStatus };
399
+ return { released: false, waitedMs: waited(), aborted: true, lastStatus, lastHolder };
351
400
  delay = Math.min(Math.round(delay * 1.5), CANCEL_POLL_MAX_MS);
352
401
  let status;
402
+ let holder;
353
403
  const lease = claimProbeLease(deps.signal, deadline - now());
354
404
  try {
355
- status = readStatus(await deps.get(taskId, {
405
+ const record = await deps.get(taskId, {
356
406
  signal: lease.signal,
357
407
  ...(deps.session !== undefined ? { session: deps.session } : {}),
358
- }));
408
+ });
409
+ status = readStatus(record);
410
+ holder = readClaimHolder(record);
359
411
  }
360
412
  catch {
361
413
  lease.release();
362
414
  if (isAborted())
363
- return { released: false, waitedMs: waited(), aborted: true, lastStatus };
415
+ return { released: false, waitedMs: waited(), aborted: true, lastStatus, lastHolder };
364
416
  // 🔴 本发是被**窗自己的截止**掐掉的(lease 到点;caller 未中止)⇒ 没有读到任何回答 ——
365
417
  // 既不是释放证据,也不是「读不出来」的新证据,上一发**完成了的**读数保持在座(#244 F1
366
418
  // 换装批 G-c 尾竞态定谳:R2 [medium] 的「只记最近一次」指最近一次**有回答**的探测 ——
@@ -371,6 +423,7 @@ export async function waitForClaimRelease(taskId, deps) {
371
423
  // 这一发**有回答但读不出**(404/传输错)⇒ 最新证据是「不知道」,陈旧的成功读数当场作废
372
424
  // (见 lastStatus 头注)。
373
425
  lastStatus = null;
426
+ lastHolder = 'unknown';
374
427
  // 🔴 **404 也只是「读不到」,不是「释放了」**(二次评审 [high] 处置,2026-08-14):这个读口的
375
428
  // 404 在 SDK 契约上是 `not_found.run` —— 「这条 run 不存在」与「它不是你的」**共用同一个码,
376
429
  // 且是刻意设计的不可分辨**(跨租户不给存在性预言机)。凭它断言「那条 run 已经不占着会话」,
@@ -381,10 +434,22 @@ export async function waitForClaimRelease(taskId, deps) {
381
434
  }
382
435
  lease.release(); // 🔴 定时器必清:这只是**普通**定时器(见 claimProbeLease),不清就把进程多拖活一拍
383
436
  lastStatus = status; // 奇形记录(读不出 status)同样把陈旧读数清掉 —— 最新证据仍是「不知道」
437
+ lastHolder = holder;
438
+ // ── 🔴 0.68.0 / L-230(server 7.73.0 S-122 P-45 读面消费)—— **两条释放证据,取并** ────────
439
+ // ① `heldBy === null` —— 引擎**直说**会话交出来了。这是本批新接的那条,而且它比 ② 强:
440
+ // ② 是拿「run 走到了某个终态词」去**推断**锁没了,而锁与 run 的生命周期不是同一件事
441
+ // (park 态保留 claim 正是这条推断会踩的坑,也是 `CLAIM_RELEASED_STATES` 是白名单而不是
442
+ // 「running 之外的一切」的原因)。有直接证据就别再推断。
443
+ // ② `status ∈ CLAIM_RELEASED_STATES` —— 老引擎不发 `heldBy`(读出 `'unknown'`)时的**既有路**,
444
+ // 一字未动。⇒ 本件对老引擎**零行为变化**;对新引擎多了一条更早、更硬的收口证据。
445
+ // 🔴 `'unknown'` 绝不当成释放:缺席不是证据(与本文件 404 那一格逐字同一条纪律)。
446
+ if (holder === 'released') {
447
+ return { released: true, waitedMs: waited(), aborted: false, lastStatus: status, lastHolder: holder };
448
+ }
384
449
  // status 读不出(奇形记录)/ 还在跑 / park / 没想过的新词 ⇒ 都不算释放,继续等到点(保守方向:
385
450
  // 宁可不重发)。判据见 {@link CLAIM_RELEASED_STATES}。
386
451
  if (status !== null && CLAIM_RELEASED_STATES.includes(status)) {
387
- return { released: true, waitedMs: waited(), aborted: false, lastStatus: status };
452
+ return { released: true, waitedMs: waited(), aborted: false, lastStatus: status, lastHolder: holder };
388
453
  }
389
454
  }
390
455
  }
@@ -800,7 +865,13 @@ async function cancelAndConfirmRelease(taskId, durable, durableGet, deps) {
800
865
  outcome: 'timeout',
801
866
  waitedMs: verdict.waitedMs,
802
867
  aborted: verdict.aborted,
803
- confirmedHeld: verdict.lastStatus !== null && CLAIM_HELD_STATES.includes(verdict.lastStatus),
868
+ // 🔴 0.68.0 / L-230:**两条持锁证据取并**,直接证据优先 ——
869
+ // ① `heldBy` 读出 `'held'` = 引擎**直说**那条 run 还占着(server 7.73.0 S-122 P-45 读面);
870
+ // ② 老引擎不发那一位(`'unknown'`)⇒ 回落既有的按状态词推断({@link CLAIM_HELD_STATES})。
871
+ // 🔴 `'released'` 在这里到不了(released 的判决在上面就 return 了),`'unknown'` 不算证据 ——
872
+ // 两条都答不出时仍然收口成 `false` = 「确认不了」,文案绝不说「它还占着」。
873
+ confirmedHeld: verdict.lastHolder === 'held' ||
874
+ (verdict.lastStatus !== null && CLAIM_HELD_STATES.includes(verdict.lastStatus)),
804
875
  };
805
876
  }
806
877
  /**
@@ -235,7 +235,24 @@ export interface WiringManifestMcpEntry {
235
235
  * CS-7 §2.7 — turn_end usage → CC `ModelUsage` (pinned name mapping;
236
236
  * costMicroUsd/1e6 → costUSD). Surfaced separately because the slice has no
237
237
  * standalone usage SDKMessage arm; the run driver folds it into the footer /
238
- * terminal rollup. Returns `undefined` when the optional `usage` is absent.
238
+ * terminal rollup.
239
+ *
240
+ * ── 🔴 0.68.0 BREAKING(core 7.17.0 #711;异源对抗复审轮三实抓的**第五处**)──────────────────
241
+ * 返回 `undefined` 的含义从「`usage` 这一格缺席」收窄成 **「这一轮的账不知道」**,两种入形都答它:
242
+ * ① `ev.usage` 整个缺席 —— #711 之后这**不再是一条合法形**(契约违约;`runStream` 那一层另有
243
+ * 响亮留痕,本读器只如实答「不知道」);
244
+ * ② `usageMissing: true` **且六格全是 0** —— 那正是 core 的**占位**
245
+ * (`rs.turn.turnUsage ?? {六个 0}`,`run-harness-handlers.js:289-293`)。
246
+ * 🔴 为什么必须改:#711 之前,「这一轮没量出账」这件事在 wire 上的形是**不发 usage** ⇒ 本函数答
247
+ * `undefined`;#711 之后同一件事的形变成六个 0,而本函数是**公面导出** —— 不改的话,同一个真实
248
+ * 情形在引擎升级前后由同一个公开读器给出两个相反的答案(「不知道」变成「这一轮恰好花了 0」),
249
+ * 而调用方**一个字都没改**。收窄之后跨引擎升级的答案**逐字不变**。
250
+ * 🔴 **非零照交**:`usageMissing` 并不保证六格是零(core 在同一轮里可能已攒到真数字而另一次调用
251
+ * 报了缺账 ⇒ 判别位与真数字同帧并存)。占位恒为全零,所以**任何一格非零**都说明这一轮真的量到过
252
+ * ⇒ 照交镜像(那些数字是**下界**,而「是不是下界」由帧上的 `usageMissing` / 终帧的
253
+ * `_sema_usage_lower_bound` 回答,不由本函数回答)。
254
+ * ⚠️ 要**原样**的 wire 镜像(不做任何判断)请直接调 {@link turnUsageToModelUsage} —— 那一只是
255
+ * 纯映射,本函数是**带缺席语义的读器**,两者刻意分开。
239
256
  */
240
257
  export declare function turnEndUsage(ev: Extract<AgentEvent, {
241
258
  type: 'turn_end';
@@ -877,8 +877,14 @@ export function eventToSdkMessage(ev, ctx) {
877
877
  *
878
878
  * ── 这是什么 ────────────────────────────────────────────────────────────────────────────────
879
879
  * 「assistant 的这一段散文写完了」—— 模型关掉了那个 text content block,而它的字节刚刚以
880
- * `text_delta` 流过。core 的臂注逐字:`content` = **该段的权威全文**(与那一段 delta 的拼接逐字节
881
- * 相等,取自 brain 自己的累加),消费方**据它提交这一段**,而不是信自己的 delta 缝合。
880
+ * `text_delta` 流过。core 的臂注逐字:`content` = **该段的权威全文**(取自 brain 自己的累加),
881
+ * 消费方**据它提交这一段**,而不是信自己的 delta 缝合。
882
+ *
883
+ * 🔴 **「与 delta 拼接逐字节相等」这句自 server 7.75.3 起不再成立**(L-310,0.68.1 订正;此前本注
884
+ * 照抄 core 臂注的那半句):`text_end.content` 与 `result`/账本走**同一只脱敏器**,而 `text_delta`
885
+ * 仍逐字(跨 chunk 的凭据无法就地判,流式脱敏是独立设计件)⇒ 含凭据形的段上两者**字节不同**。
886
+ * server `ASSISTANT-WIRE-CONTRACT` §5.1 live 面逐字要求消费端「在 `text_end` 到达时以它**整段
887
+ * 替换**已攒的 delta,而不是只当段界信号」。
882
888
  *
883
889
  * 🔴 **它的存在意义 = 让消费方撤掉 idle-flush 启发式**(core 臂注点名的那件事)。本包的
884
890
  * `adapt/textStream.ts` 至今用「静默 1.5s + 句末/段末边界 + 每段一刀」猜段边界(#323 症状① 的止血
@@ -893,11 +899,15 @@ export function eventToSdkMessage(ev, ctx) {
893
899
  * 信号(段边界 + 权威全文),静默丢 = 把一个真实能力缺口做成 fail-open,正是本文件对
894
900
  * `approval_request` 那段头注点名的病形。⇒ 投中性内部臂,宿主(壳 REPL 桥 / web / desktop 座位层)
895
901
  * 在臂上读它、决定何时提交段。
896
- * · 📋 **本包内的接线如实留白**(不在本批做):要让 `adapt/textStream.ts` 真的**撤掉** idle-flush,
897
- * 得先答一个行为面问题 —— core 臂注明写这是「诚实缺席」的位(只有会报块结束的 brain 才发它,
898
- * 自定义 brain 可能整条流一帧都没有),所以消费方必须按**每条流**判「这条流带不带边界帧」再决定
899
- * 退不退启发式。那是一条带状态的策略,属行为面改动,按宪法三问单独走,不在本提货批里顺手加。
900
- * 本批只把信号送到宿主手上(壳侧接线是下一棒),缺口写在这里,不留白。
902
+ * · **包内接线**(0.68.1 / L-310 起):`adapt/arms.ts` 的 `textSegmentEndArm` 拿这条内部臂调
903
+ * `adapt/textStream.ts` 的 `replaceAnswerSegment(content, frame)` —— **整段替换**段缓冲与活体尾巴,
904
+ * 再把 `diverged` / `committedPrefixDiverged` / `committedPrefixLen` 三个 never-false 键挂到
905
+ * chrome `text_segment_end` 上交给宿主。
906
+ * · 📋 **仍然留白的那一件**:让 `textStream` 真的**撤掉** idle-flush 启发式。core 臂注明写这是
907
+ * 「诚实缺席」的位(只有会报块结束的 brain 才发它,自定义 brain 可能整条流一帧都没有),所以
908
+ * 消费方必须按**每条流**判「这条流带不带边界帧」再决定退不退启发式 —— 那是一条带状态的策略,
909
+ * 属行为面改动,按宪法三问单独走。权威替换**兼容**启发式(半段已被 flush 那一形由两个前缀键
910
+ * 如实交代),不是它的继任。
901
911
  *
902
912
  * ── 畸形与空段(fail-closed 方向 + 对位 core 的「空段无帧」)────────────────────────────────
903
913
  * · `content` **非串** ⇒ `malformed`:承重位读不动(wire 是 JSON,SDK 只 JSON.parse 不校型)。
@@ -1198,10 +1208,41 @@ function humanInputProjection(ev, ctx) {
1198
1208
  * CS-7 §2.7 — turn_end usage → CC `ModelUsage` (pinned name mapping;
1199
1209
  * costMicroUsd/1e6 → costUSD). Surfaced separately because the slice has no
1200
1210
  * standalone usage SDKMessage arm; the run driver folds it into the footer /
1201
- * terminal rollup. Returns `undefined` when the optional `usage` is absent.
1211
+ * terminal rollup.
1212
+ *
1213
+ * ── 🔴 0.68.0 BREAKING(core 7.17.0 #711;异源对抗复审轮三实抓的**第五处**)──────────────────
1214
+ * 返回 `undefined` 的含义从「`usage` 这一格缺席」收窄成 **「这一轮的账不知道」**,两种入形都答它:
1215
+ * ① `ev.usage` 整个缺席 —— #711 之后这**不再是一条合法形**(契约违约;`runStream` 那一层另有
1216
+ * 响亮留痕,本读器只如实答「不知道」);
1217
+ * ② `usageMissing: true` **且六格全是 0** —— 那正是 core 的**占位**
1218
+ * (`rs.turn.turnUsage ?? {六个 0}`,`run-harness-handlers.js:289-293`)。
1219
+ * 🔴 为什么必须改:#711 之前,「这一轮没量出账」这件事在 wire 上的形是**不发 usage** ⇒ 本函数答
1220
+ * `undefined`;#711 之后同一件事的形变成六个 0,而本函数是**公面导出** —— 不改的话,同一个真实
1221
+ * 情形在引擎升级前后由同一个公开读器给出两个相反的答案(「不知道」变成「这一轮恰好花了 0」),
1222
+ * 而调用方**一个字都没改**。收窄之后跨引擎升级的答案**逐字不变**。
1223
+ * 🔴 **非零照交**:`usageMissing` 并不保证六格是零(core 在同一轮里可能已攒到真数字而另一次调用
1224
+ * 报了缺账 ⇒ 判别位与真数字同帧并存)。占位恒为全零,所以**任何一格非零**都说明这一轮真的量到过
1225
+ * ⇒ 照交镜像(那些数字是**下界**,而「是不是下界」由帧上的 `usageMissing` / 终帧的
1226
+ * `_sema_usage_lower_bound` 回答,不由本函数回答)。
1227
+ * ⚠️ 要**原样**的 wire 镜像(不做任何判断)请直接调 {@link turnUsageToModelUsage} —— 那一只是
1228
+ * 纯映射,本函数是**带缺席语义的读器**,两者刻意分开。
1202
1229
  */
1203
1230
  export function turnEndUsage(ev) {
1204
- return ev.usage ? turnUsageToModelUsage(ev.usage) : undefined;
1231
+ // 🔴 **成形判据不是 `!raw`**(异源复审轮四同族):wire 是 JSON、SSE 解析原样透传 ⇒ `usage: null` /
1232
+ // `usage: 7` 这类形真到得了这里。`null` 会让映射在读字段时抛;标量更坏 —— 它会被映射成一份
1233
+ // **看起来已测量**的全零账(`toCcModelUsage(7)` 逐格读不出、逐格折 0)。坏形与缺席同义:
1234
+ // **这一轮的账不知道**。
1235
+ const rawValue = ev.usage;
1236
+ if (typeof rawValue !== 'object' || rawValue === null || Array.isArray(rawValue))
1237
+ return undefined;
1238
+ const raw = rawValue;
1239
+ const unknown = ev.usageMissing === true;
1240
+ // 占位判据 = **六格全零**(不是「有 usageMissing 就算占位」)。逐格按值判,非有限值当 0 看待
1241
+ // (那一格本来就读不出,它不构成「量到过」的证据)。
1242
+ const placeholder = unknown &&
1243
+ [raw.inputTokens, raw.totalInputTokens, raw.outputTokens, raw.cacheReadTokens, raw.cacheWriteTokens, raw.costMicroUsd]
1244
+ .every((v) => !(typeof v === 'number' && Number.isFinite(v) && v !== 0));
1245
+ return placeholder ? undefined : turnUsageToModelUsage(raw);
1205
1246
  }
1206
1247
  /**
1207
1248
  * Defensive open-set read of a `suspended` gate kind for the run driver's HITL
@@ -304,6 +304,35 @@ export interface MutableSubagentUsageRow {
304
304
  */
305
305
  keyFromParentFallback?: true;
306
306
  }
307
+ /**
308
+ * 子代用量面的**下界判别位键名**(`_sema_subagent_usage_partial`)。
309
+ *
310
+ * 🔴 它与终帧的 `_sema_usage_lower_bound` **刻意不同名**(见上段):两者答的是两个问题,
311
+ * 一个消费面同时拿到两者时必须分得出来。
312
+ * 🔴 本包**不在任何 wire 帧上铸它** —— 它是**渲染面**的位(端把一行子代用量交给自己的视图时用)。
313
+ * 包给名与判据,是为了三端零自拼(同 `gateIdentity` 的三条身份键字面同一条纪律)。
314
+ */
315
+ export declare const SEMA_SUBAGENT_USAGE_PARTIAL_KEY = "_sema_subagent_usage_partial";
316
+ /**
317
+ * 一行子代用量的数字**是不是下界**({@link SEMA_SUBAGENT_USAGE_PARTIAL_KEY} 该不该立)。
318
+ *
319
+ * 三个来源**取并**,每一条都能独立让这一行的数字不是最终数:
320
+ * · `row.usageMissing` —— 这只子代**至少有一轮**引擎没报账(数字照累加,但它是下界);
321
+ * · `row.keyCollision` —— 两个命名空间的 id 撞了字面,这一行是**几只子代的账并起来的**;
322
+ * · `opts.tablePartial` —— 整张表对不上引擎的权威合计(`_sema_nested_usage_by_task_partial`)⇒
323
+ * 表里**每一行**都不可证完整。
324
+ *
325
+ * 🔴 **`false` 不是「这一行是最终数」的证据**:它只说「本端没有任何一条理由认为它是下界」。
326
+ * 行本身还没收口(端自己的 store 知道,包不知道)时,端要自己把那一条并进来 —— 所以这只谓词
327
+ * 收一个 `opts`,而不是假装它掌握全部真相。
328
+ * 🔴 非对象 / 缺席入参 ⇒ `false`(答不出,不是断言);坏形位(非 `true` 的值)不当真。
329
+ */
330
+ export declare function subagentUsageIsPartial(row: {
331
+ readonly usageMissing?: unknown;
332
+ readonly keyCollision?: unknown;
333
+ } | null | undefined, opts?: {
334
+ readonly tablePartial?: boolean;
335
+ }): boolean;
307
336
  /** 终帧两个超集键的产物形(见 {@link nestedUsageByTaskParts})。 */
308
337
  export interface SemaNestedUsageByTask {
309
338
  readonly rows: Readonly<Record<string, SemaSubagentUsageRow>>;
@@ -1,4 +1,5 @@
1
1
  import { stamp } from '../types.js';
2
+ import { putOwnKey } from '../../ownKey.js';
2
3
  // 0.60.0(engine ≥7.64.0 / sdk 8.4.0):终局读数的**单一读器**(两代字节 → 一个带标因由)。
3
4
  import { isReviewPark, readRunTerminal, runTerminalCode, runTerminalGateToolName, } from '../../runTerminal.js';
4
5
  import { toCcModelUsage } from './turnUsageToModelUsage.js';
@@ -160,22 +161,11 @@ function permissionDenialParts(stats) {
160
161
  }
161
162
  /**
162
163
  * 0.67.1 —— **以 wire 给的 id / 键名当对象键**时的唯一落键姿势(`__proto__` 陷阱)。
163
- *
164
- * 🔴 `Object.prototype.__proto__` 是一个 **accessor**:在一只普通对象上写 `o["__proto__"] = v`
165
- * 走的是那只 setter ——**不产生自有属性**(v 是对象时还顺手改了 `o` 的原型),于是那一行在
166
- * `Object.keys` / `JSON.stringify` 里**整条消失**,连行数都少一。而本文件这几张表的键全都来自
167
- * wire(taskId / modelId / core 开集的 costBreakdown 键名),没有任何一条保证它们不等于这个字面。
168
- * ⇒ 落键一律走 `defineProperty`,与本包 `hitl/crashConverged.ts` 交付快照时的处置**同一条**
169
- * (那里逐字:「落键仍走 `defineProperty`(`__proto__` 同理)」)。
170
- *
171
- * 🔴 **不改成 null 原型对象交付**:端拿到的仍是一只正常对象(`hasOwnProperty` / `toString` 都在),
172
- * 本修只改「落键」这一步,不改交付形 —— 换原型会在宿主侧造出一类新的 `TypeError`。
173
- * 描述符与普通赋值**逐位相同**(`writable/enumerable/configurable` 三真),所以除了 `__proto__`
174
- * 这一个字面,其余每一个键的行为一个字节都没变。
164
+ * 🔴 0.68.0 / L-246 B2:实现**下沉到 `src/ownKey.ts`**(单源)—— 修前本文件与
165
+ * `hitl/parkResolver.ts` 各持一份同形实现,理由、陷阱与「不改成 null 原型交付」的取舍都在
166
+ * 那个模块的头注里。本别名保留是为了本文件三十余处调用点零改动。
175
167
  */
176
- function putOwn(table, key, value) {
177
- Object.defineProperty(table, key, { value, enumerable: true, writable: true, configurable: true });
178
- }
168
+ const putOwn = putOwnKey;
179
169
  /** 有限数窄化(非数 / 非有限 ⇒ 缺席;`0` 是事实不是缺席)。 */
180
170
  function finiteOrAbsent(v) {
181
171
  return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
@@ -319,6 +309,47 @@ function costFactParts(stats, observed) {
319
309
  // (本位在上面与流内观测取并后已铸;这里不再重复。)
320
310
  };
321
311
  }
312
+ // ══ 0.68.0(L-244 包侧半场)—— 子代用量「这笔账是下界」的**自有位**,与终帧那一位**分名** ══
313
+ //
314
+ // ── 病形(三重复审 A4/B10 在壳上实抓)──────────────────────────────────────────────────────
315
+ // 壳的子代用量 store 把「这一行还没收口(`partial`)」∪「这一轮引擎没报账(`usageMissing`)」两件事
316
+ // 铸成了一个**与终帧同名**的键 `_sema_usage_lower_bound`。两者是**同名异义**:
317
+ // · 终帧那一位答的是「**这条 run 的合计**是下界」(来源 = `stats.usageMissing` ∪ 流内观测);
318
+ // · 子代面那一位答的是「**这一只子代的这一行**现在还不是最终数」(来源 = 行还没收口 / 那一轮没报账)。
319
+ // 同名的代价是消费面分不出自己读到的是哪一个:一个按键名做聚合的面(把所有 `_sema_usage_lower_bound`
320
+ // 收起来渲一句「本次会话的账是下界」)会把一条**只是还没收口的子代行**算成整条 run 的账不可信。
321
+ //
322
+ // ⇒ 包侧给出**自有名**与**唯一判据**,壳/web/desktop 三端照它渲,谁都不再自己拼一个键名:
323
+ /**
324
+ * 子代用量面的**下界判别位键名**(`_sema_subagent_usage_partial`)。
325
+ *
326
+ * 🔴 它与终帧的 `_sema_usage_lower_bound` **刻意不同名**(见上段):两者答的是两个问题,
327
+ * 一个消费面同时拿到两者时必须分得出来。
328
+ * 🔴 本包**不在任何 wire 帧上铸它** —— 它是**渲染面**的位(端把一行子代用量交给自己的视图时用)。
329
+ * 包给名与判据,是为了三端零自拼(同 `gateIdentity` 的三条身份键字面同一条纪律)。
330
+ */
331
+ export const SEMA_SUBAGENT_USAGE_PARTIAL_KEY = '_sema_subagent_usage_partial';
332
+ /**
333
+ * 一行子代用量的数字**是不是下界**({@link SEMA_SUBAGENT_USAGE_PARTIAL_KEY} 该不该立)。
334
+ *
335
+ * 三个来源**取并**,每一条都能独立让这一行的数字不是最终数:
336
+ * · `row.usageMissing` —— 这只子代**至少有一轮**引擎没报账(数字照累加,但它是下界);
337
+ * · `row.keyCollision` —— 两个命名空间的 id 撞了字面,这一行是**几只子代的账并起来的**;
338
+ * · `opts.tablePartial` —— 整张表对不上引擎的权威合计(`_sema_nested_usage_by_task_partial`)⇒
339
+ * 表里**每一行**都不可证完整。
340
+ *
341
+ * 🔴 **`false` 不是「这一行是最终数」的证据**:它只说「本端没有任何一条理由认为它是下界」。
342
+ * 行本身还没收口(端自己的 store 知道,包不知道)时,端要自己把那一条并进来 —— 所以这只谓词
343
+ * 收一个 `opts`,而不是假装它掌握全部真相。
344
+ * 🔴 非对象 / 缺席入参 ⇒ `false`(答不出,不是断言);坏形位(非 `true` 的值)不当真。
345
+ */
346
+ export function subagentUsageIsPartial(row, opts) {
347
+ if (opts?.tablePartial === true)
348
+ return true;
349
+ if (typeof row !== 'object' || row === null)
350
+ return false;
351
+ return row.usageMissing === true || row.keyCollision === true;
352
+ }
322
353
  /**
323
354
  * L-228 —— 流内子代分表 → 终帧两个超集键的**唯一 mint 点**(成功臂与错误信封共用)。
324
355
  *
@@ -32,7 +32,7 @@
32
32
  */
33
33
  import type { AgentEvent } from '@sema-agent/sdk';
34
34
  import { type SDKMessage, type EmitContext, type ModelUsage } from './types.js';
35
- import type { EngineTurnUsage } from './downstream/turnUsageToModelUsage.js';
35
+ import { type EngineTurnUsage } from './downstream/turnUsageToModelUsage.js';
36
36
  /**
37
37
  * 409 body 的 pendingGate 材料(A-028.1,2026-08-12 具名化并补 `governanceForced` 位 ——
38
38
  * 此前包侧只有 {kind, decidePath} 两位,壳侧抄件已多出该位 = 同一 wire 位两份解析器形不同,
@@ -178,14 +178,34 @@ export declare function _resetDroppedFrameReportForTest(): void;
178
178
  * 证不了「表有没有涨」;两者在到顶之后恰好分道扬镳,所以必须直接读表)。 */
179
179
  export declare function _droppedFrameMemoSizeForTest(): number;
180
180
  export interface RunStreamHandle {
181
- /** The latest folded turn usage (footer counters); updated on each turn_end. */
181
+ /**
182
+ * The latest folded turn usage (footer counters).
183
+ *
184
+ * 🔴 **0.68.0 收窄成「最近一次**已测量**的 turn」**(core 7.17.0 #711 的跟车修;异源对抗复审实抓):
185
+ * #711 之后,一轮没量出账时引擎发的是**六个 0 + `usageMissing:true`**(不再是「不发 usage」)。
186
+ * 修前这里是无条件覆盖 ⇒ 那种轮会把一份**占位全零**盖进来,而本形上**没有任何判别位** ——
187
+ * 只吃这个出口的 footer 于是把「不知道」渲成一笔精确的零账(消息臂与 chrome 臂上的判别位
188
+ * 保护不到这个出口)。⇒ 未测量的那一轮**不覆盖**(保留上一次真读数,与 #711 之前的行为逐字
189
+ * 相同),并由 {@link latestUsageMissing} 说出「最新那一轮没测出账」。
190
+ */
182
191
  latestUsage?: ModelUsage;
183
192
  /**
184
193
  * [2295] 裁 ② 逐字通道:与 latestUsage 同拍更新的引擎 `turn_end.usage` **原形**(六键含
185
194
  * `totalInputTokens`)。镜像键求和≠总量(仅 cache 族一致时相等),总量消费面吃这份。
186
195
  * 旧引擎(core <3.0.0)wire 缺形时为 undefined —— 诚实缺席,不造零值。
196
+ * 🔴 0.68.0:与 {@link latestUsage} **同拍同律** —— 未测量的轮不覆盖。
187
197
  */
188
198
  latestEngineUsage?: EngineTurnUsage | undefined;
199
+ /**
200
+ * 🔴 0.68.0 —— **最新那一轮的账知不知道**。在场(恒 `true`)= 最近走过的那个 `turn_end`
201
+ * **没测出账**(`usageMissing:true`),或者它是一条**契约违约**帧(连 usage 都没有);
202
+ * 此时上面两份读数属于**更早的**那一轮,别当成最新那一轮的账。
203
+ *
204
+ * 🔴 **never false**:测量到账的那一轮**把这一位删掉**(键不在场 ⇔ 上面两份就是最新那一轮的账)。
205
+ * 它是**逐轮**的判别位,不是「这条流上曾经有过缺口」的单调位 —— 后者在终帧上
206
+ * (`_sema_usage_lower_bound`),两者答的是两个问题。
207
+ */
208
+ latestUsageMissing?: true;
189
209
  }
190
210
  export declare function isRunStreamActive(): boolean;
191
211
  export declare function runStream(events: AsyncIterable<AgentEvent>, ctx: EmitContext, handle?: RunStreamHandle): AsyncGenerator<SDKMessage>;