evolcore 0.0.21 → 0.0.23
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/AGENTS.md +62 -0
- package/CHANGELOG.md +53 -0
- package/README.md +2 -0
- package/bin/codex-managed-acl.ps1 +155 -0
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +376 -96
- package/dist/agents/baseagent-availability.js +48 -0
- package/dist/agents/baseagent.js +1 -1
- package/dist/agents/claude-runner.js +44 -226
- package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
- package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
- package/dist/agents/codex/catalog.js +70 -0
- package/dist/agents/codex/diagnostics.js +154 -0
- package/dist/agents/codex-runner.js +1292 -219
- package/dist/agents/ecagent-runner.js +10 -31
- package/dist/agents/gemini-runner.js +2 -6
- package/dist/aun/aid/store.js +2 -0
- package/dist/aun/menu-client.js +272 -0
- package/dist/aun/msg/group.js +65 -4
- package/dist/aun/msg/index.js +1 -1
- package/dist/aun/msg/managed-operation.js +51 -4
- package/dist/aun/outbox.js +7 -0
- package/dist/aun/rpc/client.js +8 -1
- package/dist/channels/aun.js +240 -36
- package/dist/channels/wechat.js +81 -10
- package/dist/cli/aun-commands.js +48 -4
- package/dist/cli/command-log.js +21 -2
- package/dist/cli/daemon-commands.js +169 -51
- package/dist/cli/index.js +20 -0
- package/dist/cli/init.js +2 -10
- package/dist/cli/menu-client.js +95 -0
- package/dist/cli/model.js +23 -8
- package/dist/cli/task-context.js +16 -2
- package/dist/cli/trigger-command.js +22 -5
- package/dist/config/config-field-policy.js +3 -0
- package/dist/config/config-manager.js +32 -5
- package/dist/config/contact-book-store.js +25 -3
- package/dist/config/schema-migration.js +14 -2
- package/dist/core/auth/agent-delegation.js +15 -0
- package/dist/core/auth/auth-gateway.js +8 -0
- package/dist/core/auth/authorization-audit.js +70 -6
- package/dist/core/auth/operation-catalog.js +4 -0
- package/dist/core/auth/remote-access-store.js +135 -0
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +93 -25
- package/dist/core/command/command-handler.js +62 -5
- package/dist/core/command/daemon-diagnostics.js +448 -0
- package/dist/core/command/menu-catalog.js +3 -0
- package/dist/core/command/menu-handler.js +150 -33
- package/dist/core/command/menu-protocol.js +120 -1
- package/dist/core/command/slash-gate.js +3 -3
- package/dist/core/command/slash-handler.js +83 -40
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +32 -0
- package/dist/core/handoff/runtime.js +23 -3
- package/dist/core/message/im-renderer.js +7 -3
- package/dist/core/message/message-bridge.js +60 -2
- package/dist/core/message/message-log.js +33 -0
- package/dist/core/message/message-queue.js +41 -15
- package/dist/core/message/response-engine.js +246 -114
- package/dist/core/model/catalog-service.js +308 -0
- package/dist/core/model/model-catalog.js +124 -334
- package/dist/core/model/model-diagnostics.js +4 -5
- package/dist/core/model/model-fallback.js +9 -16
- package/dist/core/model/model-permission.js +1 -1
- package/dist/core/permission/approval-gateway.js +16 -1
- package/dist/core/permission/ec-command-parser.js +272 -70
- package/dist/core/permission/protected-paths.js +74 -14
- package/dist/core/permission/sandbox-runtime.js +4 -2
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +290 -30
- package/dist/core/runtime-lock.js +13 -8
- package/dist/core/session/session-manager.js +30 -0
- package/dist/core/session/session-renew.js +18 -1
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/index.js +104 -34
- package/dist/ipc.js +218 -96
- package/dist/paths.js +18 -0
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/feedback.js +14 -2
- package/dist/trigger/manager.js +3 -1
- package/dist/trigger/parser.js +86 -13
- package/dist/trigger/patch.js +104 -1
- package/dist/trigger/scheduler.js +20 -3
- package/dist/utils/ecweb-supervisor.js +11 -2
- package/dist/utils/ecweb-utils.js +8 -26
- package/dist/utils/error-utils.js +7 -0
- package/dist/utils/logger.js +9 -4
- package/dist/utils/npm-ops.js +47 -15
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/evolcore/INDEX.md +3 -2
- package/kits/docs/evolcore/agent-create.md +159 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/group-collaboration.md +256 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/menu-client.md +34 -0
- package/kits/docs/evolcore/trigger.md +6 -3
- package/kits/docs/prompt-loading-architecture.md +6 -0
- package/kits/eck_message_manifest.json +6 -6
- package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
- package/kits/migrations/migrate-role-config-v4.mjs +11 -5
- package/kits/migrations/migrate-role-config-v5.mjs +8 -2
- package/kits/schemas/_meta.json +3 -2
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +10 -2
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +11 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { getRunnerCatalog, peekRunnerCatalog, catalogEfforts, resolveCatalogModel, assertCatalogSelectable } from '../core/model/model-catalog.js';
|
|
1
2
|
import { query, forkSession as sdkForkSession, getSessionMessages as sdkGetSessionMessages, resolveSettings as sdkResolveSettings, } from '@anthropic-ai/claude-agent-sdk';
|
|
2
3
|
import { spawn } from 'child_process';
|
|
3
|
-
import {
|
|
4
|
+
import { ensureDir } from '../utils/atomic-write.js';
|
|
4
5
|
import { resolveAnthropicConfig } from './baseagent.js';
|
|
5
6
|
import { buildModelRequestHeaders } from './request-identity.js';
|
|
6
7
|
import { DEFAULT_PERMISSION_MODE } from '../types.js';
|
|
@@ -261,103 +262,6 @@ async function assertClaudeSettingSourcesHaveLiteralSandboxPaths(cwd, settingSou
|
|
|
261
262
|
}
|
|
262
263
|
}
|
|
263
264
|
}
|
|
264
|
-
// ── 模型别名解析 ──
|
|
265
|
-
// SDK 内置的别名表可能落后于代理实际可用的最新模型,
|
|
266
|
-
// 因此优先从 {baseUrl}/models 动态获取各系列最新版本,失败则使用持久化的最近成功值。
|
|
267
|
-
// 已验证可用但尚未出现在 /models 列表中的模型 ID 会被注入候选列表,
|
|
268
|
-
// 等列表更新后注入自动变为 no-op。
|
|
269
|
-
const MODEL_FAMILIES = ['opus', 'sonnet', 'haiku'];
|
|
270
|
-
/** 已验证可用但可能尚未出现在 /models 列表中的模型 ID(注入候选) */
|
|
271
|
-
const INJECTED_MODELS = [];
|
|
272
|
-
/** 启动默认值:某网关尚无任何成功刷新记录时使用。 */
|
|
273
|
-
const BOOTSTRAP_MODEL_ALIASES = {
|
|
274
|
-
'opus': 'claude-opus-4-8',
|
|
275
|
-
'sonnet': 'claude-sonnet-4-6',
|
|
276
|
-
'haiku': 'claude-haiku-4-5-20251001',
|
|
277
|
-
};
|
|
278
|
-
const MODEL_ALIAS_TTL_MS = 5 * 60 * 1000; // 5min
|
|
279
|
-
const MODEL_ALIAS_STORE_VERSION = 1;
|
|
280
|
-
const modelAliasCache = new Map(); // key: baseUrl
|
|
281
|
-
const modelAliasFallbacks = new Map(); // key: baseUrl,最近一次成功解析值
|
|
282
|
-
const modelAliasInFlight = new Set(); // 去重并发刷新
|
|
283
|
-
const loadedModelAliasFallbacks = new Set(); // key: store path + gateway hash
|
|
284
|
-
function normalizeModelGatewayUrl(baseUrl) {
|
|
285
|
-
return baseUrl.trim().replace(/\/+$/, '');
|
|
286
|
-
}
|
|
287
|
-
function modelGatewayKey(baseUrl) {
|
|
288
|
-
return crypto.createHash('sha256').update(normalizeModelGatewayUrl(baseUrl)).digest('hex');
|
|
289
|
-
}
|
|
290
|
-
function modelAliasStorePath() {
|
|
291
|
-
return resolvePaths().claudeModelCache;
|
|
292
|
-
}
|
|
293
|
-
function sanitizeModelAliases(value) {
|
|
294
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
295
|
-
return {};
|
|
296
|
-
const aliases = {};
|
|
297
|
-
for (const family of MODEL_FAMILIES) {
|
|
298
|
-
const model = value[family];
|
|
299
|
-
if (typeof model !== 'string')
|
|
300
|
-
continue;
|
|
301
|
-
const pattern = new RegExp(`^claude-${family}-[A-Za-z0-9._-]+$`);
|
|
302
|
-
if (pattern.test(model))
|
|
303
|
-
aliases[family] = model;
|
|
304
|
-
}
|
|
305
|
-
return aliases;
|
|
306
|
-
}
|
|
307
|
-
function readPersistedAliasStore() {
|
|
308
|
-
try {
|
|
309
|
-
const value = atomicReadJson(modelAliasStorePath());
|
|
310
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
311
|
-
return { $schema_version: MODEL_ALIAS_STORE_VERSION, gateways: {} };
|
|
312
|
-
}
|
|
313
|
-
const raw = value;
|
|
314
|
-
if (raw.$schema_version !== MODEL_ALIAS_STORE_VERSION || !raw.gateways || typeof raw.gateways !== 'object' || Array.isArray(raw.gateways)) {
|
|
315
|
-
return { $schema_version: MODEL_ALIAS_STORE_VERSION, gateways: {} };
|
|
316
|
-
}
|
|
317
|
-
const gateways = {};
|
|
318
|
-
for (const [key, entry] of Object.entries(raw.gateways)) {
|
|
319
|
-
if (!/^[a-f0-9]{64}$/.test(key) || !entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
320
|
-
continue;
|
|
321
|
-
const record = entry;
|
|
322
|
-
const aliases = sanitizeModelAliases(record.aliases);
|
|
323
|
-
if (Object.keys(aliases).length === 0)
|
|
324
|
-
continue;
|
|
325
|
-
gateways[key] = {
|
|
326
|
-
aliases,
|
|
327
|
-
updatedAt: typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt) ? record.updatedAt : 0,
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
return { $schema_version: MODEL_ALIAS_STORE_VERSION, gateways };
|
|
331
|
-
}
|
|
332
|
-
catch (error) {
|
|
333
|
-
logger.warn(`[AgentRunner] Failed to load persisted model aliases: ${error instanceof Error ? error.message : String(error)}`);
|
|
334
|
-
return { $schema_version: MODEL_ALIAS_STORE_VERSION, gateways: {} };
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
function loadPersistedModelAliases(baseUrl) {
|
|
338
|
-
const cacheKey = normalizeModelGatewayUrl(baseUrl);
|
|
339
|
-
const storePath = modelAliasStorePath();
|
|
340
|
-
const gatewayKey = modelGatewayKey(cacheKey);
|
|
341
|
-
const loadKey = `${storePath}\0${gatewayKey}`;
|
|
342
|
-
if (loadedModelAliasFallbacks.has(loadKey))
|
|
343
|
-
return;
|
|
344
|
-
loadedModelAliasFallbacks.add(loadKey);
|
|
345
|
-
const persisted = readPersistedAliasStore().gateways[gatewayKey];
|
|
346
|
-
if (!persisted)
|
|
347
|
-
return;
|
|
348
|
-
modelAliasFallbacks.set(cacheKey, persisted.aliases);
|
|
349
|
-
logger.info(`[AgentRunner] Loaded persisted model aliases: ${JSON.stringify(persisted.aliases)}`);
|
|
350
|
-
}
|
|
351
|
-
function persistModelAliases(baseUrl, aliases, updatedAt) {
|
|
352
|
-
try {
|
|
353
|
-
const store = readPersistedAliasStore();
|
|
354
|
-
store.gateways[modelGatewayKey(baseUrl)] = { aliases, updatedAt };
|
|
355
|
-
atomicWriteJson(modelAliasStorePath(), store);
|
|
356
|
-
}
|
|
357
|
-
catch (error) {
|
|
358
|
-
logger.warn(`[AgentRunner] Failed to persist model aliases: ${error instanceof Error ? error.message : String(error)}`);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
265
|
// ── 网关价格缓存(从 /v1/models 的 pricing/effective_pricing 提取)─────────────
|
|
362
266
|
// 与别名刷新同范式:按 baseUrl 缓存,1h TTL,stale-while-revalidate(缺失/过期时
|
|
363
267
|
// fire-and-forget 触发刷新,本轮先用旧值或回退,不阻塞查询)。
|
|
@@ -432,94 +336,6 @@ async function refreshGatewayPricing(baseUrl, apiKey, requestHeaders) {
|
|
|
432
336
|
gatewayPricingInFlight.delete(baseUrl);
|
|
433
337
|
}
|
|
434
338
|
}
|
|
435
|
-
/** 从模型 ID 列表中提取各 claude 系列的最新版本(按 major.minor 取最高,minor 可省略) */
|
|
436
|
-
function deriveAliasesFromModelIds(ids) {
|
|
437
|
-
// 注入已验证可用的模型(如果列表中已有则去重无影响)
|
|
438
|
-
const allIds = [...new Set([...ids, ...INJECTED_MODELS])];
|
|
439
|
-
const best = {};
|
|
440
|
-
for (const id of allIds) {
|
|
441
|
-
const m = id.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?/);
|
|
442
|
-
if (!m)
|
|
443
|
-
continue;
|
|
444
|
-
const [, family, majorStr, minorStr] = m;
|
|
445
|
-
const major = parseInt(majorStr, 10);
|
|
446
|
-
const minor = minorStr ? parseInt(minorStr, 10) : 0;
|
|
447
|
-
const cur = best[family];
|
|
448
|
-
if (!cur || major > cur.major || (major === cur.major && minor > cur.minor)) {
|
|
449
|
-
best[family] = { id, major, minor };
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
const aliases = {};
|
|
453
|
-
for (const family of MODEL_FAMILIES) {
|
|
454
|
-
const info = best[family];
|
|
455
|
-
if (info)
|
|
456
|
-
aliases[family] = info.id;
|
|
457
|
-
}
|
|
458
|
-
return aliases;
|
|
459
|
-
}
|
|
460
|
-
/** 异步刷新某 baseUrl 的别名缓存(失败静默,不抛出) */
|
|
461
|
-
async function refreshModelAliases(baseUrl, apiKey, requestHeaders) {
|
|
462
|
-
const cacheKey = normalizeModelGatewayUrl(baseUrl);
|
|
463
|
-
if (modelAliasInFlight.has(cacheKey))
|
|
464
|
-
return;
|
|
465
|
-
modelAliasInFlight.add(cacheKey);
|
|
466
|
-
loadPersistedModelAliases(cacheKey);
|
|
467
|
-
try {
|
|
468
|
-
const url = `${cacheKey}/models`;
|
|
469
|
-
const controller = new AbortController();
|
|
470
|
-
const timer = setTimeout(() => controller.abort(), 5000);
|
|
471
|
-
const resp = await fetch(url, {
|
|
472
|
-
signal: controller.signal,
|
|
473
|
-
headers: {
|
|
474
|
-
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
475
|
-
...requestHeaders,
|
|
476
|
-
},
|
|
477
|
-
});
|
|
478
|
-
clearTimeout(timer);
|
|
479
|
-
if (!resp.ok)
|
|
480
|
-
return;
|
|
481
|
-
const json = await resp.json();
|
|
482
|
-
const ids = Array.isArray(json?.data)
|
|
483
|
-
? json.data.map((m) => m?.id).filter((x) => typeof x === 'string')
|
|
484
|
-
: [];
|
|
485
|
-
const aliases = deriveAliasesFromModelIds(ids);
|
|
486
|
-
if (ids.length > 0 || Object.keys(aliases).length > 0) {
|
|
487
|
-
const updatedAt = Date.now();
|
|
488
|
-
modelAliasCache.set(cacheKey, { aliases, ids, fetchedAt: updatedAt });
|
|
489
|
-
if (Object.keys(aliases).length > 0) {
|
|
490
|
-
const previousAliases = modelAliasFallbacks.get(cacheKey) || {};
|
|
491
|
-
const latestAliases = { ...previousAliases, ...aliases };
|
|
492
|
-
modelAliasFallbacks.set(cacheKey, latestAliases);
|
|
493
|
-
persistModelAliases(cacheKey, latestAliases, updatedAt);
|
|
494
|
-
}
|
|
495
|
-
logger.info(`[AgentRunner] Refreshed models from ${url}: ${ids.length} ids, aliases ${JSON.stringify(aliases)}`);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
catch {
|
|
499
|
-
// 网络/解析失败:保留该网关最近一次成功结果,不打断查询
|
|
500
|
-
}
|
|
501
|
-
finally {
|
|
502
|
-
modelAliasInFlight.delete(cacheKey);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
/** 将短别名展开为完整 model ID,已是完整 ID 则原样返回 */
|
|
506
|
-
function resolveModelAlias(model, baseUrl) {
|
|
507
|
-
// 非短别名(已经是完整 ID)直接返回
|
|
508
|
-
if (!MODEL_FAMILIES.includes(model))
|
|
509
|
-
return model;
|
|
510
|
-
const family = model;
|
|
511
|
-
// 优先使用动态缓存
|
|
512
|
-
if (baseUrl) {
|
|
513
|
-
const cacheKey = normalizeModelGatewayUrl(baseUrl);
|
|
514
|
-
loadPersistedModelAliases(cacheKey);
|
|
515
|
-
const cached = modelAliasCache.get(cacheKey);
|
|
516
|
-
if (cached && (Date.now() - cached.fetchedAt < MODEL_ALIAS_TTL_MS)) {
|
|
517
|
-
return cached.aliases[family] || modelAliasFallbacks.get(cacheKey)?.[family] || BOOTSTRAP_MODEL_ALIASES[family];
|
|
518
|
-
}
|
|
519
|
-
return modelAliasFallbacks.get(cacheKey)?.[family] || BOOTSTRAP_MODEL_ALIASES[family];
|
|
520
|
-
}
|
|
521
|
-
return BOOTSTRAP_MODEL_ALIASES[family];
|
|
522
|
-
}
|
|
523
339
|
/**
|
|
524
340
|
* 为支持 1M 上下文的模型追加 `[1m]` 后缀——仅在交给 SDK query() 时调用。
|
|
525
341
|
* 目录与校验层始终使用不带后缀的基础 ID,避免与网关 /models 返回值(无 `[1m]`)冲突。
|
|
@@ -531,10 +347,6 @@ function applyContextWindow(modelId) {
|
|
|
531
347
|
return `${modelId}[1m]`;
|
|
532
348
|
return modelId;
|
|
533
349
|
}
|
|
534
|
-
/** 解析别名 + 追加 1M 后缀,得到最终交给 SDK 的 model 串。 */
|
|
535
|
-
function resolveSdkModel(model, baseUrl) {
|
|
536
|
-
return applyContextWindow(resolveModelAlias(model, baseUrl));
|
|
537
|
-
}
|
|
538
350
|
class MessageStream {
|
|
539
351
|
queue = [];
|
|
540
352
|
waiting = null;
|
|
@@ -719,6 +531,20 @@ export class AgentRunner {
|
|
|
719
531
|
...(evolcoreSessionId ? { EVOLCORE_SESSION_ID: evolcoreSessionId } : {}),
|
|
720
532
|
...(runtimeEnv ?? {}),
|
|
721
533
|
}, { lockExecutableResolution: permissionMode === 'readonly' });
|
|
534
|
+
const trustedRootFullAccess = permissionMode === 'fullaccess'
|
|
535
|
+
&& typeof process.getuid === 'function'
|
|
536
|
+
&& process.getuid() === 0
|
|
537
|
+
&& typeof evolcoreSessionId === 'string'
|
|
538
|
+
&& hasTrustedFullAccessContext(this.permissionContexts.get(evolcoreSessionId));
|
|
539
|
+
// Claude Code refuses --dangerously-skip-permissions as root unless its
|
|
540
|
+
// caller asserts that the execution is intentionally sandbox-managed.
|
|
541
|
+
// EvolCore fullaccess is an explicit daemon-owner-authorized host run, so
|
|
542
|
+
// scope this assertion to that single trusted call. Never inherit a
|
|
543
|
+
// process-wide IS_SANDBOX into ordinary or untrusted Claude sessions.
|
|
544
|
+
if (trustedRootFullAccess)
|
|
545
|
+
env.IS_SANDBOX = '1';
|
|
546
|
+
else
|
|
547
|
+
delete env.IS_SANDBOX;
|
|
722
548
|
const bubblewrapPath = resolveBubblewrapPath();
|
|
723
549
|
return bubblewrapPath
|
|
724
550
|
? prependExecutableDirectory(env, bubblewrapPath)
|
|
@@ -766,31 +592,22 @@ export class AgentRunner {
|
|
|
766
592
|
},
|
|
767
593
|
};
|
|
768
594
|
}
|
|
595
|
+
catalogConnection() {
|
|
596
|
+
return { baseUrl: this.baseUrl, apiKey: this.apiKey, headers: this.customHeaders, queryParams: this.customQueryParams };
|
|
597
|
+
}
|
|
598
|
+
getModelCatalog() {
|
|
599
|
+
return getRunnerCatalog('claude', this.agentAid, this.catalogConnection());
|
|
600
|
+
}
|
|
769
601
|
async listModels() {
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
let cached = modelAliasCache.get(cacheKey);
|
|
774
|
-
const stale = !cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS);
|
|
775
|
-
// 缓存为空(首次打开)→ 等待刷新;缓存仅过期 → 后台刷新不阻塞
|
|
776
|
-
if (!cached) {
|
|
777
|
-
await refreshModelAliases(this.baseUrl, this.apiKey, this.customHeaders);
|
|
778
|
-
cached = modelAliasCache.get(cacheKey);
|
|
779
|
-
}
|
|
780
|
-
else if (stale) {
|
|
781
|
-
refreshModelAliases(this.baseUrl, this.apiKey, this.customHeaders);
|
|
782
|
-
}
|
|
783
|
-
// 有缓存时返回网关 /models 的全量原始 ID
|
|
784
|
-
if (cached && cached.ids.length > 0)
|
|
785
|
-
return cached.ids;
|
|
786
|
-
}
|
|
787
|
-
// 无 baseUrl / 刷新超时或失败 → 回退短别名
|
|
788
|
-
const fallback = this.baseUrl ? modelAliasFallbacks.get(normalizeModelGatewayUrl(this.baseUrl)) : undefined;
|
|
789
|
-
return Object.values({ ...BOOTSTRAP_MODEL_ALIASES, ...fallback });
|
|
602
|
+
const catalog = await this.getModelCatalog();
|
|
603
|
+
assertCatalogSelectable(catalog);
|
|
604
|
+
return catalog.models.map(m => m.id);
|
|
790
605
|
}
|
|
791
|
-
/** 将短别名解析为当前代理实际使用的完整 model ID(仅用于展示,不改变持久化值) */
|
|
792
606
|
resolveModelId(model) {
|
|
793
|
-
return
|
|
607
|
+
return resolveCatalogModel(peekRunnerCatalog('claude', this.agentAid, this.catalogConnection()), model);
|
|
608
|
+
}
|
|
609
|
+
async getAvailableEfforts(model) {
|
|
610
|
+
return catalogEfforts(await this.getModelCatalog(), model, 'claude');
|
|
794
611
|
}
|
|
795
612
|
setEffort(effort) {
|
|
796
613
|
this.effort = effort;
|
|
@@ -1953,13 +1770,9 @@ export class AgentRunner {
|
|
|
1953
1770
|
// 记录当前 evolcore session ID,用于 Agent ctl 环境变量注入
|
|
1954
1771
|
// 同步用户级配置到内存
|
|
1955
1772
|
this.syncFromUserSettings();
|
|
1956
|
-
//
|
|
1773
|
+
// Run preparation and model menus resolve aliases from the same snapshot.
|
|
1774
|
+
const runCatalog = await this.getModelCatalog();
|
|
1957
1775
|
if (this.baseUrl) {
|
|
1958
|
-
const cacheKey = normalizeModelGatewayUrl(this.baseUrl);
|
|
1959
|
-
const cached = modelAliasCache.get(cacheKey);
|
|
1960
|
-
if (!cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS)) {
|
|
1961
|
-
refreshModelAliases(this.baseUrl, this.apiKey, this.customHeaders);
|
|
1962
|
-
}
|
|
1963
1776
|
// 顺带预热网关价格缓存(1h TTL),让本轮结束写库时已就绪
|
|
1964
1777
|
const pricing = gatewayPricingCache.get(this.baseUrl);
|
|
1965
1778
|
if (!pricing || (Date.now() - pricing.fetchedAt > GATEWAY_PRICING_TTL_MS)) {
|
|
@@ -2525,7 +2338,7 @@ export class AgentRunner {
|
|
|
2525
2338
|
else {
|
|
2526
2339
|
logger.info(`[AgentRunner] systemPromptAppend: none`);
|
|
2527
2340
|
}
|
|
2528
|
-
const sdkModel =
|
|
2341
|
+
const sdkModel = applyContextWindow(resolveCatalogModel(runCatalog, callModel));
|
|
2529
2342
|
const capabilityOptions = await this.resolveCapabilityRunOptions(projectPath);
|
|
2530
2343
|
const managedSettings = callPermissionMode === 'fullaccess'
|
|
2531
2344
|
? undefined
|
|
@@ -2932,16 +2745,19 @@ export class AgentRunner {
|
|
|
2932
2745
|
}
|
|
2933
2746
|
});
|
|
2934
2747
|
}
|
|
2935
|
-
runSessionCommand(prompt, agentSessionId, projectPath, modelOverride) {
|
|
2936
|
-
const callModel = modelOverride?.model || this.model;
|
|
2748
|
+
async runSessionCommand(prompt, agentSessionId, projectPath, modelOverride, useModel = true) {
|
|
2749
|
+
const callModel = modelOverride?.model || (useModel ? this.model : undefined);
|
|
2937
2750
|
const callEffort = (modelOverride?.effortMode === 'model_default'
|
|
2938
2751
|
? undefined
|
|
2939
|
-
: modelOverride?.effort ?? this.effort);
|
|
2752
|
+
: modelOverride?.effort ?? (useModel ? this.effort : undefined));
|
|
2753
|
+
const catalog = useModel ? await this.getModelCatalog() : undefined;
|
|
2754
|
+
if (catalog)
|
|
2755
|
+
assertCatalogSelectable(catalog);
|
|
2940
2756
|
return query({
|
|
2941
2757
|
prompt,
|
|
2942
2758
|
options: {
|
|
2943
2759
|
cwd: projectPath,
|
|
2944
|
-
model:
|
|
2760
|
+
...(catalog && callModel ? { model: applyContextWindow(resolveCatalogModel(catalog, callModel)) } : {}),
|
|
2945
2761
|
resume: agentSessionId,
|
|
2946
2762
|
maxTurns: 1,
|
|
2947
2763
|
tools: [],
|
|
@@ -2950,7 +2766,7 @@ export class AgentRunner {
|
|
|
2950
2766
|
strictMcpConfig: true,
|
|
2951
2767
|
settingSources: [],
|
|
2952
2768
|
managedSettings: buildClaudeManagedLockdownSettings(),
|
|
2953
|
-
...(callEffort ? { effort: callEffort } : {}),
|
|
2769
|
+
...(catalog && callEffort ? { effort: callEffort } : {}),
|
|
2954
2770
|
permissionMode: this.toSdkPermissionMode(),
|
|
2955
2771
|
env: this.getAgentEnv()
|
|
2956
2772
|
}
|
|
@@ -2965,7 +2781,7 @@ export class AgentRunner {
|
|
|
2965
2781
|
let lastEvent;
|
|
2966
2782
|
try {
|
|
2967
2783
|
logger.info(`[AgentRunner] Compacting session: session=${sessionId} agent=${agentSessionId} project=${projectPath}`);
|
|
2968
|
-
const stream = this.runSessionCommand('/compact', agentSessionId, projectPath, modelOverride);
|
|
2784
|
+
const stream = await this.runSessionCommand('/compact', agentSessionId, projectPath, modelOverride);
|
|
2969
2785
|
this.activeStreams.set(sessionId, stream);
|
|
2970
2786
|
try {
|
|
2971
2787
|
let boundary;
|
|
@@ -3035,7 +2851,9 @@ export class AgentRunner {
|
|
|
3035
2851
|
async clearSession(sessionId, agentSessionId, projectPath) {
|
|
3036
2852
|
try {
|
|
3037
2853
|
logger.info(`[AgentRunner] Clearing session via SDK: ${agentSessionId}`);
|
|
3038
|
-
|
|
2854
|
+
// Clearing is a backend lifecycle operation. It does not depend on any
|
|
2855
|
+
// Session model or model configuration/selection.
|
|
2856
|
+
const stream = await this.runSessionCommand('/clear', agentSessionId, projectPath, undefined, false);
|
|
3039
2857
|
this.activeStreams.set(sessionId, stream);
|
|
3040
2858
|
try {
|
|
3041
2859
|
let cleared = false;
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import readline from 'readline';
|
|
3
|
-
import { logger } from '
|
|
4
|
-
import { buildHClassGuardCommand, collectSandboxGuardDiagnostics, summarizeBubblewrapArgsForLog, summarizeSandboxGuardDiagnostics, } from '
|
|
5
|
-
import { resolveCodexLaunchCommand } from '
|
|
3
|
+
import { logger } from '../../utils/logger.js';
|
|
4
|
+
import { buildHClassGuardCommand, collectSandboxGuardDiagnostics, summarizeBubblewrapArgsForLog, summarizeSandboxGuardDiagnostics, } from '../../core/permission/sandbox-runtime.js';
|
|
5
|
+
import { resolveCodexLaunchCommand } from '../../utils/codex-cli.js';
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
|
-
import { sanitizeShellExecutionEnvironment } from '
|
|
7
|
+
import { sanitizeShellExecutionEnvironment } from '../../core/permission/shell-environment.js';
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import { ensureProcessManagedTempDir } from '
|
|
11
|
-
import { getPackageRoot, resolvePaths } from '
|
|
12
|
-
import {
|
|
10
|
+
import { ensureProcessManagedTempDir } from '../../cli/task-context.js';
|
|
11
|
+
import { getPackageRoot, resolvePaths } from '../../paths.js';
|
|
12
|
+
import { killProcess } from '../../utils/cross-platform.js';
|
|
13
|
+
import { registerCodexAppServerProcess, unregisterCodexAppServerProcess, } from './app-server-registry.js';
|
|
13
14
|
const BUILT_IN_CODEX_PROVIDER_IDS = new Set(['openai']);
|
|
15
|
+
export const CODEX_THREAD_ACTIVE_WRITER = 'THREAD_ACTIVE_WRITER';
|
|
14
16
|
function tomlProviderKey(value) {
|
|
15
17
|
if (!value || /[\0\r\n]/.test(value)) {
|
|
16
18
|
throw new Error(`Invalid Codex provider name: ${value}`);
|
|
@@ -114,6 +116,61 @@ export class CodexAppServerClient {
|
|
|
114
116
|
...(options?.selectedCapabilityRoots ? { selectedCapabilityRoots: options.selectedCapabilityRoots } : {}),
|
|
115
117
|
});
|
|
116
118
|
}
|
|
119
|
+
async threadArchive(threadId) {
|
|
120
|
+
await this.request('thread/archive', { threadId });
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
async threadUnarchive(threadId) {
|
|
124
|
+
return this.request('thread/unarchive', { threadId });
|
|
125
|
+
}
|
|
126
|
+
async threadUnsubscribe(threadId) {
|
|
127
|
+
return this.request('thread/unsubscribe', { threadId });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Drop this connection's subscription and wait until the server has released
|
|
131
|
+
* the in-memory thread. This is deliberately scoped to one thread; closing
|
|
132
|
+
* the app-server process would also disrupt unrelated concurrent sessions.
|
|
133
|
+
*/
|
|
134
|
+
async threadUnsubscribeAndWait(threadId, timeoutMs = 2_000) {
|
|
135
|
+
let closed = false;
|
|
136
|
+
let resolveClosed;
|
|
137
|
+
const closedPromise = new Promise(resolve => {
|
|
138
|
+
resolveClosed = resolve;
|
|
139
|
+
});
|
|
140
|
+
const removeNotificationHandler = this.onNotification(notification => {
|
|
141
|
+
if (notification.method !== 'thread/closed')
|
|
142
|
+
return;
|
|
143
|
+
const params = (notification.params || {});
|
|
144
|
+
const closedThreadId = params.threadId ?? params.thread_id;
|
|
145
|
+
if (closedThreadId !== threadId || closed)
|
|
146
|
+
return;
|
|
147
|
+
closed = true;
|
|
148
|
+
resolveClosed();
|
|
149
|
+
});
|
|
150
|
+
let timer;
|
|
151
|
+
try {
|
|
152
|
+
const response = await this.threadUnsubscribe(threadId);
|
|
153
|
+
if (response.status !== 'unsubscribed')
|
|
154
|
+
return response;
|
|
155
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
156
|
+
timer = setTimeout(() => {
|
|
157
|
+
if (closed)
|
|
158
|
+
return;
|
|
159
|
+
const error = new Error(`thread/closed timed out for thread ${threadId}`);
|
|
160
|
+
error.code = 'CODEX_THREAD_CLOSE_TIMEOUT';
|
|
161
|
+
reject(error);
|
|
162
|
+
}, timeoutMs);
|
|
163
|
+
timer.unref?.();
|
|
164
|
+
});
|
|
165
|
+
await Promise.race([closedPromise, timeoutPromise]);
|
|
166
|
+
return response;
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
if (timer)
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
removeNotificationHandler();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
117
174
|
async turnStart(threadId, input, options) {
|
|
118
175
|
const sandboxPolicy = this.toTurnSandboxPolicy(options?.sandbox);
|
|
119
176
|
return this.request('turn/start', {
|
|
@@ -126,6 +183,14 @@ export class CodexAppServerClient {
|
|
|
126
183
|
...(options?.effort ? { effort: options.effort } : {}),
|
|
127
184
|
});
|
|
128
185
|
}
|
|
186
|
+
async turnSteer(threadId, expectedTurnId, input, additionalContext) {
|
|
187
|
+
return this.request('turn/steer', {
|
|
188
|
+
threadId,
|
|
189
|
+
expectedTurnId,
|
|
190
|
+
input,
|
|
191
|
+
...(additionalContext ? { additionalContext } : {}),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
129
194
|
toTurnSandboxPolicy(sandbox) {
|
|
130
195
|
if (!sandbox)
|
|
131
196
|
return undefined;
|
|
@@ -286,8 +351,8 @@ export class CodexAppServerClient {
|
|
|
286
351
|
await this.request('turn/interrupt', { threadId, turnId });
|
|
287
352
|
return true;
|
|
288
353
|
}
|
|
289
|
-
async modelList(includeHidden = false) {
|
|
290
|
-
return this.request('model/list', { includeHidden });
|
|
354
|
+
async modelList(includeHidden = false, cursor) {
|
|
355
|
+
return this.request('model/list', { includeHidden, ...(cursor ? { cursor } : {}) });
|
|
291
356
|
}
|
|
292
357
|
async modelProviderCapabilitiesRead() {
|
|
293
358
|
return this.request('modelProvider/capabilities/read', {});
|
|
@@ -332,7 +397,10 @@ export class CodexAppServerClient {
|
|
|
332
397
|
}
|
|
333
398
|
// 优雅关闭:先关闭 stdin,然后发送 SIGTERM,等待进程退出
|
|
334
399
|
proc.stdin.end();
|
|
335
|
-
|
|
400
|
+
if (process.platform === 'win32' && proc.pid)
|
|
401
|
+
killProcess(proc.pid, false);
|
|
402
|
+
else
|
|
403
|
+
proc.kill('SIGTERM');
|
|
336
404
|
// 等待进程退出,最多等待 5 秒
|
|
337
405
|
await new Promise((resolve) => {
|
|
338
406
|
let exited = false;
|
|
@@ -341,7 +409,10 @@ export class CodexAppServerClient {
|
|
|
341
409
|
return;
|
|
342
410
|
logger.warn('[CodexAppServer] Process did not exit after SIGTERM, sending SIGKILL');
|
|
343
411
|
try {
|
|
344
|
-
|
|
412
|
+
if (process.platform === 'win32' && proc.pid)
|
|
413
|
+
killProcess(proc.pid, true);
|
|
414
|
+
else
|
|
415
|
+
proc.kill('SIGKILL');
|
|
345
416
|
}
|
|
346
417
|
catch { }
|
|
347
418
|
resolve();
|
|
@@ -416,6 +487,7 @@ export class CodexAppServerClient {
|
|
|
416
487
|
const managedRequirements = this.ensureManagedHookRuntime();
|
|
417
488
|
const guardedCommand = buildHClassGuardCommand(command, commandArgs, guardRoot, {
|
|
418
489
|
managedCodexRequirements: managedRequirements,
|
|
490
|
+
protectEvolcoreFilesystem: this.options.protectEvolcoreFilesystem !== false,
|
|
419
491
|
});
|
|
420
492
|
if (!guardedCommand) {
|
|
421
493
|
throw new Error('Codex 缺少 EvolCore managed-hook 隔离运行时,已拒绝启动 app-server');
|
|
@@ -561,8 +633,8 @@ export class CodexAppServerClient {
|
|
|
561
633
|
}
|
|
562
634
|
const stat = fs.lstatSync(runtimeLockDir);
|
|
563
635
|
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
564
|
-
|| (typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
565
|
-
|| (stat.mode & 0o077) !== 0) {
|
|
636
|
+
|| (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
637
|
+
|| (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
|
|
566
638
|
throw new Error('runtime lock directory is not private');
|
|
567
639
|
}
|
|
568
640
|
env.EVOLCORE_RUNTIME_LOCK_DIR = runtimeLockDir;
|
|
@@ -613,7 +685,10 @@ export class CodexAppServerClient {
|
|
|
613
685
|
// second command or changing the hook's behavior.
|
|
614
686
|
if (!command || !hook || /[\r\n;&|<>]/.test(command))
|
|
615
687
|
return false;
|
|
616
|
-
|
|
688
|
+
const commandWithoutSocket = command
|
|
689
|
+
.replace(/\s+%EVOLCORE_INSTANCE_SOCKET%\s*$/i, '')
|
|
690
|
+
.trim();
|
|
691
|
+
return commandWithoutSocket.replace(/["']+$/, '').endsWith(hook);
|
|
617
692
|
};
|
|
618
693
|
const managedHookLoaded = Array.isArray(preToolUse)
|
|
619
694
|
&& preToolUse.some(group => {
|
|
@@ -659,7 +734,7 @@ export class CodexAppServerClient {
|
|
|
659
734
|
const headerEnvNames = Object.fromEntries(Object.keys(this.options.headers ?? {}).map(name => [name, codexHeaderEnvName(name)]));
|
|
660
735
|
const hasHeaders = Object.keys(headerEnvNames).length > 0;
|
|
661
736
|
const hasQueryParams = Object.keys(this.options.queryParams ?? {}).length > 0;
|
|
662
|
-
if (hasHeaders || hasQueryParams) {
|
|
737
|
+
if (hasHeaders || hasQueryParams || this.options.baseUrl || this.options.provider) {
|
|
663
738
|
const configuredProvider = this.options.provider?.trim();
|
|
664
739
|
const useRuntimeProvider = !configuredProvider || BUILT_IN_CODEX_PROVIDER_IDS.has(configuredProvider.toLowerCase());
|
|
665
740
|
const providerName = !useRuntimeProvider
|
|
@@ -765,8 +840,19 @@ export class CodexAppServerClient {
|
|
|
765
840
|
this.pending.delete(messageId);
|
|
766
841
|
if (message.error) {
|
|
767
842
|
const detail = typeof message.error?.message === 'string' ? message.error.message : JSON.stringify(message.error);
|
|
768
|
-
|
|
769
|
-
|
|
843
|
+
// The CLI's one-time bubblewrap configuration warning is process-wide
|
|
844
|
+
// startup noise, not evidence about this RPC. Do not append it to an
|
|
845
|
+
// unrelated request failure such as thread/resume.
|
|
846
|
+
const stderrLines = this.stderrBuffer.filter(line => !/could not find bubblewrap on PATH/i.test(line));
|
|
847
|
+
const stderr = stderrLines.length ? `\n${stderrLines.join('\n')}` : '';
|
|
848
|
+
const error = new Error(`${pending.method} failed: ${detail}${stderr}`);
|
|
849
|
+
if (/no rollout found for thread id/i.test(detail)) {
|
|
850
|
+
error.code = 'CODEX_ROLLOUT_NOT_FOUND';
|
|
851
|
+
}
|
|
852
|
+
else if (/already has an active writer/i.test(detail)) {
|
|
853
|
+
error.code = CODEX_THREAD_ACTIVE_WRITER;
|
|
854
|
+
}
|
|
855
|
+
pending.reject(error);
|
|
770
856
|
return;
|
|
771
857
|
}
|
|
772
858
|
pending.resolve(message.result ?? null);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { resolvePaths } from '
|
|
4
|
-
import { getProcessStartTime } from '
|
|
3
|
+
import { resolvePaths } from '../../paths.js';
|
|
4
|
+
import { getProcessStartTime } from '../../utils/process-introspect.js';
|
|
5
5
|
const CODEX_APP_SERVER_RECORD_PREFIX = 'codex-app-server-';
|
|
6
6
|
const CODEX_APP_SERVER_RECORD_SUFFIX = '.json';
|
|
7
7
|
function recordFilePath(pid) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { CodexAppServerClient } from './app-server-client.js';
|
|
3
|
+
import { resolveCodexLaunchCommand } from '../../utils/codex-cli.js';
|
|
4
|
+
import { parseModelList } from '../../core/model/catalog-service.js';
|
|
5
|
+
async function cliModels() {
|
|
6
|
+
const launch = resolveCodexLaunchCommand(['debug', 'models']);
|
|
7
|
+
if (!launch)
|
|
8
|
+
return null;
|
|
9
|
+
return new Promise(resolve => {
|
|
10
|
+
execFile(launch.command, launch.args, { timeout: 3000, windowsHide: true, maxBuffer: 16 * 1024 * 1024, encoding: 'utf8' }, (error, stdout) => {
|
|
11
|
+
try {
|
|
12
|
+
if (error)
|
|
13
|
+
return resolve(null);
|
|
14
|
+
const json = JSON.parse(stdout);
|
|
15
|
+
if (!Array.isArray(json?.models))
|
|
16
|
+
return resolve(null);
|
|
17
|
+
resolve(parseModelList(json.models.filter((m) => m.visibility === 'list'))
|
|
18
|
+
.map(m => ({ ...m, verification: 'unverified' })));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
resolve(null);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** A catalog-only client uses the same transport configuration as inference. */
|
|
27
|
+
export async function codexNativeCatalog(context) {
|
|
28
|
+
if (context.baseagent !== 'codex')
|
|
29
|
+
return null;
|
|
30
|
+
const client = new CodexAppServerClient({
|
|
31
|
+
apiKey: context.apiKey ?? '', baseUrl: context.baseUrl, provider: context.provider,
|
|
32
|
+
headers: context.headers, queryParams: context.queryParams,
|
|
33
|
+
});
|
|
34
|
+
let timer;
|
|
35
|
+
let expired = false;
|
|
36
|
+
try {
|
|
37
|
+
return await Promise.race([
|
|
38
|
+
(async () => {
|
|
39
|
+
const models = [];
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
let cursor;
|
|
42
|
+
do {
|
|
43
|
+
const page = await client.modelList(false, cursor);
|
|
44
|
+
if (expired)
|
|
45
|
+
throw new Error('Model catalog timeout');
|
|
46
|
+
if (!Array.isArray(page.data))
|
|
47
|
+
throw new Error('Invalid native model list');
|
|
48
|
+
models.push(...parseModelList(page.data.filter(m => !m.hidden)));
|
|
49
|
+
cursor = page.nextCursor || undefined;
|
|
50
|
+
if (cursor && (seen.has(cursor) || seen.size >= 100))
|
|
51
|
+
throw new Error('Invalid native model pagination');
|
|
52
|
+
if (cursor)
|
|
53
|
+
seen.add(cursor);
|
|
54
|
+
} while (cursor);
|
|
55
|
+
return [...new Map(models.map(m => [m.id, m])).values()];
|
|
56
|
+
})(),
|
|
57
|
+
new Promise((_, reject) => { timer = setTimeout(() => { expired = true; reject(new Error('Model catalog timeout')); }, 5000); }),
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// The host CLI catalog has no guarantee of matching this Agent connection.
|
|
62
|
+
// It is useful metadata/suggestions and always remains unverified.
|
|
63
|
+
return await cliModels();
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
if (timer)
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
await client.close();
|
|
69
|
+
}
|
|
70
|
+
}
|