@sema-agent/client-core 0.5.0 → 0.6.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.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * clientSlice.ts — **verb 门面接口定型**(B4,设计稿 §8-1 裁决:「要,但只定接口不定实现」)。
3
+ *
4
+ * ## 为什么要有这一层
5
+ *
6
+ * §6.1.4 解包 CC 桌面端量到 **854 条 IPC 通道**(`ns_$_Service_$_method`),其中会话相关的
7
+ * `LocalSessions` 一族就有 212 个 verb。那一层客观存在而且很大 —— **不定接口,每端会各长一套**,
8
+ * 于是 HITL 权威登记、通知直结这些「必须只有一个落点」的东西会被重写三遍。
9
+ *
10
+ * ## 但它**只是接口**
11
+ *
12
+ * 🔴 本文件不导出任何 IPC / HTTP / SDK 实现,一行运行时代码都没有(编译产物只有类型)。
13
+ * 依据公理一:CC 的皮肤形状不进 wire ⇒ **IPC 的形状同样不该进 wire**。谁来实现:
14
+ * · TUI = 直接把 `@sema-agent/sdk` 的 AgentClient 方法接上;
15
+ * · web = BFF 转发(逐请求注入 principal);
16
+ * · 桌面 = 主进程 IPC(CC 同款 `ns_$_Service_$_method` 形)。
17
+ *
18
+ * ## 判据:什么进这个接口
19
+ *
20
+ * **会话运行时自己会调的 verb 才进**。纯 UI 触发、端各自决定的动作(开面板、切主题、选文件)
21
+ * 不进 —— 那些是端的事。今天这张表按 cli 现役会话链路归纳,`additive` 演进(只增不改),
22
+ * 与 `CHROME_ARMS` 一样做成可断言的数据出口。
23
+ */
24
+ /** 一个 verb 的登记项(覆盖率断言用;与 `CHROME_ARMS` 同形)。 */
25
+ export interface ClientVerbSpec {
26
+ /** `ClientSliceLike` 上的方法名。 */
27
+ verb: string;
28
+ /** 缺它会让**已发生的行为丢失**(而不是少个装饰)⇒ required。 */
29
+ required: boolean;
30
+ duty: string;
31
+ }
32
+ /**
33
+ * 会话运行时需要宿主提供的 verb 集合(**结构型**:实现方不必 implements,形状对上即可)。
34
+ *
35
+ * 全部返回 Promise —— 三端里有两端是跨进程/跨网络的,同步形会逼 web/桌面造假。
36
+ */
37
+ export interface ClientSliceLike {
38
+ /** 发起一个 turn,拿到 wire 帧流(喂给 `adapt()`)。 */
39
+ streamTask(req: unknown, opts?: {
40
+ signal?: {
41
+ readonly aborted: boolean;
42
+ };
43
+ }): AsyncIterable<unknown>;
44
+ /** 取消在跑的 run(Esc / `-p` 的信号兜底)。 */
45
+ cancelRun(runId: string): Promise<void>;
46
+ /** 工具审批决断。`boundCallId` / `boundInputHash` **必须原样回显**,一个字节不改。 */
47
+ respondApproval(input: {
48
+ approvalId: string;
49
+ decision: 'allow' | 'deny';
50
+ boundCallId: string;
51
+ boundInputHash: string;
52
+ }): Promise<void>;
53
+ /** AskUserQuestion 应答。 */
54
+ respondQuestion(input: {
55
+ questionId: string;
56
+ answer: unknown;
57
+ }): Promise<void>;
58
+ /** plan-review 决断(`done{status:"needs_review"}` + gate `plan_review` 的 resume 腿)。 */
59
+ respondPlanReview(input: {
60
+ taskId: string;
61
+ approved: boolean;
62
+ feedback?: string;
63
+ }): Promise<void>;
64
+ /** 后台子代的实时转录尾巴(查看态)。 */
65
+ tailSubagent(taskId: string): AsyncIterable<unknown>;
66
+ /** 给在跑的子代插话(steer)。 */
67
+ steerSubagent(input: {
68
+ taskId: string;
69
+ text: string;
70
+ }): Promise<void>;
71
+ /** 唤醒已停机的保留子代(design/144 `retainSubagentSessions` 的兑现面)。 */
72
+ wakeSubagent(input: {
73
+ taskId: string;
74
+ message: string;
75
+ }): Promise<void>;
76
+ /** fleet 行帧流(常开;后台子代唯一在场的 lane)。 */
77
+ streamFleet(): AsyncIterable<unknown>;
78
+ /** 列会话(resume 选单)。 */
79
+ listSessions(input?: {
80
+ limit?: number;
81
+ }): Promise<unknown>;
82
+ /** 读一个会话的转录(桌面与 TUI 共享 `~/.claude/projects/**` 的先例,§8-11)。 */
83
+ readSession(sessionId: string): Promise<unknown>;
84
+ /** 引擎 caps(壳侧所有 `engineCapTrue` 门的数据源)。 */
85
+ capabilities(): Promise<unknown>;
86
+ /** 健康探测(🔴 语义差见 `probeHealth` 记账:SDK 要求 `status==='ok'`,壳现实现只要 `res.ok`)。 */
87
+ probeHealth(): Promise<{
88
+ ok: boolean;
89
+ version?: string;
90
+ }>;
91
+ }
92
+ /**
93
+ * verb 覆盖率断言出口(§8-1 的另一半)。
94
+ *
95
+ * 宿主自检遍历本表,对 `required:true` 的逐条断言"我实现了"。不实现 ≠ 报错,是**那个面在该端是哑的**
96
+ * —— 与 `CHROME_ARMS` 同一套纪律:把义务做成数据,而不是散落在注释里。
97
+ */
98
+ export declare const CLIENT_VERBS: readonly [{
99
+ readonly verb: "streamTask";
100
+ readonly required: true;
101
+ readonly duty: "发起 turn 并回 wire 帧流(会话运行时的入口)";
102
+ }, {
103
+ readonly verb: "cancelRun";
104
+ readonly required: true;
105
+ readonly duty: "取消在跑的 run(不实现 = Esc 只停渲染,引擎照跑)";
106
+ }, {
107
+ readonly verb: "respondApproval";
108
+ readonly required: true;
109
+ readonly duty: "工具审批;两元组 verbatim 回显(安全不变量)";
110
+ }, {
111
+ readonly verb: "respondQuestion";
112
+ readonly required: true;
113
+ readonly duty: "AskUserQuestion 应答(不实现 = 问了没法答,turn 挂死)";
114
+ }, {
115
+ readonly verb: "respondPlanReview";
116
+ readonly required: false;
117
+ readonly duty: "plan-review 决断(fail-soft:不实现只是少一张审批卡)";
118
+ }, {
119
+ readonly verb: "tailSubagent";
120
+ readonly required: false;
121
+ readonly duty: "后台子代查看态实时转录";
122
+ }, {
123
+ readonly verb: "steerSubagent";
124
+ readonly required: false;
125
+ readonly duty: "给在跑的子代插话";
126
+ }, {
127
+ readonly verb: "wakeSubagent";
128
+ readonly required: false;
129
+ readonly duty: "唤醒保留子代(SendMessage 语义)";
130
+ }, {
131
+ readonly verb: "streamFleet";
132
+ readonly required: false;
133
+ readonly duty: "fleet 行帧流(后台子代面板行的唯一来源)";
134
+ }, {
135
+ readonly verb: "listSessions";
136
+ readonly required: false;
137
+ readonly duty: "resume 选单";
138
+ }, {
139
+ readonly verb: "readSession";
140
+ readonly required: false;
141
+ readonly duty: "读历史转录";
142
+ }, {
143
+ readonly verb: "capabilities";
144
+ readonly required: true;
145
+ readonly duty: "引擎 caps(所有能力门的数据源;不实现 = 门全判假)";
146
+ }, {
147
+ readonly verb: "probeHealth";
148
+ readonly required: false;
149
+ readonly duty: "健康探测";
150
+ }];
151
+ type _MissingFromTable = Exclude<keyof ClientSliceLike, (typeof CLIENT_VERBS)[number]['verb']>;
152
+ type _ExtraInTable = Exclude<(typeof CLIENT_VERBS)[number]['verb'], keyof ClientSliceLike>;
153
+ type _MustBeNever<T extends never> = T;
154
+ export type _VerbTableClosure = [_MustBeNever<_MissingFromTable>, _MustBeNever<_ExtraInTable>];
155
+ /**
156
+ * 🔴 `probeHealth` 的**行为差记账**(B4,设计稿 §3 B4 第四件明确要求先保旧行为)。
157
+ *
158
+ * 壳里两处手拼 `/health`(`S/limitsWire.ts:225`、`S/detachWire.ts:82`)判的是 **`res.ok`**
159
+ * (HTTP 2xx 即算活);SDK 的 `probeHealth` 额外要求 **`body.status === 'ok'`**。
160
+ * 差异面:引擎起来了但自报 degraded(2xx + `status!=='ok'`)时 —— 手拼判**活**,SDK 判**死**。
161
+ *
162
+ * B4 的处置:**先保旧行为**(不换,两处仍判 `res.ok`),差异记在这里候裁。
163
+ * 换的话是行为改动,要按宪法三问单独立项:
164
+ * · 谁需要:degraded 引擎上 `--max-*` 版本门与 detach 头版本门本就该保守拒发 ⇒ SDK 语义更对;
165
+ * · 谁受害:自报 degraded 但功能其实在线的引擎,`-p` 会少发 detach 头(断连即杀 run)⇒ 真回归;
166
+ * · 补偿:换之前先量一次"degraded 但可用"在真部署里出不出现。
167
+ */
168
+ export declare const PROBE_HEALTH_DIVERGENCE: string;
169
+ export {};
@@ -0,0 +1,62 @@
1
+ /**
2
+ * clientSlice.ts — **verb 门面接口定型**(B4,设计稿 §8-1 裁决:「要,但只定接口不定实现」)。
3
+ *
4
+ * ## 为什么要有这一层
5
+ *
6
+ * §6.1.4 解包 CC 桌面端量到 **854 条 IPC 通道**(`ns_$_Service_$_method`),其中会话相关的
7
+ * `LocalSessions` 一族就有 212 个 verb。那一层客观存在而且很大 —— **不定接口,每端会各长一套**,
8
+ * 于是 HITL 权威登记、通知直结这些「必须只有一个落点」的东西会被重写三遍。
9
+ *
10
+ * ## 但它**只是接口**
11
+ *
12
+ * 🔴 本文件不导出任何 IPC / HTTP / SDK 实现,一行运行时代码都没有(编译产物只有类型)。
13
+ * 依据公理一:CC 的皮肤形状不进 wire ⇒ **IPC 的形状同样不该进 wire**。谁来实现:
14
+ * · TUI = 直接把 `@sema-agent/sdk` 的 AgentClient 方法接上;
15
+ * · web = BFF 转发(逐请求注入 principal);
16
+ * · 桌面 = 主进程 IPC(CC 同款 `ns_$_Service_$_method` 形)。
17
+ *
18
+ * ## 判据:什么进这个接口
19
+ *
20
+ * **会话运行时自己会调的 verb 才进**。纯 UI 触发、端各自决定的动作(开面板、切主题、选文件)
21
+ * 不进 —— 那些是端的事。今天这张表按 cli 现役会话链路归纳,`additive` 演进(只增不改),
22
+ * 与 `CHROME_ARMS` 一样做成可断言的数据出口。
23
+ */
24
+ /**
25
+ * verb 覆盖率断言出口(§8-1 的另一半)。
26
+ *
27
+ * 宿主自检遍历本表,对 `required:true` 的逐条断言"我实现了"。不实现 ≠ 报错,是**那个面在该端是哑的**
28
+ * —— 与 `CHROME_ARMS` 同一套纪律:把义务做成数据,而不是散落在注释里。
29
+ */
30
+ // 🔴 **不要**给这个 const 加 `: readonly ClientVerbSpec[]` 注解 —— 注解会把 `verb` 拓宽成 `string`,
31
+ // 下面那道编译期双向闭合门就变成恒真的空转(B4 首写时正是这么写的,加注解后 `Exclude` 恒为 `string`
32
+ // ⇒ 门当场红且删表项也红,红得毫无判别力)。用 `as const satisfies` 既约束形状又保住字面量类型。
33
+ export const CLIENT_VERBS = [
34
+ { verb: 'streamTask', required: true, duty: '发起 turn 并回 wire 帧流(会话运行时的入口)' },
35
+ { verb: 'cancelRun', required: true, duty: '取消在跑的 run(不实现 = Esc 只停渲染,引擎照跑)' },
36
+ { verb: 'respondApproval', required: true, duty: '工具审批;两元组 verbatim 回显(安全不变量)' },
37
+ { verb: 'respondQuestion', required: true, duty: 'AskUserQuestion 应答(不实现 = 问了没法答,turn 挂死)' },
38
+ { verb: 'respondPlanReview', required: false, duty: 'plan-review 决断(fail-soft:不实现只是少一张审批卡)' },
39
+ { verb: 'tailSubagent', required: false, duty: '后台子代查看态实时转录' },
40
+ { verb: 'steerSubagent', required: false, duty: '给在跑的子代插话' },
41
+ { verb: 'wakeSubagent', required: false, duty: '唤醒保留子代(SendMessage 语义)' },
42
+ { verb: 'streamFleet', required: false, duty: 'fleet 行帧流(后台子代面板行的唯一来源)' },
43
+ { verb: 'listSessions', required: false, duty: 'resume 选单' },
44
+ { verb: 'readSession', required: false, duty: '读历史转录' },
45
+ { verb: 'capabilities', required: true, duty: '引擎 caps(所有能力门的数据源;不实现 = 门全判假)' },
46
+ { verb: 'probeHealth', required: false, duty: '健康探测' },
47
+ ];
48
+ /**
49
+ * 🔴 `probeHealth` 的**行为差记账**(B4,设计稿 §3 B4 第四件明确要求先保旧行为)。
50
+ *
51
+ * 壳里两处手拼 `/health`(`S/limitsWire.ts:225`、`S/detachWire.ts:82`)判的是 **`res.ok`**
52
+ * (HTTP 2xx 即算活);SDK 的 `probeHealth` 额外要求 **`body.status === 'ok'`**。
53
+ * 差异面:引擎起来了但自报 degraded(2xx + `status!=='ok'`)时 —— 手拼判**活**,SDK 判**死**。
54
+ *
55
+ * B4 的处置:**先保旧行为**(不换,两处仍判 `res.ok`),差异记在这里候裁。
56
+ * 换的话是行为改动,要按宪法三问单独立项:
57
+ * · 谁需要:degraded 引擎上 `--max-*` 版本门与 detach 头版本门本就该保守拒发 ⇒ SDK 语义更对;
58
+ * · 谁受害:自报 degraded 但功能其实在线的引擎,`-p` 会少发 detach 头(断连即杀 run)⇒ 真回归;
59
+ * · 补偿:换之前先量一次"degraded 但可用"在真部署里出不出现。
60
+ */
61
+ export const PROBE_HEALTH_DIVERGENCE = 'probeHealth: SDK 要求 status==="ok",壳侧 limitsWire:225 / detachWire:82 手拼只要 res.ok。' +
62
+ 'B4 保旧行为(res.ok),差异候裁 —— 换成 SDK 语义会让"自报 degraded 但可用"的引擎少发 detach 头。';
@@ -27,7 +27,31 @@ export type WireAuthToken = string | {
27
27
  };
28
28
  /** loopback 判定(engineTarget.isLoopbackEngineUrl 逐字镜像;解析失败=非 loopback)。 */
29
29
  export declare function isLoopbackWireUrl(url: string): boolean;
30
- /** #118 三态凭证解析(resolveLiveAuthToken 纯半场镜像)。 */
30
+ /**
31
+ * §8-2 定谳的**唯一**凭证解析口(B4 收编)—— 11 处构造点的公共分母。
32
+ *
33
+ * 背景(设计稿 §8-2 + B4 实测):壳里今天是 **2 份三态解析**(`engineTarget.resolveLiveAuthToken`
34
+ * 产品半场 + 本包 `wireAuthTokenFor` 纯半场,后者头注自述是前者的「逐字镜像」⇒ 两份必然漂移)
35
+ * 外加 **11 处 `new AgentClient` 直造**,其中 7 处根本不走任何解析口,直接手写
36
+ * `token ?? { mode:'loopback-unauthed' }` —— 那一形**丢了 'anon' 那一态**:非 loopback 且无 token
37
+ * 时它给的是 loopback-unauthed(出站零 Authorization 头),而三态解析给 'anon'。对 fail-closed
38
+ * worker 两者都是 401,但对 **owner-gated 路由**不是同一回事,所以这是真分歧不是等价写法。
39
+ *
40
+ * 🔴 **env 缺省留在端上**:`resolveLiveAuthToken` 的第二参缺省是 `process.env.SEMA_LIVE_TOKEN`,
41
+ * 那一半**绝不进包**(本包零 `process.env` —— portability 门盯着)。壳侧 shim 保留缺省参即可:
42
+ * `resolveLiveAuthToken(u, t = process.env.SEMA_LIVE_TOKEN) => resolveWireAuth(u, t)`。
43
+ *
44
+ * 三态(#118):token 有 ⇒ 原样 Bearer;无 token ∧ loopback ⇒ `{mode:'loopback-unauthed'}`
45
+ * (出站零 Authorization 头,绝不伪造);无 token ∧ 非 loopback ⇒ `'anon'`(fail-closed 换诚实 401)。
46
+ * `baseUrl` 允许 undefined(= `resolveLiveAuthToken` 的签名超集,本地自 spawn 态)⇒ 'anon'。
47
+ */
48
+ export declare function resolveWireAuth(baseUrl: string | undefined, token?: string): WireAuthToken;
49
+ /**
50
+ * #118 三态凭证解析(旧名,B2 引入)。
51
+ * B4 起是 `resolveWireAuth` 的**别名**(baseUrl 必填的窄签名);两者行为逐字相同,保留是为了
52
+ * 壳侧既有 import 零改动 —— 不设代际让位([clean-cut-no-legacy-compat] 只约束**行为面**硬切,
53
+ * 同一函数的两个名字不是两套行为)。新代码一律用 `resolveWireAuth`。
54
+ */
31
55
  export declare function wireAuthTokenFor(baseUrl: string, token?: string): WireAuthToken;
32
56
  export type EngineWireClientConfig = {
33
57
  baseUrl: string;
@@ -32,14 +32,40 @@ export function isLoopbackWireUrl(url) {
32
32
  return false;
33
33
  }
34
34
  }
35
- /** #118 三态凭证解析(resolveLiveAuthToken 纯半场镜像)。 */
36
- export function wireAuthTokenFor(baseUrl, token) {
35
+ /**
36
+ * §8-2 定谳的**唯一**凭证解析口(B4 收编)—— 11 处构造点的公共分母。
37
+ *
38
+ * 背景(设计稿 §8-2 + B4 实测):壳里今天是 **2 份三态解析**(`engineTarget.resolveLiveAuthToken`
39
+ * 产品半场 + 本包 `wireAuthTokenFor` 纯半场,后者头注自述是前者的「逐字镜像」⇒ 两份必然漂移)
40
+ * 外加 **11 处 `new AgentClient` 直造**,其中 7 处根本不走任何解析口,直接手写
41
+ * `token ?? { mode:'loopback-unauthed' }` —— 那一形**丢了 'anon' 那一态**:非 loopback 且无 token
42
+ * 时它给的是 loopback-unauthed(出站零 Authorization 头),而三态解析给 'anon'。对 fail-closed
43
+ * worker 两者都是 401,但对 **owner-gated 路由**不是同一回事,所以这是真分歧不是等价写法。
44
+ *
45
+ * 🔴 **env 缺省留在端上**:`resolveLiveAuthToken` 的第二参缺省是 `process.env.SEMA_LIVE_TOKEN`,
46
+ * 那一半**绝不进包**(本包零 `process.env` —— portability 门盯着)。壳侧 shim 保留缺省参即可:
47
+ * `resolveLiveAuthToken(u, t = process.env.SEMA_LIVE_TOKEN) => resolveWireAuth(u, t)`。
48
+ *
49
+ * 三态(#118):token 有 ⇒ 原样 Bearer;无 token ∧ loopback ⇒ `{mode:'loopback-unauthed'}`
50
+ * (出站零 Authorization 头,绝不伪造);无 token ∧ 非 loopback ⇒ `'anon'`(fail-closed 换诚实 401)。
51
+ * `baseUrl` 允许 undefined(= `resolveLiveAuthToken` 的签名超集,本地自 spawn 态)⇒ 'anon'。
52
+ */
53
+ export function resolveWireAuth(baseUrl, token) {
37
54
  if (token)
38
55
  return token;
39
- if (isLoopbackWireUrl(baseUrl))
56
+ if (baseUrl !== undefined && isLoopbackWireUrl(baseUrl))
40
57
  return { mode: 'loopback-unauthed' };
41
58
  return 'anon';
42
59
  }
60
+ /**
61
+ * #118 三态凭证解析(旧名,B2 引入)。
62
+ * B4 起是 `resolveWireAuth` 的**别名**(baseUrl 必填的窄签名);两者行为逐字相同,保留是为了
63
+ * 壳侧既有 import 零改动 —— 不设代际让位([clean-cut-no-legacy-compat] 只约束**行为面**硬切,
64
+ * 同一函数的两个名字不是两套行为)。新代码一律用 `resolveWireAuth`。
65
+ */
66
+ export function wireAuthTokenFor(baseUrl, token) {
67
+ return resolveWireAuth(baseUrl, token);
68
+ }
43
69
  /**
44
70
  * 构造一个引擎 wire 用 AgentClient。null = 构造失败(SDK fail-closed 拒绝——不该发生:
45
71
  * wireAuthTokenFor 恒给合法形;防御性 fail-soft,调用方按各自离线/探测失败路径处理)。
@@ -48,7 +74,7 @@ export function makeEngineWireClient(cfg) {
48
74
  try {
49
75
  return new AgentClient({
50
76
  baseUrl: cfg.baseUrl,
51
- authToken: wireAuthTokenFor(cfg.baseUrl, cfg.token),
77
+ authToken: resolveWireAuth(cfg.baseUrl, cfg.token),
52
78
  principal: cfg.principal ?? 'anon:shell-live',
53
79
  ...(cfg.timeoutMs !== undefined ? { timeoutMs: cfg.timeoutMs } : {}),
54
80
  maxRetries: cfg.maxRetries ?? 0,
package/dist/host.d.ts ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * host.ts — **包级宿主装配**(B4;B3 交接报告的建议件 + 设计稿 §5.4 「web 宿主的最小接入面」)。
3
+ *
4
+ * ## 两种口,别混
5
+ *
6
+ * · `AdapterContext`(seam.ts)= **per-turn 切片** —— 时钟、铸号、signal、定时器、攒批节拍。
7
+ * 它随每个 `adapt()` 调用传进来,因为这些东西**每个 turn 可以不一样**(比如这一 turn 可中断)。
8
+ * · `installHost()`(本文件)= **进程/端级装配** —— settings 读取、fs、通知队列。
9
+ * 它们在会话开始之前就该在位,而且被**请求构造期**的代码调用(`hooksForWire()` /
10
+ * `scratchpadDirField()`),那时候根本没有 `ctx` 可传。
11
+ *
12
+ * ⇒ 判据:**这个能力是不是每 turn 会变?** 会 ⇒ ctx;不会 ⇒ installHost。
13
+ *
14
+ * ## 缺席语义(与 AdapterContext 同一条纪律)
15
+ *
16
+ * 🔴 缺席 = **该能力不启用**,不是报错、更不是「换个方式偷偷做」。库绝不 `require('fs')`、
17
+ * 绝不 `console.log`、绝不读 `process.env` 兜底 —— 那正是搬迁要切掉的东西。
18
+ * 但「静默不启用」有个已知的失败形:**宿主漏装了,而症状是某个面悄悄空掉**。所以每个口都带
19
+ * miss 计数(`hostPortMisses()`),宿主自检断言它恒为 0 —— 与 `notificationQueuePortMisses()`
20
+ * 同一套姿势([probe-must-prove-it-speaks]:不能让「什么都没有」看起来像「一切正常」)。
21
+ */
22
+ import type { AdapterLogLevel } from './seam.js';
23
+ import { type NotificationQueuePort } from './notifications.js';
24
+ /**
25
+ * settings 口 —— 用户 settings 文件的读取面(壳=三层合并后的文件读;web=center 下发;桌面=主进程)。
26
+ *
27
+ * 🔴 `shouldSkipHookDueToTrust` 是**安全门**不是取值:未受信目录的 hook 绝不上 wire。
28
+ * 端必须真实现它;不实现(端返回恒 false)= 把信任门拆了。所以它在 required 清单里。
29
+ */
30
+ export interface SettingsPort {
31
+ /** 按来源取 settings(cli `utils/settings/settings.ts` 的 `getSettingsForSource`)。 */
32
+ getSettingsForSource(source: string): unknown;
33
+ /** 信任门:该 hook 是否因来源不受信而跳过。**返回 true = 跳过**。 */
34
+ shouldSkipHookDueToTrust(hook: unknown): boolean;
35
+ }
36
+ /**
37
+ * fs 口 —— scratchpad 目录族(cli `utils/permissions/filesystem.ts`)。
38
+ * `ensureScratchpadDir` 是**真 IO**(mkdir);浏览器宿主整个口省略即可(⇒ 不 stamp scratchpadDir,
39
+ * additive 字段缺席对引擎无害)。
40
+ */
41
+ export interface FsPort {
42
+ getScratchpadDir(): string | undefined;
43
+ isScratchpadEnabled(): boolean;
44
+ /** 🔴 壳侧真身是 **async**(mkdir 0o700);端可返 void 或 Promise,库两种都吞(fire-and-forget)。 */
45
+ ensureScratchpadDir(): void | Promise<void>;
46
+ }
47
+ /** 定时器口(进程级默认;`ctx.setTimer` 仍可按 turn 覆盖)。 */
48
+ export interface TimersPort {
49
+ setTimer(ms: number, fn: () => void): unknown;
50
+ clearTimer(handle: unknown): void;
51
+ }
52
+ /** `installHost` 的入参 —— 全部可选,缺席即该能力不启用。 */
53
+ export interface HostPorts {
54
+ log?(level: AdapterLogLevel, msg: string, data?: unknown): void;
55
+ probe?(channel: string, line: string): void;
56
+ /** 通知队列(B2 引入)。经本口装 == 直接调 `installNotificationQueuePort`,单一真源不分裂。 */
57
+ queue?: NotificationQueuePort | null;
58
+ timers?: TimersPort;
59
+ settings?: SettingsPort;
60
+ fs?: FsPort;
61
+ }
62
+ /**
63
+ * 装配宿主口。**可多次调用做增量装配**(只覆盖本次给出的键),便于分模块装。
64
+ * 传 `queue` 会直通 `installNotificationQueuePort` —— 队列的真源仍在 notifications.ts,
65
+ * 本文件不另存一份([paired-mechanisms-must-share-premise]:两处各存一份 = 装了 A 读了 B)。
66
+ * 返回一个还原函数(测试/热重载用)。
67
+ */
68
+ export declare function installHost(ports: HostPorts): () => void;
69
+ /** 日志(缺席 = 静默,**不计 miss**:诊断面本就是可选的)。 */
70
+ export declare function hostLog(level: AdapterLogLevel, msg: string, data?: unknown): void;
71
+ /** 行式探针(缺席 = 静默,不计 miss:同上)。 */
72
+ export declare function hostProbe(channel: string, line: string): void;
73
+ /** settings 口(缺席 = undefined + 计 miss:漏装会让 hooks 整条 wire 静默空掉)。 */
74
+ export declare function hostSettings(): SettingsPort | undefined;
75
+ /** fs 口(缺席 = undefined + 计 miss)。 */
76
+ export declare function hostFs(): FsPort | undefined;
77
+ /** 定时器口(缺席 = undefined,不计 miss:T40 竞速本就允许不启用)。 */
78
+ export declare function hostTimers(): TimersPort | undefined;
79
+ /**
80
+ * 🔴 宿主自检出口 —— 恒应为空对象。非空 = 有口漏装,**对应的面已经在静默失效**。
81
+ * 含通知队列的 miss(从 notifications.ts 汇过来,宿主只需看这一个数)。
82
+ */
83
+ export declare function hostPortMisses(): Record<string, number>;
84
+ /**
85
+ * 测试钩:清空装配与计数。
86
+ * 🔴 队列那一路必须走 `_resetNotificationQueuePortForTest()` 而不是 `installNotificationQueuePort(null)`
87
+ * —— 后者只卸口、**不清 miss 计数**,于是 `hostPortMisses()` 会带着上一段测试的历史 miss,
88
+ * 让「未装配时 misses 为空」这类断言恒假(B4 首写时就撞上了)。
89
+ */
90
+ export declare function _resetHostForTest(): void;
package/dist/host.js ADDED
@@ -0,0 +1,84 @@
1
+ import { _resetNotificationQueuePortForTest, installNotificationQueuePort, notificationQueuePortMisses, } from './notifications.js';
2
+ let host = {};
3
+ const misses = Object.create(null);
4
+ const warned = new Set();
5
+ function miss(name) {
6
+ misses[name] = (misses[name] ?? 0) + 1;
7
+ if (!warned.has(name)) {
8
+ warned.add(name);
9
+ // eslint-disable-next-line no-console
10
+ console.error(`[client-core] host port "${name}" NOT installed — the surface it drives is silently inert. ` +
11
+ 'Host must call installHost({ ' +
12
+ name +
13
+ ': … }) at startup (see host.ts).');
14
+ }
15
+ return undefined;
16
+ }
17
+ /**
18
+ * 装配宿主口。**可多次调用做增量装配**(只覆盖本次给出的键),便于分模块装。
19
+ * 传 `queue` 会直通 `installNotificationQueuePort` —— 队列的真源仍在 notifications.ts,
20
+ * 本文件不另存一份([paired-mechanisms-must-share-premise]:两处各存一份 = 装了 A 读了 B)。
21
+ * 返回一个还原函数(测试/热重载用)。
22
+ */
23
+ export function installHost(ports) {
24
+ const prev = { ...host };
25
+ if ('log' in ports)
26
+ host.log = ports.log;
27
+ if ('probe' in ports)
28
+ host.probe = ports.probe;
29
+ if ('timers' in ports)
30
+ host.timers = ports.timers;
31
+ if ('settings' in ports)
32
+ host.settings = ports.settings;
33
+ if ('fs' in ports)
34
+ host.fs = ports.fs;
35
+ if ('queue' in ports)
36
+ installNotificationQueuePort(ports.queue ?? null);
37
+ return () => {
38
+ host = prev;
39
+ };
40
+ }
41
+ /** 日志(缺席 = 静默,**不计 miss**:诊断面本就是可选的)。 */
42
+ export function hostLog(level, msg, data) {
43
+ host.log?.(level, msg, data);
44
+ }
45
+ /** 行式探针(缺席 = 静默,不计 miss:同上)。 */
46
+ export function hostProbe(channel, line) {
47
+ host.probe?.(channel, line);
48
+ }
49
+ /** settings 口(缺席 = undefined + 计 miss:漏装会让 hooks 整条 wire 静默空掉)。 */
50
+ export function hostSettings() {
51
+ return host.settings ?? miss('settings');
52
+ }
53
+ /** fs 口(缺席 = undefined + 计 miss)。 */
54
+ export function hostFs() {
55
+ return host.fs ?? miss('fs');
56
+ }
57
+ /** 定时器口(缺席 = undefined,不计 miss:T40 竞速本就允许不启用)。 */
58
+ export function hostTimers() {
59
+ return host.timers;
60
+ }
61
+ /**
62
+ * 🔴 宿主自检出口 —— 恒应为空对象。非空 = 有口漏装,**对应的面已经在静默失效**。
63
+ * 含通知队列的 miss(从 notifications.ts 汇过来,宿主只需看这一个数)。
64
+ */
65
+ export function hostPortMisses() {
66
+ const out = { ...misses };
67
+ const q = notificationQueuePortMisses();
68
+ if (q > 0)
69
+ out.queue = q;
70
+ return out;
71
+ }
72
+ /**
73
+ * 测试钩:清空装配与计数。
74
+ * 🔴 队列那一路必须走 `_resetNotificationQueuePortForTest()` 而不是 `installNotificationQueuePort(null)`
75
+ * —— 后者只卸口、**不清 miss 计数**,于是 `hostPortMisses()` 会带着上一段测试的历史 miss,
76
+ * 让「未装配时 misses 为空」这类断言恒假(B4 首写时就撞上了)。
77
+ */
78
+ export function _resetHostForTest() {
79
+ host = {};
80
+ for (const k of Object.keys(misses))
81
+ delete misses[k];
82
+ warned.clear();
83
+ _resetNotificationQueuePortForTest();
84
+ }
package/dist/index.d.ts CHANGED
@@ -5,6 +5,13 @@
5
5
  * 沿革:@sema-agent/wire-cc-adapter 0.1.0 = seam 类型 + id 确定性派生 + 首批踩坑纯函数;
6
6
  * 0.1.2(#52a)= adapt() 管线首批(纯投影臂全落 + 壳态耦合臂投影成 ChromeEvent + 差分守卫);
7
7
  * 0.2.0 = 迁入 sema-client-core 独立仓并改名(旧 npm 名 deprecate 指本包);
8
+ * 0.6.0 = **B4 批**(设计稿 §3 B4):A 层帧分派**收官**(第 14 臂 `task_progress` + 行绑定/
9
+ * inline stats/alias/#6 常驻台账/SubagentStart/workflow lane 三级门 + `settlePanelTasks` 三处 sweep
10
+ * 全接线)· P0-2 tool label 侧信道补漏(0.5.0 真缺口)· **请求面合一**(`request/taskRequest.ts`:
11
+ * 三个构造器的字段集与合并语义收成一份,车道差异做成 `REQUEST_FIELD_MATRIX` 数据表)·
12
+ * §8-2 构造点收编(`resolveWireAuth`)· §8-1 verb 门面接口定型(`ClientSliceLike` + `CLIENT_VERBS`,
13
+ * **编译期**双向闭合门)· `CHROME_ARMS` 覆盖率出口 · 包级宿主装配 `installHost()`(settings/fs/
14
+ * queue/timers 四口 + `hostPortMisses()` 自检)· `scratchpadWireCaps` 搬入(FsPort 端到端证明)。
8
15
  * 0.5.0 = **B3 批**(设计稿 §3 B3):`AdapterContext` 扩容(log/probe/signal/setTimer/clearTimer/
9
16
  * coalesceIntervalMs,**只加可选位 = minor 不 BREAKING**)· 适配内核搬入(`adapter/types` 运行时
10
17
  * 半场 + `adapter/downstream` 三件 + `adapter/runStream`)· runStream **两条外向边切除**
@@ -29,9 +36,15 @@
29
36
  * —— B2 新增:notifications.ts(11 个通知台账 + 队列 port + 两个 probe 槽)·
30
37
  * agentsWireCaps.ts(prepare 缓存/投影闭包/一次性告警集)· liveModelCatalog.ts(目录 + refresher)·
31
38
  * sessionModelLatch.ts(三个 latch)· ultracodeWireCaps.ts(sticky preset)。
39
+ * —— B4 新增:scratchpadWireCaps.ts(`ensured` 建目录闩)· host.ts(宿主口装配 + miss 计数);
40
+ * 适配器**实例级**(非 module 级)新增 `firedSubagentStartHookTaskIds`——cli 那份是 module 级,
41
+ * 本包放实例级是**有意的**(多 session 宿主上 module 形会让两个会话互吞 SubagentStart,见 DIVERGENCE-6)。
32
42
  * —— B3 新增:adapter/runStream.ts(`inFlightTurns` 计数,读口 `isRunStreamActive()`;
33
43
  * 两份实例 = 引擎滚动升级门恒读 false ⇒ **turn 中途热换引擎**,正是它当初要防的事)。
34
- * 🔴 **宿主装配**(B2 起本包有必须由宿主注入才完整的口):`installNotificationQueuePort()` ——
44
+ * 🔴 **宿主装配**——B4 起总入口是 `installHost({log,probe,queue,timers,settings,fs})`
45
+ * (`installNotificationQueuePort()` 仍是队列的真源出口,`installHost({queue})` 直通它,两者不分裂);
46
+ * 自检口 `hostPortMisses()` 恒应为空对象,非空 = 有口漏装、对应的面已在静默失效。
47
+ * —— B2 起的既有说明:`installNotificationQueuePort()` ——
35
48
  * 不装 = task-notification 投递静默丢失(`notificationQueuePortMisses()` 恒应为 0)。
36
49
  * —— B3 新增(可选口,缺席 = 对应 UI 面在该宿主上是哑的,不是报错):`ctx.emitChrome`
37
50
  * (runStream 的两条切边:statusline 真 usage / plan-review 审批卡)· `ctx.setTimer`
@@ -85,3 +98,7 @@ export * from './adapter/downstream/turnUsageToModelUsage.js';
85
98
  export * from './adapter/downstream/eventToSdkMessage.js';
86
99
  export * from './adapter/downstream/terminalToSdkResult.js';
87
100
  export * from './adapter/runStream.js';
101
+ export * from './request/taskRequest.js';
102
+ export * from './clientSlice.js';
103
+ export * from './host.js';
104
+ export * from './scratchpadWireCaps.js';
package/dist/index.js CHANGED
@@ -5,6 +5,13 @@
5
5
  * 沿革:@sema-agent/wire-cc-adapter 0.1.0 = seam 类型 + id 确定性派生 + 首批踩坑纯函数;
6
6
  * 0.1.2(#52a)= adapt() 管线首批(纯投影臂全落 + 壳态耦合臂投影成 ChromeEvent + 差分守卫);
7
7
  * 0.2.0 = 迁入 sema-client-core 独立仓并改名(旧 npm 名 deprecate 指本包);
8
+ * 0.6.0 = **B4 批**(设计稿 §3 B4):A 层帧分派**收官**(第 14 臂 `task_progress` + 行绑定/
9
+ * inline stats/alias/#6 常驻台账/SubagentStart/workflow lane 三级门 + `settlePanelTasks` 三处 sweep
10
+ * 全接线)· P0-2 tool label 侧信道补漏(0.5.0 真缺口)· **请求面合一**(`request/taskRequest.ts`:
11
+ * 三个构造器的字段集与合并语义收成一份,车道差异做成 `REQUEST_FIELD_MATRIX` 数据表)·
12
+ * §8-2 构造点收编(`resolveWireAuth`)· §8-1 verb 门面接口定型(`ClientSliceLike` + `CLIENT_VERBS`,
13
+ * **编译期**双向闭合门)· `CHROME_ARMS` 覆盖率出口 · 包级宿主装配 `installHost()`(settings/fs/
14
+ * queue/timers 四口 + `hostPortMisses()` 自检)· `scratchpadWireCaps` 搬入(FsPort 端到端证明)。
8
15
  * 0.5.0 = **B3 批**(设计稿 §3 B3):`AdapterContext` 扩容(log/probe/signal/setTimer/clearTimer/
9
16
  * coalesceIntervalMs,**只加可选位 = minor 不 BREAKING**)· 适配内核搬入(`adapter/types` 运行时
10
17
  * 半场 + `adapter/downstream` 三件 + `adapter/runStream`)· runStream **两条外向边切除**
@@ -29,9 +36,15 @@
29
36
  * —— B2 新增:notifications.ts(11 个通知台账 + 队列 port + 两个 probe 槽)·
30
37
  * agentsWireCaps.ts(prepare 缓存/投影闭包/一次性告警集)· liveModelCatalog.ts(目录 + refresher)·
31
38
  * sessionModelLatch.ts(三个 latch)· ultracodeWireCaps.ts(sticky preset)。
39
+ * —— B4 新增:scratchpadWireCaps.ts(`ensured` 建目录闩)· host.ts(宿主口装配 + miss 计数);
40
+ * 适配器**实例级**(非 module 级)新增 `firedSubagentStartHookTaskIds`——cli 那份是 module 级,
41
+ * 本包放实例级是**有意的**(多 session 宿主上 module 形会让两个会话互吞 SubagentStart,见 DIVERGENCE-6)。
32
42
  * —— B3 新增:adapter/runStream.ts(`inFlightTurns` 计数,读口 `isRunStreamActive()`;
33
43
  * 两份实例 = 引擎滚动升级门恒读 false ⇒ **turn 中途热换引擎**,正是它当初要防的事)。
34
- * 🔴 **宿主装配**(B2 起本包有必须由宿主注入才完整的口):`installNotificationQueuePort()` ——
44
+ * 🔴 **宿主装配**——B4 起总入口是 `installHost({log,probe,queue,timers,settings,fs})`
45
+ * (`installNotificationQueuePort()` 仍是队列的真源出口,`installHost({queue})` 直通它,两者不分裂);
46
+ * 自检口 `hostPortMisses()` 恒应为空对象,非空 = 有口漏装、对应的面已在静默失效。
47
+ * —— B2 起的既有说明:`installNotificationQueuePort()` ——
35
48
  * 不装 = task-notification 投递静默丢失(`notificationQueuePortMisses()` 恒应为 0)。
36
49
  * —— B3 新增(可选口,缺席 = 对应 UI 面在该宿主上是哑的,不是报错):`ctx.emitChrome`
37
50
  * (runStream 的两条切边:statusline 真 usage / plan-review 审批卡)· `ctx.setTimer`
@@ -63,8 +76,8 @@ export * from './controlRouter.js';
63
76
  export * from './sseIdleTriage.js';
64
77
  export * from './engineWireSdk.js';
65
78
  export * from './classifierVerdictWire.js';
66
- // caps / wire 门族(17 个 *WireCaps —— 壳侧 hooksWireCaps / scratchpadWireCaps 因宿主耦合未搬,
67
- // README B2 记账)
79
+ // caps / wire 门族(17 个 *WireCaps。B2 hooksWireCaps / scratchpadWireCaps 因宿主耦合未搬;
80
+ // B4 已搬 scratchpadWireCaps(走 FsPort),hooksWireCaps 仍在壳里等 SettingsPort 接线 —— 见交接报告)
68
81
  export * from './agentsWireCaps.js';
69
82
  export * from './attachmentsWireCaps.js';
70
83
  export * from './clientContextWireCaps.js';
@@ -95,3 +108,13 @@ export * from './adapter/downstream/turnUsageToModelUsage.js';
95
108
  export * from './adapter/downstream/eventToSdkMessage.js';
96
109
  export * from './adapter/downstream/terminalToSdkResult.js';
97
110
  export * from './adapter/runStream.js';
111
+ // ── B4 批:A 层帧分派收官 + 请求面合一 + 构造点收编(2026-07-27)────────────────────────────────
112
+ // 请求面:三个构造器(seamQuery.toTaskRequest / seamQueryEngine / liveClient.toLiveRequest)
113
+ // 的**字段集与合并语义**收成一份;车道差异做成 REQUEST_FIELD_MATRIX 数据表(gap:true = 判为漏)。
114
+ export * from './request/taskRequest.js';
115
+ export * from './clientSlice.js';
116
+ // 包级宿主装配(per-turn 的 AdapterContext 之外的那一层:settings / fs / queue / timers)。
117
+ // 🔴 `installNotificationQueuePort` 仍是队列的真源出口,`installHost({queue})` 只是直通它。
118
+ export * from './host.js';
119
+ // B2 判给 B4 的宿主耦合件之一(小的那件,同时是 FsPort 的端到端证明)。
120
+ export * from './scratchpadWireCaps.js';