@sema-agent/client-core 0.11.5 → 0.11.7
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.d.ts +1 -0
- package/dist/index.js +10 -0
- package/dist/request/taskRequest.d.ts +2 -0
- package/dist/request/taskRequest.js +10 -1
- package/dist/seatContract.d.ts +486 -0
- package/dist/seatContract.js +427 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -279,3 +279,13 @@ export * from './compensations.js';
|
|
|
279
279
|
// 去重键**不出第二个名字**:print 侧那份手写算法废除,直接用 `taskNotificationDedupKeyFromWire`。
|
|
280
280
|
// 请求面同批补上可执行判据 `unregisteredRequestKeys`(见 request/taskRequest.ts 文件末)。
|
|
281
281
|
export * from './request/printNotification.js';
|
|
282
|
+
// ── B18 批:desktop/web 座位 IPC 契约单一真源(design/159,2026-07-29)────────────────────────
|
|
283
|
+
// 修的是一条**静默漂移通道**:同一份座位契约此前有两份声明(desktop `shared/localSessions.ts`
|
|
284
|
+
// 15 型生产端 / web `lib/desktop/local-sessions-transport.ts` 9 型 `Seat*` 消费端),两边名字零重合
|
|
285
|
+
// ⇒ 编译器永远不会告诉你它们漂了。实测已漂,其中 `LocalSessionEvent.outcome` 在 web 镜像里整位
|
|
286
|
+
// 缺席 = [1947] 红③ 一直销不掉账的机制原因(宿主发了词,消费端连接词的位都没有)。
|
|
287
|
+
// 🔴 纯类型 + 常量 + 纯谓词,**零 import** —— portability 门的三个 EXPECTED_PACKAGES 一个字不变。
|
|
288
|
+
// 🔴 防再漂钉在文件末:`LocalSessionsApi` 的 verb/store 名由类型结构式派生 → 名字常量(编译期钉)
|
|
289
|
+
// → `LOCAL_SESSIONS_SPEC` 键集(run-seat-contract-keys-test 运行期对账)。加 verb 忘了加 SPEC
|
|
290
|
+
// 的后果不是报错而是 preload 不注册 channel、渲染端读到 undefined(假 affordance 家族)。
|
|
291
|
+
export * from './seatContract.js';
|
|
@@ -79,6 +79,8 @@ export interface TaskRequestInput {
|
|
|
79
79
|
promptProfile?: string;
|
|
80
80
|
enableFork?: boolean;
|
|
81
81
|
attachments?: Record<string, unknown>;
|
|
82
|
+
/** 预上传附件句柄(`uploadAttachment` 回执 id)。空数组 = 没带附件 ⇒ 整键不 stamp。 */
|
|
83
|
+
attachmentIds?: readonly string[];
|
|
82
84
|
finalVerification?: boolean;
|
|
83
85
|
limits?: Record<string, unknown>;
|
|
84
86
|
interactiveTools?: false;
|
|
@@ -28,7 +28,12 @@ export const REQUEST_FIELD_MATRIX = [
|
|
|
28
28
|
{ field: 'finalVerification', lanes: ['print'], live: true, why: 'P1-1 终验:无人值守车道才需要引擎自证;交互 REPL 由人当场看结果。#106 裁 B(让位+告知)后交互面默认关' },
|
|
29
29
|
{ field: 'limits', lanes: ['print'], live: true, why: 'P2-3-b:`-p` 的预算护栏(--max-* flag 族),交互 REPL 由人随时 Esc' },
|
|
30
30
|
{ field: 'interactiveTools', lanes: ['print'], live: true, why: '[909]B 件3:无人值守 stamp false,从 roster 源头灭掉 AskUserQuestion/plan 门。交互车道 stamp false 等于自废武功' },
|
|
31
|
-
// ──
|
|
31
|
+
// ── interactive 车道独有 ────────────────────────────────────────────────────────────────────
|
|
32
|
+
// ⚠️ 「interactive」**不等于**「TUI」:web 的 BFF lane 也报 `interactive`
|
|
33
|
+
// (`sema-web-client` `WEB_REQUEST_LANE`)。下面多数条目的理由写的是壳的 TUI 面 —— 它们是
|
|
34
|
+
// B4 从壳的三个构造器逐行读出来的,那时这条车道只有壳一个消费者。`attachmentIds` 是**反过来**
|
|
35
|
+
// 的第一条:它今天只有 web/desktop 在发,壳反而没有这个入口。别把本节读成「壳独有」。
|
|
36
|
+
{ field: 'attachmentIds', lanes: ['interactive'], live: true, why: '附件**字节通道**(uploadAttachment 回执 id;server ≥1.289 绑定会话并把文件物化进 run 的 attachments/)。有上传入口的端才有 id 可引用:web/desktop 有,壳走 images / 本地文件路径,`-p` 车道连 uploadAttachment 都没有 ⇒ print 缺席是**没有来源**,不是漏。🔴 与上面的 `attachments` 同名不同物(那是 turn 边界的配置键),两条永远不许合并' },
|
|
32
37
|
{ field: 'settings.ultracode', lanes: ['interactive'], live: true, why: 'design/111:sticky `/effort ultracode` 拨盘 + 当轮关键词嗅探,两个来源都只在交互面存在', gap: true },
|
|
33
38
|
{ field: 'systemPrompt', lanes: ['interactive'], live: false, why: 'CC QueryParams.systemPrompt;print 腿的 params 没有这一位' },
|
|
34
39
|
{ field: 'reasoningEffort', lanes: ['interactive'], live: false, why: '`/effort` 拨盘存在 AppState,print 无 AppState', gap: true },
|
|
@@ -106,6 +111,10 @@ export function buildTaskRequest(input, lane) {
|
|
|
106
111
|
...(on('promptProfile', input.promptProfile) ? { promptProfile: input.promptProfile } : {}),
|
|
107
112
|
...(on('enableFork', input.enableFork) ? { enableFork: input.enableFork } : {}),
|
|
108
113
|
...(on('attachments', input.attachments) ? { attachments: input.attachments } : {}),
|
|
114
|
+
// 空数组按 `images` 同款处理:没带附件与「带了 0 个附件」在 wire 上是同一件事,发小的那个。
|
|
115
|
+
...(on('attachmentIds', input.attachmentIds) && (input.attachmentIds?.length ?? 0) > 0
|
|
116
|
+
? { attachmentIds: [...(input.attachmentIds ?? [])] }
|
|
117
|
+
: {}),
|
|
109
118
|
...(on('clientContext', input.clientContext) ? { clientContext: input.clientContext } : {}),
|
|
110
119
|
...(on('scratchpadDir', input.scratchpadDir) ? { scratchpadDir: input.scratchpadDir } : {}),
|
|
111
120
|
...(on('finalVerification', input.finalVerification)
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seatContract — desktop/web **座位 IPC 契约的单一真源**(design/159 B18,2026-07-29)。
|
|
3
|
+
*
|
|
4
|
+
* 搬迁前的形:同一份契约有**两份声明**,而且跨仓名字零重合 ——
|
|
5
|
+
* · `sema-desktop/src/shared/localSessions.ts` = 15 个类型 + 3 个词表常量 + 6 个校验器 + SPEC(生产端)
|
|
6
|
+
* · `sema-web-client/ui/lib/desktop/local-sessions-transport.ts` = 9 个 `Seat*` 本地重声明(消费端)
|
|
7
|
+
* 两边名字不同 ⇒ **编译器永远不会告诉你它们漂了**。实测已经漂了(见下「漂移清单」),其中
|
|
8
|
+
* `LocalSessionEvent.outcome` 那一条是真 bug:宿主发了词、消费端的镜像里根本没有这个位,
|
|
9
|
+
* 于是「拒绝 / TTL 到点 / 人点了允许」在屏上长得一模一样 —— 正是 localSessions 自己在
|
|
10
|
+
* 「接缝记账」里写下的第 1 条,它一直没被销账的原因就是没有任何机制会让它红。
|
|
11
|
+
*
|
|
12
|
+
* 落点为什么是本包:desktop 与 web 都已经依赖 `@sema-agent/client-core`(0.11.5),而本文件是
|
|
13
|
+
* **纯类型 + 常量 + 纯谓词**,零值级 import、零 Node 内建 ⇒ 不动本包的可移植闭包(portability 门的
|
|
14
|
+
* `EXPECTED_PACKAGES_INDEX` 一个字不变)。
|
|
15
|
+
* ⚠️ 唯一一条 `import type`(`./effortWire.js` 的 `ReasoningEffort`)是**类型边**:portability 门
|
|
16
|
+
* 走值级传递闭包时按规则剥掉 type-only import(门里 `/^\s*type\b/` 那一跳),bundle 里一个字节都
|
|
17
|
+
* 不多。写成 import 而不是就地复刻那七档,是因为「座位闭集」与「wire 投影闭集」必须是**同一套词**
|
|
18
|
+
* ——复刻一份就等于埋一条只有真用户能发现的漂移(拨盘上的档在座位 lane 上静默到不了引擎)。
|
|
19
|
+
*
|
|
20
|
+
* ── 搬迁时按「真源为准」收敛的漂移(逐条,web 侧镜像 → 本文件)────────────────────────────
|
|
21
|
+
* 🔴 D1 `LocalSessionEvent.outcome` —— web 镜像整位缺席 ⇒ `tool_permission_resolved` 只会清面,
|
|
22
|
+
* 不会告诉用户为什么工具没跑。**真 bug**,本批只做「位补回来」;把词渲成可见痕迹是渲染面的活。
|
|
23
|
+
* D2 `SendMessageRequest.permissionMode` —— web 镜像是裸 `string`,真源是闭集 `PermissionModeIntent`,
|
|
24
|
+
* 而宿主入站门 `requireOptionalPermissionMode` 是**会抛的闭集门**。今日两边词表逐字相等
|
|
25
|
+
* (web `ComposerMode` 五档 ≡ `PERMISSION_MODE_INTENTS` 五档,已核),所以是**潜伏**不是现症:
|
|
26
|
+
* ComposerMode 哪天加一档,裸 string 一路放行到宿主门口才抛。收敛成闭集后当天编译红。
|
|
27
|
+
* D3 `AttachmentUploadReceipt.sha256` / `ToolPermissionRequest.createdAt` / `StartSessionOptions.cwd`
|
|
28
|
+
* / `.excludeTools` / `LocalSessionRecord.queuedMessages`·`model`·`engineSessionId` ——
|
|
29
|
+
* web 镜像缺位。都是「没接线」不是「接错线」(web 侧今日无消费点,已核),收敛后位在、待接。
|
|
30
|
+
* D4 `LocalSessionEvent.request` —— 真源有这个位,**宿主从不发**(session-host 零处写 `request:`);
|
|
31
|
+
* web 镜像没有它反而与运行时事实一致。真源保留该位(CC `fQe` 的 form 位),记在这里免得
|
|
32
|
+
* 下一棒把「web 没有」当成漏。
|
|
33
|
+
* D5 `EipcStoreClient.getStateSync` web 侧是 optional、真源是 required;`SeatSessionDirectoryRow.createdAt`
|
|
34
|
+
* web 侧 optional、真源 required。两条都是「更弱」方向,收敛到真源即可(宿主校验器
|
|
35
|
+
* `isLocalSessionRecord` 本来就要求 createdAt 在场,运行时保证一直成立)。
|
|
36
|
+
* · web 侧**没有**镜像、因而至今拿不到的面:队列三动词 / setFocusedSession / setModel /
|
|
37
|
+
* focusedSessionState / settingsState / `CHROME_EVENT_KINDS` / `ATTACHMENT_IDS_PER_TASK_CAP`。
|
|
38
|
+
* 本文件导出后它们对 web 首次可见(接不接线是渲染面的决定)。
|
|
39
|
+
*
|
|
40
|
+
* ── 沿革(以下正文自 sema-desktop/src/shared/localSessions.ts 逐字迁入)──────────────────────
|
|
41
|
+
* LocalSessions — the desktop host's session service contract, FORM-ported from the CC desktop
|
|
42
|
+
* app's `claude.web / LocalSessions` eipc interface (212 methods there; FIRST BATCH here = the four
|
|
43
|
+
* families clay scoped: session lifecycle / event subscription / interrupt / approvals).
|
|
44
|
+
*
|
|
45
|
+
* Naming: CC ns `claude.web` → `sema.web` (brand table). Method names kept verbatim where the CC
|
|
46
|
+
* method exists (`start`/`stop`/`interrupt`/`sendMessage`/`onEvent`/`onToolPermissionRequest`/
|
|
47
|
+
* `respondToToolPermission` are all real CC LocalSessions methods — see the 854-channel census).
|
|
48
|
+
*
|
|
49
|
+
* Event payload contract: ported in FORM from CC's `onEvent` runtime validator `fQe`
|
|
50
|
+
* (unpack `.vite/build/index.chunk-CnWKsyE_.js:369773`). The load-bearing property we keep
|
|
51
|
+
* byte-faithful ([1832] axiom 2, independently proven by CC's own code): on an otherwise
|
|
52
|
+
* fully-schema'd IPC surface, the `message`/`messages` fields (= CC session-vocabulary SDKMessage
|
|
53
|
+
* bodies, our transcript plane) are UNVALIDATED passthrough positions, while every other field is a
|
|
54
|
+
* neutral typed position. Our implementation体 differs (sema wire via @sema-agent/sdk +
|
|
55
|
+
* @sema-agent/client-core), the contract shape does not.
|
|
56
|
+
*/
|
|
57
|
+
import type { ReasoningEffort } from "./effortWire.js";
|
|
58
|
+
export type EipcMethodKind = "invoke" | "event";
|
|
59
|
+
export interface EipcMethodSpec {
|
|
60
|
+
kind: EipcMethodKind;
|
|
61
|
+
/** invoke: validates the impl's RESULT before it crosses back to the renderer (CC's outbound
|
|
62
|
+
* schema gate — `Result from method "<m>" in interface "<S>" failed to pass validation`).
|
|
63
|
+
* event: validates the PAYLOAD before `webContents.send` (CC's dispatcher-side validator, the
|
|
64
|
+
* `fQe` position for `LocalSessions.onEvent`). Absent = any result/payload passes (CC has
|
|
65
|
+
* `void`-returning methods with trivially-true validators). */
|
|
66
|
+
validate?: (value: unknown) => boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface EipcStoreSpec {
|
|
69
|
+
/** Validates the STATE payload both on read replies and on update pushes (fail-closed like the
|
|
70
|
+
* method gates). Absent = any state passes. */
|
|
71
|
+
validate?: (state: unknown) => boolean;
|
|
72
|
+
}
|
|
73
|
+
export interface EipcServiceSpec {
|
|
74
|
+
ns: string;
|
|
75
|
+
service: string;
|
|
76
|
+
methods: Record<string, EipcMethodSpec>;
|
|
77
|
+
/** `$store$` reactive states owned by this service (CC codegen form; see eipcStoreChannel). */
|
|
78
|
+
stores?: Record<string, EipcStoreSpec>;
|
|
79
|
+
}
|
|
80
|
+
/** Neutral session record (CC's `session` position is schema-gated by `aM`; ours is this shape). */
|
|
81
|
+
export interface LocalSessionRecord {
|
|
82
|
+
/** Host-minted stable session id — the id every event carries (CC form: sessionId on every event). */
|
|
83
|
+
sessionId: string;
|
|
84
|
+
/** Engine (service)-minted session id, known after the first reply; absent before it. */
|
|
85
|
+
engineSessionId?: string;
|
|
86
|
+
model?: string;
|
|
87
|
+
title?: string;
|
|
88
|
+
isRunning: boolean;
|
|
89
|
+
createdAt: string;
|
|
90
|
+
lastActivityAt?: string;
|
|
91
|
+
/** Host-side deferred queue (CC `deferredSends` seat, in submit order — drives the queued-message
|
|
92
|
+
* bar). Present (possibly empty) whenever the host projects the record during a running turn. */
|
|
93
|
+
queuedMessages?: Array<{
|
|
94
|
+
uuid: string;
|
|
95
|
+
text: string;
|
|
96
|
+
}>;
|
|
97
|
+
}
|
|
98
|
+
/** HITL request row (CC's `request` position is schema-gated by `S1`; ours is this shape, projected
|
|
99
|
+
* from the sema wire's PendingCheckpoint — see session-host). */
|
|
100
|
+
export interface ToolPermissionRequest {
|
|
101
|
+
/** Stable request key — echo back on respondToToolPermission. FOUR id domains, prefix-tagged
|
|
102
|
+
* (the prefix is what routes the decision back to the right wire verb — see session-host):
|
|
103
|
+
* `live:<approvalId>` = LIVE in-band tool-approval face (frame rides the token
|
|
104
|
+
* stream; unanswered → engine fail-CLOSED deny at its TTL);
|
|
105
|
+
* `durable:<engineSessionId>:<callId>` = the durable checkpoint class (approvals inbox);
|
|
106
|
+
* `question:<questionId>` = LIVE `AskUserQuestion` (the agent's own structured ask;
|
|
107
|
+
* unanswered → engine fail-OPEN: the model takes the
|
|
108
|
+
* headless default `{answers:[]}` at the 5-min TTL, which is
|
|
109
|
+
* why leaving this family unrendered is silent, [1947] 红④);
|
|
110
|
+
* `elicit:<elicitationId>` = LIVE inbound-MCP elicitation (a server asks the user for
|
|
111
|
+
* form input mid-tool-call).
|
|
112
|
+
* The three LIVE domains are same-replica + non-durable: a late/duplicate decision 404s, which the
|
|
113
|
+
* host treats as "already released" (dismiss), never as a retryable error. */
|
|
114
|
+
requestId: string;
|
|
115
|
+
sessionId: string;
|
|
116
|
+
/** For the two ask families this is the CC tool vocabulary name the renderer branches on:
|
|
117
|
+
* `AskUserQuestion` (question domain) / `McpElicitation` (elicit domain). */
|
|
118
|
+
toolName?: string | null;
|
|
119
|
+
/** The ENGINE's tool-call id (`call_…`) — the SAME id the assistant message's `tool_use` block
|
|
120
|
+
* carries, so a renderer can key the approval onto the tool card that actually raised it.
|
|
121
|
+
* Present on tool-approval rows from server ≥1.307; `null` when the wire did not offer one
|
|
122
|
+
* (old server, or the ask families, which have no tool call of their own). NEVER synthesized. */
|
|
123
|
+
toolCallId?: string | null;
|
|
124
|
+
/** UNTRUSTED, service-redacted tool args / question payload (render-only, never re-fed). */
|
|
125
|
+
input?: unknown;
|
|
126
|
+
/** UNTRUSTED, service-redacted engine ask text (live face only) — display only. */
|
|
127
|
+
message?: string;
|
|
128
|
+
/** design/80 D-1 TOCTOU binding pair (durable face only) — echo VERBATIM into the decision.
|
|
129
|
+
* 🔴 出站**只读**位:回程的 verbatim 回显由宿主自己的登记表做(session-host 的
|
|
130
|
+
* `row.boundCallId/boundInputHash`),`ToolPermissionDecision` 因此没有对应席位 —— 渲染端
|
|
131
|
+
* 拿到它只为展示,绝不需要(也不该)自己回传。 */
|
|
132
|
+
boundCallId?: string;
|
|
133
|
+
boundInputHash?: string;
|
|
134
|
+
createdAt?: number;
|
|
135
|
+
}
|
|
136
|
+
export interface LocalSessionEvent {
|
|
137
|
+
type: string;
|
|
138
|
+
sessionId: string;
|
|
139
|
+
/** 🔴 transcript-plane passthrough (SDKMessage body) — deliberately UNVALIDATED (fQe posture). */
|
|
140
|
+
message?: unknown;
|
|
141
|
+
/** 🔴 transcript-plane passthrough batch — array-checked only, elements unvalidated (fQe posture). */
|
|
142
|
+
messages?: unknown[];
|
|
143
|
+
error?: string;
|
|
144
|
+
errorCategory?: string;
|
|
145
|
+
code?: number;
|
|
146
|
+
/** CC `fQe` 的 form 位。🔴 **本宿主从不发这个位**(session-host 零处写 `request:`;HITL 行走
|
|
147
|
+
* 独立的 `onToolPermissionRequest` 通道)—— 位保留是为了与 CC 的 fQe 逐位同形,消费端不该
|
|
148
|
+
* 为它写分支(见文件头 D4)。 */
|
|
149
|
+
request?: ToolPermissionRequest;
|
|
150
|
+
data?: string;
|
|
151
|
+
sources?: string[];
|
|
152
|
+
permissionMode?: string;
|
|
153
|
+
session?: LocalSessionRecord;
|
|
154
|
+
userMessageUuid?: string;
|
|
155
|
+
hasPreClearSession?: boolean;
|
|
156
|
+
/** On `tool_permission_resolved`: which request settled (so a native notification for it can be
|
|
157
|
+
* dismissed — CC `closePermissionNotification(requestId)`). */
|
|
158
|
+
requestId?: string;
|
|
159
|
+
/**
|
|
160
|
+
* On `tool_permission_resolved`: HOW it settled ([1947] 红③). Without this the renderer only ever
|
|
161
|
+
* learned that a card should disappear — a TTL expiry, a disconnect-settle and a human's own
|
|
162
|
+
* "allow" were indistinguishable, so a REFUSAL rendered as a card silently vanishing and the user
|
|
163
|
+
* was never told why the tool did not run.
|
|
164
|
+
*
|
|
165
|
+
* Words currently emitted (see HITL_RESOLUTION_OUTCOMES): the tool-approval wire's own
|
|
166
|
+
* `allowed`/`denied`/`expired`, the question wire's `answered`/`unanswered`, the elicitation
|
|
167
|
+
* wire's `accept`/`decline`/`cancel`. ABSENT is meaningful and honest: the durable inbox sweep
|
|
168
|
+
* learns only that a row is gone, never why.
|
|
169
|
+
*
|
|
170
|
+
* 🔴 Validated as a plain string, NOT against a closed set, deliberately: the words originate on
|
|
171
|
+
* the wire, and a fail-closed gate keyed to today's vocabulary would DROP THE WHOLE EVENT (card
|
|
172
|
+
* stuck on screen forever) the day the server adds an outcome word. Renderers branch on the words
|
|
173
|
+
* they know and treat an unknown one as "settled, reason unknown".
|
|
174
|
+
*/
|
|
175
|
+
outcome?: string;
|
|
176
|
+
/** On `chrome_event`: one client-core ChromeEvent arm (the NEUTRAL typed chrome vocabulary —
|
|
177
|
+
* [1832] axiom 2's non-transcript half). Only the renderer-wired arm kinds cross the wire
|
|
178
|
+
* (see CHROME_EVENT_KINDS); the outbound gate checks kind membership + the laneProof discipline. */
|
|
179
|
+
chrome?: {
|
|
180
|
+
kind: string;
|
|
181
|
+
laneProof?: unknown;
|
|
182
|
+
[k: string]: unknown;
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The resolution words the host currently puts on `tool_permission_resolved.outcome` ([1947] 红③).
|
|
187
|
+
* DOCUMENTARY + test anchor only — the gate does NOT enforce membership (see the field's doc: a
|
|
188
|
+
* closed set would fail-close the whole event on the day the wire grows a word). Grouped by family:
|
|
189
|
+
* - tool approval (wire `ToolApprovalFrame.outcome`, + our own settle sweep): allowed/denied/expired
|
|
190
|
+
* - question (wire `QuestionFrame.outcome`): answered/unanswered
|
|
191
|
+
* - elicitation (wire `ElicitationFrame.action`): accept/decline/cancel
|
|
192
|
+
*/
|
|
193
|
+
export declare const HITL_RESOLUTION_OUTCOMES: readonly string[];
|
|
194
|
+
/**
|
|
195
|
+
* 接缝记账 — what the HOST now guarantees vs what a RENDERER still owes ([1947] 红③/红④ split).
|
|
196
|
+
*
|
|
197
|
+
* The host half is done and pinned by desktop's `scripts/run-hitl-frame-transit-fence-test.mjs`:
|
|
198
|
+
* every live ask family reaches `onToolPermissionRequest` with its own requestId domain, every
|
|
199
|
+
* resolution reaches `onEvent` with an `outcome`, and every decision routes to the right wire verb.
|
|
200
|
+
*
|
|
201
|
+
* The renderer half is where these become VISIBLE, and until it lands the data is present but
|
|
202
|
+
* unread (stating it plainly so nobody reads "the host emits it" as "the user sees it"):
|
|
203
|
+
* 1. `tool_permission_resolved.outcome` — an `expired` / `denied` / `unanswered` resolution must
|
|
204
|
+
* leave a visible refusal trace. Today the seat transport clears the surface and drops the
|
|
205
|
+
* word, so a refusal is still indistinguishable from an approval on screen.
|
|
206
|
+
* 🔴 B18(2026-07-29)查明这条一直不销账的**机制原因**:web 侧的镜像类型里连 `outcome` 这个
|
|
207
|
+
* 位都没有,所以「没读」在两边都编译得过。本批把位收进真源 = 让它至少可见于类型面。
|
|
208
|
+
* 2. `toolCallId` — now real (not null), so an approval can be anchored to the tool card that
|
|
209
|
+
* raised it instead of "the most recent tool_start" (an anchor that lands on the wrong card
|
|
210
|
+
* when a prior tool is still running).
|
|
211
|
+
* 3. `question:` rows need NO new renderer code: they arrive as `toolName: "AskUserQuestion"` with
|
|
212
|
+
* `input.questions`, which is exactly what the seat's ask-user-question surface already reads,
|
|
213
|
+
* and its answer envelope is exactly what this host expects back.
|
|
214
|
+
* 4. 座位 wire 补齐批 (2026-07-28) closed two entries that used to live here as "如实,未做":
|
|
215
|
+
* `permissionMode` now has a seat on `sendMessage` (was: the composer's Plan badge never
|
|
216
|
+
* crossed and the turn ran with the engine default), and the attachment BYTE channel exists
|
|
217
|
+
* (`uploadAttachment` + `sendMessage.attachmentIds`) — so a seat renderer no longer has to
|
|
218
|
+
* simulate "attachment ready" for a file nothing received. What is still NOT here, stated
|
|
219
|
+
* plainly: plan REVISE feedback text (the decide face has no `reason` seat, so revise maps to
|
|
220
|
+
* a bare deny) and a per-session `permissionMode` write (the wire's is per-TURN only).
|
|
221
|
+
* 5. `elicit:` rows have no dedicated surface yet: they fall through to the generic approval card,
|
|
222
|
+
* which shows the MCP server's prompt and can accept/decline but CANNOT collect form values.
|
|
223
|
+
* A real form surface (built from `input.requestedSchema`) is the renderer's remaining piece;
|
|
224
|
+
* until then an approve carries no content — the host never invents form values. That generic
|
|
225
|
+
* card also offers "Always allow (this session)", which this domain has no concept of: the host
|
|
226
|
+
* ignores `remember` here rather than pretend, so the renderer should hide that control for
|
|
227
|
+
* `elicit:` rows (a button that changes nothing is the false-affordance family again).
|
|
228
|
+
*/
|
|
229
|
+
/** Chrome arms wired to the renderer position (batch 4); other arms stay host-side on the ring. */
|
|
230
|
+
export declare const CHROME_EVENT_KINDS: readonly string[];
|
|
231
|
+
export interface StartSessionOptions {
|
|
232
|
+
model?: string;
|
|
233
|
+
title?: string;
|
|
234
|
+
/** Working directory for the session's hand tools (TaskRequest.cwd). Default = a per-session dir
|
|
235
|
+
* under the host's workspace base. A UI that picks a project directory passes it here — CC desktop
|
|
236
|
+
* form (session.cwd / worktreePath). Honored only on the local host-adapter engine. */
|
|
237
|
+
cwd?: string;
|
|
238
|
+
/** Wire TRUE-UNMOUNT list (TaskRequest.excludeTools, server ≥1.221): named tools' schemas never
|
|
239
|
+
* reach the model for ANY turn of this session. Tighten-only. */
|
|
240
|
+
excludeTools?: string[];
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* PERMISSION MODE INTENT — the RAW mode the composer is showing, carried verbatim to the engine as
|
|
244
|
+
* `TaskRequest.permissionMode` (SDK types.d.ts:259; the SERVICE interprets it, axis-aware and
|
|
245
|
+
* tighten-only: `plan` ⇒ mount `present_plan` + read-only hands = CC EnterPlanMode).
|
|
246
|
+
*
|
|
247
|
+
* 🔴 Why this seat exists at all (红批注 web#1): without it, a user who switched the composer to
|
|
248
|
+
* Plan mode inside the desktop seat got a turn that ran with the engine's DEFAULT mode — the badge
|
|
249
|
+
* said "plan", the engine was never told, and the agent happily edited files. A mode the UI shows
|
|
250
|
+
* but never sends is the false-affordance family with teeth.
|
|
251
|
+
*
|
|
252
|
+
* The vocabulary is the wire's own (identical to the web client's `ComposerMode`), so the seat
|
|
253
|
+
* passes the word through unchanged rather than re-deriving core fields locally — one interpretation
|
|
254
|
+
* across TUI + web + desktop. 🔴 B18 起 web 侧的镜像位也是这个闭集(此前是裸 `string`):
|
|
255
|
+
* ComposerMode 加一档而宿主闭集门不认的那天,红在编译期而不是用户提交那一刻。
|
|
256
|
+
*/
|
|
257
|
+
export type PermissionModeIntent = "default" | "plan" | "acceptEdits" | "bypassPermissions" | "auto";
|
|
258
|
+
/** The closed set the inbound gate checks. CLOSED deliberately, unlike `outcome` above: this word
|
|
259
|
+
* ORIGINATES in our own renderer and goes OUT to a wire that 400s on anything else, so an unknown
|
|
260
|
+
* value is a local bug we want reported at the door — not an unknown future wire word we must
|
|
261
|
+
* tolerate. (The direction of travel is what decides open vs closed, not the field's shape.) */
|
|
262
|
+
export declare const PERMISSION_MODE_INTENTS: readonly PermissionModeIntent[];
|
|
263
|
+
/** Per-task attachment cap — the server's own limit (`attachmentIds` > 16 ⇒ 400). Gated here so the
|
|
264
|
+
* seat gets a precise local error instead of a wire 400 halfway through a turn. */
|
|
265
|
+
export declare const ATTACHMENT_IDS_PER_TASK_CAP = 16;
|
|
266
|
+
/** The per-turn `reasoningEffort` accept-set — core's ThinkingLevel tiers, re-exported from the
|
|
267
|
+
* package's own wire projection (`effortWire.ReasoningEffort`) rather than re-typed here.
|
|
268
|
+
*
|
|
269
|
+
* CLOSED for the same reason `PERMISSION_MODE_INTENTS` is: the word ORIGINATES in our own composer
|
|
270
|
+
* and travels OUT to a wire that 400s (fail-loud) on an unknown tier — so an unknown value is a
|
|
271
|
+
* local bug we want reported at the seat's door, not a future wire word to tolerate. `ultracode` is
|
|
272
|
+
* deliberately NOT a tier here: in CC it is a standalone `settings.ultracode` preset boolean that
|
|
273
|
+
* COEXISTS with `xhigh` (see `SeatTurnSettings`), and folding it into this axis is exactly how the
|
|
274
|
+
* orchestration axis gets eaten by the reasoning axis. */
|
|
275
|
+
export declare const SEAT_REASONING_EFFORTS: readonly ReasoningEffort[];
|
|
276
|
+
/** The `settings` sub-object a seat turn may carry. A CLOSED set with its own name list + pin,
|
|
277
|
+
* exactly like the top-level request: `SEND_MESSAGE_REQUEST_KEYS` checks `keyof SendMessageRequest`
|
|
278
|
+
* (top-level names), so a dotted `"settings.ultracode"` entry could never be pinned by `Covers` —
|
|
279
|
+
* the shape that stays consistent with the existing closed sets is one top-level key plus a nested
|
|
280
|
+
* closed set of its own. */
|
|
281
|
+
export interface SeatTurnSettings {
|
|
282
|
+
/** CC's `settings.ultracode` PRESET boolean (max thinking × workflow orchestration, service ≥1.48.0
|
|
283
|
+
* reads `body.settings.ultracode` and expands it). NOT a seventh effort tier — the dial separately
|
|
284
|
+
* coerces the reasoning axis to `xhigh`, and collapsing the two loses the orchestration half
|
|
285
|
+
* (`ultracodeWireCaps` in this package records that semantics verbatim). */
|
|
286
|
+
ultracode?: boolean;
|
|
287
|
+
}
|
|
288
|
+
export declare const SEAT_TURN_SETTINGS_KEYS: readonly ["ultracode"];
|
|
289
|
+
export interface SendMessageRequest {
|
|
290
|
+
sessionId: string;
|
|
291
|
+
text: string;
|
|
292
|
+
model?: string;
|
|
293
|
+
/** Client-supplied echo key for the user message (CC form: userMessageUuid); minted if absent. */
|
|
294
|
+
userMessageUuid?: string;
|
|
295
|
+
/** Composer permission mode for THIS turn → `TaskRequest.permissionMode`. Omit/`default` ⇒ no stamp. */
|
|
296
|
+
permissionMode?: PermissionModeIntent;
|
|
297
|
+
/** Pre-uploaded attachment handles (`uploadAttachment` receipts) → `TaskRequest.attachmentIds`.
|
|
298
|
+
* Bytes never ride this call: they went up once through the dedicated verb and the engine
|
|
299
|
+
* materializes them into the run's `attachments/` dir. */
|
|
300
|
+
attachmentIds?: string[];
|
|
301
|
+
/** Per-turn `/effort` dial pick → `TaskRequest.reasoningEffort`. Omit ⇒ the engine's own default.
|
|
302
|
+
* Closed set (`SEAT_REASONING_EFFORTS`) — gate it at the host's door, not at the wire.
|
|
303
|
+
*
|
|
304
|
+
* 🔴 Why this position exists (web 收编第一批, 2026-07-29): it did NOT, and the absence was
|
|
305
|
+
* STRUCTURAL — the seat lane's only payload is this closed-set interface, so the composer's effort
|
|
306
|
+
* dial had nowhere to put the user's pick and the web client honestly disabled the control on the
|
|
307
|
+
* desktop seat lane. "The control is missing" was the visible symptom; the missing wire position
|
|
308
|
+
* was the cause. */
|
|
309
|
+
reasoningEffort?: ReasoningEffort;
|
|
310
|
+
/** Keep this turn's background processes alive past the turn boundary → `TaskRequest.
|
|
311
|
+
* retainBackgroundProcesses`. Same structural-absence story as `reasoningEffort` above; the
|
|
312
|
+
* shell's lane defaults it ON (2026-07-26 flip), so a seat that cannot send it is not "defaulting
|
|
313
|
+
* to the same thing" — it is letting the ENGINE's default decide, which is a different decision. */
|
|
314
|
+
retainBackgroundProcesses?: boolean;
|
|
315
|
+
/** Per-turn settings sub-object → `TaskRequest.settings`. Closed shape (`SeatTurnSettings`);
|
|
316
|
+
* today's only member is the `ultracode` preset boolean. */
|
|
317
|
+
settings?: SeatTurnSettings;
|
|
318
|
+
}
|
|
319
|
+
/** `uploadAttachment` receipt — the SDK's `AttachmentInfo` verbatim (server POST /v1/attachments 201).
|
|
320
|
+
* `name` is the SERVER's sanitized basename, not the local filename: it is the name the engine
|
|
321
|
+
* materializes, so the renderer must display THAT one. */
|
|
322
|
+
export interface AttachmentUploadReceipt {
|
|
323
|
+
id: string;
|
|
324
|
+
name: string;
|
|
325
|
+
mime: string;
|
|
326
|
+
sha256: string;
|
|
327
|
+
sizeBytes: number;
|
|
328
|
+
}
|
|
329
|
+
/** One row of the seat's model catalog (`GET /v1/models`, non-secret projection: the service already
|
|
330
|
+
* strips baseUrl/apiKey/headers). `contextWindow`/`maxOutputTokens` are soft-degrade extras. */
|
|
331
|
+
export interface SeatModelInfo {
|
|
332
|
+
id: string;
|
|
333
|
+
name?: string;
|
|
334
|
+
provider?: string;
|
|
335
|
+
contextWindow?: number;
|
|
336
|
+
maxOutputTokens?: number;
|
|
337
|
+
}
|
|
338
|
+
/** `listModels` envelope. `defaultId` = the deployment's resolved default model id (display only —
|
|
339
|
+
* the per-turn choice always rides `sendMessage({model})`). */
|
|
340
|
+
export interface SeatModelCatalog {
|
|
341
|
+
models: SeatModelInfo[];
|
|
342
|
+
defaultId?: string;
|
|
343
|
+
}
|
|
344
|
+
export interface SendMessageAck {
|
|
345
|
+
sessionId: string;
|
|
346
|
+
userMessageUuid: string;
|
|
347
|
+
/** true = a turn was running, the message entered the deferred queue (CC sendMessage semantics)
|
|
348
|
+
* instead of starting a turn; absent/false = the turn started immediately. */
|
|
349
|
+
queued?: boolean;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* One decision, four wire verbs — the requestId's domain picks which (session-host routes it):
|
|
353
|
+
* `live:` → POST /v1/tool-approvals/:id/respond (allow | allow_session | deny)
|
|
354
|
+
* `durable:` → POST /v1/approvals/:sessionId/decide (+ the TOCTOU binding pair, echoed verbatim)
|
|
355
|
+
* `question:` → POST /v1/questions/:id/respond (approve ⇒ `answer`; deny ⇒ `{answers:[]}`,
|
|
356
|
+
* the wire's own NO_HUMAN — releases the ask NOW instead of hanging it to its TTL)
|
|
357
|
+
* `elicit:` → POST /v1/elicitations/:id/respond (approve ⇒ accept + `updatedInput` as the
|
|
358
|
+
* form content; deny ⇒ decline)
|
|
359
|
+
*/
|
|
360
|
+
export interface ToolPermissionDecision {
|
|
361
|
+
sessionId: string;
|
|
362
|
+
requestId: string;
|
|
363
|
+
decision: "approve" | "deny";
|
|
364
|
+
/** AskUserQuestion answer (approve only) — `{answers:[{header, selected: string[], note?}]}`,
|
|
365
|
+
* keyed by the question's HEADER, `selected` echoing the EXACT option labels the user saw (core
|
|
366
|
+
* fences `selected ⊆ options`). Malformed shapes are refused at the door, never coerced: a
|
|
367
|
+
* silently emptied answer is the user's choice being thrown away without anyone noticing. */
|
|
368
|
+
answer?: unknown;
|
|
369
|
+
/** Approve-with-edit rewritten args (design/37 last-wins). On the `elicit:` domain this position
|
|
370
|
+
* carries the submitted FORM CONTENT — a flat record of `string|number|boolean|string[]`
|
|
371
|
+
* (the only shape the wire accepts; anything else is refused, not filtered). */
|
|
372
|
+
updatedInput?: unknown;
|
|
373
|
+
/** "Don't ask again this session" (approve only). */
|
|
374
|
+
remember?: "session";
|
|
375
|
+
}
|
|
376
|
+
/** Renderer-side `$store$` client (CC form: read + subscribe; `update` channel is the push). */
|
|
377
|
+
export interface EipcStoreClient<T> {
|
|
378
|
+
getState(): Promise<T>;
|
|
379
|
+
getStateSync(): T;
|
|
380
|
+
onStateChange(cb: (state: T) => void): () => void;
|
|
381
|
+
}
|
|
382
|
+
export interface SettingsState {
|
|
383
|
+
defaultModel: string | null;
|
|
384
|
+
engineVersion?: string;
|
|
385
|
+
dataRoot?: string;
|
|
386
|
+
}
|
|
387
|
+
export interface SessionSearchResult {
|
|
388
|
+
sessionId: string;
|
|
389
|
+
title?: string;
|
|
390
|
+
matches: Array<{
|
|
391
|
+
snippet: string;
|
|
392
|
+
}>;
|
|
393
|
+
}
|
|
394
|
+
/** Renderer-facing surface exposed by the mainView preload as `sema.web.LocalSessions`. */
|
|
395
|
+
export interface LocalSessionsApi {
|
|
396
|
+
start(opts?: StartSessionOptions): Promise<LocalSessionRecord>;
|
|
397
|
+
sendMessage(req: SendMessageRequest): Promise<SendMessageAck>;
|
|
398
|
+
interrupt(req: {
|
|
399
|
+
sessionId: string;
|
|
400
|
+
}): Promise<{
|
|
401
|
+
interrupted: boolean;
|
|
402
|
+
}>;
|
|
403
|
+
stop(req: {
|
|
404
|
+
sessionId: string;
|
|
405
|
+
}): Promise<{
|
|
406
|
+
stopped: boolean;
|
|
407
|
+
}>;
|
|
408
|
+
respondToToolPermission(req: ToolPermissionDecision): Promise<{
|
|
409
|
+
ok: boolean;
|
|
410
|
+
}>;
|
|
411
|
+
/** Queue family (CC LocalSessionManager semantics; booleans, false = too-late/no-op form). */
|
|
412
|
+
cancelQueuedMessage(sessionId: string, userMessageUuid: string): Promise<boolean>;
|
|
413
|
+
/** CC signature: (sessionId, uuid, TARGET uuid) — position the message relative to the target
|
|
414
|
+
* (after it when moving down, before it when moving up); NOT an index. */
|
|
415
|
+
reorderQueuedMessage(sessionId: string, userMessageUuid: string, targetUserMessageUuid: string): Promise<boolean>;
|
|
416
|
+
promoteQueuedMessage(sessionId: string, userMessageUuid: string): Promise<boolean>;
|
|
417
|
+
/** CC census verbs (batch 4). setFocusedSession(null) clears focus. */
|
|
418
|
+
setFocusedSession(sessionId: string | null): Promise<{
|
|
419
|
+
ok: boolean;
|
|
420
|
+
}>;
|
|
421
|
+
setModel(sessionId: string, model: string): Promise<boolean>;
|
|
422
|
+
getTranscript(sessionId: string): Promise<{
|
|
423
|
+
sessionId: string;
|
|
424
|
+
messages: unknown[];
|
|
425
|
+
}>;
|
|
426
|
+
searchSessions(query: string): Promise<{
|
|
427
|
+
results: SessionSearchResult[];
|
|
428
|
+
}>;
|
|
429
|
+
/** Attachment BYTE channel (红批注 web#1 的另一半). Upload first, then reference the receipt id
|
|
430
|
+
* from `sendMessage({attachmentIds})`. Rejects (413/415/501 → thrown typed error) surface to the
|
|
431
|
+
* renderer as-is: a shelf row that says "ready" for a file the engine never received is the
|
|
432
|
+
* same lie as a mode badge that never crossed the wire. */
|
|
433
|
+
uploadAttachment(req: {
|
|
434
|
+
name: string;
|
|
435
|
+
mimeType?: string;
|
|
436
|
+
bytes: Uint8Array;
|
|
437
|
+
}): Promise<AttachmentUploadReceipt>;
|
|
438
|
+
/** Read-only model catalog (engine `GET /v1/models`). Empty `models` = the deployment offers no
|
|
439
|
+
* catalog; the renderer must degrade honestly rather than fall back to a fixture list. */
|
|
440
|
+
listModels(): Promise<SeatModelCatalog>;
|
|
441
|
+
onEvent(cb: (ev: LocalSessionEvent) => void): () => void;
|
|
442
|
+
onToolPermissionRequest(cb: (req: ToolPermissionRequest) => void): () => void;
|
|
443
|
+
/** `$store$` reactive states (read + subscribe; writes go through the verbs above). */
|
|
444
|
+
sessionsStateStore: EipcStoreClient<{
|
|
445
|
+
sessions: LocalSessionRecord[];
|
|
446
|
+
}>;
|
|
447
|
+
focusedSessionStateStore: EipcStoreClient<{
|
|
448
|
+
sessionId: string | null;
|
|
449
|
+
}>;
|
|
450
|
+
settingsStateStore: EipcStoreClient<SettingsState>;
|
|
451
|
+
}
|
|
452
|
+
export declare function isLocalSessionRecord(s: unknown): s is LocalSessionRecord;
|
|
453
|
+
export declare function isToolPermissionRequest(r: unknown): r is ToolPermissionRequest;
|
|
454
|
+
/**
|
|
455
|
+
* onEvent payload validator — line-for-line FORM port of CC's `fQe`
|
|
456
|
+
* (`.vite/build/index.chunk-CnWKsyE_.js:369773`). Field-by-field mapping:
|
|
457
|
+
* - `type`/`sessionId`: required strings (same);
|
|
458
|
+
* - `message`: CC's minified source reads `typeof e.message<"u"` inside a comma expression whose
|
|
459
|
+
* value is discarded — i.e. NO validation, a deliberate transcript-plane passthrough. Same here.
|
|
460
|
+
* - `messages`: array-checked, elements `every(t=>!0)` (constant true) — array shape only. Same here.
|
|
461
|
+
* - `error`/`errorCategory`: optional strings (same); `code`: optional number (same);
|
|
462
|
+
* - `request`: CC gates with `S1` (ToolPermissionRequest schema) → our isToolPermissionRequest;
|
|
463
|
+
* - `session`: CC gates with `aM` (SessionRecord schema) → our isLocalSessionRecord;
|
|
464
|
+
* - `data`: optional string / `sources`: optional string[] / `permissionMode`: optional string /
|
|
465
|
+
* `userMessageUuid`: optional string / `hasPreClearSession`: optional boolean (same);
|
|
466
|
+
* - CC fields we do not emit in this batch (`tccFolderKind`/`initializationStatus`/`fsFile`/
|
|
467
|
+
* `localMcpServers`/`refusalFallbackPrompt`) are not carried — their positions return when the
|
|
468
|
+
* corresponding service families are ported.
|
|
469
|
+
*/
|
|
470
|
+
export declare function isLocalSessionEvent(v: unknown): v is LocalSessionEvent;
|
|
471
|
+
export declare function isAttachmentUploadReceipt(v: unknown): v is AttachmentUploadReceipt;
|
|
472
|
+
export declare function isSeatModelCatalog(v: unknown): v is SeatModelCatalog;
|
|
473
|
+
export declare function isSendMessageAck(v: unknown): v is SendMessageAck;
|
|
474
|
+
export declare const LOCAL_SESSIONS_SPEC: EipcServiceSpec;
|
|
475
|
+
/** verb 名清单 = `LOCAL_SESSIONS_SPEC.methods` 应有的键集(逐元素相等,门里断言)。 */
|
|
476
|
+
export declare const SEAT_METHOD_NAMES: readonly ["start", "sendMessage", "interrupt", "stop", "respondToToolPermission", "cancelQueuedMessage", "reorderQueuedMessage", "promoteQueuedMessage", "setFocusedSession", "setModel", "getTranscript", "searchSessions", "uploadAttachment", "listModels", "onEvent", "onToolPermissionRequest"];
|
|
477
|
+
/** `$store$` 属性名清单;SPEC 的 store 键 + `"Store"` 必须逐条等于这里的名字(门里断言)。 */
|
|
478
|
+
export declare const SEAT_STORE_PROPS: readonly ["sessionsStateStore", "focusedSessionStateStore", "settingsStateStore"];
|
|
479
|
+
/** 事件式(main→renderer push)的 verb —— SPEC 里 `kind: "event"` 的那一撮,门里逐元素对账。 */
|
|
480
|
+
export declare const SEAT_EVENT_METHOD_NAMES: readonly ["onEvent", "onToolPermissionRequest"];
|
|
481
|
+
export declare const LOCAL_SESSION_EVENT_KEYS: readonly ["type", "sessionId", "message", "messages", "error", "errorCategory", "code", "request", "data", "sources", "permissionMode", "session", "userMessageUuid", "hasPreClearSession", "requestId", "outcome", "chrome"];
|
|
482
|
+
export declare const TOOL_PERMISSION_REQUEST_KEYS: readonly ["requestId", "sessionId", "toolName", "toolCallId", "input", "message", "boundCallId", "boundInputHash", "createdAt"];
|
|
483
|
+
export declare const LOCAL_SESSION_RECORD_KEYS: readonly ["sessionId", "engineSessionId", "model", "title", "isRunning", "createdAt", "lastActivityAt", "queuedMessages"];
|
|
484
|
+
export declare const SEND_MESSAGE_REQUEST_KEYS: readonly ["sessionId", "text", "model", "userMessageUuid", "permissionMode", "attachmentIds", "reasoningEffort", "retainBackgroundProcesses", "settings"];
|
|
485
|
+
export declare const TOOL_PERMISSION_DECISION_KEYS: readonly ["sessionId", "requestId", "decision", "answer", "updatedInput", "remember"];
|
|
486
|
+
export declare const START_SESSION_OPTION_KEYS: readonly ["model", "title", "cwd", "excludeTools"];
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seatContract — desktop/web **座位 IPC 契约的单一真源**(design/159 B18,2026-07-29)。
|
|
3
|
+
*
|
|
4
|
+
* 搬迁前的形:同一份契约有**两份声明**,而且跨仓名字零重合 ——
|
|
5
|
+
* · `sema-desktop/src/shared/localSessions.ts` = 15 个类型 + 3 个词表常量 + 6 个校验器 + SPEC(生产端)
|
|
6
|
+
* · `sema-web-client/ui/lib/desktop/local-sessions-transport.ts` = 9 个 `Seat*` 本地重声明(消费端)
|
|
7
|
+
* 两边名字不同 ⇒ **编译器永远不会告诉你它们漂了**。实测已经漂了(见下「漂移清单」),其中
|
|
8
|
+
* `LocalSessionEvent.outcome` 那一条是真 bug:宿主发了词、消费端的镜像里根本没有这个位,
|
|
9
|
+
* 于是「拒绝 / TTL 到点 / 人点了允许」在屏上长得一模一样 —— 正是 localSessions 自己在
|
|
10
|
+
* 「接缝记账」里写下的第 1 条,它一直没被销账的原因就是没有任何机制会让它红。
|
|
11
|
+
*
|
|
12
|
+
* 落点为什么是本包:desktop 与 web 都已经依赖 `@sema-agent/client-core`(0.11.5),而本文件是
|
|
13
|
+
* **纯类型 + 常量 + 纯谓词**,零值级 import、零 Node 内建 ⇒ 不动本包的可移植闭包(portability 门的
|
|
14
|
+
* `EXPECTED_PACKAGES_INDEX` 一个字不变)。
|
|
15
|
+
* ⚠️ 唯一一条 `import type`(`./effortWire.js` 的 `ReasoningEffort`)是**类型边**:portability 门
|
|
16
|
+
* 走值级传递闭包时按规则剥掉 type-only import(门里 `/^\s*type\b/` 那一跳),bundle 里一个字节都
|
|
17
|
+
* 不多。写成 import 而不是就地复刻那七档,是因为「座位闭集」与「wire 投影闭集」必须是**同一套词**
|
|
18
|
+
* ——复刻一份就等于埋一条只有真用户能发现的漂移(拨盘上的档在座位 lane 上静默到不了引擎)。
|
|
19
|
+
*
|
|
20
|
+
* ── 搬迁时按「真源为准」收敛的漂移(逐条,web 侧镜像 → 本文件)────────────────────────────
|
|
21
|
+
* 🔴 D1 `LocalSessionEvent.outcome` —— web 镜像整位缺席 ⇒ `tool_permission_resolved` 只会清面,
|
|
22
|
+
* 不会告诉用户为什么工具没跑。**真 bug**,本批只做「位补回来」;把词渲成可见痕迹是渲染面的活。
|
|
23
|
+
* D2 `SendMessageRequest.permissionMode` —— web 镜像是裸 `string`,真源是闭集 `PermissionModeIntent`,
|
|
24
|
+
* 而宿主入站门 `requireOptionalPermissionMode` 是**会抛的闭集门**。今日两边词表逐字相等
|
|
25
|
+
* (web `ComposerMode` 五档 ≡ `PERMISSION_MODE_INTENTS` 五档,已核),所以是**潜伏**不是现症:
|
|
26
|
+
* ComposerMode 哪天加一档,裸 string 一路放行到宿主门口才抛。收敛成闭集后当天编译红。
|
|
27
|
+
* D3 `AttachmentUploadReceipt.sha256` / `ToolPermissionRequest.createdAt` / `StartSessionOptions.cwd`
|
|
28
|
+
* / `.excludeTools` / `LocalSessionRecord.queuedMessages`·`model`·`engineSessionId` ——
|
|
29
|
+
* web 镜像缺位。都是「没接线」不是「接错线」(web 侧今日无消费点,已核),收敛后位在、待接。
|
|
30
|
+
* D4 `LocalSessionEvent.request` —— 真源有这个位,**宿主从不发**(session-host 零处写 `request:`);
|
|
31
|
+
* web 镜像没有它反而与运行时事实一致。真源保留该位(CC `fQe` 的 form 位),记在这里免得
|
|
32
|
+
* 下一棒把「web 没有」当成漏。
|
|
33
|
+
* D5 `EipcStoreClient.getStateSync` web 侧是 optional、真源是 required;`SeatSessionDirectoryRow.createdAt`
|
|
34
|
+
* web 侧 optional、真源 required。两条都是「更弱」方向,收敛到真源即可(宿主校验器
|
|
35
|
+
* `isLocalSessionRecord` 本来就要求 createdAt 在场,运行时保证一直成立)。
|
|
36
|
+
* · web 侧**没有**镜像、因而至今拿不到的面:队列三动词 / setFocusedSession / setModel /
|
|
37
|
+
* focusedSessionState / settingsState / `CHROME_EVENT_KINDS` / `ATTACHMENT_IDS_PER_TASK_CAP`。
|
|
38
|
+
* 本文件导出后它们对 web 首次可见(接不接线是渲染面的决定)。
|
|
39
|
+
*
|
|
40
|
+
* ── 沿革(以下正文自 sema-desktop/src/shared/localSessions.ts 逐字迁入)──────────────────────
|
|
41
|
+
* LocalSessions — the desktop host's session service contract, FORM-ported from the CC desktop
|
|
42
|
+
* app's `claude.web / LocalSessions` eipc interface (212 methods there; FIRST BATCH here = the four
|
|
43
|
+
* families clay scoped: session lifecycle / event subscription / interrupt / approvals).
|
|
44
|
+
*
|
|
45
|
+
* Naming: CC ns `claude.web` → `sema.web` (brand table). Method names kept verbatim where the CC
|
|
46
|
+
* method exists (`start`/`stop`/`interrupt`/`sendMessage`/`onEvent`/`onToolPermissionRequest`/
|
|
47
|
+
* `respondToToolPermission` are all real CC LocalSessions methods — see the 854-channel census).
|
|
48
|
+
*
|
|
49
|
+
* Event payload contract: ported in FORM from CC's `onEvent` runtime validator `fQe`
|
|
50
|
+
* (unpack `.vite/build/index.chunk-CnWKsyE_.js:369773`). The load-bearing property we keep
|
|
51
|
+
* byte-faithful ([1832] axiom 2, independently proven by CC's own code): on an otherwise
|
|
52
|
+
* fully-schema'd IPC surface, the `message`/`messages` fields (= CC session-vocabulary SDKMessage
|
|
53
|
+
* bodies, our transcript plane) are UNVALIDATED passthrough positions, while every other field is a
|
|
54
|
+
* neutral typed position. Our implementation体 differs (sema wire via @sema-agent/sdk +
|
|
55
|
+
* @sema-agent/client-core), the contract shape does not.
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* The resolution words the host currently puts on `tool_permission_resolved.outcome` ([1947] 红③).
|
|
59
|
+
* DOCUMENTARY + test anchor only — the gate does NOT enforce membership (see the field's doc: a
|
|
60
|
+
* closed set would fail-close the whole event on the day the wire grows a word). Grouped by family:
|
|
61
|
+
* - tool approval (wire `ToolApprovalFrame.outcome`, + our own settle sweep): allowed/denied/expired
|
|
62
|
+
* - question (wire `QuestionFrame.outcome`): answered/unanswered
|
|
63
|
+
* - elicitation (wire `ElicitationFrame.action`): accept/decline/cancel
|
|
64
|
+
*/
|
|
65
|
+
export const HITL_RESOLUTION_OUTCOMES = [
|
|
66
|
+
"allowed",
|
|
67
|
+
"denied",
|
|
68
|
+
"expired",
|
|
69
|
+
"answered",
|
|
70
|
+
"unanswered",
|
|
71
|
+
"accept",
|
|
72
|
+
"decline",
|
|
73
|
+
"cancel",
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* 接缝记账 — what the HOST now guarantees vs what a RENDERER still owes ([1947] 红③/红④ split).
|
|
77
|
+
*
|
|
78
|
+
* The host half is done and pinned by desktop's `scripts/run-hitl-frame-transit-fence-test.mjs`:
|
|
79
|
+
* every live ask family reaches `onToolPermissionRequest` with its own requestId domain, every
|
|
80
|
+
* resolution reaches `onEvent` with an `outcome`, and every decision routes to the right wire verb.
|
|
81
|
+
*
|
|
82
|
+
* The renderer half is where these become VISIBLE, and until it lands the data is present but
|
|
83
|
+
* unread (stating it plainly so nobody reads "the host emits it" as "the user sees it"):
|
|
84
|
+
* 1. `tool_permission_resolved.outcome` — an `expired` / `denied` / `unanswered` resolution must
|
|
85
|
+
* leave a visible refusal trace. Today the seat transport clears the surface and drops the
|
|
86
|
+
* word, so a refusal is still indistinguishable from an approval on screen.
|
|
87
|
+
* 🔴 B18(2026-07-29)查明这条一直不销账的**机制原因**:web 侧的镜像类型里连 `outcome` 这个
|
|
88
|
+
* 位都没有,所以「没读」在两边都编译得过。本批把位收进真源 = 让它至少可见于类型面。
|
|
89
|
+
* 2. `toolCallId` — now real (not null), so an approval can be anchored to the tool card that
|
|
90
|
+
* raised it instead of "the most recent tool_start" (an anchor that lands on the wrong card
|
|
91
|
+
* when a prior tool is still running).
|
|
92
|
+
* 3. `question:` rows need NO new renderer code: they arrive as `toolName: "AskUserQuestion"` with
|
|
93
|
+
* `input.questions`, which is exactly what the seat's ask-user-question surface already reads,
|
|
94
|
+
* and its answer envelope is exactly what this host expects back.
|
|
95
|
+
* 4. 座位 wire 补齐批 (2026-07-28) closed two entries that used to live here as "如实,未做":
|
|
96
|
+
* `permissionMode` now has a seat on `sendMessage` (was: the composer's Plan badge never
|
|
97
|
+
* crossed and the turn ran with the engine default), and the attachment BYTE channel exists
|
|
98
|
+
* (`uploadAttachment` + `sendMessage.attachmentIds`) — so a seat renderer no longer has to
|
|
99
|
+
* simulate "attachment ready" for a file nothing received. What is still NOT here, stated
|
|
100
|
+
* plainly: plan REVISE feedback text (the decide face has no `reason` seat, so revise maps to
|
|
101
|
+
* a bare deny) and a per-session `permissionMode` write (the wire's is per-TURN only).
|
|
102
|
+
* 5. `elicit:` rows have no dedicated surface yet: they fall through to the generic approval card,
|
|
103
|
+
* which shows the MCP server's prompt and can accept/decline but CANNOT collect form values.
|
|
104
|
+
* A real form surface (built from `input.requestedSchema`) is the renderer's remaining piece;
|
|
105
|
+
* until then an approve carries no content — the host never invents form values. That generic
|
|
106
|
+
* card also offers "Always allow (this session)", which this domain has no concept of: the host
|
|
107
|
+
* ignores `remember` here rather than pretend, so the renderer should hide that control for
|
|
108
|
+
* `elicit:` rows (a button that changes nothing is the false-affordance family again).
|
|
109
|
+
*/
|
|
110
|
+
/** Chrome arms wired to the renderer position (batch 4); other arms stay host-side on the ring. */
|
|
111
|
+
export const CHROME_EVENT_KINDS = [
|
|
112
|
+
"panel_task",
|
|
113
|
+
"notification_terminal",
|
|
114
|
+
"prompt_suggestions",
|
|
115
|
+
"subagent_lifecycle",
|
|
116
|
+
];
|
|
117
|
+
/** The closed set the inbound gate checks. CLOSED deliberately, unlike `outcome` above: this word
|
|
118
|
+
* ORIGINATES in our own renderer and goes OUT to a wire that 400s on anything else, so an unknown
|
|
119
|
+
* value is a local bug we want reported at the door — not an unknown future wire word we must
|
|
120
|
+
* tolerate. (The direction of travel is what decides open vs closed, not the field's shape.) */
|
|
121
|
+
export const PERMISSION_MODE_INTENTS = [
|
|
122
|
+
"default",
|
|
123
|
+
"plan",
|
|
124
|
+
"acceptEdits",
|
|
125
|
+
"bypassPermissions",
|
|
126
|
+
"auto",
|
|
127
|
+
];
|
|
128
|
+
/** Per-task attachment cap — the server's own limit (`attachmentIds` > 16 ⇒ 400). Gated here so the
|
|
129
|
+
* seat gets a precise local error instead of a wire 400 halfway through a turn. */
|
|
130
|
+
export const ATTACHMENT_IDS_PER_TASK_CAP = 16;
|
|
131
|
+
/** The per-turn `reasoningEffort` accept-set — core's ThinkingLevel tiers, re-exported from the
|
|
132
|
+
* package's own wire projection (`effortWire.ReasoningEffort`) rather than re-typed here.
|
|
133
|
+
*
|
|
134
|
+
* CLOSED for the same reason `PERMISSION_MODE_INTENTS` is: the word ORIGINATES in our own composer
|
|
135
|
+
* and travels OUT to a wire that 400s (fail-loud) on an unknown tier — so an unknown value is a
|
|
136
|
+
* local bug we want reported at the seat's door, not a future wire word to tolerate. `ultracode` is
|
|
137
|
+
* deliberately NOT a tier here: in CC it is a standalone `settings.ultracode` preset boolean that
|
|
138
|
+
* COEXISTS with `xhigh` (see `SeatTurnSettings`), and folding it into this axis is exactly how the
|
|
139
|
+
* orchestration axis gets eaten by the reasoning axis. */
|
|
140
|
+
export const SEAT_REASONING_EFFORTS = [
|
|
141
|
+
"off",
|
|
142
|
+
"minimal",
|
|
143
|
+
"low",
|
|
144
|
+
"medium",
|
|
145
|
+
"high",
|
|
146
|
+
"xhigh",
|
|
147
|
+
"max",
|
|
148
|
+
];
|
|
149
|
+
export const SEAT_TURN_SETTINGS_KEYS = ["ultracode"];
|
|
150
|
+
// ───────────────────────────── validators (CC dual-gate posture) ─────────────────────────────
|
|
151
|
+
export function isLocalSessionRecord(s) {
|
|
152
|
+
const e = s;
|
|
153
|
+
return !(!e ||
|
|
154
|
+
typeof e !== "object" ||
|
|
155
|
+
typeof e.sessionId !== "string" ||
|
|
156
|
+
(typeof e.engineSessionId !== "undefined" && typeof e.engineSessionId !== "string") ||
|
|
157
|
+
(typeof e.model !== "undefined" && typeof e.model !== "string") ||
|
|
158
|
+
(typeof e.title !== "undefined" && typeof e.title !== "string") ||
|
|
159
|
+
typeof e.isRunning !== "boolean" ||
|
|
160
|
+
typeof e.createdAt !== "string" ||
|
|
161
|
+
(typeof e.lastActivityAt !== "undefined" && typeof e.lastActivityAt !== "string") ||
|
|
162
|
+
(typeof e.queuedMessages !== "undefined" &&
|
|
163
|
+
!(Array.isArray(e.queuedMessages) &&
|
|
164
|
+
e.queuedMessages.every((q) => !!q && typeof q.uuid === "string" && typeof q.text === "string"))));
|
|
165
|
+
}
|
|
166
|
+
export function isToolPermissionRequest(r) {
|
|
167
|
+
const e = r;
|
|
168
|
+
return !(!e ||
|
|
169
|
+
typeof e !== "object" ||
|
|
170
|
+
typeof e.requestId !== "string" ||
|
|
171
|
+
typeof e.sessionId !== "string" ||
|
|
172
|
+
(typeof e.toolName !== "undefined" && e.toolName !== null && typeof e.toolName !== "string") ||
|
|
173
|
+
(typeof e.toolCallId !== "undefined" && e.toolCallId !== null && typeof e.toolCallId !== "string") ||
|
|
174
|
+
// `input` is the untrusted redacted payload — passthrough (render-only), like CC's request args.
|
|
175
|
+
(typeof e.message !== "undefined" && typeof e.message !== "string") ||
|
|
176
|
+
(typeof e.boundCallId !== "undefined" && typeof e.boundCallId !== "string") ||
|
|
177
|
+
(typeof e.boundInputHash !== "undefined" && typeof e.boundInputHash !== "string") ||
|
|
178
|
+
(typeof e.createdAt !== "undefined" && typeof e.createdAt !== "number"));
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* onEvent payload validator — line-for-line FORM port of CC's `fQe`
|
|
182
|
+
* (`.vite/build/index.chunk-CnWKsyE_.js:369773`). Field-by-field mapping:
|
|
183
|
+
* - `type`/`sessionId`: required strings (same);
|
|
184
|
+
* - `message`: CC's minified source reads `typeof e.message<"u"` inside a comma expression whose
|
|
185
|
+
* value is discarded — i.e. NO validation, a deliberate transcript-plane passthrough. Same here.
|
|
186
|
+
* - `messages`: array-checked, elements `every(t=>!0)` (constant true) — array shape only. Same here.
|
|
187
|
+
* - `error`/`errorCategory`: optional strings (same); `code`: optional number (same);
|
|
188
|
+
* - `request`: CC gates with `S1` (ToolPermissionRequest schema) → our isToolPermissionRequest;
|
|
189
|
+
* - `session`: CC gates with `aM` (SessionRecord schema) → our isLocalSessionRecord;
|
|
190
|
+
* - `data`: optional string / `sources`: optional string[] / `permissionMode`: optional string /
|
|
191
|
+
* `userMessageUuid`: optional string / `hasPreClearSession`: optional boolean (same);
|
|
192
|
+
* - CC fields we do not emit in this batch (`tccFolderKind`/`initializationStatus`/`fsFile`/
|
|
193
|
+
* `localMcpServers`/`refusalFallbackPrompt`) are not carried — their positions return when the
|
|
194
|
+
* corresponding service families are ported.
|
|
195
|
+
*/
|
|
196
|
+
export function isLocalSessionEvent(v) {
|
|
197
|
+
const e = v;
|
|
198
|
+
return !(!e ||
|
|
199
|
+
typeof e !== "object" ||
|
|
200
|
+
typeof e.type !== "string" ||
|
|
201
|
+
typeof e.sessionId !== "string" ||
|
|
202
|
+
// `message`: intentionally unvalidated passthrough (see doc above — CC fQe does the same).
|
|
203
|
+
(typeof e.messages !== "undefined" && !Array.isArray(e.messages)) ||
|
|
204
|
+
(typeof e.error !== "undefined" && typeof e.error !== "string") ||
|
|
205
|
+
(typeof e.errorCategory !== "undefined" && typeof e.errorCategory !== "string") ||
|
|
206
|
+
(typeof e.code !== "undefined" && typeof e.code !== "number") ||
|
|
207
|
+
(typeof e.request !== "undefined" && !isToolPermissionRequest(e.request)) ||
|
|
208
|
+
(typeof e.data !== "undefined" && typeof e.data !== "string") ||
|
|
209
|
+
(typeof e.sources !== "undefined" &&
|
|
210
|
+
!(Array.isArray(e.sources) && e.sources.every((t) => typeof t === "string"))) ||
|
|
211
|
+
(typeof e.permissionMode !== "undefined" && typeof e.permissionMode !== "string") ||
|
|
212
|
+
(typeof e.session !== "undefined" && !isLocalSessionRecord(e.session)) ||
|
|
213
|
+
(typeof e.userMessageUuid !== "undefined" && typeof e.userMessageUuid !== "string") ||
|
|
214
|
+
(typeof e.hasPreClearSession !== "undefined" && typeof e.hasPreClearSession !== "boolean") ||
|
|
215
|
+
(typeof e.requestId !== "undefined" && typeof e.requestId !== "string") ||
|
|
216
|
+
// `outcome`: type-checked only, NOT membership-checked (see the field doc — an unknown future
|
|
217
|
+
// wire word must reach the renderer as "settled, reason unknown", never take the event down).
|
|
218
|
+
(typeof e.outcome !== "undefined" && typeof e.outcome !== "string") ||
|
|
219
|
+
(typeof e.chrome !== "undefined" &&
|
|
220
|
+
!(!!e.chrome &&
|
|
221
|
+
typeof e.chrome === "object" &&
|
|
222
|
+
typeof e.chrome.kind === "string" &&
|
|
223
|
+
CHROME_EVENT_KINDS.includes(e.chrome.kind) &&
|
|
224
|
+
// lane discipline first-class ([1617] family): a chrome arm always carries its LaneProof.
|
|
225
|
+
!!e.chrome.laneProof)));
|
|
226
|
+
}
|
|
227
|
+
export function isAttachmentUploadReceipt(v) {
|
|
228
|
+
const e = v;
|
|
229
|
+
return !(!e ||
|
|
230
|
+
typeof e !== "object" ||
|
|
231
|
+
typeof e.id !== "string" ||
|
|
232
|
+
typeof e.name !== "string" ||
|
|
233
|
+
typeof e.mime !== "string" ||
|
|
234
|
+
typeof e.sha256 !== "string" ||
|
|
235
|
+
typeof e.sizeBytes !== "number");
|
|
236
|
+
}
|
|
237
|
+
export function isSeatModelCatalog(v) {
|
|
238
|
+
const e = v;
|
|
239
|
+
return !(!e ||
|
|
240
|
+
typeof e !== "object" ||
|
|
241
|
+
!Array.isArray(e.models) ||
|
|
242
|
+
!e.models.every((m) => !!m &&
|
|
243
|
+
typeof m === "object" &&
|
|
244
|
+
typeof m.id === "string" &&
|
|
245
|
+
["undefined", "string"].includes(typeof m.name) &&
|
|
246
|
+
["undefined", "string"].includes(typeof m.provider) &&
|
|
247
|
+
["undefined", "number"].includes(typeof m.contextWindow) &&
|
|
248
|
+
["undefined", "number"].includes(typeof m.maxOutputTokens)) ||
|
|
249
|
+
(typeof e.defaultId !== "undefined" && typeof e.defaultId !== "string"));
|
|
250
|
+
}
|
|
251
|
+
export function isSendMessageAck(v) {
|
|
252
|
+
const e = v;
|
|
253
|
+
return !(!e ||
|
|
254
|
+
typeof e !== "object" ||
|
|
255
|
+
typeof e.sessionId !== "string" ||
|
|
256
|
+
typeof e.userMessageUuid !== "string" ||
|
|
257
|
+
(typeof e.queued !== "undefined" && typeof e.queued !== "boolean"));
|
|
258
|
+
}
|
|
259
|
+
// ───────────────────────────── service spec (eipc registration table) ─────────────────────────────
|
|
260
|
+
export const LOCAL_SESSIONS_SPEC = {
|
|
261
|
+
ns: "sema.web",
|
|
262
|
+
service: "LocalSessions",
|
|
263
|
+
methods: {
|
|
264
|
+
start: { kind: "invoke", validate: isLocalSessionRecord },
|
|
265
|
+
sendMessage: { kind: "invoke", validate: isSendMessageAck },
|
|
266
|
+
interrupt: {
|
|
267
|
+
kind: "invoke",
|
|
268
|
+
validate: (v) => !!v && typeof v.interrupted === "boolean",
|
|
269
|
+
},
|
|
270
|
+
stop: {
|
|
271
|
+
kind: "invoke",
|
|
272
|
+
validate: (v) => !!v && typeof v.stopped === "boolean",
|
|
273
|
+
},
|
|
274
|
+
respondToToolPermission: {
|
|
275
|
+
kind: "invoke",
|
|
276
|
+
validate: (v) => !!v && typeof v.ok === "boolean",
|
|
277
|
+
},
|
|
278
|
+
// Queue family — CC returns bare booleans (false = too-late/no-op), same here.
|
|
279
|
+
cancelQueuedMessage: { kind: "invoke", validate: (v) => typeof v === "boolean" },
|
|
280
|
+
reorderQueuedMessage: { kind: "invoke", validate: (v) => typeof v === "boolean" },
|
|
281
|
+
promoteQueuedMessage: { kind: "invoke", validate: (v) => typeof v === "boolean" },
|
|
282
|
+
setFocusedSession: {
|
|
283
|
+
kind: "invoke",
|
|
284
|
+
validate: (v) => !!v && typeof v.ok === "boolean",
|
|
285
|
+
},
|
|
286
|
+
setModel: { kind: "invoke", validate: (v) => typeof v === "boolean" },
|
|
287
|
+
getTranscript: {
|
|
288
|
+
kind: "invoke",
|
|
289
|
+
// messages = the transcript-plane passthrough batch (fQe `messages` posture: array-checked,
|
|
290
|
+
// elements deliberately unvalidated — they are CC SDKMessage bodies).
|
|
291
|
+
validate: (v) => !!v &&
|
|
292
|
+
typeof v.sessionId === "string" &&
|
|
293
|
+
Array.isArray(v.messages),
|
|
294
|
+
},
|
|
295
|
+
searchSessions: {
|
|
296
|
+
kind: "invoke",
|
|
297
|
+
validate: (v) => !!v &&
|
|
298
|
+
Array.isArray(v.results) &&
|
|
299
|
+
v.results.every((r) => !!r &&
|
|
300
|
+
typeof r.sessionId === "string" &&
|
|
301
|
+
Array.isArray(r.matches) &&
|
|
302
|
+
r.matches.every((m) => !!m && typeof m.snippet === "string")),
|
|
303
|
+
},
|
|
304
|
+
uploadAttachment: { kind: "invoke", validate: isAttachmentUploadReceipt },
|
|
305
|
+
listModels: { kind: "invoke", validate: isSeatModelCatalog },
|
|
306
|
+
onEvent: { kind: "event", validate: isLocalSessionEvent },
|
|
307
|
+
onToolPermissionRequest: { kind: "event", validate: isToolPermissionRequest },
|
|
308
|
+
},
|
|
309
|
+
stores: {
|
|
310
|
+
sessionsState: {
|
|
311
|
+
validate: (v) => !!v &&
|
|
312
|
+
Array.isArray(v.sessions) &&
|
|
313
|
+
v.sessions.every((s) => isLocalSessionRecord(s)),
|
|
314
|
+
},
|
|
315
|
+
focusedSessionState: {
|
|
316
|
+
validate: (v) => {
|
|
317
|
+
const s = v;
|
|
318
|
+
return !!v && (s.sessionId === null || typeof s.sessionId === "string");
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
settingsState: {
|
|
322
|
+
validate: (v) => {
|
|
323
|
+
const s = v;
|
|
324
|
+
return (!!v &&
|
|
325
|
+
(s.defaultModel === null || typeof s.defaultModel === "string") &&
|
|
326
|
+
(typeof s.engineVersion === "undefined" || typeof s.engineVersion === "string") &&
|
|
327
|
+
(typeof s.dataRoot === "undefined" || typeof s.dataRoot === "string"));
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
/** verb 名清单 = `LOCAL_SESSIONS_SPEC.methods` 应有的键集(逐元素相等,门里断言)。 */
|
|
333
|
+
export const SEAT_METHOD_NAMES = [
|
|
334
|
+
"start",
|
|
335
|
+
"sendMessage",
|
|
336
|
+
"interrupt",
|
|
337
|
+
"stop",
|
|
338
|
+
"respondToToolPermission",
|
|
339
|
+
"cancelQueuedMessage",
|
|
340
|
+
"reorderQueuedMessage",
|
|
341
|
+
"promoteQueuedMessage",
|
|
342
|
+
"setFocusedSession",
|
|
343
|
+
"setModel",
|
|
344
|
+
"getTranscript",
|
|
345
|
+
"searchSessions",
|
|
346
|
+
"uploadAttachment",
|
|
347
|
+
"listModels",
|
|
348
|
+
"onEvent",
|
|
349
|
+
"onToolPermissionRequest",
|
|
350
|
+
];
|
|
351
|
+
/** `$store$` 属性名清单;SPEC 的 store 键 + `"Store"` 必须逐条等于这里的名字(门里断言)。 */
|
|
352
|
+
export const SEAT_STORE_PROPS = [
|
|
353
|
+
"sessionsStateStore",
|
|
354
|
+
"focusedSessionStateStore",
|
|
355
|
+
"settingsStateStore",
|
|
356
|
+
];
|
|
357
|
+
/** 事件式(main→renderer push)的 verb —— SPEC 里 `kind: "event"` 的那一撮,门里逐元素对账。 */
|
|
358
|
+
export const SEAT_EVENT_METHOD_NAMES = ["onEvent", "onToolPermissionRequest"];
|
|
359
|
+
// ── payload 键集镜像(与各自 interface 之间有类型钉,不可能漂)────────────────────────────────
|
|
360
|
+
export const LOCAL_SESSION_EVENT_KEYS = [
|
|
361
|
+
"type",
|
|
362
|
+
"sessionId",
|
|
363
|
+
"message",
|
|
364
|
+
"messages",
|
|
365
|
+
"error",
|
|
366
|
+
"errorCategory",
|
|
367
|
+
"code",
|
|
368
|
+
"request",
|
|
369
|
+
"data",
|
|
370
|
+
"sources",
|
|
371
|
+
"permissionMode",
|
|
372
|
+
"session",
|
|
373
|
+
"userMessageUuid",
|
|
374
|
+
"hasPreClearSession",
|
|
375
|
+
"requestId",
|
|
376
|
+
"outcome",
|
|
377
|
+
"chrome",
|
|
378
|
+
];
|
|
379
|
+
export const TOOL_PERMISSION_REQUEST_KEYS = [
|
|
380
|
+
"requestId",
|
|
381
|
+
"sessionId",
|
|
382
|
+
"toolName",
|
|
383
|
+
"toolCallId",
|
|
384
|
+
"input",
|
|
385
|
+
"message",
|
|
386
|
+
"boundCallId",
|
|
387
|
+
"boundInputHash",
|
|
388
|
+
"createdAt",
|
|
389
|
+
];
|
|
390
|
+
export const LOCAL_SESSION_RECORD_KEYS = [
|
|
391
|
+
"sessionId",
|
|
392
|
+
"engineSessionId",
|
|
393
|
+
"model",
|
|
394
|
+
"title",
|
|
395
|
+
"isRunning",
|
|
396
|
+
"createdAt",
|
|
397
|
+
"lastActivityAt",
|
|
398
|
+
"queuedMessages",
|
|
399
|
+
];
|
|
400
|
+
export const SEND_MESSAGE_REQUEST_KEYS = [
|
|
401
|
+
"sessionId",
|
|
402
|
+
"text",
|
|
403
|
+
"model",
|
|
404
|
+
"userMessageUuid",
|
|
405
|
+
"permissionMode",
|
|
406
|
+
"attachmentIds",
|
|
407
|
+
// 2026-07-29(web 收编第一批②):三位补齐。此前座位 lane **结构性**拿不到它们 —— 闭集就是
|
|
408
|
+
// 载体本身,不在集里 = 那个决定在这条 lane 上根本没有位置可放。
|
|
409
|
+
"reasoningEffort",
|
|
410
|
+
"retainBackgroundProcesses",
|
|
411
|
+
"settings",
|
|
412
|
+
];
|
|
413
|
+
export const TOOL_PERMISSION_DECISION_KEYS = [
|
|
414
|
+
"sessionId",
|
|
415
|
+
"requestId",
|
|
416
|
+
"decision",
|
|
417
|
+
"answer",
|
|
418
|
+
"updatedInput",
|
|
419
|
+
"remember",
|
|
420
|
+
];
|
|
421
|
+
export const START_SESSION_OPTION_KEYS = ["model", "title", "cwd", "excludeTools"];
|
|
422
|
+
const _seatContractPins = [
|
|
423
|
+
true, true, true, true, true, true, true, true, true,
|
|
424
|
+
true, true, true, true, true, true, true, true,
|
|
425
|
+
true, true,
|
|
426
|
+
];
|
|
427
|
+
void _seatContractPins;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/client-core",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.7",
|
|
4
4
|
"description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Blackboard [1832] design axioms; [1651]/[1652]/[1653] signed seam design. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|