@harness-mix/cli 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +469 -467
  3. package/output/native-build/desktop-controller.mjs +1 -1
  4. package/output/native-build/renderer-extension.js +23 -4
  5. package/package.json +11 -9
  6. package/scripts/antigravity-adapter-test.cjs +647 -626
  7. package/scripts/codex-adapter-test.cjs +162 -127
  8. package/scripts/collaboration-test.cjs +274 -262
  9. package/scripts/jsonl-stdin-test.cjs +40 -31
  10. package/scripts/kiro-cursor-adapters-test.cjs +124 -100
  11. package/scripts/native-acp-depth-test.cjs +30 -5
  12. package/scripts/native-update-apply-test.cjs +269 -215
  13. package/scripts/native-update.cjs +78 -0
  14. package/scripts/native-vendor-adapters-test.cjs +196 -154
  15. package/scripts/salvage-rollout-writes.cjs +72 -0
  16. package/scripts/zcode-adapter-test.cjs +329 -0
  17. package/scripts/zcode-live-probe.cjs +66 -0
  18. package/src/main/adapters/antigravity.js +1428 -1418
  19. package/src/main/adapters/codex.js +656 -649
  20. package/src/main/adapters/native-acp-command.js +51 -48
  21. package/src/main/adapters/native-acp.js +47 -12
  22. package/src/main/adapters/qoder.js +12 -8
  23. package/src/main/adapters/zcode.js +921 -10
  24. package/src/main/host/collaboration.js +723 -715
  25. package/src/main/host/jsonl.js +130 -120
  26. package/src/main/native/config.js +9 -9
  27. package/src/main/native/launcher.js +252 -237
  28. package/src/main/native/process-utils.js +157 -57
  29. package/src/main/native/protocol.js +1221 -1187
  30. package/src/main/native/update-state.js +123 -110
  31. package/src/main/native/updater.js +460 -394
  32. package/src/native-ui/desktop-control/src/renderer-cdp-control-session.ts +358 -358
  33. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3211 -3181
  34. package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -2
@@ -1,10 +1,921 @@
1
- const { nativeAcp } = require('./native-acp');
2
-
3
- // Requires an explicitly configured ACP executable; no guessed official command.
4
- module.exports = nativeAcp({
5
- id: 'zcode', name: 'ZCode', args: [],
6
- capabilities: { questions: false, thinkingLevels: false, usage: true, contextUsage: true, attachments: false, fork: false, compaction: false },
7
- });
8
- // ZCode scans, per scope: .zcode/skills then .agents/skills (deeper workspace levels win).
9
- // https://zcode.z.ai/en/docs/skill
10
- module.exports.manifest.integrations.skills = { global: ['.zcode/skills', '.agents/skills'], project: ['.zcode/skills', '.agents/skills'] };
1
+ // Native ZCode adapter: speaks the ZCode Protocol stdio app-server directly
2
+ // (`zcode.cjs app-server --stdio`), the same entry the ZCode desktop app uses.
3
+ // The ZCode CLI does not speak ACP the previous nativeAcp-based adapter could
4
+ // never work without an ACP bridge that nobody ships.
5
+ //
6
+ // Verified protocol (zcode.cjs 0.16.5, "ZCode Protocol" v1):
7
+ // - framing: newline JSON {id, method, params} without a "jsonrpc" key
8
+ // (JsonlProcess jsonrpc:false); server→client requests are answered {id, result}.
9
+ // - session/create {workspace:{workspacePath, workspaceKey}} → {session:{sessionId,...},
10
+ // projection:{contextUsed, contextWindow, mode, status,...}}
11
+ // - session/subscribe {sessionId, deliveryKind:'desktop-continuous'} enables
12
+ // session/event push notifications {eventId, payload:{type,...}}.
13
+ // - session/send {sessionId, content} → {accepted, stateRevision}; events:
14
+ // turn.started, part.delta {field:text|reasoning|input|output, delta},
15
+ // tool.updated (kinds scheduled|started|progress|result|error), turn.completed
16
+ // {response, tokenCount, usage, toolCallCount, duration}, turn.failed {error}.
17
+ // - session/send also accepts attachments: [{kind:'image', filename,
18
+ // mimeType, sizeBytes?, dataBase64?|localPath?}] (union also covers
19
+ // audio/video/pdf/file, but Harness Mix only routes images). Verified live:
20
+ // localPath delivers the image to the model; dataBase64 degrades to a
21
+ // "[Attached image/*: name]" metadata placeholder, so base64-only images are
22
+ // materialized to a temp file and sent as localPath instead.
23
+ // - collaboration: the protocol has NO runtime MCP registration RPC and the
24
+ // plugin root (~/.zcode/cli/plugins) is the user's own native storage, which
25
+ // Harness Mix never rewrites — so ZCode joins multi-agent work as a
26
+ // dispatchable worker / Agent-Team member / /delegate target (all
27
+ // kernel-driven), but cannot take the `#` lead role yet. Lead-side wiring
28
+ // would need a harness-mix plugin installed via the sanctioned
29
+ // plugins/install RPC plus a PATH-resolved bridge shim; documented as the
30
+ // follow-up design.
31
+ // - models arrive via state.updated patches {model:{available:[{providerId, modelId,...}]}}
32
+ // once the logged-in account materializes; session/setModel {sessionId, model}.
33
+ // - server requests: session/requestRuntimePreferences (answer the fixed
34
+ // preference block), interaction/requestOfficialMcpAuthHeaders (decline),
35
+ // interaction/requestPermission (surfaces as an approval card, answered
36
+ // {decision:'allow'|'deny'}), interaction/requestUserInput (question card).
37
+ // - permission modes: session/setMode {sessionId, mode} with the canonical
38
+ // enum plan|build|edit|yolo|auto ('auto' is internal); the desktop selector's
39
+ // 计划模式/变更前确认/自动编辑/完全访问 map onto the first four.
40
+ const { execFile } = require('node:child_process');
41
+ const fs = require('node:fs');
42
+ const os = require('node:os');
43
+ const path = require('node:path');
44
+ const { randomUUID } = require('node:crypto');
45
+ const { JsonlProcess, cliSpawn } = require('../host/jsonl');
46
+
47
+ const manifest = {
48
+ id: 'zcode',
49
+ name: 'ZCode',
50
+ icon: 'zcode-color.svg',
51
+ capabilities: {
52
+ // Dispatchable worker + Agent-Team member; the `#` lead role stays off
53
+ // until MCP injection exists (see the header note).
54
+ collaborationTools: true,
55
+ plan: true, streaming: true, thinking: false, tools: true,
56
+ approvals: true, questions: true, models: true, thinkingLevels: true,
57
+ permissionModes: true, resume: true, fork: false, forkFromMessage: false,
58
+ compaction: false, nativeDiff: false, nativePatch: false,
59
+ usage: true, contextUsage: true, cost: false, attachments: true,
60
+ },
61
+ };
62
+
63
+ const RUNTIME_PREFERENCES = {
64
+ nativeSearchEnhancementsEnabled: false,
65
+ memoryEnabled: false,
66
+ askUserQuestionAutoResolutionEnabled: true,
67
+ modelContextBudgetStrategy: 'preflight-v1',
68
+ };
69
+
70
+ // Same four modes the official desktop selector offers; ids are the
71
+ // session/setMode enum. `default` marks the agent's own startup mode (build)
72
+ // so the renderer shows a real selection instead of a placeholder; `dangerous`
73
+ // is presentation-only and gets projected to the renderer catalog.
74
+ const PERMISSION_MODES = [
75
+ { id: 'plan', label: '计划模式', description: '探索并制定计划;批准计划后才执行变更。' },
76
+ { id: 'build', label: '变更前确认', description: '自动允许读取;写入或执行操作前询问。', default: true },
77
+ { id: 'edit', label: '自动编辑', description: '自动允许读取和写入;执行操作前询问。' },
78
+ { id: 'yolo', label: '完全访问', description: '无需批准提示即可运行所有工具操作。', dangerous: true },
79
+ ];
80
+
81
+ // Headless entry resolution. The desktop-bundled zcode.cjs is the primary
82
+ // source; a standalone CLI on PATH and explicit env overrides also work.
83
+ // The desktop app executable itself is NOT a headless CLI — never guess it
84
+ // (the Qoder IDE-launcher lesson).
85
+ function bundledCli() {
86
+ if (process.platform === 'win32') return path.join(process.env.LOCALAPPDATA || '', 'Programs', 'ZCode', 'resources', 'glm', 'zcode.cjs');
87
+ if (process.platform === 'darwin') return path.join(process.env.HOME || '', 'Applications', 'ZCode.app', 'Contents', 'Resources', 'glm', 'zcode.cjs');
88
+ return null;
89
+ }
90
+
91
+ function resolveLaunch() {
92
+ const official = process.env.ZCODE_AGENT_SERVER_COMMAND?.trim();
93
+ if (official) {
94
+ let extra = [];
95
+ try { extra = process.env.ZCODE_AGENT_SERVER_ARGS_JSON ? JSON.parse(process.env.ZCODE_AGENT_SERVER_ARGS_JSON) : []; } catch { /* ignore malformed override */ }
96
+ return { command: official, args: [...extra, 'app-server', '--stdio'] };
97
+ }
98
+ const override = process.env.HARNESS_MIX_ZCODE_EXECUTABLE;
99
+ for (const candidate of [override, bundledCli()].filter(Boolean)) {
100
+ try {
101
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
102
+ if (/\.(cjs|mjs|js)$/i.test(candidate)) return { command: process.execPath, args: [candidate, 'app-server', '--stdio'] };
103
+ return { command: candidate, args: ['app-server', '--stdio'] };
104
+ }
105
+ } catch { /* fall through to the next candidate */ }
106
+ }
107
+ const suffixes = process.platform === 'win32' ? ['.cmd', '.exe'] : [''];
108
+ for (const dir of (process.env.PATH || '').split(path.delimiter).filter(Boolean)) {
109
+ for (const suffix of suffixes) {
110
+ const file = path.join(dir, `zcode${suffix}`);
111
+ try {
112
+ if (fs.existsSync(file) && fs.statSync(file).isFile()) {
113
+ if (/\.cmd$/i.test(file)) return cliSpawn('zcode', ['app-server', '--stdio']);
114
+ return { command: file, args: ['app-server', '--stdio'] };
115
+ }
116
+ } catch { /* keep scanning */ }
117
+ }
118
+ }
119
+ throw new Error('未找到 ZCode 无头 CLI;请安装 ZCode 桌面版(自带 glm/zcode.cjs)或设置 HARNESS_MIX_ZCODE_EXECUTABLE 指向 zcode.cjs');
120
+ }
121
+
122
+ function workspaceIdentity(cwd) {
123
+ // The desktop itself uses the workspace path as the key.
124
+ return { workspacePath: cwd, workspaceKey: cwd };
125
+ }
126
+
127
+ // The desktop feeds the agent its provider catalog through these env vars
128
+ // (createNodeProviderRuntimePathEnv); the agent requires BOTH paths and
129
+ // without the builtin file the provider registry stays empty ("Select a
130
+ // model before continuing").
131
+ function bundledCli() {
132
+ if (process.platform === 'win32') return path.join(process.env.LOCALAPPDATA || '', 'Programs', 'ZCode', 'resources', 'glm', 'zcode.cjs');
133
+ if (process.platform === 'darwin') return path.join(process.env.HOME || '', 'Applications', 'ZCode.app', 'Contents', 'Resources', 'glm', 'zcode.cjs');
134
+ return null;
135
+ }
136
+
137
+ function providerConfigPaths() {
138
+ const desktopDir = bundledCli() ? path.dirname(path.dirname(bundledCli())) : null;
139
+ const builtin = process.env.HARNESS_MIX_ZCODE_BUILTIN_CONFIG
140
+ || (desktopDir && path.join(desktopDir, 'config', 'provider', 'zcode-builtin.json'));
141
+ const personal = process.env.USERPROFILE ? path.join(process.env.USERPROFILE, '.zcode', 'v2', 'provider_config.json') : null;
142
+ return {
143
+ builtin: builtin && fs.existsSync(builtin) ? builtin : null,
144
+ personal: personal && fs.existsSync(personal) ? personal : null,
145
+ };
146
+ }
147
+
148
+ function agentEnvironment() {
149
+ const paths = providerConfigPaths();
150
+ return {
151
+ ...(paths.builtin ? { ZCODE_BUILTIN_PROVIDER_CONFIG_FILE: paths.builtin } : {}),
152
+ ...(paths.personal ? { ZCODE_PERSONAL_PROVIDER_CONFIG_FILE: paths.personal } : {}),
153
+ ...systemProxyEnv(),
154
+ };
155
+ }
156
+
157
+ // Console children do not inherit the WinINET proxy that GUI apps use; the
158
+ // agent's provider/entitlement checks fail silently without it. Read the
159
+ // machine proxy once (registry) and export it for the child only.
160
+ let cachedProxyEnv;
161
+ function systemProxyEnv() {
162
+ if (process.env.HTTP_PROXY || process.env.HTTPS_PROXY) return {};
163
+ if (cachedProxyEnv !== undefined) return cachedProxyEnv;
164
+ cachedProxyEnv = {};
165
+ if (process.platform !== 'win32') return cachedProxyEnv;
166
+ try {
167
+ const reg = require('node:child_process').execFileSync('reg', [
168
+ 'query', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
169
+ ], { encoding: 'utf8', windowsHide: true, timeout: 5000 }).toString();
170
+ const enabled = /ProxyEnable\s+REG_DWORD\s+0x1/i.test(reg);
171
+ const server = /ProxyServer\s+REG_SZ\s+(\S+)/i.exec(reg)?.[1];
172
+ if (enabled && server) {
173
+ const url = /^[a-z]+:\/\//i.test(server) ? server : `http://${server}`;
174
+ cachedProxyEnv = { HTTP_PROXY: url, HTTPS_PROXY: url, NO_PROXY: 'localhost,127.0.0.1' };
175
+ }
176
+ } catch { /* registry unavailable: no proxy */ }
177
+ return cachedProxyEnv;
178
+ }
179
+
180
+ // The builtin file declares one account provider per coding plan
181
+ // (account:bigmodel-individual-coding-plan, …). The account push links each
182
+ // one to the user's credential ENTRY NAME via states.connectionKey — an
183
+ // identifier, never the credential value itself, which the agent resolves
184
+ // from its own store.
185
+ function accountProviders(builtinPath) {
186
+ try {
187
+ const builtin = JSON.parse(fs.readFileSync(builtinPath, 'utf8'));
188
+ const rules = builtin?.config?.providerConfigRules?.providerRules ?? [];
189
+ const declared = rules
190
+ .filter(rule => /^account:/.test(rule?.providerId) && rule?.config?.access?.type === 'zhipu-account')
191
+ .map(rule => ({ providerId: rule.providerId, modelIds: rule.config.builtinModelIds ?? [] }));
192
+ const credPath = process.env.USERPROFILE ? path.join(process.env.USERPROFILE, '.zcode', 'v2', 'credentials.json') : null;
193
+ let credentialKeys = [];
194
+ try {
195
+ // Key NAMES only; values are never read or retained.
196
+ credentialKeys = Object.keys(JSON.parse(fs.readFileSync(credPath, 'utf8'))).filter(key => /^account-provider:.*:api-key$/.test(key));
197
+ } catch { /* no credentials file: nothing to link */ }
198
+ return declared
199
+ .map(entry => {
200
+ const slug = entry.providerId.replace(/^account:/, '');
201
+ const connectionKey = credentialKeys.find(key => key.includes(slug));
202
+ return connectionKey ? { ...entry, connectionKey } : null;
203
+ })
204
+ .filter(Boolean);
205
+ } catch {
206
+ return [];
207
+ }
208
+ }
209
+
210
+ // The registry applies an account push only when basedOnZCodeBuiltinRevision
211
+ // equals the agent's builtin snapshot revision, which is
212
+ // `zcode-builtin:${fileRevision}:${sha256(resolvedBuiltinPath)}` (Fy/TSo).
213
+ function builtinRevision(builtinPath) {
214
+ try {
215
+ const revision = JSON.parse(fs.readFileSync(builtinPath, 'utf8'))?.revision;
216
+ if (revision === undefined) return null;
217
+ return `zcode-builtin:${revision}:${require('node:crypto').createHash('sha256').update(path.resolve(builtinPath)).digest('hex')}`;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ // Key-free account declaration: providers carry {builtinModelIds,
224
+ // access:{type, entitled}} only and states link the credential entry by NAME
225
+ // (connectionKey) — the agent resolves the actual key from its own store, so
226
+ // no secret ever flows through Harness Mix.
227
+ async function pushAccountConfig(session) {
228
+ const paths = providerConfigPaths();
229
+ if (!paths.builtin) return;
230
+ const basedOn = builtinRevision(paths.builtin);
231
+ const accounts = accountProviders(paths.builtin);
232
+ if (!basedOn || !accounts.length) return;
233
+ const providers = {};
234
+ const states = {};
235
+ for (const account of accounts) {
236
+ providers[account.providerId] = { builtinModelIds: account.modelIds, access: { type: 'zhipu-account', entitled: true } };
237
+ states[account.providerId] = { availability: 'available', entitled: true, current: true, connectionKey: account.connectionKey };
238
+ }
239
+ try {
240
+ await session.proc.request('provider/updateAccountConfig', {
241
+ revision: `account:${Date.now()}`,
242
+ basedOnZCodeBuiltinRevision: basedOn,
243
+ providers,
244
+ states,
245
+ });
246
+ } catch (error) {
247
+ session.diagnostic?.(`ZCode 账号声明推送失败:${error.message}`);
248
+ }
249
+ }
250
+
251
+ // The session only broadcasts the model catalog (state.updated
252
+ // {model:{available:[…]}}) after a model is actually selected, so bootstrap
253
+ // the selection with the first builtin model of the first entitled account.
254
+ // GLM models require a reasoningLevel; try the common levels until one is
255
+ // accepted.
256
+ async function ensureModelCatalog(session) {
257
+ const paths = providerConfigPaths();
258
+ if (!paths.builtin) return;
259
+ const accounts = accountProviders(paths.builtin);
260
+ const modelId = accounts[0]?.modelIds?.[0];
261
+ if (!accounts[0] || !modelId) return;
262
+ for (const level of ['high', 'low', 'max', 'medium']) {
263
+ try {
264
+ await session.proc.request('session/setModel', {
265
+ sessionId: session.state.sessionId,
266
+ model: { providerId: accounts[0].providerId, modelId, options: { reasoningLevel: level } },
267
+ });
268
+ return;
269
+ } catch (error) {
270
+ if (!/Reasoning level/.test(String(error.message))) return;
271
+ }
272
+ }
273
+ }
274
+
275
+ // The agent's available-list can lag its registry (e.g. GLM-5.3-Flash is
276
+ // selectable but never broadcast). Probe each declared model: registered
277
+ // models answer the reasoning-level demand, unregistered ones reject outright.
278
+ // The session's active model is restored afterwards.
279
+ async function discoverModels(session) {
280
+ const paths = providerConfigPaths();
281
+ if (!paths.builtin) return;
282
+ const accounts = accountProviders(paths.builtin);
283
+ const known = new Set(session.state.models.map(model => model.id));
284
+ const fallback = session.state.models[0] ?? null;
285
+ for (const account of accounts) {
286
+ for (const modelId of account.modelIds) {
287
+ if (known.has(modelId)) continue;
288
+ const levels = [];
289
+ for (const level of ['low', 'high', 'max', 'medium']) {
290
+ try {
291
+ await session.proc.request('session/setModel', {
292
+ sessionId: session.state.sessionId,
293
+ model: { providerId: account.providerId, modelId, options: { reasoningLevel: level } },
294
+ });
295
+ levels.push(level);
296
+ } catch (error) {
297
+ if (/Reasoning level/.test(String(error.message))) continue;
298
+ break;
299
+ }
300
+ }
301
+ if (levels.length) {
302
+ known.add(modelId);
303
+ session.state.models.push({
304
+ id: modelId, name: modelId, provider: account.providerId,
305
+ efforts: levels, defaultEffort: levels[0],
306
+ });
307
+ }
308
+ }
309
+ }
310
+ if (fallback) {
311
+ const level = fallback.efforts?.[0] ?? fallback.defaultEffort;
312
+ await session.proc.request('session/setModel', {
313
+ sessionId: session.state.sessionId,
314
+ model: { providerId: fallback.provider, modelId: fallback.id, ...(level ? { options: { reasoningLevel: level } } : {}) },
315
+ }).catch(() => {});
316
+ }
317
+ }
318
+
319
+ function modelView(entry) {
320
+ const id = entry?.ref?.modelId ?? entry?.modelId ?? entry?.id;
321
+ if (!id || typeof id !== 'string') return null;
322
+ const provider = typeof entry?.ref?.providerId === 'string' ? entry.ref.providerId : (typeof entry?.providerId === 'string' ? entry.providerId : undefined);
323
+ const levels = Array.isArray(entry?.reasoning?.levels)
324
+ ? entry.reasoning.levels.map(level => (typeof level === 'string' ? level : level?.value)).filter(Boolean)
325
+ : [];
326
+ const view = { id, name: entry?.label ?? entry?.displayName ?? id };
327
+ if (provider) view.provider = provider;
328
+ if (levels.length) view.efforts = levels;
329
+ if (entry?.reasoning?.defaultLevel) view.defaultEffort = entry.reasoning.defaultLevel;
330
+ if (Number.isFinite(entry?.contextWindow)) view.contextWindow = entry.contextWindow;
331
+ return view;
332
+ }
333
+
334
+ function usageView(turn) {
335
+ const usage = turn?.usage && typeof turn.usage === 'object' ? turn.usage : {};
336
+ return {
337
+ inputTokens: usage.inputTokens,
338
+ outputTokens: usage.outputTokens,
339
+ cachedInputTokens: turn?.cacheStats?.cacheReadTokens ?? usage.cacheReadTokens,
340
+ totalTokens: turn?.tokenCount ?? usage.totalTokens,
341
+ };
342
+ }
343
+
344
+ function asText(value) {
345
+ if (value === undefined || value === null) return undefined;
346
+ return typeof value === 'string' ? value : JSON.stringify(value);
347
+ }
348
+
349
+ // dataBase64-only images degrade to a metadata placeholder on this wire
350
+ // (verified live), so materialize them to a temp file and hand over localPath.
351
+ const IMAGE_EXTENSIONS = { 'image/png': '.png', 'image/jpeg': '.jpg', 'image/gif': '.gif', 'image/webp': '.webp', 'image/svg+xml': '.svg' };
352
+ function materializeImage(session, image) {
353
+ const extension = IMAGE_EXTENSIONS[image.mime] ?? '.png';
354
+ const file = path.join(os.tmpdir(), `harness-mix-zcode-${randomUUID()}${extension}`);
355
+ fs.writeFileSync(file, Buffer.from(image.data, 'base64'));
356
+ session.state.tempFiles.push(file);
357
+ return file;
358
+ }
359
+
360
+ function nativeAttachments(session, attachments) {
361
+ return (attachments?.images ?? []).map(image => {
362
+ const localPath = image.path || (image.data ? materializeImage(session, image) : null);
363
+ if (!localPath) return null;
364
+ return {
365
+ kind: 'image',
366
+ filename: image.name ?? 'image.png',
367
+ mimeType: image.mime ?? 'image/png',
368
+ localPath,
369
+ ...(image.data ? { sizeBytes: Math.floor(image.data.length * 3 / 4) } : {}),
370
+ };
371
+ }).filter(Boolean);
372
+ }
373
+
374
+ function attachSession(launch, { thread, emit, diagnostic }) {
375
+ const session = {
376
+ proc: null, threadRef: thread, cwd: thread.cwd, model: null, emit, diagnostic,
377
+ state: {
378
+ sessionId: null, active: false, turn: null, closed: false,
379
+ models: [], usage: undefined, context: undefined, turnText: '',
380
+ pending: new Map(), seenEvents: new Set(), afterSeq: 0, pollTimer: null, polling: false,
381
+ // 取消后到下一回合开始之间的 turn.completed/turn.failed 属于被停掉的旧回合,
382
+ // 不得结算新回合(否则下一轮秒回空文本)
383
+ suppressCompletions: false,
384
+ // 进程代际:重连后旧进程的迟到 onExit/onEvent 不得污染新会话状态
385
+ generation: 0,
386
+ // 附件临时文件(base64 落盘):会话关闭时清理
387
+ tempFiles: [],
388
+ // 子进程 stderr 尾部:进程异常退出时并入错误消息,连接页可见真实原因
389
+ stderrTail: [],
390
+ },
391
+ };
392
+ bindProcess(session, launch, thread);
393
+ return session;
394
+ }
395
+
396
+ // (Re)bind a transport process onto an existing session object. Reconnects
397
+ // reuse this: the generation guard makes the retired process's late
398
+ // exit/event callbacks no-ops.
399
+ function bindProcess(session, launch, thread) {
400
+ const generation = ++session.state.generation;
401
+ const stale = () => generation !== session.state.generation;
402
+ session.proc = new JsonlProcess(launch.command, launch.args, {
403
+ cwd: session.cwd, env: { ...process.env, ...agentEnvironment(), ...(thread.environment || {}) }, jsonrpc: false,
404
+ }, {
405
+ onRequest: request => { if (stale()) return {}; return handleServerRequest(session, request); },
406
+ onEvent: value => { if (!stale()) handleNotification(session, value); },
407
+ onDiagnostic: line => {
408
+ if (stale()) return;
409
+ const text = String(line);
410
+ session.state.stderrTail.push(text);
411
+ if (session.state.stderrTail.length > 12) session.state.stderrTail.shift();
412
+ diagnosticGuard(session, text);
413
+ },
414
+ onExit: error => {
415
+ if (stale()) return;
416
+ session.state.closed = true;
417
+ if (session.state.pollTimer) { clearInterval(session.state.pollTimer); session.state.pollTimer = null; }
418
+ // Server requests parked on user answers can no longer be answered.
419
+ for (const resolve of session.state.pending.values()) resolve({ cancelled: true });
420
+ session.state.pending.clear();
421
+ const tail = session.state.stderrTail.join('\n').slice(-800);
422
+ if (tail) error.message = `${error.message}\n${tail}`;
423
+ session.state.turn?.reject(error);
424
+ session.state.turn = null;
425
+ session.state.active = false;
426
+ },
427
+ });
428
+ }
429
+
430
+ function diagnosticGuard(session, text) {
431
+ try { session.diagnostic?.(text); } catch { /* renderer diagnostics must not break the pump */ }
432
+ }
433
+
434
+ // The context projection rides the subscribe snapshot (and state.updated
435
+ // patches); normalize it to the keys projectUsage consumes (tokens/contextWindow).
436
+ function captureProjection(session, projection) {
437
+ if (!projection || typeof projection !== 'object') return;
438
+ const used = Number(projection.contextUsed);
439
+ const window = Number(projection.contextWindow);
440
+ if (Number.isFinite(used) && Number.isFinite(window) && window > 0) {
441
+ session.state.context = { tokens: used, contextWindow: window };
442
+ }
443
+ }
444
+
445
+ // A dead transport between turns used to hang the next send forever (requests
446
+ // to an exited JsonlProcess never settle). Restore the same native session on
447
+ // a fresh process and replay the confirmed mode/model selections.
448
+ async function reconnectSession(session, applySelections) {
449
+ const thread = session.threadRef;
450
+ session.diagnostic?.('ZCode 原生进程已退出,正在恢复原生会话…');
451
+ try { session.proc?.stop(); } catch { /* already gone */ }
452
+ if (session.state.pollTimer) { clearInterval(session.state.pollTimer); session.state.pollTimer = null; }
453
+ session.state.pending.clear();
454
+ session.state.closed = false;
455
+ session.state.turnText = '';
456
+ session.state.afterSeq = 0;
457
+ // 新进程 = 新事件空间:eventId 可能与旧进程撞号(fixture 计数器如此,真实
458
+ // 服务端亦不保证跨进程唯一)。跨重连保留去重集会把恢复后的全部事件当
459
+ // 重复丢弃,回合永不结算。
460
+ session.state.seenEvents.clear();
461
+ bindProcess(session, resolveLaunch(), thread);
462
+ await startSession(session, { ...thread, restore: true, nativeSessionId: session.state.sessionId });
463
+ const subscribed = await session.proc.request('session/subscribe', { sessionId: session.state.sessionId, deliveryKind: 'desktop-continuous', includeSnapshot: true }).catch(() => null);
464
+ if (Number.isFinite(Number(subscribed?.eventSeq))) session.state.afterSeq = Number(subscribed.eventSeq);
465
+ captureProjection(session, subscribed?.snapshot?.projection);
466
+ startEventPolling(session);
467
+ // 新进程没有任何供应商声明:重放账号推送,否则 setModel/send 全被拒。
468
+ await pushAccountConfig(session);
469
+ await applySelections();
470
+ }
471
+
472
+ function emitInteraction(session, method, params, requestId) {
473
+ if (method === 'interaction/requestPermission') {
474
+ session.emit({
475
+ kind: 'approval', requestId,
476
+ title: `ZCode 权限 · ${params?.toolName ?? '工具'}`,
477
+ message: params?.reason ?? asText(params?.input) ?? '',
478
+ options: [
479
+ { id: 'accept', label: '允许' },
480
+ { id: 'decline', label: '拒绝', kind: 'reject' },
481
+ ],
482
+ });
483
+ return;
484
+ }
485
+ if (method === 'interaction/requestUserInput') {
486
+ const choices = Array.isArray(params?.choices) ? params.choices : [];
487
+ session.emit({
488
+ kind: 'approval', requestId,
489
+ ...(choices.length ? {} : { method: 'input' }),
490
+ title: 'ZCode 提问',
491
+ message: params?.prompt ?? '',
492
+ ...(choices.length
493
+ ? { options: choices.map(choice => ({ id: choice, label: choice })) }
494
+ : { placeholder: '请输入…' }),
495
+ });
496
+ }
497
+ }
498
+
499
+ // Permissions and questions stay open until the user answers via respond();
500
+ // the returned promise is what JsonlProcess writes back to the server.
501
+ function parkInteraction(session, request, buildAnswer) {
502
+ const requestId = `zcode-${request.id}-${request.method}`;
503
+ emitInteraction(session, request.method, request.params, requestId);
504
+ return new Promise(resolve => {
505
+ session.state.pending.set(requestId, answer => {
506
+ session.state.pending.delete(requestId);
507
+ resolve(answer?.cancelled ? { cancelled: true } : buildAnswer(answer));
508
+ });
509
+ });
510
+ }
511
+
512
+ function handleServerRequest(session, request) {
513
+ if (request.method === 'session/requestRuntimePreferences') return RUNTIME_PREFERENCES;
514
+ if (request.method === 'interaction/requestOfficialMcpAuthHeaders') {
515
+ return { ok: false, reason: 'official_auth_unavailable' };
516
+ }
517
+ if (request.method === 'interaction/requestProviderRuntimeHeaders') {
518
+ // Before every model request the agent asks its client for the request
519
+ // auth (same-machine stdio, exactly like the desktop client). The key is
520
+ // read in memory only: never logged, persisted, or sent anywhere else.
521
+ const providerId = request.params?.providerId;
522
+ const apiKey = credentialValueFor(providerId);
523
+ if (!apiKey) return { headersApplied: false, errorMessage: 'credential unavailable' };
524
+ return { headersApplied: true, requestAuth: { apiKey } };
525
+ }
526
+ if (request.method === 'interaction/requestPermission') {
527
+ return parkInteraction(session, request, answer => ({
528
+ decision: answer.optionId === 'decline' ? 'deny' : 'allow',
529
+ }));
530
+ }
531
+ if (request.method === 'interaction/requestUserInput') {
532
+ return parkInteraction(session, request, answer => ({ value: answer.optionId ?? answer.value ?? '' }));
533
+ }
534
+ session.diagnostic?.(`ZCode server request unhandled: ${request.method}`);
535
+ return {};
536
+ }
537
+
538
+ // Resolve the credential VALUE for an account provider, in memory only, and
539
+ // hand it straight to the local agent's runtime-headers request — exactly the
540
+ // flow the official desktop client runs. Values are AES-256-GCM encrypted at
541
+ // rest (enc:v1:<iv>.<tag>.<ct>, key = sha256(secret)); the secret comes from
542
+ // ZCODE_CREDENTIAL_SECRET or ZCode's platform fallback. The plaintext is never
543
+ // logged, persisted, or sent anywhere but the local agent's stdin.
544
+ function credentialValueFor(providerId) {
545
+ const paths = providerConfigPaths();
546
+ if (!paths.builtin) return null;
547
+ const account = accountProviders(paths.builtin).find(entry => entry.providerId === providerId);
548
+ if (!account) return null;
549
+ const file = process.env.HARNESS_MIX_ZCODE_CREDENTIALS
550
+ || (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, '.zcode', 'v2', 'credentials.json') : null);
551
+ try {
552
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'))?.[account.connectionKey];
553
+ if (typeof raw !== 'string') return null;
554
+ const secret = process.env.ZCODE_CREDENTIAL_SECRET?.trim() || (() => {
555
+ let username = 'unknown';
556
+ try { username = require('node:os').userInfo().username; } catch { /* keep fallback */ }
557
+ const os = require('node:os');
558
+ return `zcode-credential-fallback:${os.platform()}:${os.homedir()}:${username}`;
559
+ })();
560
+ const crypto = require('node:crypto');
561
+ const key = crypto.createHash('sha256').update(secret).digest();
562
+ const parts = raw.slice('enc:v1:'.length).split('.');
563
+ if (parts.length !== 3) return null;
564
+ const iv = Buffer.from(parts[0], 'base64url');
565
+ const tag = Buffer.from(parts[1], 'base64url');
566
+ const body = Buffer.from(parts[2], 'base64url');
567
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
568
+ decipher.setAuthTag(tag);
569
+ const plain = Buffer.concat([decipher.update(body), decipher.final()]).toString('utf-8');
570
+ return plain.trim() || null;
571
+ } catch {
572
+ return null;
573
+ }
574
+ }
575
+
576
+ // The agent's stream recovery (streamRecovery, recoveredFromRequestId) replays
577
+ // the tail window of the model stream after every upstream hiccup: the same
578
+ // span arrives a second time, the replay extending a few characters past the
579
+ // first copy, under fresh eventIds the eventId dedup cannot catch. Flatten by
580
+ // trimming any replayed overlap — a delta that begins with the accumulated
581
+ // text's own tail. A genuine continuation cannot repeat the exact 12+ chars it
582
+ // just ended with, while a replayed window always does.
583
+ function trimStreamReplayOverlap(accumulated, delta) {
584
+ if (!accumulated) return delta;
585
+ const longest = Math.min(accumulated.length, delta.length);
586
+ for (let length = longest; length >= 12; length -= 1) {
587
+ if (accumulated.endsWith(delta.slice(0, length))) return delta.slice(length);
588
+ }
589
+ return delta;
590
+ }
591
+
592
+ function settleTurn(session, error) {
593
+ const turn = session.state.turn;
594
+ session.state.turn = null;
595
+ session.state.active = false;
596
+ if (!turn) return;
597
+ if (error) turn.reject(error);
598
+ else turn.resolve();
599
+ }
600
+
601
+ function projectSessionEvent(session, payload, eventId) {
602
+ if (!payload || typeof payload !== 'object') return;
603
+ if (eventId !== undefined) {
604
+ if (session.state.seenEvents.has(eventId)) return;
605
+ session.state.seenEvents.add(eventId);
606
+ if (session.state.seenEvents.size > 500) {
607
+ for (const item of session.state.seenEvents) { session.state.seenEvents.delete(item); break; }
608
+ }
609
+ }
610
+ const emit = session.emit;
611
+ switch (payload.type) {
612
+ case 'part.delta': {
613
+ if (payload.field === 'text') {
614
+ const delta = trimStreamReplayOverlap(session.state.turnText, payload.delta);
615
+ if (delta) { session.state.turnText += delta; emit({ kind: 'text-delta', text: delta }); }
616
+ }
617
+ else if (payload.field === 'reasoning') emit({ kind: 'thinking-delta', text: payload.delta });
618
+ break;
619
+ }
620
+ case 'model.streaming': {
621
+ if (payload.kind === 'text_delta' && payload.delta) {
622
+ const delta = trimStreamReplayOverlap(session.state.turnText, payload.delta);
623
+ if (delta) { session.state.turnText += delta; emit({ kind: 'text-delta', text: delta }); }
624
+ }
625
+ else if (payload.kind === 'reasoning_delta' && payload.delta) emit({ kind: 'thinking-delta', text: payload.delta });
626
+ break;
627
+ }
628
+ case 'tool.updated': {
629
+ const title = payload.toolName ?? payload.toolCallId ?? 'tool';
630
+ if (payload.kind === 'scheduled' || payload.kind === 'started' || payload.kind === 'progress') {
631
+ emit({
632
+ kind: 'tool', toolCallId: payload.toolCallId, title, state: 'running',
633
+ ...(asText(payload.input) !== undefined ? { input: asText(payload.input) } : {}),
634
+ });
635
+ } else if (payload.kind === 'result') {
636
+ emit({
637
+ kind: 'tool', toolCallId: payload.toolCallId, title, state: 'done',
638
+ ...(asText(payload.output ?? payload.result ?? payload.text) !== undefined
639
+ ? { output: asText(payload.output ?? payload.result ?? payload.text) } : {}),
640
+ });
641
+ } else if (payload.kind === 'error') {
642
+ emit({
643
+ kind: 'tool', toolCallId: payload.toolCallId, title, state: 'error',
644
+ ...(payload.error?.message ? { output: String(payload.error.message) } : {}),
645
+ });
646
+ }
647
+ break;
648
+ }
649
+ case 'turn.started': {
650
+ session.state.turnText = '';
651
+ session.state.suppressCompletions = false;
652
+ break;
653
+ }
654
+ case 'turn.completed': {
655
+ if (session.state.suppressCompletions) break;
656
+ session.state.usage = usageView(payload);
657
+ emit({ kind: 'usage', usage: session.state.usage });
658
+ // SSE 在部分网络下只经拉取通道送达:若无流式 delta,补发完整回复
659
+ if (payload.response && !session.state.turnText) emit({ kind: 'text-delta', text: payload.response });
660
+ emit({ kind: 'completed', finalAnswer: true });
661
+ settleTurn(session, null);
662
+ break;
663
+ }
664
+ case 'turn.failed': {
665
+ if (session.state.suppressCompletions) break;
666
+ const message = payload.error?.message ?? 'ZCode 回合失败';
667
+ emit({ kind: 'error', message });
668
+ settleTurn(session, new Error(message));
669
+ break;
670
+ }
671
+ default:
672
+ break;
673
+ }
674
+ }
675
+
676
+ function handleNotification(session, value) {
677
+ if (value.method === 'session/event') {
678
+ // Wire shape: {deliveryKind, eventId, type, payload:{…fields}} — type sits
679
+ // on the envelope, payload holds only the fields. Normalize before projecting.
680
+ const params = value.params ?? {};
681
+ const payload = { ...(params.payload ?? {}), ...(params.type ? { type: params.type } : {}) };
682
+ projectSessionEvent(session, payload, params.eventId);
683
+ return;
684
+ }
685
+ if (value.method !== 'state.updated') return;
686
+ const patch = value.params?.patch;
687
+ if (!patch || typeof patch !== 'object') return;
688
+ if (Array.isArray(patch.model?.available)) {
689
+ // Merge, don't replace: probed entries (models the agent accepts but
690
+ // never broadcasts) would otherwise be wiped by every patch.
691
+ const reported = patch.model.available.map(modelView).filter(Boolean);
692
+ const merged = [...reported];
693
+ for (const extra of session.state.models) {
694
+ if (!merged.some(model => model.id === extra.id)) merged.push(extra);
695
+ }
696
+ session.state.models = merged;
697
+ }
698
+ const projection = patch.projection;
699
+ if (projection && typeof projection === 'object') {
700
+ captureProjection(session, projection);
701
+ }
702
+ }
703
+
704
+ async function startSession(session, thread) {
705
+ const resumeId = thread.restore ? thread.nativeSessionId : null;
706
+ if (resumeId) {
707
+ try {
708
+ const resumed = await session.proc.request('session/resume', { sessionId: resumeId, workspace: workspaceIdentity(thread.cwd) });
709
+ session.state.sessionId = resumed?.session?.sessionId ?? resumeId;
710
+ return;
711
+ } catch (error) {
712
+ session.diagnostic?.(`ZCode 会话恢复失败,改用新会话:${error.message}`);
713
+ }
714
+ }
715
+ const created = await session.proc.request('session/create', { workspace: workspaceIdentity(thread.cwd) });
716
+ session.state.sessionId = created?.session?.sessionId;
717
+ if (!session.state.sessionId) throw new Error('ZCode app-server 未返回 sessionId');
718
+ }
719
+
720
+ // The desktop-continuous push channel silently stalls on some networks while
721
+ // the pull API (session/events + afterSeq) keeps delivering; poll it as the
722
+ // primary event source, with push handled too (eventId dedup keeps them from
723
+ // double-firing).
724
+ function startEventPolling(session) {
725
+ session.state.pollTimer = setInterval(async () => {
726
+ if (session.state.closed || session.state.polling) return;
727
+ session.state.polling = true;
728
+ try {
729
+ const result = await session.proc.request('session/events', { sessionId: session.state.sessionId, afterSeq: session.state.afterSeq });
730
+ const events = result?.events ?? [];
731
+ let maxSeq = session.state.afterSeq;
732
+ for (const event of events) {
733
+ const seq = Number(event.seq);
734
+ if (Number.isFinite(seq) && seq > maxSeq) maxSeq = seq;
735
+ const payload = { ...(event.payload ?? {}), ...(event.type ? { type: event.type } : {}) };
736
+ projectSessionEvent(session, payload, event.eventId);
737
+ }
738
+ if (maxSeq > session.state.afterSeq) session.state.afterSeq = maxSeq;
739
+ } catch { /* transient poll failure; the next tick retries */ }
740
+ session.state.polling = false;
741
+ }, 350);
742
+ }
743
+
744
+ function create() {
745
+ return {
746
+ manifest,
747
+
748
+ async inspect() {
749
+ try {
750
+ const launch = resolveLaunch();
751
+ return await new Promise(resolve => {
752
+ execFile(launch.command, [...launch.args, '--version'], { timeout: 15000, windowsHide: true }, (error, out) => {
753
+ resolve(error ? { available: false, detail: `${manifest.name} CLI 启动失败` } : { available: true, detail: String(out).trim() });
754
+ });
755
+ });
756
+ } catch (error) {
757
+ return { available: false, detail: error.message };
758
+ }
759
+ },
760
+
761
+ async describe() { return { models: null, thinkingLevels: [], permissionModes: PERMISSION_MODES }; },
762
+
763
+ async open({ thread, emit, diagnostic = () => {}, collaboration }) {
764
+ // `collaboration` is accepted (worker/Agent-Team membership works through
765
+ // kernel-driven dispatch) but the lead-side MCP tools are not wired yet —
766
+ // see the header note; deliberately NOT setting collaborationEnabled
767
+ // keeps the `#`-lead gate honest until that lands.
768
+ void collaboration;
769
+ const launch = resolveLaunch();
770
+ const session = attachSession(launch, { thread, emit, diagnostic });
771
+ try {
772
+ await startSession(session, thread);
773
+ const subscribed = await session.proc.request('session/subscribe', { sessionId: session.state.sessionId, deliveryKind: 'desktop-continuous', includeSnapshot: true }).catch(() => null);
774
+ if (Number.isFinite(Number(subscribed?.eventSeq))) session.state.afterSeq = Number(subscribed.eventSeq);
775
+ captureProjection(session, subscribed?.snapshot?.projection);
776
+ startEventPolling(session);
777
+ // The user's mode choice rides on the thread options (new-thread
778
+ // preference or mid-session selector); apply it like the desktop does.
779
+ if (thread.options?.permissionMode) await this.setPermissionMode(session, thread.options.permissionMode);
780
+ await pushAccountConfig(session);
781
+ await ensureModelCatalog(session).catch(() => {});
782
+ emit({ kind: 'session', nativeSessionId: session.state.sessionId });
783
+ return session;
784
+ } catch (error) {
785
+ session.proc.stop();
786
+ throw error;
787
+ }
788
+ },
789
+
790
+ async send(session, prompt, _hooks, attachments) {
791
+ if (session.state.active) throw new Error('ZCode 当前回合尚未结束');
792
+ if (session.state.closed) {
793
+ await reconnectSession(session, async () => {
794
+ if (session.permissionMode) await this.setPermissionMode(session, session.permissionMode)
795
+ .catch(error => session.diagnostic?.(`ZCode 恢复权限模式失败:${error.message}`));
796
+ if (session.model) await this.setModel(session, session.model)
797
+ .catch(error => session.diagnostic?.(`ZCode 恢复模型选择失败:${error.message}`));
798
+ else await ensureModelCatalog(session).catch(() => {});
799
+ });
800
+ }
801
+ session.state.active = true;
802
+ const settled = new Promise((resolve, reject) => { session.state.turn = { resolve, reject }; });
803
+ const images = nativeAttachments(session, attachments);
804
+ try {
805
+ const result = await session.proc.request('session/send', {
806
+ sessionId: session.state.sessionId,
807
+ content: prompt,
808
+ ...(images.length ? { attachments: images } : {}),
809
+ });
810
+ if (result && result.accepted === false) throw new Error('ZCode 拒绝了这条消息');
811
+ } catch (error) {
812
+ settleTurn(session, error);
813
+ throw error;
814
+ }
815
+ return settled;
816
+ },
817
+
818
+ async cancel(session) {
819
+ if (session.state.sessionId) {
820
+ session.proc.request('session/stop', { sessionId: session.state.sessionId }).catch(() => {});
821
+ }
822
+ for (const resolve of session.state.pending.values()) resolve({ cancelled: true });
823
+ session.state.pending.clear();
824
+ // 旧回合的迟到 turn.completed/turn.failed 不得结算下一个回合;见
825
+ // projectSessionEvent 的 suppressCompletions 守卫(turn.started 复位)。
826
+ session.state.suppressCompletions = true;
827
+ settleTurn(session, null);
828
+ },
829
+
830
+ async respond(session, requestId, response) {
831
+ const resolvePending = session.state.pending.get(requestId);
832
+ if (!resolvePending) throw new Error('ZCode 原生请求已经结束');
833
+ // decline 必须走 buildAnswer 映射成 {decision:'deny'};短路成 {cancelled:true}
834
+ // 会让服务端把「拒绝」当「无应答」处理。
835
+ if (response?.cancelled || response?.confirmed === false) resolvePending({ cancelled: true });
836
+ else resolvePending({ optionId: response?.optionId, value: response?.value });
837
+ },
838
+
839
+ async listModelsFor(session) {
840
+ return (await this.describeFor(session)).models;
841
+ },
842
+
843
+ async describeFor(session) {
844
+ // Models materialize once the account declaration is applied and the
845
+ // agent resolves its own credentials; if the first push candidate didn't
846
+ // match, retry the remaining revision forms before giving up.
847
+ const deadline = Date.now() + 6000;
848
+ while (!session.state.models.length && Date.now() < deadline && !session.state.closed) {
849
+ await new Promise(resolve => setTimeout(resolve, 300));
850
+ }
851
+ if (!session.state.models.length && !session.state.closed) {
852
+ await pushAccountConfig(session);
853
+ const extended = Date.now() + 6000;
854
+ while (!session.state.models.length && Date.now() < extended && !session.state.closed) {
855
+ await new Promise(resolve => setTimeout(resolve, 300));
856
+ }
857
+ }
858
+ if (session.state.models.length && !session.state.closed) {
859
+ await discoverModels(session);
860
+ }
861
+ if (!session.state.models.length) {
862
+ session.diagnostic?.('ZCode 模型目录未物化:需要 ZCode 桌面版安装(内建供应商目录)、已登录的共享凭据(connectionKey)与可达 Z.AI 的网络。');
863
+ }
864
+ const levels = [...new Set(session.state.models.flatMap(model => model.efforts ?? []))];
865
+ return {
866
+ models: session.state.models.length ? session.state.models : null,
867
+ thinkingLevels: levels.map(level => ({ id: level, label: level })),
868
+ permissionModes: PERMISSION_MODES,
869
+ };
870
+ },
871
+
872
+ async setPermissionMode(session, mode) {
873
+ if (!PERMISSION_MODES.some(entry => entry.id === mode)) throw new Error(`未知的 ZCode 权限模式:${mode}`);
874
+ await session.proc.request('session/setMode', { sessionId: session.state.sessionId, mode });
875
+ session.permissionMode = mode;
876
+ },
877
+
878
+ async setModel(session, model) {
879
+ // GLM models require an explicit reasoningLevel; default to the model's
880
+ // own default level when the selection didn't carry one.
881
+ const view = session.state.models.find(entry => entry.id === model.id);
882
+ const level = model.effort ?? view?.defaultEffort ?? view?.efforts?.[0]
883
+ ?? session.state.models.flatMap(entry => entry.efforts ?? [])[0];
884
+ await session.proc.request('session/setModel', {
885
+ sessionId: session.state.sessionId,
886
+ model: {
887
+ providerId: model.provider ?? view?.provider ?? 'account:bigmodel-individual-coding-plan',
888
+ modelId: model.id,
889
+ ...(level ? { options: { reasoningLevel: level } } : {}),
890
+ },
891
+ });
892
+ session.model = { id: model.id, name: model.name ?? model.id, provider: model.provider ?? view?.provider };
893
+ return session.model;
894
+ },
895
+
896
+ async setThinkingLevel(session, level) {
897
+ await session.proc.request('session/setThoughtLevel', { sessionId: session.state.sessionId, thoughtLevel: level });
898
+ },
899
+
900
+ async getContextUsage(session) { return session.state.context; },
901
+
902
+ async close(session) {
903
+ session.state.closed = true;
904
+ if (session.state.pollTimer) { clearInterval(session.state.pollTimer); session.state.pollTimer = null; }
905
+ for (const resolve of session.state.pending.values()) resolve({ cancelled: true });
906
+ session.state.pending.clear();
907
+ for (const file of session.state.tempFiles.splice(0)) { try { fs.rmSync(file, { force: true }); } catch { /* best-effort cleanup */ } }
908
+ settleTurn(session, null);
909
+ if (session.state.sessionId) {
910
+ session.proc.request('session/close', { sessionId: session.state.sessionId }).catch(() => {});
911
+ }
912
+ session.proc.stop();
913
+ },
914
+ };
915
+ }
916
+
917
+ module.exports = { manifest, create, resolveLaunch, modelView, usageView, handleNotification, trimStreamReplayOverlap };
918
+
919
+ // ZCode scans, per scope: .zcode/skills then .agents/skills (deeper workspace levels win).
920
+ // https://zcode.z.ai/en/docs/skill
921
+ module.exports.manifest.integrations = { mcp: false, skills: { global: ['.zcode/skills', '.agents/skills'], project: ['.zcode/skills', '.agents/skills'] } };