@sema-agent/client-core 0.6.0 → 0.8.0

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 (68) hide show
  1. package/README.md +31 -11
  2. package/dist/adapt.d.ts +4 -2
  3. package/dist/adapt.js +225 -25
  4. package/dist/compensations.d.ts +63 -0
  5. package/dist/compensations.js +138 -0
  6. package/dist/detachWire.d.ts +141 -0
  7. package/dist/detachWire.js +182 -0
  8. package/dist/diff/patch.d.ts +29 -0
  9. package/dist/diff/patch.js +45 -0
  10. package/dist/engineSessionParam.d.ts +11 -0
  11. package/dist/engineSessionParam.js +41 -0
  12. package/dist/engineWireTarget.d.ts +14 -0
  13. package/dist/engineWireTarget.js +53 -0
  14. package/dist/finalVerifyWire.d.ts +74 -0
  15. package/dist/finalVerifyWire.js +63 -0
  16. package/dist/fleet/fleetLedger.d.ts +132 -0
  17. package/dist/fleet/fleetLedger.js +393 -0
  18. package/dist/fleet/fleetProjection.d.ts +181 -0
  19. package/dist/fleet/fleetProjection.js +258 -0
  20. package/dist/headlessPermissionModeWire.d.ts +55 -0
  21. package/dist/headlessPermissionModeWire.js +111 -0
  22. package/dist/headlessReconnectWire.d.ts +96 -0
  23. package/dist/headlessReconnectWire.js +141 -0
  24. package/dist/hooksWireCaps.d.ts +91 -0
  25. package/dist/hooksWireCaps.js +359 -0
  26. package/dist/host.d.ts +34 -2
  27. package/dist/host.js +6 -0
  28. package/dist/index.d.ts +74 -1
  29. package/dist/index.js +104 -1
  30. package/dist/interactiveToolsWire.d.ts +48 -0
  31. package/dist/interactiveToolsWire.js +86 -0
  32. package/dist/limitsWire.d.ts +89 -0
  33. package/dist/limitsWire.js +225 -0
  34. package/dist/liveInitToolFace.d.ts +53 -0
  35. package/dist/liveInitToolFace.js +251 -0
  36. package/dist/model/modelFamilies.json +63 -0
  37. package/dist/model/providerPresets.d.ts +46 -0
  38. package/dist/model/providerPresets.js +160 -0
  39. package/dist/model/providerPresets.json +1179 -0
  40. package/dist/notifications.d.ts +7 -0
  41. package/dist/notifications.js +32 -0
  42. package/dist/sandboxWire.d.ts +75 -0
  43. package/dist/sandboxWire.js +138 -0
  44. package/dist/scenarioWire.d.ts +62 -0
  45. package/dist/scenarioWire.js +115 -0
  46. package/dist/seam.d.ts +52 -5
  47. package/dist/seam.js +15 -2
  48. package/dist/subagent/engineCompactWire.d.ts +12 -0
  49. package/dist/subagent/engineCompactWire.js +176 -0
  50. package/dist/subagent/engineDelegatedPrompt.d.ts +44 -0
  51. package/dist/subagent/engineDelegatedPrompt.js +205 -0
  52. package/dist/subagent/engineRowStopGate.d.ts +18 -0
  53. package/dist/subagent/engineRowStopGate.js +54 -0
  54. package/dist/subagent/engineSubagentOutput.d.ts +15 -0
  55. package/dist/subagent/engineSubagentOutput.js +98 -0
  56. package/dist/subagent/engineSubagentSteer.d.ts +10 -0
  57. package/dist/subagent/engineSubagentSteer.js +68 -0
  58. package/dist/subagent/engineSubagentTail.d.ts +40 -0
  59. package/dist/subagent/engineSubagentTail.js +241 -0
  60. package/dist/subagent/engineTaskHandleWire.d.ts +90 -0
  61. package/dist/subagent/engineTaskHandleWire.js +212 -0
  62. package/dist/toolResult.d.ts +118 -0
  63. package/dist/toolResult.js +774 -0
  64. package/dist/workflowClient.d.ts +42 -0
  65. package/dist/workflowClient.js +467 -0
  66. package/dist/workflowMonitor.d.ts +94 -0
  67. package/dist/workflowMonitor.js +43 -0
  68. package/package.json +5 -3
package/README.md CHANGED
@@ -23,13 +23,15 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
23
23
 
24
24
  ## Scope
25
25
 
26
- - **Today (0.5.0)** — the adapter seam, the `adapt()` pipeline, the notification/caps/model
27
- families, and the adapter kernel (stream driver + downstream projectors).
28
- - **Planned** the shell's remaining shared session logic lands here in batches: the
29
- five-layer frame dispatch, the compensation arms, the notification/dedup ledgers, the
30
- fleet/panel projections. The TUI then keeps only its Ink rendering and keyboard interaction;
31
- the web session view (first target consumer) and the desktop client (second) consume the
32
- same package.
26
+ - **Today (0.8.0)** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
27
+ B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
28
+ + downstream projectors), the request-shaping surface, the fleet/panel projections and the
29
+ subagent wire family, the workflow monitor contract + live source, and the model catalog tables.
30
+ - **Planned** what is still in the shell: HITL / approvals (`hitlBridge`, `liveHitlAskWire`,
31
+ `liveToolApprovalWire`, `planReviewWire`) and the single-authority completion event (batch B7);
32
+ plus everything that is genuinely host-specific — Ink rendering and keyboard interaction,
33
+ filesystem persistence, pty/terminal, and the signal-path teardown. The web session view
34
+ (first target consumer) and the desktop client (second) consume the same package.
33
35
 
34
36
  ## Dependencies
35
37
 
@@ -127,15 +129,33 @@ grew the host context to carry what a non-Node host must supply:
127
129
  desktop host coalesces single-frame deltas at 16 ms; the 2.1.207 CLI line we track uses 100 ms),
128
130
  so the cadence is host render policy while the merge *semantics* stay here, in one place.
129
131
 
130
- Known intentional deltas from the CLI are enumerated in `ADAPTER_DIVERGENCES`.
132
+ 0.6.0 0.8.0 (batches B4 – B6) completed the frame dispatch and moved in everything that is not
133
+ host-specific. The blow-by-blow lives in the `src/index.ts` head comment (the source of truth); the
134
+ short version:
135
+
136
+ - **0.6.0** — A-layer dispatch finished (14th arm: `task_progress`, with row binding, the workflow
137
+ lane gate and the panel sweeps), the request surface unified into one builder with a field
138
+ matrix, one credential resolver for all construction points, the compile-time verb façade, and
139
+ package-level host assembly `installHost({log,probe,queue,timers,settings,fs,session})`.
140
+ - **0.7.0** — the tool-result card B/D/E layers, structured-output whitelist takeover of the
141
+ regex reverse-parsers, client-side diff hunks, and the paired ledgers folded back to module scope.
142
+ - **0.8.0** — fleet ledger/projection + the subagent wire family, the headless detach wire (split:
143
+ decision here, signal-path dispatch stays in the host), the workflow monitor contract + live
144
+ source, `hooksWireCaps` on the `SettingsPort`, `liveInitToolFace`, the model catalog tables, and
145
+ `MIGRATED_COMPENSATIONS` — the ledger of which shell-side compensation was split where, which
146
+ chrome arm carries the host's half, and when upstream lets it retire.
147
+
148
+ Known intentional deltas from the CLI are enumerated in `ADAPTER_DIVERGENCES`. The *compensation*
149
+ ledger is a different table (`MIGRATED_COMPENSATIONS`): the entries there behave identically on
150
+ both sides — what it records is why a compensation exists and when it can be deleted.
131
151
 
132
152
  ## Guards
133
153
 
134
154
  ```bash
135
155
  npm install
136
- node scripts/run-client-core-pure-test.mjs # 429 consumer-view checks (22 + 140 + 208 + 59); no CLI tree needed
137
- node scripts/run-client-core-portability-test.mjs # 14 checks: kernel closure + browser bundle
138
- node scripts/run-client-core-diff-test.mjs # 284 checks: differential + replay-id + ledger
156
+ node scripts/run-client-core-pure-test.mjs # 867 consumer-view checks (22 + 140 + 208 + 59 + 101 + 96 + 241); no CLI tree needed
157
+ node scripts/run-client-core-portability-test.mjs # 39 checks: kernel + A-layer + index closures, browser bundle
158
+ node scripts/run-client-core-diff-test.mjs # 1397 checks: differential + replay-id + ledger
139
159
  ```
140
160
 
141
161
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
package/dist/adapt.d.ts CHANGED
@@ -84,11 +84,13 @@ export declare const ADAPTER_COVERAGE: {
84
84
  readonly b3: readonly ["T38 三清第三清 + T57 第五处断闸:prompt_suggestions(turn 开场作废 + 子流断闸)", "T40 IDLE-FLUSH 1.5s 竞速(需 ctx.setTimer;缺席=不启用)", "T58 手动 iterator 的 finally cleanup(early break/throw 都要关上游生成器)", "T59 abort 半场(ctx.signal;中断后不产终答)", "T60 无 result 收尾腿(泄尾增量 → 清 liveness → 尾思考 → 尾文本段)"];
85
85
  /** B4(0.6.0)新落码的臂/半场 —— A 层收官批,同样别再在 todo 里留同名条目。 */
86
86
  readonly b4: readonly ["A 层第 14 臂 task_progress:workflow lane 三级门 + 判据④ belt 回植 + C1 alias + bindTaskCard(显式父/FIFO/含糊即 unbound)+ inline stats + T50 双形读 + SubagentStart + #6 resident 台账 + 面板 tick", "settlePanelTasks 三处 sweep 全接线(tool_end 显式关卡带报告 / result 防御 sweep / turn 末 + abort sweep)", "P0-2 tool label 侧信道(sanitizeToolUseBlock 摘 label + engineToolLabelStore 登记;0.5.0 真缺口,label 会随 tool_use block 落转录 → /compact 直发 provider 400)", "task_notification 的两件宿主义务收回库内自持:dropQueuedNotificationsForRun + clearEnginePanelTaskResident;stop hook 的 if-started 门由库解", "T8 flattenWireOutput 提为独立导出(A 层 settle 报告与 B 层卡体同一份)"];
87
+ /** B5(0.7.0)新落码的臂/半场 —— B/D/E 层收批,同样别再在 todo 里留同名条目。 */
88
+ readonly b5: readonly ["B 层 structuredToToolUseResult 14 case(ask-user-question/bash/edit/create/update/text/grep/glob/notebook-edit/cron-create/cron-delete/cron-list/agent/task-output)+ workflow-run 登记副作用", "D 层 wireOutputToBody 3 臂(bash / taskoutput / 泛化);structured 白名单在场时 T11+T14 正则退位,缺席保回落", "E 层 4 臂:#117a bg Bash 回执(判定在库→chrome bgshell_register)· TodoWrite oldTodos 富卡 · ReportFindings 按名认领 · Remember→memory_saved 系统消息", "tool_end_result → user tool_result 正常路径(卡本体);result 与 turn 末两处开卡兜底关闭(abort 不兜底,cli 同)", "task/task-list structured → chrome task_ledger_sync{source:\"structured\"}(判定归一在库、落盘留宿主)", "T7 诚实缺席(§8-4:不搬 mock 合成器,改 degraded 标记)· T20 diff hunks 进包(src/diff/patch.ts)"];
87
89
  /** 如实留白——写不动/依赖未搬运资产的臂,#52b 起分批接。 */
88
- readonly todo: readonly ["tool_end_result user tool_result 正常路径(需搬 wireOutputToBody/structuredToToolUseResult/mock 合成器 + TodoWrite/ReportFindings 富卡片,约 600 行;workflow 轮询信封的人话投影已在 workflow.ts 就位,等这条接线)= B5", "tool_end_result 的两个侧产物:Remember → memory_saved 系统消息、task/task-list structured 面板台账全量同步 = B5", "result/turn 末的开卡兜底关闭(同上,依赖合成器)= B5", "#117a bg Bash 回执探测(detectEngineBgShellReceipt 文案锚定探测器)= B5(E 层按工具名分臂)", "abort 半场里依赖开卡合成器的那件(兜底关卡)= B5;行 sweep 半场 B4 已接", "macrotask 让渡(cli 在提交前让出宏任务给 Ink;宿主渲染策略,不进库)"];
90
+ readonly todo: readonly ["T13 **D 层** 半场(taskoutput block content)未随 structured 退位:退位会改 block content 形状(cli 那里是裸对象 = 件1 隐患),属行为面改动,无真实需求驱动 DIVERGENCE-8", "macrotask 让渡(cli 在提交前让出宏任务给 Ink;宿主渲染策略,不进库)"];
89
91
  };
90
92
  /** 本批**已知的、有意的**行为分歧(差分守卫显式登记,绝不当等价) */
91
- export declare const ADAPTER_DIVERGENCES: readonly [string, string, string, string, string, string];
93
+ export declare const ADAPTER_DIVERGENCES: readonly [string, string, string, string, string, string, string, string, string];
92
94
  /** 有台账序列化位的适配器(seam 的 WireToCcAdapter 只定了 exportLedger——见 SEAM-GAP-1)。 */
93
95
  export interface WireToCcAdapterWithLedger extends WireToCcAdapter {
94
96
  exportLedger(): AdapterLedgerState & Record<string, unknown>;
package/dist/adapt.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { deriveTranscriptId } from './seam.js';
2
- import { dropQueuedNotificationsForRun, normalizeTaskNotification, renderTaskNotificationXml, taskNotificationDedupKey, } from './notifications.js';
2
+ import { dropQueuedNotificationsForRun, isEngineWorkflowNotified, isWorkflowCompletionCardEnqueued, listNotifiedRuns, listWorkflowCompletionCardsEnqueued, markEngineWorkflowNotified, normalizeTaskNotification, noteWorkflowCompletionCardEnqueued, renderTaskNotificationXml, taskNotificationDedupKey, } from './notifications.js';
3
3
  import { mapBrainStatusToRetry } from './retryStatus.js';
4
4
  import { steeringInjectedToAttachments } from './steering.js';
5
5
  import { projectDiagnosticsFrame } from './diagnostics.js';
6
6
  // ── B4:A 层搬入后适配器**自持**的四个本包台账(写者与读者同包 = R4 单实例纪律;
7
7
  // 这四件都没有 UI 订阅面,做成 chrome 臂只会凭空长出一条「宿主不实现就静默失效」的义务)。
8
+ // ── B5:B/D/E 层(工具结果卡)。纯投影 + 三个本包台账副作用,零宿主依赖。
9
+ import { degradedToolResultBody, detectEngineBgShellReceipt, reportFindingsToolUseResult, structuredToToolUseResult, todoWriteToolUseResult, wireOutputToBody, } from './toolResult.js';
8
10
  import { recordEngineToolLabel } from './engineToolLabelStore.js';
9
11
  import { registerSubagentAlias } from './subagentContentStore.js';
10
12
  import { isWorkflowAgentTaskId, recordWorkflowAgentTaskId } from './workflow.js';
@@ -212,13 +214,18 @@ export const ADAPTER_COVERAGE = {
212
214
  'task_notification 的两件宿主义务收回库内自持:dropQueuedNotificationsForRun + clearEnginePanelTaskResident;stop hook 的 if-started 门由库解',
213
215
  'T8 flattenWireOutput 提为独立导出(A 层 settle 报告与 B 层卡体同一份)',
214
216
  ],
217
+ /** B5(0.7.0)新落码的臂/半场 —— B/D/E 层收批,同样别再在 todo 里留同名条目。 */
218
+ b5: [
219
+ 'B 层 structuredToToolUseResult 14 case(ask-user-question/bash/edit/create/update/text/grep/glob/notebook-edit/cron-create/cron-delete/cron-list/agent/task-output)+ workflow-run 登记副作用',
220
+ 'D 层 wireOutputToBody 3 臂(bash / taskoutput / 泛化);structured 白名单在场时 T11+T14 正则退位,缺席保回落',
221
+ 'E 层 4 臂:#117a bg Bash 回执(判定在库→chrome bgshell_register)· TodoWrite oldTodos 富卡 · ReportFindings 按名认领 · Remember→memory_saved 系统消息',
222
+ 'tool_end_result → user tool_result 正常路径(卡本体);result 与 turn 末两处开卡兜底关闭(abort 不兜底,cli 同)',
223
+ 'task/task-list structured → chrome task_ledger_sync{source:"structured"}(判定归一在库、落盘留宿主)',
224
+ 'T7 诚实缺席(§8-4:不搬 mock 合成器,改 degraded 标记)· T20 diff hunks 进包(src/diff/patch.ts)',
225
+ ],
215
226
  /** 如实留白——写不动/依赖未搬运资产的臂,#52b 起分批接。 */
216
227
  todo: [
217
- 'tool_end_result user tool_result 正常路径(需搬 wireOutputToBody/structuredToToolUseResult/mock 合成器 + TodoWrite/ReportFindings 富卡片,约 600 行;workflow 轮询信封的人话投影已在 workflow.ts 就位,等这条接线)= B5',
218
- 'tool_end_result 的两个侧产物:Remember → memory_saved 系统消息、task/task-list structured → 面板台账全量同步 = B5',
219
- 'result/turn 末的开卡兜底关闭(同上,依赖合成器)= B5',
220
- '#117a bg Bash 回执探测(detectEngineBgShellReceipt 文案锚定探测器)= B5(E 层按工具名分臂)',
221
- 'abort 半场里依赖开卡合成器的那件(兜底关卡)= B5;行 sweep 半场 B4 已接',
228
+ 'T13 **D 层** 半场(taskoutput block content)未随 structured 退位:退位会改 block content 形状(cli 那里是裸对象 = 件1 隐患),属行为面改动,无真实需求驱动 ⇒ 见 DIVERGENCE-8',
222
229
  'macrotask 让渡(cli 在提交前让出宏任务给 Ink;宿主渲染策略,不进库)',
223
230
  ],
224
231
  };
@@ -241,7 +248,26 @@ export const ADAPTER_DIVERGENCES = [
241
248
  'DIVERGENCE-6(B4):`subagent_lifecycle` 的**去重与 if-started 门在库里解**,cli 在 module 级 Set 上解。' +
242
249
  '可观测后果相同(同一 taskId 的 Start 恒一次;没 Start 过的 taskId 收不到通知臂的 Stop),' +
243
250
  '差在「谁持台账」:cli=进程级、本包=适配器实例级(= 一个 session)。多 session 宿主(web 一页两会话)' +
244
- '上本包语义**更正确**——cli 的 module 形会让两个会话互吞对方的 Start。',
251
+ '上本包语义**更正确**——cli 的 module 形会让两个会话互吞对方的 Start。' +
252
+ '⚠️ B5 更正范围:同批把 `notifiedRuns`/`cardEnqueuedRuns` **折回 module 台账**(它们与队列 port/' +
253
+ 'idle watcher/outstanding 药丸配对,per-session 会造成前提不共享);本条只剩 ' +
254
+ '`firedSubagentStartHookTaskIds` 与 `renderedNotifications` 两项仍是实例级。',
255
+ 'DIVERGENCE-7(B5,§8-4 裁定的**有意**分歧):`tool_end.output === undefined` 时 —— cli 用 mock ' +
256
+ 'toolIO 注册表**反造**结果体(卡片显示「看起来像真的、但不是这次执行产物」的内容);本包**诚实缺席**:' +
257
+ '`content: \'\'` + 消息上打 `_sema_degraded: \'wire_carried_no_output\'`,显示决策留端。' +
258
+ '依据=宪法「诚实优先于产出」+ 多端复制会各自编不同的假话。' +
259
+ '⚠️ B6 现势([1868]):B5 壳侧收批后 cli 的 tool_result 卡**就是本包的透传**(壳的 B 层与 mock ' +
260
+ '合成器整树已删)⇒ 本条对 `--cli=worktree` 侧**已不可观测**,差分守卫的哨兵归一段同批删除,改' +
261
+ '**原样逐字比**;独立 oracle 保留(标记集必须恰等于「有 tool_end 但没带 output 的开卡集」,' +
262
+ '少标/多标两向都红)。本条仍留登记表:它对 `--cli=freeze`(FREEZE_SHA 冻结侧,§4.4)依然成立。',
263
+ 'DIVERGENCE-8(B5):**structured 在场时正则反解退位**(T11/T14;[1840]§一 白名单 + 设计稿 R9②)。' +
264
+ '同一帧上 cli 从模型面框架文本反解 {stdout,stderr,exitCode},本包直接读 `structured.bash.*`。' +
265
+ '框架**自洽**时两者逐字节相等(差分守卫 6 条 bash 双路 fixture 实证);不自洽时本包跟 structured ' +
266
+ '(框架里的尾换行等是渲染副产物)。T13/T24 的 **B 层**半场同款惰性退位(structured 带齐 ' +
267
+ 'status+content 就根本不解模型面文本);T13 的 **D 层** block-content 半场本批**未**退位 —— ' +
268
+ '那一步会改 block content 形状(cli 在这里是裸对象,件1 隐患),无真实需求驱动,如实留白。',
269
+ 'DIVERGENCE-9(B5):`tool_result` 消息的 uuid —— cli 在缺 `eventId` 时铸随机 v4,本包派生 ' +
270
+ '`wtc_<toolUseId>`(DIVERGENCE-2 同族:同流重放 ⇒ 同 id 序列,[1653])。守卫按别名结构比,不比字面。',
245
271
  ];
246
272
  const TASK_NOTIFICATION_TAG = 'task-notification';
247
273
  const TASK_ID_TAG = 'task-id';
@@ -252,8 +278,10 @@ const STATUS_TAG = 'status';
252
278
  */
253
279
  class WireToCcAdapterImpl {
254
280
  renderedNotifications = new Set();
255
- notifiedRuns = new Set();
256
- cardEnqueuedRuns = new Set();
281
+ // 🔴 B5 ①:`notifiedRuns` / `cardEnqueuedRuns` **不在实例上** —— 它们的配对半场(清账的
282
+ // dropQueuedNotificationsForRun、记账的两个 enqueue、idle watcher)全在 notifications.ts 的
283
+ // module 台账上,放实例 = 前提不共享 = 双卡/零显示/模型二收(0.6.0 真缺口,[1857])。
284
+ // 适配器一律经 notifications.ts 的四个口读写,别在这里再长一份影子副本。
257
285
  /** W1 三级判据素材:本壳见过的 Workflow 工具 tool_use id(module 级台账的实例化)。 */
258
286
  seenWorkflowToolCallIds = new Set();
259
287
  /**
@@ -268,8 +296,9 @@ class WireToCcAdapterImpl {
268
296
  return {
269
297
  version: 1,
270
298
  renderedNotifications: [...this.renderedNotifications],
271
- notifiedRuns: [...this.notifiedRuns],
272
- cardEnqueuedRuns: [...this.cardEnqueuedRuns],
299
+ // B5 ①:这两项是 module 台账的快照(不是实例私有),跨实例一致。
300
+ notifiedRuns: listNotifiedRuns(),
301
+ cardEnqueuedRuns: listWorkflowCompletionCardsEnqueued(),
273
302
  };
274
303
  }
275
304
  importLedger(state) {
@@ -284,10 +313,21 @@ class WireToCcAdapterImpl {
284
313
  }
285
314
  }
286
315
  };
316
+ /** B5 ①:module 台账那两项经真源写口回植(has 判重 = 与 take 同样只数新增)。 */
317
+ const takeModule = (v, has, add) => {
318
+ if (!Array.isArray(v))
319
+ return;
320
+ for (const k of v) {
321
+ if (typeof k === 'string' && k.length > 0 && !has(k)) {
322
+ add(k);
323
+ n++;
324
+ }
325
+ }
326
+ };
287
327
  const s = state;
288
328
  take(s.renderedNotifications, this.renderedNotifications);
289
- take(s.notifiedRuns, this.notifiedRuns);
290
- take(s.cardEnqueuedRuns, this.cardEnqueuedRuns);
329
+ takeModule(s.notifiedRuns, isEngineWorkflowNotified, markEngineWorkflowNotified);
330
+ takeModule(s.cardEnqueuedRuns, isWorkflowCompletionCardEnqueued, noteWorkflowCompletionCardEnqueued);
291
331
  return n;
292
332
  }
293
333
  /**
@@ -336,7 +376,9 @@ class WireToCcAdapterImpl {
336
376
  const external = /\btype="external"/.test(attrs);
337
377
  this.renderedNotifications.add(`${external ? 'external:' : ''}${taskId}:${status}`);
338
378
  if (!external && (status === 'completed' || status === 'failed' || status === 'killed')) {
339
- this.notifiedRuns.add(taskId);
379
+ // cli seedEngineTaskNotificationDedupFromTranscript 同位:回植走 module 台账
380
+ // (outstanding 药丸 / idle watcher 收摊那半场读的就是它)。
381
+ markEngineWorkflowNotified(taskId);
340
382
  }
341
383
  seeded++;
342
384
  }
@@ -345,7 +387,9 @@ class WireToCcAdapterImpl {
345
387
  return seeded;
346
388
  }
347
389
  noteCardEnqueued(runId) {
348
- this.cardEnqueuedRuns.add(runId);
390
+ // B5 ①:写 module 台账(与 dropQueuedNotificationsForRun 的清账半场共享前提)
391
+ // 壳 B4 的队列端口回钩仍可调它 —— 三条入队链本来就写同一个集合,回钩因此变成幂等 no-op。
392
+ noteWorkflowCompletionCardEnqueued(runId);
349
393
  }
350
394
  async *adapt(frames, ctx) {
351
395
  // 下面几个 `function*` 表达式(非箭头 —— generator 不能写成箭头)要访问实例台账,
@@ -424,6 +468,11 @@ class WireToCcAdapterImpl {
424
468
  const card = taskCardBinding.get(taskId);
425
469
  return card !== undefined ? { lane: 'subagent', parentToolCallId: card } : MAIN;
426
470
  };
471
+ /**
472
+ * T32 行绑定。**typed 双键在场恒赢**(B6 定案):wire 点名了 `parentToolCallId` 时,下面的
473
+ * FIFO 臂**一次都不会被考虑** —— 连「点名的卡不合格」那一支也是直接返回 unbound,而不是
474
+ * 退回去猜。判据锚在这里:显式支的两条出口(绑定 / 拒绝)都不落进 else。
475
+ */
427
476
  const bindTaskCard = (taskId, parentToolCallId) => {
428
477
  const existing = taskCardBinding.get(taskId);
429
478
  if (existing !== undefined)
@@ -441,6 +490,20 @@ class WireToCcAdapterImpl {
441
490
  target = parentToolCallId;
442
491
  }
443
492
  else {
493
+ // ── old-engine fallback(FIFO 猜父)—— B6 P1-2 归属定案:**不删** ────────────────────
494
+ // 🔴 判据先摆正:这条臂不是「P1-2 之前的临时权宜」,它是 **tolerate-absent 的保护臂**。
495
+ // 决定后果方向的量 = 「这条子代行有没有归属」:
496
+ // · 回落在 ⇒ 老引擎(不发 parentToolCallId)上行仍绑到唯一那张开着的委派卡 ⇒ 行有归属、
497
+ // inline stats 有去处、关卡时被 settle;
498
+ // · 回落删掉 ⇒ 老引擎上行恒 unbound ⇒ 转录里那条群组行永远不长统计、卡关了也不结,
499
+ // 用户看到一条转圈到 turn 末的行。
500
+ // ⇒ **保护型缺席 = 漏保护**,与 `parked → awaiting approval` 同判(不删)。
501
+ // 猜错的风险已被两道门收窄:只考虑**未绑**且**subagent 形**的卡;≥2 张候选时上层
502
+ // 直接判含糊、保持 unbound(见 task_progress 臂的 unboundSubagentCandidates() > 1)。
503
+ // 退役条件(登记在 compensations.ts T32):P1-2 到货 **且** 达标线 ≥ server 1.290 —— 即
504
+ // 「不再支持不发 typed 双键的引擎」那一天,才是删它的日子;只要还能连老引擎就得留着。
505
+ // 覆盖:diff 门 14 条 task_progress 帧里有 6 条不带 parentToolCallId(其中 2 条走 workflow
506
+ // lane 门、不到这里),真正压这条回落臂的是 4 条(FIFO 命中 / 含糊拒绝 / settle / 跨 turn)。
444
507
  for (const [id, p] of pending) {
445
508
  if (!boundToolCalls.has(id) && SUBAGENT_TOOL_NAMES.has(p.name)) {
446
509
  target = id;
@@ -524,6 +587,79 @@ class WireToCcAdapterImpl {
524
587
  });
525
588
  }
526
589
  };
590
+ // ══ B5 —— B/D/E 层:开卡 → `tool_result` 转录消息 ═══════════════════════════════════════
591
+ /**
592
+ * TodoWrite 划删线 diff 的 `oldTodos`。
593
+ * 🔴 **turn 级**(cli 同:`let lastLiveTodos` 声明在 generator 体内,不是 module 级 ——
594
+ * cli 的注释写着「module-level, per-process」但代码不是,以**代码**为准,差分守卫认代码)。
595
+ */
596
+ let lastLiveTodos = [];
597
+ /**
598
+ * 一张开卡的收口(cli `buildToolResult` + 它内联的 #117a 桥)。产出顺序与 cli 的
599
+ * 「副作用先于 yield」一致:先 chrome(bg shell 建行),再 transcript(tool_result 卡)。
600
+ */
601
+ const buildToolResult = function* (p, end) {
602
+ const isError = end?.isError ?? false;
603
+ // D 层 / T7:wire 带正文 ⇒ 真身投影;没带 ⇒ **诚实缺席**(§8-4,不再 mock 反造)。
604
+ const body = end && end.output !== undefined
605
+ ? wireOutputToBody(p.name, end.output, isError, end.truncated === true, flattenWireOutput, end.structured)
606
+ : degradedToolResultBody(isError);
607
+ // ── E 层 ③:#117a 模型发起的后台 Bash 回执 → ctrl+B 面板行(判定在库、执行留宿主)。
608
+ if (p.name === 'Bash' && !isError && end?.output !== undefined) {
609
+ const reg = detectEngineBgShellReceipt(p.rawInput, flattenWireOutput(end.output));
610
+ if (reg) {
611
+ yield chrome({ kind: 'bgshell_register', laneProof: MAIN, taskId: reg.taskId, registration: reg });
612
+ }
613
+ }
614
+ // B 层:structured → 逐工具 typed toolUseResult(卡的 outputSchema safeParse 过才渲富卡)。
615
+ let rich = end?.structured !== undefined
616
+ ? structuredToToolUseResult(end.structured, end.output !== undefined ? flattenWireOutput(end.output) : undefined)
617
+ : null;
618
+ // E 层 ④/⑤:两条按**工具名**的补位(引擎不发 type 判别位,B 层 switch 认领不了)。
619
+ if (rich === null && p.name === 'TodoWrite') {
620
+ const todo = todoWriteToolUseResult(p.rawInput, lastLiveTodos, isError);
621
+ if (todo !== null) {
622
+ rich = { toolUseResult: todo.toolUseResult, isError: todo.isError };
623
+ lastLiveTodos = todo.newTodos;
624
+ }
625
+ }
626
+ if (rich === null && !isError && p.name === 'ReportFindings') {
627
+ rich = reportFindingsToolUseResult(end?.structured, p.rawInput);
628
+ }
629
+ const eventId = end?.eventId ?? p.eventId;
630
+ const parentToolCallId = end?.parentToolCallId ?? p.parentToolCallId;
631
+ yield transcript({
632
+ type: 'user',
633
+ message: {
634
+ role: 'user',
635
+ content: [
636
+ {
637
+ type: 'tool_result',
638
+ tool_use_id: p.id,
639
+ content: body.content,
640
+ is_error: rich?.isError === true || body.isError,
641
+ },
642
+ ],
643
+ },
644
+ // DIVERGENCE-2:cli 在缺 eventId 时铸随机 v4,本包一律**稳定键派生**
645
+ // (`wtc_<toolUseId>` —— 一张卡一个结果,天然唯一;同流重放 ⇒ 同 id 序列,[1653])。
646
+ uuid: eventId ?? deriveTranscriptId({ toolCallId: p.id }, ctx),
647
+ session_id: ctx.sessionId,
648
+ ...(parentToolCallId !== undefined ? { parent_tool_use_id: parentToolCallId } : {}),
649
+ // CC 结果卡读 sidecar 上的 typed 输出(不是 block content)。
650
+ toolUseResult: rich !== null ? rich.toolUseResult : (body.toolUseResult ?? body.content),
651
+ // T7:wire 没带正文的诚实标记 —— 在场 = 这张卡没有真实执行结果(UI 显示决策留端)。
652
+ ...(body.degraded !== undefined ? { _sema_degraded: body.degraded } : {}),
653
+ });
654
+ };
655
+ /** 关掉所有还开着的卡(流被截断 / 无 result 收尾;cli 两处同形,abort 路径**不做**)。 */
656
+ const sweepOpenCards = function* () {
657
+ for (const [id, p] of pending) {
658
+ pending.delete(id);
659
+ assistantResponseId = null; // S1
660
+ yield* buildToolResult(p);
661
+ }
662
+ };
527
663
  /** 提交累积的思考块(cli takeThinking:committed 形 + 关活体块 + 收活动行)。 */
528
664
  function* takeThinking() {
529
665
  if (thinking.length === 0)
@@ -794,15 +930,60 @@ class WireToCcAdapterImpl {
794
930
  // B4 起本臂做三件:① P0-2 label 补位(只有 tool_end 带 label 的场景;台账自身幂等不覆盖)
795
931
  // ② MF-10 **关卡 settle**(子代从不发终态 tick,父卡的 tool_end 就是完成信号;卡的 output
796
932
  // 文本 = 子代最终报告,串到行 end 事件上)③ 卡台账出栈 + response-id 复位(S1)。
797
- // tool_result 消息本体(B 层结构化卡)仍见 ADAPTER_COVERAGE.todo B5。
933
+ // 🆕 B5:④ **B/D/E 层** —— `tool_result` 卡本体 + 三个侧产物
934
+ // (Remember→memory_saved / task 族 structured 台账 / #117a bg Bash 桥)。
798
935
  const callId = typeof m.toolCallId === 'string' ? m.toolCallId : undefined;
799
936
  recordEngineToolLabel(callId, m.label);
800
937
  if (callId !== undefined) {
801
938
  yield* settlePanelTasks(callId, m.isError === true, m.output !== undefined ? flattenWireOutput(m.output) : undefined);
802
939
  }
803
- if (callId !== undefined && pending.has(callId)) {
804
- pending.delete(callId);
805
- assistantResponseId = null;
940
+ const p = callId !== undefined ? pending.get(callId) : undefined;
941
+ // 无对应开卡的 tool_end(乱序 / 重复)直接丢(cli 同)。
942
+ if (p === undefined || callId === undefined)
943
+ break;
944
+ pending.delete(callId);
945
+ // S1:结果关闭该 model response —— **yield 之前**复位(生成器会在 yield 上挂起)。
946
+ assistantResponseId = null;
947
+ const structured = m.structured;
948
+ yield* buildToolResult(p, {
949
+ output: m.output,
950
+ structured,
951
+ truncated: m.truncated === true,
952
+ isError: m.isError === true,
953
+ eventId: typeof m.eventId === 'string' ? m.eventId : undefined,
954
+ parentToolCallId: typeof m.parentToolCallId === 'string' ? m.parentToolCallId : undefined,
955
+ });
956
+ const s = structured !== undefined && structured !== null && typeof structured === 'object'
957
+ ? structured
958
+ : undefined;
959
+ // ── E 层 ①:CC198 gap #2 —— 引擎 Remember 工具 → CC198 automemory「Saved N memories」卡
960
+ // (system{subtype:'memory_saved'})。引擎不发写入路径,note 本身就是那一条 writtenPaths。
961
+ // 上面的 tool_result 照旧走泛化兜底卡(双覆盖);structuredToToolUseResult 的白名单不动
962
+ // (Remember 从不进那个 switch)。core 1.217 形 {type:'memory-saved', ok, note, …}。
963
+ if (p.name === 'Remember' && m.isError !== true) {
964
+ if (s && s.ok === true && typeof s.note === 'string' && s.note.length > 0) {
965
+ yield transcript({
966
+ type: 'system',
967
+ subtype: 'memory_saved',
968
+ writtenPaths: [s.note],
969
+ // cli 用 `new Date().toISOString()` + randomUUID();本包一律走 ctx(可重放)。
970
+ // 见 DIVERGENCE-7。
971
+ timestamp: new Date(ctx.now()).toISOString(),
972
+ uuid: idOf(m, 'memory-saved'),
973
+ isMeta: false,
974
+ });
975
+ }
976
+ }
977
+ // ── E 层 ②:core 1.217 task 族 structured —— 权威全量态精确同步 ctrl+t 面板(引擎真实 id)。
978
+ // 落盘是宿主职责(T44 拆缝:判定+归一进库、落盘留宿主)。
979
+ if (s && (s.type === 'task' || s.type === 'task-list')) {
980
+ yield chrome({
981
+ kind: 'task_ledger_sync',
982
+ laneProof: MAIN,
983
+ source: 'structured',
984
+ structuredType: s.type,
985
+ structured: s,
986
+ });
806
987
  }
807
988
  break;
808
989
  }
@@ -968,6 +1149,8 @@ class WireToCcAdapterImpl {
968
1149
  // MF-10 防御 sweep(sweep 点 1/3):结掉那些卡从没关过的行(流被截断)。
969
1150
  // cli 同位:setRetryStatus(null) 之后、开卡兜底关闭之前。
970
1151
  yield* settlePanelTasks(null, false);
1152
+ // B5:开卡兜底关闭(cli 同位 —— 在终答文本**之前**,卡才不会挂着空白)。
1153
+ yield* sweepOpenCards();
971
1154
  yield* takeThinking();
972
1155
  if (!endEmitted) {
973
1156
  const u = m.usage;
@@ -1094,13 +1277,15 @@ class WireToCcAdapterImpl {
1094
1277
  }
1095
1278
  }
1096
1279
  // 双通道反向去重:probe 合成的完成卡已入队 ⇒ 后到的推送帧只做上面的记账,不渲第二条。
1097
- if (n.task_type !== 'external' && this.cardEnqueuedRuns.has(taskId))
1280
+ // 🔴 B5 ①:读的是 **module 台账**(cli `isWorkflowCompletionCardEnqueued` 逐字)——
1281
+ // 记账方(两个 enqueue + idle watcher)与清账方(上面的 drop)都在那儿。
1282
+ if (n.task_type !== 'external' && isWorkflowCompletionCardEnqueued(taskId))
1098
1283
  break;
1099
1284
  // 跨通道记账(cli markEngineWorkflowNotified 同位、同条件:非 external,不看终态):
1100
1285
  // 这帧到达 = 引擎已 server-side steer-inject 过,补发通道(workflow_complete)不得再注入。
1101
1286
  // external 豁免([769] 对抗复审收紧:external 从不经补发通道,mark 只会缴械 probe 兜底)。
1102
1287
  if (n.task_type !== 'external')
1103
- this.notifiedRuns.add(taskId);
1288
+ markEngineWorkflowNotified(taskId);
1104
1289
  yield transcript({
1105
1290
  type: 'user',
1106
1291
  message: { role: 'user', content: [{ type: 'text', text: renderTaskNotificationXml(fields) }] },
@@ -1114,12 +1299,26 @@ class WireToCcAdapterImpl {
1114
1299
  if (runId === undefined)
1115
1300
  break;
1116
1301
  // 跨通道去重(cli enqueueEngineWorkflowNotification 首行):该 run 已由推送帧记账过
1117
- // (引擎 server-side 注入过)⇒ Path B 绝不再喂模型一遍。适配器自持,不摊给宿主。
1118
- if (this.notifiedRuns.has(runId))
1302
+ // (引擎 server-side 注入过)⇒ Path B 绝不再喂模型一遍。
1303
+ // 🔴 B5 ①:门与记账都在 module 台账 —— cli 里这一整段就是 enqueueEngineWorkflowNotification
1304
+ // 的前三行,probe/idle watcher 合成的那两条链写的也是它。
1305
+ // 🔴 B6 契约钉死(见 seam.ts 本臂注释「门归包、投递归宿主」):门 + 下面两条记号
1306
+ // (markEngineWorkflowNotified / noteWorkflowCompletionCardEnqueued)**都在库内**,
1307
+ // 宿主拿到臂只管裸入队。宿主若借自带同一道门的老函数投递 ⇒ 恒早退、一条都进不了队列。
1308
+ if (isEngineWorkflowNotified(runId))
1119
1309
  break;
1120
- this.notifiedRuns.add(runId);
1310
+ markEngineWorkflowNotified(runId);
1121
1311
  const wfStatus = m.status === 'failed' ? 'failed' : 'completed';
1122
1312
  const wfSummary = typeof m.summary === 'string' ? m.summary : '';
1313
+ // 🔴 B6 时序修(先红后绿,pure 门「早退 ⇒ cardEnqueued 仍落」):这一行原本在 `yield`
1314
+ // **之后** —— 生成器在 yield 处挂起,后面的语句只有消费者**再要下一个值**时才跑;
1315
+ // 消费者早退(turn 中止 / `it.return()`)时它**永不执行**。而同臂的
1316
+ // `markEngineWorkflowNotified` 在 yield 之前恒执行 ⇒ 两条记号一条恒落一条条件落,
1317
+ // 且方向正好是坏的那个:notified 落了 ⇒ idle watcher 不补发;cardEnqueued 没落 ⇒
1318
+ // 推送帧到达时判不出卡的所有权 ⇒ **渲第二张完成卡**(B1c 反钉②)。
1319
+ // 现存驱动全是走完整流的 `for await`,所以这条路径此前从未被断言过([1521]F1 同形)。
1320
+ // 两条记号必须同在 yield 之前落 —— 「臂已发出」这件事在 yield 那一刻就已成立。
1321
+ noteWorkflowCompletionCardEnqueued(runId);
1123
1322
  yield chrome({
1124
1323
  kind: 'workflow_notification_enqueue',
1125
1324
  laneProof: MAIN,
@@ -1129,7 +1328,6 @@ class WireToCcAdapterImpl {
1129
1328
  // 模型面正文照铸(宿主直接入队即可,不必各自拼 XML;与 core 的 tag/段序同形)
1130
1329
  message: renderTaskNotificationXml({ taskId: runId, status: wfStatus, summary: wfSummary }),
1131
1330
  });
1132
- this.cardEnqueuedRuns.add(runId);
1133
1331
  break;
1134
1332
  }
1135
1333
  case 'diagnostics': {
@@ -1245,6 +1443,8 @@ class WireToCcAdapterImpl {
1245
1443
  yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
1246
1444
  // 复审修 #2a 孪生:没有 `result` 臂的收尾路径同样 sweep(sweep 点 3/3)。
1247
1445
  yield* settlePanelTasks(null, false);
1446
+ // B5:无 `result` 收尾腿同样关掉还开着的卡(cli 同位;abort 路径**不做**,见上)。
1447
+ yield* sweepOpenCards();
1248
1448
  yield* takeThinking();
1249
1449
  yield* takeAnswerSegment();
1250
1450
  ctx.log?.('debug', `turn done · emittedAssistantText=${emittedAssistantText} answerLen=${answer.length}`);
@@ -0,0 +1,63 @@
1
+ /**
2
+ * compensations.ts — **补偿层登记表**(B6 引入,设计稿 §2 补偿表 T1–T60 的落码侧账本)。
3
+ *
4
+ * ## 为什么要有这张表
5
+ *
6
+ * 搬迁把「壳替引擎补的那 60 件事」分成三种命运:整条进包 / **拆两半**(判定归库、执行留宿主)/
7
+ * 留宿主。前两种里最危险的是**拆**:它天然是 [paired-mechanisms-must-share-premise] 的形状 ——
8
+ * 库这半以为「宿主会做另一半」,宿主那半以为「库已经处理了」,两半各自都对、组合零结果。
9
+ * 而且这种失效**不报错**,只是某个面悄悄空掉。
10
+ *
11
+ * ⇒ 与 `CHROME_ARMS` / `CLIENT_VERBS` 同一套纪律:**把义务做成数据**,让它可被断言,而不是
12
+ * 散在注释里。pure 门对本表跑三条:①id 唯一 ②每条 `split` 的 `chromeArm` 必须真在
13
+ * `CHROME_ARMS` 里 ③而且必须是 `required: true` —— 拆缝对面的义务若是「可选」,那这一刀就等于
14
+ * 在多端上静默丢行为。
15
+ *
16
+ * ## 它不是什么
17
+ *
18
+ * 🔴 **不是 `ADAPTER_DIVERGENCES`**。那张表登记的是「adapt() 与 cli `sdkMessagesToCcEvents`
19
+ * **行为不同**」的地方,差分守卫按它豁免比对。本表里的东西两侧**行为相同**(补偿是从 cli 原样
20
+ * 搬过来的),登记的是「这条补偿为什么存在、上游到货后什么时候能删」。把回落臂写进 DIVERGENCES
21
+ * 会让差分表撒谎 —— 那等于宣布两侧在这里不等价,而它们是等价的。
22
+ *
23
+ * ## 增长方式
24
+ *
25
+ * 每批只登记**这一批处置掉**的条目(B6 = P5 那八条)。不预先把 T1–T60 全填进来:填了但没落码的
26
+ * 行只会让表看起来完整而实际没有判别力([probe-must-prove-it-speaks])。
27
+ */
28
+ import { type ChromeArmKind } from './seam.js';
29
+ export interface CompensationRecord {
30
+ /** census 编号(设计稿 §2 补偿表 T1–T60)。 */
31
+ id: string;
32
+ /** 它补的是什么(一句话;完整说明在落码处的注释里,那里是真源)。 */
33
+ what: string;
34
+ /** `in-library` = 整条进包;`split` = 判定归库、执行留宿主。 */
35
+ disposition: 'in-library' | 'split';
36
+ /** 库这一半落在哪(模块 / 函数)。 */
37
+ libraryHalf: string;
38
+ /** 拆缝对面的宿主义务经由哪个 chrome 臂交付(`in-library` 恒 null)。 */
39
+ chromeArm: ChromeArmKind | null;
40
+ /** 宿主那一半要做什么(`in-library` 恒 null)。 */
41
+ hostHalf: string | null;
42
+ /**
43
+ * 退休条件 —— 上游到货后这条补偿可以删或简化;`null` = 它不是补偿而是**长期职责**
44
+ * (删了就是丢功能,不是清债)。
45
+ */
46
+ retireOn: string | null;
47
+ }
48
+ /**
49
+ * B6 P5 处置的八条(T32/T34/T47/T48/T49 整条进包 · T35/T36/T44 拆)。
50
+ *
51
+ * 🔴 事实核对(B6 余项批实测,不是照抄设计稿):这八条的落码**都发生在 B4**
52
+ * (`adapt.ts` A 层第 14 臂 `task_progress` 收官那一批),本批做的是**核实 + 登记 + 定案**,
53
+ * 不是重新搬一遍。核实方式 = 逐条在 `adapt.ts` 找到落码点并配上断言,见 pure 门 B6 段。
54
+ */
55
+ export declare const MIGRATED_COMPENSATIONS: readonly CompensationRecord[];
56
+ /**
57
+ * 自检:每条 `split` 的宿主义务(若经 chrome 臂交付)必须真在 `CHROME_ARMS` 里,而且是
58
+ * `required: true`。返回违规条目;**恒应为空数组**。
59
+ *
60
+ * 🔴 判据锚在「决定结果的量」上:拆缝对面若挂在一个 `required:false` 的臂上,宿主可以合法不实现,
61
+ * 于是这一刀就是「库不做了、端也不做」——正是配对机制两半各自都对、组合零结果的那个形状。
62
+ */
63
+ export declare function compensationSplitViolations(): string[];