@sema-agent/client-core 0.4.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.
package/dist/index.js CHANGED
@@ -5,6 +5,19 @@
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 端到端证明)。
15
+ * 0.5.0 = **B3 批**(设计稿 §3 B3):`AdapterContext` 扩容(log/probe/signal/setTimer/clearTimer/
16
+ * coalesceIntervalMs,**只加可选位 = minor 不 BREAKING**)· 适配内核搬入(`adapter/types` 运行时
17
+ * 半场 + `adapter/downstream` 三件 + `adapter/runStream`)· runStream **两条外向边切除**
18
+ * (lastTurnUsageStore / planReviewWire ⇒ `last_turn_usage` / `plan_review_park` 两个 chrome 臂)·
19
+ * T38 三清第三清 + T40 IDLE-FLUSH + T57 第五处断闸 + T58 iterator cleanup + T59 abort + T60 收尾腿。
20
+ * 🆕 第三道守卫 `scripts/run-client-core-portability-test.mjs`(内核闭包 1571 文件 → **6 文件**)。
8
21
  * 0.4.0 = **B2 批**(设计稿 §3 B2):通知族三文件合并进 notifications.ts(engineTaskNotification 全文 +
9
22
  * taskNotificationErrorSupplement 全文 + hookNoticeStore 判定半场)· 17 个 `*WireCaps` + engineWireSdk +
10
23
  * sseIdleTriage + classifierVerdictWire 判定半场 + controlRouter + 模型面纯逻辑 4 件 + 新写 hostEnv;
@@ -23,8 +36,20 @@
23
36
  * —— B2 新增:notifications.ts(11 个通知台账 + 队列 port + 两个 probe 槽)·
24
37
  * agentsWireCaps.ts(prepare 缓存/投影闭包/一次性告警集)· liveModelCatalog.ts(目录 + refresher)·
25
38
  * sessionModelLatch.ts(三个 latch)· ultracodeWireCaps.ts(sticky preset)。
26
- * 🔴 **宿主装配**(B2 起本包有必须由宿主注入才完整的口):`installNotificationQueuePort()` ——
39
+ * —— B4 新增:scratchpadWireCaps.ts(`ensured` 建目录闩)· host.ts(宿主口装配 + miss 计数);
40
+ * 适配器**实例级**(非 module 级)新增 `firedSubagentStartHookTaskIds`——cli 那份是 module 级,
41
+ * 本包放实例级是**有意的**(多 session 宿主上 module 形会让两个会话互吞 SubagentStart,见 DIVERGENCE-6)。
42
+ * —— B3 新增:adapter/runStream.ts(`inFlightTurns` 计数,读口 `isRunStreamActive()`;
43
+ * 两份实例 = 引擎滚动升级门恒读 false ⇒ **turn 中途热换引擎**,正是它当初要防的事)。
44
+ * 🔴 **宿主装配**——B4 起总入口是 `installHost({log,probe,queue,timers,settings,fs})`
45
+ * (`installNotificationQueuePort()` 仍是队列的真源出口,`installHost({queue})` 直通它,两者不分裂);
46
+ * 自检口 `hostPortMisses()` 恒应为空对象,非空 = 有口漏装、对应的面已在静默失效。
47
+ * —— B2 起的既有说明:`installNotificationQueuePort()` ——
27
48
  * 不装 = task-notification 投递静默丢失(`notificationQueuePortMisses()` 恒应为 0)。
49
+ * —— B3 新增(可选口,缺席 = 对应 UI 面在该宿主上是哑的,不是报错):`ctx.emitChrome`
50
+ * (runStream 的两条切边:statusline 真 usage / plan-review 审批卡)· `ctx.setTimer`
51
+ * (T40 IDLE-FLUSH,不给 = 长工具参数生成期已生成内容仍押到下一帧才上屏)· `ctx.signal`
52
+ * (Esc 中断)· `ctx.log` / `ctx.probe`(诊断面)。
28
53
  */
29
54
  export * from './seam.js';
30
55
  export * from './workflow.js';
@@ -51,8 +76,8 @@ export * from './controlRouter.js';
51
76
  export * from './sseIdleTriage.js';
52
77
  export * from './engineWireSdk.js';
53
78
  export * from './classifierVerdictWire.js';
54
- // caps / wire 门族(17 个 *WireCaps —— 壳侧 hooksWireCaps / scratchpadWireCaps 因宿主耦合未搬,
55
- // README B2 记账)
79
+ // caps / wire 门族(17 个 *WireCaps。B2 hooksWireCaps / scratchpadWireCaps 因宿主耦合未搬;
80
+ // B4 已搬 scratchpadWireCaps(走 FsPort),hooksWireCaps 仍在壳里等 SettingsPort 接线 —— 见交接报告)
56
81
  export * from './agentsWireCaps.js';
57
82
  export * from './attachmentsWireCaps.js';
58
83
  export * from './clientContextWireCaps.js';
@@ -74,3 +99,22 @@ export * from './liveModelCatalog.js';
74
99
  export * from './modelBudgetRule.js';
75
100
  export * from './sessionModelLatch.js';
76
101
  export * from './effortWire.js';
102
+ // ── B3 批:适配内核(增量合并 + 计量 + 流驱动)(2026-07-27)──────────────────────────────────
103
+ // M/adapter 的 downstream 三件 + types 运行时半场 + runStream 整搬(设计稿 §3 B3)。
104
+ // 🔴 runStream.ts 持 module 级 `inFlightTurns` 计数(isRunStreamActive 的读口)——单例清单新增项:
105
+ // 写方=runStream 自己,读方=壳的引擎滚动升级门(turn 中途绝不热换引擎)。两份实例 = 门恒读 false。
106
+ export * from './adapter/types.js';
107
+ export * from './adapter/downstream/turnUsageToModelUsage.js';
108
+ export * from './adapter/downstream/eventToSdkMessage.js';
109
+ export * from './adapter/downstream/terminalToSdkResult.js';
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';
@@ -0,0 +1,135 @@
1
+ /**
2
+ * request/taskRequest.ts — **请求面合一**(B4,多端改造设计稿 §3 B4 第二件 + §8-5 裁决)。
3
+ *
4
+ * ## 它解决的是什么
5
+ *
6
+ * 搬迁前壳里有 **三个**独立的请求构造器,谁也不知道对方长什么样:
7
+ * · `S/seamQuery.toTaskRequest`(:535-869)—— 交互 REPL 车道;
8
+ * · `S/seamQueryEngine`(:546-578)—— headless `-p` 车道,**自己另拼一份**;
9
+ * · `S/liveClient.toLiveRequest`(:322-448)—— 两条车道共用的 live 兜底层。
10
+ *
11
+ * census §3.4 实测的漂移(B4 复核确认,逐字段见 `REQUEST_FIELD_MATRIX`):
12
+ * · **print 独有** `finalVerification` / `limits` / `interactiveTools`;
13
+ * · **TUI 独有** `ultracode` / `systemPrompt` / `reasoningEffort` / `images` / `model` /
14
+ * rewind 三件 / `clientContext` / `scratchpadDir` / 已解析 settings。
15
+ * 这不是「两条车道本来就该不同」——里面**一部分是有理由的,一部分纯粹是漏了**。合一的目的不是
16
+ * 把它们抹平(那是行为改动),而是把「谁有谁没有、为什么」变成**一张表**:再漏就会在 diff 里显形。
17
+ *
18
+ * ## 分工铁律
19
+ *
20
+ * 🔴 本模块**零 env、零全局状态、零 IO**——所有值由端解析好之后放进 `TaskRequestInput`。
21
+ * 壳读 `process.env` / `getAppState()` / `getSessionId()` / `process.argv`,web 读 BFF 下发的配置,
22
+ * 桌面读主进程。包只负责三件**端无关**的事:
23
+ * ① **字段集**:哪条车道 stamp 哪些字段(`REQUEST_FIELD_MATRIX`,带逐条理由);
24
+ * ② **合并语义**:`settings` 四源合并、objective 的一次性装饰序、sessionId 三态;
25
+ * ③ **live 门**:一个 `input.live` 布尔,替掉壳里散落的 20 处 `process.env.SEMA_LIVE_BASEURL` 判读。
26
+ */
27
+ import type { TaskRequest } from '@sema-agent/sdk';
28
+ /** 请求车道(§8-5:print lane 收编进来,但 stderr 分诊文案与 PrintStreamProjector 留壳)。 */
29
+ export type RequestLane = 'interactive' | 'print';
30
+ /** `TaskRequest` 的可写投影(包内按结构操作;SDK 型只做出口约束)。 */
31
+ export type TaskRequestLike = Record<string, unknown>;
32
+ /**
33
+ * 字段矩阵的一条 —— **这就是「不许静默统一」的载体**。
34
+ *
35
+ * `lanes` 是**事实**(今天哪条车道 stamp 它),`why` 是**理由**(为什么另一条没有)。
36
+ * 两者分开写:`lanes` 变了会被下面的自洽断言逮住,`why` 让下一棒知道该不该补齐。
37
+ * `gap:true` = 这条差异**没有正当理由,是漏的**(候补齐;补齐是行为改动,要单独一条测试)。
38
+ */
39
+ export interface RequestFieldSpec {
40
+ field: string;
41
+ lanes: readonly RequestLane[];
42
+ /** 仅在 live 车道 stamp(mock/pty fixture 车道请求形状恒不变)。 */
43
+ live: boolean;
44
+ why: string;
45
+ gap?: true;
46
+ }
47
+ /**
48
+ * 逐字段对照表(B4 从三个构造器**逐行**读出来的,不是设计出来的)。
49
+ *
50
+ * 读法:`lanes` 只有一条 = 今天的漂移面;`gap:true` = 我判它是**漏**而不是**设计**。
51
+ * 🔴 `gap` 是【判断】不是【事实】——补齐任何一条都要按宪法三问单独立项,别当 B4 的收尾顺手做。
52
+ */
53
+ export declare const REQUEST_FIELD_MATRIX: readonly RequestFieldSpec[];
54
+ /** live 兜底层(`toLiveRequest`)追加的字段 —— 两条车道**都**经过,故不进上表。 */
55
+ export declare const LIVE_DEFAULT_FIELDS: readonly ["cwd", "additionalDirectories", "forwardSubagentEvents", "retainSubagentSessions", "agents", "appendSystemPrompt|settings.outputStyle", "suggestNextPrompts", "compactionModel", "sessionId(三态解析)"];
56
+ /** 端解析好的输入 —— 每一项都是**值**,不是取值方式(取值方式属端)。 */
57
+ export interface TaskRequestInput {
58
+ /** 本 turn 的模型面输入(已含 slash-skill 正文 / 注入式 meta / 历史种子等端侧组装)。 */
59
+ objective: string;
60
+ /** 壳会话 id(单命名空间);print 非 live 车道传 mock 常量。 */
61
+ sessionId: string;
62
+ /** live 车道(= 壳的 `process.env.SEMA_LIVE_BASEURL` 判真)。false ⇒ 全部 live-gated 字段不 stamp。 */
63
+ live: boolean;
64
+ scenario?: string;
65
+ systemPrompt?: string;
66
+ reasoningEffort?: string;
67
+ model?: string;
68
+ images?: readonly unknown[];
69
+ skills?: unknown;
70
+ mcpServers?: unknown;
71
+ agents?: unknown;
72
+ clientContext?: Record<string, unknown>;
73
+ scratchpadDir?: string;
74
+ sandboxImageProfile?: string;
75
+ permissionMode?: string;
76
+ selfOrchestration?: boolean;
77
+ deferTools?: readonly string[];
78
+ retainBackgroundProcesses?: boolean;
79
+ promptProfile?: string;
80
+ enableFork?: boolean;
81
+ attachments?: Record<string, unknown>;
82
+ finalVerification?: boolean;
83
+ limits?: Record<string, unknown>;
84
+ interactiveTools?: false;
85
+ /** rewind 三件(E18);端自己判空。 */
86
+ rewind?: Record<string, unknown>;
87
+ /** `settings` 的四个来源(端各自解析;本模块只负责合并序与「全空则不 stamp」)。 */
88
+ settings?: {
89
+ resolved?: Record<string, unknown>;
90
+ webSearch?: Record<string, unknown>;
91
+ ultracode?: boolean;
92
+ hooks?: Record<string, unknown>;
93
+ outputStyle?: string;
94
+ };
95
+ }
96
+ /**
97
+ * 合一后的请求构造器 —— **按车道出两形**,字段集差异全部由 `REQUEST_FIELD_MATRIX` 决定。
98
+ *
99
+ * 🔴 行为纪律:本函数**不做任何统一**。print 没有 `ultracode` 就是没有(表里 `gap:true` 记着账),
100
+ * 补齐要另立项 —— 在这里顺手加一行,就是把「合一」偷换成「行为改动」。
101
+ */
102
+ export declare function buildTaskRequest(input: TaskRequestInput, lane: RequestLane): TaskRequestLike;
103
+ /** `applyLiveRequestDefaults` 的宿主输入(端解析好的值,同样零取值方式)。 */
104
+ export interface LiveDefaultsInput {
105
+ cwd?: string;
106
+ additionalDirectories?: readonly string[];
107
+ agents?: unknown;
108
+ /** 引擎 caps 判真 ⇒ 走 `appendSystemPrompt` 一等位;否则借道 `settings.outputStyle`。 */
109
+ appendSystemPromptCapable: boolean;
110
+ /** 自我认知块的合并结果(端把既有值传进来,包只决定落哪个位)。 */
111
+ selfKnowledge?: {
112
+ forAppend: string;
113
+ forOutputStyle: string;
114
+ };
115
+ /** E12 opt-in(缺省关 —— 开着 = 每个 completed run 后端多跑一次 LLM pass)。 */
116
+ suggestNextPrompts?: boolean;
117
+ /** 三重门都过之后的 cheap 槽模型名;端负责 caps + 配置 + 目录同步校验。 */
118
+ compactionModel?: string;
119
+ /** 前一 turn 捕获的引擎 session id(连续性最高优先)。 */
120
+ capturedSessionId?: string;
121
+ /** 端的 mock session 常量(命中 ⇒ 删 sessionId 让引擎新铸)。 */
122
+ mockSessionIdConstant?: string;
123
+ /** 需要从 scenario 位剔除的 mock 关键字。 */
124
+ mockScenarioKeys?: ReadonlySet<string>;
125
+ }
126
+ /**
127
+ * live 兜底层(cli `liveClient.toLiveRequest` 的**包内半场**)—— 两条车道共用。
128
+ *
129
+ * 🔴 与 cli 的差分记账:cli 版在函数体里直接读 `process.cwd()` / `getAppStateStoreRef()` /
130
+ * `getLiveModelCatalog()` / `engineCapTrue()`。这些是**取值方式**,全部上移到 `LiveDefaultsInput`。
131
+ * 「调用方已带值就不覆盖」的语义(cli 每条都是 `if (out.X === undefined)`)在这里逐条保留。
132
+ */
133
+ export declare function applyLiveRequestDefaults(req: TaskRequestLike, host: LiveDefaultsInput): TaskRequestLike;
134
+ /** 出口约束:构造结果确实是一个 `TaskRequest`(型只在这里碰 SDK,运行时零依赖)。 */
135
+ export type BuiltTaskRequest = TaskRequest;
@@ -0,0 +1,176 @@
1
+ /**
2
+ * 逐字段对照表(B4 从三个构造器**逐行**读出来的,不是设计出来的)。
3
+ *
4
+ * 读法:`lanes` 只有一条 = 今天的漂移面;`gap:true` = 我判它是**漏**而不是**设计**。
5
+ * 🔴 `gap` 是【判断】不是【事实】——补齐任何一条都要按宪法三问单独立项,别当 B4 的收尾顺手做。
6
+ */
7
+ export const REQUEST_FIELD_MATRIX = [
8
+ // ── 两条车道都有 ────────────────────────────────────────────────────────────────────────────
9
+ { field: 'objective', lanes: ['interactive', 'print'], live: false, why: '本 turn 的真实输入,两条车道都必需' },
10
+ { field: 'sessionId', lanes: ['interactive', 'print'], live: false, why: '单命名空间(TP-A Step 2):壳 id 即引擎 session id;print 在非 live 下保留 mock 常量' },
11
+ { field: 'scenario', lanes: ['interactive', 'print'], live: false, why: 'TUI 走 selectScenario(objective) 嗅探,print 走 --scenario/SEMA_HEADLESS_SCENARIO 显式值' },
12
+ { field: 'sandboxImageProfile', lanes: ['interactive', 'print'], live: true, why: '[686]② 显式 profile;auto 档只在 TUI 有(一次性 advisory 进 objective)' },
13
+ { field: 'permissionMode', lanes: ['interactive', 'print'], live: true, why: 'TUI 读 AppState.toolPermissionContext(带 stampDefault 门),print 读 flag>env(headlessPermissionModeWire)' },
14
+ { field: 'selfOrchestration', lanes: ['interactive', 'print'], live: true, why: 'SEMA_SELF_ORCHESTRATION opt-in → core 挂 run_workflow' },
15
+ { field: 'deferTools', lanes: ['interactive', 'print'], live: true, why: '[1052]① Workflow 延迟披露;selfOrchestration 前置门' },
16
+ { field: 'retainBackgroundProcesses', lanes: ['interactive', 'print'], live: true, why: '缺省 ON(clay 2026-07-26 翻转);SEMA_RETAIN_BACKGROUND=0 逃生' },
17
+ { field: 'promptProfile', lanes: ['interactive', 'print'], live: true, why: 'core 1.328 呈现轴 A/B 口,缺省不 stamp = 引擎缺省 simple' },
18
+ { field: 'enableFork', lanes: ['interactive', 'print'], live: true, why: '显式布尔才 stamp([503]② fail-OPEN 教训:off 必须显式)' },
19
+ { field: 'attachments', lanes: ['interactive', 'print'], live: true, why: 'design/133 + G1 缺省开的 turn 边界 attachment' },
20
+ { field: 'skills', lanes: ['interactive', 'print'], live: true, why: '本地 .claude/skills 投影;两条车道都由 launchRepl 载入面注入' },
21
+ { field: 'mcpServers', lanes: ['interactive', 'print'], live: true, why: '本地 .mcp.json 投影;service 单用户门决定是否兑现' },
22
+ { field: 'settings.webSearch', lanes: ['interactive', 'print'], live: true, why: 'SEMA_WEBSEARCH_* / settings.json;per-request 配置赢过部署 env' },
23
+ { field: 'settings.hooks', lanes: ['interactive', 'print'], live: true, why: '[495]① 用户 settings 文件 hooks 逐字上 wire' },
24
+ // ── print 独有(§8-5 认定的**有理由**的差异)──────────────────────────────────────────────
25
+ { field: 'finalVerification', lanes: ['print'], live: true, why: 'P1-1 终验:无人值守车道才需要引擎自证;交互 REPL 由人当场看结果。#106 裁 B(让位+告知)后交互面默认关' },
26
+ { field: 'limits', lanes: ['print'], live: true, why: 'P2-3-b:`-p` 的预算护栏(--max-* flag 族),交互 REPL 由人随时 Esc' },
27
+ { field: 'interactiveTools', lanes: ['print'], live: true, why: '[909]B 件3:无人值守 stamp false,从 roster 源头灭掉 AskUserQuestion/plan 门。交互车道 stamp false 等于自废武功' },
28
+ // ── TUI 独有 ────────────────────────────────────────────────────────────────────────────────
29
+ { field: 'settings.ultracode', lanes: ['interactive'], live: true, why: 'design/111:sticky `/effort ultracode` 拨盘 + 当轮关键词嗅探,两个来源都只在交互面存在', gap: true },
30
+ { field: 'systemPrompt', lanes: ['interactive'], live: false, why: 'CC QueryParams.systemPrompt;print 腿的 params 没有这一位' },
31
+ { field: 'reasoningEffort', lanes: ['interactive'], live: false, why: '`/effort` 拨盘存在 AppState,print 无 AppState', gap: true },
32
+ { field: 'model', lanes: ['interactive'], live: false, why: '`/model` 中途换模型读 toolUseContext.options.mainLoopModel;print 的模型走 MODEL_ID/--model 另一条路', gap: true },
33
+ { field: 'images', lanes: ['interactive'], live: false, why: '贴图提交是交互动作,`-p` 的 stdin 没有图片块' },
34
+ { field: 'resumeAt/rewindFiles/rewindFilesTo', lanes: ['interactive'], live: false, why: 'E18 `/rewind` 是交互命令' },
35
+ { field: 'clientContext', lanes: ['interactive'], live: false, why: 'IANA 时区 + 可选邮箱 → core 本地化 `# Environment` 的 today;print 同样跑在用户机器上,缺席让引擎误标 (UTC)', gap: true },
36
+ { field: 'scratchpadDir', lanes: ['interactive'], live: true, why: '[816]③ per-session 暂存目录;print 也有 session,缺席让 `-p` 的工具写不进 exemptDir', gap: true },
37
+ { field: 'settings.<resolved>', lanes: ['interactive'], live: true, why: 'settings resolver 的 effective 快照(hooks/env/model + 已解析权限);print 只带 webSearch+hooks 两键', gap: true },
38
+ ];
39
+ /** live 兜底层(`toLiveRequest`)追加的字段 —— 两条车道**都**经过,故不进上表。 */
40
+ export const LIVE_DEFAULT_FIELDS = [
41
+ 'cwd',
42
+ 'additionalDirectories',
43
+ 'forwardSubagentEvents',
44
+ 'retainSubagentSessions',
45
+ 'agents',
46
+ 'appendSystemPrompt|settings.outputStyle',
47
+ 'suggestNextPrompts',
48
+ 'compactionModel',
49
+ 'sessionId(三态解析)',
50
+ ];
51
+ const laneHas = (field, lane) => REQUEST_FIELD_MATRIX.find(f => f.field === field)?.lanes.includes(lane) ?? false;
52
+ /**
53
+ * 合一后的请求构造器 —— **按车道出两形**,字段集差异全部由 `REQUEST_FIELD_MATRIX` 决定。
54
+ *
55
+ * 🔴 行为纪律:本函数**不做任何统一**。print 没有 `ultracode` 就是没有(表里 `gap:true` 记着账),
56
+ * 补齐要另立项 —— 在这里顺手加一行,就是把「合一」偷换成「行为改动」。
57
+ */
58
+ export function buildTaskRequest(input, lane) {
59
+ const live = input.live;
60
+ /** stamp 门:字段在本车道的表里 ∧(非 live-gated ∨ 本次是 live 车道)∧ 值非空。 */
61
+ const on = (field, value) => {
62
+ if (value === undefined || value === null)
63
+ return false;
64
+ const spec = REQUEST_FIELD_MATRIX.find(f => f.field === field);
65
+ if (spec === undefined)
66
+ return false;
67
+ if (!spec.lanes.includes(lane))
68
+ return false;
69
+ if (spec.live && !live)
70
+ return false;
71
+ return true;
72
+ };
73
+ // settings 四源合并(cli 原式:任一存在则 stamp,全缺则整个 `settings` 键都不出现)。
74
+ // ultracode 只在交互车道进表 ⇒ print 传了也不 stamp(表说了算,不是调用方说了算)。
75
+ const s = input.settings ?? {};
76
+ const settingsOut = {
77
+ ...(laneHas('settings.<resolved>', lane) && live ? (s.resolved ?? {}) : {}),
78
+ ...(on('settings.webSearch', s.webSearch) ? { webSearch: s.webSearch } : {}),
79
+ ...(on('settings.ultracode', s.ultracode) ? { ultracode: s.ultracode } : {}),
80
+ ...(on('settings.hooks', s.hooks) ? { hooks: s.hooks } : {}),
81
+ ...(s.outputStyle !== undefined ? { outputStyle: s.outputStyle } : {}),
82
+ };
83
+ const out = {
84
+ objective: input.objective,
85
+ sessionId: input.sessionId,
86
+ ...(on('scenario', input.scenario) ? { scenario: input.scenario } : {}),
87
+ ...(on('systemPrompt', input.systemPrompt) ? { systemPrompt: input.systemPrompt } : {}),
88
+ ...(on('reasoningEffort', input.reasoningEffort) ? { reasoningEffort: input.reasoningEffort } : {}),
89
+ ...(on('model', input.model) ? { model: input.model } : {}),
90
+ ...(on('images', input.images) && (input.images?.length ?? 0) > 0 ? { images: input.images } : {}),
91
+ ...(on('skills', input.skills) ? { skills: input.skills } : {}),
92
+ ...(on('mcpServers', input.mcpServers) ? { mcpServers: input.mcpServers } : {}),
93
+ ...(on('resumeAt/rewindFiles/rewindFilesTo', input.rewind) ? input.rewind : {}),
94
+ ...(on('permissionMode', input.permissionMode) ? { permissionMode: input.permissionMode } : {}),
95
+ ...(on('sandboxImageProfile', input.sandboxImageProfile)
96
+ ? { sandboxImageProfile: input.sandboxImageProfile }
97
+ : {}),
98
+ ...(on('selfOrchestration', input.selfOrchestration) ? { selfOrchestration: input.selfOrchestration } : {}),
99
+ ...(on('deferTools', input.deferTools) ? { deferTools: input.deferTools } : {}),
100
+ ...(on('retainBackgroundProcesses', input.retainBackgroundProcesses)
101
+ ? { retainBackgroundProcesses: input.retainBackgroundProcesses }
102
+ : {}),
103
+ ...(on('promptProfile', input.promptProfile) ? { promptProfile: input.promptProfile } : {}),
104
+ ...(on('enableFork', input.enableFork) ? { enableFork: input.enableFork } : {}),
105
+ ...(on('attachments', input.attachments) ? { attachments: input.attachments } : {}),
106
+ ...(on('clientContext', input.clientContext) ? { clientContext: input.clientContext } : {}),
107
+ ...(on('scratchpadDir', input.scratchpadDir) ? { scratchpadDir: input.scratchpadDir } : {}),
108
+ ...(on('finalVerification', input.finalVerification)
109
+ ? { finalVerification: input.finalVerification }
110
+ : {}),
111
+ ...(on('limits', input.limits) ? { limits: input.limits } : {}),
112
+ ...(on('interactiveTools', input.interactiveTools) && input.interactiveTools === false
113
+ ? { interactiveTools: false }
114
+ : {}),
115
+ ...(Object.keys(settingsOut).length > 0 ? { settings: settingsOut } : {}),
116
+ };
117
+ return out;
118
+ }
119
+ /**
120
+ * live 兜底层(cli `liveClient.toLiveRequest` 的**包内半场**)—— 两条车道共用。
121
+ *
122
+ * 🔴 与 cli 的差分记账:cli 版在函数体里直接读 `process.cwd()` / `getAppStateStoreRef()` /
123
+ * `getLiveModelCatalog()` / `engineCapTrue()`。这些是**取值方式**,全部上移到 `LiveDefaultsInput`。
124
+ * 「调用方已带值就不覆盖」的语义(cli 每条都是 `if (out.X === undefined)`)在这里逐条保留。
125
+ */
126
+ export function applyLiveRequestDefaults(req, host) {
127
+ const out = { ...req };
128
+ if (out.cwd == null && host.cwd !== undefined)
129
+ out.cwd = host.cwd;
130
+ if (out.additionalDirectories === undefined && (host.additionalDirectories?.length ?? 0) > 0) {
131
+ out.additionalDirectories = [...(host.additionalDirectories ?? [])];
132
+ }
133
+ // C1 / design/144:交互与 headless 都常开 —— 不开则引擎不建 SubagentRetainLedger,
134
+ // 后台子代 SendMessage 唤醒直接 "session was not retained" 拒绝。老引擎按 body→spec 白名单
135
+ // 忽略未知字段,version-safe。
136
+ if (out.forwardSubagentEvents === undefined)
137
+ out.forwardSubagentEvents = true;
138
+ if (out.retainSubagentSessions === undefined)
139
+ out.retainSubagentSessions = true;
140
+ if (out.agents === undefined && host.agents !== undefined)
141
+ out.agents = host.agents;
142
+ if (typeof out.scenario === 'string' && host.mockScenarioKeys?.has(out.scenario)) {
143
+ delete out.scenario;
144
+ }
145
+ // 自我认知块双臂([1487]/[1478] R1 终形):caps 判真走一等位,否则借道 outputStyle。
146
+ // 两臂同一合并序(知识块恒前、调用方块恒后)——序由端在 selfKnowledge 里拼好,包只选位。
147
+ if (host.selfKnowledge !== undefined) {
148
+ if (host.appendSystemPromptCapable) {
149
+ out.appendSystemPrompt = host.selfKnowledge.forAppend;
150
+ }
151
+ else {
152
+ const settings = (out.settings ?? {});
153
+ out.settings = { ...settings, outputStyle: host.selfKnowledge.forOutputStyle };
154
+ }
155
+ }
156
+ // E12:与 verify/cascade 互斥(那两个返 result 非 streamed run ⇒ 400)。
157
+ if (out.suggestNextPrompts === undefined &&
158
+ out.verify === undefined &&
159
+ out.cascade === undefined &&
160
+ host.suggestNextPrompts === true) {
161
+ out.suggestNextPrompts = true;
162
+ }
163
+ if (out.compactionModel === undefined && host.compactionModel !== undefined) {
164
+ out.compactionModel = host.compactionModel;
165
+ }
166
+ // 单命名空间三态(cli 逐字):① 已捕获的引擎 session 赢(连续性 —— 引擎 session 绝不在
167
+ // 会话中途被换掉);③ 还带着 mock 常量 ⇒ 删掉让引擎新铸;② 其余保留(首 turn 壳 id 直传)。
168
+ if (host.capturedSessionId) {
169
+ out.sessionId = host.capturedSessionId;
170
+ }
171
+ else if (host.mockSessionIdConstant !== undefined &&
172
+ out.sessionId === host.mockSessionIdConstant) {
173
+ delete out.sessionId;
174
+ }
175
+ return out;
176
+ }
@@ -0,0 +1,16 @@
1
+ export interface ScratchpadDirField {
2
+ scratchpadDir?: string;
3
+ }
4
+ /**
5
+ * Per-turn scratchpadDir stamp。gate off(tengu_scratch)⇒ 空对象(请求形状不变,mock parity)。
6
+ * 路径 per-session 稳定(sessionId 定 getScratchpadDir),重复调用幂等。
7
+ *
8
+ * 与 cli 的差分记账:
9
+ * · `ensureScratchpadDir` 在壳里是 async(mkdir 0o700),这里 **两种返回都吞**(端可给同步实现);
10
+ * 建失败只影响引擎首写时自建(server 半场也建),**不阻 turn** —— 与 cli 逐字同语义。
11
+ * · fs 口缺席(浏览器宿主)⇒ 返回空对象 = 不 stamp。additive 字段缺席对引擎无害,
12
+ * 但会计一次 `hostPortMisses().fs`,让「Node 宿主漏装」与「浏览器本就没有」在自检里可区分。
13
+ */
14
+ export declare function scratchpadDirField(): ScratchpadDirField;
15
+ /** 测试钩:复位 module 级建目录闩。 */
16
+ export declare function _resetScratchpadEnsuredForTest(): void;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * ⇄ B4 批搬迁(2026-07-27,多端改造设计稿 §3 B4 第五件):cli `src/sema/scratchpadWireCaps.ts` 搬入。
3
+ * 拆缝 = 两条外向边换 host port:`utils/permissions/filesystem`(getScratchpadDir /
4
+ * isScratchpadEnabled / ensureScratchpadDir)→ `hostFs()`;`utils/debug.logForDebugging` → `hostLog()`。
5
+ * 本文件是 B2 判给 B4 的两件「宿主耦合 WireCaps」里的**小的那件**,同时充当 `FsPort` 的端到端证明。
6
+ */
7
+ /**
8
+ * scratchpad 域的 wire 半场([816] 壳侧承诺③ / [819]④ / [820]③,2026-07-14)。
9
+ *
10
+ * 链路:core `TaskSpec.envFacts.scratchpadDir` 字段 + 提示词消费链(buildScratchpadSection)1.257.3
11
+ * 起全在;server 半场 = buildEnvFacts 填字段 + 建目录 + 塞进 createFsWriteGatePolicy 的 exemptDirs。
12
+ * 客户端把 getScratchpadDir()(CC 同型:`/tmp/claude-{uid}/{sanitized-cwd}/{sessionId}/scratchpad`)
13
+ * stamp 到 `TaskRequest.scratchpadDir` —— server 半场没到时字段发了引擎忽略 = 无害(additive 铁律);
14
+ * 到货后 server 以客户端带的路径为准(本机 TOC 引擎与壳同 fs,壳造的目录引擎直接可写),或自造(TOB 远端)。
15
+ *
16
+ * 🔴 [1840] 勘误:G3 `scratchpadDir` **不是死键** —— server 已把「死键」的说法收回改接单
17
+ * ([816]③/[820]③ additive 预挂)。别再按「没人读」把这条 stamp 删掉。
18
+ *
19
+ * 🔴 单实例纪律:本模块持 module 级 `ensured` 闩(整进程只 fire-and-forget 建一次目录)。
20
+ * 两份实例 = 建两次(幂等无害),但与本包其余台账同理,壳侧必须只解析到一份。
21
+ */
22
+ import { hostFs, hostLog } from './host.js';
23
+ let ensured = false;
24
+ /**
25
+ * Per-turn scratchpadDir stamp。gate off(tengu_scratch)⇒ 空对象(请求形状不变,mock parity)。
26
+ * 路径 per-session 稳定(sessionId 定 getScratchpadDir),重复调用幂等。
27
+ *
28
+ * 与 cli 的差分记账:
29
+ * · `ensureScratchpadDir` 在壳里是 async(mkdir 0o700),这里 **两种返回都吞**(端可给同步实现);
30
+ * 建失败只影响引擎首写时自建(server 半场也建),**不阻 turn** —— 与 cli 逐字同语义。
31
+ * · fs 口缺席(浏览器宿主)⇒ 返回空对象 = 不 stamp。additive 字段缺席对引擎无害,
32
+ * 但会计一次 `hostPortMisses().fs`,让「Node 宿主漏装」与「浏览器本就没有」在自检里可区分。
33
+ */
34
+ export function scratchpadDirField() {
35
+ try {
36
+ const fs = hostFs();
37
+ if (fs === undefined)
38
+ return {};
39
+ if (!fs.isScratchpadEnabled())
40
+ return {};
41
+ const dir = fs.getScratchpadDir();
42
+ if (!ensured) {
43
+ ensured = true;
44
+ try {
45
+ const r = fs.ensureScratchpadDir();
46
+ if (r !== undefined && typeof r.catch === 'function') {
47
+ void r.catch((e) => {
48
+ hostLog('debug', `scratchpadWireCaps: ensureScratchpadDir failed (non-fatal): ${String(e)}`);
49
+ });
50
+ }
51
+ }
52
+ catch (e) {
53
+ hostLog('debug', `scratchpadWireCaps: ensureScratchpadDir threw (non-fatal): ${String(e)}`);
54
+ }
55
+ }
56
+ return dir !== undefined ? { scratchpadDir: dir } : {};
57
+ }
58
+ catch {
59
+ return {};
60
+ }
61
+ }
62
+ /** 测试钩:复位 module 级建目录闩。 */
63
+ export function _resetScratchpadEnsuredForTest() {
64
+ ensured = false;
65
+ }