evolcore 0.0.26 → 0.0.27

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