evolcore 0.0.35 → 0.0.36

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 +31 -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 +134 -48
  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 +109 -34
  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
@@ -1,34 +1,37 @@
1
+ import { parsePermissionApprovalDecision } from '../permission/approval-gateway.js';
1
2
  import { hasModelSwitcher, hasPermissionController } from '../../agents/runner-types.js';
2
3
  import { normalizeBaseagent } from '../../agents/baseagent.js';
3
4
  import { buildEnvelope, isDeliveryTargetForChannel } from '../message/message-utils.js';
4
5
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
5
6
  import { logger } from '../../utils/logger.js';
6
- import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
7
+ import { shouldSuppressRealRestart, createRestartRequest, updateRestartRequest, removeRestartRequest } from '../../utils/restart-safety.js';
8
+ import { RESTART_REQUEST_ID_ENV } from '../../product.js';
7
9
  import crypto from 'crypto';
8
10
  import path from 'path';
9
11
  import fs from 'fs';
10
12
  import { ensureFreshPackageVersionInfo, refreshManagedPackageVersions } from '../../utils/package-version-info.js';
11
- import { loadDaemonConfig } from '../../config-store.js';
12
13
  import { remoteAccessService } from '../auth/remote-access-store.js';
13
14
  import { read as cfgRead, resolveEffective, routeFieldPath, write as cfgWrite, } from '../../config/config-manager.js';
14
15
  import { execAgentAction } from './agent-control.js';
15
16
  import { handleMemorySlash, memoryHelpEntries } from './memory-slash.js';
16
17
  import { handleSkillSlash, skillHelpEntries } from './skill-slash.js';
17
18
  import { handleTopicSlash, topicHelpEntries, canUseTopicSlash } from './topic-slash.js';
18
- import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
19
+ import { authorizeOperation, authorizeFreshOperation, buildAuthSubject } from '../auth/auth-gateway.js';
20
+ import { getDaemonOwners, ownerResolver, refreshOwnershipForAuthorization, OWNERSHIP_CACHE_MAX_AGE_MS } from '../ownership/owner-resolver.js';
19
21
  import { authorizationNextStep, formatAuthorizationDenial } from '../auth/authorization-denial.js';
20
22
  import { resolvePermissionMode, writeScope } from '../model/config-scope.js';
21
23
  import { formatPeerKey } from '../relation/peer-identity.js';
24
+ import { isMainSession, isTopicSession, isTopicThreadId, sessionTopicThreadId } from '../thread/topic-id.js';
22
25
  import { modelMatches, getCatalog, assertCatalogSelectable, resolveCatalogModel, CONFIGURABLE_EFFORTS, configurableEfforts } from '../model/model-catalog.js';
23
26
  import { formatModelCheck, runModelCheck } from '../model/model-diagnostics.js';
24
27
  import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
25
28
  import { resolveRuntimeStringField } from '../role/runtime-policy.js';
26
29
  import { displaySessionTitle } from '../session/session-title.js';
27
- import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
30
+ import { chatmodeFieldForPeer, isPrivateChatmodeRoleLocked, resolveChatModeForField } from '../message/peer-mode.js';
28
31
  import { normalizePermissionMode as normalizePermissionModeContract, PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
29
32
  import { isManagementRole } from '../../config/builtin-roles.js';
30
33
  import { isSystemControlChannel } from '../system-channels.js';
31
- import { launchMonitorAndConfirm, resolveMonitorLaunch, cleanupMonitorStage } from '../../utils/cross-platform.js';
34
+ import { launchMonitorAndConfirm, resolveMonitorLaunch, cleanupMonitorStage, isProcessRunning } from '../../utils/cross-platform.js';
32
35
  import { scanInstances } from '../../utils/instance-registry.js';
33
36
  import { guardIdleCommand, guardGroupSlashCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, probeSessionActivity, } from './slash-gate.js';
34
37
  import { MAX_AUN_ATTACHMENT_SIZE } from '../../aun/msg/upload.js';
@@ -60,13 +63,14 @@ function resolveSlashChatmodeTarget(params) {
60
63
  const field = slashChatmodeField(params.session, params.chatType);
61
64
  return {
62
65
  sel: { self, peerKey: formatPeerKey(channelType, peerId) },
66
+ role: params.role,
63
67
  field,
64
68
  fieldPath: `chatmode.${field}`,
65
69
  };
66
70
  }
67
71
  function readSlashChatmode(target) {
68
72
  try {
69
- const mode = resolveChatModeForField({ ...target.sel, field: target.field });
73
+ const mode = resolveChatModeForField({ ...target.sel, role: target.role, field: target.field });
70
74
  return mode === 'interactive' || mode === 'proactive'
71
75
  ? mode
72
76
  : defaultSlashChatmode(target.field);
@@ -192,7 +196,7 @@ async function authorizeSlashIntent(params) {
192
196
  if (intent.scope === 'relation') {
193
197
  intent.args = buildSlashRelationIntentArgs({ args: intent.args, selfAid: subject.selfAid, peerKey: subject.peerKey ?? peerKey });
194
198
  }
195
- const decision = await authorizeOperation({ source: 'slash', intent, subject });
199
+ const decision = await authorizeFreshOperation({ source: 'slash', intent, subject });
196
200
  if (!decision.allow) {
197
201
  const reasonCode = decision.command?.reasonCode;
198
202
  return {
@@ -260,7 +264,7 @@ async function canReadSlashModelList(params) {
260
264
  processOwners: isDaemonOwner && userId ? [userId] : [],
261
265
  fromControlChannel: false,
262
266
  });
263
- const decision = await authorizeOperation({ source: 'slash', intent, subject, audit: false });
267
+ const decision = await authorizeFreshOperation({ source: 'slash', intent, subject, audit: false });
264
268
  return decision.allow;
265
269
  }
266
270
  function getAgentBusyInfo(handler, aid, excludeSessionKey) {
@@ -394,6 +398,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
394
398
  chatType = undefined;
395
399
  // 解析身份(按实例名)
396
400
  const narrowedChatType = chatType === 'group' ? 'group' : chatType === 'private' ? 'private' : undefined;
401
+ await refreshOwnershipForAuthorization(selfAID ?? this.agentRegistry?.resolveByChannel(channel)?.aid ?? this.resolveSelfAID(channel), {
402
+ maxAgeMs: OWNERSHIP_CACHE_MAX_AGE_MS,
403
+ });
397
404
  const identityConversationId = narrowedChatType === 'group' ? channelId : userId;
398
405
  const resolvedIdentity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, narrowedChatType, identityConversationId);
399
406
  const authSubject = overrideSubject ?? buildAuthSubject({
@@ -407,7 +414,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
407
414
  chatType: narrowedChatType,
408
415
  conversationId: identityConversationId,
409
416
  identity: resolvedIdentity,
410
- processOwners: loadDaemonConfig().owners ?? [],
417
+ processOwners: getDaemonOwners(),
411
418
  fromControlChannel: false,
412
419
  });
413
420
  const identity = authSubject.identity;
@@ -681,7 +688,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
681
688
  return { ok: false, reason: 'unknown' };
682
689
  }
683
690
  }
684
- if (!current || current.threadId)
691
+ if (!current || isTopicSession(current))
685
692
  return { ok: false, reason: 'missing' };
686
693
  // Preserve the legacy main-session /compact exclusion. Topic commands
687
694
  // use the stronger barrier below; main sessions retain the historical
@@ -712,7 +719,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
712
719
  logger.warn(`[CommandHandler] session snapshot read failed: ${error instanceof Error ? error.message : String(error)}`);
713
720
  return { ok: false, reason: 'unknown' };
714
721
  }
715
- if (!current || (current.threadId || undefined) !== expectedThreadId) {
722
+ // expectedThreadId 到这里一定是话题 id,用线上形态比较。
723
+ if (!current || sessionTopicThreadId(current) !== expectedThreadId) {
716
724
  return { ok: false, reason: 'missing' };
717
725
  }
718
726
  const activity = probeBackendActivity(current, snapshot);
@@ -789,7 +797,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
789
797
  '',
790
798
  '🔐 权限管理:',
791
799
  ...(canReadPermission ? [' /perm - 查看当前权限模式'] : []),
792
- ...(canAnswerPermission ? [' /perm allow|always|deny - 审批权限请求'] : []),
800
+ ...(canAnswerPermission ? [' /perm <动作> - 审批权限请求(按授权卡选项)'] : []),
793
801
  ] : []),
794
802
  '',
795
803
  '其他:',
@@ -832,7 +840,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
832
840
  '',
833
841
  '🔐 权限管理:',
834
842
  ...(canReadPermission ? [' /perm - 查看当前权限模式'] : []),
835
- ...(canAnswerPermission ? [' /perm allow|always|deny - 审批权限请求'] : []),
843
+ ...(canAnswerPermission ? [' /perm <动作> - 审批权限请求(按授权卡选项)'] : []),
836
844
  ] : []),
837
845
  ...(canUseFile ? [
838
846
  '',
@@ -885,7 +893,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
885
893
  '🔐 权限管理:',
886
894
  ...(canReadPermission ? [' /perm - 查看当前权限模式'] : []),
887
895
  ...(canReadPermission ? [' 权限模式请通过角色策略编辑器修改'] : []),
888
- ...(canAnswerPermission ? [' /perm allow|always|deny - 审批权限请求'] : []),
896
+ ...(canAnswerPermission ? [' /perm <动作> - 审批权限请求(按授权卡选项)'] : []),
889
897
  '',
890
898
  '🛠️ 运维:',
891
899
  ' /status - 显示会话状态',
@@ -1027,9 +1035,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1027
1035
  if (canReadPermission)
1028
1036
  cmds.push({ command: '/perm', description: '查看当前角色决定的权限模式', category: '权限管理', roles: [identity.role] });
1029
1037
  if (canAnswerPermission)
1030
- cmds.push({ command: '/perm', args: 'allow|always|deny', description: '审批权限请求', category: '权限管理', roles: [identity.role] });
1038
+ cmds.push({ command: '/perm', args: '[requestId] <动作>', description: '审批权限请求(按授权卡选项,支持文件/目录 60 分钟)', category: '权限管理', roles: [identity.role] });
1031
1039
  // 记忆管理(紧跟权限管理):能一键执行的都展开成无参数条目
1032
- const memoryOwnerForHelp = this.agentRegistry?.get?.(authSubject.selfAid ?? '')?.config?.owners?.[0];
1040
+ const memoryOwnerForHelp = ownerResolver.primaryOwner(authSubject.selfAid ?? '');
1033
1041
  cmds.push(...memoryHelpEntries({
1034
1042
  chatType: 'private',
1035
1043
  role: identity.role,
@@ -1089,6 +1097,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1089
1097
  const permissionDecisionLabels = {
1090
1098
  allow: '✓ 已授权(本次),继续执行……',
1091
1099
  always: '✓ 已授权(当前会话临时授权 30 分钟),继续执行……',
1100
+ file_60m: '✓ 已授权(同文件 60 分钟),继续执行……',
1101
+ directory_60m: '✓ 已授权(当前目录及子目录 60 分钟),继续执行……',
1102
+ all_directories_60m: '✓ 已授权(当前会话所有普通目录 60 分钟),继续执行……',
1092
1103
  deny: '✓ 已拒绝',
1093
1104
  };
1094
1105
  // Explicit request IDs are globally unique and already bound to an exact
@@ -1096,11 +1107,11 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1096
1107
  // lookup so an owner can answer a handoff in an otherwise idle AUN chat.
1097
1108
  const explicitParts = args.split(/\s+/);
1098
1109
  if (explicitParts.length === 2
1099
- && (explicitParts[1] === 'allow' || explicitParts[1] === 'always' || explicitParts[1] === 'deny')) {
1110
+ && parsePermissionApprovalDecision(explicitParts[1])) {
1100
1111
  if (!this.permissionGateway) {
1101
1112
  return { kind: 'command.error', text: '❌ 权限审批未启用' };
1102
1113
  }
1103
- const decision = explicitParts[1];
1114
+ const decision = parsePermissionApprovalDecision(explicitParts[1]);
1104
1115
  const resolved = this.permissionGateway.resolvePermissionByRequestId(explicitParts[0], decision, userId, channel);
1105
1116
  if (!resolved) {
1106
1117
  return { kind: 'command.error', text: '❌ 请求不存在、无法验证审批人身份或审批资格已失效' };
@@ -1181,9 +1192,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1181
1192
  // /perm <mode> 不再修改配置;allow/always/deny 仍用于快捷审批。
1182
1193
  if (parts.length === 1) {
1183
1194
  const arg = parts[0];
1184
- // /perm allow|always|deny:快捷审批
1185
- if (arg === 'allow' || arg === 'always' || arg === 'deny') {
1186
- const authDenied = await authorizePermissionAnswer(arg);
1195
+ // /perm <action>: approve the sole pending request with the offered action.
1196
+ const decision = parsePermissionApprovalDecision(arg);
1197
+ if (decision) {
1198
+ const authDenied = await authorizePermissionAnswer(decision);
1187
1199
  if (authDenied)
1188
1200
  return authDenied;
1189
1201
  if (!this.permissionGateway) {
@@ -1197,7 +1209,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1197
1209
  return { kind: 'command.error', text: `❌ 当前有 ${pendingIds.length} 个待审批请求,请指定 requestId:\n${pendingIds.map((id) => ` /perm ${id} ${arg}`).join('\n')}` };
1198
1210
  }
1199
1211
  const requestId = pendingIds[0];
1200
- return resolvePermissionAnswer(requestId, arg);
1212
+ return resolvePermissionAnswer(requestId, decision);
1201
1213
  }
1202
1214
  // /perm <mode>:提示改用角色策略编辑器。
1203
1215
  if (hasPermissionController(permAgent)) {
@@ -1211,9 +1223,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1211
1223
  if (PERMISSION_MODE_KEYS.includes(arg)) {
1212
1224
  return { kind: 'command.error', text: '❌ permissionMode 只能通过角色策略编辑器修改' };
1213
1225
  }
1214
- return { kind: 'command.error', text: `❌ 未知参数: ${arg}\n用法: /perm 或 /perm allow|always|deny` };
1226
+ return { kind: 'command.error', text: `❌ 未知参数: ${arg}\n用法: /perm [requestId] <动作>;动作以授权卡为准:allow|file_60m|directory_60m|all_directories_60m|always|deny` };
1215
1227
  }
1216
- return { kind: 'command.error', text: `❌ 未知参数: ${args}\n用法: /perm、/perm allow|always|deny 或 /perm <requestId> allow|always|deny` };
1228
+ return { kind: 'command.error', text: `❌ 未知参数: ${args}\n用法: /perm [requestId] <动作>;动作以授权卡为准:allow|file_60m|directory_60m|all_directories_60m|always|deny` };
1217
1229
  }
1218
1230
  // /ask 命令:回答 AskUserQuestion / ExitPlanMode 的交互式问题
1219
1231
  if (normalizedContent.startsWith('/ask')) {
@@ -1327,8 +1339,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1327
1339
  }
1328
1340
  const currentSession = await getExistingSessionForCommand();
1329
1341
  const previousBaseagent = owningAgent.baseagent || this.parseDefaultBaseagent();
1330
- if (currentSession?.threadId) {
1331
- const rotation = await withIdleTopicBackend(currentSession.id, currentSession.threadId, async (current) => {
1342
+ const currentTopicThreadId = sessionTopicThreadId(currentSession);
1343
+ if (currentSession && currentTopicThreadId) {
1344
+ const rotation = await withIdleTopicBackend(currentSession.id, currentTopicThreadId, async (current) => {
1332
1345
  try {
1333
1346
  owningAgent.setActiveBaseagent(args);
1334
1347
  }
@@ -1338,7 +1351,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1338
1351
  try {
1339
1352
  const result = await this.sessionManager.rotateTopicBackend({
1340
1353
  sessionId: current.id,
1341
- expectedThreadId: current.threadId,
1354
+ // withIdleTopicBackend 已校验 sessionTopicThreadId(current) === currentTopicThreadId
1355
+ expectedThreadId: currentTopicThreadId,
1342
1356
  baseagent: args,
1343
1357
  });
1344
1358
  return { ok: true, result };
@@ -2286,6 +2300,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2286
2300
  if (arg === currentMode) {
2287
2301
  return { kind: 'command.result', text: `📋 当前会话模式已是 ${arg}` };
2288
2302
  }
2303
+ if (arg === 'interactive' && isPrivateChatmodeRoleLocked(target.field, target.role)) {
2304
+ return { kind: 'command.error', text: '❌ 私聊交互模式仅限 owner/admin,当前角色固定为 proactive' };
2305
+ }
2289
2306
  // 仅在真正需要切换时才要求会话空闲
2290
2307
  if (chatmodeSession && threadId) {
2291
2308
  const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
@@ -2473,7 +2490,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2473
2490
  if (normalizedContent !== '/renew') {
2474
2491
  return { kind: 'system.error', text: '❌ /renew 不接受参数,请直接使用 /renew', subtype: 'session.renew.invalid_arguments', recoverable: true };
2475
2492
  }
2476
- if (!threadId) {
2493
+ if (!isTopicThreadId(threadId)) {
2477
2494
  return { kind: 'system.error', text: '⚠️ /renew 仅支持话题会话', subtype: 'session.renew.unsupported', recoverable: true };
2478
2495
  }
2479
2496
  // Card callbacks use synthetic message IDs on several adapters. They are
@@ -2515,7 +2532,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2515
2532
  logger.warn(`[CommandHandler] /renew strict session read failed: session=${renewSession.id}: ${error instanceof Error ? error.message : String(error)}`);
2516
2533
  return { kind: 'unknown' };
2517
2534
  }
2518
- if (!current || current.threadId !== threadId)
2535
+ if (!current || sessionTopicThreadId(current) !== threadId)
2519
2536
  return { kind: 'missing' };
2520
2537
  let claim;
2521
2538
  try {
@@ -2580,9 +2597,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2580
2597
  ? { kind: 'command.error', text: '⚠️ 当前会话状态未知,无法安全压缩,请稍后再试' }
2581
2598
  : { kind: 'command.error', text: '❌ 当前没有活跃会话,无需压缩' };
2582
2599
  }
2583
- if (!session.threadId)
2600
+ if (isMainSession(session))
2584
2601
  await interruptPausedSessionBeforeReplacement(session);
2585
- const outcome = await withIdleTopicBackend(session.id, session.threadId || undefined, async (session) => {
2602
+ const outcome = await withIdleTopicBackend(session.id, sessionTopicThreadId(session), async (session) => {
2586
2603
  if (!session.agentSessionId) {
2587
2604
  return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需压缩' };
2588
2605
  }
@@ -2711,7 +2728,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2711
2728
  const sessionAgent = this.getAgent(channel, session.baseagent);
2712
2729
  const isCurrentlyProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
2713
2730
  const queueLength = this.messageQueue.getQueueLength(sessionKey);
2714
- const isThread = !!session.threadId;
2731
+ const isThread = isTopicSession(session);
2715
2732
  let sessionStatus = isCurrentlyProcessing ? '处理中' : '空闲';
2716
2733
  // 处理中时显示时长
2717
2734
  if (isCurrentlyProcessing) {
@@ -2833,7 +2850,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2833
2850
  sourceChannel: sourceForConsolidation?.channelType || channel,
2834
2851
  sourceChannelId: channelId,
2835
2852
  sourceChatType: activeChatType,
2836
- sourceThreadId: sourceForConsolidation?.threadId || undefined,
2853
+ sourceThreadId: sessionTopicThreadId(sourceForConsolidation),
2837
2854
  sourceSelfAid: sourceForConsolidation?.selfAID
2838
2855
  || selfAID
2839
2856
  || this.getOwningAgent(channel)?.aid
@@ -3103,7 +3120,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3103
3120
  }
3104
3121
  // /restart 命令:重启服务(进程级,仅 daemon owner)
3105
3122
  if (normalizedContent === '/restart') {
3106
- // 进程级操作:必须是 daemon owner(daemon.json.owners),与 menu 协议 /system restart 一致。
3123
+ // 进程级操作:必须是已核验的 Control AID 协议 Owner,与 menu 协议 /system restart 一致。
3107
3124
  // agent-channel 的 owner/admin 角色不足以重启整个 daemon。
3108
3125
  const restartSelfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
3109
3126
  const commandSession = await getExistingSessionForCommand();
@@ -3207,6 +3224,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3207
3224
  channel,
3208
3225
  channelId,
3209
3226
  timestamp: Date.now(),
3227
+ requestId: crypto.randomUUID(),
3228
+ requesterPid: process.pid,
3229
+ status: 'requested',
3210
3230
  ...(restartReplyContext?.threadId ? { threadId: restartReplyContext.threadId } : {}),
3211
3231
  ...(restartReplyContext?.delivery ? { delivery: restartReplyContext.delivery } : {}),
3212
3232
  ...(restartReplyContext?.replyToMessageId ? { rootId: restartReplyContext.replyToMessageId } : {}),
@@ -3224,16 +3244,35 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3224
3244
  }
3225
3245
  const controlDir = daemonControlDir();
3226
3246
  fs.mkdirSync(controlDir, { recursive: true });
3227
- fs.writeFileSync(path.join(controlDir, 'restart-pending.json'), JSON.stringify(restartInfo));
3228
- const confirmation = await launchMonitorAndConfirm([...monitorEntry.args, 'restart-monitor'], { EVOLCORE_HOME: resolvePaths().root, ...monitorEntry.env }, pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid === pid), pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid !== pid));
3247
+ const pendingFile = path.join(controlDir, 'restart-pending.json');
3248
+ if (scanInstances().restartMonitors.some(item => item.alive)
3249
+ || !createRestartRequest(pendingFile, restartInfo, isProcessRunning)) {
3250
+ if (monitorEntry.stageDir)
3251
+ cleanupMonitorStage(monitorEntry.stageDir);
3252
+ return { code: 'RESTART_PENDING_CONFIRMATION', reason: '已有重启请求正在处理', requestId: restartInfo.requestId };
3253
+ }
3254
+ const confirmation = await launchMonitorAndConfirm([...monitorEntry.args, 'restart-monitor'], { EVOLCORE_HOME: resolvePaths().root, ...monitorEntry.env, [RESTART_REQUEST_ID_ENV]: restartInfo.requestId }, pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid === pid
3255
+ && m.record.requestId === restartInfo.requestId && m.record.phase === 'accepted'), pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid !== pid
3256
+ && m.record.requestId === restartInfo.requestId && m.record.phase === 'accepted'), {
3257
+ requestId: restartInfo.requestId,
3258
+ logFilePath: path.join(resolvePaths().logs, `restart-launch-${restartInfo.requestId}.log`),
3259
+ onSpawn: pid => { updateRestartRequest(pendingFile, restartInfo.requestId, { launcherPid: pid }); },
3260
+ isPendingInRegistry: () => scanInstances().restartMonitors.some(m => m.alive && m.record.requestId === restartInfo.requestId),
3261
+ });
3229
3262
  if (!confirmation.ok) {
3263
+ if (confirmation.pending) {
3264
+ logger.warn(`[System] Restart ownership remains unconfirmed (requestId=${restartInfo.requestId}, PID=${confirmation.pid ?? 'unknown'})`);
3265
+ return {
3266
+ code: 'RESTART_PENDING_CONFIRMATION',
3267
+ reason: confirmation.reason ?? 'ownership confirmation timed out',
3268
+ requestId: restartInfo.requestId,
3269
+ ...(confirmation.pid ? { pid: confirmation.pid } : {}),
3270
+ };
3271
+ }
3230
3272
  // 没有看门狗就不要停服务:撤掉回执文件,让本进程继续跑。
3231
3273
  if (monitorEntry.stageDir)
3232
3274
  cleanupMonitorStage(monitorEntry.stageDir);
3233
- try {
3234
- fs.rmSync(path.join(controlDir, 'restart-pending.json'), { force: true });
3235
- }
3236
- catch { }
3275
+ removeRestartRequest(pendingFile, restartInfo.requestId);
3237
3276
  logger.error(`[System] Refusing restart: restart-monitor did not come up (${confirmation.reason})`);
3238
3277
  for (const line of (confirmation.stderrTail ?? '').split(/\r?\n/).filter(Boolean).slice(-8)) {
3239
3278
  logger.error(`[System] | ${line}`);
@@ -3274,7 +3313,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3274
3313
  },
3275
3314
  },
3276
3315
  });
3277
- await adapter.send(envelope, { kind: 'command.result', text: '🔄 服务正在重启,请稍候...(约 5 秒后恢复)' });
3316
+ await adapter.send(envelope, { kind: 'command.result', text: '🔄 重启监控已接管,服务正在重启;完成后会另行通知。' });
3278
3317
  // 等待消息发送完成后再延迟 kill
3279
3318
  await new Promise(resolve => setTimeout(resolve, 500));
3280
3319
  }
@@ -3318,6 +3357,13 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3318
3357
  reason: restarted.code,
3319
3358
  };
3320
3359
  }
3360
+ if (typeof restarted === 'object' && restarted.code === 'RESTART_PENDING_CONFIRMATION') {
3361
+ return {
3362
+ kind: 'command.error',
3363
+ text: `⚠️ 重启请求状态暂不可确认(requestId=${restarted.requestId})。监控进程仍在运行,可能继续执行;请勿立即重复发起。可稍后检查 restart.log 中该 requestId 的记录。`,
3364
+ reason: restarted.code,
3365
+ };
3366
+ }
3321
3367
  if (typeof restarted === 'object' && restarted.code === 'RESTART_MONITOR_DEAD') {
3322
3368
  return {
3323
3369
  kind: 'command.error',
@@ -3577,7 +3623,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3577
3623
  }
3578
3624
  // /slist — 仅显示 EvolCore 会话
3579
3625
  const sessions = await this.sessionManager.listSessions(channel, channelId);
3580
- const currentProjectSessions = sessions.filter((s) => s.projectPath === session.projectPath && s.baseagent === session.baseagent && !s.threadId);
3626
+ const currentProjectSessions = sessions.filter((s) => s.projectPath === session.projectPath && s.baseagent === session.baseagent && isMainSession(s));
3581
3627
  // 从 SDK 同步会话名称(发现 CLI 改名)
3582
3628
  try {
3583
3629
  const sdkSessions = await this.sessionManager.listSdkSessions(session.projectPath, session.baseagent, session.selfAID);
@@ -3701,7 +3747,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3701
3747
  if (!targetSession && /^\d+$/.test(sessionName) && session) {
3702
3748
  const idx = parseInt(sessionName, 10);
3703
3749
  const allSessions = await this.sessionManager.listSessions(channel, channelId);
3704
- const visibleSessions = allSessions.filter((s) => s.projectPath === session.projectPath && s.baseagent === session.baseagent && !s.threadId);
3750
+ const visibleSessions = allSessions.filter((s) => s.projectPath === session.projectPath && s.baseagent === session.baseagent && isMainSession(s));
3705
3751
  if (idx >= 1 && idx <= visibleSessions.length) {
3706
3752
  targetSession = visibleSessions[idx - 1];
3707
3753
  }
@@ -3712,7 +3758,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3712
3758
  if (!targetSession && sessionName.length >= 8) {
3713
3759
  targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
3714
3760
  }
3715
- if (targetSession?.threadId) {
3761
+ if (isTopicSession(targetSession)) {
3716
3762
  return { kind: 'command.error', text: `❌ 话题会话不支持通过 /s 切换\n请在对应话题内继续对话` };
3717
3763
  }
3718
3764
  const canImport = policy.canImportCliSession(session?.chatType || 'private', identity.role);
@@ -3764,7 +3810,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3764
3810
  return { kind: 'command.result', text: `当前已在会话: ${displaySessionTitle(targetSession.name, sessionName)}` };
3765
3811
  }
3766
3812
  // 阻止从主会话切换到话题会话
3767
- if (!session.threadId && targetSession.threadId) {
3813
+ if (isMainSession(session) && isTopicSession(targetSession)) {
3768
3814
  return { kind: 'command.error', text: `❌ 无法从主会话切换到话题会话\n话题会话仅在对应话题内可用` };
3769
3815
  }
3770
3816
  await interruptPausedSessionBeforeReplacement(session);
@@ -3809,14 +3855,15 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3809
3855
  });
3810
3856
  }
3811
3857
  // P1.2: 若当前是话题会话,同步更新话题索引
3812
- if (success && session.threadId) {
3858
+ const renamedTopicThreadId = sessionTopicThreadId(session);
3859
+ if (success && renamedTopicThreadId) {
3813
3860
  try {
3814
3861
  const { ThreadService } = await import('../../core/thread/index.js');
3815
3862
  const selfAid = session.selfAID;
3816
3863
  const peer = session.channelId;
3817
3864
  if (selfAid && peer) {
3818
3865
  const svc = new ThreadService(this.sessionManager);
3819
- await svc.renameThread({ selfAid, peer, threadId: session.threadId, name: newName });
3866
+ await svc.renameThread({ selfAid, peer, threadId: renamedTopicThreadId, name: newName });
3820
3867
  }
3821
3868
  }
3822
3869
  catch (err) {
@@ -3846,7 +3893,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3846
3893
  if (!targetSession && /^\d+$/.test(sessionName)) {
3847
3894
  const idx = parseInt(sessionName, 10);
3848
3895
  const allSessions = await this.sessionManager.listSessions(channel, channelId);
3849
- const visibleSessions = allSessions.filter((s) => s.projectPath === session.projectPath && s.baseagent === session.baseagent && !s.threadId);
3896
+ const visibleSessions = allSessions.filter((s) => s.projectPath === session.projectPath && s.baseagent === session.baseagent && isMainSession(s));
3850
3897
  if (idx >= 1 && idx <= visibleSessions.length) {
3851
3898
  targetSession = visibleSessions[idx - 1];
3852
3899
  }
@@ -3857,7 +3904,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3857
3904
  if (!targetSession && sessionName.length >= 8) {
3858
3905
  targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
3859
3906
  }
3860
- if (targetSession?.threadId) {
3907
+ if (isTopicSession(targetSession)) {
3861
3908
  return { kind: 'command.error', text: `❌ 请使用话题管理删除话题会话` };
3862
3909
  }
3863
3910
  if (!targetSession) {
@@ -3949,12 +3996,13 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3949
3996
  if (!['chat', 'file', 'all'].includes(mode)) {
3950
3997
  return { kind: 'command.error', text: `❌ 无效模式 "${mode}",可选:chat | file | all` };
3951
3998
  }
3952
- if (session.threadId) {
3999
+ const rewindTopicThreadId = sessionTopicThreadId(session);
4000
+ if (rewindTopicThreadId) {
3953
4001
  // 话题只开放 N>1;N=1 会清空 backend binding,仍保留原有禁止边界。
3954
4002
  if (turnNum === 1) {
3955
4003
  return { kind: 'command.error', text: '❌ 话题会话暂不支持 /rewind 1,仅开放 N>1' };
3956
4004
  }
3957
- const outcome = await withIdleTopicBackend(session.id, session.threadId, async (current) => {
4005
+ const outcome = await withIdleTopicBackend(session.id, rewindTopicThreadId, async (current) => {
3958
4006
  if (!current.baseagent) {
3959
4007
  return '❌ 当前会话缺少 baseagent 绑定,拒绝隐式切换';
3960
4008
  }
@@ -4020,7 +4068,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
4020
4068
  // A topic backend binding may only be changed by an explicit boundary
4021
4069
  // rotation/activation. Repair clears runner-local state, but must not
4022
4070
  // turn a legacy empty callback into an authoritative binding mutation.
4023
- if (!repairSession.threadId) {
4071
+ if (isMainSession(repairSession)) {
4024
4072
  await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
4025
4073
  }
4026
4074
  repairAgent.updateSessionId(repairSession.id, '');
@@ -4071,7 +4119,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
4071
4119
  // /trigger 命令
4072
4120
  if (normalizedContent === '/memory' || normalizedContent.startsWith('/memory ')) {
4073
4121
  const memoryAgent = this.agentRegistry?.get?.(authSubject.selfAid ?? '');
4074
- const memoryOwnerAid = memoryAgent?.config?.owners?.[0];
4122
+ const memoryOwnerAid = ownerResolver.primaryOwner(authSubject.selfAid ?? '');
4075
4123
  const memorySession = await getExistingSessionForCommand();
4076
4124
  return await handleMemorySlash({
4077
4125
  content: normalizedContent,
@@ -13,6 +13,8 @@
13
13
  * 不支持跨关系浏览以外的越权:非本关系的话题需要显式给出 peer,且按同一套角色判定。
14
14
  */
15
15
  import { ThreadService, ThreadServiceError } from '../thread/thread-service.js';
16
+ import { isTopicThreadId } from '../thread/topic-id.js';
17
+ import { parseTimeArg, TIME_ARG_FORMATS } from '../../cli/cli-argv.js';
16
18
  const TOPIC_STATUSES = ['pending', 'in_progress', 'completed', 'archived'];
17
19
  const USAGE = [
18
20
  '用法(更名与总结默认作用于当前话题,无需手写 ref):',
@@ -139,7 +141,7 @@ function formatThreadLine(t, currentThreadId) {
139
141
  formatTime(t.lastActiveAt),
140
142
  t.lastMsgSnippet ? truncate(t.lastMsgSnippet, 30) : '',
141
143
  ].filter(Boolean);
142
- return ` ${bits.join(' · ')}${mark}\n ref: ${t.peer}/${t.threadId} (${t.msgTotal ?? 0} 条)`;
144
+ return ` ${bits.join(' · ')}${mark}\n ref: ${t.threadId} (${t.msgTotal ?? 0} 条)`;
143
145
  }
144
146
  function formatThreadList(threads, opts, currentThreadId) {
145
147
  if (threads.length === 0)
@@ -241,15 +243,16 @@ export async function handleTopicSlash(ctx) {
241
243
  const target = resolveRef(ref, ctx, service);
242
244
  if (isPayload(target))
243
245
  return target;
244
- if (!target.threadId)
245
- return error('当前是主会话,不显示话题元数据;请指定 <peer>/<threadId>');
246
+ if (!isTopicThreadId(target.threadId)) {
247
+ return error('当前是主话题;/thread show 只显示话题元数据,请指定 threadId');
248
+ }
246
249
  const t = service.findInIndex(ctx.selfAid, target.peer, target.threadId);
247
250
  if (!t)
248
251
  return error(`话题不存在: ${target.peer}/${target.threadId}`);
249
252
  const summary = service.showSummary(ctx.selfAid, t.sessionId);
250
253
  const lines = [
251
254
  `话题: ${t.name}`,
252
- `ref: ${t.peer}/${t.threadId}`,
255
+ `ref: ${t.threadId}`,
253
256
  `session: ${t.sessionId}`,
254
257
  `对端: ${t.peer}(${t.peerKind === 'group' ? '群' : '私聊'})`,
255
258
  `状态: ${t.topicStatus}`,
@@ -269,11 +272,19 @@ export async function handleTopicSlash(ctx) {
269
272
  const limit = typeof flags.limit === 'string' ? parseInt(flags.limit, 10) : 20;
270
273
  if (!Number.isFinite(limit) || limit <= 0)
271
274
  return error('--limit 必须是正整数');
275
+ const after = typeof flags.after === 'string' ? parseTimeArg(flags.after) : undefined;
276
+ if (typeof flags.after === 'string' && after === undefined)
277
+ return error(`--after 不是有效时间(支持 ${TIME_ARG_FORMATS})`);
278
+ const before = typeof flags.before === 'string' ? parseTimeArg(flags.before) : undefined;
279
+ if (typeof flags.before === 'string' && before === undefined)
280
+ return error(`--before 不是有效时间(支持 ${TIME_ARG_FORMATS})`);
272
281
  const h = await service.historyThread({
273
282
  selfAid: ctx.selfAid,
274
283
  peer: target.peer,
275
284
  threadId: target.threadId,
276
285
  limit,
286
+ after,
287
+ before,
277
288
  });
278
289
  return result(formatHistory(h.messages, { total: h.total, peer: h.peer, threadId: h.threadId }));
279
290
  }
@@ -307,7 +318,7 @@ export async function handleTopicSlash(ctx) {
307
318
  ...(typeof flags.label === 'string' ? { label: flags.label } : {}),
308
319
  ...(status ? { topicStatus: status } : {}),
309
320
  });
310
- return result(`✅ 已创建话题: ${created.name}\nref: ${created.peer}/${created.threadId}`);
321
+ return result(`✅ 已创建话题: ${created.name}\nref: ${created.threadId}`);
311
322
  }
312
323
  if (sub === 'rename') {
313
324
  const ref = refSource(positional, flags);
@@ -12,6 +12,7 @@ import { removeAll } from '../utils/instance-registry.js';
12
12
  import { controlReplyContext } from './message/outbound.js';
13
13
  import { TriggerDefinitionManager } from '../trigger/manager.js';
14
14
  import { seedUpgradeCheckTrigger } from '../trigger/system-definitions.js';
15
+ import { ownerResolver } from './ownership/owner-resolver.js';
15
16
  export function detectAvailableBaseagents() {
16
17
  // 与 init/ECWeb 的探测保持同一判据;claude 在这里也走内置二进制,
17
18
  // 不再只看 PATH——否则 daemon 会对外声明一个实际跑不起来的 baseagent。
@@ -36,6 +37,11 @@ export async function runBindBootstrapDaemon(daemonCfg) {
36
37
  getUptimeSeconds: () => Math.floor(process.uptime()),
37
38
  });
38
39
  bindService.startCleanup();
40
+ ownerResolver.setDaemonAid(daemonCfg.aid);
41
+ ownerResolver.setConfiguredOwners(daemonCfg.aid, [
42
+ daemonCfg.owner,
43
+ ...(daemonCfg.owners ?? []),
44
+ ].filter((owner) => typeof owner === 'string'));
39
45
  let controlChannel;
40
46
  let shutdown;
41
47
  let pendingShutdownReason;
@@ -102,7 +108,7 @@ export async function runBindBootstrapDaemon(daemonCfg) {
102
108
  }));
103
109
  ipcServer.setControlToken(controlToken);
104
110
  ipcServer.setRemoteAccessService(remoteAccessService);
105
- ipcServer.setRemoteAccessOwnerProvider(() => daemonCfg.owners?.[0]);
111
+ ipcServer.setRemoteAccessOwnerProvider(() => ownerResolver.primaryOwner(daemonCfg.aid));
106
112
  ipcServer.setShutdownExecutor(async (reason) => {
107
113
  if (!shutdown) {
108
114
  pendingShutdownReason = reason;
@@ -10,6 +10,8 @@ import { isProcessLevelOwner } from './command/command-handler.js';
10
10
  import { onShutdown } from '../utils/cross-platform.js';
11
11
  import { logger } from '../utils/logger.js';
12
12
  import { resolvePaths } from '../paths.js';
13
+ import { ownerResolver, OWNERSHIP_AUTHORIZATION_MAX_AGE_MS, OWNERSHIP_CACHE_MAX_AGE_MS } from './ownership/owner-resolver.js';
14
+ import { subscribeUpgradeJob } from '../utils/upgrade-jobs.js';
13
15
  const controlMenuDeduper = new MenuRequestDeduper();
14
16
  export function startDaemonControlAid({ daemonCfg, cmdHandler, bindService, diagService, getProcessOwners, }) {
15
17
  if (!daemonCfg.aid)
@@ -36,6 +38,11 @@ export function startDaemonControlAid({ daemonCfg, cmdHandler, bindService, diag
36
38
  logger.warn(`控制 AID 首连失败(后台自动重连,不影响 daemon 主流程): ${e?.message || e}`);
37
39
  });
38
40
  controlChannel.onMessage(async (opts) => {
41
+ const text = (opts.content || '').trim();
42
+ await ownerResolver.refresh(daemonCfg.aid, {
43
+ maxAgeMs: /^\/(?:pair|diag)(?:\s|$)/i.test(text)
44
+ ? OWNERSHIP_AUTHORIZATION_MAX_AGE_MS : OWNERSHIP_CACHE_MAX_AGE_MS,
45
+ });
39
46
  let activeMenuRequest;
40
47
  let menuCompletionLogged = false;
41
48
  const menuFlowContext = {
@@ -50,7 +57,6 @@ export function startDaemonControlAid({ daemonCfg, cmdHandler, bindService, diag
50
57
  };
51
58
  try {
52
59
  const menuTiming = normalizeMenuResponseTiming(opts);
53
- const text = (opts.content || '').trim();
54
60
  let parsed;
55
61
  try {
56
62
  parsed = JSON.parse(text);
@@ -173,6 +179,18 @@ export function startDaemonControlAid({ daemonCfg, cmdHandler, bindService, diag
173
179
  ? menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, { code: 'CONFLICT', message: 'Request ID was already used with a different payload' })
174
180
  : deduped.value;
175
181
  await sendControlMenuResponse(response);
182
+ const upgradeData = 'data' in response ? response.data : undefined;
183
+ if (parsed?.type === 'menu.action' && parsed.action === 'upgrade' && (parsed.name === 'system' || parsed.cmd === '/system') && upgradeData?.jobId) {
184
+ const jobId = upgradeData.jobId;
185
+ subscribeUpgradeJob(jobId, async (event) => {
186
+ const permission = await cmdHandler.execMenuForControl({ type: 'menu.action', id: parsed.id, name: 'system', action: 'upgrade', args: { mode: 'status', jobId } }, opts.peerId, {
187
+ encrypted: opts.encrypted, protectedHeaders: opts.protectedHeaders,
188
+ });
189
+ if (permission.error)
190
+ throw new Error('Upgrade event authorization expired');
191
+ await controlChannel.sendStructuredOrThrow(opts.channelId, event, menuResponseContext());
192
+ }, [opts.channelId, opts.peerId, opts.peerDeviceId || '', jobId].join('\u001f'));
193
+ }
176
194
  return;
177
195
  }
178
196
  await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取远程控制配对码)、/diag <问题描述>、/diag stop', controlReplyContext(opts));