evolcore 0.0.35 → 0.0.37

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 (224) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +6 -0
  3. package/bin/install-codex-managed-hooks.mjs +20 -3
  4. package/dist/agents/baseagent-availability.js +1 -1
  5. package/dist/agents/baseagent.js +88 -0
  6. package/dist/agents/claude-runner.js +59 -12
  7. package/dist/agents/codex/app-server-client.js +18 -1
  8. package/dist/agents/codex-runner.js +262 -21
  9. package/dist/agents/ecagent-runner.js +47 -11
  10. package/dist/agents/gemini-runner.js +36 -12
  11. package/dist/aun/aid/client.js +4 -3
  12. package/dist/aun/aid/control-aid.js +270 -8
  13. package/dist/aun/aid/identity.js +6 -3
  14. package/dist/aun/aid/ownership-operation.js +58 -0
  15. package/dist/aun/aid/store.js +1 -0
  16. package/dist/aun/group/fs-download.js +67 -2
  17. package/dist/aun/group/history.js +20 -5
  18. package/dist/aun/group/operations.js +8 -2
  19. package/dist/aun/menu/client.js +39 -2
  20. package/dist/aun/menu/transport.js +1 -1
  21. package/dist/aun/msg/history.js +43 -4
  22. package/dist/aun/msg/managed-operation.js +30 -53
  23. package/dist/aun/msg/p2p.js +51 -15
  24. package/dist/aun/outbox.js +4 -0
  25. package/dist/channels/aun.js +260 -73
  26. package/dist/channels/daemon.js +6 -4
  27. package/dist/channels/feishu.js +3 -0
  28. package/dist/cli/agent-command.js +161 -223
  29. package/dist/cli/agent-create.js +190 -223
  30. package/dist/cli/agent.js +3 -1
  31. package/dist/cli/aid-bind-command.js +53 -0
  32. package/dist/cli/aun-commands.js +384 -91
  33. package/dist/cli/bench.js +1 -1
  34. package/dist/cli/cli-argv.js +74 -0
  35. package/dist/cli/daemon-commands.js +105 -74
  36. package/dist/cli/diag.js +1 -1
  37. package/dist/cli/fs-command.js +18 -19
  38. package/dist/cli/getmeta-command.js +159 -6
  39. package/dist/cli/index.js +6 -12
  40. package/dist/cli/init-channel.js +8 -55
  41. package/dist/cli/init.js +125 -218
  42. package/dist/cli/queue-command.js +11 -8
  43. package/dist/cli/restart-monitor.js +177 -85
  44. package/dist/cli/session-command.js +1 -1
  45. package/dist/cli/stats.js +7 -5
  46. package/dist/cli/thread-command.js +103 -30
  47. package/dist/cli/trigger-command.js +1 -1
  48. package/dist/cli/watch-msg-diag.js +1 -0
  49. package/dist/config/access-policy.js +2 -2
  50. package/dist/config/boot-log.js +14 -11
  51. package/dist/config/config-field-policy.js +9 -1
  52. package/dist/config/config-manager.js +176 -90
  53. package/dist/config/config-operation-service.js +7 -14
  54. package/dist/config/contact-book-store.js +142 -26
  55. package/dist/config/contact-book.js +4 -2
  56. package/dist/config/lifecycle.js +1 -1
  57. package/dist/config/owner-config.js +96 -0
  58. package/dist/config/peer-role-resolver.js +12 -18
  59. package/dist/config/providers-config.js +382 -0
  60. package/dist/config/providers-import.js +74 -0
  61. package/dist/config/providers-management.js +328 -0
  62. package/dist/config/providers.js +22 -0
  63. package/dist/config/resolved-config-op.js +17 -14
  64. package/dist/config/snapshot.js +28 -16
  65. package/dist/config-store.js +18 -14
  66. package/dist/core/agent-ipc-registration.js +21 -63
  67. package/dist/core/agent-runtime.js +16 -5
  68. package/dist/core/auth/agent-delegation.js +199 -25
  69. package/dist/core/auth/auth-gateway.js +44 -2
  70. package/dist/core/auth/authorization-denial.js +1 -1
  71. package/dist/core/auth/debug-context.js +2 -1
  72. package/dist/core/auth/delegation-authorization.js +2 -2
  73. package/dist/core/auth/delegation-diagnostics.js +4 -1
  74. package/dist/core/auth/operation-authorizer.js +10 -1
  75. package/dist/core/auth/operation-catalog.js +19 -1
  76. package/dist/core/bootstrap-service.js +43 -20
  77. package/dist/core/channel-loader.js +0 -1
  78. package/dist/core/channel-runtime.js +30 -4
  79. package/dist/core/command/agent-control.js +196 -37
  80. package/dist/core/command/cli-intent-parser.js +60 -4
  81. package/dist/core/command/command-handler.js +212 -60
  82. package/dist/core/command/connect-menu.js +1 -0
  83. package/dist/core/command/diag-service.js +5 -4
  84. package/dist/core/command/file-search.js +368 -0
  85. package/dist/core/command/menu-catalog.js +2 -1
  86. package/dist/core/command/menu-handler.js +292 -107
  87. package/dist/core/command/menu-protocol.js +23 -0
  88. package/dist/core/command/role-menu.js +7 -19
  89. package/dist/core/command/skill-slash.js +130 -25
  90. package/dist/core/command/slash-handler.js +101 -53
  91. package/dist/core/command/topic-slash.js +16 -5
  92. package/dist/core/daemon-bootstrap.js +7 -1
  93. package/dist/core/daemon-control.js +19 -1
  94. package/dist/core/daemon-lifecycle.js +2 -1
  95. package/dist/core/event-catalog.js +2 -0
  96. package/dist/core/evolagent-registry.js +13 -2
  97. package/dist/core/evolagent.js +3 -6
  98. package/dist/core/handoff/ipc-registration.js +2 -2
  99. package/dist/core/handoff/runtime.js +40 -6
  100. package/dist/core/handoff/store.js +1 -0
  101. package/dist/core/inference/text-inference.js +95 -6
  102. package/dist/core/ipc-registration.js +43 -24
  103. package/dist/core/memory/consolidation-context.js +4 -1
  104. package/dist/core/memory/consolidation.js +25 -14
  105. package/dist/core/message/aun-ipc-sender.js +296 -88
  106. package/dist/core/message/aun-request-registry.js +226 -0
  107. package/dist/core/message/create-status.js +33 -4
  108. package/dist/core/message/message-bridge.js +62 -13
  109. package/dist/core/message/message-log.js +0 -1
  110. package/dist/core/message/message-queue.js +4 -1
  111. package/dist/core/message/message-utils.js +8 -1
  112. package/dist/core/message/peer-mode.js +17 -9
  113. package/dist/core/message/response-engine.js +266 -124
  114. package/dist/core/message/retry-scheduler.js +11 -8
  115. package/dist/core/model/catalog-service.js +60 -18
  116. package/dist/core/model/model-catalog.js +112 -9
  117. package/dist/core/model/model-fallback.js +94 -26
  118. package/dist/core/ownership/owner-resolver.js +687 -0
  119. package/dist/core/permission/approval-gateway.js +196 -62
  120. package/dist/core/permission/ec-command-parser.js +138 -25
  121. package/dist/core/permission/filesystem-approval-scope.js +141 -0
  122. package/dist/core/permission/protected-paths.js +14 -1
  123. package/dist/core/permission/sandbox-runtime.js +171 -18
  124. package/dist/core/permission/tool-policy.js +13 -2
  125. package/dist/core/role/runtime-policy.js +3 -4
  126. package/dist/core/session/session-idle-monitor.js +10 -8
  127. package/dist/core/session/session-manager.js +72 -45
  128. package/dist/core/session/session-mapper.js +18 -7
  129. package/dist/core/session/session-renew.js +5 -4
  130. package/dist/core/session/session-title.js +2 -1
  131. package/dist/core/session/session-turn-coordinator.js +4 -2
  132. package/dist/core/skill/index-generator.js +2 -16
  133. package/dist/core/task/task-recovery-service.js +15 -2
  134. package/dist/core/thread/command-parser.js +28 -13
  135. package/dist/core/thread/index.js +1 -0
  136. package/dist/core/thread/ipc-operation.js +11 -8
  137. package/dist/core/thread/thread-operation.js +79 -44
  138. package/dist/core/thread/thread-route.js +54 -6
  139. package/dist/core/thread/thread-service.js +236 -45
  140. package/dist/core/thread/topic-id.js +248 -0
  141. package/dist/eck/kit-renderer.js +1 -1
  142. package/dist/eck/message-renderer.js +12 -2
  143. package/dist/index.js +50 -27
  144. package/dist/ipc.js +59 -6
  145. package/dist/paths.js +1 -0
  146. package/dist/product.js +1 -0
  147. package/dist/response-system/engines/v1/proactive-flow.js +10 -3
  148. package/dist/stats/db.js +8 -0
  149. package/dist/stats/writer.js +26 -4
  150. package/dist/trigger/authorization.js +48 -3
  151. package/dist/trigger/feedback.js +28 -10
  152. package/dist/trigger/ipc-service.js +86 -10
  153. package/dist/trigger/local-files.js +55 -23
  154. package/dist/trigger/scheduler.js +57 -11
  155. package/dist/trigger/system-definitions.js +1 -0
  156. package/dist/trigger/validation.js +44 -15
  157. package/dist/utils/aid-bind.js +17 -297
  158. package/dist/utils/avatar-upload.js +16 -4
  159. package/dist/utils/cross-platform.js +115 -36
  160. package/dist/utils/ecweb-supervisor.js +1 -1
  161. package/dist/utils/ecweb-utils.js +8 -1
  162. package/dist/utils/error-utils.js +44 -6
  163. package/dist/utils/instance-registry.js +12 -2
  164. package/dist/utils/npm-ops.js +102 -27
  165. package/dist/utils/package-version-info.js +72 -27
  166. package/dist/utils/restart-safety.js +50 -0
  167. package/dist/utils/upgrade-jobs.js +304 -0
  168. package/dist/utils/windows-autostart.js +17 -10
  169. package/kits/docs/INDEX.md +5 -0
  170. package/kits/docs/aun/ABILITY_CALLING.md +239 -0
  171. package/kits/docs/evolcore/INDEX.md +14 -3
  172. package/kits/docs/evolcore/agent-create.md +55 -141
  173. package/kits/docs/evolcore/agent.md +38 -16
  174. package/kits/docs/evolcore/aid.md +34 -0
  175. package/kits/docs/evolcore/config.md +20 -1
  176. package/kits/docs/evolcore/event.md +2 -2
  177. package/kits/docs/evolcore/fs.md +18 -2
  178. package/kits/docs/evolcore/group-collaboration.md +13 -246
  179. package/kits/docs/evolcore/group.md +2 -1
  180. package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
  181. package/kits/docs/evolcore/menu-protocol.md +65 -1
  182. package/kits/docs/evolcore/msg.md +64 -2
  183. package/kits/docs/evolcore/skill.md +86 -0
  184. package/kits/docs/evolcore/stats.md +6 -0
  185. package/kits/docs/evolcore/task.md +2 -0
  186. package/kits/docs/evolcore/thread-chain.md +145 -51
  187. package/kits/docs/evolcore/trigger.md +48 -3
  188. package/kits/eck_manifest.json +21 -0
  189. package/kits/eck_message_manifest.json +4 -4
  190. package/kits/migrations/migrate-role-config-v5.mjs +20 -7
  191. package/kits/rules/01-overview.md +2 -1
  192. package/kits/rules/03-identity.md +3 -1
  193. package/kits/rules/04-relation.md +1 -1
  194. package/kits/rules/07-memory-and-thread.md +1 -1
  195. package/kits/schemas/_meta.json +16 -1
  196. package/kits/schemas/agent-config.schema.13.json +44 -2
  197. package/kits/schemas/contact-book.schema.3.json +5 -0
  198. package/kits/schemas/daemon.schema.6.json +3 -1
  199. package/kits/schemas/daemon.schema.8.json +329 -0
  200. package/kits/schemas/defaults.schema.5.json +39 -3
  201. package/kits/schemas/providers.schema.1.json +76 -0
  202. package/kits/schemas/relation-config.schema.9.json +5 -1
  203. package/kits/skills/.switches.json +28 -0
  204. package/kits/skills/aid-ability/SKILL.md +214 -0
  205. package/kits/skills/ec-agent-create/SKILL.md +162 -0
  206. package/kits/skills/ec-group/SKILL.md +2 -1
  207. package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
  208. package/kits/skills/ec-identity/SKILL.md +143 -0
  209. package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
  210. package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
  211. package/kits/skills/ec-trigger/SKILL.md +167 -0
  212. package/kits/skills/ec-trigger/references/examples.md +153 -0
  213. package/kits/skills/thread-ops/SKILL.md +108 -420
  214. package/kits/templates/message-fragments/thread-context.md +1 -2
  215. package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
  216. package/kits/templates/roles/member.json +1 -1
  217. package/kits/templates/roles/visitor.json +1 -0
  218. package/kits/templates/system-fragments/bootstrap.md +1 -0
  219. package/kits/templates/system-fragments/channel.md +2 -1
  220. package/kits/templates/system-fragments/commands.md +5 -4
  221. package/kits/templates/system-fragments/identity.md +2 -0
  222. package/kits/templates/system-fragments/session.md +1 -1
  223. package/kits/templates/system-fragments/venue.md +1 -1
  224. package/package.json +4 -3
@@ -7,19 +7,23 @@ import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelect
7
7
  import { hasModelSwitcher } from '../../agents/runner-types.js';
8
8
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
9
9
  import { buildEnvelope, isDeliveryTarget, isDeliveryTargetForChannel, replyContextFromSession } from '../message/message-utils.js';
10
+ import { isMainSession, isTopicSession, sessionLocalTopicId, sessionTopicThreadId } from '../thread/topic-id.js';
10
11
  import path from 'path';
11
12
  import fs from 'fs';
13
+ import { parseFileSearchArgs, searchProjectFiles } from './file-search.js';
12
14
  import os from 'os';
13
15
  import crypto from 'crypto';
14
16
  import { CronExpressionParser } from 'cron-parser';
15
17
  import { parseDuration } from '../../trigger/parser.js';
16
- import { isLinkedInstall, resolveGlobalPkg } from '../../utils/npm-ops.js';
18
+ import { resolveGlobalPkg } from '../../utils/npm-ops.js';
17
19
  import { refreshManagedPackageVersions, VERSION_INFO_PACKAGES } from '../../utils/package-version-info.js';
18
- import { resolveCliEntry, launchMonitorAndConfirm } from '../../utils/cross-platform.js';
20
+ import { resolveUpgradeDistTag } from '../../utils/package-version-info.js';
21
+ import { readUpgradeJob, startUpgradeJob, structuredUpgradeCheck, upgradeJobView } from '../../utils/upgrade-jobs.js';
22
+ import { launchMonitorAndConfirm, resolveCliEntry, isProcessRunning } from '../../utils/cross-platform.js';
19
23
  import { cachedCliVersion } from '../../utils/cross-platform.js';
20
24
  import { scanInstances } from '../../utils/instance-registry.js';
21
25
  import { loadDefaults, loadDaemonConfig } from '../../config-store.js';
22
- import { WEB_PACKAGE_NAME } from '../../product.js';
26
+ import { WEB_PACKAGE_NAME, RESTART_REQUEST_ID_ENV, RUNTIME_HOME_ENV, SESSION_ID_ENV } from '../../product.js';
23
27
  import { read as cfgRead, resolveEffective, resolveEffectiveFieldWithSource, routeFieldPath, write as cfgWrite, ConfigTarget, } from '../../config/config-manager.js';
24
28
  import { execAgentAction, execAgentQuery, execAgentOptions, execAidAvatar, resolveProjectPath } from './agent-control.js';
25
29
  import { gatewayList, gatewayUpdate, gatewayDelete, gatewayTest, gatewayModels, gatewaySetPrice, gatewaySyncEnv } from '../../config/gateway-config.js';
@@ -29,18 +33,20 @@ import { buildForkHistorySnapshot, pageForkHistory } from '../session/session-ma
29
33
  import { isCapabilityType, listCapabilityOptions, queryCapabilityTypes, resolveCapabilityContext, updateCapabilityPolicy, } from '../capability/capability-manager.js';
30
34
  import { normalizeCliArgv, parseCliIntent, parseLegacyCliCommand, validateCliArgv, withDefaultRelationContext } from './cli-intent-parser.js';
31
35
  import { auditCommandAuthorization, auditFullAccessEvent, hashArgv } from '../auth/authorization-audit.js';
32
- import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
36
+ import { authorizeOperation, authorizeFreshOperation, buildAuthSubject } from '../auth/auth-gateway.js';
37
+ import { ownerResolver, getDaemonOwners, refreshOwnershipForAuthorization, OWNERSHIP_CACHE_MAX_AGE_MS } from '../ownership/owner-resolver.js';
38
+ import { parseOwnershipCommand, executeOwnershipCommand, ownershipFailure } from '../../aun/aid/ownership-operation.js';
33
39
  import { authorizationDenialData, formatAuthorizationDenial } from '../auth/authorization-denial.js';
34
40
  import { evaluateRoleOperationCapability } from '../auth/operation-authorizer.js';
35
41
  import { isUnsupportedBaseagentConfigField } from '../../config/config-field-policy.js';
36
42
  import { splitConfigBatchGetArgv } from '../../cli/cli-argv.js';
37
- import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
43
+ import { chatmodeFieldForPeer, isPrivateChatmodeRoleLocked, resolveChatModeForField } from '../message/peer-mode.js';
38
44
  import { PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
39
45
  import { resolveRuntimePermissionMode, resolveRuntimeStringField, validateRuntimeStringFieldOverride, } from '../role/runtime-policy.js';
40
46
  import { isManagementRole } from '../../config/builtin-roles.js';
41
47
  import { SYSTEM_CONTROL_CHANNEL } from '../system-channels.js';
42
48
  import { logger } from '../../utils/logger.js';
43
- import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
49
+ import { shouldSuppressRealRestart, createRestartRequest, readRestartRequest, updateRestartRequest, removeRestartRequest } from '../../utils/restart-safety.js';
44
50
  import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
45
51
  import { evolMenuResponseTransportMetadata, menuFailure, menuCommandForName, menuSuccess, normalizeMenuError, validateTaskActivityQueryArgs, validateConfigWriteScope, validateMenuRequest, } from './menu-protocol.js';
46
52
  import { parseMenuClientProtectedHeaders } from './menu-protocol.js';
@@ -57,6 +63,7 @@ import { dataRootFailure, dataRootResolver } from './data-root-resolver.js';
57
63
  import { agentStorageMenuService, isAgentStorageMenuRequest } from './agent-storage-menu.js';
58
64
  import { MAX_AUN_ATTACHMENT_SIZE } from '../../aun/msg/upload.js';
59
65
  import { normalizeTopicLabel, normalizeTopicStatus, resolveTopicArchiveFilter, } from '../session/session-mapper.js';
66
+ import { notifyThreadRename } from '../thread/thread-operation.js';
60
67
  /**
61
68
  * 获取 baseagent CLI 的版本号(claude/gemini/codex)。
62
69
  * 失败返回 null(命令不存在或执行失败)。
@@ -206,7 +213,7 @@ function buildSessionPayload(session, name) {
206
213
  const payload = { id: session.id, name };
207
214
  if (session.label)
208
215
  payload.label = session.label;
209
- if (session.threadId)
216
+ if (isTopicSession(session))
210
217
  payload.topicStatus = session.topicStatus ?? 'pending';
211
218
  if (session.agentSessionId)
212
219
  payload.agentSessionId = session.agentSessionId;
@@ -217,10 +224,10 @@ function buildSessionPayload(session, name) {
217
224
  }
218
225
  async function findMainSessionTarget(sessionManager, channel, channelId, target, activeSession) {
219
226
  if (!target) {
220
- return activeSession && !activeSession.threadId ? activeSession : undefined;
227
+ return activeSession && isMainSession(activeSession) ? activeSession : undefined;
221
228
  }
222
229
  const sessions = (await sessionManager.listSessions(channel, channelId))
223
- .filter((s) => !s.threadId);
230
+ .filter((s) => isMainSession(s));
224
231
  return sessions.find((s) => s.name === target ||
225
232
  s.id === target ||
226
233
  (target.length >= 8 && s.id.startsWith(target)) ||
@@ -232,7 +239,7 @@ async function resolveTopicForkSource(sessionManager, channel, channelId, source
232
239
  if (threadId)
233
240
  return sessionManager.getThreadSession(channel, channelId, threadId);
234
241
  const active = await sessionManager.getActiveSession(channel, channelId);
235
- return active && !active.threadId ? active : undefined;
242
+ return active && isMainSession(active) ? active : undefined;
236
243
  }
237
244
  /**
238
245
  * 解析并校验 menu `name=file` 的目标路径(query/fetch 共用)。
@@ -392,6 +399,7 @@ const CLI_EXEC_WHITELIST = {
392
399
  agent: new Set(['list', 'show', 'get']),
393
400
  aid: new Set(['list', 'show', 'lookup']),
394
401
  storage: new Set(['ls', 'quota']),
402
+ thread: new Set(['summary']),
395
403
  };
396
404
  function tokenizeArgv(line) {
397
405
  const out = [];
@@ -588,9 +596,18 @@ function resolveMenuChatmodeTarget(params) {
588
596
  }
589
597
  if (!peerKey)
590
598
  return { error: 'relation scope 需要 peer/peerKey,或可推导的当前对端', code: 'MISSING_PEER' };
591
- return { scope, sel: { ...sel, peerKey }, role: params.role, field, fieldPath: `chatmode.${field}` };
599
+ return {
600
+ scope,
601
+ sel: { ...sel, peerKey },
602
+ role: params.role,
603
+ roleLocked: !params.fromControlChannel && isPrivateChatmodeRoleLocked(field, params.role),
604
+ field,
605
+ fieldPath: `chatmode.${field}`,
606
+ };
592
607
  }
593
608
  function readMenuChatmode(target) {
609
+ if (target.roleLocked)
610
+ return { value: 'proactive', source: 'role' };
594
611
  try {
595
612
  const resolved = resolveEffectiveFieldWithSource(target.fieldPath, target.sel, { cache: true });
596
613
  const decision = resolveRuntimeStringField({
@@ -774,7 +791,7 @@ function resolveMenuModelTarget(params) {
774
791
  return { error: 'relation scope 需要 peer/peerKey,或可推导的当前对端', code: 'MISSING_PEER' };
775
792
  return { scope, sel: { ...sel, peerKey }, role: params.role, baseagent, field: params.field, fieldPath: `baseagents.${baseagent}.${params.field}` };
776
793
  }
777
- function readMenuModel(target, agent) {
794
+ function readMenuModel(target, agent, agentSessionId) {
778
795
  try {
779
796
  const fieldPath = menuModelConfigFieldPath(target);
780
797
  const resolved = resolveEffectiveFieldWithSource(fieldPath, target.sel, { cache: true });
@@ -1050,6 +1067,8 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
1050
1067
  }
1051
1068
  if (verb === 'action') {
1052
1069
  if (cmdBase === '/file') {
1070
+ if (action === 'search')
1071
+ return intent('file.search', 'filesystem', { filePath: args?.path ?? '.' });
1053
1072
  if (action === 'list')
1054
1073
  return intent('file.list', 'filesystem', { filePath: args?.path });
1055
1074
  if (action === 'fetch')
@@ -1140,7 +1159,7 @@ function buildMenuAuthSubject(owner, params) {
1140
1159
  chatType,
1141
1160
  conversationId: peerKeyId,
1142
1161
  identity: params.identity,
1143
- processOwners: loadDaemonConfig().owners ?? [],
1162
+ processOwners: getDaemonOwners(),
1144
1163
  fromControlChannel: params.fromControlChannel,
1145
1164
  });
1146
1165
  }
@@ -1173,6 +1192,14 @@ async function authorizeMenuIntent(params) {
1173
1192
  if (intent.operation.startsWith('trigger.') && !subject.selfAid) {
1174
1193
  return { error: 'Trigger operation requires a channel bound to an Agent', code: 'ROLE_ACCESS_DENIED' };
1175
1194
  }
1195
+ if (intent.operation.startsWith('trigger.') && intent.args.triggerScope !== undefined) {
1196
+ if (intent.args.triggerScope !== 'daemon') {
1197
+ return { error: 'triggerScope 仅支持 daemon', code: 'INVALID_ARGUMENT' };
1198
+ }
1199
+ if (!fromControlChannel || subject.selfAid !== this.getDaemonTriggerOwnerAid?.(channel)) {
1200
+ return { error: '主机级 Trigger 必须通过控制 AID 管理', code: 'NOT_SUPPORTED' };
1201
+ }
1202
+ }
1176
1203
  if (intent.scope === 'relation') {
1177
1204
  intent.args = buildRelationIntentArgs({
1178
1205
  args: intent.args,
@@ -1185,7 +1212,7 @@ async function authorizeMenuIntent(params) {
1185
1212
  }
1186
1213
  const source = params.source ?? 'menu';
1187
1214
  intent.source = source;
1188
- const decision = await authorizeOperation({
1215
+ const decision = await authorizeFreshOperation({
1189
1216
  source,
1190
1217
  intent,
1191
1218
  subject,
@@ -1363,6 +1390,22 @@ function menuResultFailure(error) {
1363
1390
  return { error: normalized.message, code: normalized.code, ...(normalized.data !== undefined ? { data: normalized.data } : {}) };
1364
1391
  }
1365
1392
  function resolveExternalMenuAgentChannel(owner, payload, fallbackChannel, required) {
1393
+ const requestCmd = payload?.name ? menuCommandForName(payload.name) ?? payload.cmd : payload?.cmd;
1394
+ const isTrigger = menuCommandToken(requestCmd) === '/trigger';
1395
+ if (isTrigger && payload?.args?.triggerScope !== undefined) {
1396
+ if (payload.args.triggerScope !== 'daemon') {
1397
+ return { error: 'triggerScope 仅支持 daemon', code: 'INVALID_ARGUMENT' };
1398
+ }
1399
+ if (payload.agent !== undefined
1400
+ || ['aid', 'agentId', 'self', 'context'].some(key => payload.args[key] !== undefined)) {
1401
+ return { error: '主机级 Trigger 不接受 Agent 选择参数', code: 'INVALID_ARGUMENT' };
1402
+ }
1403
+ const channel = owner.getDaemonTriggerChannel?.() ?? 'daemon';
1404
+ if (!owner.getDaemonTriggerOwnerAid?.(channel)) {
1405
+ return { error: '主机调度器不可用', code: 'NOT_SUPPORTED' };
1406
+ }
1407
+ return { channel };
1408
+ }
1366
1409
  const requestedAid = typeof payload?.agent === 'string' && payload.agent.trim()
1367
1410
  ? payload.agent.trim()
1368
1411
  : typeof payload?.args?.aid === 'string'
@@ -1541,7 +1584,7 @@ export function getMenuItems(role, chatType = 'private', scope = 'agent', authSu
1541
1584
  }
1542
1585
  // 记忆管理(紧跟权限管理):条目与 evolhelp 同源,无参数的一点即执行,需要 slug 的弹输入框
1543
1586
  const memoryOwnerAid = authSubject?.selfAid
1544
- ? this?.agentRegistry?.get?.(authSubject.selfAid)?.config?.owners?.[0]
1587
+ ? ownerResolver.primaryOwner(authSubject.selfAid)
1545
1588
  : undefined;
1546
1589
  const memoryEntries = isControlScope ? [] : memoryHelpEntries({
1547
1590
  chatType: normalizedChatType,
@@ -1723,7 +1766,12 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1723
1766
  if ('error' in res)
1724
1767
  throw { code: res.code, message: res.error };
1725
1768
  return res.data.agents.map(ag => ({
1726
- value: ag.aid, label: ag.name || ag.aid, desc: ag.status, lifecycle: ag.lifecycle ?? null,
1769
+ value: ag.aid,
1770
+ label: ag.name || ag.aid,
1771
+ desc: ag.status,
1772
+ lifecycle: ag.lifecycle ?? null,
1773
+ baseagent: ag.baseagent ?? null,
1774
+ projectPath: ag.projectPath ?? null,
1727
1775
  }));
1728
1776
  }
1729
1777
  // agent channel:直接读取自身,避免先枚举全部 Agent 再过滤。
@@ -1737,7 +1785,12 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1737
1785
  throw { code: res.code, message: res.error };
1738
1786
  const ag = res.data;
1739
1787
  return [{
1740
- value: ag.aid, label: ag.identity?.name || ag.name || ag.aid, desc: ag.status, lifecycle: ag.lifecycle ?? null,
1788
+ value: ag.aid,
1789
+ label: ag.identity?.name || ag.name || ag.aid,
1790
+ desc: ag.status,
1791
+ lifecycle: ag.lifecycle ?? null,
1792
+ baseagent: ag.baseagent ?? null,
1793
+ projectPath: ag.projectPath ?? null,
1741
1794
  }];
1742
1795
  }
1743
1796
  if (cmd === '/capability') {
@@ -1833,7 +1886,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1833
1886
  }
1834
1887
  }
1835
1888
  return sessions
1836
- .filter((s) => !!s.threadId && (!hasLabel || s.label === label) && (!topicStatus || s.topicStatus === topicStatus))
1889
+ .filter((s) => isTopicSession(s) && (!hasLabel || s.label === label) && (!topicStatus || s.topicStatus === topicStatus))
1837
1890
  .map((s) => this.buildTopicMenuItem(s));
1838
1891
  }
1839
1892
  if (cmd === '/s' || cmd === '/session' || cmd === '/del') {
@@ -1841,7 +1894,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1841
1894
  const active = cmd === '/del' ? await this.sessionManager.getActiveSession(channel, channelId) : null;
1842
1895
  const currentSession = session;
1843
1896
  const items = sessions
1844
- .filter((s) => !s.threadId)
1897
+ .filter((s) => isMainSession(s))
1845
1898
  .filter((s) => !active || s.id !== active.id)
1846
1899
  .map((s) => {
1847
1900
  const displayName = displaySessionTitle(s.name, s.id.slice(0, 8));
@@ -1915,7 +1968,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1915
1968
  const rawModels = catalog.models.map(m => m.id);
1916
1969
  const models = filterModelsForRole(optionsIdentity.role, target.baseagent, rawModels, model => resolveCatalogModel(catalog, model), target.sel.self);
1917
1970
  const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'current');
1918
- const currentModel = requestedModel || readMenuModel(target, agent).value || agent.getModel();
1971
+ const currentModel = requestedModel || readMenuModel(target, agent, session?.agentSessionId).value || agent.getModel();
1919
1972
  if (models.length > 0)
1920
1973
  return models.map((m) => ({ value: m, label: modelDisplayLabel(agent, m) + (catalog.models.find(e => e.id === m)?.verification === 'unverified' ? ' [unverified]' : ''), selected: modelMatches(agent, m, currentModel) }));
1921
1974
  }
@@ -1982,9 +2035,9 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1982
2035
  }
1983
2036
  const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'currentModel');
1984
2037
  const modelTarget = { ...target, field: 'model', fieldPath: `baseagents.${target.baseagent}.model` };
1985
- const currentModel = requestedModel || (hasModelSwitcher(agent) ? (readMenuModel(modelTarget, agent).value || agent.getModel()) : agent.name);
2038
+ const currentModel = requestedModel || (hasModelSwitcher(agent) ? (readMenuModel(modelTarget, agent, session?.agentSessionId).value || agent.getModel()) : agent.name);
1986
2039
  const efforts = await getAvailableEfforts(agent, currentModel);
1987
- const currentEffort = menuStringArg(args, 'effort') ?? menuStringArg(args, 'current') ?? readMenuModel(target, agent).value ?? 'auto';
2040
+ const currentEffort = menuStringArg(args, 'effort') ?? menuStringArg(args, 'current') ?? readMenuModel(target, agent, session?.agentSessionId).value ?? 'auto';
1988
2041
  const allItems = [...efforts, 'auto'];
1989
2042
  return allItems.map(e => ({ value: e, label: e === 'auto' ? 'auto (SDK默认)' : e, selected: e === currentEffort }));
1990
2043
  }
@@ -2506,7 +2559,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2506
2559
  const page = pageForkHistory(snapshot, before, limit);
2507
2560
  const forkSource = current.metadata?.forkSource;
2508
2561
  return { data: {
2509
- threadId: current.threadId,
2562
+ threadId: sessionLocalTopicId(current) ?? '',
2510
2563
  sourceAssistantMessageId: source.assistantMessageId,
2511
2564
  historyVersion: 1,
2512
2565
  historyDigest: forkSource?.historyDigest,
@@ -2538,7 +2591,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2538
2591
  turns = this.sessionManager.getSessionFileInfo(topic.projectPath, topic.agentSessionId, topic.baseagent, topic.selfAID).turns;
2539
2592
  }
2540
2593
  const data = {
2541
- threadId: topic.threadId,
2594
+ threadId: sessionLocalTopicId(topic) ?? '',
2542
2595
  name: topic.name || null,
2543
2596
  label: topic.label || null,
2544
2597
  topicStatus: topic.topicStatus ?? 'pending',
@@ -2594,7 +2647,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2594
2647
  if ('error' in target)
2595
2648
  return target;
2596
2649
  const agent = this.getAgent(channel, target.baseagent);
2597
- const current = readMenuModel(target, agent);
2650
+ const current = readMenuModel(target, agent, session?.agentSessionId);
2598
2651
  return {
2599
2652
  data: {
2600
2653
  model: current.value,
@@ -2623,7 +2676,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2623
2676
  if ('error' in target)
2624
2677
  return target;
2625
2678
  const agent = this.getAgent(channel, target.baseagent);
2626
- const current = readMenuModel(target, agent);
2679
+ const current = readMenuModel(target, agent, session?.agentSessionId);
2627
2680
  return {
2628
2681
  data: {
2629
2682
  effort: current.value,
@@ -2777,6 +2830,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2777
2830
  data.fastaunVersion = fp2.version;
2778
2831
  }
2779
2832
  catch { }
2833
+ data.avatarCreateSupported = true;
2780
2834
  return { data };
2781
2835
  }
2782
2836
  if (view === 'monitor') {
@@ -3052,7 +3106,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
3052
3106
  const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
3053
3107
  if (!triggerScheduler)
3054
3108
  return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
3055
- const updated = await this.updateTriggerFromPatch(triggerScheduler, patch.nameOrId, patch, channel, channelId, userId ?? '', isAdmin, undefined, undefined, subject.isDaemonOwner, subject.canApprovePersistentFullAccess);
3109
+ const updated = await this.updateTriggerFromPatch(triggerScheduler, patch.nameOrId, patch, channel, channelId, userId ?? '', isAdmin, undefined, undefined, subject.isDaemonOwner, subject.canApprovePersistentFullAccess, session?.chatType);
3056
3110
  if (!updated.ok) {
3057
3111
  return {
3058
3112
  error: updated.error,
@@ -3108,8 +3162,9 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
3108
3162
  return { error: `无效 baseagent: ${arg},可选: ${valid.join(' / ')}`, code: 'INVALID_VALUE' };
3109
3163
  }
3110
3164
  const previousBaseagent = evolagent.baseagent;
3111
- if (session?.threadId) {
3112
- if (transportReplyContext?.threadId !== session.threadId) {
3165
+ const sessionTopicId = sessionTopicThreadId(session);
3166
+ if (session && sessionTopicId) {
3167
+ if (transportReplyContext?.threadId !== sessionTopicId) {
3113
3168
  return { error: '话题路由上下文不匹配,拒绝切换', code: 'THREAD_CONTEXT_MISMATCH' };
3114
3169
  }
3115
3170
  if (typeof this.messageQueue.withSessionBarrier !== 'function') {
@@ -3119,7 +3174,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
3119
3174
  const currentAgent = this.getAgent(channel, session.baseagent);
3120
3175
  const outcome = await this.messageQueue.withSessionBarrier(sessionKey, async (snapshot) => {
3121
3176
  const latest = await this.sessionManager.getSessionById?.(session.id);
3122
- if (!latest || latest.threadId !== session.threadId) {
3177
+ if (!latest || sessionTopicThreadId(latest) !== sessionTopicId) {
3123
3178
  return { error: '话题会话不存在或归属已变化', code: 'NOT_FOUND' };
3124
3179
  }
3125
3180
  const busy = !!latest.processingState
@@ -3137,7 +3192,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
3137
3192
  evolagent.setActiveBaseagent(arg);
3138
3193
  await this.sessionManager.rotateTopicBackend({
3139
3194
  sessionId: latest.id,
3140
- expectedThreadId: latest.threadId,
3195
+ expectedThreadId: sessionTopicId,
3141
3196
  baseagent: arg,
3142
3197
  });
3143
3198
  return { ok: true };
@@ -3166,7 +3221,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
3166
3221
  baseagentChanged: previousBaseagent !== arg,
3167
3222
  scope: 'agent',
3168
3223
  sessionId: session.id,
3169
- threadId: session.threadId,
3224
+ threadId: sessionTopicId,
3170
3225
  sessionIdChanged: false,
3171
3226
  notice: '已切换 Baseagent: ' + previousBaseagent + ' → ' + arg + ',话题会话 ID 保持不变',
3172
3227
  } };
@@ -3291,7 +3346,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
3291
3346
  return target;
3292
3347
  const agent = this.getAgent(channel, target.baseagent);
3293
3348
  const modelTarget = { ...target, field: 'model', fieldPath: `baseagents.${target.baseagent}.model` };
3294
- const currentModel = hasModelSwitcher(agent) ? (readMenuModel(modelTarget, agent).value || agent.getModel()) : agent.name;
3349
+ const currentModel = hasModelSwitcher(agent) ? (readMenuModel(modelTarget, agent, session?.agentSessionId).value || agent.getModel()) : agent.name;
3295
3350
  const validEfforts = await getAvailableEfforts(agent, currentModel);
3296
3351
  const allValid = [...validEfforts, 'auto'];
3297
3352
  if (!allValid.includes(arg)) {
@@ -3346,6 +3401,9 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
3346
3401
  });
3347
3402
  if ('error' in target)
3348
3403
  return target;
3404
+ if (arg === 'interactive' && target.roleLocked) {
3405
+ return { error: '私聊交互模式仅限 owner/admin,当前角色固定为 proactive', code: 'ROLE_VALUE_NOT_ALLOWED' };
3406
+ }
3349
3407
  const roleDecision = validateRuntimeStringFieldOverride({
3350
3408
  selfAid: target.sel.self,
3351
3409
  role: target.role,
@@ -3458,6 +3516,17 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3458
3516
  return { error: '缺少命令', code: 'MISSING_CMD' };
3459
3517
  if (!action)
3460
3518
  return { error: '缺少 action', code: 'MISSING_VALUE' };
3519
+ if (cmdBase === '/file' && action === 'search') {
3520
+ try {
3521
+ parseFileSearchArgs(args);
3522
+ }
3523
+ catch (error) {
3524
+ return { error: error.message, code: 'INVALID_ARGUMENT' };
3525
+ }
3526
+ if (fromControlChannel || authSubject?.fromControlChannel) {
3527
+ return { error: 'File search must be addressed to the current Agent AID', code: 'NOT_SUPPORTED' };
3528
+ }
3529
+ }
3461
3530
  const gated = gateControlScope.call(this, { cmdBase, action, args, channel, fromControlChannel });
3462
3531
  if (gated)
3463
3532
  return gated;
@@ -3943,7 +4012,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3943
4012
  success: true,
3944
4013
  topic: {
3945
4014
  ...buildSessionPayload(existingTopic, existingTopic.name || ''),
3946
- threadId: existingTopic.threadId,
4015
+ threadId: sessionLocalTopicId(existingTopic) ?? '',
3947
4016
  sourceSessionId: forkSource.sessionId,
3948
4017
  sourceThreadId: forkSource.threadId,
3949
4018
  sourceAssistantMessageId: forkSource.assistantMessageId,
@@ -3977,7 +4046,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3977
4046
  || (this.messageQueue.getQueueLength?.(sourceKey) ?? 0) > 0
3978
4047
  || currentAgent.hasActiveStream(sourceKey)
3979
4048
  || (this.processor?.isPauseRequested?.(sourceKey) ?? false);
3980
- if (!source.threadId && sourceIsBusy(source, agent)) {
4049
+ if (isMainSession(source) && sourceIsBusy(source, agent)) {
3981
4050
  return { error: '来源会话正在处理消息,请完成后重试', code: 'BUSY' };
3982
4051
  }
3983
4052
  const targetKey = JSON.stringify([source.channelType || source.channel, source.selfAID || '', channelId, targetThreadId]);
@@ -4023,7 +4092,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4023
4092
  sessionId: topic.id,
4024
4093
  sourceSessionId: current.id,
4025
4094
  name: topic.name,
4026
- threadId: topic.threadId,
4095
+ threadId: sessionTopicThreadId(topic),
4027
4096
  sourceAssistantMessageId: actualSourceMessageId,
4028
4097
  sourceTurn: forkTurn.index,
4029
4098
  });
@@ -4033,9 +4102,9 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4033
4102
  success: true,
4034
4103
  topic: {
4035
4104
  ...buildSessionPayload(topic, topic.name || ''),
4036
- threadId: topic.threadId,
4105
+ threadId: sessionTopicThreadId(topic) ?? '',
4037
4106
  sourceSessionId: current.id,
4038
- sourceThreadId: current.threadId || null,
4107
+ sourceThreadId: sessionTopicThreadId(current) ?? null,
4039
4108
  sourceAssistantMessageId: actualSourceMessageId,
4040
4109
  sourceTurn: forkTurn.index,
4041
4110
  },
@@ -4043,7 +4112,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4043
4112
  };
4044
4113
  };
4045
4114
  const runForkUnderBarrier = async () => {
4046
- if (source.threadId) {
4115
+ if (isTopicSession(source)) {
4047
4116
  if (typeof this.messageQueue.withSessionBarrier !== 'function') {
4048
4117
  return { error: '当前运行环境不支持安全话题分叉', code: 'NOT_SUPPORTED' };
4049
4118
  }
@@ -4053,7 +4122,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4053
4122
  if (!current) {
4054
4123
  return { error: '来源会话不存在或归属已变化', code: 'NOT_FOUND' };
4055
4124
  }
4056
- if (current.id !== source.id || (current.threadId || '') !== (source.threadId || '')) {
4125
+ if (current.id !== source.id || sessionTopicThreadId(current) !== sessionTopicThreadId(source)) {
4057
4126
  return { error: '来源会话不存在或归属已变化', code: 'NOT_FOUND' };
4058
4127
  }
4059
4128
  if (!current.agentSessionId) {
@@ -4148,6 +4217,9 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4148
4217
  const topic = await this.sessionManager.getThreadSession(channel, channelId, target);
4149
4218
  if (!topic)
4150
4219
  return { error: '话题不存在', code: 'NOT_FOUND' };
4220
+ // target 可能是主话题规范 id,getThreadSession 会解析到主会话;这些动作只针对话题。
4221
+ if (!isTopicSession(topic))
4222
+ return { error: '主会话不支持此操作,请指定话题', code: 'NOT_SUPPORTED' };
4151
4223
  const chatType = topic.chatType === 'group'
4152
4224
  ? 'group'
4153
4225
  : topic.chatType === 'private'
@@ -4163,7 +4235,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4163
4235
  return {
4164
4236
  data: {
4165
4237
  action: 'label', success: true,
4166
- topic: { ...buildSessionPayload(topic, topic.name || ''), threadId: topic.threadId, label: label ?? null },
4238
+ topic: { ...buildSessionPayload(topic, topic.name || ''), threadId: sessionLocalTopicId(topic) ?? '', label: label ?? null },
4167
4239
  },
4168
4240
  };
4169
4241
  }
@@ -4171,7 +4243,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4171
4243
  const success = await this.sessionManager.setTopicStatus(topic.id, topicStatus);
4172
4244
  if (!success)
4173
4245
  return { error: '话题不存在', code: 'NOT_FOUND' };
4174
- return { data: { action: 'status', success: true, topic: { ...buildSessionPayload(topic, topic.name || ''), threadId: topic.threadId, topicStatus } } };
4246
+ return { data: { action: 'status', success: true, topic: { ...buildSessionPayload(topic, topic.name || ''), threadId: sessionLocalTopicId(topic) ?? '', topicStatus } } };
4175
4247
  }
4176
4248
  if (action === 'rename') {
4177
4249
  const newName = renameName;
@@ -4179,7 +4251,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4179
4251
  if (existing && existing.id !== topic.id) {
4180
4252
  return { error: `名称 "${newName}" 已存在`, code: 'CONFLICT' };
4181
4253
  }
4182
- const oldName = displaySessionTitle(topic.name, topic.threadId || '(未命名)');
4254
+ const oldName = displaySessionTitle(topic.name, sessionTopicThreadId(topic) || '(未命名)');
4183
4255
  const success = await this.sessionManager.renameSession(topic.id, newName);
4184
4256
  if (!success)
4185
4257
  return { error: '重命名失败', code: 'EXEC_FAILED' };
@@ -4191,14 +4263,25 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4191
4263
  catch { }
4192
4264
  }
4193
4265
  // P1a.5.3: 若为话题会话,同步更新话题索引
4194
- if (topic.threadId) {
4266
+ const renamedTopicThreadId = sessionTopicThreadId(topic);
4267
+ if (renamedTopicThreadId) {
4195
4268
  try {
4196
4269
  const { ThreadService } = await import('../../core/thread/index.js');
4197
4270
  const selfAid = topic.selfAID;
4198
4271
  const peer = topic.channelId;
4199
4272
  if (selfAid && peer) {
4200
4273
  const svc = new ThreadService(this.sessionManager);
4201
- await svc.renameThread({ selfAid, peer, threadId: topic.threadId, name: newName });
4274
+ const result = await svc.renameThread({ selfAid, peer, threadId: renamedTopicThreadId, name: newName });
4275
+ await notifyThreadRename({
4276
+ selfAid,
4277
+ peer,
4278
+ threadId: renamedTopicThreadId,
4279
+ result,
4280
+ threadService: svc,
4281
+ sendSystemMsg: (topic.channelType || channel.split('#', 1)[0]) === 'aun'
4282
+ ? this.getThreadSystemMsgSender?.()
4283
+ : undefined,
4284
+ });
4202
4285
  }
4203
4286
  }
4204
4287
  catch (err) {
@@ -4212,7 +4295,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4212
4295
  success: true,
4213
4296
  topic: {
4214
4297
  ...buildSessionPayload(topic, newName),
4215
- threadId: topic.threadId,
4298
+ threadId: sessionLocalTopicId(topic) ?? '',
4216
4299
  },
4217
4300
  },
4218
4301
  };
@@ -4316,7 +4399,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4316
4399
  }
4317
4400
  // ── /system 系列 ──
4318
4401
  if (cmdBase === '/system') {
4319
- // D1 迁移:进程级鉴权统一查 daemon.json owners,替代各 action 内联的 identity.role 判断
4402
+ // 进程级鉴权统一使用 Control AID 的协议 Owner,替代各 action 内联的 identity.role 判断。
4320
4403
  if (!subject.isDaemonOwner && subject.processRole !== 'daemon-service') {
4321
4404
  return {
4322
4405
  error: formatAuthorizationDenial('This daemon-level operation requires DaemonOwner.', 'DAEMON_OWNER_REQUIRED'),
@@ -4358,63 +4441,97 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4358
4441
  : 'AUN_OUTBOUND_ROUTE_REQUIRED',
4359
4442
  };
4360
4443
  }
4444
+ let restartRequestId;
4361
4445
  if (!suppressRealRestart) {
4362
- // 这条路径同样以 daemon 自杀收尾,入口缺失必须在写 pending 和自杀之前拦下。
4363
- const restartEntry = resolveCliEntry(getPackageRoot());
4364
- if (!fs.existsSync(restartEntry.entry)) {
4365
- return {
4366
- error: `找不到重启入口:${restartEntry.entry}。请先执行 npm run build 重建产物。`,
4367
- code: 'RESTART_ENTRY_MISSING',
4368
- data: { reasonCode: 'RESTART_ENTRY_MISSING', entry: restartEntry.entry },
4369
- };
4370
- }
4446
+ restartRequestId = crypto.randomUUID();
4371
4447
  const restartInfo = {
4372
4448
  channel,
4373
4449
  channelId,
4374
4450
  timestamp: Date.now(),
4451
+ requestId: restartRequestId,
4452
+ requesterPid: process.pid,
4453
+ status: 'requested',
4375
4454
  ...(restartReplyContext?.threadId ? { threadId: restartReplyContext.threadId } : {}),
4376
4455
  ...(restartDelivery ? { delivery: restartDelivery } : {}),
4377
4456
  ...(restartReplyContext?.replyToMessageId ? { rootId: restartReplyContext.replyToMessageId } : {}),
4378
4457
  };
4379
4458
  const controlDir = daemonControlDir();
4380
4459
  fs.mkdirSync(controlDir, { recursive: true });
4381
- fs.writeFileSync(path.join(controlDir, 'restart-pending.json'), JSON.stringify(restartInfo));
4382
- // Use the canonical CLI restart path so the restart itself performs
4383
- // the version check and automatic upgrades before replacing the daemon.
4384
- // Do not inherit managed-session credentials: otherwise cmdRestart can
4385
- // delegate back to this daemon and recursively enqueue another restart.
4386
- //
4387
- // `ec restart` owns the monitor liveness check, but it can itself die on
4388
- // load (tsx + a broken source tree), and this path kills the daemon
4389
- // afterwards — so confirm the CLI process survived before proceeding.
4390
- const restartLaunch = await launchMonitorAndConfirm([...restartEntry.args, 'restart'], {
4391
- EVOLCORE_HOME: resolvePaths().root,
4392
- EVOLCORE_SESSION_ID: '',
4460
+ const pendingFile = path.join(controlDir, 'restart-pending.json');
4461
+ const monitorEntry = resolveCliEntry(getPackageRoot());
4462
+ if (!fs.existsSync(monitorEntry.entry)) {
4463
+ return {
4464
+ error: `找不到重启监控入口:${monitorEntry.entry}。请先执行 npm run build 重建产物。`,
4465
+ code: 'RESTART_ENTRY_MISSING',
4466
+ data: { reasonCode: 'RESTART_ENTRY_MISSING', entry: monitorEntry.entry },
4467
+ };
4468
+ }
4469
+ if (scanInstances().restartMonitors.some(item => item.alive)
4470
+ || !createRestartRequest(pendingFile, restartInfo, isProcessRunning)) {
4471
+ return { error: '已有重启请求正在处理,请等待其结果,不要重复发起。', code: 'RESTART_ALREADY_PENDING' };
4472
+ }
4473
+ const requestId = restartRequestId;
4474
+ const accepted = () => {
4475
+ const request = readRestartRequest(pendingFile, requestId);
4476
+ return request?.status === 'accepted' || request?.status === 'ready';
4477
+ };
4478
+ const logFilePath = path.join(resolvePaths().logs, `restart-launch-${requestId}.log`);
4479
+ const restartLaunch = await launchMonitorAndConfirm([...monitorEntry.args, 'restart'], {
4480
+ [RUNTIME_HOME_ENV]: resolvePaths().root,
4481
+ [SESSION_ID_ENV]: '',
4393
4482
  [AGENT_DELEGATION_TOKEN_ENV]: '',
4394
- },
4395
- // `ec restart` does not register itself; staying alive is the signal.
4396
- () => false, pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid !== pid), 8_000);
4483
+ [RESTART_REQUEST_ID_ENV]: requestId,
4484
+ }, () => accepted(), () => scanInstances().restartMonitors.some(item => item.alive && item.record.requestId === requestId && item.record.phase === 'accepted'), {
4485
+ requestId,
4486
+ timeoutMs: 8_000,
4487
+ logFilePath,
4488
+ onSpawn: pid => { updateRestartRequest(pendingFile, requestId, { launcherPid: pid }); },
4489
+ isPendingInRegistry: () => scanInstances().restartMonitors.some(item => item.alive && item.record.requestId === requestId),
4490
+ });
4397
4491
  if (!restartLaunch.ok) {
4398
- try {
4399
- fs.rmSync(path.join(controlDir, 'restart-pending.json'), { force: true });
4492
+ if (restartLaunch.pending) {
4493
+ logger.error(`[System] Restart ownership is still unconfirmed (requestId=${restartRequestId}, PID=${restartLaunch.pid ?? 'unknown'})`);
4494
+ return {
4495
+ error: `重启请求状态暂不可确认(requestId=${restartRequestId})。启动链仍在运行,可能继续执行;请勿立即重复发起。诊断日志:${logFilePath},monitor 记录见 restart.log。`,
4496
+ code: 'RESTART_PENDING_CONFIRMATION',
4497
+ data: {
4498
+ action: 'restart',
4499
+ success: false,
4500
+ status: 'pending-confirmation',
4501
+ requestId: restartRequestId,
4502
+ ...(restartLaunch.pid ? { pid: restartLaunch.pid } : {}),
4503
+ },
4504
+ };
4400
4505
  }
4401
- catch { }
4402
- logger.error(`[System] Refusing menu restart: launcher did not survive (${restartLaunch.reason})`);
4506
+ removeRestartRequest(pendingFile, requestId);
4507
+ logger.error(`[System] Refusing menu restart: restart-monitor did not take ownership (${restartLaunch.reason})`);
4403
4508
  for (const line of (restartLaunch.stderrTail ?? '').split(/\r?\n/).filter(Boolean).slice(-8)) {
4404
4509
  logger.error(`[System] | ${line}`);
4405
4510
  }
4406
4511
  return {
4407
- error: `重启已中止:启动器未能运行(${restartLaunch.reason})。服务未停止,仍在运行当前版本。`,
4408
- code: 'RESTART_LAUNCH_FAILED',
4409
- data: { reasonCode: 'RESTART_LAUNCH_FAILED', reason: restartLaunch.reason },
4512
+ error: `重启已中止:重启监控未接管(${restartLaunch.reason})。服务未停止,仍在运行当前版本。`,
4513
+ code: 'RESTART_MONITOR_DEAD',
4514
+ data: {
4515
+ reasonCode: 'RESTART_MONITOR_DEAD',
4516
+ reason: restartLaunch.reason,
4517
+ requestId: restartRequestId,
4518
+ ...(restartLaunch.pid ? { pid: restartLaunch.pid } : {}),
4519
+ },
4410
4520
  };
4411
4521
  }
4522
+ logger.info(`[System] Restart monitor accepted request ${restartRequestId} (PID ${restartLaunch.pid ?? 'unknown'})`);
4412
4523
  }
4413
4524
  else {
4414
4525
  logger.info('[System] Suppressed real menu restart in test runtime');
4415
4526
  }
4416
4527
  this.eventBus.publish({ type: 'system:restart', channel, channelId });
4417
- return { data: { action: 'restart', success: true } };
4528
+ return {
4529
+ data: {
4530
+ action: 'restart',
4531
+ success: true,
4532
+ ...(restartRequestId ? { status: 'accepted', requestId: restartRequestId } : {}),
4533
+ },
4534
+ };
4418
4535
  }
4419
4536
  if (action === 'paircode') {
4420
4537
  // Keep Menu pairing on the same daemon-owned store as /pair, ECWeb, and
@@ -4428,26 +4545,28 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4428
4545
  return r;
4429
4546
  }
4430
4547
  if (action === 'upgrade') {
4431
- const devMode = isLinkedInstall();
4432
- const results = await refreshManagedPackageVersions(VERSION_INFO_PACKAGES, { source: 'menu-upgrade' });
4433
- const [evolcore, fastaunWeb, web] = [results[0], results[2], results[1]];
4434
- const item = (result) => ({
4435
- local: result.entry.currentVersion,
4436
- remote: result.status === 'ok' ? result.entry.latestVersion ?? null : null,
4437
- hasUpdate: result.status === 'ok' && result.hasUpdate,
4438
- status: result.status,
4439
- checkedAt: result.status === 'ok' ? result.entry.checkedAt ?? null : null,
4440
- lastCheckedAt: result.entry.checkedAt ?? null,
4441
- ...(result.error ? { error: result.error } : {}),
4442
- });
4443
- return {
4444
- data: {
4445
- devMode,
4446
- evolcore: item(evolcore),
4447
- fastaun: { ...item(fastaunWeb), managedBy: 'evolcore' },
4448
- ecweb: item(web),
4449
- },
4450
- };
4548
+ const mode = args?.mode ?? 'check';
4549
+ if (!['check', 'update', 'status'].includes(mode)
4550
+ || Object.keys(args ?? {}).some(key => !['mode', 'jobId', 'context'].includes(key))
4551
+ || (mode !== 'status' && args?.jobId !== undefined))
4552
+ return { error: '无效的更新参数', code: 'INVALID_ARGUMENT' };
4553
+ if (mode === 'status') {
4554
+ if (typeof args?.jobId !== 'string' || !/^job_[a-f0-9-]{36}$/.test(args.jobId))
4555
+ return { error: '缺少或无效的 jobId', code: 'INVALID_ARGUMENT' };
4556
+ const job = readUpgradeJob(args.jobId);
4557
+ const scope = JSON.stringify([source, channel, channelId, subject.actorId, transportReplyContext?.toDeviceId, transportReplyContext?.threadId]);
4558
+ const expectedKey = job ? crypto.createHash('sha256').update(JSON.stringify([scope, job.requestId])).digest('hex') : undefined;
4559
+ return job && job.requestKey === expectedKey
4560
+ ? { data: upgradeJobView(job) }
4561
+ : { error: '更新任务不存在或不可访问', code: 'NOT_FOUND' };
4562
+ }
4563
+ if (mode === 'update') {
4564
+ const result = await startUpgradeJob(requestId || crypto.randomUUID(), JSON.stringify([source, channel, channelId, subject.actorId, transportReplyContext?.toDeviceId, transportReplyContext?.threadId]));
4565
+ return { data: { ...upgradeJobView(result.job), reused: result.reused } };
4566
+ }
4567
+ const distTag = resolveUpgradeDistTag();
4568
+ const results = await refreshManagedPackageVersions(VERSION_INFO_PACKAGES, { source: 'menu-upgrade', distTag, includeUninstalled: true });
4569
+ return { data: structuredUpgradeCheck(results, distTag) };
4451
4570
  }
4452
4571
  return { error: `不支持的 system action: ${action}`, code: 'NOT_SUPPORTED' };
4453
4572
  }
@@ -4466,7 +4585,8 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4466
4585
  const requestedArgv = [...argvValidation.argv];
4467
4586
  argv = normalizeCliArgv(argv);
4468
4587
  const cliAgent = this.getOwningAgent?.(channel);
4469
- const cliSelfAid = cliAgent?.aid;
4588
+ const cliSelfAid = cliAgent?.aid ?? (argv[0] === 'aid' && argv[1] === 'bind'
4589
+ ? session?.selfAID ?? authSubject?.selfAid ?? ownerResolver.getDaemonAid() : undefined);
4470
4590
  const cliChannelType = this.resolveChannelType?.(channel);
4471
4591
  const cliChatType = session?.chatType ?? explicitChatType;
4472
4592
  const cliPeerKeyId = cliChatType === 'group'
@@ -4529,7 +4649,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4529
4649
  for (let i = 0; i < parsedCommands.length; i++) {
4530
4650
  const parsed = parsedCommands[i];
4531
4651
  const authArgv = parsed.resolvedConfigCommand?.canonicalArgv ?? intentArgvs[i] ?? argv;
4532
- const decision = await authorizeOperation({
4652
+ const decision = await authorizeFreshOperation({
4533
4653
  source: 'menu.cli',
4534
4654
  intent: parsed.intent,
4535
4655
  subject,
@@ -4562,7 +4682,35 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4562
4682
  }
4563
4683
  authorizedCommands.push({ parsed, decision });
4564
4684
  }
4565
- const rawExecution = await this.execCliPassthrough(argv, subject.role);
4685
+ const executionIntent = parsedCommands[0]?.intent;
4686
+ const ownershipOperation = executionIntent?.operation;
4687
+ let rawExecution;
4688
+ if (ownershipOperation === 'ec.aid.bind' || ownershipOperation === 'ec.aid.bind.status') {
4689
+ try {
4690
+ rawExecution = { data: { ok: true, state: await executeOwnershipCommand(parseOwnershipCommand(argv), cliSelfAid) } };
4691
+ }
4692
+ catch (error) {
4693
+ const failure = ownershipFailure(error);
4694
+ const code = ['INVALID_ARGUMENT', 'INVALID_AID', 'INVALID_OWNER'].includes(failure.code) ? 'INVALID_ARGUMENT'
4695
+ : ['SUBJECT_REQUIRED', 'SUBJECT_UNAVAILABLE'].includes(failure.code) ? 'NOT_FOUND'
4696
+ : ['OWNER_ALREADY_BOUND', 'BIND_CONFLICT', 'BIND_UNCERTAIN', 'CONSENSUS_REQUIRED', 'CLIENT_REPLACED', 'conflict', 'owner_locked'].includes(failure.code)
4697
+ ? 'CONFLICT' : 'INTERNAL_ERROR';
4698
+ rawExecution = { error: failure.error, code, data: failure };
4699
+ }
4700
+ }
4701
+ else if (executionIntent?.operation === 'ec.thread.summary-show'
4702
+ || executionIntent?.operation === 'ec.thread.summary-log') {
4703
+ rawExecution = await this.executeThreadSummaryCli({
4704
+ action: String(executionIntent.args.action),
4705
+ selfAid: String(executionIntent.args.self),
4706
+ peer: String(executionIntent.args.peer),
4707
+ threadId: String(executionIntent.args.threadId),
4708
+ authenticatedSelfAid: subject.selfAid ?? '',
4709
+ });
4710
+ }
4711
+ else {
4712
+ rawExecution = await this.execCliPassthrough(argv, subject.role);
4713
+ }
4566
4714
  const execution = appendGroupRulesCompatibilityNote(rawExecution, parsedCommands[0]?.resolvedConfigCommand, session);
4567
4715
  const executionData = 'data' in execution ? execution.data : undefined;
4568
4716
  for (const { parsed, decision } of authorizedCommands) {
@@ -4575,7 +4723,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4575
4723
  channel, channelId, role: identity.role, isDaemonOwner,
4576
4724
  fromControlChannel: fromControlChannel ?? false, decision: 'allow',
4577
4725
  executed: true,
4578
- executionState: executionData?.exitCode === undefined || executionData.exitCode === 0 ? 'completed' : 'failed',
4726
+ executionState: 'error' in execution || executionData?.ok === false || (executionData?.exitCode !== undefined && executionData.exitCode !== 0) ? 'failed' : 'completed',
4579
4727
  matchedRule: decision.command?.matchedRule, taskId: requestId, argvHash: hashArgv(argv),
4580
4728
  name: 'cli', action: 'exec',
4581
4729
  args: { argv: [...requestedArgv] },
@@ -4589,6 +4737,41 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4589
4737
  // fetch 文件作为独立 result.file 消息异步发回;把请求 id 作为 correlationId 透传,
4590
4738
  // 客户端用它把异步到达的文件消息对回这次 fetch 点击。
4591
4739
  if (cmdBase === '/file') {
4740
+ if (action === 'search') {
4741
+ for (const operation of ['file.list', ...(args.content !== undefined ? ['file.fetch'] : [])]) {
4742
+ const denied = await authorizeMenuIntent.call(this, {
4743
+ intent: { operation, scope: 'filesystem', source, args: { ...args, filePath: args.path ?? '.' } },
4744
+ identity: authIdentity, subject, session: authSession, explicitChatType,
4745
+ channel, channelId, userId, fromControlChannel, source,
4746
+ });
4747
+ if (denied)
4748
+ return denied;
4749
+ }
4750
+ const projectPath = session?.projectPath ?? evolagent?.projectPath ?? this.getOwningAgent?.(channel)?.projectPath;
4751
+ try {
4752
+ const data = await searchProjectFiles(projectPath, args, {
4753
+ canAccessFile: async (relative, absolute, isDirectory) => {
4754
+ const filePath = path.isAbsolute(args.path ?? '.') ? absolute : relative;
4755
+ const operations = [
4756
+ 'file.search', 'file.list', ...(!isDirectory && args.content !== undefined ? ['file.fetch'] : []),
4757
+ ];
4758
+ for (const operation of operations) {
4759
+ const decision = await authorizeOperation({
4760
+ source, subject, audit: false,
4761
+ intent: { operation, scope: 'filesystem', source, args: { ...args, path: filePath, filePath } },
4762
+ });
4763
+ if (!decision.allow)
4764
+ return false;
4765
+ }
4766
+ return true;
4767
+ },
4768
+ });
4769
+ return { data };
4770
+ }
4771
+ catch (error) {
4772
+ return menuResultFailure(error);
4773
+ }
4774
+ }
4592
4775
  if (action === 'list') {
4593
4776
  const dirArg = (args?.path ?? '.').toString().trim() || '.';
4594
4777
  const offset = parseFileListOffset(args?.offset);
@@ -4739,7 +4922,7 @@ export function resolveControlMenuAuthorization(input) {
4739
4922
  return {
4740
4923
  allowed: false,
4741
4924
  code: 'NOT_ALLOWED',
4742
- message: formatAuthorizationDenial('Configure daemon.json.owners before using Control Menu operations.', 'DAEMON_OWNER_REQUIRED'),
4925
+ message: formatAuthorizationDenial('Verify the protocol Owner of the daemon Control AID before using Control Menu operations.', 'DAEMON_OWNER_REQUIRED'),
4743
4926
  data: authorizationDenialData('DAEMON_OWNER_REQUIRED'),
4744
4927
  };
4745
4928
  }
@@ -4771,7 +4954,7 @@ async function execMenuForSystemControl(payload, context) {
4771
4954
  // operation is available through Host/control: file operations must be
4772
4955
  // addressed to the current Agent AID so the Agent project boundary applies.
4773
4956
  const isFileFetch = (payload?.type === 'menu.query'
4774
- || (payload?.type === 'menu.action' && payload?.action === 'fetch'))
4957
+ || (payload?.type === 'menu.action' && (payload?.action === 'fetch' || payload?.action === 'search')))
4775
4958
  && (name === 'file' || compatCmd === '/file');
4776
4959
  const isDataRootQuery = payload?.type === 'menu.query'
4777
4960
  && (name === 'agent-data-root'
@@ -4973,6 +5156,7 @@ async function execMenuForSystemControl(payload, context) {
4973
5156
  }
4974
5157
  /** ECWeb 专用入口:只信任 IPC 信封中的服务端认证上下文,不信任 Menu payload 自报身份。 */
4975
5158
  export async function execMenuForEcweb(payload, trusted) {
5159
+ await refreshOwnershipForAuthorization(undefined, { maxAgeMs: OWNERSHIP_CACHE_MAX_AGE_MS });
4976
5160
  const id = payload?.id ?? '';
4977
5161
  const name = payload?.name;
4978
5162
  const validationError = validateMenuRequest(payload ?? {});
@@ -4988,7 +5172,7 @@ export async function execMenuForEcweb(payload, trusted) {
4988
5172
  // ECWeb authentication grants daemon-owner authority independently of the
4989
5173
  // configured owners. Prefer an owner for audit/Trigger attribution when
4990
5174
  // available; otherwise keep local and remote management actors distinct.
4991
- const owners = loadDaemonConfig().owners ?? [];
5175
+ const owners = getDaemonOwners();
4992
5176
  const userId = (trusted.actorAid && owners.includes(trusted.actorAid))
4993
5177
  ? trusted.actorAid
4994
5178
  : owners[0] ?? (trusted.localDirect ? 'local-direct' : 'ecweb-remote');
@@ -5016,12 +5200,13 @@ export async function execMenuForEcweb(payload, trusted) {
5016
5200
  * process authorization path. The authenticated envelope peerId remains the
5017
5201
  * requester; token mode maps to a configured owner as its management actor. */
5018
5202
  export async function execMenuForControl(payload, peerId, auth = {}) {
5203
+ await refreshOwnershipForAuthorization(undefined, { maxAgeMs: OWNERSHIP_CACHE_MAX_AGE_MS });
5019
5204
  const id = payload?.id ?? '';
5020
5205
  const name = payload?.name;
5021
5206
  const validationError = validateMenuRequest(payload ?? {});
5022
5207
  if (validationError)
5023
5208
  return menuFailure({ id, ...(name ? { name } : {}) }, validationError);
5024
- const owners = loadDaemonConfig().owners ?? [];
5209
+ const owners = getDaemonOwners();
5025
5210
  const compatCmd = menuCommandToken(payload?.cmd);
5026
5211
  const isRoleRequest = name === 'role' || compatCmd === '/role';
5027
5212
  const isConnectRequest = name === 'connect' || compatCmd === '/connect';