evolcore 0.0.22 → 0.0.24

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 (117) hide show
  1. package/CHANGELOG.md +74 -14
  2. package/README.md +4 -0
  3. package/bin/codex-managed-acl.ps1 +160 -0
  4. package/bin/codex-managed-hook.mjs +127 -10
  5. package/bin/ec-safe-output.js +85 -3
  6. package/bin/install-codex-managed-hooks.mjs +384 -95
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +30 -2
  9. package/dist/agents/claude-runner.js +179 -240
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +369 -0
  14. package/dist/agents/codex-runner.js +839 -192
  15. package/dist/agents/ecagent-runner.js +27 -34
  16. package/dist/agents/gemini-runner.js +62 -8
  17. package/dist/agents/runner-types.js +1 -1
  18. package/dist/aun/aid/managed-operation.js +28 -6
  19. package/dist/aun/aid/store.js +43 -0
  20. package/dist/aun/menu-client.js +272 -0
  21. package/dist/aun/msg/group.js +65 -4
  22. package/dist/aun/msg/index.js +1 -1
  23. package/dist/aun/msg/managed-operation.js +57 -5
  24. package/dist/aun/outbox.js +56 -18
  25. package/dist/aun/rpc/client.js +7 -1
  26. package/dist/channels/aun.js +178 -77
  27. package/dist/channels/daemon.js +7 -1
  28. package/dist/channels/dingtalk.js +1 -0
  29. package/dist/channels/feishu.js +10 -4
  30. package/dist/channels/wechat.js +81 -10
  31. package/dist/channels/wecom.js +2 -1
  32. package/dist/cli/agent-command.js +29 -3
  33. package/dist/cli/agent-create.js +538 -0
  34. package/dist/cli/agent.js +26 -675
  35. package/dist/cli/aun-commands.js +108 -24
  36. package/dist/cli/command-log.js +21 -2
  37. package/dist/cli/ctl-command.js +2 -0
  38. package/dist/cli/daemon-commands.js +289 -61
  39. package/dist/cli/index.js +22 -2
  40. package/dist/cli/init.js +2 -10
  41. package/dist/cli/managed-command-guard.js +3 -0
  42. package/dist/cli/menu-client.js +95 -0
  43. package/dist/cli/model.js +23 -8
  44. package/dist/cli/restart-monitor.js +432 -8
  45. package/dist/cli/stats.js +110 -0
  46. package/dist/cli/task-context.js +12 -2
  47. package/dist/cli/trigger-command.js +9 -1
  48. package/dist/config/builtin-roles.js +2 -0
  49. package/dist/config/schema-migration.js +14 -2
  50. package/dist/core/auth/agent-delegation.js +10 -7
  51. package/dist/core/auth/auth-gateway.js +3 -2
  52. package/dist/core/auth/authorization-audit.js +75 -18
  53. package/dist/core/auth/operation-authorizer.js +63 -3
  54. package/dist/core/auth/operation-catalog.js +14 -2
  55. package/dist/core/auth/remote-access-store.js +136 -0
  56. package/dist/core/baseagent-loader.js +4 -4
  57. package/dist/core/bootstrap-messages.js +39 -18
  58. package/dist/core/bootstrap-service.js +32 -26
  59. package/dist/core/capability/capability-manager.js +8 -4
  60. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  61. package/dist/core/command/agent-control.js +7 -2
  62. package/dist/core/command/command-handler.js +128 -30
  63. package/dist/core/command/daemon-diagnostics.js +448 -0
  64. package/dist/core/command/menu-catalog.js +6 -1
  65. package/dist/core/command/menu-handler.js +410 -98
  66. package/dist/core/command/menu-protocol.js +123 -0
  67. package/dist/core/command/slash-gate.js +4 -6
  68. package/dist/core/command/slash-handler.js +199 -70
  69. package/dist/core/data-migration.js +5 -15
  70. package/dist/core/event-catalog.js +3 -0
  71. package/dist/core/handoff/runtime.js +3 -1
  72. package/dist/core/handoff/store.js +7 -1
  73. package/dist/core/message/im-renderer.js +13 -6
  74. package/dist/core/message/message-bridge.js +5 -36
  75. package/dist/core/message/message-queue.js +20 -15
  76. package/dist/core/message/message-utils.js +6 -3
  77. package/dist/core/message/response-engine.js +147 -94
  78. package/dist/core/model/catalog-service.js +310 -0
  79. package/dist/core/model/model-catalog.js +134 -334
  80. package/dist/core/model/model-diagnostics.js +4 -5
  81. package/dist/core/model/model-fallback.js +9 -16
  82. package/dist/core/model/model-permission.js +5 -2
  83. package/dist/core/permission/approval-gateway.js +183 -32
  84. package/dist/core/permission/protected-paths.js +63 -4
  85. package/dist/core/permission/sandbox-runtime.js +4 -2
  86. package/dist/core/permission/tool-policy.js +257 -31
  87. package/dist/core/runtime-lock.js +13 -8
  88. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  89. package/dist/core/session/session-manager.js +49 -13
  90. package/dist/core/session/session-renew.js +1 -0
  91. package/dist/index.js +214 -57
  92. package/dist/ipc.js +191 -102
  93. package/dist/paths.js +23 -1
  94. package/dist/stats/billing.js +18 -4
  95. package/dist/trigger/manager.js +3 -1
  96. package/dist/trigger/parser.js +77 -13
  97. package/dist/trigger/patch.js +104 -1
  98. package/dist/utils/atomic-write.js +26 -2
  99. package/dist/utils/cross-platform.js +436 -1
  100. package/dist/utils/ecweb-supervisor.js +11 -2
  101. package/dist/utils/ecweb-utils.js +8 -26
  102. package/dist/utils/error-utils.js +16 -2
  103. package/dist/utils/model-prices.jsonl +23 -20
  104. package/dist/utils/npm-ops.js +70 -15
  105. package/dist/utils/windows-autostart.js +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -0
  107. package/kits/docs/evolcore/agent-create.md +13 -0
  108. package/kits/docs/evolcore/group-collaboration.md +7 -2
  109. package/kits/docs/evolcore/menu-client.md +34 -0
  110. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  111. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  112. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  113. package/kits/templates/roles/admin.json +2 -0
  114. package/kits/templates/roles/visitor.json +1 -0
  115. package/kits/templates/system-fragments/bootstrap.md +8 -1
  116. package/package.json +16 -4
  117. package/dist/utils/codex-cli.js +0 -43
@@ -1,6 +1,8 @@
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 { atomicReadJson, atomicWriteJson, ensureDir } from '../utils/atomic-write.js';
4
+ import { createRequire } from 'module';
5
+ import { ensureDir } from '../utils/atomic-write.js';
4
6
  import { resolveAnthropicConfig } from './baseagent.js';
5
7
  import { buildModelRequestHeaders } from './request-identity.js';
6
8
  import { DEFAULT_PERMISSION_MODE } from '../types.js';
@@ -29,6 +31,42 @@ import { auditToolInfrastructureFailure, auditToolPreflightDenial } from '../cor
29
31
  import { getManagedTaskTempDir, isManagedSessionRuntimeDir } from '../cli/task-context.js';
30
32
  import { hasTrustedFullAccessContext } from './runner-types.js';
31
33
  import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
34
+ const resolveModule = createRequire(import.meta.url);
35
+ /**
36
+ * Resolve the platform package explicitly instead of relying on the SDK's
37
+ * internal optional-dependency lookup. The latter is sensitive to the module
38
+ * installation/loader context and can report a missing binary even when the
39
+ * package is present in the same node_modules tree.
40
+ */
41
+ export function resolveBundledClaudeExecutable() {
42
+ if (process.platform !== 'win32')
43
+ return undefined;
44
+ const nativePackage = process.arch === 'arm64'
45
+ ? '@anthropic-ai/claude-agent-sdk-win32-arm64'
46
+ : process.arch === 'x64'
47
+ ? '@anthropic-ai/claude-agent-sdk-win32-x64'
48
+ : undefined;
49
+ if (!nativePackage)
50
+ return undefined;
51
+ const binarySpecifier = `${nativePackage}/claude.exe`;
52
+ try {
53
+ const executable = resolveModule.resolve(binarySpecifier);
54
+ if (fs.existsSync(executable))
55
+ return executable;
56
+ }
57
+ catch {
58
+ // Fall through to a direct sibling lookup for package layouts where the
59
+ // optional package does not expose its binary through exports.
60
+ }
61
+ try {
62
+ const sdkEntry = resolveModule.resolve('@anthropic-ai/claude-agent-sdk');
63
+ const executable = path.resolve(path.dirname(sdkEntry), '..', '@anthropic-ai', nativePackage.slice('@anthropic-ai/'.length), 'claude.exe');
64
+ return fs.existsSync(executable) ? executable : undefined;
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
32
70
  export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
33
71
  // Built-in tools execute inside the Claude runtime and are covered by the
34
72
  // runner's filesystem/shell policy. MCP and future unknown tools can perform
@@ -108,6 +146,40 @@ function appendQueryParams(baseUrl, queryParams) {
108
146
  return baseUrl;
109
147
  }
110
148
  }
149
+ function claudeSessionFilePath(projectPath, agentSessionId) {
150
+ return path.join(os.homedir(), '.claude', 'projects', encodePath(projectPath), `${agentSessionId}.jsonl`);
151
+ }
152
+ /** A discovered Claude session is resumable only after its transcript header is durable. */
153
+ function isClaudeSessionFileReady(projectPath, agentSessionId) {
154
+ const sessionFile = claudeSessionFilePath(projectPath, agentSessionId);
155
+ if (!fs.existsSync(sessionFile))
156
+ return false;
157
+ try {
158
+ const content = fs.readFileSync(sessionFile, 'utf8');
159
+ for (const line of content.split('\n')) {
160
+ if (!line.trim())
161
+ continue;
162
+ try {
163
+ const data = JSON.parse(line);
164
+ if (data.sessionId && data.version)
165
+ return true;
166
+ }
167
+ catch { }
168
+ }
169
+ }
170
+ catch { }
171
+ return false;
172
+ }
173
+ async function waitForClaudeSessionFile(projectPath, agentSessionId, timeoutMs = 250) {
174
+ const deadline = Date.now() + timeoutMs;
175
+ do {
176
+ if (isClaudeSessionFileReady(projectPath, agentSessionId))
177
+ return true;
178
+ if (Date.now() >= deadline)
179
+ return false;
180
+ await new Promise(resolve => setTimeout(resolve, 10));
181
+ } while (true);
182
+ }
111
183
  function stableClaudePermissionValue(value, depth = 0) {
112
184
  if (depth > 8)
113
185
  return '[truncated]';
@@ -261,103 +333,6 @@ async function assertClaudeSettingSourcesHaveLiteralSandboxPaths(cwd, settingSou
261
333
  }
262
334
  }
263
335
  }
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
336
  // ── 网关价格缓存(从 /v1/models 的 pricing/effective_pricing 提取)─────────────
362
337
  // 与别名刷新同范式:按 baseUrl 缓存,1h TTL,stale-while-revalidate(缺失/过期时
363
338
  // fire-and-forget 触发刷新,本轮先用旧值或回退,不阻塞查询)。
@@ -432,94 +407,6 @@ async function refreshGatewayPricing(baseUrl, apiKey, requestHeaders) {
432
407
  gatewayPricingInFlight.delete(baseUrl);
433
408
  }
434
409
  }
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
410
  /**
524
411
  * 为支持 1M 上下文的模型追加 `[1m]` 后缀——仅在交给 SDK query() 时调用。
525
412
  * 目录与校验层始终使用不带后缀的基础 ID,避免与网关 /models 返回值(无 `[1m]`)冲突。
@@ -531,10 +418,6 @@ function applyContextWindow(modelId) {
531
418
  return `${modelId}[1m]`;
532
419
  return modelId;
533
420
  }
534
- /** 解析别名 + 追加 1M 后缀,得到最终交给 SDK 的 model 串。 */
535
- function resolveSdkModel(model, baseUrl) {
536
- return applyContextWindow(resolveModelAlias(model, baseUrl));
537
- }
538
421
  class MessageStream {
539
422
  queue = [];
540
423
  waiting = null;
@@ -692,9 +575,10 @@ export class AgentRunner {
692
575
  this.agentAid = runtime?.agentAid ?? legacy?.['evolcoreAgentAid'];
693
576
  this.agentConfig = runtime?.agentConfig ?? legacy?.['evolcoreAgentConfig'];
694
577
  this.onSessionIdUpdate = onSessionIdUpdate;
578
+ this.claudeExecutablePath = resolveBundledClaudeExecutable();
695
579
  if (config) {
696
580
  const anthropic = resolveAnthropicConfig(config);
697
- this.claudeExecutablePath = anthropic.pathToClaudeCodeExecutable;
581
+ this.claudeExecutablePath = anthropic.pathToClaudeCodeExecutable || this.claudeExecutablePath;
698
582
  this.customHeaders = buildModelRequestHeaders({
699
583
  baseagent: 'claude',
700
584
  baseUrl: anthropic.baseUrl,
@@ -780,31 +664,22 @@ export class AgentRunner {
780
664
  },
781
665
  };
782
666
  }
667
+ catalogConnection() {
668
+ return { baseUrl: this.baseUrl, apiKey: this.apiKey, headers: this.customHeaders, queryParams: this.customQueryParams };
669
+ }
670
+ getModelCatalog() {
671
+ return getRunnerCatalog('claude', this.agentAid, this.catalogConnection());
672
+ }
783
673
  async listModels() {
784
- if (this.baseUrl) {
785
- const cacheKey = normalizeModelGatewayUrl(this.baseUrl);
786
- loadPersistedModelAliases(cacheKey);
787
- let cached = modelAliasCache.get(cacheKey);
788
- const stale = !cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS);
789
- // 缓存为空(首次打开)→ 等待刷新;缓存仅过期 → 后台刷新不阻塞
790
- if (!cached) {
791
- await refreshModelAliases(this.baseUrl, this.apiKey, this.customHeaders);
792
- cached = modelAliasCache.get(cacheKey);
793
- }
794
- else if (stale) {
795
- refreshModelAliases(this.baseUrl, this.apiKey, this.customHeaders);
796
- }
797
- // 有缓存时返回网关 /models 的全量原始 ID
798
- if (cached && cached.ids.length > 0)
799
- return cached.ids;
800
- }
801
- // 无 baseUrl / 刷新超时或失败 → 回退短别名
802
- const fallback = this.baseUrl ? modelAliasFallbacks.get(normalizeModelGatewayUrl(this.baseUrl)) : undefined;
803
- return Object.values({ ...BOOTSTRAP_MODEL_ALIASES, ...fallback });
804
- }
805
- /** 将短别名解析为当前代理实际使用的完整 model ID(仅用于展示,不改变持久化值) */
674
+ const catalog = await this.getModelCatalog();
675
+ assertCatalogSelectable(catalog);
676
+ return catalog.models.map(m => m.id);
677
+ }
806
678
  resolveModelId(model) {
807
- return resolveModelAlias(model, this.baseUrl);
679
+ return resolveCatalogModel(peekRunnerCatalog('claude', this.agentAid, this.catalogConnection()), model);
680
+ }
681
+ async getAvailableEfforts(model) {
682
+ return catalogEfforts(await this.getModelCatalog(), model, 'claude');
808
683
  }
809
684
  setEffort(effort) {
810
685
  this.effort = effort;
@@ -1454,8 +1329,9 @@ export class AgentRunner {
1454
1329
  * SDK 原始事件 → 标准 AgentEvent 转换
1455
1330
  * 所有 SDK 特有的事件类型引用封装在此方法内
1456
1331
  */
1457
- async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile, permissionMode, turn) {
1332
+ async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile, permissionMode, turn, projectPath = process.cwd(), topicBinding = false) {
1458
1333
  let lastSessionId;
1334
+ let pendingSessionId;
1459
1335
  let hasTurnActivity = false;
1460
1336
  let ignoredPreTurnResult = false;
1461
1337
  let waitingForBackgroundCompletion = false;
@@ -1475,19 +1351,41 @@ export class AgentRunner {
1475
1351
  let lastAssistantUuid;
1476
1352
  // 流式收集各次大模型调用(fallback:SDK iterations 为空时使用)
1477
1353
  const collectedCalls = [];
1354
+ const activatePendingSession = async (waitForFile) => {
1355
+ const candidate = pendingSessionId;
1356
+ if (!candidate)
1357
+ return undefined;
1358
+ if (!isResuming && topicBinding) {
1359
+ const ready = waitForFile
1360
+ ? await waitForClaudeSessionFile(projectPath, candidate)
1361
+ : isClaudeSessionFileReady(projectPath, candidate);
1362
+ if (!ready)
1363
+ return undefined;
1364
+ }
1365
+ if (!await this.acceptDiscoveredSessionId(sessionId, candidate, turn)) {
1366
+ logger.warn(`[AgentRunner] Rejected stale backend discovery: sessionId=${sessionId} backend=${candidate} turn=${turn?.turnId ?? 'legacy'}`);
1367
+ const error = new Error('backend discovery rejected by active turn lease');
1368
+ error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
1369
+ throw error;
1370
+ }
1371
+ pendingSessionId = undefined;
1372
+ return candidate;
1373
+ };
1478
1374
  try {
1479
1375
  for await (const event of sdkStream) {
1376
+ const activatedBeforeEvent = await activatePendingSession(false);
1377
+ if (activatedBeforeEvent)
1378
+ yield { type: 'session_id', sessionId: activatedBeforeEvent };
1480
1379
  // 提取 session_id(任意 SDK 事件都可能携带)
1481
1380
  if (event.session_id && event.session_id !== lastSessionId) {
1482
1381
  lastSessionId = event.session_id;
1483
- if (await this.acceptDiscoveredSessionId(sessionId, event.session_id, turn)) {
1484
- yield { type: 'session_id', sessionId: event.session_id };
1382
+ pendingSessionId = event.session_id;
1383
+ const activated = await activatePendingSession(event.type === 'result');
1384
+ if (activated) {
1385
+ yield { type: 'session_id', sessionId: activated };
1485
1386
  }
1486
- else {
1487
- logger.warn(`[AgentRunner] Rejected stale backend discovery: sessionId=${sessionId} backend=${event.session_id} turn=${turn?.turnId ?? 'legacy'}`);
1488
- const error = new Error('backend discovery rejected by active turn lease');
1489
- error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
1490
- throw error;
1387
+ else if (!isResuming) {
1388
+ logger.info(`[AgentRunner] Deferring backend activation until Claude session file is durable: sessionId=${sessionId} backend=${event.session_id}`);
1491
1389
  }
1492
1390
  }
1493
1391
  if (event.type === 'user' && event.uuid === inputId) {
@@ -1582,7 +1480,17 @@ export class AgentRunner {
1582
1480
  }
1583
1481
  if (event.type === 'stream_event') {
1584
1482
  const streamEvent = event.event;
1585
- if (streamEvent?.type === 'message_start' && streamEvent.message?.usage) {
1483
+ if (streamEvent?.type === 'content_block_start'
1484
+ && (streamEvent.content_block?.type === 'thinking' || streamEvent.content_block?.type === 'redacted_thinking')) {
1485
+ hasTurnActivity = true;
1486
+ yield { type: 'heartbeat', source: 'reasoning', turn: turnCount || undefined };
1487
+ }
1488
+ else if (streamEvent?.type === 'content_block_delta'
1489
+ && streamEvent.delta?.type === 'thinking_delta') {
1490
+ hasTurnActivity = true;
1491
+ yield { type: 'heartbeat', source: 'reasoning', turn: turnCount || undefined };
1492
+ }
1493
+ else if (streamEvent?.type === 'message_start' && streamEvent.message?.usage) {
1586
1494
  hasTurnActivity = true;
1587
1495
  lastAssistantWasSynthetic = streamEvent.message.model === '<synthetic>';
1588
1496
  lastModelCall = {
@@ -1616,6 +1524,13 @@ export class AgentRunner {
1616
1524
  }
1617
1525
  continue;
1618
1526
  }
1527
+ // During redacted thinking Claude may emit token estimates instead of
1528
+ // readable thinking deltas. They still prove the model is active.
1529
+ if (event.type === 'system' && event.subtype === 'thinking_tokens') {
1530
+ hasTurnActivity = true;
1531
+ yield { type: 'heartbeat', source: 'reasoning', turn: turnCount || undefined };
1532
+ continue;
1533
+ }
1619
1534
  // system: compact_boundary → compact
1620
1535
  if (event.type === 'system' && event.subtype === 'compact_boundary') {
1621
1536
  yield {
@@ -1686,7 +1601,11 @@ export class AgentRunner {
1686
1601
  // 统计本轮 base agent 全部输出字符数(text + tool_use input)
1687
1602
  let turnOutputChars = 0;
1688
1603
  for (const content of event.message.content) {
1689
- if (content.type === 'tool_use') {
1604
+ if (content.type === 'thinking' || content.type === 'redacted_thinking') {
1605
+ hasTurnActivity = true;
1606
+ yield { type: 'heartbeat', source: 'reasoning', turn: turnCount || undefined };
1607
+ }
1608
+ else if (content.type === 'tool_use') {
1690
1609
  const inputStr = typeof content.input === 'string' ? content.input : JSON.stringify(content.input || '');
1691
1610
  turnOutputChars += inputStr.length;
1692
1611
  }
@@ -1729,6 +1648,12 @@ export class AgentRunner {
1729
1648
  }
1730
1649
  // result → complete(含 permission_denials 提取)
1731
1650
  if (event.type === 'result') {
1651
+ // The SDK may announce session_id on the initial system event and omit
1652
+ // it from the terminal result. Give the transcript a short grace
1653
+ // period here before deciding that a topic backend is unavailable.
1654
+ const activatedAtResult = await activatePendingSession(true);
1655
+ if (activatedAtResult)
1656
+ yield { type: 'session_id', sessionId: activatedAtResult };
1732
1657
  // Most SDK streams surface denials as user-message tool_result blocks.
1733
1658
  // The terminal summary is only a fallback for incomplete streams.
1734
1659
  if (Array.isArray(event.permission_denials)) {
@@ -1858,6 +1783,15 @@ export class AgentRunner {
1858
1783
  modelCalls,
1859
1784
  assistantUuid: lastAssistantUuid,
1860
1785
  };
1786
+ // A topic binding must never outlive the provider transcript that can
1787
+ // resume it. If Claude reports success before its session file becomes
1788
+ // durable, fail this turn while leaving the topic unbound; /renew is
1789
+ // then available for older sessions that already contain a bad bind.
1790
+ if (!event.is_error && topicBinding && pendingSessionId) {
1791
+ const error = new Error(`bound topic backend is unavailable: ${pendingSessionId}`);
1792
+ error.code = 'TOPIC_BACKEND_UNAVAILABLE';
1793
+ throw error;
1794
+ }
1861
1795
  // Fatal results belong to the query transport itself and must terminate
1862
1796
  // even when the current input was not echoed back by the CLI.
1863
1797
  if (event.is_error) {
@@ -1967,13 +1901,9 @@ export class AgentRunner {
1967
1901
  // 记录当前 evolcore session ID,用于 Agent ctl 环境变量注入
1968
1902
  // 同步用户级配置到内存
1969
1903
  this.syncFromUserSettings();
1970
- // 异步刷新模型别名缓存(fire-and-forget,不阻塞查询)
1904
+ // Run preparation and model menus resolve aliases from the same snapshot.
1905
+ const runCatalog = await this.getModelCatalog();
1971
1906
  if (this.baseUrl) {
1972
- const cacheKey = normalizeModelGatewayUrl(this.baseUrl);
1973
- const cached = modelAliasCache.get(cacheKey);
1974
- if (!cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS)) {
1975
- refreshModelAliases(this.baseUrl, this.apiKey, this.customHeaders);
1976
- }
1977
1907
  // 顺带预热网关价格缓存(1h TTL),让本轮结束写库时已就绪
1978
1908
  const pricing = gatewayPricingCache.get(this.baseUrl);
1979
1909
  if (!pricing || (Date.now() - pricing.fetchedAt > GATEWAY_PRICING_TTL_MS)) {
@@ -2539,7 +2469,7 @@ export class AgentRunner {
2539
2469
  else {
2540
2470
  logger.info(`[AgentRunner] systemPromptAppend: none`);
2541
2471
  }
2542
- const sdkModel = resolveSdkModel(callModel, this.baseUrl);
2472
+ const sdkModel = applyContextWindow(resolveCatalogModel(runCatalog, callModel));
2543
2473
  const capabilityOptions = await this.resolveCapabilityRunOptions(projectPath);
2544
2474
  const managedSettings = callPermissionMode === 'fullaccess'
2545
2475
  ? undefined
@@ -2814,7 +2744,7 @@ export class AgentRunner {
2814
2744
  this.interruptFns.set(sessionId, () => sdkStream.interrupt());
2815
2745
  }
2816
2746
  // 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
2817
- const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile, callPermissionMode, modelOverride?.turn);
2747
+ const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile, callPermissionMode, modelOverride?.turn, projectPath, topicBinding);
2818
2748
  const self = this;
2819
2749
  return (async function* () {
2820
2750
  try {
@@ -2946,16 +2876,19 @@ export class AgentRunner {
2946
2876
  }
2947
2877
  });
2948
2878
  }
2949
- runSessionCommand(prompt, agentSessionId, projectPath, modelOverride) {
2950
- const callModel = modelOverride?.model || this.model;
2879
+ async runSessionCommand(prompt, agentSessionId, projectPath, modelOverride, useModel = true) {
2880
+ const callModel = modelOverride?.model || (useModel ? this.model : undefined);
2951
2881
  const callEffort = (modelOverride?.effortMode === 'model_default'
2952
2882
  ? undefined
2953
- : modelOverride?.effort ?? this.effort);
2883
+ : modelOverride?.effort ?? (useModel ? this.effort : undefined));
2884
+ const catalog = useModel ? await this.getModelCatalog() : undefined;
2885
+ if (catalog)
2886
+ assertCatalogSelectable(catalog);
2954
2887
  return query({
2955
2888
  prompt,
2956
2889
  options: {
2957
2890
  cwd: projectPath,
2958
- model: resolveSdkModel(callModel, this.baseUrl),
2891
+ ...(catalog && callModel ? { model: applyContextWindow(resolveCatalogModel(catalog, callModel)) } : {}),
2959
2892
  resume: agentSessionId,
2960
2893
  maxTurns: 1,
2961
2894
  tools: [],
@@ -2964,9 +2897,12 @@ export class AgentRunner {
2964
2897
  strictMcpConfig: true,
2965
2898
  settingSources: [],
2966
2899
  managedSettings: buildClaudeManagedLockdownSettings(),
2967
- ...(callEffort ? { effort: callEffort } : {}),
2900
+ ...(catalog && callEffort ? { effort: callEffort } : {}),
2968
2901
  permissionMode: this.toSdkPermissionMode(),
2969
- env: this.getAgentEnv()
2902
+ env: this.getAgentEnv(),
2903
+ ...(this.claudeExecutablePath
2904
+ ? { pathToClaudeCodeExecutable: this.claudeExecutablePath }
2905
+ : {})
2970
2906
  }
2971
2907
  });
2972
2908
  }
@@ -2979,7 +2915,7 @@ export class AgentRunner {
2979
2915
  let lastEvent;
2980
2916
  try {
2981
2917
  logger.info(`[AgentRunner] Compacting session: session=${sessionId} agent=${agentSessionId} project=${projectPath}`);
2982
- const stream = this.runSessionCommand('/compact', agentSessionId, projectPath, modelOverride);
2918
+ const stream = await this.runSessionCommand('/compact', agentSessionId, projectPath, modelOverride);
2983
2919
  this.activeStreams.set(sessionId, stream);
2984
2920
  try {
2985
2921
  let boundary;
@@ -3049,7 +2985,9 @@ export class AgentRunner {
3049
2985
  async clearSession(sessionId, agentSessionId, projectPath) {
3050
2986
  try {
3051
2987
  logger.info(`[AgentRunner] Clearing session via SDK: ${agentSessionId}`);
3052
- const stream = this.runSessionCommand('/clear', agentSessionId, projectPath);
2988
+ // Clearing is a backend lifecycle operation. It does not depend on any
2989
+ // Session model or model configuration/selection.
2990
+ const stream = await this.runSessionCommand('/clear', agentSessionId, projectPath, undefined, false);
3053
2991
  this.activeStreams.set(sessionId, stream);
3054
2992
  try {
3055
2993
  let cleared = false;
@@ -3216,6 +3154,7 @@ export class AgentRunner {
3216
3154
  strictMcpConfig: true,
3217
3155
  settingSources: [],
3218
3156
  managedSettings: buildClaudeManagedLockdownSettings(),
3157
+ ...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
3219
3158
  permissionMode: this.toSdkPermissionMode(),
3220
3159
  stderr: (data) => { stderrChunks.push(data); },
3221
3160
  env: this.getAgentEnv(),