@sema-agent/client-core 0.75.1 → 0.76.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 (65) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +11 -1
  3. package/dist/adapt/arms.js +137 -35
  4. package/dist/adapt/ids.d.ts +29 -2
  5. package/dist/adapt/ids.js +29 -2
  6. package/dist/adapt/panelTasks.d.ts +4 -3
  7. package/dist/adapt/panelTasks.js +29 -11
  8. package/dist/adapt/textStream.js +5 -5
  9. package/dist/adapt/toolCards.js +2 -2
  10. package/dist/adapt/turnFlags.js +5 -5
  11. package/dist/adapt.d.ts +1 -1
  12. package/dist/adapt.js +8 -8
  13. package/dist/adapter/downstream/eventToSdkMessage.d.ts +57 -0
  14. package/dist/adapter/downstream/eventToSdkMessage.js +125 -17
  15. package/dist/adapter/runStream.js +14 -5
  16. package/dist/adapter/types.d.ts +12 -0
  17. package/dist/adapter/types.js +30 -0
  18. package/dist/agentsWireCaps.d.ts +15 -2
  19. package/dist/approvalsStreamLiveCapability.js +15 -35
  20. package/dist/deviceExecutorManagementCapability.js +15 -35
  21. package/dist/engineAgentPanelStore.js +13 -1
  22. package/dist/engineCapReader.d.ts +77 -0
  23. package/dist/engineCapReader.js +87 -0
  24. package/dist/engineErrorCodes.d.ts +4 -0
  25. package/dist/engineErrorCodes.js +14 -0
  26. package/dist/executionLaneCapability.js +15 -38
  27. package/dist/fleet/fleetRowAgentType.js +1 -1
  28. package/dist/hitl/approvalOutcomeNote.d.ts +0 -10
  29. package/dist/hitl/approvalOutcomeNote.js +35 -8
  30. package/dist/hitl/approvalResolution.d.ts +148 -0
  31. package/dist/hitl/approvalResolution.js +199 -0
  32. package/dist/hitl/approvalsFeed.d.ts +100 -2
  33. package/dist/hitl/approvalsFeed.js +234 -18
  34. package/dist/hitl/livePendingAsk.d.ts +26 -6
  35. package/dist/hitl/livePendingAsk.js +52 -12
  36. package/dist/hitl/persistedRulesWire.d.ts +20 -0
  37. package/dist/hitl/persistedRulesWire.js +23 -0
  38. package/dist/index.d.ts +8 -0
  39. package/dist/index.js +25 -1
  40. package/dist/mcpLiveness.d.ts +179 -0
  41. package/dist/mcpLiveness.js +218 -0
  42. package/dist/mcpPanel.d.ts +17 -0
  43. package/dist/mcpPanel.js +21 -0
  44. package/dist/mcpProbeCapability.d.ts +84 -0
  45. package/dist/mcpProbeCapability.js +124 -0
  46. package/dist/memoryComplianceCapability.d.ts +73 -0
  47. package/dist/memoryComplianceCapability.js +109 -0
  48. package/dist/memoryEntriesWire.d.ts +302 -0
  49. package/dist/memoryEntriesWire.js +595 -0
  50. package/dist/memoryOriginCapability.d.ts +68 -0
  51. package/dist/memoryOriginCapability.js +102 -0
  52. package/dist/memorySpecWire.d.ts +175 -0
  53. package/dist/memorySpecWire.js +320 -0
  54. package/dist/peerLaneCapability.d.ts +62 -0
  55. package/dist/peerLaneCapability.js +100 -0
  56. package/dist/permissionRulesWriteCapability.d.ts +64 -0
  57. package/dist/permissionRulesWriteCapability.js +98 -0
  58. package/dist/seam.d.ts +53 -5
  59. package/dist/seam.js +10 -1
  60. package/dist/selfOrchestrationDenial.js +7 -2
  61. package/dist/sqlEngineCapability.js +15 -35
  62. package/dist/webSearchBackendCapability.js +15 -35
  63. package/dist/writeProtectionCapability.js +15 -35
  64. package/docs/INTEGRATION-CLIENTS.md +148 -15
  65. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { deriveTranscriptId } from '../seam.js';
2
2
  import { degradedToolResultBody, detectEngineBgShellReceipt, reportFindingsToolUseResult, structuredToToolUseResult, todoWriteToolUseResult, wireOutputToBody, } from '../toolResult.js';
3
- import { chrome, MAIN, transcript } from './ids.js';
3
+ import { chrome, mainLane, transcript } from './ids.js';
4
4
  import { flattenWireOutput } from './wireShapes.js';
5
5
  export function createToolCardLedger(ctx, idOf) {
6
6
  const pending = new Map();
@@ -26,7 +26,7 @@ export function createToolCardLedger(ctx, idOf) {
26
26
  if ((p.name === 'Bash' || p.name === 'Monitor') && !isError && end?.output !== undefined) {
27
27
  const reg = detectEngineBgShellReceipt(p.name, p.rawInput, flattenWireOutput(end.output));
28
28
  if (reg) {
29
- yield chrome({ kind: 'bgshell_register', laneProof: MAIN, taskId: reg.taskId, registration: reg });
29
+ yield chrome({ kind: 'bgshell_register', laneProof: mainLane(), taskId: reg.taskId, registration: reg });
30
30
  }
31
31
  }
32
32
  // B 层:structured → 逐工具 typed toolUseResult(卡的 outputSchema safeParse 过才渲富卡)。
@@ -1,4 +1,4 @@
1
- import { chrome, MAIN } from './ids.js';
1
+ import { chrome, mainLane } from './ids.js';
2
2
  export function createTurnFlags(ctx, turnStartAt) {
3
3
  let messageStartEmitted = false;
4
4
  let endEmitted = false;
@@ -29,20 +29,20 @@ export function createTurnFlags(ctx, turnStartAt) {
29
29
  // (主 brain 仍可能在退避等待,#47 矩阵 #5 同族判别)。落旗后 `takeRetryOverlay()` 会对随后
30
30
  // 到达的 `recovered` 直报帧返回 false ⇒ 不补第二发(两条腿合计恰好一发)。
31
31
  retryOverlayActive = false;
32
- yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
32
+ yield chrome({ kind: 'retry_status', laneProof: mainLane(), status: null });
33
33
  }
34
34
  if (m.type === 'stream_event' && !messageStartEmitted) {
35
35
  messageStartEmitted = true;
36
36
  yield chrome({
37
37
  kind: 'response_metrics',
38
- laneProof: MAIN,
38
+ laneProof: mainLane(),
39
39
  phase: 'start',
40
40
  ttftMs: ctx.now() - turnStartAt,
41
41
  });
42
42
  }
43
43
  },
44
44
  *emitTurnUsageEnd(outputTokens) {
45
- yield chrome({ kind: 'response_metrics', laneProof: MAIN, phase: 'end', outputTokens });
45
+ yield chrome({ kind: 'response_metrics', laneProof: mainLane(), phase: 'end', outputTokens });
46
46
  endEmitted = true;
47
47
  messageStartEmitted = false; // 下一 model round 重新压基线(与 noteModelRoundBoundary 同一件事)
48
48
  },
@@ -55,7 +55,7 @@ export function createTurnFlags(ctx, turnStartAt) {
55
55
  const outputTokens = readOutputTokens();
56
56
  if (outputTokens === undefined)
57
57
  return;
58
- yield chrome({ kind: 'response_metrics', laneProof: MAIN, phase: 'end', outputTokens });
58
+ yield chrome({ kind: 'response_metrics', laneProof: mainLane(), phase: 'end', outputTokens });
59
59
  endEmitted = true;
60
60
  },
61
61
  noteRetryOverlay: () => {
package/dist/adapt.d.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * ── A 族拆分后的地图(REF-CC-SPLIT-01~05 / FIX-05,2026-08-02;设计=docs/refactor/ADAPT-RW-MATRIX.md)
17
17
  * `adapt()` 曾是 1139 行单方法(26 个 turn 级可变量 + 14 个闭包 + 15 个 case)。现在:
18
18
  * 本文件 = 公面契约表 + 适配器实例(台账)+ **驱动壳**(迭代器竞速 / abort / finally / 收口三形)
19
- * adapt/ids.ts 零状态共用件(双平面包装、Frame、MAIN、id 派生工厂)
19
+ * adapt/ids.ts 零状态共用件(双平面包装、Frame、主车道证明出厂口、id 派生工厂)
20
20
  * adapt/wireShapes.ts 帧形词表 + 纯投影助手(公面十件由本文件具名再导出)
21
21
  * adapt/textStream.ts M1 流合并器(矩阵 §1.1 十二行状态 + 竞速/节拍两处判据)
22
22
  * adapt/toolCards.ts M2 工具卡台账(§1.2 三行;S1 复位收进 close() 第一个 yield 前)
package/dist/adapt.js CHANGED
@@ -9,7 +9,7 @@ import { isEngineWorkflowNotified, isWorkflowCompletionCardEnqueued, listNotifie
9
9
  */
10
10
  export { CANCEL_MESSAGE, estimateCjkTokens, flattenWireOutput, FLUSH_INTERVAL_MS, IDLE_FLUSH_MS, REJECT_MESSAGE, sanitizeToolUseBlock, shortTaskLabel, SUBAGENT_TOOL_NAMES, WORKFLOW_TOOL_NAMES, } from './adapt/wireShapes.js';
11
11
  import { IDLE_FLUSH_MS } from './adapt/wireShapes.js';
12
- import { chrome, MAIN, makeIdOf } from './adapt/ids.js';
12
+ import { chrome, mainLane, makeIdOf } from './adapt/ids.js';
13
13
  import { createTextStream } from './adapt/textStream.js';
14
14
  import { createToolCardLedger } from './adapt/toolCards.js';
15
15
  import { createPanelTaskLedger } from './adapt/panelTasks.js';
@@ -237,10 +237,10 @@ class WireToCcAdapterImpl {
237
237
  const deps = { ctx, idOf, text, cards, panel, flags, inst };
238
238
  // turn 开场卫生(T38 三清,cli 逐条同序):清上一 turn 的 retry 覆盖层 + **上一轮的建议批** +
239
239
  // 思考活动行,再发 request-start(T39)。三清缺一都是"上一轮的东西挂着不掉"。
240
- yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
241
- yield chrome({ kind: 'prompt_suggestions', laneProof: MAIN, suggestions: null });
242
- yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: false });
243
- yield chrome({ kind: 'request_start', laneProof: MAIN });
240
+ yield chrome({ kind: 'retry_status', laneProof: mainLane(), status: null });
241
+ yield chrome({ kind: 'prompt_suggestions', laneProof: mainLane(), suggestions: null });
242
+ yield chrome({ kind: 'thinking_activity', laneProof: mainLane(), active: false });
243
+ yield chrome({ kind: 'request_start', laneProof: mainLane() });
244
244
  // ── T40/T58/T59:手动 iterator + IDLE-FLUSH 竞速 + abort 断流 ────────────────────────────────
245
245
  // `for await` 换成手动 next():① 才能与 idle 计时器 race(T40);② 才能在 abort 时立刻停;
246
246
  // 代价是失去 for-await 的隐式 cleanup ⇒ finally 里手动 return()(T58,少了它 = SSE 不拆)。
@@ -342,8 +342,8 @@ class WireToCcAdapterImpl {
342
342
  // 渲 InterruptedByUser chrome,那是宿主的事)。但**瞬态覆盖层必须收干净**,否则 Esc 之后
343
343
  // 上一轮的 retry 行/思考活动行会一直挂着。
344
344
  if (ctx.signal?.aborted) {
345
- yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
346
- yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: false });
345
+ yield chrome({ kind: 'retry_status', laneProof: mainLane(), status: null });
346
+ yield chrome({ kind: 'thinking_activity', laneProof: mainLane(), active: false });
347
347
  // 复审修 #2a:被打断的 turn 也**必须**结掉自己的面板行,否则 Esc 之后 footer 药丸永远
348
348
  // 显示 "1 local agent"(sweep 点 3/4;四点各有正控,见 pure 门 SPLIT 段钉①)。
349
349
  yield* panel.settle(null, false);
@@ -353,7 +353,7 @@ class WireToCcAdapterImpl {
353
353
  // T60 —— turn 收口(cli 尾部同序,覆盖"流结束了但没有 result 臂"的路径):
354
354
  // 泄尾增量 → 清 liveness → 提交尾思考 → 提交尾文本段。
355
355
  yield* text.drainLive();
356
- yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
356
+ yield chrome({ kind: 'retry_status', laneProof: mainLane(), status: null });
357
357
  // 复审修 #2a 孪生:没有 `result` 臂的收尾路径同样 sweep(sweep 点 4/4)。
358
358
  yield* panel.settle(null, false);
359
359
  // B5:无 `result` 收尾腿同样关掉还开着的卡(cli 同位;abort 路径**不做**,见上)。
@@ -35,6 +35,7 @@
35
35
  */
36
36
  import type { AgentEvent, CheckpointGate } from '@sema-agent/sdk';
37
37
  import { type SDKMessage, type EmitContext, type ModelUsage } from '../types.js';
38
+ import { type McpLivenessView } from '../../mcpLiveness.js';
38
39
  /**
39
40
  * 本适配器铸出的**内部臂** `type` 全集(A-004.4 上游半场,2026-08-08)——「内部臂」= 桥面
40
41
  * (bridge-facing)消息,**不是** CC SDK stdout 联合体里的帧形(那些是 assistant/stream_event/
@@ -230,6 +231,17 @@ export interface WiringManifestMcpEntryView {
230
231
  * 包再校一遍只会在上游合法演进的当天把一个真读数判没(与 `errorCode` 的开集纪律同规)。
231
232
  */
232
233
  httpStatus?: number;
234
+ /**
235
+ * 引擎最后一次**活性观察**(CC-36;第六键,引擎侧自 core 7.24.3 / server 7.91.2 起)。判据、词表与
236
+ * 「缺席 = 没有可用观察记录」那四句话的单一铸点在 {@link readMcpLiveness} 的顶注。
237
+ * 🔴 **缺席不铸**,且缺席**不是** `unknown`:后者是一句更强的断言(引擎看了,看不出来)。
238
+ * 🔴 与同行的 {@link status} / re-dial 回执**各说各的**,三者不合并、不互证(`status:'failed'` 与
239
+ * `liveness.state:'reachable'` 同行是真行:活着,且配错了)。
240
+ */
241
+ liveness?: McpLivenessView;
242
+ /** never false:这一行的活性格**在场而读不出**(与整格缺席不是一句话 —— 「送来一份我读不懂的」要
243
+ * 能与「引擎没报」分开,否则腿级判词会拿读不出当可达的证据)。 */
244
+ livenessUnreadable?: true;
233
245
  }
234
246
  export declare function projectMcpSection(raw: unknown): WiringManifestMcpEntryView[] | undefined;
235
247
  /** core 7.20.1(0.71.4):`wiring_manifest.writeProtection` 第七段 —— 写保护判官自报的目标视图(present-iff 这条腿编译出了判官)。 */
@@ -276,6 +288,51 @@ export interface ContextUsageSectionView {
276
288
  tokens: number;
277
289
  kind?: string;
278
290
  }
291
+ /**
292
+ * CC-91(0.76.0):`prompt_assembled.sections[]` 一行 —— 编译 IR 面的**一段**(core `prompt.assembled` 的 v2 面)。
293
+ *
294
+ * 🔴 **只有 `chars`,没有 `tokens`** —— 这不是本层的取舍,是上游真字节里就没有这一位:core 的臂声明
295
+ * (`core/trace.ts` `kind:"prompt.assembled"` 的 `sections?: Array<{id, slot, carrier, cadence, cacheClass, chars, hash, contentHash?}>`)
296
+ * 与 server 的逐字段白名单(`observability/prompt-manifest.ts` `promptManifestRecordOf`)两面都是 `chars`。
297
+ * ⇒ 本层**不铸** `tokens`:拿 `chars / 系数` 编一个数就是把「不知道」渲成一个看起来精确的假值。
298
+ * 要引擎自己的 token 估算,读 **`context_usage.sections[].tokens`**(core `prompt-assembly/section-usage.ts`
299
+ * `contextUsageSections` 就是拿同一份 `chars` 按引擎系数估的)。🔴 这条 join **只在两面的段视图都在场时成立**:
300
+ * 那一面也是从同一份 manifest 段列投影的,段列缺席时它同样缺席 ⇒ v1-only 腿上没有这条 join,那时**别**拿
301
+ * `blocks` 当段的同名替代({@link PromptManifestBlockView}:两张词表不同,且段→块可多对一)。
302
+ * 🔴 `id` 是 core pack 的 `core/<slot>.<name>` 命名空间**开集**(`core/role.base` / `core/harness.head` /
303
+ * `core/discovery.mcp-instructions` / `core/environment.context` / `core/memory.tail` …),**不是**任何端
304
+ * /context 面板的展示分类名;本层按开集读,认不得的 id 原词照带。
305
+ * `slot` = 语义槽(让视图不必认识每个 id 就能分组),**逐字沿用上游的键名**:同一件事在 `context_usage.sections[]`
306
+ * 那一面叫 `kind`,在本面叫 `slot`,两边各自跟自己的上游,本层**不改名**。改名看着整齐,代价是同一个事实在包内
307
+ * 多一个只有本包知道的别名 —— 而本层有两个入口(投影口 / 直喂 `adapt()`),别名一多两条路就会读出不同的值。
308
+ * 缺席 = 那个条目没声明槽,不是没语义。
309
+ */
310
+ export interface PromptManifestSectionView {
311
+ id: string;
312
+ /** 这一段**渲染出来的字符数**(prepare 期冻结)。🔴 不是 token 数;别拿它与 `context_usage.usedTokens` 相减。 */
313
+ chars: number;
314
+ slot?: string;
315
+ }
316
+ /** CC-91(0.76.0):`prompt_assembled.blocks[]` 一行 —— v1 面的**带标签块**(`role.base` / `harness.context` /
317
+ * `env.context` / `memory.tail` …)。🔴 与 `sections[].id` 是**两张不同的词表**,而且**多对一**:多个段可折进
318
+ * 同一个块(core `prompt-assembly/types.ts` 的 `blockId`)⇒ 块不是段的另一种写法,别把两面混着分组。
319
+ * v1-only 引擎上段视图整键缺席,这一面是那时唯一的组成视图 —— 🔴 **那时没有与 `context_usage` 的 token join**
320
+ * (那一面的段视图同样是从 manifest 段列投影的,段列缺席时它也缺席)⇒ 别拿块 id 去 join 段面。
321
+ * ⚠️ 两张词表**没有命名空间隔离的保证**:v1 块 id 不带前缀(`role.base`),而 operator 声明的段 id 是自由串
322
+ * (core 的 `EpochDeclaredSections.id`)⇒ 理论上能撞出同名。所以「join 不上」不是唯一的失败形,
323
+ * **按面各自分组**才是安全读法,别靠 id 形状猜自己在哪一面。
324
+ * `chars` 同律:不是 token 数。 */
325
+ export interface PromptManifestBlockView {
326
+ id: string;
327
+ chars: number;
328
+ }
329
+ /** CC-91(0.76.0):`prompt_assembled.tools[]` 一行,**只留 wireName**(这一腿挂载了哪些工具;MCP 类按 server 分组渲染靠它)。
330
+ * 🔴 上游那一行的其余位(`aliases` / `contractId` / `implementationRevision` / `cardId` / `shapeDigest` / `wireSchemaDigest`)
331
+ * 是**内容寻址对账锚**、不是尺寸面,本层刻意不过境(要它们另立票,别顺手整只透传)。⚠️ 这一面**没有任何尺寸位** ——
332
+ * 「工具 schema 占了多少」在这条 wire 上读不出来,缺席不是零。 */
333
+ export interface PromptManifestToolView {
334
+ wireName: string;
335
+ }
279
336
  /**
280
337
  * CS-7 §2.7 — turn_end usage → CC `ModelUsage` (pinned name mapping;
281
338
  * costMicroUsd/1e6 → costUSD). Surfaced separately because the slice has no
@@ -1,9 +1,10 @@
1
- import { stamp, } from '../types.js';
1
+ import { stamp, snapshotSegmentIdentity, } from '../types.js';
2
2
  import { turnUsageToModelUsage } from './turnUsageToModelUsage.js';
3
3
  // 0.60.0(engine ≥7.64.0 / sdk 8.4.0):`tool_end.gate` 的三端共用窄读器(判定归包,端不许自拼)。
4
4
  import { gateOutcomeOf } from '../../gateOutcome.js';
5
5
  // CC-08(0.71.0):名册与名册增量的窄读器归 toolRoster.ts(0.63.0 起在),本层只接线。
6
6
  import { projectToolRoster, projectToolRosterDelta } from '../../toolRoster.js';
7
+ import { readMcpLiveness } from '../../mcpLiveness.js';
7
8
  import { SEGMENT_END_IDENTITY_KEYS } from '../types.js';
8
9
  // 0.65.0(core 7.12.0 CHANGELOG「Removed (BREAKING)」`WiringManifest.autoMode.breaker`):
9
10
  // 熔断族整只退役 ⇒ 本臂此前对 `autoMode.breaker` 的窄读(`classifierBreakerOf`)与它的型位一并
@@ -46,6 +47,8 @@ export const INTERNAL_SDK_ARM_TYPES = new Set([
46
47
  'context_usage',
47
48
  // CC-89(0.75.0):压缩的非 compacted 结局 → 中性内部臂(print 车道同律:吞或转,裸透传 = 越界帧)。
48
49
  'compaction_outcome',
50
+ // CC-91(0.76.0):这一次 prepare 的提示词装配清单 → 中性内部臂(print 车道同律:吞或转,裸透传 = 越界帧)。
51
+ 'prompt_assembled',
49
52
  'workflow_complete',
50
53
  'tool_end_result',
51
54
  'retry_status',
@@ -831,13 +834,14 @@ export function eventToSdkMessage(ev, ctx) {
831
834
  // `meta`:流首帧的 taskId/sessionId 一等信号 —— 本切片无可渲染内容(runStream 只按 `eventSeq` 去重)。
832
835
  // ⚠️ 活性普查(2026-09-19)更正:传输层**不**替宿主消费它;需要 sessionId / taskId 的端(desktop / web-client)各自直读本帧。
833
836
  case 'meta':
834
- // `file_link` / `prompt_assembled` / `model_usage` / `context_usage` / `config_assembled`:
837
+ // `file_link` / `model_usage` / `config_assembled`:
835
838
  // 引擎的可观测/审计面帧,本切片(CC transcript 投影)无对位渲染物;
836
839
  // 消费它们的是 fleet/审计面而不是会话转录。
837
840
  // ⚠️ `message_committed` **已在 0.65.0 搬出本档**(B-072 ④):它不是「没有对位物」——
838
841
  // 它是 `compacted.preserved_segment` 的唯一解析腿。新臂见本文件 `case 'message_committed'`。
842
+ // ⚠️ `context_usage` 自 0.71.0 CC-08 ③、`compaction_outcome` 自 0.75.0 CC-89、`prompt_assembled` 自
843
+ // 0.76.0 CC-91 起各自**进切片**为内部臂 → chrome 臂,不再在本组(见各自的 case)。
839
844
  case 'file_link':
840
- case 'prompt_assembled':
841
845
  case 'model_usage':
842
846
  case 'config_assembled':
843
847
  // `needs_review`:plan-review park 的**事件**形;壳消费的是 done{status:'needs_review'} 终帧
@@ -920,6 +924,11 @@ export function eventToSdkMessage(ev, ctx) {
920
924
  // 没有任何一端看得见。转录里没有对位物(`compact_boundary` 只对成功压缩)⇒ 走内部臂 → chrome 臂,不编消息。
921
925
  case 'compaction_outcome':
922
926
  return compactionOutcomeProjection(ev, ctx);
927
+ // CC-91(0.76.0):`prompt_assembled` 从 not_in_slice 组里拿出来 —— 这一次 prepare 的**提示词装配清单**
928
+ // (每段多大、挂了哪些工具)是 /context 类面板的唯一引擎侧真值来源;此前三端只能按本地镜像自己估。
929
+ // 转录里没有对位物(它是 ids/digests/counts 的观测面,不是一条消息)⇒ 走内部臂 → chrome 臂,不编消息。
930
+ case 'prompt_assembled':
931
+ return promptAssembledProjection(ev, ctx);
923
932
  case 'tool_progress':
924
933
  return toolProgressProjection(ev, ctx);
925
934
  default:
@@ -930,18 +939,35 @@ export function eventToSdkMessage(ev, ctx) {
930
939
  return dropped('unknown_arm', String(ev.type ?? 'unknown'));
931
940
  }
932
941
  }
933
- /** CC-08:三键身份(sdk `EventIdentity`)—— 在场非串 ⇒ 坏值不买路;在场(含空串)⇒ 原样透传;`eventId` 非空才带。 */
942
+ /**
943
+ * 身份四键(`eventId` + 三个子流键)的**一次性快照**:每键**只读一次**,读取抛错 ⇒ 整份不可信。
944
+ *
945
+ * 🔴 为什么是快照而不是就地多读(异源对抗复审第三轮 [medium],实测可复现):wire 是 JSON,但**进程内**的帧
946
+ * (宿主自建管线 / 重放存量转录 / 测试替身)可以带 accessor。同一个键读两次拿到两个值时,「形门读一次、
947
+ * 抄写再读一次、lane 判定又读一次」会让同一帧在不同读点上归属**不同 lane** —— 一条子代帧能被洗成主会话的
948
+ * 组成信息。读一次、全程用同一份,是这条歧义唯一的关门方式;抛错的 getter 同样在这里被关住(此前会从投影口逸出)。
949
+ * ⚠️ 仍按 `ev[k]` 读(**走原型链**:进程内对象的身份键常长在原型上),只是每键只读一次。
950
+ */
951
+ function identitySnapshot(ev) {
952
+ // 0.76.1:单源 —— 与 adapt 侧各臂共用同一只快照助手(见 adapter/types.ts 的 snapshotSegmentIdentity 头注)。
953
+ return snapshotSegmentIdentity(ev);
954
+ }
955
+ /** CC-08:三键身份(sdk `EventIdentity`)—— 在场非串 ⇒ 坏值不买路;在场(含空串)⇒ 原样透传;`eventId` 非空才带。
956
+ * 🔴 判据与抄写都取 {@link identitySnapshot} 的**同一份**读数(每键只读一次)。 */
934
957
  function identityOrMalformed(ev, arm) {
958
+ const snap = identitySnapshot(ev);
959
+ if (snap === null)
960
+ return { identity: {}, bad: dropped('malformed', arm) };
935
961
  for (const k of SEGMENT_END_IDENTITY_KEYS) {
936
- if (ev[k] !== undefined && typeof ev[k] !== 'string')
962
+ if (snap[k] !== undefined && typeof snap[k] !== 'string')
937
963
  return { identity: {}, bad: dropped('malformed', arm) };
938
964
  }
939
965
  const identity = {};
940
966
  for (const k of ['eventId', ...SEGMENT_END_IDENTITY_KEYS]) {
941
967
  // 🔴 三键**空串也透**:子流律是「键在场 = 子代帧」(`isSubFlowSegmentEnd` / `text_end` 投影同律),按 length>0 才抄会把
942
968
  // `sourceTaskId: ''` 的子代帧洗成主流(异源对抗复审第 1 轮 finding③);`eventId` 是幂等键,仍要非空。
943
- if (typeof ev[k] === 'string' && (k === 'eventId' ? ev[k].length > 0 : true))
944
- identity[k] = ev[k];
969
+ if (typeof snap[k] === 'string' && (k === 'eventId' ? snap[k].length > 0 : true))
970
+ identity[k] = snap[k];
945
971
  }
946
972
  return { identity, bad: null };
947
973
  }
@@ -1087,18 +1113,14 @@ function segmentEndProjection(ev, ctx, arm) {
1087
1113
  // 只带 `sourceTaskId`(无 `parentToolCallId`)是合法的子代帧形。修前只透 `parentToolCallId`,臂只按它断闸 ⇒ 那一形的
1088
1114
  // 子代段末帧被当 leader 投上来,端据它换 leader 的思考/文本行 = 子代推理覆盖 leader 推理面。⇒ 三键**原样穿上内部臂**,
1089
1115
  // 任一键在场却非串 ⇒ 整帧 malformed(与 `parentToolCallId` 同一条 B3-DIRTY 律:坏值不许买路)。
1090
- for (const k of SEGMENT_END_IDENTITY_KEYS) {
1091
- if (ev[k] !== undefined && typeof ev[k] !== 'string')
1092
- return dropped('malformed', arm);
1093
- }
1116
+ // 0.76.1(外部验收方对 0.76.0 的 F2):此前在同一个表达式里把每只身份键读了两次 ⇒ 「每键恰读一次」在这两条臂上不成立
1117
+ // (方向 fail-closed:值随读变的合法子代帧被判 malformed)。改走与其余七条内部臂同一只 identityOrMalformed(一次性快照:
1118
+ // 在场非串 ⇒ malformed;在场含空串 ⇒ 原样;`eventId` 非空才带)。
1119
+ const { identity, bad } = identityOrMalformed(ev, arm);
1120
+ if (bad !== null)
1121
+ return bad;
1094
1122
  if (content.length === 0)
1095
1123
  return nothing('empty_payload');
1096
- const identity = {
1097
- ...(typeof ev.eventId === 'string' ? { eventId: ev.eventId } : {}),
1098
- ...(typeof ev.parentToolCallId === 'string' ? { parentToolCallId: ev.parentToolCallId } : {}),
1099
- ...(typeof ev.sourceTaskId === 'string' ? { sourceTaskId: ev.sourceTaskId } : {}),
1100
- ...(typeof ev.bgAgentId === 'string' ? { bgAgentId: ev.bgAgentId } : {}),
1101
- };
1102
1124
  // 两处字面量铸点(不是一处参数化):内部臂词表门按 `armBody({ type: '<字面量>'` 抽铸点,参数化会让两臂在账上「多登记」。
1103
1125
  return arm === 'text_end'
1104
1126
  ? projected(stamp(ctx, armBody({ type: 'text_end', content, ...identity })))
@@ -1127,6 +1149,9 @@ export function projectMcpSection(raw) {
1127
1149
  // S6-B(0.60.0):两位与既有五位同律 —— 逐键挑、开集读、缺席不铸。
1128
1150
  ...(typeof r.delivered === 'string' && r.delivered.length > 0 ? { delivered: r.delivered } : {}),
1129
1151
  ...(typeof r.httpStatus === 'number' && Number.isFinite(r.httpStatus) ? { httpStatus: r.httpStatus } : {}),
1152
+ // CC-36(0.76.1):第六位活性格 —— 判据不在这里写第二遍,全在 readMcpLiveness 那一只铸点里
1153
+ // (三向读数:缺席不铸 / 在场读不出立 never-false 位 / 读得出铸那次观察)。
1154
+ ...((cell) => cell.kind === 'observation' ? { liveness: cell.liveness } : cell.kind === 'unreadable' ? { livenessUnreadable: true } : {})(readMcpLiveness(r)),
1130
1155
  });
1131
1156
  }
1132
1157
  // 🔴 见顶注:`raw.length === 0` 是**正面事实**(照铸 `[]`);非空却零行幸存 = 读不出来 ⇒ 段缺席。
@@ -1357,6 +1382,89 @@ function compactionOutcomeProjection(frame, ctx) {
1357
1382
  ...identity,
1358
1383
  }))));
1359
1384
  }
1385
+ /**
1386
+ * CC-91(0.76.0):`prompt_assembled`(一次 prepare 的提示词装配清单)→ 中性内部臂。
1387
+ *
1388
+ * 逐键诚实:`totalChars` 有限非负才在场(缺席 ≠ 0);`blocks` / `sections` / `tools` 三面**逐行**挑
1389
+ * (`id` / `wireName` 非空串 ∧ `chars` 有限非负),坏行逐条丢、全丢光或非数组 ⇒ 该键缺席。
1390
+ * 🔴 于是键缺席是**三支合流**(上游整键缺席 / 空数组 / 逐行滤完只剩空),本层分不出是哪一支 ⇒ 对外只能说
1391
+ * 「本事件没有可读的这一面」,**不许**把它写成一句关于引擎的诊断(如「这条腿没装配出段 IR」)。不铸 `[]`:
1392
+ * 空数组会被读成「装配出来是空的」,那是另一件事。
1393
+ * 🔴 一条组成面的键都读不出 ⇒ `malformed`:core 把 `blocks` / `totalChars` 声明为必填,两件全丢的帧是坏 producer,
1394
+ * 投一份空清单会让消费端把「读不出」渲成「提示词是空的」。
1395
+ * 🔴 帧上的 `constitution`(谁拥有宪法层)**刻意不过境**:本包与三端今天没有任何消费场景,而每一个上了公面的键都是
1396
+ * 一条要长期兑现的契约 —— additive 键晚加不破坏、早加却删不掉。哪天有端点名要它,再按表态制加(本层零改动成本)。
1397
+ * 🔴 **零 `tokens` 铸点**(见 {@link PromptManifestSectionView} 头注):真字节里没有这一位。
1398
+ */
1399
+ function promptAssembledProjection(frame, ctx) {
1400
+ // 🔴 不浅拷(与 compaction_outcome 同律):展开会擦掉原型链上的身份键(进程内对象把子代帧洗成主会话)、
1401
+ // 还会执行无关的可枚举 getter;就在同一份记录上逐键各读一次。
1402
+ const ev = frame;
1403
+ const { identity, bad } = identityOrMalformed(ev, 'prompt_assembled');
1404
+ if (bad !== null)
1405
+ return bad;
1406
+ const str = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
1407
+ const size = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : undefined);
1408
+ const totalChars = size(ev.totalChars);
1409
+ const sections = (() => {
1410
+ if (!Array.isArray(ev.sections))
1411
+ return undefined;
1412
+ const rows = [];
1413
+ for (const r of ev.sections) {
1414
+ if (typeof r !== 'object' || r === null || Array.isArray(r))
1415
+ continue;
1416
+ const o = r;
1417
+ const id = str(o.id);
1418
+ const chars = size(o.chars);
1419
+ if (id === undefined || chars === undefined)
1420
+ continue;
1421
+ const slot = str(o.slot);
1422
+ rows.push({ id, chars, ...(slot !== undefined ? { slot } : {}) });
1423
+ }
1424
+ return rows.length > 0 ? rows : undefined;
1425
+ })();
1426
+ const blocks = (() => {
1427
+ if (!Array.isArray(ev.blocks))
1428
+ return undefined;
1429
+ const rows = [];
1430
+ for (const r of ev.blocks) {
1431
+ if (typeof r !== 'object' || r === null || Array.isArray(r))
1432
+ continue;
1433
+ const o = r;
1434
+ const id = str(o.id);
1435
+ const chars = size(o.chars);
1436
+ if (id === undefined || chars === undefined)
1437
+ continue;
1438
+ rows.push({ id, chars });
1439
+ }
1440
+ return rows.length > 0 ? rows : undefined;
1441
+ })();
1442
+ const tools = (() => {
1443
+ if (!Array.isArray(ev.tools))
1444
+ return undefined;
1445
+ const rows = [];
1446
+ for (const r of ev.tools) {
1447
+ if (typeof r !== 'object' || r === null || Array.isArray(r))
1448
+ continue;
1449
+ const wireName = str(r.wireName);
1450
+ if (wireName === undefined)
1451
+ continue;
1452
+ rows.push({ wireName });
1453
+ }
1454
+ return rows.length > 0 ? rows : undefined;
1455
+ })();
1456
+ if (totalChars === undefined && sections === undefined && blocks === undefined && tools === undefined) {
1457
+ return dropped('malformed', 'prompt_assembled');
1458
+ }
1459
+ return projected(internalArmExit(stamp(ctx, armBody({
1460
+ type: 'prompt_assembled',
1461
+ ...(totalChars !== undefined ? { totalChars } : {}),
1462
+ ...(blocks !== undefined ? { blocks } : {}),
1463
+ ...(sections !== undefined ? { sections } : {}),
1464
+ ...(tools !== undefined ? { tools } : {}),
1465
+ ...identity,
1466
+ }))));
1467
+ }
1360
1468
  function engineNoticeProjection(ev, ctx) {
1361
1469
  const code = typeof ev.code === 'string' && ev.code.length > 0 ? ev.code : undefined;
1362
1470
  if (code === undefined)
@@ -12,8 +12,17 @@ import { coerceOutput, publishSubagentContentEvent } from '../subagentContentSto
12
12
  import { ACTIVE_RUN_BUSY_ERROR_CODE, OUTPUT_INVALID, isLimitsExceededCode } from '../engineErrorCodes.js';
13
13
  // 0.60.0(engine ≥7.64.0 / sdk 8.4.0):终局读数的单一读器(纯叶子:零 Node、零宿主端口)。
14
14
  import { isReviewPark, readRunTerminal, runTerminalCode } from '../runTerminal.js';
15
- /** 本文件发的 chrome 事件全在 leader lane(子代内容在上面就被 divert 走了)。 */
16
- const MAIN = { lane: 'main' };
15
+ /**
16
+ * 本文件发的 chrome 事件全在 leader lane(子代内容在上面就被 divert 走了)。
17
+ *
18
+ * 🔴 **出厂口而不是常量**(0.76.1,与 A 层那半场同一条修法):车道证明在下游会被就地改写,
19
+ * 一只模块级常量对象会让本文件三个发点(用量臂 / 停靠臂 / 对账臂)与**别的流**的同名事件共享
20
+ * 同一只证明,一次改写打穿整个进程。每次新建一只。
21
+ * 🔴 **刻意是本地的一行、不去 import A 层那只同名出厂口**:本文件是适配内核的闭包起点,闭包件数
22
+ * 是只许降的棘轮 —— 为一行 `{lane:'main'}` 把 A 层(或 seam)拖进内核闭包是拿棘轮换重复,
23
+ * 这一行字面量本来就是 `LaneProof` 主臂自己的形,不构成第二份判据。
24
+ */
25
+ const mainLane = () => ({ lane: 'main' });
17
26
  /**
18
27
  * **fire-and-forget 的 chrome 发射口**(0.66.0 / 异源对抗复审 [high] 逼出的同形族扫)。
19
28
  *
@@ -639,7 +648,7 @@ async function* runStreamInner(events, ctx, handle = {}) {
639
648
  // (与 turn_end.usage 本身「整体 optional,缺席≠空对象」同一语义,不是「键在值 undefined」)。
640
649
  emitChromeFireAndForget(ctx, {
641
650
  kind: 'last_turn_usage',
642
- laneProof: MAIN,
651
+ laneProof: mainLane(),
643
652
  usage,
644
653
  // 🔴 0.68.0:`engineUsage` 的条件 spread 退役 —— `turn_end.usage` 恒在场
645
654
  // (缺席已在违约闸里整帧收口),这里再判一次就是给一个不可能的形留座位。
@@ -916,7 +925,7 @@ async function* runStreamInner(events, ctx, handle = {}) {
916
925
  // 同步 park 腿的 plan-review 卡不弹。因由形那一半覆盖 done 帧的引擎结果,两半各管一条腿。
917
926
  if (ev.type === 'done' && isReviewPark(doneTerminal) && ctx.emitChrome) {
918
927
  try {
919
- await ctx.emitChrome({ kind: 'plan_review_park', laneProof: MAIN, result: ev.result });
928
+ await ctx.emitChrome({ kind: 'plan_review_park', laneProof: mainLane(), result: ev.result });
920
929
  }
921
930
  catch { /* fail-soft — the terminal frame below still renders */ }
922
931
  }
@@ -937,7 +946,7 @@ async function* runStreamInner(events, ctx, handle = {}) {
937
946
  // `terminalToSdkResult(..., observed)` 是**同一份**读数。
938
947
  const costFacts = readRunCostFacts(doneStats, { usageMissingObserved });
939
948
  if (costFacts !== undefined) {
940
- emitChromeFireAndForget(ctx, { kind: 'run_cost_reconciled', laneProof: MAIN, ...costFacts.reconcile });
949
+ emitChromeFireAndForget(ctx, { kind: 'run_cost_reconciled', laneProof: mainLane(), ...costFacts.reconcile });
941
950
  }
942
951
  }
943
952
  // L-228 / 🔴 0.67.2 件 I-2b 订正:分表**不再挂到 `ctx` 上**,而是与观测位一起随第三参按值交给
@@ -129,3 +129,15 @@ export declare const SEGMENT_END_IDENTITY_KEYS: readonly ["parentToolCallId", "s
129
129
  * 参数收任意对象(帧型不限;typeshape 纪律:边界层不让 `unknown` 出境)。
130
130
  */
131
131
  export declare function isSubFlowSegmentEnd(m: object): boolean;
132
+ /**
133
+ * 身份四键(`eventId` + 三只子流键)的**一次性快照**:每键恰读一次,读取抛错 ⇒ `null`(= 整帧不可信,调用方拒帧)。
134
+ *
135
+ * 🔴 投影口与 adapt 侧的**每一条**读身份的臂都必须走这一只(0.76.1;外部验收方对 0.76.0 的两条发现):
136
+ * 0.76.0 只把投影口换成了一次性快照,adapt 侧的 compaction 臂仍先判子流再取 `parentToolCallId`(读两次),
137
+ * text_end / reasoning_end 的投影更是在同一个表达式里读两次 ⇒ 值随读变的帧让「投影口 → 内部臂」与
138
+ * 「直喂 adapt()」两条路分叉 —— 正是一次性快照要消灭的那一形,却只修了一侧。
139
+ * 读了两次的两个值不能相信任何一个;读一次的那一份,两条路都拿同一份。
140
+ */
141
+ export declare function snapshotSegmentIdentity(m: object): Record<string, unknown> | null;
142
+ /** 与 {@link isSubFlowSegmentEnd} 同一条判据,但吃的是快照(不再回头读帧)。 */
143
+ export declare function snapshotIsSubFlow(snap: Record<string, unknown>): boolean;
@@ -39,3 +39,33 @@ export function isSubFlowSegmentEnd(m) {
39
39
  return true;
40
40
  return false;
41
41
  }
42
+ /**
43
+ * 身份四键(`eventId` + 三只子流键)的**一次性快照**:每键恰读一次,读取抛错 ⇒ `null`(= 整帧不可信,调用方拒帧)。
44
+ *
45
+ * 🔴 投影口与 adapt 侧的**每一条**读身份的臂都必须走这一只(0.76.1;外部验收方对 0.76.0 的两条发现):
46
+ * 0.76.0 只把投影口换成了一次性快照,adapt 侧的 compaction 臂仍先判子流再取 `parentToolCallId`(读两次),
47
+ * text_end / reasoning_end 的投影更是在同一个表达式里读两次 ⇒ 值随读变的帧让「投影口 → 内部臂」与
48
+ * 「直喂 adapt()」两条路分叉 —— 正是一次性快照要消灭的那一形,却只修了一侧。
49
+ * 读了两次的两个值不能相信任何一个;读一次的那一份,两条路都拿同一份。
50
+ */
51
+ export function snapshotSegmentIdentity(m) {
52
+ const rec = m;
53
+ const snap = {};
54
+ try {
55
+ for (const k of ['eventId', ...SEGMENT_END_IDENTITY_KEYS])
56
+ snap[k] = rec[k];
57
+ }
58
+ catch {
59
+ // §C1 带理由的吞:读身份键本身抛 = 这一帧连自己是谁都说不出 ⇒ 整帧拒,由调用方决定「丢」的姿势
60
+ // (投影口判 malformed 留痕、adapt 侧零 chrome);再向上抛会从两个入口逸出、打断整条 adapt 流。
61
+ return null;
62
+ }
63
+ return snap;
64
+ }
65
+ /** 与 {@link isSubFlowSegmentEnd} 同一条判据,但吃的是快照(不再回头读帧)。 */
66
+ export function snapshotIsSubFlow(snap) {
67
+ for (const k of SEGMENT_END_IDENTITY_KEYS)
68
+ if (snap[k] !== undefined)
69
+ return true;
70
+ return false;
71
+ }
@@ -18,10 +18,23 @@ export type TaskAgentWireSkill = {
18
18
  };
19
19
  /** CC 形自定义 agent 的 `memory:` 三词(cli `loadAgentsDir` 的 zod 枚举同源)。 */
20
20
  export declare const AGENT_MEMORY_WORDS: ReadonlySet<string>;
21
- /** `agents[].memory` 上 wire 的形(server `spec-fields.ts` `memoryOk` 闭合白名单的子集:只用 `scopes` + `writeScope`)。 */
21
+ /**
22
+ * `agents[].memory` 上 wire 的形(上游闭合白名单 `{ scopes?, writeScope?, enabled?, scopeContract? }` 的子集:本包只用前两键)。
23
+ *
24
+ * 🔴 **CC-96 放宽**:`writeScope` 此前写死成 `string`(恒在、且不可为 `null`),而 wire 的取值域是 `string | null`
25
+ * (上游直讯确认该域不变)。写死的后果是**读得出却铸不出** —— {@link import('./memorySpecWire.js').readAgentMemorySpec}
26
+ * 能把显式 `null` 读成「这一次 run 对记忆面只读」(无 remember 工具、不做 consolidation 写,recall 仍可),
27
+ * 而端**没有任何办法经本包声明**这一形:只剩「写到某一层」一条路。半条腿的读面比没有更坏,因为它让
28
+ * 「本端支持只读记忆」看起来是真的。
29
+ * · `writeScope: null` = 显式声明只读;
30
+ * · `writeScope` 键缺席 = 未指定(缺省归引擎);
31
+ * · `writeScope: '<层名>'` = 写到那一层。
32
+ * 🔴 放宽的是**类型**,不是本包投影器的纪律:{@link projectAgentsForWire} 仍**恒显式钉** `writeScope`
33
+ * (见下方投影处的 🔴 注),漏钉会让新条目静默写进全局层 —— 那条约束一个字都没松。
34
+ */
22
35
  export type TaskAgentWireMemory = {
23
36
  scopes: string[];
24
- writeScope: string;
37
+ writeScope?: string | null;
25
38
  };
26
39
  /** 发往 server 的单个 agent 定义(白名单内字段的壳侧子集——observer 面壳无语义,恒不发;`memory` 自 0.72.3 按定义发)。 */
27
40
  export type TaskAgentWireItem = {
@@ -13,7 +13,7 @@
13
13
  * ③ 「要不要对账」的判据归包:{@link livePendingNeedsReconcile} —— 只有 present 且 true 才 false;其余一律 true(少查一次 = 一只等人的 ask 迟迟不出现)。
14
14
  */
15
15
  import { engineWireTarget } from './engineWireTarget.js';
16
- import { snapshotCapsGeneration, capsGenerationStillCurrent } from './engineCapsGenerationGuard.js';
16
+ import { createEngineCapReader } from './engineCapReader.js';
17
17
  /** caps 回体 → 本格读数;畸形一律 `undefined`(= 这一格不写 ⇒ 读口答 `unobserved`)。 */
18
18
  export function projectApprovalsStreamLiveCapability(caps) {
19
19
  if (caps === null || typeof caps !== 'object')
@@ -27,41 +27,23 @@ export function projectApprovalsStreamLiveCapability(caps) {
27
27
  return undefined;
28
28
  return { kind: 'present', live: v };
29
29
  }
30
- const readingByBase = new Map();
30
+ /**
31
+ * 本格的 per-baseUrl 读账 + tee / 读口 / 失效口 / 测试钩四口 —— 各只能力位读器**共用同一份实现**
32
+ * ({@link createEngineCapReader});本文件只留这一格真正不同的部分:投影函数 {@link projectApprovalsStreamLiveCapability}、doctor 措辞表,以及本位自己的判据助手(如有)。
33
+ * 🔴 Map 在工厂闭包里,**一只读器一张**:收到读不懂的回体只删自己这一格,绝不连坐别的能力面。
34
+ */
35
+ const reader = createEngineCapReader({
36
+ name: 'approvalsStreamLive',
37
+ project: projectApprovalsStreamLiveCapability,
38
+ makeUnobserved: () => ({ kind: 'unobserved' }),
39
+ });
31
40
  /** 宿主 caps probe 的读面 tee 落点(与四只兄弟并列)。绝不 throw;畸形 ⇒ 删格;`opts.generation` 关掉旧探测覆盖新读数的竞态。 */
32
41
  export function noteEngineCapsForApprovalsStreamLive(baseUrl, caps, opts) {
33
- if (typeof baseUrl !== 'string' || baseUrl === '')
34
- return;
35
- // 🔴 代际只在入口**快照一次**(异源对抗复审 R3):opts 是调用方的对象,回体上的 getter 能在投影中途改它;首末两次核对都用这份快照,
36
- // 读 opts / 读代际抛出 ⇒ 不动格(与陈旧代际同一处置)。
37
- const gen = snapshotCapsGeneration(opts);
38
- if (gen === null)
39
- return; // 调用方 opts 的 getter 抛出 = 供给坏了 ⇒ 不动格
40
- if (!capsGenerationStillCurrent(baseUrl, gen))
41
- return;
42
- let reading;
43
- try {
44
- reading = projectApprovalsStreamLiveCapability(caps);
45
- }
46
- catch {
47
- // 🔴 0.74.2 同形存量(CC-69 复审 R1 族扫):抛 getter / Proxy 的回体 = 畸形,与「投影答 undefined」同一处置 —— 删格,不许留着上一台的读数。
48
- reading = undefined;
49
- }
50
- // 🔴 投影之后**再核一次代际**(异源对抗复审 R2):投影会执行回体上不可信的 getter,它可以在中途让这一格换代
51
- // (invalidate + 新代际的 tee 已写入);此时无论投影返回还是抛出,都不许动新代际的格。
52
- if (!capsGenerationStillCurrent(baseUrl, gen))
53
- return;
54
- if (reading === undefined) {
55
- readingByBase.delete(baseUrl);
56
- return;
57
- }
58
- readingByBase.set(baseUrl, reading);
42
+ reader.note(baseUrl, caps, opts);
59
43
  }
60
44
  /** 本进程观测到的读数;这一格空缺 ⇒ `{kind:'unobserved'}`。 */
61
45
  export function observedApprovalsStreamLive(baseUrl = engineWireTarget()?.baseUrl) {
62
- if (typeof baseUrl !== 'string' || baseUrl === '')
63
- return { kind: 'unobserved' };
64
- return readingByBase.get(baseUrl) ?? { kind: 'unobserved' };
46
+ return reader.observed(baseUrl);
65
47
  }
66
48
  /**
67
49
  * feed 消费方「要不要靠对账节拍拉悬挂 ask」的判据单源:只有引擎明说会推(present ∧ true)才 `false`;
@@ -85,11 +67,9 @@ export function approvalsStreamLiveDoctorDetail(reading) {
85
67
  }
86
68
  /** 换代失效口(引擎温切成功后调):清成未观测。空串 ⇒ no-op;绝不 throw。 */
87
69
  export function forgetApprovalsStreamLiveReading(baseUrl) {
88
- if (typeof baseUrl !== 'string' || baseUrl === '')
89
- return;
90
- readingByBase.delete(baseUrl);
70
+ reader.forget(baseUrl);
91
71
  }
92
72
  /** 测试钩子。 */
93
73
  export function __resetApprovalsStreamLiveReadingsForTests() {
94
- readingByBase.clear();
74
+ reader.__resetForTests();
95
75
  }