@sema-agent/client-core 0.7.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 (47) hide show
  1. package/README.md +31 -11
  2. package/dist/adapt.js +36 -3
  3. package/dist/compensations.d.ts +63 -0
  4. package/dist/compensations.js +138 -0
  5. package/dist/detachWire.d.ts +141 -0
  6. package/dist/detachWire.js +182 -0
  7. package/dist/engineSessionParam.d.ts +6 -2
  8. package/dist/engineSessionParam.js +24 -18
  9. package/dist/engineWireTarget.d.ts +14 -0
  10. package/dist/engineWireTarget.js +53 -0
  11. package/dist/fleet/fleetLedger.d.ts +132 -0
  12. package/dist/fleet/fleetLedger.js +393 -0
  13. package/dist/fleet/fleetProjection.d.ts +181 -0
  14. package/dist/fleet/fleetProjection.js +258 -0
  15. package/dist/hooksWireCaps.d.ts +91 -0
  16. package/dist/hooksWireCaps.js +359 -0
  17. package/dist/host.d.ts +34 -2
  18. package/dist/host.js +6 -0
  19. package/dist/index.d.ts +56 -1
  20. package/dist/index.js +82 -1
  21. package/dist/liveInitToolFace.d.ts +53 -0
  22. package/dist/liveInitToolFace.js +251 -0
  23. package/dist/model/modelFamilies.json +63 -0
  24. package/dist/model/providerPresets.d.ts +46 -0
  25. package/dist/model/providerPresets.js +160 -0
  26. package/dist/model/providerPresets.json +1179 -0
  27. package/dist/seam.d.ts +19 -3
  28. package/dist/seam.js +14 -2
  29. package/dist/subagent/engineCompactWire.d.ts +12 -0
  30. package/dist/subagent/engineCompactWire.js +176 -0
  31. package/dist/subagent/engineDelegatedPrompt.d.ts +44 -0
  32. package/dist/subagent/engineDelegatedPrompt.js +205 -0
  33. package/dist/subagent/engineRowStopGate.d.ts +18 -0
  34. package/dist/subagent/engineRowStopGate.js +54 -0
  35. package/dist/subagent/engineSubagentOutput.d.ts +15 -0
  36. package/dist/subagent/engineSubagentOutput.js +98 -0
  37. package/dist/subagent/engineSubagentSteer.d.ts +10 -0
  38. package/dist/subagent/engineSubagentSteer.js +68 -0
  39. package/dist/subagent/engineSubagentTail.d.ts +40 -0
  40. package/dist/subagent/engineSubagentTail.js +241 -0
  41. package/dist/subagent/engineTaskHandleWire.d.ts +90 -0
  42. package/dist/subagent/engineTaskHandleWire.js +212 -0
  43. package/dist/workflowClient.d.ts +42 -0
  44. package/dist/workflowClient.js +467 -0
  45. package/dist/workflowMonitor.d.ts +94 -0
  46. package/dist/workflowMonitor.js +43 -0
  47. package/package.json +2 -2
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.js CHANGED
@@ -255,8 +255,11 @@ export const ADAPTER_DIVERGENCES = [
255
255
  'DIVERGENCE-7(B5,§8-4 裁定的**有意**分歧):`tool_end.output === undefined` 时 —— cli 用 mock ' +
256
256
  'toolIO 注册表**反造**结果体(卡片显示「看起来像真的、但不是这次执行产物」的内容);本包**诚实缺席**:' +
257
257
  '`content: \'\'` + 消息上打 `_sema_degraded: \'wire_carried_no_output\'`,显示决策留端。' +
258
- '依据=宪法「诚实优先于产出」+ 多端复制会各自编不同的假话。差分守卫按「哨兵成对替换 + 独立 oracle」' +
259
- '比对(标记集必须恰等于「有 tool_end 但没带 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)依然成立。',
260
263
  'DIVERGENCE-8(B5):**structured 在场时正则反解退位**(T11/T14;[1840]§一 白名单 + 设计稿 R9②)。' +
261
264
  '同一帧上 cli 从模型面框架文本反解 {stdout,stderr,exitCode},本包直接读 `structured.bash.*`。' +
262
265
  '框架**自洽**时两者逐字节相等(差分守卫 6 条 bash 双路 fixture 实证);不自洽时本包跟 structured ' +
@@ -465,6 +468,11 @@ class WireToCcAdapterImpl {
465
468
  const card = taskCardBinding.get(taskId);
466
469
  return card !== undefined ? { lane: 'subagent', parentToolCallId: card } : MAIN;
467
470
  };
471
+ /**
472
+ * T32 行绑定。**typed 双键在场恒赢**(B6 定案):wire 点名了 `parentToolCallId` 时,下面的
473
+ * FIFO 臂**一次都不会被考虑** —— 连「点名的卡不合格」那一支也是直接返回 unbound,而不是
474
+ * 退回去猜。判据锚在这里:显式支的两条出口(绑定 / 拒绝)都不落进 else。
475
+ */
468
476
  const bindTaskCard = (taskId, parentToolCallId) => {
469
477
  const existing = taskCardBinding.get(taskId);
470
478
  if (existing !== undefined)
@@ -482,6 +490,20 @@ class WireToCcAdapterImpl {
482
490
  target = parentToolCallId;
483
491
  }
484
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)。
485
507
  for (const [id, p] of pending) {
486
508
  if (!boundToolCalls.has(id) && SUBAGENT_TOOL_NAMES.has(p.name)) {
487
509
  target = id;
@@ -1280,11 +1302,23 @@ class WireToCcAdapterImpl {
1280
1302
  // (引擎 server-side 注入过)⇒ Path B 绝不再喂模型一遍。
1281
1303
  // 🔴 B5 ①:门与记账都在 module 台账 —— cli 里这一整段就是 enqueueEngineWorkflowNotification
1282
1304
  // 的前三行,probe/idle watcher 合成的那两条链写的也是它。
1305
+ // 🔴 B6 契约钉死(见 seam.ts 本臂注释「门归包、投递归宿主」):门 + 下面两条记号
1306
+ // (markEngineWorkflowNotified / noteWorkflowCompletionCardEnqueued)**都在库内**,
1307
+ // 宿主拿到臂只管裸入队。宿主若借自带同一道门的老函数投递 ⇒ 恒早退、一条都进不了队列。
1283
1308
  if (isEngineWorkflowNotified(runId))
1284
1309
  break;
1285
1310
  markEngineWorkflowNotified(runId);
1286
1311
  const wfStatus = m.status === 'failed' ? 'failed' : 'completed';
1287
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);
1288
1322
  yield chrome({
1289
1323
  kind: 'workflow_notification_enqueue',
1290
1324
  laneProof: MAIN,
@@ -1294,7 +1328,6 @@ class WireToCcAdapterImpl {
1294
1328
  // 模型面正文照铸(宿主直接入队即可,不必各自拼 XML;与 core 的 tag/段序同形)
1295
1329
  message: renderTaskNotificationXml({ taskId: runId, status: wfStatus, summary: wfSummary }),
1296
1330
  });
1297
- noteWorkflowCompletionCardEnqueued(runId);
1298
1331
  break;
1299
1332
  }
1300
1333
  case 'diagnostics': {
@@ -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[];
@@ -0,0 +1,138 @@
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 { CHROME_ARMS } from './seam.js';
29
+ /**
30
+ * B6 P5 处置的八条(T32/T34/T47/T48/T49 整条进包 · T35/T36/T44 拆)。
31
+ *
32
+ * 🔴 事实核对(B6 余项批实测,不是照抄设计稿):这八条的落码**都发生在 B4**
33
+ * (`adapt.ts` A 层第 14 臂 `task_progress` 收官那一批),本批做的是**核实 + 登记 + 定案**,
34
+ * 不是重新搬一遍。核实方式 = 逐条在 `adapt.ts` 找到落码点并配上断言,见 pure 门 B6 段。
35
+ */
36
+ export const MIGRATED_COMPENSATIONS = [
37
+ {
38
+ id: 'T32',
39
+ what: 'bindTaskCard —— 把引擎子代行绑到它的委派工具卡上(显式父 / FIFO 猜父 / 含糊即 unbound)',
40
+ disposition: 'in-library',
41
+ libraryHalf: 'adapt.ts bindTaskCard()',
42
+ chromeArm: null,
43
+ hostHalf: null,
44
+ // 见下方 P1-2 定案段:FIFO 那一臂**不删**,它是 tolerate-absent 的保护臂。
45
+ retireOn: 'P1-2 到货且达标线 ≥ server 1.290(typed parentToolCallId 在每条 tick 上恒在)后,' +
46
+ 'FIFO 回落臂可删 —— 但只在**确认不再支持老引擎**时删,见 bindTaskCard 的 old-engine fallback 注释',
47
+ },
48
+ {
49
+ id: 'T34',
50
+ what: '面板行 live / inert / unbound 三态 —— 只 sweep 本 turn 真活绑的行',
51
+ disposition: 'in-library',
52
+ libraryHalf: 'adapt.ts liveBoundPanelTasks + settlePanelTasks(cardId===null) 防御 sweep',
53
+ chromeArm: null,
54
+ hostHalf: null,
55
+ retireOn: null, // 长期职责:只要子代不发终态 tick,这个三态就得有
56
+ },
57
+ {
58
+ id: 'T35',
59
+ what: '壳替引擎 fire SubagentStart/Stop hook —— 双进程下 vendor 的 AgentTool/runAgent 永不跑',
60
+ disposition: 'split',
61
+ libraryHalf: 'adapt.ts fireSubagentStart()(类型恒记 + 一生一次去重 + if-started 门)',
62
+ chromeArm: 'subagent_lifecycle',
63
+ hostHalf: '真执行用户 hook 进程(浏览器宿主根本不能执行 ⇒ 这一半必须留端)',
64
+ retireOn: 'G21 / P3a(引擎侧发正式的 subagent 生命周期事件)后,壳替 fire 整条退役',
65
+ },
66
+ {
67
+ id: 'T36',
68
+ what: '子代永不发终态 tick ⇒ 四处防御 sweep 兜住面板行,不让它们永远转圈',
69
+ disposition: 'in-library',
70
+ libraryHalf: 'adapt.ts settlePanelTasks 的四个调用点(tool_end 关卡 / result / turn 末 / abort)',
71
+ chromeArm: 'inline_task_stats',
72
+ hostHalf: null,
73
+ retireOn: 'W6(引擎为每条子代发终态 tick)后,防御 sweep 降为纯兜底',
74
+ },
75
+ {
76
+ id: 'T44',
77
+ what: '引擎 TaskCreate/TaskUpdate → ctrl+t 任务面板的磁盘 task store',
78
+ disposition: 'split',
79
+ libraryHalf: 'adapt.ts E 层 ② —— structured task/task-list 的判定 + 归一',
80
+ chromeArm: 'task_ledger_sync',
81
+ hostHalf: '按 toolUseId 幂等落盘(cli engineTaskListSync 的 fs 面;web 宿主落 BFF/本地存储)',
82
+ retireOn: null, // 长期职责:落盘位置本来就是端决策
83
+ },
84
+ {
85
+ id: 'T47',
86
+ what: 'workflow lane 三级判据门 —— workflow 子代的 tick 绝不进前台 subagent lane',
87
+ disposition: 'in-library',
88
+ libraryHalf: 'adapt.ts task_progress 臂的 workflowRunId / wa-id / workflow-card-parent 三级门',
89
+ chromeArm: null,
90
+ hostHalf: null,
91
+ retireOn: 'P1-2 到货后判据②③(形状嗅探 + 实例台账)可删,只留判据①(帧带 workflowRunId)',
92
+ },
93
+ {
94
+ id: 'T48',
95
+ what: '判据④ 回植 footerRowBelt —— fleet 双生行按尾段查台账,不再进 footer task 树',
96
+ disposition: 'split',
97
+ libraryHalf: 'workflow.ts recordWorkflowAgentTaskId()(写)+ isKnownWorkflowAgentTaskId()(读)',
98
+ chromeArm: null,
99
+ hostHalf: 'footer 行过滤 isFooterTaskRow/filterFooterTaskRows —— 含「丢弃所有无 parentId 顶层行」' +
100
+ '这条**端专属决策**(主 run 头部行由端自己合成渲染),留 TUI',
101
+ // 🔴 这条的写者与读者**都在库内**(设计稿 §2.4.1-B 已裁):做成 chrome 臂会让「宿主没接线 ⇒
102
+ // Set 恒空 ⇒ 判据④ 静默失效」复活,所以 chromeArm 恒 null,拆的是**过滤策略**不是台账。
103
+ retireOn: 'P1-2 到货后 fleet 行自带 parentToolCallId ⇒ 双生行不需要按尾段查',
104
+ },
105
+ {
106
+ id: 'T49',
107
+ what: 'subagent alias 只能在 tick 上学 —— content 帧只带 parentToolCallId、没有 taskId',
108
+ disposition: 'in-library',
109
+ libraryHalf: 'adapt.ts task_progress 臂 registerSubagentAlias(explicitParent, taskId)',
110
+ chromeArm: null,
111
+ hostHalf: null,
112
+ retireOn: 'P1-2 到货后 content 帧自带 taskId ⇒ alias 学习整条可删',
113
+ },
114
+ ];
115
+ /**
116
+ * 自检:每条 `split` 的宿主义务(若经 chrome 臂交付)必须真在 `CHROME_ARMS` 里,而且是
117
+ * `required: true`。返回违规条目;**恒应为空数组**。
118
+ *
119
+ * 🔴 判据锚在「决定结果的量」上:拆缝对面若挂在一个 `required:false` 的臂上,宿主可以合法不实现,
120
+ * 于是这一刀就是「库不做了、端也不做」——正是配对机制两半各自都对、组合零结果的那个形状。
121
+ */
122
+ export function compensationSplitViolations() {
123
+ const byKind = new Map(CHROME_ARMS.map(a => [a.kind, a]));
124
+ const bad = [];
125
+ for (const c of MIGRATED_COMPENSATIONS) {
126
+ if (c.chromeArm === null)
127
+ continue;
128
+ const arm = byKind.get(c.chromeArm);
129
+ if (arm === undefined) {
130
+ bad.push(`${c.id}: chrome 臂 "${c.chromeArm}" 不在 CHROME_ARMS 里(拆缝对面没有交付口)`);
131
+ continue;
132
+ }
133
+ if (!arm.required) {
134
+ bad.push(`${c.id}: chrome 臂 "${c.chromeArm}" 是 required:false —— 拆缝对面的义务不许是可选的`);
135
+ }
136
+ }
137
+ return bad;
138
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):cli `src/sema/detachWire.ts` **拆**搬 —— 判定半场 +
3
+ * cancel-arm 台账进包,**信号路径的裸 fetch 发射留宿主**(设计稿 §3 表 `S/detachWire.ts` 那条
4
+ * 脚注:`:221` 裸 cancel = TUI 留,宪法例外 census C1-4)。
5
+ *
6
+ * 🔴 **拆缝画在哪、为什么**:`fireBestEffortDetachCancel` 是**信号处理器**里同步 DISPATCH 的
7
+ * fire-and-forget —— 它成立的前提是「进程正在被 SIGINT/SIGTERM 拆掉,不能 await、不能构造重对象」。
8
+ * 这套前提是 **Node 进程模型**的,浏览器/桌面渲染进程根本没有信号;而且它有意**绕开 SDK verb**
9
+ * (`AgentClient.runs.cancel` 是 await 型,包一层 void+catch 反而把纪律面弄模糊)——那是壳的
10
+ * 宪法例外,不该被搬成「库里有一处裸 fetch」。⇒ 发射留宿主,库出**台账 + 读口**。
11
+ *
12
+ * 🔴 **模块级状态与读写口同包**([multiclient-migration-batch-discipline]):`cancelArm` 一份,
13
+ * 写口 `armDetachCancel`(seamQueryEngine.ask 在头真拼上请求时调)、读口 `isDetachArmed`
14
+ * (`detachDurableOffHint` 的第一道门)/ `detachedTaskId`(分诊文案)/ `detachCancelArm`
15
+ * (**宿主发射腿**唯一取件口)全在本文件。壳侧 shim 的 `fireBestEffortDetachCancel` 从
16
+ * `detachCancelArm()` 取件 ⇒ 仍然只有一份台账;壳若自己再存一份 = 头发了但 hint 判不出、
17
+ * Ctrl+C 取消不掉(两个静默失效各一半)。
18
+ *
19
+ * 🔴 env 读取走 `hostEnv()`(本包零 `process`,portability 门盯着);Node 下同一对象 ⇒ 壳侧
20
+ * 「不传 env 参数」的调用点行为一字节不变。
21
+ *
22
+ * ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
23
+ *
24
+ * detachWire — headless `-p` detach-on-disconnect wire (重连腿 M1 壳半场, 2026-07-15,
25
+ * design/HEADLESS-RECONNECT-DESIGN.md §6 M1; mirrors finalVerifyWire/limitsWire in shape).
26
+ *
27
+ * WHY: the `/v1/tasks/stream` lane ABORTS the run on client disconnect by design (anti token-burn —
28
+ * this is also the interactive cancel mechanism). server ≥1.196 accepts an opt-in escape hatch: the
29
+ * `x-detach-on-disconnect: true` REQUEST HEADER makes a disconnect DETACH the run instead — it keeps
30
+ * running to its terminal and lands in the durable run ledger (`GET /v1/runs/:id`; a deployment with
31
+ * no durable ledger 400s fail-loud — the shell's self-spawned engine always sets DB_BACKEND=local,
32
+ * so the ledger is always there on that lane). M1 = header + cancel fallback ONLY; reconnect/resume
33
+ * (R1 runs.events lane-switch / R2 idempotent re-POST) is M2/M3 — NOT here.
34
+ *
35
+ * 铁律 1 (design §2.1): the header is stamped ONLY on the headless `-p` lane (seamQueryEngine.ask),
36
+ * live-gated on SEMA_LIVE_BASEURL exactly like finalVerifyWire — the interactive REPL lane
37
+ * (seamQuery.ts) never imports this module, so Esc/interrupt keeps its disconnect==cancel semantics
38
+ * verbatim, and the mock/pty lanes never see the header (offline projection byte-identical).
39
+ *
40
+ * 铁律 2 (design §2.2): once detach is on, a USER abort must carry an explicit best-effort cancel —
41
+ * a detached run would otherwise burn tokens to completion in the background after Ctrl+C. The
42
+ * cancel-arm half of this module (armDetachCancel / the host's fireBestEffortDetachCancel) is
43
+ * consumed by the `-p` SIGINT handler and the SIGTERM/SIGHUP tail-frame handler (cli/print.ts):
44
+ * fire-and-forget `POST /v1/runs/:id/cancel` (the SDK RunsResource.cancel verb, runs.js — verified:
45
+ * no body), never awaited on the signal path. taskId not yet captured (pre X-Task-Id window) ⇒
46
+ * ACCEPTED uncancellable window — detach's inherent cost, no waiting introduced (design §2.2 记账).
47
+ *
48
+ * PRECEDENCE (off wins; otherwise DEFAULT ON):
49
+ * a) `SEMA_HEADLESS_DETACH` env = 'false'/'0' (settings env lane, fail-soft — any other value
50
+ * including typos stays default-on, finalVerifyWire discipline) → off.
51
+ * b) VERSION GATE: SEMA_ENGINE_URL in play (user-supplied external engine) and `/health` CONFIRMS
52
+ * version <1.196.0 (or no version field = pre-1.96) → off + SEMA_DEBUG log.
53
+ * Unlike limitsWire (warn-and-send: an old server drops an unknown BODY field server-side), here
54
+ * we GATE: an old server silently ignores the unknown HEADER, so a disconnect still kills the
55
+ * run — sending it would only manufacture a false "detached" belief downstream (判死文案 would
56
+ * lie). Probe unreachable / unparsable response ⇒ fail-soft SEND (can't confirm staleness; the
57
+ * stream POST itself fails loudly if the engine is truly down).
58
+ * c) self-spawn lane (no SEMA_ENGINE_URL): release-pinned ≥1.197.0 — trusted, no probe, header on.
59
+ */
60
+ import { type EnvLike } from './hostEnv.js';
61
+ /** The server ≥1.196 opt-in header: disconnect ⇒ detach (run continues to durable terminal). */
62
+ export declare const DETACH_HEADER = "x-detach-on-disconnect";
63
+ /** Settings-lane env knob(settings.json `env` 块 → 1a-envseed → process.env)。'false'/'0' = off。 */
64
+ export declare const HEADLESS_DETACH_ENV = "SEMA_HEADLESS_DETACH";
65
+ /**
66
+ * First server version that honors the detach header — 1.196.0, the SAME floor as the limits wire
67
+ * (both landed in the [857] batch), so the version predicate is limitsWire.versionSupportsLimits
68
+ * re-exported under the detach name (one comparator, two wires — 抽共用 per design).
69
+ */
70
+ export declare const DETACH_WIRE_MIN_ENGINE: readonly [number, number, number];
71
+ /** True when the engine version string satisfies the detach-wire minimum (≥1.196.0). Unparsable ⇒ false. */
72
+ export declare const versionSupportsDetach: (v: string | undefined) => boolean;
73
+ /**
74
+ * The settings-lane off-switch: `SEMA_HEADLESS_DETACH` = 'false' or '0' (case-insensitive) disables
75
+ * the header. Any other value (empty/'true'/typo) = default ON — fail-soft, a settings typo never
76
+ * flips the lane silently to the strict-alignment (disconnect-kills-run) mode (finalVerifyWire 纪律).
77
+ */
78
+ export declare function headlessDetachDisabledByEnv(env?: EnvLike): boolean;
79
+ /**
80
+ * Version-gate probe: GET `${baseUrl}/health`, read `version`. Returns FALSE only when the engine is
81
+ * CONFIRMED older than 1.196.0 (parsable old version, or a healthy response with no version field =
82
+ * pre-1.96). Unreachable / timeout / non-2xx / unparsable JSON ⇒ TRUE (fail-soft: can't confirm
83
+ * staleness — the header is harmless on the wire, and the stream POST fails loudly on a dead engine).
84
+ * Same probe shape as limitsWire.engineLimitsSupportWarning (1.5s budget, never throws).
85
+ */
86
+ export declare function engineSupportsDetach(baseUrl: string, opts?: {
87
+ fetchImpl?: typeof fetch;
88
+ timeoutMs?: number;
89
+ }): Promise<boolean>;
90
+ export type DetachResolution = {
91
+ on: true;
92
+ reason: 'default-on';
93
+ } | {
94
+ on: false;
95
+ reason: 'env-off' | 'old-engine';
96
+ };
97
+ /**
98
+ * Resolve the detach header for a headless `-p` submit. DEFAULT ON; `SEMA_HEADLESS_DETACH`=false|0
99
+ * ⇒ off (env-off); SEMA_ENGINE_URL external engine confirmed <1.196.0 ⇒ off (old-engine — the caller
100
+ * SEMA_DEBUG-logs it). Self-spawn lane (no SEMA_ENGINE_URL) never probes (pin ≥1.197 trusted).
101
+ * The caller MUST live-gate on SEMA_LIVE_BASEURL (finalVerifyWire 同款) — mock/pty lanes never
102
+ * reach this resolver, so they never see the header.
103
+ */
104
+ export declare function resolveHeadlessDetach(baseUrl: string, env?: EnvLike, opts?: {
105
+ fetchImpl?: typeof fetch;
106
+ timeoutMs?: number;
107
+ }): Promise<DetachResolution>;
108
+ /** server 400 body 判别锚(server.ts sendJson 原文:"x-detach-on-disconnect requires a durable run
109
+ * (a run store + sessionId) — without one the detached result would be unqueryable")。 */
110
+ export declare const DETACH_DURABLE_OFF_400_ANCHOR = "x-detach-on-disconnect requires a durable run";
111
+ /**
112
+ * detach 头引发的 durable-off 400 → 人话提示行;其它错误(非 400 / 非该 body / 头没发过)恒 null
113
+ * (绝不误伤其它 400 —— sandboxImageProfile 400 等各有自己的 hint)。判别三重:头确已发出
114
+ * (isDetachArmed —— armDetachCancel 只在头拼上请求时调)+ SDK APIError.status===400 + message 含
115
+ * server 原文锚(sdk errors.js toApiError: msg = body.error 原文)。
116
+ */
117
+ export declare function detachDurableOffHint(err: unknown): string | null;
118
+ export interface DetachCancelArm {
119
+ baseUrl: string;
120
+ /** 真 token 串,或 loopback 显式免鉴权(#118:cancel 兜底同样零 Authorization 头,绝不伪造 'anon')。 */
121
+ authToken: string | {
122
+ mode: 'loopback-unauthed';
123
+ };
124
+ principal: string;
125
+ /** Reads the durable taskId early-captured off the X-Task-Id response header; null before capture. */
126
+ getTaskId: () => string | null;
127
+ }
128
+ /** Arm the signal-path cancel fallback (called by ask when the detach header is actually sent). */
129
+ export declare function armDetachCancel(arm: DetachCancelArm): void;
130
+ /** Whether this process sent the detach header on its `-p` stream (the 判死文案 upgrade gate). */
131
+ export declare function isDetachArmed(): boolean;
132
+ /** The detached run's durable taskId, when already captured (X-Task-Id) — for the triage copy. */
133
+ export declare function detachedTaskId(): string | null;
134
+ /**
135
+ * 🔴 **宿主发射腿的唯一取件口**(拆缝的库这一侧)。宿主的 `fireBestEffortDetachCancel` 必须从这里
136
+ * 取 arm,**不许自己再存一份** —— 台账两份 = 头发了但 hint 判不出 / Ctrl+C 取消不掉。
137
+ * null = 没 arm 过(detach 关 / 版本门挡下 / mock 车道)⇒ 宿主零动作、零行为变化。
138
+ */
139
+ export declare function detachCancelArm(): DetachCancelArm | null;
140
+ /** Test-only reset (module state is process-lifetime; `-p` is a one-shot). */
141
+ export declare function _resetDetachWireForTest(): void;