@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,4 +1,4 @@
1
- import { isSubFlowSegmentEnd } from '../adapter/types.js';
1
+ import { isSubFlowSegmentEnd, SEGMENT_END_IDENTITY_KEYS, snapshotSegmentIdentity, snapshotIsSubFlow } from '../adapter/types.js';
2
2
  import { classifyPeerNotification, renderPeerFrameTranscriptText } from '../peerFrames.js';
3
3
  import { dropQueuedNotificationsForRun, isEngineWorkflowNotified, isWorkflowCompletionCardEnqueued, markEngineWorkflowNotified, noteOwnWorkflowRun, normalizeTaskNotification, noteWorkflowCompletionCardEnqueued, renderTaskNotificationXml, taskNotificationDedupKey } from '../notifications.js';
4
4
  import { mapBrainStatusToRetry } from '../retryStatus.js';
@@ -13,7 +13,7 @@ import { isTerminalStatus } from '../runTerminal.js';
13
13
  import { resolveEnginePanelTaskId } from '../engineAgentPanelStore.js';
14
14
  // 0.75.1 CC-95(b):子代类型的诚实来源台账(fleet 行帧折进 name 之前留的那一份;包内叶)。
15
15
  import { fleetRowAgentType } from '../fleet/fleetRowAgentType.js';
16
- import { chrome, MAIN, transcript, messageIdentityOf } from './ids.js';
16
+ import { chrome, mainLane, transcript, messageIdentityOf } from './ids.js';
17
17
  import { CANCEL_MESSAGE, decisionOf, flattenWireOutput, REJECT_MESSAGE, sanitizeToolUseBlock, shortTaskLabel, TASK_TOOL_NAMES, WORKFLOW_TOOL_NAMES, } from './wireShapes.js';
18
18
  // ══ M2/M1 —— assistant(tool_use 半场 + durable 文本/思考半场)══════════════════════════════════
19
19
  const assistantArm = function* (m, { ctx, idOf, text, cards, inst }) {
@@ -53,7 +53,7 @@ const assistantArm = function* (m, { ctx, idOf, text, cards, inst }) {
53
53
  if (TASK_TOOL_NAMES.has(toolName)) {
54
54
  yield chrome({
55
55
  kind: 'task_ledger_sync',
56
- laneProof: MAIN,
56
+ laneProof: mainLane(),
57
57
  source: 'tool_use',
58
58
  toolName,
59
59
  toolUseId,
@@ -190,7 +190,7 @@ const systemArm = function* (m, { ctx, idOf }) {
190
190
  continue;
191
191
  yield chrome({
192
192
  kind: 'attachment',
193
- laneProof: MAIN,
193
+ laneProof: mainLane(),
194
194
  id: idOf(m, `attached-file-${i++}`),
195
195
  attachment: {
196
196
  type: 'compact_file_reference',
@@ -207,7 +207,7 @@ const diagnosticsArm = function* (m, { idOf }) {
207
207
  const att = projectDiagnosticsFrame(m);
208
208
  if (att === null)
209
209
  return;
210
- yield chrome({ kind: 'attachment', laneProof: MAIN, id: idOf(m), attachment: att });
210
+ yield chrome({ kind: 'attachment', laneProof: mainLane(), id: idOf(m), attachment: att });
211
211
  };
212
212
  const steeringInjectedArm = function* (m, { idOf }) {
213
213
  if (typeof m.parentToolCallId === 'string')
@@ -220,7 +220,7 @@ const steeringInjectedArm = function* (m, { idOf }) {
220
220
  for (const att of steeringInjectedToAttachments(source, preview)) {
221
221
  yield chrome({
222
222
  kind: 'attachment',
223
- laneProof: MAIN,
223
+ laneProof: mainLane(),
224
224
  id: idOf(m, `steering-${i++}`),
225
225
  attachment: att,
226
226
  });
@@ -234,7 +234,7 @@ const workspaceChangedArm = function* (m, { idOf }) {
234
234
  return;
235
235
  yield chrome({
236
236
  kind: 'attachment',
237
- laneProof: MAIN,
237
+ laneProof: mainLane(),
238
238
  id: idOf(m),
239
239
  attachment: { type: 'workspace_changed', cwd },
240
240
  });
@@ -267,7 +267,7 @@ const humanInputArm = function* (m) {
267
267
  };
268
268
  yield chrome({
269
269
  kind: 'human_input',
270
- laneProof: MAIN,
270
+ laneProof: mainLane(),
271
271
  carrier,
272
272
  ...(str(m.inputId) !== undefined ? { inputId: str(m.inputId) } : {}),
273
273
  ...(typeof m.sessionSeq === 'number' && Number.isFinite(m.sessionSeq) ? { sessionSeq: m.sessionSeq } : {}),
@@ -301,7 +301,7 @@ const messageCommittedArm = function* (m) {
301
301
  return;
302
302
  yield chrome({
303
303
  kind: 'message_committed',
304
- laneProof: MAIN,
304
+ laneProof: mainLane(),
305
305
  entryId,
306
306
  role,
307
307
  ...(typeof m.toolCallId === 'string' && m.toolCallId.length > 0 ? { toolCallId: m.toolCallId } : {}),
@@ -336,7 +336,7 @@ const engineNoticeArm = function* (m) {
336
336
  : {};
337
337
  yield chrome({
338
338
  kind: 'engine_notice',
339
- laneProof: MAIN,
339
+ laneProof: mainLane(),
340
340
  code,
341
341
  message: typeof m.message === 'string' ? m.message : '',
342
342
  detail,
@@ -392,7 +392,7 @@ const wiringManifestArm = function* (m) {
392
392
  return;
393
393
  yield chrome({
394
394
  kind: 'wiring_manifest',
395
- laneProof: MAIN,
395
+ laneProof: mainLane(),
396
396
  ...(hasGate ? { modelGate: modelGate } : {}),
397
397
  ...(hasAuto ? { autoMode: autoMode } : {}),
398
398
  ...(hasMcp ? { mcp: mcp } : {}),
@@ -428,7 +428,7 @@ const approvalFrameArm = (kind) => function* (m) {
428
428
  const frame = m.frame;
429
429
  if (typeof frame !== 'object' || frame === null)
430
430
  return;
431
- yield chrome({ kind, laneProof: MAIN, frame, schemaVersion });
431
+ yield chrome({ kind, laneProof: mainLane(), frame, schemaVersion });
432
432
  };
433
433
  /**
434
434
  * `text_end` 内部臂 → **权威段替换** + chrome `text_segment_end`(#323 / core #447;L-310,
@@ -485,7 +485,7 @@ const toolDisclosureArm = function* (m) {
485
485
  return; // 二道判:非投影口喂进来的帧
486
486
  yield chrome({
487
487
  kind: 'tool_disclosure',
488
- laneProof: MAIN,
488
+ laneProof: mainLane(),
489
489
  policy,
490
490
  ...(typeof m.thresholdPercent === 'number' ? { thresholdPercent: m.thresholdPercent } : {}),
491
491
  deferred,
@@ -508,7 +508,7 @@ const toolProgressArm = function* (m) {
508
508
  const output = typeof m.output === 'string' ? m.output : undefined;
509
509
  yield chrome({
510
510
  kind: 'tool_progress',
511
- laneProof: MAIN,
511
+ laneProof: mainLane(),
512
512
  toolCallId,
513
513
  toolName,
514
514
  ...(typeof m.elapsedTimeSeconds === 'number' ? { elapsedSeconds: m.elapsedTimeSeconds } : {}),
@@ -530,7 +530,7 @@ const toolRosterDeltaArm = function* (m) {
530
530
  return;
531
531
  yield chrome({
532
532
  kind: 'tool_roster_delta',
533
- laneProof: MAIN,
533
+ laneProof: mainLane(),
534
534
  delta: delta,
535
535
  ...(typeof m.eventId === 'string' && m.eventId.length > 0 ? { eventId: m.eventId } : {}),
536
536
  });
@@ -551,7 +551,7 @@ const contextUsageArm = function* (m) {
551
551
  return;
552
552
  yield chrome({
553
553
  kind: 'context_usage',
554
- laneProof: MAIN,
554
+ laneProof: mainLane(),
555
555
  ...(usedTokens !== undefined ? { usedTokens } : {}),
556
556
  ...(windowTokens !== undefined ? { windowTokens } : {}),
557
557
  ...(compactAtTokens !== undefined ? { compactAtTokens } : {}),
@@ -569,16 +569,21 @@ const compactionOutcomeArm = function* (m) {
569
569
  const outcome = str(m.outcome);
570
570
  if (outcome === undefined)
571
571
  return;
572
- let laneProof = MAIN;
573
- if (isSubFlowSegmentEnd(m)) {
574
- const parent = str(m.parentToolCallId);
572
+ // 🔴 身份四键一次性快照(0.76.1;外部验收方对 0.76.0 的 F1):修前先 isSubFlowSegmentEnd(m) 再 str(m.parentToolCallId),
573
+ // parentToolCallId 读了两次 ⇒ 值随读变的帧让直喂入口丢事件、经投影却出 subagent chrome —— 两入口分叉。
574
+ const idSnap = snapshotSegmentIdentity(m);
575
+ if (idSnap === null)
576
+ return; // 读身份就抛 ⇒ 整帧拒(与投影口判 malformed 同一处置,不发 chrome)
577
+ let laneProof = mainLane();
578
+ if (snapshotIsSubFlow(idSnap)) {
579
+ const parent = str(idSnap.parentToolCallId);
575
580
  if (parent === undefined)
576
581
  return;
577
582
  laneProof = { lane: 'subagent', parentToolCallId: parent };
578
583
  }
579
584
  const trigger = str(m.trigger);
580
585
  const reason = str(m.reason);
581
- const eventId = str(m.eventId);
586
+ const eventId = str(idSnap.eventId);
582
587
  yield chrome({
583
588
  kind: 'compaction_outcome',
584
589
  laneProof,
@@ -588,6 +593,94 @@ const compactionOutcomeArm = function* (m) {
588
593
  ...(eventId !== undefined ? { eventId } : {}),
589
594
  });
590
595
  };
596
+ /**
597
+ * CC-91(0.76.0):逐行**重建**白名单行(不是 `Array.isArray` 就整只透传)——
598
+ * 🔴 本臂与投影口是**两个入口**:宿主自建管线 / 重放存量转录可以绕过投影口直接把 wire 帧喂进 `adapt()`,
599
+ * 那条路上未校验的行会把本臂对外承诺的四条(零 `tokens`、零 digest 过境、`chars` 有限非负、全坏即缺席)
600
+ * 一起破掉,还可能把违反公开型的行(`null` / `chars: -1`)递到消费端手里
601
+ * (异源对抗复审 R1 [medium] 实测可复现:`sections:[{id:'x',chars:-1,tokens:123,contentHash:'…'}]` 原样进 chrome)。
602
+ * ⇒ 与投影口**同律同形**:好行留、坏行逐条丢、全丢光或非数组 ⇒ 键缺席(不铸 `[]`)。
603
+ * 两处实现由门逐字对拍(同一帧走投影口 vs 直喂 adapt,chrome 行必须一模一样),drift 即红。
604
+ */
605
+ const promptManifestRows = (v, pick) => {
606
+ if (!Array.isArray(v))
607
+ return undefined;
608
+ const rows = [];
609
+ for (const r of v) {
610
+ if (typeof r !== 'object' || r === null || Array.isArray(r))
611
+ continue;
612
+ const row = pick(r);
613
+ if (row !== undefined)
614
+ rows.push(row);
615
+ }
616
+ return rows.length > 0 ? rows : undefined;
617
+ };
618
+ /**
619
+ * CC-91(0.76.0):`prompt_assembled` 内部臂 → chrome `prompt_assembled`。每帧一条;转录面零行。
620
+ * 🔴 **包不去重**:一次任务的每一次 prepare(cascade / verify 的每一 rung、resume 腿)各自一条,server 逐条保序落账 ⇒
621
+ * 每条都交出去,「只要最后一次」是**宿主**的动作(与 `context_usage` 同律;在包内按最后一条折会把先前已计费 rung 的组成面抹掉)。
622
+ * ⚠️ 上游那条流的**重放去重**是另一件事,按 `eventSeq` 在 `adapter/runStream.ts` 做;本臂只说「同一条流上的多条 prepare 都算真事实」。
623
+ * 🔴 lane 如实:`parentToolCallId` 非空串 ⇒ subagent;只有 `sourceTaskId` / `bgAgentId`(或父键空串)的帧没有诚实的 lane 可挂 ⇒ 不发,
624
+ * 绝不落到主会话名下。一条组成面的键都读不出的帧不发(非投影口喂进来的坏帧兜底)。
625
+ * 🔴 **零 tokens 铸点**:本臂只搬 `chars`(上游真字节无 token 位),不在这一层编任何 token 数。
626
+ */
627
+ const promptAssembledArm = function* (m) {
628
+ const str = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
629
+ const size = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : undefined);
630
+ // ⚠️ 帧上的 `constitution` 刻意不过境(投影口头注写明理由:今天零消费场景,键即契约)。
631
+ const totalChars = size(m.totalChars);
632
+ const blocks = promptManifestRows(m.blocks, (o) => {
633
+ const id = str(o.id);
634
+ const chars = size(o.chars);
635
+ return id === undefined || chars === undefined ? undefined : { id, chars };
636
+ });
637
+ const sections = promptManifestRows(m.sections, (o) => {
638
+ const id = str(o.id);
639
+ const chars = size(o.chars);
640
+ if (id === undefined || chars === undefined)
641
+ return undefined;
642
+ // 🔴 语义槽在**两个入口上都只认一个键名** `slot`(上游的名字,本层不改名):投影口产物与 wire 原形同名,
643
+ // 所以两条路读的是同一个位。曾经的写法是「`kind` 优先、回退 `slot`」—— 那会让一条**同时**带
644
+ // `kind` 与 `slot`(或只带 `kind`)的帧在两条路上读出不同的槽,而这种帧是合法的(上游行是开集)。
645
+ const slot = str(o.slot);
646
+ return { id, chars, ...(slot !== undefined ? { slot } : {}) };
647
+ });
648
+ const tools = promptManifestRows(m.tools, (o) => {
649
+ const wireName = str(o.wireName);
650
+ return wireName === undefined ? undefined : { wireName };
651
+ });
652
+ if (totalChars === undefined && blocks === undefined && sections === undefined && tools === undefined)
653
+ return;
654
+ // 🔴 身份四键**一次性快照**,与投影口的 `identitySnapshot` / `identityOrMalformed` 同律(每键只读一次、抛错整帧拒):
655
+ // ① 三个子流键**逐个**过形门(在场即必须是串)—— 只守 `parentToolCallId` 的话,一条
656
+ // `{parentToolCallId:'tc-9', sourceTaskId: 7}` 会在投影口被整帧判坏、在直喂这条路上照发一条 subagent chrome;
657
+ // ② **不能就地多读**:进程内帧可以带 accessor,同一个键读两次拿到两个值时,「形门读一次、子流判定再读一次、
658
+ // 取 parent 又读一次」会让同一帧在两条路上归属不同 lane —— 子代的组成信息被洗成主会话的
659
+ // (第三轮复审 [medium] 实测可复现)。所以这里**不**用 `isSubFlowSegmentEnd(m)`(它会再读一遍),
660
+ // 子流判定直接看快照里「三键有没有在场」,语义与它逐字同(键在场 = 子代帧,空串也算在场)。
661
+ const idSnap = snapshotSegmentIdentity(m);
662
+ if (idSnap === null)
663
+ return; // 读身份就抛 ⇒ 整帧拒(理由见 snapshotSegmentIdentity 头注与本臂上方的说明)
664
+ if (SEGMENT_END_IDENTITY_KEYS.some((k) => idSnap[k] !== undefined && typeof idSnap[k] !== 'string'))
665
+ return;
666
+ let laneProof = mainLane();
667
+ if (SEGMENT_END_IDENTITY_KEYS.some((k) => idSnap[k] !== undefined)) {
668
+ const parent = str(idSnap.parentToolCallId);
669
+ if (parent === undefined)
670
+ return;
671
+ laneProof = { lane: 'subagent', parentToolCallId: parent };
672
+ }
673
+ const eventId = str(idSnap.eventId);
674
+ yield chrome({
675
+ kind: 'prompt_assembled',
676
+ laneProof,
677
+ ...(totalChars !== undefined ? { totalChars } : {}),
678
+ ...(blocks !== undefined ? { blocks } : {}),
679
+ ...(sections !== undefined ? { sections } : {}),
680
+ ...(tools !== undefined ? { tools } : {}),
681
+ ...(eventId !== undefined ? { eventId } : {}),
682
+ });
683
+ };
591
684
  const textSegmentEndArm = function* (m, { text }) {
592
685
  // 🔴 断闸按「**键在不在**」判,不按「是不是串」判(异源对抗复审第三轮 [medium] 采纳)。
593
686
  // 投影层已对坏 lane 位整帧 fail-closed;这一道是给**非投影口**喂进来的帧(宿主自建管线 /
@@ -606,7 +699,7 @@ const textSegmentEndArm = function* (m, { text }) {
606
699
  const r = text.replaceAnswerSegment(content, m);
607
700
  yield chrome({
608
701
  kind: 'text_segment_end',
609
- laneProof: MAIN,
702
+ laneProof: mainLane(),
610
703
  content,
611
704
  // CC-01:帧上带的是**轮换前**的段身份 —— 与本段已过境的 committed 行同值(归属的第二把钥匙)。
612
705
  segmentId: text.segmentId(),
@@ -638,7 +731,7 @@ const thinkingSegmentEndArm = function* (m, { text }) {
638
731
  const r = text.replaceThinkingSegment(content, m);
639
732
  yield chrome({
640
733
  kind: 'thinking_segment_end',
641
- laneProof: MAIN,
734
+ laneProof: mainLane(),
642
735
  content,
643
736
  ...(r.diverged ? { diverged: true } : {}),
644
737
  ...(r.committedPrefixDiverged ? { committedPrefixDiverged: true } : {}),
@@ -659,7 +752,7 @@ const promptSuggestionsArm = function* (m) {
659
752
  // 空批不发(cli 同门:`if (list.length > 0)`)—— 作废由 turn 开场那一发负责,
660
753
  // 这里再发一次 null 会把本轮真产出的建议在极端交错下擦掉。
661
754
  if (list.length > 0) {
662
- yield chrome({ kind: 'prompt_suggestions', laneProof: MAIN, suggestions: list });
755
+ yield chrome({ kind: 'prompt_suggestions', laneProof: mainLane(), suggestions: list });
663
756
  }
664
757
  };
665
758
  // ══ M2 —— tool_end_result(关卡 + B/D/E 层卡本体 + 两个侧产物)══════════════════════════════════
@@ -705,14 +798,19 @@ const toolEndResultArm = function* (m, { ctx, idOf, cards, panel }) {
705
798
  if (p === undefined || callId === undefined)
706
799
  return;
707
800
  const structured = m.structured;
801
+ // 身份键各读一次(0.76.1:此前 typeof 再取值 = 读两次;值随读变的帧会让判形与取值各看一份)。
802
+ const closeEventIdRaw = m.eventId;
803
+ const closeParentRaw = m.parentToolCallId;
804
+ const closeEventId = typeof closeEventIdRaw === 'string' ? closeEventIdRaw : undefined;
805
+ const closeParent = typeof closeParentRaw === 'string' ? closeParentRaw : undefined;
708
806
  // 出栈 + S1 复位(**yield 之前**)已收进 close() —— 见 toolCards.ts 的拆分硬约束。
709
807
  yield* cards.close(p, {
710
808
  output: m.output,
711
809
  structured,
712
810
  truncated: m.truncated === true,
713
811
  isError: m.isError === true,
714
- eventId: typeof m.eventId === 'string' ? m.eventId : undefined,
715
- parentToolCallId: typeof m.parentToolCallId === 'string' ? m.parentToolCallId : undefined,
812
+ eventId: closeEventId,
813
+ parentToolCallId: closeParent,
716
814
  });
717
815
  const s = structured !== undefined && structured !== null && typeof structured === 'object'
718
816
  ? structured
@@ -740,7 +838,7 @@ const toolEndResultArm = function* (m, { ctx, idOf, cards, panel }) {
740
838
  if (s && (s.type === 'task' || s.type === 'task-list')) {
741
839
  yield chrome({
742
840
  kind: 'task_ledger_sync',
743
- laneProof: MAIN,
841
+ laneProof: mainLane(),
744
842
  source: 'structured',
745
843
  structuredType: s.type,
746
844
  structured: s,
@@ -763,10 +861,12 @@ const taskProgressArm = function* (m, { ctx, cards, panel, inst }) {
763
861
  // 只带 rollup 数字 = CC `AgentProgress` 同形 ⇒ 忠实的 CC-187 面是 footer 面板行
764
862
  // (CC 自己对进程外 agent 的先例:面板行 + task-notification,从不合成 inline 消息轨)。
765
863
  // **不产任何 transcript 消息**:全是 chrome。
766
- const taskId = typeof m.taskId === 'string' ? m.taskId : undefined;
864
+ const taskIdRaw = m.taskId;
865
+ const taskId = typeof taskIdRaw === 'string' ? taskIdRaw : undefined;
767
866
  if (taskId === undefined)
768
867
  return;
769
- const explicitParent = typeof m.parentToolCallId === 'string' ? m.parentToolCallId : undefined;
868
+ const explicitParentRaw = m.parentToolCallId; // 读一次(0.76.1:typeof 再取值 = 读两次)
869
+ const explicitParent = typeof explicitParentRaw === 'string' ? explicitParentRaw : undefined;
770
870
  // ── W1 段2([1617]②,frame-lane-matrix #2/#8)workflow lane 门:workflow 子代的 tick 属
771
871
  // fleet lane 自描述行(footer 只渲 workflow 聚合行),**绝不**进前台 subagent lane
772
872
  // (不登 alias、不 bind、不 livePanelTasks、不发 panel tick —— 修前每个 workflow agent
@@ -982,7 +1082,7 @@ function prefixOf(whole, part) {
982
1082
  return null;
983
1083
  }
984
1084
  const resultArm = function* (m, { ctx, idOf, text, cards, panel, flags }) {
985
- yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
1085
+ yield chrome({ kind: 'retry_status', laneProof: mainLane(), status: null });
986
1086
  // MF-10 防御 sweep(sweep 点 2/4):结掉那些卡从没关过的行(流被截断)。
987
1087
  // cli 同位:setRetryStatus(null) 之后、开卡兜底关闭之前。
988
1088
  yield* panel.settle(null, false);
@@ -1073,7 +1173,7 @@ const resultArm = function* (m, { ctx, idOf, text, cards, panel, flags }) {
1073
1173
  // 改口。两个位是**长度**不是内容 —— 不复述任何一份正文。
1074
1174
  yield chrome({
1075
1175
  kind: 'result_text_diverged',
1076
- laneProof: MAIN,
1176
+ laneProof: mainLane(),
1077
1177
  committedLength: committed.length,
1078
1178
  resultLength: terminalText.length,
1079
1179
  });
@@ -1098,7 +1198,7 @@ const taskNotificationArm = function* (m, { ctx, idOf, panel, inst }) {
1098
1198
  if (settleAllowed && terminal) {
1099
1199
  yield chrome({
1100
1200
  kind: 'bgshell_settle',
1101
- laneProof: MAIN,
1201
+ laneProof: mainLane(),
1102
1202
  taskId,
1103
1203
  status: rawStatus,
1104
1204
  ...(typeof n.output_file === 'string' && n.output_file.length > 0
@@ -1121,7 +1221,7 @@ const taskNotificationArm = function* (m, { ctx, idOf, panel, inst }) {
1121
1221
  if (n.task_type !== 'external' && terminal) {
1122
1222
  yield chrome({
1123
1223
  kind: 'notification_terminal',
1124
- laneProof: MAIN,
1224
+ laneProof: mainLane(),
1125
1225
  taskId,
1126
1226
  status: rawStatus,
1127
1227
  ...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
@@ -1225,7 +1325,7 @@ const workflowCompleteArm = function* (m) {
1225
1325
  noteWorkflowCompletionCardEnqueued(runId);
1226
1326
  yield chrome({
1227
1327
  kind: 'workflow_notification_enqueue',
1228
- laneProof: MAIN,
1328
+ laneProof: mainLane(),
1229
1329
  runId,
1230
1330
  status: wfStatus,
1231
1331
  summary: wfSummary,
@@ -1278,10 +1378,10 @@ const retryStatusArm = function* (m, { ctx, flags }) {
1278
1378
  // 首版写成「无条件发 + clearRetryOverlay」,在常态顺序上双清(对抗复审命中)。
1279
1379
  if (status === null) {
1280
1380
  if (flags.takeRetryOverlay())
1281
- yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
1381
+ yield chrome({ kind: 'retry_status', laneProof: mainLane(), status: null });
1282
1382
  return;
1283
1383
  }
1284
- yield chrome({ kind: 'retry_status', laneProof: MAIN, status });
1384
+ yield chrome({ kind: 'retry_status', laneProof: mainLane(), status });
1285
1385
  // ⟨帧序耦合 6/7⟩ 挂旗;下一个主 lane 内容帧到达时由 TurnFlags 的帧前置清掉(恢复即清)。
1286
1386
  flags.noteRetryOverlay();
1287
1387
  };
@@ -1315,6 +1415,8 @@ export const ARMS = new Map([
1315
1415
  ['context_usage', contextUsageArm],
1316
1416
  // CC-89(0.75.0):压缩的非 compacted 结局(additive;不接 = 压缩失败对用户不可见)。
1317
1417
  ['compaction_outcome', compactionOutcomeArm],
1418
+ // CC-91(0.76.0):这一次 prepare 的提示词装配清单(additive;不接 = 端只能按本地镜像自己估)。
1419
+ ['prompt_assembled', promptAssembledArm],
1318
1420
  ['wiring_manifest', wiringManifestArm],
1319
1421
  // B-078 / L-208(0.65.0):design/172 流内审批两帧 —— 修前它们在投影层就 `dropped`,
1320
1422
  // 壳自己另接一份 ⇒ desktop/web 拿不到卡集与撤卡(归层违例)。
@@ -1,8 +1,35 @@
1
1
  import type { AdapterContext, AdapterOutput, ChromeEvent, LaneProof } from '../seam.js';
2
2
  /** 帧 = wire 投影出来的中性 arm(cli seam/adapter downstream 的 SDKMessage 形),结构化读取。 */
3
3
  export type Frame = Record<string, unknown>;
4
- /** 主车道证明——本批实现的臂全在 leader lane(子代/workflow lane 的 tick 面留 #52b)。 */
5
- export declare const MAIN: LaneProof;
4
+ /**
5
+ * 主车道证明的**出厂口** —— 每次调用交出一只**新的** `{lane:'main'}`(本批实现的臂全在 leader
6
+ * lane;子代/workflow lane 的 tick 面留 #52b)。
7
+ *
8
+ * 🔴 **为什么是函数不是常量**(0.76.1)。此前这里是一只模块级常量对象,`laneOf(未登记 id)` 与本包
9
+ * 约五十个 `laneProof:` 主车道发点交出去的是**同一只**对象 —— 跨适配器实例、跨流、跨 turn 全是
10
+ * 它。实测(修前,本门 L1/L5 段的同一条路径):两个互不相干的消费者各取一次未登记行的证明,
11
+ * `===` 为真;给第一只补上卡号之后,第二只当场读出 `{lane:'subagent', parentToolCallId:'…'}`,
12
+ * 开流序幕那四条主车道事件也一起变 —— **一次写打穿整个进程**,而且静默。
13
+ *
14
+ * 🔴 **判据如实**:这是**别名风险**,不是「已经在串味的现场」。修前逐站点穷举(语法树扫写表达式)
15
+ * 的结论是:本包内对车道证明**零就地写**(两处看起来像写的是给局部变量**换绑**新对象,不改旧的);
16
+ * 三个消费端的证明用法全是只读(读 `lane` 位 / 解构丢弃)。⇒ 本批修的是「交出去的可变引用被
17
+ * 当成缺省值别名」这一形,不是在修一条已经在错归因的行。发布文案不许把它写成用户可见的回归。
18
+ * 这条形本身仍然要修:`laneProof` 是**公开输出面**上的一位,拿它做 WeakMap 键 / 身份去重 /
19
+ * 视图层身份记忆的消费者,在两条无关行共享一只对象时会把它们认成同一条 —— **一个字都不写也照错**。
20
+ * 这也正是冻结那条修法救不了的一半。
21
+ *
22
+ * 🔴 **为什么不是冻结**(两条都摆过,这条落选):
23
+ * ① 冻结只挡「写」,不解「别名」——上一段那三种按身份认物的消费面照错;每次新建两件一起解决。
24
+ * ② 子代臂 `{lane:'subagent', parentToolCallId}` 与 workflow 臂本来就**逐次新建**且可写,只冻主
25
+ * 车道这一半 ⇒ 同一处写操作会因为「这条行恰好绑没绑卡」而一边抛一边收,失败随行漂成间歇形;
26
+ * 要对称就得连十几处内联字面量一起冻,比每次新建更大的改动换更小的收益。
27
+ * ③ 证明是在宿主手里被写的:冻结把一条本包的别名缺陷变成宿主侧的抛错,而宿主的抛会顺着
28
+ * `for await` 打断整条 adapt 流。适配层不该给主链添这种反噬。
29
+ * ⚠️ 代价如实:每条主车道事件多一只小对象(≈ 一个键的字面量)。别为了省它把出厂口缓存成模块级
30
+ * 单例(`const M = mainLane()` 就是把这一形原样种回来)—— 结构反钉门盯着这一形。
31
+ */
32
+ export declare const mainLane: () => LaneProof;
6
33
  /** 帧 → 稳定转录 id(`suffix` = 同一帧派生多条消息时的去撞位)。 */
7
34
  export type IdOf = (frame: Frame, suffix?: string) => string;
8
35
  /**
package/dist/adapt/ids.js CHANGED
@@ -1,6 +1,33 @@
1
1
  import { deriveTranscriptId } from '../seam.js';
2
- /** 主车道证明——本批实现的臂全在 leader lane(子代/workflow lane 的 tick 面留 #52b)。 */
3
- export const MAIN = { lane: 'main' };
2
+ /**
3
+ * 主车道证明的**出厂口** —— 每次调用交出一只**新的** `{lane:'main'}`(本批实现的臂全在 leader
4
+ * lane;子代/workflow lane 的 tick 面留 #52b)。
5
+ *
6
+ * 🔴 **为什么是函数不是常量**(0.76.1)。此前这里是一只模块级常量对象,`laneOf(未登记 id)` 与本包
7
+ * 约五十个 `laneProof:` 主车道发点交出去的是**同一只**对象 —— 跨适配器实例、跨流、跨 turn 全是
8
+ * 它。实测(修前,本门 L1/L5 段的同一条路径):两个互不相干的消费者各取一次未登记行的证明,
9
+ * `===` 为真;给第一只补上卡号之后,第二只当场读出 `{lane:'subagent', parentToolCallId:'…'}`,
10
+ * 开流序幕那四条主车道事件也一起变 —— **一次写打穿整个进程**,而且静默。
11
+ *
12
+ * 🔴 **判据如实**:这是**别名风险**,不是「已经在串味的现场」。修前逐站点穷举(语法树扫写表达式)
13
+ * 的结论是:本包内对车道证明**零就地写**(两处看起来像写的是给局部变量**换绑**新对象,不改旧的);
14
+ * 三个消费端的证明用法全是只读(读 `lane` 位 / 解构丢弃)。⇒ 本批修的是「交出去的可变引用被
15
+ * 当成缺省值别名」这一形,不是在修一条已经在错归因的行。发布文案不许把它写成用户可见的回归。
16
+ * 这条形本身仍然要修:`laneProof` 是**公开输出面**上的一位,拿它做 WeakMap 键 / 身份去重 /
17
+ * 视图层身份记忆的消费者,在两条无关行共享一只对象时会把它们认成同一条 —— **一个字都不写也照错**。
18
+ * 这也正是冻结那条修法救不了的一半。
19
+ *
20
+ * 🔴 **为什么不是冻结**(两条都摆过,这条落选):
21
+ * ① 冻结只挡「写」,不解「别名」——上一段那三种按身份认物的消费面照错;每次新建两件一起解决。
22
+ * ② 子代臂 `{lane:'subagent', parentToolCallId}` 与 workflow 臂本来就**逐次新建**且可写,只冻主
23
+ * 车道这一半 ⇒ 同一处写操作会因为「这条行恰好绑没绑卡」而一边抛一边收,失败随行漂成间歇形;
24
+ * 要对称就得连十几处内联字面量一起冻,比每次新建更大的改动换更小的收益。
25
+ * ③ 证明是在宿主手里被写的:冻结把一条本包的别名缺陷变成宿主侧的抛错,而宿主的抛会顺着
26
+ * `for await` 打断整条 adapt 流。适配层不该给主链添这种反噬。
27
+ * ⚠️ 代价如实:每条主车道事件多一只小对象(≈ 一个键的字面量)。别为了省它把出厂口缓存成模块级
28
+ * 单例(`const M = mainLane()` 就是把这一形原样种回来)—— 结构反钉门盯着这一形。
29
+ */
30
+ export const mainLane = () => ({ lane: 'main' });
4
31
  /**
5
32
  * L-215③(0.65.0)—— 一条 assistant 转录消息的**身份键**(`message.id` / `message.model`)。
6
33
  *
@@ -40,8 +40,9 @@ export interface PanelTaskLedger {
40
40
  unboundCandidates(): number;
41
41
  /** 行的车道证明:绑到卡的 = subagent lane(带父卡 id),未绑的 = 主 lane。 */
42
42
  laneOf(taskId: string): LaneProof;
43
- /** 这条行已经绑过卡了吗(含糊回落的前置判位;别拿 `laneOf(...) === MAIN` 代替 —— 那是
44
- * 按引用相等在推断,判据锚在会决定结果的量上才对)。 */
43
+ /** 这条行已经绑过卡了吗(含糊回落的前置判位;别拿「`laneOf(...)` 是不是那只主车道常量」代替 ——
44
+ * 那是按引用相等在推断,判据锚在会决定结果的量上才对。0.76.1 起主车道证明**逐次新建**,
45
+ * 那种写法连偶然成立都做不到了)。 */
45
46
  isBound(taskId: string): boolean;
46
47
  /**
47
48
  * B-074 —— 这条行绑在一张**本 turn 还开着**的卡上吗(`liveBound`,#6 的 live/inert 判位)。
@@ -75,7 +76,7 @@ export interface PanelTaskLedger {
75
76
  * B-074(0.65.0)**终态 tick 边** —— core 明写的那一拍(`task_progress.status` = `completed`/
76
77
  * `failed`)按 taskId 直接落行。
77
78
  *
78
- * 为什么不复用 {@link settleFromNotification}:那一条是**通知帧**那半场,它按 `MAIN` 车道发
79
+ * 为什么不复用 {@link settleFromNotification}:那一条是**通知帧**那半场,它按**主车道**发
79
80
  * end(bg 子代行本来就是 unbound 的),而终态 tick 的行**可能绑着一张前台委派卡** ⇒ 车道证明
80
81
  * 必须走 {@link laneOf},否则一条 subagent lane 的行会拿 main lane 的证明去落终态(帧×lane
81
82
  * 矩阵那一族的病)。也不复用 {@link settle}:那一条按**卡 id** 收(关卡/防御 sweep 的语义),
@@ -1,5 +1,5 @@
1
1
  import { clearEnginePanelTaskResident, clearEnginePanelTaskResidentByWire, markEnginePanelTaskResident } from '../engineAgentPanelStore.js';
2
- import { chrome, MAIN } from './ids.js';
2
+ import { chrome, mainLane } from './ids.js';
3
3
  import { SUBAGENT_TOOL_NAMES } from './wireShapes.js';
4
4
  export function createPanelTaskLedger(ctx, cards, inst) {
5
5
  /** taskId → 绑定的 open tool_use id。 */
@@ -22,7 +22,25 @@ export function createPanelTaskLedger(ctx, cards, inst) {
22
22
  const subagentTypeById = new Map();
23
23
  const laneOf = (taskId) => {
24
24
  const card = taskCardBinding.get(taskId);
25
- return card !== undefined ? { lane: 'subagent', parentToolCallId: card } : MAIN;
25
+ return card !== undefined ? { lane: 'subagent', parentToolCallId: card } : mainLane();
26
+ };
27
+ /**
28
+ * 一条行要发**多条**事件时的车道证明来源(0.76.1,异源对抗复审 R1 [medium] 实测逼出)。
29
+ *
30
+ * 病形:终态那两处(关卡 / 终态 tick)此前 `const lane = laneOf(taskId)` 取一次,然后 stop 与
31
+ * end **两条事件共用那一只** —— 消费者拿到 stop 后改它的归属,同一条行的 end 跟着变;逐条消费
32
+ * 的宿主甚至能在 end 那一次 `yield` **之前**就把它污染掉(生成器在两条之间是挂起的)。
33
+ *
34
+ * 修法**不是**在每条事件那里各调一次 `laneOf`:两次调用之间生成器会挂起,消费者若在这中间绑了卡,
35
+ * 同一条行的两条事件就会**分流到两个归属**上(一条算主车道、一条算子代)——那比别名更坏。
36
+ * ⇒ **归属判一次、证明铸多只**:这里把归属(绑到哪张卡 / 没绑)先定死,返回的铸口每次交出一只
37
+ * 新对象、值恒相同。
38
+ */
39
+ const laneMinterFor = (taskId) => {
40
+ const card = taskCardBinding.get(taskId);
41
+ return card !== undefined
42
+ ? () => ({ lane: 'subagent', parentToolCallId: card })
43
+ : mainLane;
26
44
  };
27
45
  return {
28
46
  laneOf,
@@ -131,7 +149,7 @@ export function createPanelTaskLedger(ctx, cards, inst) {
131
149
  yield chrome({ kind: 'inline_task_stats', laneProof: { lane: 'subagent', parentToolCallId: cardId }, op: 'settle', cardId, isError });
132
150
  }
133
151
  else {
134
- yield chrome({ kind: 'inline_task_stats', laneProof: MAIN, op: 'settle_all' });
152
+ yield chrome({ kind: 'inline_task_stats', laneProof: mainLane(), op: 'settle_all' });
135
153
  }
136
154
  for (const taskId of livePanelTasks) {
137
155
  if (cardId !== null && taskCardBinding.get(taskId) !== cardId)
@@ -144,18 +162,18 @@ export function createPanelTaskLedger(ctx, cards, inst) {
144
162
  if (endedPanelTasks.has(taskId))
145
163
  continue;
146
164
  endedPanelTasks.add(taskId);
147
- const lane = laneOf(taskId);
165
+ const mintLane = laneMinterFor(taskId);
148
166
  const agentType = subagentTypeById.get(taskId);
149
167
  yield chrome({
150
168
  kind: 'subagent_lifecycle',
151
- laneProof: lane,
169
+ laneProof: mintLane(),
152
170
  phase: 'stop',
153
171
  taskId,
154
172
  ...(agentType !== undefined ? { agentType } : {}),
155
173
  });
156
174
  yield chrome({
157
175
  kind: 'panel_task',
158
- laneProof: lane,
176
+ laneProof: mintLane(),
159
177
  event: {
160
178
  kind: 'end',
161
179
  taskId,
@@ -184,7 +202,7 @@ export function createPanelTaskLedger(ctx, cards, inst) {
184
202
  endedPanelTasks.add(taskId);
185
203
  yield chrome({
186
204
  kind: 'panel_task',
187
- laneProof: MAIN,
205
+ laneProof: mainLane(),
188
206
  event: { kind: 'end', taskId, isError },
189
207
  });
190
208
  // SubagentStop 只对**真 fire 过 Start** 的子代(workflow runId / 外来 id 不配对乱 fire)。
@@ -193,7 +211,7 @@ export function createPanelTaskLedger(ctx, cards, inst) {
193
211
  if (inst.hasFiredSubagentStart(taskId)) {
194
212
  yield chrome({
195
213
  kind: 'subagent_lifecycle',
196
- laneProof: MAIN,
214
+ laneProof: mainLane(),
197
215
  phase: 'stop',
198
216
  taskId,
199
217
  guard: 'if-started',
@@ -215,7 +233,7 @@ export function createPanelTaskLedger(ctx, cards, inst) {
215
233
  return;
216
234
  endedPanelTasks.add(taskId);
217
235
  clearResidencyBothKeys(taskId);
218
- const lane = laneOf(taskId);
236
+ const mintLane = laneMinterFor(taskId);
219
237
  const card = taskCardBinding.get(taskId);
220
238
  if (card !== undefined) {
221
239
  yield chrome({
@@ -231,7 +249,7 @@ export function createPanelTaskLedger(ctx, cards, inst) {
231
249
  const agentType = subagentTypeById.get(taskId);
232
250
  yield chrome({
233
251
  kind: 'subagent_lifecycle',
234
- laneProof: lane,
252
+ laneProof: mintLane(),
235
253
  phase: 'stop',
236
254
  taskId,
237
255
  ...(agentType !== undefined ? { agentType } : {}),
@@ -240,7 +258,7 @@ export function createPanelTaskLedger(ctx, cards, inst) {
240
258
  }
241
259
  yield chrome({
242
260
  kind: 'panel_task',
243
- laneProof: lane,
261
+ laneProof: mintLane(),
244
262
  event: { kind: 'end', taskId, isError },
245
263
  });
246
264
  },
@@ -1,4 +1,4 @@
1
- import { chrome, MAIN, transcript, messageIdentityOf } from './ids.js';
1
+ import { chrome, mainLane, transcript, messageIdentityOf } from './ids.js';
2
2
  import { estimateCjkTokens, FLUSH_INTERVAL_MS } from './wireShapes.js';
3
3
  // L-318:六形判决单源(两条车道同吃;落账各归各层,见 textSegmentAuthority.ts 头注)。
4
4
  import { resolveTextSegmentAuthority } from './textSegmentAuthority.js';
@@ -196,7 +196,7 @@ export function createTextStream(ctx, idOf) {
196
196
  thinkingAnchor = null;
197
197
  thinkingBlockOpen = false;
198
198
  // P2d:活体 "∴ Thinking…" 行在 committed 块落地的**同一刻**收掉(先收行再上消息,cli 同序)
199
- yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: false });
199
+ yield chrome({ kind: 'thinking_activity', laneProof: mainLane(), active: false });
200
200
  yield transcript(msg, ctx.now());
201
201
  }
202
202
  /** 段提交的**唯一本体**(两个入口 `takeAnswerSegment` / `takeAnswerSegmentOnIdle` 共用;
@@ -266,7 +266,7 @@ export function createTextStream(ctx, idOf) {
266
266
  thinkingBlockOpen = true;
267
267
  yield chrome({
268
268
  kind: 'stream_delta',
269
- laneProof: MAIN,
269
+ laneProof: mainLane(),
270
270
  channel: 'thinking',
271
271
  text: thinkingPending,
272
272
  estimatedTokens: estimateCjkTokens(thinkingPending),
@@ -283,7 +283,7 @@ export function createTextStream(ctx, idOf) {
283
283
  ctx.probe?.('stream', `${ctx.now()} text_flush len=${textPending.length}`);
284
284
  yield chrome({
285
285
  kind: 'stream_delta',
286
- laneProof: MAIN,
286
+ laneProof: mainLane(),
287
287
  channel: 'text',
288
288
  text: textPending,
289
289
  estimatedTokens: estimateCjkTokens(textPending),
@@ -328,7 +328,7 @@ export function createTextStream(ctx, idOf) {
328
328
  // 对照物只在 `reasoning_end` 用过之后清(段界由引擎说了算),turn 结束整只丢,天然有界。
329
329
  // P2d:elapsed 锚在**首条** leader 推理增量(token 累计由 stream_delta.estimatedTokens
330
330
  // 承载,宿主自加;>30s 无增量的 stall 提示同样由宿主按增量时间戳判)。
331
- yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: true });
331
+ yield chrome({ kind: 'thinking_activity', laneProof: mainLane(), active: true });
332
332
  }
333
333
  thinking += delta;
334
334
  thinkingPending += delta;