evolcore 0.0.23 → 0.0.25

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 (245) hide show
  1. package/CHANGELOG.md +583 -516
  2. package/LICENSE +21 -21
  3. package/README.md +455 -453
  4. package/bin/codex-managed-acl.ps1 +160 -155
  5. package/bin/codex-managed-hook.mjs +209 -92
  6. package/bin/ec-safe-output.js +327 -245
  7. package/bin/ec.js +29 -29
  8. package/bin/install-codex-managed-hooks.mjs +555 -544
  9. package/dist/agents/baseagent-availability.js +4 -4
  10. package/dist/agents/baseagent.js +29 -1
  11. package/dist/agents/claude-runner.js +152 -14
  12. package/dist/agents/codex/app-server-client.js +18 -8
  13. package/dist/agents/codex/catalog.js +1 -1
  14. package/dist/agents/codex/diagnostics.js +215 -0
  15. package/dist/agents/codex-runner.js +445 -63
  16. package/dist/agents/ecagent-runner.js +31 -16
  17. package/dist/agents/gemini-runner.js +82 -4
  18. package/dist/agents/runner-types.js +1 -1
  19. package/dist/aun/aid/agentmd.js +56 -4
  20. package/dist/aun/aid/index.js +1 -0
  21. package/dist/aun/aid/managed-operation.js +80 -6
  22. package/dist/aun/aid/store.js +41 -0
  23. package/dist/aun/msg/managed-operation.js +6 -1
  24. package/dist/aun/outbox.js +56 -18
  25. package/dist/aun/rpc/client.js +4 -5
  26. package/dist/aun/storage/client.js +4 -1
  27. package/dist/aun/storage/index.js +1 -1
  28. package/dist/channels/aun.js +147 -76
  29. package/dist/channels/daemon.js +7 -1
  30. package/dist/channels/dingtalk.js +1 -0
  31. package/dist/channels/feishu.js +10 -4
  32. package/dist/channels/wecom.js +2 -1
  33. package/dist/cli/agent-command.js +214 -89
  34. package/dist/cli/agent-create.js +576 -0
  35. package/dist/cli/agent.js +129 -676
  36. package/dist/cli/aun-commands.js +419 -313
  37. package/dist/cli/bench.js +20 -20
  38. package/dist/cli/config.js +20 -20
  39. package/dist/cli/contact.js +10 -10
  40. package/dist/cli/ctl-command.js +22 -20
  41. package/dist/cli/daemon-commands.js +163 -32
  42. package/dist/cli/fs-command.js +67 -67
  43. package/dist/cli/handoff-command.js +7 -7
  44. package/dist/cli/index.js +79 -79
  45. package/dist/cli/link-rules.js +21 -21
  46. package/dist/cli/managed-command-guard.js +3 -0
  47. package/dist/cli/menu-client.js +11 -11
  48. package/dist/cli/net-check.js +22 -22
  49. package/dist/cli/queue-command.js +29 -29
  50. package/dist/cli/response.js +30 -30
  51. package/dist/cli/restart-monitor.js +462 -38
  52. package/dist/cli/stats.js +161 -51
  53. package/dist/cli/trigger-command.js +56 -56
  54. package/dist/config/builtin-roles.js +3 -0
  55. package/dist/config/config-manager.js +15 -0
  56. package/dist/config/config-operation-service.js +20 -3
  57. package/dist/config/resolved-config-op.js +39 -1
  58. package/dist/core/auth/auth-gateway.js +3 -2
  59. package/dist/core/auth/authorization-audit.js +72 -19
  60. package/dist/core/auth/operation-authorizer.js +125 -7
  61. package/dist/core/auth/operation-catalog.js +26 -2
  62. package/dist/core/auth/remote-access-store.js +3 -2
  63. package/dist/core/baseagent-loader.js +4 -4
  64. package/dist/core/bootstrap-messages.js +39 -18
  65. package/dist/core/bootstrap-service.js +32 -26
  66. package/dist/core/capability/capability-manager.js +8 -4
  67. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  68. package/dist/core/command/agent-control.js +60 -26
  69. package/dist/core/command/cli-intent-parser.js +18 -0
  70. package/dist/core/command/command-handler.js +173 -36
  71. package/dist/core/command/menu-catalog.js +4 -1
  72. package/dist/core/command/menu-handler.js +315 -75
  73. package/dist/core/command/menu-protocol.js +7 -2
  74. package/dist/core/command/slash-gate.js +1 -3
  75. package/dist/core/command/slash-handler.js +166 -57
  76. package/dist/core/data-migration.js +5 -15
  77. package/dist/core/event-catalog.js +8 -0
  78. package/dist/core/evolagent.js +3 -3
  79. package/dist/core/handoff/runtime.js +3 -1
  80. package/dist/core/handoff/store.js +7 -1
  81. package/dist/core/message/im-renderer.js +13 -6
  82. package/dist/core/message/message-bridge.js +5 -36
  83. package/dist/core/message/message-utils.js +6 -3
  84. package/dist/core/message/response-engine.js +85 -27
  85. package/dist/core/model/catalog-service.js +3 -1
  86. package/dist/core/model/model-catalog.js +10 -0
  87. package/dist/core/model/model-permission.js +4 -1
  88. package/dist/core/permission/approval-gateway.js +169 -33
  89. package/dist/core/permission/tool-policy.js +56 -10
  90. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  91. package/dist/core/session/session-manager.js +49 -13
  92. package/dist/core/session/session-renew.js +1 -0
  93. package/dist/index.js +327 -134
  94. package/dist/ipc.js +68 -21
  95. package/dist/paths.js +23 -1
  96. package/dist/stats/billing.js +18 -4
  97. package/dist/stats/db.js +159 -159
  98. package/dist/stats/eck-vars.js +9 -9
  99. package/dist/stats/query.js +139 -139
  100. package/dist/stats/role-budget.js +6 -6
  101. package/dist/stats/writer.js +51 -51
  102. package/dist/utils/atomic-write.js +26 -2
  103. package/dist/utils/avatar-upload.js +282 -23
  104. package/dist/utils/cross-platform.js +436 -1
  105. package/dist/utils/error-dict.json +160 -160
  106. package/dist/utils/error-utils.js +11 -4
  107. package/dist/utils/macos-autostart.js +22 -22
  108. package/dist/utils/model-prices.jsonl +23 -20
  109. package/dist/utils/npm-ops.js +24 -1
  110. package/dist/utils/welcome.js +22 -22
  111. package/dist/utils/windows-autostart.js +1 -1
  112. package/ecagent/LICENSE +21 -21
  113. package/ecagent/NOTICE +11 -11
  114. package/ecagent/README.md +61 -61
  115. package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
  116. package/ecagent/dist/harness/compaction/compaction.js +83 -83
  117. package/ecagent/dist/harness/messages.js +7 -7
  118. package/kits/docs/GUIDE.md +20 -20
  119. package/kits/docs/INDEX.md +66 -66
  120. package/kits/docs/aun/CHEATSHEET.md +19 -19
  121. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
  122. package/kits/docs/channels/aun.md +56 -56
  123. package/kits/docs/channels/feishu.md +56 -56
  124. package/kits/docs/context-assembly.md +365 -364
  125. package/kits/docs/eck_templates/GUIDE.template.md +22 -22
  126. package/kits/docs/eck_templates/INDEX.template.md +28 -28
  127. package/kits/docs/eck_templates/path-registry.template.md +33 -33
  128. package/kits/docs/eck_templates/runtime.template.md +19 -19
  129. package/kits/docs/evolcore/INDEX.md +71 -69
  130. package/kits/docs/evolcore/agent-create.md +170 -156
  131. package/kits/docs/evolcore/agent.md +83 -73
  132. package/kits/docs/evolcore/aid.md +64 -62
  133. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
  134. package/kits/docs/evolcore/config.md +195 -195
  135. package/kits/docs/evolcore/contact.md +68 -68
  136. package/kits/docs/evolcore/ctl.md +46 -46
  137. package/kits/docs/evolcore/event.md +216 -216
  138. package/kits/docs/evolcore/fs-architecture.md +1215 -1215
  139. package/kits/docs/evolcore/fs.md +110 -110
  140. package/kits/docs/evolcore/group-collaboration.md +243 -243
  141. package/kits/docs/evolcore/group-fs.md +17 -17
  142. package/kits/docs/evolcore/group-rules.md +251 -251
  143. package/kits/docs/evolcore/group.md +152 -152
  144. package/kits/docs/evolcore/menu-client.md +34 -34
  145. package/kits/docs/evolcore/model.md +50 -50
  146. package/kits/docs/evolcore/msg.md +165 -165
  147. package/kits/docs/evolcore/response.md +75 -75
  148. package/kits/docs/evolcore/rpc.md +37 -37
  149. package/kits/docs/evolcore/self-summary.md +29 -29
  150. package/kits/docs/evolcore/stats.md +83 -83
  151. package/kits/docs/evolcore/storage.md +50 -50
  152. package/kits/docs/evolcore/trigger.md +575 -575
  153. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
  154. package/kits/docs/identity/PATH_OPS.md +16 -16
  155. package/kits/docs/identity/ROLE_DETAIL.md +25 -25
  156. package/kits/docs/identity/identity-tools.md +26 -26
  157. package/kits/docs/path-registry.md +62 -62
  158. package/kits/docs/prompt-loading-architecture.md +270 -269
  159. package/kits/docs/venues/aun-group.md +45 -45
  160. package/kits/docs/venues/aun-private.md +10 -10
  161. package/kits/docs/venues/client-desktop.md +10 -10
  162. package/kits/docs/venues/client-mobile.md +10 -10
  163. package/kits/docs/venues/feishu-group.md +13 -13
  164. package/kits/docs/venues/feishu-private.md +9 -9
  165. package/kits/docs/venues/group.md +25 -25
  166. package/kits/docs/venues/private.md +10 -10
  167. package/kits/eck_manifest.auxiliary.json +43 -43
  168. package/kits/eck_manifest.json +195 -183
  169. package/kits/eck_message_manifest.json +74 -74
  170. package/kits/migrations/README-role-config-v4.md +32 -32
  171. package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
  172. package/kits/migrations/migrate-role-config-v4.mjs +614 -614
  173. package/kits/migrations/migrate-role-config-v5.mjs +365 -365
  174. package/kits/migrations/rename-config-file.mjs +99 -99
  175. package/kits/rules/01-overview.md +162 -162
  176. package/kits/rules/02-navigation.md +96 -96
  177. package/kits/rules/03-identity.md +39 -37
  178. package/kits/rules/04-relation.md +75 -75
  179. package/kits/rules/05-venue.md +60 -60
  180. package/kits/rules/06-channel.md +63 -63
  181. package/kits/schemas/_meta.json +53 -53
  182. package/kits/schemas/agent-config.schema.1.json +177 -177
  183. package/kits/schemas/agent-config.schema.10.json +395 -395
  184. package/kits/schemas/agent-config.schema.11.json +421 -421
  185. package/kits/schemas/agent-config.schema.12.json +427 -427
  186. package/kits/schemas/agent-config.schema.2.json +239 -239
  187. package/kits/schemas/agent-config.schema.3.json +119 -119
  188. package/kits/schemas/agent-config.schema.4.json +208 -208
  189. package/kits/schemas/agent-config.schema.5.json +326 -326
  190. package/kits/schemas/agent-config.schema.6.json +322 -322
  191. package/kits/schemas/agent-config.schema.7.json +303 -303
  192. package/kits/schemas/agent-config.schema.8.json +304 -304
  193. package/kits/schemas/agent-config.schema.9.json +364 -364
  194. package/kits/schemas/contact-book.schema.1.json +36 -36
  195. package/kits/schemas/contact-book.schema.2.json +43 -43
  196. package/kits/schemas/contact-book.schema.3.json +67 -67
  197. package/kits/schemas/daemon.schema.1.json +91 -91
  198. package/kits/schemas/daemon.schema.2.json +101 -101
  199. package/kits/schemas/daemon.schema.3.json +123 -123
  200. package/kits/schemas/daemon.schema.4.json +136 -136
  201. package/kits/schemas/daemon.schema.5.json +135 -135
  202. package/kits/schemas/daemon.schema.6.json +131 -131
  203. package/kits/schemas/defaults.schema.1.json +81 -81
  204. package/kits/schemas/defaults.schema.2.json +85 -85
  205. package/kits/schemas/defaults.schema.3.json +73 -73
  206. package/kits/schemas/defaults.schema.4.json +107 -107
  207. package/kits/schemas/defaults.schema.5.json +119 -119
  208. package/kits/schemas/menu-exec-schema-commands.md +208 -208
  209. package/kits/schemas/migrations/README.md +30 -30
  210. package/kits/schemas/relation-config.schema.1.json +158 -158
  211. package/kits/schemas/relation-config.schema.2.json +73 -73
  212. package/kits/schemas/relation-config.schema.3.json +50 -50
  213. package/kits/schemas/relation-config.schema.4.json +47 -47
  214. package/kits/schemas/relation-config.schema.5.json +47 -47
  215. package/kits/schemas/relation-config.schema.6.json +46 -46
  216. package/kits/schemas/relation-config.schema.7.json +59 -59
  217. package/kits/schemas/relation-config.schema.8.json +103 -103
  218. package/kits/schemas/role-config.schema.1.json +201 -201
  219. package/kits/schemas/role-registry.schema.1.json +35 -35
  220. package/kits/schemas/single-session.schema.1.json +31 -31
  221. package/kits/schemas/single-session.schema.2.json +57 -57
  222. package/kits/schemas/single-session.schema.3.json +32 -32
  223. package/kits/templates/bootstrap-welcome.md +17 -17
  224. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
  225. package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
  226. package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
  227. package/kits/templates/message-fragments/inject-default.md +2 -2
  228. package/kits/templates/message-fragments/item.md +2 -2
  229. package/kits/templates/roles/admin.json +96 -82
  230. package/kits/templates/roles/member.json +97 -96
  231. package/kits/templates/roles/owner.json +9 -9
  232. package/kits/templates/roles/visitor.json +86 -85
  233. package/kits/templates/system-fragments/baseagent.md +14 -14
  234. package/kits/templates/system-fragments/bootstrap.md +38 -29
  235. package/kits/templates/system-fragments/channel.md +64 -64
  236. package/kits/templates/system-fragments/commands.md +28 -28
  237. package/kits/templates/system-fragments/identity.md +13 -13
  238. package/kits/templates/system-fragments/relation.md +19 -19
  239. package/kits/templates/system-fragments/session.md +59 -59
  240. package/kits/templates/system-fragments/venue.md +31 -31
  241. package/package.json +98 -94
  242. package/AGENTS.md +0 -62
  243. package/dist/utils/codex-cli.js +0 -43
  244. package/skills/eclink/SKILL.md +0 -84
  245. package/skills/eclink/agents/openai.yaml +0 -4
@@ -1,9 +1,8 @@
1
1
  import { normalizePeer, writeScope } from '../model/config-scope.js';
2
2
  import { formatPeerKey } from '../relation/peer-identity.js';
3
- import { modelMatches, getCatalog, resolveCatalogModel, assertCatalogSelectable } from '../model/model-catalog.js';
3
+ import { modelMatches, getCatalog, getBaseagentCatalog, resolveCatalogModel, assertCatalogSelectable, TRANSPORT_EFFORTS } from '../model/model-catalog.js';
4
4
  import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
5
5
  import { hasModelSwitcher } from '../../agents/runner-types.js';
6
- import { execCodexCliSync, resolveCodexCliPath } from '../../utils/codex-cli.js';
7
6
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
8
7
  import { inspectDataMigrationRequirement } from '../data-migration.js';
9
8
  import { buildEnvelope, isDeliveryTarget, isDeliveryTargetForChannel, replyContextFromSession } from '../message/message-utils.js';
@@ -11,15 +10,16 @@ import path from 'path';
11
10
  import fs from 'fs';
12
11
  import os from 'os';
13
12
  import crypto from 'crypto';
14
- import { execFileSync } from 'child_process';
15
13
  import { CronExpressionParser } from 'cron-parser';
16
14
  import { parseDuration } from '../../trigger/parser.js';
17
15
  import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions, resolveGlobalPkg } from '../../utils/npm-ops.js';
18
- import { commandExists, spawnDetachedNode } from '../../utils/cross-platform.js';
16
+ import { commandExists, resolveCliEntry, launchMonitorAndConfirm } from '../../utils/cross-platform.js';
17
+ import { cachedCliVersion } from '../../utils/cross-platform.js';
18
+ import { scanInstances } from '../../utils/instance-registry.js';
19
19
  import { isFullAccessEnabled, loadDefaults, loadDaemonConfig } from '../../config-store.js';
20
20
  import { WEB_PACKAGE_NAME } from '../../product.js';
21
21
  import { read as cfgRead, resolveEffective, resolveEffectiveFieldWithSource, routeFieldPath, write as cfgWrite, ConfigTarget, } from '../../config/config-manager.js';
22
- import { execAgentAction, execAgentQuery, execAgentOptions, resolveProjectPath } from './agent-control.js';
22
+ import { execAgentAction, execAgentQuery, execAgentOptions, execAidAvatar, resolveProjectPath } from './agent-control.js';
23
23
  import { gatewayList, gatewayUpdate, gatewayDelete, gatewayTest, gatewayModels, gatewaySetPrice, gatewaySyncEnv } from '../../config/gateway-config.js';
24
24
  import { displaySessionTitle } from '../session/session-title.js';
25
25
  import { buildSessionTurnList } from '../session/session-turns.js';
@@ -52,32 +52,7 @@ import { probeSessionActivity } from './slash-gate.js';
52
52
  * 失败返回 null(命令不存在或执行失败)。
53
53
  */
54
54
  function getBaseagentVersion(cmd) {
55
- try {
56
- const executable = cmd === 'codex' ? resolveCodexCliPath() : cmd;
57
- if (!executable)
58
- return null;
59
- const output = cmd === 'codex'
60
- ? execCodexCliSync(['--version'], {
61
- encoding: 'utf-8',
62
- timeout: 3000,
63
- stdio: ['ignore', 'pipe', 'pipe'],
64
- })
65
- : execFileSync(executable, ['--version'], {
66
- encoding: 'utf-8',
67
- timeout: 3000,
68
- stdio: ['ignore', 'pipe', 'pipe'],
69
- windowsHide: process.platform === 'win32',
70
- });
71
- const versionOutput = String(output).trim();
72
- // claude: "2.1.187 (Claude Code)" → 提取 "2.1.187"
73
- // gemini: "0.38.0" → 直接返回
74
- // codex: "codex-cli 0.142.0" → 提取 "0.142.0"
75
- const match = versionOutput.match(/(\d+\.\d+\.\d+)/);
76
- return match ? match[1] : null;
77
- }
78
- catch {
79
- return null;
80
- }
55
+ return cachedCliVersion(cmd);
81
56
  }
82
57
  const allEfforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
83
58
  const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
@@ -397,7 +372,14 @@ function tokenizeArgv(line) {
397
372
  return out;
398
373
  }
399
374
  async function getAvailableEfforts(agent, model) {
400
- return (await agent.getAvailableEfforts?.(model) ?? []);
375
+ const dynamic = await agent.getAvailableEfforts?.(model);
376
+ if (Array.isArray(dynamic))
377
+ return dynamic;
378
+ // Keep Menu usable with legacy/embedded runner implementations that do not
379
+ // expose the optional catalog method. Full runners still take the dynamic
380
+ // path above, so this fallback cannot broaden their advertised capabilities.
381
+ const baseagent = agent?.baseagent || agent?.name;
382
+ return (TRANSPORT_EFFORTS[baseagent] ?? []);
401
383
  }
402
384
  function modelDisplayLabel(agent, model) {
403
385
  const full = agent.resolveModelId?.(model);
@@ -410,6 +392,29 @@ function menuStringArg(args, key) {
410
392
  const trimmed = value.trim();
411
393
  return trimmed || undefined;
412
394
  }
395
+ function hostCatalogArgs(args = {}) {
396
+ const invalid = (message) => { throw { code: 'INVALID_ARGUMENT', message }; };
397
+ if (!args || typeof args !== 'object' || Array.isArray(args))
398
+ invalid('Baseagent catalog args must be an object');
399
+ if (Object.keys(args).some(key => !['baseagent', 'ba', 'refresh', 'scope', 'context'].includes(key))) {
400
+ invalid('Baseagent catalog only accepts baseagent, ba, refresh and process context');
401
+ }
402
+ if (args.scope !== undefined && args.scope !== 'process')
403
+ invalid('Baseagent catalog requires process scope');
404
+ if (args.context !== undefined && (!args.context || typeof args.context !== 'object'
405
+ || Array.isArray(args.context) || args.context.kind !== 'process'
406
+ || Object.keys(args.context).some(key => key !== 'kind'))) {
407
+ invalid('Baseagent catalog requires process context');
408
+ }
409
+ for (const key of ['baseagent', 'ba']) {
410
+ if (args[key] !== undefined && (typeof args[key] !== 'string' || !args[key].trim())) {
411
+ invalid(`${key} must be a non-empty string`);
412
+ }
413
+ }
414
+ if (args.refresh !== undefined && typeof args.refresh !== 'boolean')
415
+ invalid('refresh must be a boolean');
416
+ return { baseagent: menuStringArg(args, 'baseagent') ?? menuStringArg(args, 'ba') ?? 'claude', refresh: args.refresh === true };
417
+ }
413
418
  const LEGACY_SESSION_CONFIG_ARG_KEYS = [
414
419
  'session',
415
420
  'sessionId',
@@ -852,6 +857,8 @@ export const SELF_MANAGE_AGENT_ACTIONS = new Set(['update', 'reload']);
852
857
  /** 判断 (cmdBase, action) 是否为进程级操作(仅控制 channel 可执行)。
853
858
  * /system 全部进程级;/agent 仅 create/delete/enable/disable 进程级;其余关系级。 */
854
859
  export function isProcessLevelAction(cmdBase, action) {
860
+ if (cmdBase === '/baseagent-models')
861
+ return true;
855
862
  if (cmdBase === '/system')
856
863
  return true;
857
864
  if (cmdBase === '/gateway')
@@ -900,6 +907,8 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
900
907
  });
901
908
  const modelScope = args?.scope === 'agent' ? 'agent' : 'relation';
902
909
  if (verb === 'query') {
910
+ if (cmdBase === '/baseagent-models')
911
+ return intent('baseagent.models.list', 'process');
903
912
  if (cmdBase === '/pwd')
904
913
  return intent('project.current', 'relation');
905
914
  if (cmdBase === '/session' || cmdBase === '/s')
@@ -1019,6 +1028,8 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
1019
1028
  if (action === 'disable')
1020
1029
  return intent('agent.disable', 'control', { action });
1021
1030
  }
1031
+ if (cmdBase === '/aid' && action === 'avatar')
1032
+ return intent('ec.aid.avatar', 'agent', { action });
1022
1033
  if (cmdBase === '/trigger') {
1023
1034
  if (action === 'create' || action === 'set')
1024
1035
  return intent('trigger.create', 'relation', { action });
@@ -1107,6 +1118,7 @@ async function authorizeMenuIntent(params) {
1107
1118
  intent.source = source;
1108
1119
  const decision = await authorizeOperation({ source, intent, subject });
1109
1120
  if (!decision.allow) {
1121
+ logger.warn(`[MenuAuth] denied operation=${intent.operation} scope=${intent.scope} source=${source} selfAid=${subject.selfAid ?? '<none>'} actor=${subject.actorId ?? '<none>'} role=${subject.role} processRole=${subject.processRole} dataScope=${subject.dataScope} daemonOwner=${subject.isDaemonOwner} control=${subject.fromControlChannel} reason=${decision.reason}`);
1110
1122
  if (intent.operation.startsWith('role.') || intent.operation.startsWith('connect.')) {
1111
1123
  const namespace = intent.operation.split('.', 1)[0];
1112
1124
  return {
@@ -1277,7 +1289,11 @@ function menuResultFailure(error) {
1277
1289
  return { error: normalized.message, code: normalized.code, ...(normalized.data !== undefined ? { data: normalized.data } : {}) };
1278
1290
  }
1279
1291
  function resolveExternalMenuAgentChannel(owner, payload, fallbackChannel, required) {
1280
- const requestedAid = typeof payload?.agent === 'string' ? payload.agent.trim() : '';
1292
+ const requestedAid = typeof payload?.agent === 'string' && payload.agent.trim()
1293
+ ? payload.agent.trim()
1294
+ : typeof payload?.args?.aid === 'string'
1295
+ ? payload.args.aid.trim()
1296
+ : '';
1281
1297
  if (!requestedAid) {
1282
1298
  return required
1283
1299
  ? { error: '缺少顶层 agent 参数', code: 'MISSING_AID' }
@@ -1510,6 +1526,8 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1510
1526
  const gated0 = gateControlScope.call(this, { cmdBase: cmdBase0, args, channel, fromControlChannel });
1511
1527
  if (gated0)
1512
1528
  throw { code: gated0.code, message: gated0.error };
1529
+ if (cmdBase0 === '/baseagent-models')
1530
+ throw { code: 'NOT_SUPPORTED', message: 'Baseagent catalog only supports menu.query' };
1513
1531
  if (cmdBase0 === '/role') {
1514
1532
  const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1515
1533
  const authorized = await authorizeRoleMenu(this, {
@@ -1628,7 +1646,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1628
1646
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
1629
1647
  const scope = args?.options === 'all' ? 'all' : 'enabled';
1630
1648
  const role = optionsIdentity.role;
1631
- const isAdmin = isManagementRole(role) || subject.isDaemonOwner;
1649
+ const isAdmin = isManagementRole(role) || subject.isDaemonOwner || subject.processRole === 'daemon-service';
1632
1650
  if (!triggerScheduler)
1633
1651
  return [];
1634
1652
  const list = triggerScheduler.list({ all: scope === 'all' });
@@ -1731,7 +1749,16 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1731
1749
  });
1732
1750
  if ('error' in target)
1733
1751
  throw { code: target.code, message: target.error };
1734
- const agent = this.getAgent(channel, target.baseagent);
1752
+ let agent;
1753
+ try {
1754
+ agent = this.getAgent(channel, target.baseagent);
1755
+ }
1756
+ catch (error) {
1757
+ if (subject.processRole === 'daemon-service') {
1758
+ return [{ value: target.baseagent, label: target.baseagent, selected: true }];
1759
+ }
1760
+ throw error;
1761
+ }
1735
1762
  if (hasModelSwitcher(agent)) {
1736
1763
  const catalog = await getCatalog(target.sel.self, target.baseagent);
1737
1764
  assertCatalogSelectable(catalog);
@@ -1793,7 +1820,16 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1793
1820
  });
1794
1821
  if ('error' in target)
1795
1822
  throw { code: target.code, message: target.error };
1796
- const agent = this.getAgent(channel, target.baseagent);
1823
+ let agent;
1824
+ try {
1825
+ agent = this.getAgent(channel, target.baseagent);
1826
+ }
1827
+ catch (error) {
1828
+ if (subject.processRole === 'daemon-service') {
1829
+ return ['low', 'medium', 'high', 'auto'].map(e => ({ value: e, label: e === 'auto' ? 'auto (SDK默认)' : e }));
1830
+ }
1831
+ throw error;
1832
+ }
1797
1833
  const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'currentModel');
1798
1834
  const modelTarget = { ...target, field: 'model', fieldPath: `baseagents.${target.baseagent}.model` };
1799
1835
  const currentModel = requestedModel || (hasModelSwitcher(agent) ? (readMenuModel(modelTarget, agent).value || agent.getModel()) : agent.name);
@@ -1947,6 +1983,18 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1947
1983
  });
1948
1984
  if (authDenied)
1949
1985
  return authDenied;
1986
+ if (cmdBase === '/baseagent-models') {
1987
+ try {
1988
+ const query = hostCatalogArgs(args);
1989
+ return { data: await getBaseagentCatalog(query.baseagent, { refresh: query.refresh }) };
1990
+ }
1991
+ catch (error) {
1992
+ if (error?.code === 'INVALID_ARGUMENT')
1993
+ return menuResultFailure(error);
1994
+ logger.error('Failed to query host baseagent catalog');
1995
+ return { error: 'Unable to query host baseagent catalog', code: 'INTERNAL_ERROR' };
1996
+ }
1997
+ }
1950
1998
  // ── /agent 查询(只读) ──
1951
1999
  // 授权由 agent.list/agent.show operation 决定;channel 闸门只负责目标范围。
1952
2000
  if (cmdBase === '/agent') {
@@ -2408,7 +2456,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2408
2456
  return { error: `不支持 query: ${cmdBase}`, code: 'NOT_SUPPORTED' };
2409
2457
  }
2410
2458
  /** menu.update — 写入新值。 */
2411
- export async function execMenuUpdate(cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel = false, args, authSubject, source = 'menu') {
2459
+ export async function execMenuUpdate(cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel = false, args, authSubject, source = 'menu', transportReplyContext) {
2412
2460
  const cmdBase = cmd.trim().split(' ')[0];
2413
2461
  if (!cmdBase)
2414
2462
  return { error: '缺少命令', code: 'MISSING_CMD' };
@@ -2424,10 +2472,27 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2424
2472
  const gated = gateControlScope.call(this, { cmdBase, args, channel, fromControlChannel });
2425
2473
  if (gated)
2426
2474
  return gated;
2475
+ const loadedContext = await this.loadMenuContext(channel, channelId);
2476
+ let session = loadedContext.session;
2477
+ const evolagent = loadedContext.evolagent;
2478
+ if (transportReplyContext?.threadId && typeof this.sessionManager.getThreadSession === 'function') {
2479
+ try {
2480
+ const topicSession = await this.sessionManager.getThreadSession(channel, channelId, transportReplyContext.threadId);
2481
+ if (!topicSession) {
2482
+ return { error: '话题会话不存在,拒绝回退到主会话', code: 'TOPIC_SESSION_NOT_FOUND' };
2483
+ }
2484
+ session = topicSession;
2485
+ }
2486
+ catch (error) {
2487
+ logger.debug('[MenuHandler] topic session lookup failed for menu update: thread=' + transportReplyContext.threadId + ': ' + (error instanceof Error ? error.message : String(error)));
2488
+ return { error: '无法读取话题会话,拒绝回退到主会话', code: 'TOPIC_SESSION_LOOKUP_FAILED' };
2489
+ }
2490
+ }
2491
+ if (cmdBase === '/baseagent-models')
2492
+ return { error: 'Baseagent catalog only supports menu.query', code: 'NOT_SUPPORTED' };
2427
2493
  const arg = value.trim();
2428
2494
  if (!arg)
2429
2495
  return { error: '缺少 value 参数', code: 'MISSING_VALUE' };
2430
- const { session, evolagent } = await this.loadMenuContext(channel, channelId);
2431
2496
  const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
2432
2497
  const isAdmin = isManagementRole(identity.role);
2433
2498
  const subject = buildMenuAuthSubject(this, {
@@ -2516,7 +2581,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2516
2581
  }
2517
2582
  if (!patch?.nameOrId)
2518
2583
  return { error: '缺少 nameOrId', code: 'INVALID_ARGS' };
2519
- const isAdmin = isManagementRole(identity.role) || subject.isDaemonOwner;
2584
+ const isAdmin = isManagementRole(identity.role) || subject.isDaemonOwner || subject.processRole === 'daemon-service';
2520
2585
  if (!isAdmin && !userId)
2521
2586
  return { error: '无法确认身份,请确保渠道提供发送者 ID', code: 'FORBIDDEN' };
2522
2587
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
@@ -2578,21 +2643,112 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2578
2643
  return { error: `无效 baseagent: ${arg},可选: ${valid.join(' / ')}`, code: 'INVALID_VALUE' };
2579
2644
  }
2580
2645
  const previousBaseagent = evolagent.baseagent;
2646
+ if (session?.threadId) {
2647
+ if (transportReplyContext?.threadId !== session.threadId) {
2648
+ return { error: '话题路由上下文不匹配,拒绝切换', code: 'THREAD_CONTEXT_MISMATCH' };
2649
+ }
2650
+ if (typeof this.messageQueue.withSessionBarrier !== 'function') {
2651
+ return { error: '当前运行环境不支持安全话题切换', code: 'NOT_SUPPORTED' };
2652
+ }
2653
+ const sessionKey = this.getQueueKey(session, channel, channelId);
2654
+ const currentAgent = this.getAgent(channel, session.baseagent);
2655
+ const outcome = await this.messageQueue.withSessionBarrier(sessionKey, async (snapshot) => {
2656
+ const latest = await this.sessionManager.getSessionById?.(session.id);
2657
+ if (!latest || latest.threadId !== session.threadId) {
2658
+ return { error: '话题会话不存在或归属已变化', code: 'NOT_FOUND' };
2659
+ }
2660
+ const busy = !!latest.processingState
2661
+ || !!latest.metadata?.turnState?.active
2662
+ || snapshot.active
2663
+ || snapshot.queued > 0
2664
+ || snapshot.dispatching
2665
+ || this.messageQueue.isProcessing?.(sessionKey)
2666
+ || (this.messageQueue.getQueueLength?.(sessionKey) ?? 0) > 0
2667
+ || currentAgent.hasActiveStream(sessionKey);
2668
+ if (busy) {
2669
+ return { error: '当前话题会话忙碌,无法切换 baseagent', code: 'BUSY' };
2670
+ }
2671
+ try {
2672
+ evolagent.setActiveBaseagent(arg);
2673
+ await this.sessionManager.rotateTopicBackend({
2674
+ sessionId: latest.id,
2675
+ expectedThreadId: latest.threadId,
2676
+ baseagent: arg,
2677
+ });
2678
+ return { ok: true };
2679
+ }
2680
+ catch (error) {
2681
+ try {
2682
+ evolagent.setActiveBaseagent(previousBaseagent);
2683
+ }
2684
+ catch { }
2685
+ return { error: error instanceof Error ? error.message : String(error), code: 'EXEC_FAILED' };
2686
+ }
2687
+ });
2688
+ if ('error' in outcome)
2689
+ return outcome;
2690
+ this.eventBus.publish({
2691
+ type: 'agent:baseagent-changed',
2692
+ aid: evolagent.aid,
2693
+ baseagent: arg,
2694
+ previousBaseagent,
2695
+ scope: 'agent',
2696
+ timestamp: Date.now(),
2697
+ });
2698
+ return { data: {
2699
+ baseagent: arg,
2700
+ previousBaseagent,
2701
+ baseagentChanged: previousBaseagent !== arg,
2702
+ scope: 'agent',
2703
+ sessionId: session.id,
2704
+ threadId: session.threadId,
2705
+ sessionIdChanged: false,
2706
+ notice: '已切换 Baseagent: ' + previousBaseagent + ' → ' + arg + ',话题会话 ID 保持不变',
2707
+ } };
2708
+ }
2581
2709
  try {
2582
2710
  evolagent.setActiveBaseagent(arg);
2583
2711
  }
2584
2712
  catch (error) {
2585
2713
  return { error: error instanceof Error ? error.message : String(error), code: 'INVALID_VALUE' };
2586
2714
  }
2587
- this.eventBus.publish({
2588
- type: 'agent:baseagent-changed',
2589
- aid: evolagent.aid,
2590
- baseagent: arg,
2591
- previousBaseagent,
2592
- scope: 'agent',
2593
- timestamp: Date.now(),
2594
- });
2595
- return { data: { baseagent: arg, scope: 'agent' } };
2715
+ try {
2716
+ const newSession = await this.sessionManager.createNewSession(channel, channelId, evolagent.projectPath, undefined, arg, session?.metadata, { previousBaseagent, baseagentChanged: previousBaseagent !== arg });
2717
+ if (session) {
2718
+ try {
2719
+ const previousAgent = this.getAgent(channel, session.baseagent);
2720
+ await previousAgent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
2721
+ await previousAgent.closeSession(session.id);
2722
+ }
2723
+ catch (error) {
2724
+ // The new Evolcore session is already active; backend cleanup is best effort.
2725
+ logger.warn(`[Menu] baseagent switch cleanup failed: session=${session.id}: ${error instanceof Error ? error.message : String(error)}`);
2726
+ }
2727
+ }
2728
+ this.eventBus.publish({
2729
+ type: 'agent:baseagent-changed',
2730
+ aid: evolagent.aid,
2731
+ baseagent: arg,
2732
+ previousBaseagent,
2733
+ scope: 'agent',
2734
+ timestamp: Date.now(),
2735
+ });
2736
+ return { data: {
2737
+ baseagent: arg,
2738
+ previousBaseagent,
2739
+ baseagentChanged: previousBaseagent !== arg,
2740
+ scope: 'agent',
2741
+ sessionId: newSession.id,
2742
+ notice: `已切换 Baseagent: ${previousBaseagent} → ${arg},已创建新的主会话`,
2743
+ } };
2744
+ }
2745
+ catch (error) {
2746
+ try {
2747
+ evolagent.setActiveBaseagent(previousBaseagent);
2748
+ }
2749
+ catch { }
2750
+ return { error: `创建新会话失败:${error instanceof Error ? error.message : String(error)}`, code: 'EXEC_FAILED' };
2751
+ }
2596
2752
  }
2597
2753
  if (cmdBase === '/model') {
2598
2754
  const target = resolveMenuModelTarget.call(this, {
@@ -2816,8 +2972,9 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2816
2972
  };
2817
2973
  }
2818
2974
  if (cmdBase === '/observable') {
2819
- if (identity.role !== 'owner')
2975
+ if (identity.role !== 'owner' && subject.processRole !== 'daemon-service' && !subject.isDaemonOwner) {
2820
2976
  return { error: '观察者模式仅限 owner 开关', code: 'NO_PERMISSION' };
2977
+ }
2821
2978
  if (arg !== 'true' && arg !== 'false')
2822
2979
  return { error: `无效值: ${arg},可选: true / false`, code: 'INVALID_VALUE' };
2823
2980
  if (!evolagent)
@@ -2837,8 +2994,25 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2837
2994
  const gated = gateControlScope.call(this, { cmdBase, action, args, channel, fromControlChannel });
2838
2995
  if (gated)
2839
2996
  return gated;
2840
- const { session: authSession } = await this.loadMenuContext(channel, channelId);
2997
+ const loadedContext = await this.loadMenuContext(channel, channelId);
2998
+ // Menu callbacks can arrive from a topic while the channel's active
2999
+ // session still points at the main conversation. Prefer the explicitly
3000
+ // transported topic route for session-scoped actions and authorization.
3001
+ let authSession = loadedContext.session;
3002
+ if (transportReplyContext?.threadId && typeof this.sessionManager.getThreadSession === 'function') {
3003
+ try {
3004
+ authSession = await this.sessionManager.getThreadSession(channel, channelId, transportReplyContext.threadId) ?? authSession;
3005
+ }
3006
+ catch (error) {
3007
+ logger.debug(`[MenuHandler] topic session lookup failed for menu action: thread=${transportReplyContext.threadId}: ${error instanceof Error ? error.message : String(error)}`);
3008
+ }
3009
+ }
3010
+ const session = authSession;
3011
+ const evolagent = loadedContext.evolagent;
3012
+ if (cmdBase === '/baseagent-models')
3013
+ return { error: 'Baseagent catalog only supports menu.query', code: 'NOT_SUPPORTED' };
2841
3014
  const authIdentity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(authSession));
3015
+ const identity = authIdentity;
2842
3016
  const subject = buildMenuAuthSubject(this, {
2843
3017
  identity: authIdentity,
2844
3018
  session: authSession,
@@ -2850,7 +3024,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2850
3024
  fromControlChannel,
2851
3025
  subject: authSubject,
2852
3026
  });
2853
- if (cmdBase === '/system' && fromControlChannel && !subject.isDaemonOwner) {
3027
+ if (cmdBase === '/system' && fromControlChannel && !subject.isDaemonOwner && subject.processRole !== 'daemon-service') {
2854
3028
  return {
2855
3029
  error: formatAuthorizationDenial('This daemon-level operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
2856
3030
  code: 'NOT_ALLOWED',
@@ -2893,7 +3067,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2893
3067
  return menuResultFailure(error);
2894
3068
  }
2895
3069
  }
2896
- if (cmdBase === '/agent' && !fromControlChannel && !args?.aid) {
3070
+ if ((cmdBase === '/agent' || cmdBase === '/aid') && !args?.aid) {
2897
3071
  const selfAid = this.getOwningAgent?.(channel)?.aid;
2898
3072
  if (selfAid)
2899
3073
  args = { ...(args ?? {}), aid: selfAid };
@@ -2927,15 +3101,37 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2927
3101
  return await gatewaySyncEnv(args);
2928
3102
  return { error: `不支持 gateway action: ${action}`, code: 'NOT_SUPPORTED' };
2929
3103
  }
2930
- const { session, evolagent } = await this.loadMenuContext(channel, channelId);
2931
- const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
2932
- const isMenuAdmin = isManagementRole(identity.role);
3104
+ const isMenuAdmin = isManagementRole(authIdentity.role);
3105
+ // ── /aid avatar ──
3106
+ // The AID namespace is the only avatar mutation surface.
3107
+ if (cmdBase === '/aid') {
3108
+ if (action !== 'avatar')
3109
+ return { error: `不支持 aid action: ${action}`, code: 'NOT_SUPPORTED' };
3110
+ const result = await execAidAvatar(args);
3111
+ if (!('error' in result)) {
3112
+ const data = result.data;
3113
+ if (data?.avatar) {
3114
+ this.eventBus.publish({
3115
+ type: 'agent:avatar_updated',
3116
+ aid: data.aid,
3117
+ avatar: data.avatar,
3118
+ ...(data.sha256 !== undefined ? { sha256: data.sha256 } : {}),
3119
+ ...(data.mime !== undefined ? { mime: data.mime } : {}),
3120
+ ...(data.width !== undefined ? { width: data.width } : {}),
3121
+ ...(data.height !== undefined ? { height: data.height } : {}),
3122
+ ...(data.aspectRatio !== undefined ? { aspectRatio: data.aspectRatio } : {}),
3123
+ timestamp: Date.now(),
3124
+ });
3125
+ }
3126
+ }
3127
+ return result;
3128
+ }
2933
3129
  // ── /agent action ──
2934
3130
  // 控制 channel:验 evolcore.owners,可执行进程级(create/delete/enable/disable)+ 自管理(update/reload)。
2935
3131
  // agent channel:闸门已挡掉进程级 + 跨 agent,仅 update/reload 能到此;reload 允许 owner/admin,update 仅 owner。
2936
3132
  if (cmdBase === '/agent') {
2937
3133
  if (fromControlChannel) {
2938
- if (!subject.isDaemonOwner) {
3134
+ if (!subject.isDaemonOwner && subject.processRole !== 'daemon-service') {
2939
3135
  return {
2940
3136
  error: formatAuthorizationDenial('This Agent lifecycle operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
2941
3137
  code: 'NOT_ALLOWED',
@@ -3054,7 +3250,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3054
3250
  // ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
3055
3251
  if (cmdBase === '/trigger') {
3056
3252
  const role = identity.role;
3057
- const isAdmin = isManagementRole(role) || subject.isDaemonOwner;
3253
+ const isAdmin = isManagementRole(role) || subject.isDaemonOwner || subject.processRole === 'daemon-service';
3058
3254
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
3059
3255
  if (!triggerScheduler)
3060
3256
  return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
@@ -3135,7 +3331,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3135
3331
  if (action === 'enable' && trigger.execution.permissionMode === 'fullaccess') {
3136
3332
  if (!isFullAccessEnabled())
3137
3333
  return { error: 'fullaccess 功能未启用', code: 'NOT_SUPPORTED' };
3138
- if (!subject.isDaemonOwner)
3334
+ if (!subject.isDaemonOwner && subject.processRole !== 'daemon-service')
3139
3335
  return { error: '只有 daemon-owner 可以启用 fullaccess Trigger', code: 'FORBIDDEN' };
3140
3336
  if (subject.canApprovePersistentFullAccess === false) {
3141
3337
  return { error: '当前认证通道不能批准持久 fullaccess Trigger', code: 'FORBIDDEN' };
@@ -3513,7 +3709,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3513
3709
  }
3514
3710
  if (action === 'new') {
3515
3711
  const name = (args?.name ?? '').toString().trim();
3516
- return await this.delegateAsAction(action, name ? `/new ${name}` : '/new', channel, channelId, userId, { enrichSession: true, authSubject: subject });
3712
+ return await this.delegateAsAction(action, name ? `/new ${name}` : '/new', channel, channelId, userId, { enrichSession: true, authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
3517
3713
  }
3518
3714
  if (action === 'rename') {
3519
3715
  const newName = getRenameName(args);
@@ -3556,33 +3752,33 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3556
3752
  const target = (args?.target ?? '').toString().trim();
3557
3753
  if (!target)
3558
3754
  return { error: '缺少 args.target', code: 'MISSING_VALUE' };
3559
- return await this.delegateAsAction(action, `/del ${target}`, channel, channelId, userId, { authSubject: subject });
3755
+ return await this.delegateAsAction(action, `/del ${target}`, channel, channelId, userId, { authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
3560
3756
  }
3561
3757
  if (action === 'switch') {
3562
3758
  const target = (args?.target ?? '').toString().trim();
3563
3759
  if (!target)
3564
3760
  return { error: '缺少 args.target', code: 'MISSING_VALUE' };
3565
- return await this.delegateAsAction(action, `/s ${target}`, channel, channelId, userId, { enrichSession: true, authSubject: subject });
3761
+ return await this.delegateAsAction(action, `/s ${target}`, channel, channelId, userId, { enrichSession: true, authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
3566
3762
  }
3567
3763
  if (action === 'compact') {
3568
3764
  const need = this.requireSession(session);
3569
3765
  if (need)
3570
3766
  return need;
3571
- return await this.delegateAsAction(action, '/compact', channel, channelId, userId, { authSubject: subject });
3767
+ return await this.delegateAsAction(action, '/compact', channel, channelId, userId, { authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
3572
3768
  }
3573
3769
  if (action === 'fork') {
3574
3770
  const need = this.requireSession(session);
3575
3771
  if (need)
3576
3772
  return need;
3577
3773
  const name = (args?.name ?? '').toString().trim();
3578
- return await this.delegateAsAction(action, name ? `/fork ${name}` : '/fork', channel, channelId, userId, { enrichSession: true, authSubject: subject });
3774
+ return await this.delegateAsAction(action, name ? `/fork ${name}` : '/fork', channel, channelId, userId, { enrichSession: true, authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
3579
3775
  }
3580
3776
  return { error: `不支持的 session action: ${action}`, code: 'NOT_SUPPORTED' };
3581
3777
  }
3582
3778
  // ── /system 系列 ──
3583
3779
  if (cmdBase === '/system') {
3584
3780
  // D1 迁移:进程级鉴权统一查 daemon.json owners,替代各 action 内联的 identity.role 判断
3585
- if (!subject.isDaemonOwner) {
3781
+ if (!subject.isDaemonOwner && subject.processRole !== 'daemon-service') {
3586
3782
  return {
3587
3783
  error: formatAuthorizationDenial('This daemon-level operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
3588
3784
  code: 'NOT_ALLOWED',
@@ -3626,17 +3822,28 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3626
3822
  const migrationRequirement = inspectDataMigrationRequirement(resolvePaths().root);
3627
3823
  if (migrationRequirement.required) {
3628
3824
  return {
3629
- error: `待处理的用户数据迁移(${migrationRequirement.operationCount} 项)阻止重启。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
3825
+ error: `待处理的用户数据迁移(${migrationRequirement.operationCount} 项)阻止重启;本次仅检查,未停止当前服务。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
3630
3826
  code: 'DATA_MIGRATION_REQUIRED',
3631
3827
  data: { reasonCode: 'DATA_MIGRATION_REQUIRED', operationCount: migrationRequirement.operationCount },
3632
3828
  };
3633
3829
  }
3634
3830
  if (!suppressRealRestart) {
3831
+ // 这条路径同样以 daemon 自杀收尾,入口缺失必须在写 pending 和自杀之前拦下。
3832
+ const restartEntry = resolveCliEntry(getPackageRoot());
3833
+ if (!fs.existsSync(restartEntry.entry)) {
3834
+ return {
3835
+ error: `找不到重启入口:${restartEntry.entry}。请先执行 npm run build 重建产物。`,
3836
+ code: 'RESTART_ENTRY_MISSING',
3837
+ data: { reasonCode: 'RESTART_ENTRY_MISSING', entry: restartEntry.entry },
3838
+ };
3839
+ }
3635
3840
  const restartInfo = {
3636
3841
  channel,
3637
3842
  channelId,
3638
3843
  timestamp: Date.now(),
3844
+ ...(restartReplyContext?.threadId ? { threadId: restartReplyContext.threadId } : {}),
3639
3845
  ...(restartDelivery ? { delivery: restartDelivery } : {}),
3846
+ ...(restartReplyContext?.replyToMessageId ? { rootId: restartReplyContext.replyToMessageId } : {}),
3640
3847
  };
3641
3848
  const controlDir = daemonControlDir();
3642
3849
  fs.mkdirSync(controlDir, { recursive: true });
@@ -3645,23 +3852,41 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3645
3852
  // the version check and automatic upgrades before replacing the daemon.
3646
3853
  // Do not inherit managed-session credentials: otherwise cmdRestart can
3647
3854
  // delegate back to this daemon and recursively enqueue another restart.
3648
- spawnDetachedNode([path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart'], {
3855
+ //
3856
+ // `ec restart` owns the monitor liveness check, but it can itself die on
3857
+ // load (tsx + a broken source tree), and this path kills the daemon
3858
+ // afterwards — so confirm the CLI process survived before proceeding.
3859
+ const restartLaunch = await launchMonitorAndConfirm([...restartEntry.args, 'restart'], {
3649
3860
  EVOLCORE_HOME: resolvePaths().root,
3650
3861
  EVOLCORE_SESSION_ID: '',
3651
3862
  [AGENT_DELEGATION_TOKEN_ENV]: '',
3652
- });
3863
+ },
3864
+ // `ec restart` does not register itself; staying alive is the signal.
3865
+ () => false, pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid !== pid), 8_000);
3866
+ if (!restartLaunch.ok) {
3867
+ try {
3868
+ fs.rmSync(path.join(controlDir, 'restart-pending.json'), { force: true });
3869
+ }
3870
+ catch { }
3871
+ logger.error(`[System] Refusing menu restart: launcher did not survive (${restartLaunch.reason})`);
3872
+ for (const line of (restartLaunch.stderrTail ?? '').split(/\r?\n/).filter(Boolean).slice(-8)) {
3873
+ logger.error(`[System] | ${line}`);
3874
+ }
3875
+ return {
3876
+ error: `重启已中止:启动器未能运行(${restartLaunch.reason})。服务未停止,仍在运行当前版本。`,
3877
+ code: 'RESTART_LAUNCH_FAILED',
3878
+ data: { reasonCode: 'RESTART_LAUNCH_FAILED', reason: restartLaunch.reason },
3879
+ };
3880
+ }
3653
3881
  }
3654
3882
  else {
3655
3883
  logger.info('[System] Suppressed real menu restart in test runtime');
3656
3884
  }
3657
3885
  this.eventBus.publish({ type: 'system:restart', channel, channelId });
3658
- if (!suppressRealRestart) {
3659
- setTimeout(() => { process.kill(process.pid, 'SIGTERM'); }, 1000);
3660
- }
3661
3886
  return { data: { action: 'restart', success: true } };
3662
3887
  }
3663
3888
  if (action === 'check') {
3664
- const r = await this.delegateAsAction(action, '/check', channel, channelId, userId, { overrideIdentity, authSubject: subject });
3889
+ const r = await this.delegateAsAction(action, '/check', channel, channelId, userId, { overrideIdentity, authSubject: subject, threadId: transportReplyContext?.threadId, chatType: transportReplyContext?.delivery?.chatType, replyContext: transportReplyContext });
3665
3890
  // delegateAsAction 会把 structured 展开到 data 顶层,并保留 data.structured 兼容旧客户端。
3666
3891
  return r;
3667
3892
  }
@@ -3927,8 +4152,8 @@ function menuCommandToken(cmd) {
3927
4152
  }
3928
4153
  function isProcessLevelMenu(name, cmd) {
3929
4154
  const cmdToken = menuCommandToken(cmd);
3930
- return name === 'system' || name === 'agent' || name === 'gateway' || name === 'config'
3931
- || cmdToken === '/system' || cmdToken === '/agent' || cmdToken === '/gateway' || cmdToken === '/config';
4155
+ return name === 'system' || name === 'agent' || name === 'gateway' || name === 'config' || name === 'baseagent-models'
4156
+ || cmdToken === '/system' || cmdToken === '/agent' || cmdToken === '/gateway' || cmdToken === '/config' || cmdToken === '/baseagent-models';
3932
4157
  }
3933
4158
  function isProcessContextMenu(payload) {
3934
4159
  const isCatalog = payload?.type === 'menu.list' && payload?.args?.view === 'catalog';
@@ -3962,9 +4187,9 @@ async function execMenuForSystemControl(payload, context) {
3962
4187
  const isRoleRequest = name === 'role' || compatCmd === '/role';
3963
4188
  const isConnectRequest = name === 'connect' || compatCmd === '/connect';
3964
4189
  const requiresAgentTarget = isRoleRequest || isConnectRequest || [
3965
- 'observable', 'baseagent', 'model', 'effort', 'trigger', 'capability', 'group',
4190
+ 'observable', 'baseagent', 'model', 'effort', 'trigger', 'capability', 'group', 'aid',
3966
4191
  ].includes(name)
3967
- || ['/observable', '/baseagent', '/model', '/effort', '/trigger', '/capability', '/group'].includes(compatCmd ?? '')
4192
+ || ['/observable', '/baseagent', '/model', '/effort', '/trigger', '/capability', '/group', '/aid'].includes(compatCmd ?? '')
3968
4193
  || (!isProcessLevel && requestedContextKind !== undefined && requestedContextKind !== 'process');
3969
4194
  const target = isProcessLevel
3970
4195
  ? { channel: SYSTEM_CONTROL_CHANNEL }
@@ -3974,6 +4199,12 @@ async function execMenuForSystemControl(payload, context) {
3974
4199
  const targetChannel = target.channel;
3975
4200
  const targetAid = this.getOwningAgent?.(targetChannel)?.aid
3976
4201
  ?? this.getDaemonTriggerOwnerAid?.(targetChannel);
4202
+ if (name === 'aid' || compatCmd === '/aid') {
4203
+ const requestedAid = typeof payload?.args?.aid === 'string' ? payload.args.aid.trim().replace(/^@/, '') : '';
4204
+ if (requestedAid && targetAid && requestedAid !== targetAid) {
4205
+ return ecwebErr(id, name, 'INVALID_ARGUMENT', 'args.aid 必须与目标 Agent 一致');
4206
+ }
4207
+ }
3977
4208
  const trustedSubject = buildAuthSubject({
3978
4209
  selfAid: targetAid,
3979
4210
  actorId: context.actorAid,
@@ -3986,10 +4217,18 @@ async function execMenuForSystemControl(payload, context) {
3986
4217
  processOwners: context.owners,
3987
4218
  fromControlChannel: true,
3988
4219
  canApprovePersistentFullAccess: context.canApprovePersistentFullAccess,
4220
+ trustedProcessRole: context.source === 'ecweb' ? 'daemon-owner' : undefined,
3989
4221
  });
3990
4222
  const trustedIdentity = trustedSubject.identity;
3991
4223
  const cmd = name ? (menuCommandForName(name) ?? payload.cmd) : payload.cmd;
3992
4224
  try {
4225
+ if (menuCommandToken(cmd) === '/baseagent-models') {
4226
+ if (payload.type !== 'menu.query')
4227
+ return ecwebErr(id, name, 'NOT_SUPPORTED', 'Baseagent catalog only supports menu.query');
4228
+ if (payload.agent !== undefined)
4229
+ return ecwebErr(id, name, 'INVALID_ARGUMENT', 'Baseagent catalog does not accept an Agent target');
4230
+ hostCatalogArgs(payload.args);
4231
+ }
3993
4232
  switch (payload?.type) {
3994
4233
  case 'menu.list':
3995
4234
  if (payload?.args?.view === 'catalog') {
@@ -4093,6 +4332,7 @@ async function execMenuForSystemControl(payload, context) {
4093
4332
  }
4094
4333
  }
4095
4334
  catch (e) {
4335
+ logger.error(`[ECWebMenu] failed type=${payload?.type ?? '<none>'} name=${name ?? '<none>'} cmd=${cmd ?? '<none>'} agent=${payload?.agent ?? '<none>'} actor=${context.actorAid ?? '<none>'}: ${e?.stack || e?.message || String(e)}`);
4096
4336
  return menuFailure({ id, ...(name ? { name } : {}) }, normalizeMenuError(e));
4097
4337
  }
4098
4338
  }