@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.
- package/README.md +31 -11
- package/dist/adapt.d.ts +4 -2
- package/dist/adapt.js +225 -25
- package/dist/compensations.d.ts +63 -0
- package/dist/compensations.js +138 -0
- package/dist/detachWire.d.ts +141 -0
- package/dist/detachWire.js +182 -0
- package/dist/diff/patch.d.ts +29 -0
- package/dist/diff/patch.js +45 -0
- package/dist/engineSessionParam.d.ts +11 -0
- package/dist/engineSessionParam.js +41 -0
- package/dist/engineWireTarget.d.ts +14 -0
- package/dist/engineWireTarget.js +53 -0
- package/dist/finalVerifyWire.d.ts +74 -0
- package/dist/finalVerifyWire.js +63 -0
- package/dist/fleet/fleetLedger.d.ts +132 -0
- package/dist/fleet/fleetLedger.js +393 -0
- package/dist/fleet/fleetProjection.d.ts +181 -0
- package/dist/fleet/fleetProjection.js +258 -0
- package/dist/headlessPermissionModeWire.d.ts +55 -0
- package/dist/headlessPermissionModeWire.js +111 -0
- package/dist/headlessReconnectWire.d.ts +96 -0
- package/dist/headlessReconnectWire.js +141 -0
- package/dist/hooksWireCaps.d.ts +91 -0
- package/dist/hooksWireCaps.js +359 -0
- package/dist/host.d.ts +34 -2
- package/dist/host.js +6 -0
- package/dist/index.d.ts +74 -1
- package/dist/index.js +104 -1
- package/dist/interactiveToolsWire.d.ts +48 -0
- package/dist/interactiveToolsWire.js +86 -0
- package/dist/limitsWire.d.ts +89 -0
- package/dist/limitsWire.js +225 -0
- package/dist/liveInitToolFace.d.ts +53 -0
- package/dist/liveInitToolFace.js +251 -0
- package/dist/model/modelFamilies.json +63 -0
- package/dist/model/providerPresets.d.ts +46 -0
- package/dist/model/providerPresets.js +160 -0
- package/dist/model/providerPresets.json +1179 -0
- package/dist/notifications.d.ts +7 -0
- package/dist/notifications.js +32 -0
- package/dist/sandboxWire.d.ts +75 -0
- package/dist/sandboxWire.js +138 -0
- package/dist/scenarioWire.d.ts +62 -0
- package/dist/scenarioWire.js +115 -0
- package/dist/seam.d.ts +52 -5
- package/dist/seam.js +15 -2
- package/dist/subagent/engineCompactWire.d.ts +12 -0
- package/dist/subagent/engineCompactWire.js +176 -0
- package/dist/subagent/engineDelegatedPrompt.d.ts +44 -0
- package/dist/subagent/engineDelegatedPrompt.js +205 -0
- package/dist/subagent/engineRowStopGate.d.ts +18 -0
- package/dist/subagent/engineRowStopGate.js +54 -0
- package/dist/subagent/engineSubagentOutput.d.ts +15 -0
- package/dist/subagent/engineSubagentOutput.js +98 -0
- package/dist/subagent/engineSubagentSteer.d.ts +10 -0
- package/dist/subagent/engineSubagentSteer.js +68 -0
- package/dist/subagent/engineSubagentTail.d.ts +40 -0
- package/dist/subagent/engineSubagentTail.js +241 -0
- package/dist/subagent/engineTaskHandleWire.d.ts +90 -0
- package/dist/subagent/engineTaskHandleWire.js +212 -0
- package/dist/toolResult.d.ts +118 -0
- package/dist/toolResult.js +774 -0
- package/dist/workflowClient.d.ts +42 -0
- package/dist/workflowClient.js +467 -0
- package/dist/workflowMonitor.d.ts +94 -0
- package/dist/workflowMonitor.js +43 -0
- package/package.json +5 -3
|
@@ -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;
|
|
@@ -0,0 +1,182 @@
|
|
|
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 { hostEnv } from './hostEnv.js';
|
|
61
|
+
import { versionSupportsLimits } from './limitsWire.js';
|
|
62
|
+
/** The server ≥1.196 opt-in header: disconnect ⇒ detach (run continues to durable terminal). */
|
|
63
|
+
export const DETACH_HEADER = 'x-detach-on-disconnect';
|
|
64
|
+
/** Settings-lane env knob(settings.json `env` 块 → 1a-envseed → process.env)。'false'/'0' = off。 */
|
|
65
|
+
export const HEADLESS_DETACH_ENV = 'SEMA_HEADLESS_DETACH';
|
|
66
|
+
/**
|
|
67
|
+
* First server version that honors the detach header — 1.196.0, the SAME floor as the limits wire
|
|
68
|
+
* (both landed in the [857] batch), so the version predicate is limitsWire.versionSupportsLimits
|
|
69
|
+
* re-exported under the detach name (one comparator, two wires — 抽共用 per design).
|
|
70
|
+
*/
|
|
71
|
+
export const DETACH_WIRE_MIN_ENGINE = [1, 196, 0];
|
|
72
|
+
/** True when the engine version string satisfies the detach-wire minimum (≥1.196.0). Unparsable ⇒ false. */
|
|
73
|
+
export const versionSupportsDetach = versionSupportsLimits;
|
|
74
|
+
/**
|
|
75
|
+
* The settings-lane off-switch: `SEMA_HEADLESS_DETACH` = 'false' or '0' (case-insensitive) disables
|
|
76
|
+
* the header. Any other value (empty/'true'/typo) = default ON — fail-soft, a settings typo never
|
|
77
|
+
* flips the lane silently to the strict-alignment (disconnect-kills-run) mode (finalVerifyWire 纪律).
|
|
78
|
+
*/
|
|
79
|
+
export function headlessDetachDisabledByEnv(env = hostEnv()) {
|
|
80
|
+
const v = env[HEADLESS_DETACH_ENV]?.trim().toLowerCase();
|
|
81
|
+
return v === 'false' || v === '0';
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Version-gate probe: GET `${baseUrl}/health`, read `version`. Returns FALSE only when the engine is
|
|
85
|
+
* CONFIRMED older than 1.196.0 (parsable old version, or a healthy response with no version field =
|
|
86
|
+
* pre-1.96). Unreachable / timeout / non-2xx / unparsable JSON ⇒ TRUE (fail-soft: can't confirm
|
|
87
|
+
* staleness — the header is harmless on the wire, and the stream POST fails loudly on a dead engine).
|
|
88
|
+
* Same probe shape as limitsWire.engineLimitsSupportWarning (1.5s budget, never throws).
|
|
89
|
+
*/
|
|
90
|
+
export async function engineSupportsDetach(baseUrl, opts) {
|
|
91
|
+
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
const timer = setTimeout(() => controller.abort(), opts?.timeoutMs ?? 1500);
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetchImpl(`${baseUrl.replace(/\/+$/, '')}/health`, {
|
|
96
|
+
method: 'GET',
|
|
97
|
+
signal: controller.signal,
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok)
|
|
100
|
+
return true; // engine answered oddly — can't confirm staleness, fail-soft send
|
|
101
|
+
const body = (await res.json());
|
|
102
|
+
const version = typeof body?.version === 'string' ? body.version : undefined;
|
|
103
|
+
// No version field = pre-1.96 engine — definitely older than 1.196.0: gate the header.
|
|
104
|
+
return versionSupportsDetach(version);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return true; // unreachable/timeout — cannot confirm staleness, fail-soft send
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the detach header for a headless `-p` submit. DEFAULT ON; `SEMA_HEADLESS_DETACH`=false|0
|
|
115
|
+
* ⇒ off (env-off); SEMA_ENGINE_URL external engine confirmed <1.196.0 ⇒ off (old-engine — the caller
|
|
116
|
+
* SEMA_DEBUG-logs it). Self-spawn lane (no SEMA_ENGINE_URL) never probes (pin ≥1.197 trusted).
|
|
117
|
+
* The caller MUST live-gate on SEMA_LIVE_BASEURL (finalVerifyWire 同款) — mock/pty lanes never
|
|
118
|
+
* reach this resolver, so they never see the header.
|
|
119
|
+
*/
|
|
120
|
+
export async function resolveHeadlessDetach(baseUrl, env = hostEnv(), opts) {
|
|
121
|
+
if (headlessDetachDisabledByEnv(env))
|
|
122
|
+
return { on: false, reason: 'env-off' };
|
|
123
|
+
if (env.SEMA_ENGINE_URL) {
|
|
124
|
+
// user-supplied external engine — the only lane where an old server is possible
|
|
125
|
+
const ok = await engineSupportsDetach(baseUrl, opts);
|
|
126
|
+
if (!ok)
|
|
127
|
+
return { on: false, reason: 'old-engine' };
|
|
128
|
+
}
|
|
129
|
+
return { on: true, reason: 'default-on' };
|
|
130
|
+
}
|
|
131
|
+
// ── durable-off 400 的人话分诊([862]④,1.0.5 小件5) ─────────────────────────────────────────────────
|
|
132
|
+
//
|
|
133
|
+
// server ≥1.196 对带 detach 头但无 durable 账本(无 runStore / 无 sessionId)的 submit 判 400 fail-loud
|
|
134
|
+
// (server http/server.ts [854]①b:静默忽略 = 断连仍杀 run,与 caller 声明语义相反;detach 成功了结果
|
|
135
|
+
// 也无处可查)。壳自 spawn 引擎恒 DB_BACKEND=local 永不命中;命中面 = SEMA_ENGINE_URL 外接引擎显式关
|
|
136
|
+
// durable。裸 `sema: API Error: 400 · x-detach-on-disconnect requires a durable run …` 是 server 视角
|
|
137
|
+
// 的契约话,用户不知道下一步 —— 追加一行人话(supplement, never mask:API Error 行字面保留)。
|
|
138
|
+
/** server 400 body 判别锚(server.ts sendJson 原文:"x-detach-on-disconnect requires a durable run
|
|
139
|
+
* (a run store + sessionId) — without one the detached result would be unqueryable")。 */
|
|
140
|
+
export const DETACH_DURABLE_OFF_400_ANCHOR = 'x-detach-on-disconnect requires a durable run';
|
|
141
|
+
/**
|
|
142
|
+
* detach 头引发的 durable-off 400 → 人话提示行;其它错误(非 400 / 非该 body / 头没发过)恒 null
|
|
143
|
+
* (绝不误伤其它 400 —— sandboxImageProfile 400 等各有自己的 hint)。判别三重:头确已发出
|
|
144
|
+
* (isDetachArmed —— armDetachCancel 只在头拼上请求时调)+ SDK APIError.status===400 + message 含
|
|
145
|
+
* server 原文锚(sdk errors.js toApiError: msg = body.error 原文)。
|
|
146
|
+
*/
|
|
147
|
+
export function detachDurableOffHint(err) {
|
|
148
|
+
if (!isDetachArmed())
|
|
149
|
+
return null;
|
|
150
|
+
const e = err;
|
|
151
|
+
if (e?.status !== 400)
|
|
152
|
+
return null;
|
|
153
|
+
const msg = typeof e?.message === 'string' ? e.message : '';
|
|
154
|
+
if (!msg.includes(DETACH_DURABLE_OFF_400_ANCHOR))
|
|
155
|
+
return null;
|
|
156
|
+
return 'engine has durable runs disabled — detach-on-disconnect unavailable; interrupted turns will abort (SEMA_HEADLESS_DETACH=false to silence)';
|
|
157
|
+
}
|
|
158
|
+
let cancelArm = null;
|
|
159
|
+
/** Arm the signal-path cancel fallback (called by ask when the detach header is actually sent). */
|
|
160
|
+
export function armDetachCancel(arm) {
|
|
161
|
+
cancelArm = arm;
|
|
162
|
+
}
|
|
163
|
+
/** Whether this process sent the detach header on its `-p` stream (the 判死文案 upgrade gate). */
|
|
164
|
+
export function isDetachArmed() {
|
|
165
|
+
return cancelArm !== null;
|
|
166
|
+
}
|
|
167
|
+
/** The detached run's durable taskId, when already captured (X-Task-Id) — for the triage copy. */
|
|
168
|
+
export function detachedTaskId() {
|
|
169
|
+
return cancelArm?.getTaskId() ?? null;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 🔴 **宿主发射腿的唯一取件口**(拆缝的库这一侧)。宿主的 `fireBestEffortDetachCancel` 必须从这里
|
|
173
|
+
* 取 arm,**不许自己再存一份** —— 台账两份 = 头发了但 hint 判不出 / Ctrl+C 取消不掉。
|
|
174
|
+
* null = 没 arm 过(detach 关 / 版本门挡下 / mock 车道)⇒ 宿主零动作、零行为变化。
|
|
175
|
+
*/
|
|
176
|
+
export function detachCancelArm() {
|
|
177
|
+
return cancelArm;
|
|
178
|
+
}
|
|
179
|
+
/** Test-only reset (module state is process-lifetime; `-p` is a one-shot). */
|
|
180
|
+
export function _resetDetachWireForTest() {
|
|
181
|
+
cancelArm = null;
|
|
182
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/diff/patch.ts — T20:**diff hunks 在客户端算**(core 已裁定归客户端,零上游依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么必须进本包:core 明确「引擎发 originalFile(客户端永远重建不出来的那一半),hunks 由客户端
|
|
5
|
+
* 算」—— 那么每个端(TUI/web/桌面)各算一遍就是三份实现三种 hunk;进包之后是**一份**。
|
|
6
|
+
* 搬自 cli `src/utils/diff.ts:81 getPatchFromContents` + `src/utils/file.ts:137
|
|
7
|
+
* convertLeadingTabsToSpaces`,**逐字**(含 & / $ 转义 token 与 CONTEXT_LINES/TIMEOUT 常量),
|
|
8
|
+
* 只丢掉宿主侧的 analytics / LOC 计数副作用(那两条在 cli 里也不在本函数内)。
|
|
9
|
+
*
|
|
10
|
+
* 🔴 唯一的运行时依赖 `diff`(jsdiff 9,零传递依赖、纯 JS、浏览器可用)—— 本包第一个 runtime dep。
|
|
11
|
+
* 判据:它不是「顺手拉进来的 hub」,而是 T20 这条能力**本身**;不引它就只能把能力退回各端。
|
|
12
|
+
* portability 门的 ALLOWED_PACKAGES 因此显式登记 `diff` 一条(登记 = 可审计,不是放行一切)。
|
|
13
|
+
*/
|
|
14
|
+
import { type StructuredPatchHunk } from 'diff';
|
|
15
|
+
export declare const CONTEXT_LINES = 3;
|
|
16
|
+
export declare const DIFF_TIMEOUT_MS = 5000;
|
|
17
|
+
/**
|
|
18
|
+
* 渲染前把**行首** tab 折成两空格 —— CC 自己的 Edit 卡在 structuredPatch 之前做同一件事
|
|
19
|
+
* (FileEditTool/utils.ts),不做的话 tab 缩进文件两边算出来的 hunk 对不上。
|
|
20
|
+
*/
|
|
21
|
+
export declare function convertLeadingTabsToSpaces(content: string): string;
|
|
22
|
+
export declare function getPatchFromContents({ filePath, oldContent, newContent, ignoreWhitespace, singleHunk, }: {
|
|
23
|
+
filePath: string;
|
|
24
|
+
oldContent: string;
|
|
25
|
+
newContent: string;
|
|
26
|
+
ignoreWhitespace?: boolean;
|
|
27
|
+
singleHunk?: boolean;
|
|
28
|
+
}): StructuredPatchHunk[];
|
|
29
|
+
export type { StructuredPatchHunk };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/diff/patch.ts — T20:**diff hunks 在客户端算**(core 已裁定归客户端,零上游依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么必须进本包:core 明确「引擎发 originalFile(客户端永远重建不出来的那一半),hunks 由客户端
|
|
5
|
+
* 算」—— 那么每个端(TUI/web/桌面)各算一遍就是三份实现三种 hunk;进包之后是**一份**。
|
|
6
|
+
* 搬自 cli `src/utils/diff.ts:81 getPatchFromContents` + `src/utils/file.ts:137
|
|
7
|
+
* convertLeadingTabsToSpaces`,**逐字**(含 & / $ 转义 token 与 CONTEXT_LINES/TIMEOUT 常量),
|
|
8
|
+
* 只丢掉宿主侧的 analytics / LOC 计数副作用(那两条在 cli 里也不在本函数内)。
|
|
9
|
+
*
|
|
10
|
+
* 🔴 唯一的运行时依赖 `diff`(jsdiff 9,零传递依赖、纯 JS、浏览器可用)—— 本包第一个 runtime dep。
|
|
11
|
+
* 判据:它不是「顺手拉进来的 hub」,而是 T20 这条能力**本身**;不引它就只能把能力退回各端。
|
|
12
|
+
* portability 门的 ALLOWED_PACKAGES 因此显式登记 `diff` 一条(登记 = 可审计,不是放行一切)。
|
|
13
|
+
*/
|
|
14
|
+
import { structuredPatch } from 'diff';
|
|
15
|
+
export const CONTEXT_LINES = 3;
|
|
16
|
+
export const DIFF_TIMEOUT_MS = 5_000;
|
|
17
|
+
// cli 原注:`&` 会让 diff 库犯迷糊,先换成 token、算完再换回来。`$` 同理(替换模式转义位)。
|
|
18
|
+
const AMPERSAND_TOKEN = '<<:AMPERSAND_TOKEN:>>';
|
|
19
|
+
const DOLLAR_TOKEN = '<<:DOLLAR_TOKEN:>>';
|
|
20
|
+
function escapeForDiff(s) {
|
|
21
|
+
return s.replaceAll('&', AMPERSAND_TOKEN).replaceAll('$', DOLLAR_TOKEN);
|
|
22
|
+
}
|
|
23
|
+
function unescapeFromDiff(s) {
|
|
24
|
+
return s.replaceAll(AMPERSAND_TOKEN, '&').replaceAll(DOLLAR_TOKEN, '$');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 渲染前把**行首** tab 折成两空格 —— CC 自己的 Edit 卡在 structuredPatch 之前做同一件事
|
|
28
|
+
* (FileEditTool/utils.ts),不做的话 tab 缩进文件两边算出来的 hunk 对不上。
|
|
29
|
+
*/
|
|
30
|
+
export function convertLeadingTabsToSpaces(content) {
|
|
31
|
+
// 无 tab 的常见情形直接短路(/gm 正则会逐行扫)。
|
|
32
|
+
if (!content.includes('\t'))
|
|
33
|
+
return content;
|
|
34
|
+
return content.replace(/^\t+/gm, _ => ' '.repeat(_.length));
|
|
35
|
+
}
|
|
36
|
+
export function getPatchFromContents({ filePath, oldContent, newContent, ignoreWhitespace = false, singleHunk = false, }) {
|
|
37
|
+
const result = structuredPatch(filePath, filePath, escapeForDiff(oldContent), escapeForDiff(newContent), undefined, undefined, {
|
|
38
|
+
ignoreWhitespace,
|
|
39
|
+
context: singleHunk ? 100_000 : CONTEXT_LINES,
|
|
40
|
+
timeout: DIFF_TIMEOUT_MS,
|
|
41
|
+
});
|
|
42
|
+
if (!result)
|
|
43
|
+
return [];
|
|
44
|
+
return result.hunks.map(_ => ({ ..._, lines: _.lines.map(unescapeFromDiff) }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 当前会话的 session 参数值;undefined = 端未装 SessionPort,或端自己也还没有值。 */
|
|
2
|
+
export declare function engineSessionParam(): string | undefined;
|
|
3
|
+
/** spread 便捷形:`{ ...engineSessionParamSpread() }` → `{ session: '<sid>' }` 或 `{}`。 */
|
|
4
|
+
export declare function engineSessionParamSpread(): {
|
|
5
|
+
session?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* 当前活跃引擎 run 的 id(壳 = `engineToolDetach.getActiveEngineTaskId()`)。
|
|
9
|
+
* 库内多处 durable 动词要「宿主 run」做寻址第一跳,统一从这里取,不各自 import 端模块。
|
|
10
|
+
*/
|
|
11
|
+
export declare function activeEngineRunId(): string | undefined;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B6 批搬迁(2026-07-27,多端改造设计稿 §3 B6 + B4「四残件」之 session-id port):
|
|
3
|
+
* cli `src/sema/engineSessionParam.ts` 搬入,唯一改动 = **取值源换 port**。
|
|
4
|
+
*
|
|
5
|
+
* engineSessionParam — durable run 面 `?session=` 参数的统一派生([1501]§三,server 1.246 起
|
|
6
|
+
* legacy 八面软拍纪律:带了且不匹配=404,没带=warn-once 放行,二拍将把「没带」也 404;新面
|
|
7
|
+
* A/B(taskOutput/taskStop)与 subagentOutput 则出生即强制)。
|
|
8
|
+
*
|
|
9
|
+
* 取值语义 = run 绑定的会话(本端所有 durable 动词的目标 run 都是本会话自有 run)。
|
|
10
|
+
*
|
|
11
|
+
* 🔴 **搬迁的唯一差分**:壳里这里是「`getLiveSessionId()`(引擎捕获值)优先,退
|
|
12
|
+
* `bootstrap/state.getSessionId()`(壳 boot-time id),后者靠动态 import 落地」——**两级优先与
|
|
13
|
+
* 那个动态 import 都是宿主生命周期资产**,整体上移到 `SessionPort.currentSessionId()`
|
|
14
|
+
* (壳侧 shim 里原样保留那两级 + 动态 import,行为一字节不变)。库这边只剩「取一个值 + 空串归一」。
|
|
15
|
+
*
|
|
16
|
+
* 🔴 [1501] 空串陷阱:`?session=`(空值)按「带了且不匹配」处理=404 —— 本 helper 恒不返回
|
|
17
|
+
* 空串:要么真值要么 undefined(调用方 spread 姿势 `...(s ? { session: s } : {})`)。
|
|
18
|
+
*
|
|
19
|
+
* 🔴 端漏装 SessionPort 的后果**不是**「少个查询参数」:session-bound run 的 taskOutput /
|
|
20
|
+
* taskStop / subagentOutput 三面一律 404 fail-closed ⇒ 子代读面整条静默哑掉。所以
|
|
21
|
+
* `hostSession()` 计 miss,宿主自检 `hostPortMisses()` 恒应为空。
|
|
22
|
+
*/
|
|
23
|
+
import { hostSession } from './host.js';
|
|
24
|
+
/** 当前会话的 session 参数值;undefined = 端未装 SessionPort,或端自己也还没有值。 */
|
|
25
|
+
export function engineSessionParam() {
|
|
26
|
+
const sid = hostSession()?.currentSessionId();
|
|
27
|
+
return typeof sid === 'string' && sid.length > 0 ? sid : undefined;
|
|
28
|
+
}
|
|
29
|
+
/** spread 便捷形:`{ ...engineSessionParamSpread() }` → `{ session: '<sid>' }` 或 `{}`。 */
|
|
30
|
+
export function engineSessionParamSpread() {
|
|
31
|
+
const s = engineSessionParam();
|
|
32
|
+
return s ? { session: s } : {};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 当前活跃引擎 run 的 id(壳 = `engineToolDetach.getActiveEngineTaskId()`)。
|
|
36
|
+
* 库内多处 durable 动词要「宿主 run」做寻址第一跳,统一从这里取,不各自 import 端模块。
|
|
37
|
+
*/
|
|
38
|
+
export function activeEngineRunId() {
|
|
39
|
+
const id = hostSession()?.activeRunId();
|
|
40
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
41
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface EngineWireTarget {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
token?: string;
|
|
4
|
+
principal: string;
|
|
5
|
+
}
|
|
6
|
+
/** 装/卸引擎 wire 目标。传 null 卸回 env 派生。返回还原函数。 */
|
|
7
|
+
export declare function installEngineWireTarget(t: EngineWireTarget | null): () => void;
|
|
8
|
+
/**
|
|
9
|
+
* 当前引擎 wire 目标。null = 无 wire(离线/mock/浏览器未装)—— 调用方走各自的回落路径,
|
|
10
|
+
* **不是**错误态。
|
|
11
|
+
*/
|
|
12
|
+
export declare function engineWireTarget(): EngineWireTarget | null;
|
|
13
|
+
/** 诊断开关(壳侧 `process.env.SEMA_DEBUG` 的等价读;本包零 process)。 */
|
|
14
|
+
export declare function engineWireDebugEnabled(): boolean;
|