evolcore 0.0.26 → 0.0.28

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 (177) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/baseagent.js +10 -5
  5. package/dist/agents/claude-runner.js +123 -22
  6. package/dist/agents/codex/app-server-client.js +70 -26
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex-runner.js +363 -421
  9. package/dist/agents/ecagent-runner.js +170 -25
  10. package/dist/agents/gemini-runner.js +6 -0
  11. package/dist/agents/runner-types.js +50 -2
  12. package/dist/aun/aid/agentmd.js +73 -31
  13. package/dist/aun/aid/client.js +1 -1
  14. package/dist/aun/aid/control-aid.js +180 -1
  15. package/dist/aun/aid/identity.js +3 -3
  16. package/dist/aun/group/history.js +303 -0
  17. package/dist/aun/group/identity.js +43 -0
  18. package/dist/aun/group/index.js +5 -0
  19. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  20. package/dist/aun/{menu-client.js → menu/client.js} +13 -7
  21. package/dist/aun/menu/index.js +2 -0
  22. package/dist/aun/menu/transport.js +49 -0
  23. package/dist/aun/msg/index.js +2 -1
  24. package/dist/aun/msg/managed-operation.js +84 -18
  25. package/dist/aun/msg/mention-schema.js +51 -0
  26. package/dist/aun/msg/p2p.js +13 -8
  27. package/dist/aun/msg/upload.js +43 -88
  28. package/dist/aun/outbox.js +29 -7
  29. package/dist/aun/rpc/client.js +1 -0
  30. package/dist/aun/storage/client.js +39 -50
  31. package/dist/channels/aun.js +544 -239
  32. package/dist/channels/daemon.js +10 -6
  33. package/dist/cli/agent-command.js +108 -6
  34. package/dist/cli/agent-create.js +515 -2
  35. package/dist/cli/agent.js +109 -8
  36. package/dist/cli/aun-commands.js +191 -42
  37. package/dist/cli/config.js +8 -5
  38. package/dist/cli/contact.js +2 -2
  39. package/dist/cli/ctl-command.js +2 -2
  40. package/dist/cli/daemon-commands.js +111 -162
  41. package/dist/cli/diag.js +3 -3
  42. package/dist/cli/fs-command.js +3 -3
  43. package/dist/cli/handoff-command.js +6 -6
  44. package/dist/cli/index.js +8 -2
  45. package/dist/cli/init.js +34 -1
  46. package/dist/cli/managed-command-guard.js +2 -1
  47. package/dist/cli/menu-client.js +13 -5
  48. package/dist/cli/model.js +3 -3
  49. package/dist/cli/queue-command.js +2 -2
  50. package/dist/cli/response.js +2 -2
  51. package/dist/cli/restart-monitor.js +81 -19
  52. package/dist/cli/task-command.js +59 -0
  53. package/dist/cli/task-context.js +97 -49
  54. package/dist/cli/trigger-command.js +20 -10
  55. package/dist/config/builtin-roles.js +10 -0
  56. package/dist/config/config-field-policy.js +11 -0
  57. package/dist/config/config-manager.js +32 -4
  58. package/dist/config/config-operation-service.js +20 -5
  59. package/dist/config/contact-book-store.js +1 -1
  60. package/dist/config/contact-book.js +1 -1
  61. package/dist/config/contact-operation-service.js +1 -1
  62. package/dist/config/peer-role-resolver.js +1 -1
  63. package/dist/config/resolved-config-op.js +3 -0
  64. package/dist/config-store.js +1 -1
  65. package/dist/core/agent-application-service.js +5 -0
  66. package/dist/core/audit/event-key.js +17 -2
  67. package/dist/core/audit/log-integrity.js +34 -8
  68. package/dist/core/auth/agent-delegation.js +48 -14
  69. package/dist/core/auth/auth-gateway.js +2 -0
  70. package/dist/core/auth/authorization-audit.js +34 -38
  71. package/dist/core/auth/operation-authorizer.js +67 -5
  72. package/dist/core/auth/operation-catalog.js +110 -0
  73. package/dist/core/auth/remote-access-store.js +115 -9
  74. package/dist/core/bootstrap-messages.js +66 -7
  75. package/dist/core/bootstrap-service.js +110 -19
  76. package/dist/core/command/agent-control.js +88 -7
  77. package/dist/core/command/agent-storage-menu.js +443 -0
  78. package/dist/core/command/cli-intent-parser.js +19 -0
  79. package/dist/core/command/command-handler.js +105 -40
  80. package/dist/core/command/data-root-resolver.js +273 -0
  81. package/dist/core/command/diag-service.js +292 -27
  82. package/dist/core/command/group-menu.js +7 -2
  83. package/dist/core/command/menu-catalog.js +34 -4
  84. package/dist/core/command/menu-handler.js +364 -96
  85. package/dist/core/command/menu-protocol.js +43 -6
  86. package/dist/core/command/role-menu.js +2 -2
  87. package/dist/core/command/slash-handler.js +61 -37
  88. package/dist/core/event-catalog.js +4 -0
  89. package/dist/core/evolagent-registry.js +28 -2
  90. package/dist/core/group/invite-service.js +924 -0
  91. package/dist/core/interaction-router.js +22 -8
  92. package/dist/core/message/im-renderer.js +94 -16
  93. package/dist/core/message/message-bridge.js +140 -13
  94. package/dist/core/message/message-log.js +5 -3
  95. package/dist/core/message/message-queue.js +73 -41
  96. package/dist/core/message/response-engine.js +729 -118
  97. package/dist/core/permission/approval-gateway.js +36 -3
  98. package/dist/core/permission/ec-command-parser.js +76 -36
  99. package/dist/core/permission/protected-paths.js +134 -16
  100. package/dist/core/permission/sandbox-runtime.js +72 -3
  101. package/dist/core/permission/tool-error-code.js +15 -0
  102. package/dist/core/permission/tool-policy.js +8 -8
  103. package/dist/core/relation/peer-identity.js +57 -6
  104. package/dist/core/relation/shared-data.js +106 -0
  105. package/dist/core/role/runtime-policy.js +64 -0
  106. package/dist/core/runtime-lock.js +10 -26
  107. package/dist/core/session/runner-task-history.js +170 -0
  108. package/dist/core/session/session-manager.js +38 -6
  109. package/dist/core/session/session-mapper.js +41 -0
  110. package/dist/core/session/session-turn-coordinator.js +37 -7
  111. package/dist/core/session/session-turns.js +48 -7
  112. package/dist/core/task/task-recovery-service.js +176 -0
  113. package/dist/core/task/task-resume-operation.js +32 -0
  114. package/dist/eck/group-rules-sync.js +5 -3
  115. package/dist/eck/kit-renderer.js +1 -0
  116. package/dist/index.js +322 -207
  117. package/dist/ipc.js +141 -15
  118. package/dist/paths.js +17 -4
  119. package/dist/product.js +3 -0
  120. package/dist/trigger/event-source.js +19 -0
  121. package/dist/trigger/feedback.js +233 -40
  122. package/dist/trigger/parser.js +55 -5
  123. package/dist/trigger/patch.js +9 -0
  124. package/dist/trigger/scheduler.js +30 -4
  125. package/dist/trigger/validation.js +20 -3
  126. package/dist/utils/atomic-write.js +98 -0
  127. package/dist/utils/cross-platform.js +87 -16
  128. package/dist/utils/ecweb-utils.js +1 -0
  129. package/dist/utils/error-utils.js +40 -2
  130. package/dist/utils/logger.js +23 -13
  131. package/dist/utils/npm-ops.js +119 -4
  132. package/dist/utils/package-version-info.js +244 -0
  133. package/dist/utils/tool-summary.js +59 -3
  134. package/dist/utils/welcome.js +174 -1
  135. package/dist/utils/windows-shell-trust.js +37 -0
  136. package/ecagent/dist/harness/agent-harness.js +3 -0
  137. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  138. package/kits/docs/INDEX.md +0 -6
  139. package/kits/docs/channels/aun.md +23 -2
  140. package/kits/docs/channels/feishu.md +1 -1
  141. package/kits/docs/context-assembly.md +29 -17
  142. package/kits/docs/evolcore/INDEX.md +8 -3
  143. package/kits/docs/evolcore/agent-create.md +39 -7
  144. package/kits/docs/evolcore/agent.md +33 -1
  145. package/kits/docs/evolcore/aid.md +9 -1
  146. package/kits/docs/evolcore/config.md +40 -1
  147. package/kits/docs/evolcore/group-collaboration.md +1 -1
  148. package/kits/docs/evolcore/group.md +117 -2
  149. package/kits/docs/evolcore/menu-client.md +16 -0
  150. package/kits/docs/evolcore/msg.md +3 -0
  151. package/kits/docs/evolcore/storage.md +3 -0
  152. package/kits/docs/evolcore/task.md +14 -0
  153. package/kits/docs/evolcore/trigger.md +51 -3
  154. package/kits/docs/venues/group.md +5 -0
  155. package/kits/eck_manifest.json +27 -0
  156. package/kits/rules/01-overview.md +11 -7
  157. package/kits/rules/04-relation.md +2 -1
  158. package/kits/rules/05-venue.md +1 -1
  159. package/kits/rules/06-channel.md +6 -4
  160. package/kits/schemas/agent-config.schema.12.json +25 -1
  161. package/kits/schemas/daemon.schema.6.json +2 -2
  162. package/kits/schemas/defaults.schema.5.json +1 -1
  163. package/kits/schemas/relation-config.schema.1.json +1 -1
  164. package/kits/schemas/relation-config.schema.5.json +1 -1
  165. package/kits/schemas/role-config.schema.1.json +14 -0
  166. package/kits/templates/roles/member.json +5 -0
  167. package/kits/templates/roles/visitor.json +5 -0
  168. package/kits/templates/system-fragments/baseagent.md +1 -1
  169. package/kits/templates/system-fragments/bootstrap.md +26 -9
  170. package/kits/templates/system-fragments/channel.md +8 -44
  171. package/kits/templates/system-fragments/commands.md +10 -2
  172. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  173. package/kits/templates/system-fragments/reply.md +56 -0
  174. package/package.json +7 -3
  175. package/dist/aun/group-identity.js +0 -9
  176. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  177. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -1,3 +1,4 @@
1
+ import { getBaseagentSnapshot, loadedBaseagentIds } from '../../agents/baseagent-availability.js';
1
2
  import { normalizePeer, writeScope } from '../model/config-scope.js';
2
3
  import { formatPeerKey } from '../relation/peer-identity.js';
3
4
  import { modelMatches, getCatalog, getBaseagentCatalog, resolveCatalogModel, assertCatalogSelectable, TRANSPORT_EFFORTS } from '../model/model-catalog.js';
@@ -12,8 +13,9 @@ import os from 'os';
12
13
  import crypto from 'crypto';
13
14
  import { CronExpressionParser } from 'cron-parser';
14
15
  import { parseDuration } from '../../trigger/parser.js';
15
- import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions, resolveGlobalPkg } from '../../utils/npm-ops.js';
16
- import { commandExists, resolveCliEntry, launchMonitorAndConfirm } from '../../utils/cross-platform.js';
16
+ import { isLinkedInstall, resolveGlobalPkg } from '../../utils/npm-ops.js';
17
+ import { refreshManagedPackageVersions, VERSION_INFO_PACKAGES } from '../../utils/package-version-info.js';
18
+ import { resolveCliEntry, launchMonitorAndConfirm } from '../../utils/cross-platform.js';
17
19
  import { cachedCliVersion } from '../../utils/cross-platform.js';
18
20
  import { scanInstances } from '../../utils/instance-registry.js';
19
21
  import { isFullAccessEnabled, loadDefaults, loadDaemonConfig } from '../../config-store.js';
@@ -29,6 +31,7 @@ import { auditCommandAuthorization, auditFullAccessEvent, hashArgv } from '../au
29
31
  import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
30
32
  import { authorizationDenialData, formatAuthorizationDenial } from '../auth/authorization-denial.js';
31
33
  import { evaluateRoleOperationCapability } from '../auth/operation-authorizer.js';
34
+ import { isUnsupportedBaseagentConfigField } from '../../config/config-field-policy.js';
32
35
  import { splitConfigBatchGetArgv } from '../../cli/cli-argv.js';
33
36
  import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
34
37
  import { PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
@@ -47,6 +50,9 @@ import { handleConnectMenu, connectMenuOperation } from './connect-menu.js';
47
50
  import { groupMenuAuthorizationArgs, groupMenuIntent, handleGroupMenu, } from './group-menu.js';
48
51
  import { getMenuCatalog, execMenuValueBatch, resolveExplicitMenuExecutionContext, validateMenuCatalogArgs, validateMenuValueBatchArgs } from './menu-catalog.js';
49
52
  import { probeSessionActivity } from './slash-gate.js';
53
+ import { dataRootFailure, dataRootResolver } from './data-root-resolver.js';
54
+ import { agentStorageMenuService, isAgentStorageMenuRequest } from './agent-storage-menu.js';
55
+ import { isArchivedTopicLable, normalizeTopicLable, resolveTopicArchiveFilter, } from '../session/session-mapper.js';
50
56
  /**
51
57
  * 获取 baseagent CLI 的版本号(claude/gemini/codex)。
52
58
  * 失败返回 null(命令不存在或执行失败)。
@@ -180,6 +186,8 @@ function getRenameName(args) {
180
186
  }
181
187
  function buildSessionPayload(session, name) {
182
188
  const payload = { id: session.id, name };
189
+ if (session.lable)
190
+ payload.lable = session.lable;
183
191
  if (session.agentSessionId)
184
192
  payload.agentSessionId = session.agentSessionId;
185
193
  return payload;
@@ -413,7 +421,10 @@ function hostCatalogArgs(args = {}) {
413
421
  }
414
422
  if (args.refresh !== undefined && typeof args.refresh !== 'boolean')
415
423
  invalid('refresh must be a boolean');
416
- return { baseagent: menuStringArg(args, 'baseagent') ?? menuStringArg(args, 'ba') ?? 'claude', refresh: args.refresh === true };
424
+ const baseagent = menuStringArg(args, 'baseagent') ?? menuStringArg(args, 'ba');
425
+ if (!baseagent)
426
+ return invalid('baseagent is required (claude / codex / ecagent / gemini)');
427
+ return { baseagent, refresh: args.refresh === true };
417
428
  }
418
429
  const LEGACY_SESSION_CONFIG_ARG_KEYS = [
419
430
  'session',
@@ -851,7 +862,7 @@ export function isProcessLevelOwner(peerId, owners) {
851
862
  // ── 控制面双轨鉴权(见 docs/.../2026-06-10-control-channel-auth-design.md)──
852
863
  // 白名单而非黑名单:默认安全,新增进程级 action 需显式加入。
853
864
  /** /agent 的进程级 action:仅控制 channel 可执行。 */
854
- export const PROCESS_LEVEL_AGENT_ACTIONS = new Set(['create', 'delete', 'enable', 'disable']);
865
+ export const PROCESS_LEVEL_AGENT_ACTIONS = new Set(['create', 'initialize', 'delete', 'enable', 'disable']);
855
866
  /** /agent 的「本 agent 自管理」action:agent channel 仅 owner/admin 可对自身 aid 执行;update 另行收紧为 owner。 */
856
867
  export const SELF_MANAGE_AGENT_ACTIONS = new Set(['update', 'reload']);
857
868
  /** 判断 (cmdBase, action) 是否为进程级操作(仅控制 channel 可执行)。
@@ -898,6 +909,10 @@ function gateControlScope(opts) {
898
909
  return null;
899
910
  }
900
911
  function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel = false) {
912
+ if (cmdBase === '/agent-data-root' || cmdBase === '/relation-data-root')
913
+ return null;
914
+ if (cmdBase === '/file' && args?.rootId !== undefined)
915
+ return null;
901
916
  const payload = { ...(args ?? {}), ...(value !== undefined ? { value } : {}) };
902
917
  const intent = (operation, scope = 'relation', extra) => ({
903
918
  operation,
@@ -909,6 +924,9 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
909
924
  if (verb === 'query') {
910
925
  if (cmdBase === '/baseagent-models')
911
926
  return intent('baseagent.models.list', 'process');
927
+ if (cmdBase === '/baseagents') {
928
+ return intent(fromControlChannel ? 'system.status' : 'agent.baseagent.list', fromControlChannel ? 'process' : 'agent');
929
+ }
912
930
  if (cmdBase === '/pwd')
913
931
  return intent('project.current', 'relation');
914
932
  if (cmdBase === '/session' || cmdBase === '/s')
@@ -1019,10 +1037,15 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
1019
1037
  return intent('system.paircode', 'process', { action });
1020
1038
  }
1021
1039
  if (cmdBase === '/agent') {
1040
+ if (action === 'update' && args?.patch && Object.hasOwn(args.patch, 'avatar')) {
1041
+ return intent('ec.aid.avatar', 'agent', { action });
1042
+ }
1022
1043
  if (action === 'reload')
1023
1044
  return intent('agent.reload', fromControlChannel ? 'control' : 'agent', { action });
1024
1045
  if (action === 'create')
1025
1046
  return intent('agent.create', 'control', { action });
1047
+ if (action === 'initialize')
1048
+ return intent('agent.initialize', 'control', { action });
1026
1049
  if (action === 'delete')
1027
1050
  return intent('agent.delete', 'control', { action });
1028
1051
  if (action === 'enable')
@@ -1063,6 +1086,7 @@ function buildMenuAuthSubject(owner, params) {
1063
1086
  if (params.subject) {
1064
1087
  return {
1065
1088
  ...params.subject,
1089
+ fromControlChannel: params.fromControlChannel || params.subject.fromControlChannel,
1066
1090
  ...(params.requestId && !params.subject.requestId ? { requestId: params.requestId } : {}),
1067
1091
  ...(params.session?.id && !params.subject.sessionId ? { sessionId: params.session.id } : {}),
1068
1092
  };
@@ -1511,9 +1535,27 @@ export function getMenuItems(role, chatType = 'private', scope = 'agent', authSu
1511
1535
  });
1512
1536
  return items;
1513
1537
  }
1538
+ /** All Menu reads and writes must resolve the same transport-scoped topic. */
1539
+ async function loadMenuTopicSession(sessionManager, channel, channelId, threadId) {
1540
+ let session;
1541
+ try {
1542
+ if (typeof sessionManager.getThreadSession !== 'function')
1543
+ throw new Error('topic lookup unavailable');
1544
+ session = await sessionManager.getThreadSession(channel, channelId, threadId);
1545
+ }
1546
+ catch (error) {
1547
+ logger.debug(`[MenuHandler] topic session lookup failed: thread=${threadId}: ${error instanceof Error ? error.message : String(error)}`);
1548
+ throw { code: 'TOPIC_SESSION_LOOKUP_FAILED', message: '无法读取话题会话,拒绝回退到主会话' };
1549
+ }
1550
+ if (!session)
1551
+ throw { code: 'TOPIC_SESSION_NOT_FOUND', message: '话题会话不存在,拒绝回退到主会话' };
1552
+ return session;
1553
+ }
1514
1554
  /** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
1515
- export async function getSubMenuItems(cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel = false, authSubject, source = 'menu') {
1516
- const session = await this.sessionManager.getActiveSession(channel, channelId);
1555
+ export async function getSubMenuItems(cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel = false, authSubject, source = 'menu', transportReplyContext) {
1556
+ const session = transportReplyContext?.threadId
1557
+ ? await loadMenuTopicSession(this.sessionManager, channel, channelId, transportReplyContext.threadId)
1558
+ : await this.sessionManager.getActiveSession(channel, channelId);
1517
1559
  const subject = buildMenuAuthSubject(this, {
1518
1560
  identity: overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session)),
1519
1561
  session,
@@ -1617,19 +1659,23 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1617
1659
  : await execAgentOptions(args);
1618
1660
  if ('error' in res)
1619
1661
  throw { code: res.code, message: res.error };
1620
- return res.data.agents.map(ag => ({ value: ag.aid, label: ag.name || ag.aid, desc: ag.status }));
1662
+ return res.data.agents.map(ag => ({
1663
+ value: ag.aid, label: ag.name || ag.aid, desc: ag.status, lifecycle: ag.lifecycle ?? null,
1664
+ }));
1621
1665
  }
1622
1666
  // agent channel:直接读取自身,避免先枚举全部 Agent 再过滤。
1623
1667
  const selfAid = this.getOwningAgent?.(channel)?.aid;
1624
1668
  if (!selfAid)
1625
1669
  throw { code: 'FORBIDDEN', message: '当前 channel 无绑定 agent' };
1626
1670
  const res = this.agentApplicationService
1627
- ? await execAgentQuery({ aid: selfAid }, this.agentApplicationService)
1628
- : await execAgentQuery({ aid: selfAid });
1671
+ ? await execAgentQuery({ aid: selfAid }, this.agentApplicationService, subject)
1672
+ : await execAgentQuery({ aid: selfAid }, undefined, subject);
1629
1673
  if ('error' in res)
1630
1674
  throw { code: res.code, message: res.error };
1631
1675
  const ag = res.data;
1632
- return [{ value: ag.aid, label: ag.identity?.name || ag.name || ag.aid, desc: ag.status }];
1676
+ return [{
1677
+ value: ag.aid, label: ag.identity?.name || ag.name || ag.aid, desc: ag.status, lifecycle: ag.lifecycle ?? null,
1678
+ }];
1633
1679
  }
1634
1680
  if (cmd === '/capability') {
1635
1681
  const type = args?.type;
@@ -1685,12 +1731,33 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1685
1731
  preview: turn.userContent,
1686
1732
  turn: turn.index,
1687
1733
  assistantMessageId: turn.assistantUuid,
1734
+ assistantMessageIds: turn.assistantUuids,
1688
1735
  userMessageId: turn.userUuid,
1736
+ userTextHash: turn.userTextHash,
1737
+ assistantTextHashes: turn.assistantTextHashes,
1689
1738
  }));
1690
1739
  }
1691
- const sessions = await this.sessionManager.listSessions(channel, channelId);
1740
+ const hasLable = args && (Object.hasOwn(args, 'lable') || Object.hasOwn(args, 'label'));
1741
+ let lable;
1742
+ if (hasLable) {
1743
+ try {
1744
+ lable = normalizeTopicLable(Object.hasOwn(args, 'lable') ? args.lable : args.label);
1745
+ }
1746
+ catch (error) {
1747
+ throw { code: 'INVALID_ARGS', message: error.message };
1748
+ }
1749
+ }
1750
+ let archiveFilter;
1751
+ try {
1752
+ const archived = args?.archived === undefined && isArchivedTopicLable(lable) ? true : args?.archived;
1753
+ archiveFilter = resolveTopicArchiveFilter(archived);
1754
+ }
1755
+ catch (error) {
1756
+ throw { code: 'INVALID_ARGS', message: error.message };
1757
+ }
1758
+ const sessions = await this.sessionManager.listSessions(channel, channelId, { archiveFilter });
1692
1759
  return sessions
1693
- .filter((s) => !!s.threadId)
1760
+ .filter((s) => !!s.threadId && (!hasLable || s.lable === lable))
1694
1761
  .map((s) => this.buildTopicMenuItem(s));
1695
1762
  }
1696
1763
  if (cmd === '/s' || cmd === '/session' || cmd === '/del') {
@@ -1735,7 +1802,12 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1735
1802
  const available = requestedAgent?.name
1736
1803
  ? (this.getAvailableBaseagentsForOwner?.(requestedAgent.name) ?? [])
1737
1804
  : this.getAvailableBaseagents(channel);
1738
- return available.map((name) => ({ value: name, label: name, selected: name === currentAgent }));
1805
+ const inventory = await getBaseagentSnapshot({
1806
+ config: { active_baseagent: currentAgent }, loaded: available, probe: false,
1807
+ });
1808
+ return inventory.baseagents
1809
+ .filter(item => item.loaded === true)
1810
+ .map(item => ({ value: item.id, label: item.id, selected: item.id === currentAgent }));
1739
1811
  }
1740
1812
  if (cmd === '/model') {
1741
1813
  const target = resolveMenuModelTarget.call(this, {
@@ -1894,7 +1966,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1894
1966
  // 所有方法返回 { data } 或 { error, code? }。code 是结构化错误码(NO_ACTIVE_SESSION 等),
1895
1967
  // 客户端可据此决定降级策略。message-bridge 把 code 透传到 menu.response。
1896
1968
  /** menu.query — 查询当前值。 */
1897
- export async function execMenuQuery(cmd, channel, channelId, userId, args, explicitChatType, fromControlChannel = false, overrideIdentity, authSubject, source = 'menu') {
1969
+ export async function execMenuQuery(cmd, channel, channelId, userId, args, explicitChatType, fromControlChannel = false, overrideIdentity, authSubject, source = 'menu', transportReplyContext) {
1898
1970
  const cmdBase = cmd.trim().split(' ')[0];
1899
1971
  if (!cmdBase)
1900
1972
  return { error: '缺少命令', code: 'MISSING_CMD' };
@@ -1905,7 +1977,17 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1905
1977
  const gated = gateControlScope.call(this, { cmdBase, args, channel, fromControlChannel });
1906
1978
  if (gated)
1907
1979
  return gated;
1908
- const { session, evolagent } = await this.loadMenuContext(channel, channelId);
1980
+ const loadedContext = await this.loadMenuContext(channel, channelId);
1981
+ const evolagent = loadedContext.evolagent;
1982
+ let session = loadedContext.session;
1983
+ if (transportReplyContext?.threadId) {
1984
+ try {
1985
+ session = await loadMenuTopicSession(this.sessionManager, channel, channelId, transportReplyContext.threadId);
1986
+ }
1987
+ catch (error) {
1988
+ return menuResultFailure(error);
1989
+ }
1990
+ }
1909
1991
  const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1910
1992
  const subject = buildMenuAuthSubject(this, {
1911
1993
  identity,
@@ -1917,6 +1999,15 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1917
1999
  fromControlChannel,
1918
2000
  subject: authSubject,
1919
2001
  });
2002
+ const storageRequest = { type: 'menu.query', cmd, args };
2003
+ if (isAgentStorageMenuRequest(storageRequest)) {
2004
+ try {
2005
+ return { data: await agentStorageMenuService.execute(storageRequest, subject, this.agentStorageClientResolver) };
2006
+ }
2007
+ catch (error) {
2008
+ return dataRootFailure(error);
2009
+ }
2010
+ }
1920
2011
  if (isRequesterRoleQuery) {
1921
2012
  if (!subject.allowAccess) {
1922
2013
  return { error: `Role ${subject.role} is not allowed to access this agent`, code: 'ROLE_ACCESS_DENIED' };
@@ -1985,6 +2076,28 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1985
2076
  });
1986
2077
  if (authDenied)
1987
2078
  return authDenied;
2079
+ if (cmdBase === '/agent-data-root') {
2080
+ if (args && Object.keys(args).length > 0) {
2081
+ return { error: 'agent-data-root does not accept args', code: 'INVALID_ARGUMENT' };
2082
+ }
2083
+ try {
2084
+ return { data: dataRootResolver.issueAgentRoot(subject) };
2085
+ }
2086
+ catch (error) {
2087
+ return dataRootFailure(error);
2088
+ }
2089
+ }
2090
+ if (cmdBase === '/relation-data-root') {
2091
+ if (args && Object.keys(args).some(key => key !== 'peerAid')) {
2092
+ return { error: 'relation-data-root only accepts args.peerAid', code: 'INVALID_ARGUMENT' };
2093
+ }
2094
+ try {
2095
+ return { data: dataRootResolver.issueRelationRoot(subject, args?.peerAid) };
2096
+ }
2097
+ catch (error) {
2098
+ return dataRootFailure(error);
2099
+ }
2100
+ }
1988
2101
  if (cmdBase === '/baseagent-models') {
1989
2102
  try {
1990
2103
  const query = hostCatalogArgs(args);
@@ -2003,8 +2116,8 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2003
2116
  if (fromControlChannel) {
2004
2117
  if (args?.aid) {
2005
2118
  return this.agentApplicationService
2006
- ? await execAgentQuery(args, this.agentApplicationService)
2007
- : await execAgentQuery(args);
2119
+ ? await execAgentQuery(args, this.agentApplicationService, subject)
2120
+ : await execAgentQuery(args, undefined, subject);
2008
2121
  }
2009
2122
  return this.agentApplicationService
2010
2123
  ? await execAgentOptions(args, this.agentApplicationService)
@@ -2014,8 +2127,8 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2014
2127
  if (!selfAid)
2015
2128
  return { error: '当前 channel 无绑定 agent', code: 'FORBIDDEN' };
2016
2129
  return this.agentApplicationService
2017
- ? await execAgentQuery({ ...(args ?? {}), aid: selfAid }, this.agentApplicationService)
2018
- : await execAgentQuery({ ...(args ?? {}), aid: selfAid });
2130
+ ? await execAgentQuery({ ...(args ?? {}), aid: selfAid }, this.agentApplicationService, subject)
2131
+ : await execAgentQuery({ ...(args ?? {}), aid: selfAid }, undefined, subject);
2019
2132
  }
2020
2133
  // ── /gateway 查询(只读,列出全部作用域的网关配置;apiKey 已掩码) ──
2021
2134
  if (cmdBase === '/gateway') {
@@ -2095,6 +2208,9 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2095
2208
  if (!session) {
2096
2209
  return { data: { status: 'no-session' } };
2097
2210
  }
2211
+ if (args?.view === 'subtasks') {
2212
+ return { data: { scope: 'retained-history', tasks: this.sessionManager.getRunnerTaskHistory(session.id) } };
2213
+ }
2098
2214
  const sessionKey = this.getQueueKey(session, channel, channelId);
2099
2215
  const sessionAgent = this.getAgent(channel, session.baseagent);
2100
2216
  const isProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
@@ -2117,6 +2233,8 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2117
2233
  status: isProcessing ? 'processing' : 'idle',
2118
2234
  createdAt: session.createdAt,
2119
2235
  updatedAt: session.updatedAt,
2236
+ subtasks: this.sessionManager.getRunnerTaskHistory(session.id).length,
2237
+ subtasksScope: 'retained-history',
2120
2238
  };
2121
2239
  if (processingDuration !== undefined)
2122
2240
  data.processingDuration = processingDuration;
@@ -2132,6 +2250,21 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2132
2250
  data.lastError = { type: health.lastErrorType || 'unknown', message: health.lastError.substring(0, 100) };
2133
2251
  return { data };
2134
2252
  }
2253
+ if (cmdBase === '/baseagents') {
2254
+ if (args && Object.keys(args).length > 0) {
2255
+ return { error: 'baseagents does not accept query arguments', code: 'INVALID_ARGUMENT' };
2256
+ }
2257
+ if (fromControlChannel) {
2258
+ return { data: await getBaseagentSnapshot({ loaded: loadedBaseagentIds(this.agentMap.keys()) }) };
2259
+ }
2260
+ if (!evolagent)
2261
+ return { error: '当前 channel 无绑定 agent', code: 'FORBIDDEN' };
2262
+ return { data: await getBaseagentSnapshot({
2263
+ config: evolagent.config ?? {},
2264
+ loaded: this.getAvailableBaseagents(channel),
2265
+ probe: false,
2266
+ }) };
2267
+ }
2135
2268
  if (cmdBase === '/topic') {
2136
2269
  const target = (args?.target ?? '').toString().trim();
2137
2270
  if (!target)
@@ -2139,6 +2272,9 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2139
2272
  const topic = await this.sessionManager.getThreadSession(channel, channelId, target);
2140
2273
  if (!topic)
2141
2274
  return { error: '话题不存在', code: 'NOT_FOUND' };
2275
+ if (args?.view === 'subtasks') {
2276
+ return { data: { scope: 'retained-history', tasks: this.sessionManager.getRunnerTaskHistory(topic.id) } };
2277
+ }
2142
2278
  const sessionKey = this.getQueueKey(topic, channel, channelId);
2143
2279
  const sessionAgent = this.getAgent(channel, topic.baseagent);
2144
2280
  const isProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
@@ -2157,10 +2293,13 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2157
2293
  const data = {
2158
2294
  threadId: topic.threadId,
2159
2295
  name: topic.name || null,
2296
+ lable: topic.lable || null,
2160
2297
  agentSessionId: topic.agentSessionId || null,
2161
2298
  status: isProcessing ? 'processing' : 'idle',
2162
2299
  createdAt: topic.createdAt,
2163
2300
  updatedAt: topic.updatedAt,
2301
+ subtasks: this.sessionManager.getRunnerTaskHistory(topic.id).length,
2302
+ subtasksScope: 'retained-history',
2164
2303
  };
2165
2304
  if (processingDuration !== undefined)
2166
2305
  data.processingDuration = processingDuration;
@@ -2436,24 +2575,41 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2436
2575
  if (ecwebPkg?.version)
2437
2576
  data.ecwebVersion = ecwebPkg.version;
2438
2577
  // 收集主机级可用的所有 baseagent 类型(检测已安装的 CLI + 版本)
2439
- const baseagents = [];
2440
- for (const cmd of ['claude', 'gemini', 'codex']) {
2441
- if (commandExists(cmd)) {
2442
- baseagents.push({ name: cmd, version: getBaseagentVersion(cmd) });
2443
- }
2444
- }
2445
- data.baseagents = baseagents;
2578
+ const inventory = await getBaseagentSnapshot({
2579
+ loaded: loadedBaseagentIds(this.agentMap.keys()),
2580
+ });
2581
+ data.baseagents = inventory.baseagents.map(item => ({
2582
+ ...item, name: item.id,
2583
+ version: item.version ?? (item.detected && item.id !== 'ecagent' ? getBaseagentVersion(item.id) : null),
2584
+ }));
2446
2585
  return { data };
2447
2586
  }
2448
2587
  // ── name=file:文件元信息(§5.1) ──
2449
2588
  // operation 权限已在入口统一判定;项目外文件仍仅 owner 可取。
2450
2589
  if (cmdBase === '/file') {
2451
- const role = identity.role;
2452
- const projectPath = session?.projectPath ?? evolagent?.projectPath ?? this.getOwningAgent?.(channel)?.projectPath;
2453
- const resolved = resolveMenuFilePath(args?.path, projectPath, role, 'file');
2454
- if ('error' in resolved)
2455
- return resolved;
2456
- const { realPath, stat } = resolved;
2590
+ let realPath;
2591
+ let stat;
2592
+ let responsePath = (args?.path ?? '').toString();
2593
+ let dataRoot;
2594
+ if (args?.rootId !== undefined) {
2595
+ try {
2596
+ const resolved = dataRootResolver.resolvePath(args.rootId, subject, args?.path, 'file');
2597
+ ({ realPath, stat } = resolved);
2598
+ responsePath = resolved.relativePath;
2599
+ dataRoot = resolved;
2600
+ }
2601
+ catch (error) {
2602
+ return dataRootFailure(error);
2603
+ }
2604
+ }
2605
+ else {
2606
+ const role = identity.role;
2607
+ const projectPath = session?.projectPath ?? evolagent?.projectPath ?? this.getOwningAgent?.(channel)?.projectPath;
2608
+ const resolved = resolveMenuFilePath(args?.path, projectPath, role, 'file');
2609
+ if ('error' in resolved)
2610
+ return resolved;
2611
+ ({ realPath, stat } = resolved);
2612
+ }
2457
2613
  // sha256 仅对 ≤ 2 MB 文件计算,超过返回 null,客户端降级到 size+mtime(§4、§7 决策 2)
2458
2614
  let sha256 = null;
2459
2615
  if (stat.size <= FILE_HASH_MAX_SIZE) {
@@ -2466,7 +2622,16 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2466
2622
  }
2467
2623
  return {
2468
2624
  data: {
2469
- path: (args?.path ?? '').toString(),
2625
+ path: responsePath,
2626
+ ...(dataRoot ? {
2627
+ root: {
2628
+ scope: dataRoot.scope,
2629
+ targetAid: dataRoot.targetAid,
2630
+ ...(dataRoot.peerAid ? { peerAid: dataRoot.peerAid } : {}),
2631
+ ...(dataRoot.peerKey ? { peerKey: dataRoot.peerKey } : {}),
2632
+ expiresAt: dataRoot.expiresAt,
2633
+ },
2634
+ } : {}),
2470
2635
  sha256,
2471
2636
  size: stat.size,
2472
2637
  mtime: stat.mtimeMs,
@@ -2495,17 +2660,12 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2495
2660
  const loadedContext = await this.loadMenuContext(channel, channelId);
2496
2661
  let session = loadedContext.session;
2497
2662
  const evolagent = loadedContext.evolagent;
2498
- if (transportReplyContext?.threadId && typeof this.sessionManager.getThreadSession === 'function') {
2663
+ if (transportReplyContext?.threadId) {
2499
2664
  try {
2500
- const topicSession = await this.sessionManager.getThreadSession(channel, channelId, transportReplyContext.threadId);
2501
- if (!topicSession) {
2502
- return { error: '话题会话不存在,拒绝回退到主会话', code: 'TOPIC_SESSION_NOT_FOUND' };
2503
- }
2504
- session = topicSession;
2665
+ session = await loadMenuTopicSession(this.sessionManager, channel, channelId, transportReplyContext.threadId);
2505
2666
  }
2506
2667
  catch (error) {
2507
- logger.debug('[MenuHandler] topic session lookup failed for menu update: thread=' + transportReplyContext.threadId + ': ' + (error instanceof Error ? error.message : String(error)));
2508
- return { error: '无法读取话题会话,拒绝回退到主会话', code: 'TOPIC_SESSION_LOOKUP_FAILED' };
2668
+ return menuResultFailure(error);
2509
2669
  }
2510
2670
  }
2511
2671
  if (cmdBase === '/baseagent-models')
@@ -2784,7 +2944,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2784
2944
  if ('error' in target)
2785
2945
  return target;
2786
2946
  const agent = this.getAgent(channel, target.baseagent);
2787
- let targetModel = arg;
2947
+ const requestedModel = arg;
2788
2948
  if (hasModelSwitcher(agent)) {
2789
2949
  const catalog = await getCatalog(target.sel.self, target.baseagent);
2790
2950
  assertCatalogSelectable(catalog);
@@ -2792,20 +2952,22 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2792
2952
  const decision = validateModelSelectionForRole({
2793
2953
  role: identity.role,
2794
2954
  baseagent: target.baseagent,
2795
- requestedModel: arg,
2955
+ requestedModel,
2796
2956
  models,
2797
2957
  selfAid: target.sel.self,
2798
2958
  resolveModelId: model => resolveCatalogModel(catalog, model),
2799
2959
  });
2800
2960
  if (!decision.ok)
2801
2961
  return { error: decision.message || `invalid model: ${arg}`, code: decision.code || 'INVALID_VALUE' };
2802
- targetModel = decision.model || arg;
2803
- if (models.length && !models.includes(targetModel)) {
2962
+ // Validate the resolved ID, but persist/echo the selected value so aliases
2963
+ // such as `sonnet` keep following the catalog instead of pinning a version.
2964
+ const resolvedModel = decision.model || requestedModel;
2965
+ if (models.length && !models.includes(resolvedModel)) {
2804
2966
  return { error: `无效模型: ${arg}`, code: 'INVALID_VALUE' };
2805
2967
  }
2806
2968
  }
2807
2969
  try {
2808
- writeMenuModel(target, targetModel);
2970
+ writeMenuModel(target, requestedModel);
2809
2971
  }
2810
2972
  catch (e) {
2811
2973
  return { error: e?.message || String(e), code: e?.code || 'CONFIG_WRITE_FAILED', ...(e?.data !== undefined ? { data: e.data } : {}) };
@@ -2815,12 +2977,12 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2815
2977
  sessionId: session?.id,
2816
2978
  agentName: evolagent?.name,
2817
2979
  baseagent: target.baseagent,
2818
- model: targetModel,
2980
+ model: requestedModel,
2819
2981
  timestamp: Date.now(),
2820
2982
  });
2821
2983
  return {
2822
2984
  data: {
2823
- model: targetModel,
2985
+ model: requestedModel,
2824
2986
  baseagent: target.baseagent,
2825
2987
  scope: target.scope,
2826
2988
  field: target.fieldPath,
@@ -3044,6 +3206,15 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3044
3206
  fromControlChannel,
3045
3207
  subject: authSubject,
3046
3208
  });
3209
+ const storageRequest = { type: 'menu.action', cmd, action, args };
3210
+ if (isAgentStorageMenuRequest(storageRequest)) {
3211
+ try {
3212
+ return { data: await agentStorageMenuService.execute(storageRequest, subject, this.agentStorageClientResolver) };
3213
+ }
3214
+ catch (error) {
3215
+ return dataRootFailure(error);
3216
+ }
3217
+ }
3047
3218
  if (cmdBase === '/system' && fromControlChannel && !subject.isDaemonOwner && subject.processRole !== 'daemon-service') {
3048
3219
  return {
3049
3220
  error: formatAuthorizationDenial('This daemon-level operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
@@ -3123,7 +3294,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3123
3294
  }
3124
3295
  const isMenuAdmin = isManagementRole(authIdentity.role);
3125
3296
  // ── /aid avatar ──
3126
- // The AID namespace is the only avatar mutation surface.
3297
+ // Canonical avatar action; legacy agent/update patch.avatar reuses the same uploader.
3127
3298
  if (cmdBase === '/aid') {
3128
3299
  if (action !== 'avatar')
3129
3300
  return { error: `不支持 aid action: ${action}`, code: 'NOT_SUPPORTED' };
@@ -3265,7 +3436,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3265
3436
  }
3266
3437
  }
3267
3438
  }
3268
- return await execAgentAction(action, a, userId ?? '', this.eventBus, this.agentApplicationService);
3439
+ return await execAgentAction(action, a, userId ?? '', this.eventBus, this.agentApplicationService, subject);
3269
3440
  }
3270
3441
  // ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
3271
3442
  if (cmdBase === '/trigger') {
@@ -3310,6 +3481,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3310
3481
  targetThreadId: args.targetThreadId,
3311
3482
  agentId: args.agentId,
3312
3483
  model: args.model,
3484
+ executionTimeoutMs: args.executionTimeoutMs,
3313
3485
  effort: args.effort,
3314
3486
  permissionMode: args.permissionMode,
3315
3487
  triggerThread,
@@ -3447,6 +3619,16 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3447
3619
  return { error: `不支持的 trigger action: ${action}`, code: 'INVALID_ARGS' };
3448
3620
  }
3449
3621
  if (cmdBase === '/topic') {
3622
+ const hasLable = args && (Object.hasOwn(args, 'lable') || Object.hasOwn(args, 'label'));
3623
+ let lable;
3624
+ if (hasLable && (action === 'fork' || action === 'label')) {
3625
+ try {
3626
+ lable = normalizeTopicLable(Object.hasOwn(args, 'lable') ? args.lable : args.label);
3627
+ }
3628
+ catch (error) {
3629
+ return { error: error.message, code: 'INVALID_ARGS' };
3630
+ }
3631
+ }
3450
3632
  if (action === 'fork') {
3451
3633
  if (!this.canReadTopics(identity.role))
3452
3634
  return { error: '无权限创建话题分支', code: 'FORBIDDEN' };
@@ -3523,21 +3705,24 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3523
3705
  const executeFork = async (current, currentAgent) => {
3524
3706
  const messages = await currentAgent.getSessionMessages(current.agentSessionId, current.projectPath);
3525
3707
  const turns = buildSessionTurnList(messages);
3526
- const forkTurn = turns.find((turn) => turn.assistantUuid === sourceAssistantMessageId);
3708
+ // fork-turns resolves bubble aliases to this canonical, complete-turn
3709
+ // anchor. Never fall back to a runner iteration or a guessed index.
3710
+ const forkTurn = turns.find(turn => turn.assistantUuid === sourceAssistantMessageId);
3527
3711
  if (!forkTurn)
3528
3712
  return { error: '分叉点已失效,请刷新历史后重试', code: 'NOT_FOUND' };
3529
- const forkedAgentSessionId = await currentAgent.forkSessionAt(current.agentSessionId, current.projectPath, sourceAssistantMessageId, name || undefined);
3713
+ const forkedAgentSessionId = await currentAgent.forkSessionAt(current.agentSessionId, current.projectPath, forkTurn.assistantUuid, name || undefined, { sessionId: current.id, permissionMode: subject.permissionMode, role: subject.role });
3530
3714
  const topic = await this.sessionManager.createForkedThreadSession(current, forkedAgentSessionId, targetThreadId, {
3531
3715
  name: name || undefined,
3716
+ ...(hasLable ? { lable: lable ?? null } : {}),
3532
3717
  creatorPeerId: userId,
3533
- sourceAssistantMessageId,
3718
+ sourceAssistantMessageId: forkTurn.assistantUuid,
3534
3719
  sourceTurn: forkTurn.index,
3535
3720
  requestId,
3536
3721
  });
3537
3722
  await currentAgent.updateSessionMetadata?.(forkedAgentSessionId, {
3538
3723
  evolcoreSessionId: topic.id,
3539
3724
  sourceSessionId: current.id,
3540
- sourceAssistantMessageId,
3725
+ sourceAssistantMessageId: forkTurn.assistantUuid,
3541
3726
  sourceTurn: forkTurn.index,
3542
3727
  }).catch((error) => {
3543
3728
  logger.debug(`[MenuHandler] Topic fork metadata sync failed: ${error}`);
@@ -3548,7 +3733,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3548
3733
  sourceSessionId: current.id,
3549
3734
  name: topic.name,
3550
3735
  threadId: topic.threadId,
3551
- sourceAssistantMessageId,
3736
+ sourceAssistantMessageId: forkTurn.assistantUuid,
3552
3737
  sourceTurn: forkTurn.index,
3553
3738
  });
3554
3739
  return {
@@ -3560,7 +3745,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3560
3745
  threadId: topic.threadId,
3561
3746
  sourceSessionId: current.id,
3562
3747
  sourceThreadId: current.threadId || null,
3563
- sourceAssistantMessageId,
3748
+ sourceAssistantMessageId: forkTurn.assistantUuid,
3564
3749
  sourceTurn: forkTurn.index,
3565
3750
  },
3566
3751
  },
@@ -3649,7 +3834,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3649
3834
  topicForkTargetsInFlight.delete(targetKey);
3650
3835
  }
3651
3836
  }
3652
- if (action !== 'delete' && action !== 'rename') {
3837
+ if (action !== 'delete' && action !== 'rename' && action !== 'label') {
3653
3838
  return { error: `不支持的 topic action: ${action}`, code: 'NOT_SUPPORTED' };
3654
3839
  }
3655
3840
  const target = (args?.target ?? '').toString().trim();
@@ -3658,6 +3843,8 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3658
3843
  const renameName = action === 'rename' ? getRenameName(args) : '';
3659
3844
  if (action === 'rename' && !renameName)
3660
3845
  return { error: '缺少 args.name', code: 'MISSING_VALUE' };
3846
+ if (action === 'label' && !hasLable)
3847
+ return { error: '缺少 args.lable', code: 'MISSING_VALUE' };
3661
3848
  const topic = await this.sessionManager.getThreadSession(channel, channelId, target);
3662
3849
  if (!topic)
3663
3850
  return { error: '话题不存在', code: 'NOT_FOUND' };
@@ -3667,7 +3854,18 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3667
3854
  ? 'private'
3668
3855
  : this.resolveMenuChatType(channel, channelId, explicitChatType);
3669
3856
  if (!this.canDeleteTopic(identity.role, chatType, topic, userId)) {
3670
- return { error: action === 'rename' ? '无权限重命名话题' : '无权限删除话题', code: 'FORBIDDEN' };
3857
+ return { error: action === 'label' ? '无权限修改话题分类' : action === 'rename' ? '无权限重命名话题' : '无权限删除话题', code: 'FORBIDDEN' };
3858
+ }
3859
+ if (action === 'label') {
3860
+ const success = await this.sessionManager.setTopicLable(topic.id, lable ?? null);
3861
+ if (!success)
3862
+ return { error: '话题不存在', code: 'NOT_FOUND' };
3863
+ return {
3864
+ data: {
3865
+ action: 'label', success: true,
3866
+ topic: { ...buildSessionPayload(topic, topic.name || ''), threadId: topic.threadId, lable: lable ?? null },
3867
+ },
3868
+ };
3671
3869
  }
3672
3870
  if (action === 'rename') {
3673
3871
  const newName = renameName;
@@ -3907,9 +4105,8 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3907
4105
  }
3908
4106
  if (action === 'paircode') {
3909
4107
  // Keep Menu pairing on the same daemon-owned store as /pair, ECWeb, and
3910
- // the local IPC endpoint. The store reuses an unexpired code and creates
3911
- // a new one only after the shared TTL has elapsed.
3912
- const pair = remoteAccessService.getPairingCode(subject.actorId);
4108
+ // the local IPC endpoint. Each successful redemption consumes the code.
4109
+ const pair = remoteAccessService.getPairingCode(subject.actorId, Date.now(), source === 'ecweb' ? 'ecweb-local' : 'menu');
3913
4110
  return { data: { code: pair.code, expiresAt: pair.expiresAt } };
3914
4111
  }
3915
4112
  if (action === 'check') {
@@ -3919,27 +4116,23 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3919
4116
  }
3920
4117
  if (action === 'upgrade') {
3921
4118
  const devMode = isLinkedInstall();
3922
- const localEvolcore = getLocalVersion();
3923
- // fastaun 本地版本:从 node_modules 读取(与 menu.query name=system 一致)
3924
- let localFastaun = null;
3925
- try {
3926
- const fp = path.join(getPackageRoot(), 'node_modules', '@agentunion', 'fastaun', 'package.json');
3927
- localFastaun = JSON.parse(fs.readFileSync(fp, 'utf-8'))?.version ?? null;
3928
- }
3929
- catch { }
3930
- const [evolcoreRemote, fastaunRemote, ecwebRemote] = await Promise.all([
3931
- checkLatestVersion('evolcore'),
3932
- checkLatestVersion('@agentunion/fastaun'),
3933
- checkLatestVersion(WEB_PACKAGE_NAME),
3934
- ]);
3935
- const cmp = (local, remote) => !!(local && remote && compareVersions(local, remote) < 0);
4119
+ const results = await refreshManagedPackageVersions(VERSION_INFO_PACKAGES, { source: 'menu-upgrade' });
4120
+ const [evolcore, fastaunWeb, web] = [results[0], results[2], results[1]];
4121
+ const item = (result) => ({
4122
+ local: result.entry.currentVersion,
4123
+ remote: result.status === 'ok' ? result.entry.latestVersion ?? null : null,
4124
+ hasUpdate: result.status === 'ok' && result.hasUpdate,
4125
+ status: result.status,
4126
+ checkedAt: result.status === 'ok' ? result.entry.checkedAt ?? null : null,
4127
+ lastCheckedAt: result.entry.checkedAt ?? null,
4128
+ ...(result.error ? { error: result.error } : {}),
4129
+ });
3936
4130
  return {
3937
4131
  data: {
3938
4132
  devMode,
3939
- evolcore: { local: localEvolcore, remote: evolcoreRemote, hasUpdate: cmp(localEvolcore, evolcoreRemote) },
3940
- fastaun: { local: localFastaun, remote: fastaunRemote, hasUpdate: cmp(localFastaun, fastaunRemote), managedBy: 'evolcore' },
3941
- // ecweb 本地版本由 ECWeb 进程自身注入(data.ecwebVersion),此处仅给 remote
3942
- ecweb: { remote: ecwebRemote },
4133
+ evolcore: item(evolcore),
4134
+ fastaun: { ...item(fastaunWeb), managedBy: 'evolcore' },
4135
+ ecweb: item(web),
3943
4136
  },
3944
4137
  };
3945
4138
  }
@@ -4043,6 +4236,15 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4043
4236
  },
4044
4237
  });
4045
4238
  if (!decision.allow) {
4239
+ // Keep unsupported runner fields in the aggregate so the CLI can
4240
+ // return a per-field error while still returning supported values.
4241
+ if (isBatchGet
4242
+ && decision.code === 'UNSUPPORTED_FIELD'
4243
+ && parsed.resolvedConfigCommand?.kind === 'field'
4244
+ && parsed.resolvedConfigCommand.subcommand === 'get'
4245
+ && isUnsupportedBaseagentConfigField(parsed.resolvedConfigCommand.field)) {
4246
+ continue;
4247
+ }
4046
4248
  return { error: decision.reason, code: decision.code };
4047
4249
  }
4048
4250
  authorizedCommands.push({ parsed, decision });
@@ -4074,26 +4276,78 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4074
4276
  // fetch 文件作为独立 result.file 消息异步发回;把请求 id 作为 correlationId 透传,
4075
4277
  // 客户端用它把异步到达的文件消息对回这次 fetch 点击。
4076
4278
  if (cmdBase === '/file') {
4077
- const projectPath = session?.projectPath ?? evolagent?.projectPath ?? this.getOwningAgent?.(channel)?.projectPath;
4078
4279
  if (action === 'list') {
4079
4280
  const dirArg = (args?.path ?? '.').toString().trim() || '.';
4080
- const resolved = resolveMenuFilePath(dirArg, projectPath, identity.role, 'directory');
4081
- if ('error' in resolved)
4082
- return resolved;
4083
4281
  const offset = parseFileListOffset(args?.offset);
4084
4282
  const limit = parseFileListLimit(args?.limit);
4085
4283
  const includeHidden = args?.includeHidden === true;
4086
- const listed = listDirectory(resolved.realPath, { offset, limit, includeHidden, projectPath: resolved.projectPath, role: identity.role });
4284
+ let listed;
4285
+ let responsePath = dirArg;
4286
+ let dataRoot;
4287
+ if (args?.rootId !== undefined) {
4288
+ try {
4289
+ const resolved = dataRootResolver.resolvePath(args.rootId, subject, dirArg, 'directory');
4290
+ listed = listDirectory(resolved.realPath, {
4291
+ offset,
4292
+ limit,
4293
+ includeHidden,
4294
+ projectPath: resolved.rootPath,
4295
+ role: undefined,
4296
+ });
4297
+ responsePath = resolved.relativePath;
4298
+ dataRoot = resolved;
4299
+ }
4300
+ catch (error) {
4301
+ return dataRootFailure(error);
4302
+ }
4303
+ }
4304
+ else {
4305
+ const projectPath = session?.projectPath ?? evolagent?.projectPath ?? this.getOwningAgent?.(channel)?.projectPath;
4306
+ const resolved = resolveMenuFilePath(dirArg, projectPath, identity.role, 'directory');
4307
+ if ('error' in resolved)
4308
+ return resolved;
4309
+ listed = listDirectory(resolved.realPath, { offset, limit, includeHidden, projectPath: resolved.projectPath, role: identity.role });
4310
+ }
4087
4311
  if ('error' in listed)
4088
4312
  return listed;
4089
- return { data: { path: dirArg, ...listed.data } };
4313
+ return {
4314
+ data: {
4315
+ path: responsePath,
4316
+ ...(dataRoot ? {
4317
+ root: {
4318
+ scope: dataRoot.scope,
4319
+ targetAid: dataRoot.targetAid,
4320
+ ...(dataRoot.peerAid ? { peerAid: dataRoot.peerAid } : {}),
4321
+ ...(dataRoot.peerKey ? { peerKey: dataRoot.peerKey } : {}),
4322
+ expiresAt: dataRoot.expiresAt,
4323
+ },
4324
+ } : {}),
4325
+ ...listed.data,
4326
+ },
4327
+ };
4090
4328
  }
4091
4329
  if (action !== 'fetch')
4092
4330
  return { error: `不支持的 file action: ${action}`, code: 'NOT_SUPPORTED' };
4093
- const resolved = resolveMenuFilePath(args?.path, projectPath, identity.role, 'file');
4094
- if ('error' in resolved)
4095
- return resolved;
4096
- const { realPath, stat } = resolved;
4331
+ let realPath;
4332
+ let stat;
4333
+ let dataRoot;
4334
+ if (args?.rootId !== undefined) {
4335
+ try {
4336
+ const resolved = dataRootResolver.resolvePath(args.rootId, subject, args?.path, 'file');
4337
+ ({ realPath, stat } = resolved);
4338
+ dataRoot = resolved;
4339
+ }
4340
+ catch (error) {
4341
+ return dataRootFailure(error);
4342
+ }
4343
+ }
4344
+ else {
4345
+ const projectPath = session?.projectPath ?? evolagent?.projectPath ?? this.getOwningAgent?.(channel)?.projectPath;
4346
+ const resolved = resolveMenuFilePath(args?.path, projectPath, identity.role, 'file');
4347
+ if ('error' in resolved)
4348
+ return resolved;
4349
+ ({ realPath, stat } = resolved);
4350
+ }
4097
4351
  if (stat.size > FILE_FETCH_MAX_SIZE) {
4098
4352
  return { error: `文件过大: ${(stat.size / 1024 / 1024).toFixed(1)} MB (限制 ${FILE_FETCH_MAX_SIZE / 1024 / 1024} MB)`, code: 'FILE_TOO_LARGE' };
4099
4353
  }
@@ -4114,15 +4368,21 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4114
4368
  }
4115
4369
  : sessionReplyContext;
4116
4370
  const sendFile = async () => {
4117
- await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath, correlationId: requestId });
4371
+ const receipt = await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath, correlationId: requestId });
4372
+ if (receipt && typeof receipt === 'object' && receipt.status === 'failed') {
4373
+ throw Object.assign(new Error(receipt.error || '文件发送失败'), {
4374
+ code: receipt.code ?? 'EXEC_FAILED',
4375
+ outboxId: receipt.outboxId,
4376
+ });
4377
+ }
4118
4378
  };
4119
4379
  if (deferSideEffect) {
4120
4380
  deferSideEffect(sendFile);
4121
- return { data: { accepted: true } };
4381
+ return { data: { accepted: true, ...(dataRoot ? { scope: dataRoot.scope, expiresAt: dataRoot.expiresAt } : {}) } };
4122
4382
  }
4123
4383
  try {
4124
4384
  await sendFile();
4125
- return { data: { accepted: true } };
4385
+ return { data: { accepted: true, ...(dataRoot ? { scope: dataRoot.scope, expiresAt: dataRoot.expiresAt } : {}) } };
4126
4386
  }
4127
4387
  catch (e) {
4128
4388
  return { error: `文件发送失败: ${e?.message ?? e}`, code: 'EXEC_FAILED' };
@@ -4179,8 +4439,8 @@ function menuCommandToken(cmd) {
4179
4439
  }
4180
4440
  function isProcessLevelMenu(name, cmd) {
4181
4441
  const cmdToken = menuCommandToken(cmd);
4182
- return name === 'system' || name === 'agent' || name === 'gateway' || name === 'config' || name === 'baseagent-models'
4183
- || cmdToken === '/system' || cmdToken === '/agent' || cmdToken === '/gateway' || cmdToken === '/config' || cmdToken === '/baseagent-models';
4442
+ return name === 'system' || name === 'agent' || name === 'gateway' || name === 'config' || name === 'baseagent-models' || name === 'baseagents'
4443
+ || cmdToken === '/system' || cmdToken === '/agent' || cmdToken === '/gateway' || cmdToken === '/config' || cmdToken === '/baseagent-models' || cmdToken === '/baseagents';
4184
4444
  }
4185
4445
  function isProcessContextMenu(payload) {
4186
4446
  const isCatalog = payload?.type === 'menu.list' && payload?.args?.view === 'catalog';
@@ -4200,8 +4460,15 @@ async function execMenuForSystemControl(payload, context) {
4200
4460
  const isFileFetch = (payload?.type === 'menu.query'
4201
4461
  || (payload?.type === 'menu.action' && payload?.action === 'fetch'))
4202
4462
  && (name === 'file' || compatCmd === '/file');
4203
- if (isFileFetch) {
4204
- return ecwebErr(id, name, 'NOT_SUPPORTED', 'Host/control 不支持 file.fetch,请通过目标 Agent AID 请求');
4463
+ const isDataRootQuery = payload?.type === 'menu.query'
4464
+ && (name === 'agent-data-root'
4465
+ || name === 'relation-data-root'
4466
+ || compatCmd === '/agent-data-root'
4467
+ || compatCmd === '/relation-data-root');
4468
+ const isRootedFile = (name === 'file' || compatCmd === '/file')
4469
+ && typeof payload?.args?.rootId === 'string';
4470
+ if (isFileFetch || isDataRootQuery || isRootedFile || isAgentStorageMenuRequest(payload ?? {})) {
4471
+ return ecwebErr(id, name, 'NOT_SUPPORTED', 'Host/control 不支持数据根文件访问,请通过目标 Agent AID 请求');
4205
4472
  }
4206
4473
  if (name === 'cli' || compatCmd === '/cli') {
4207
4474
  const message = context.source === 'ecweb'
@@ -4243,6 +4510,7 @@ async function execMenuForSystemControl(payload, context) {
4243
4510
  conversationId: context.actorAid || SYSTEM_CONTROL_CHANNEL,
4244
4511
  processOwners: context.owners,
4245
4512
  fromControlChannel: true,
4513
+ ecwebManagement: context.source === 'ecweb',
4246
4514
  canApprovePersistentFullAccess: context.canApprovePersistentFullAccess,
4247
4515
  trustedProcessRole: context.source === 'ecweb' ? 'daemon-owner' : undefined,
4248
4516
  });