evolcore 0.0.32 → 0.0.33

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 (150) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/bin/install-codex-managed-hooks.mjs +124 -37
  3. package/dist/agents/claude-runner.js +29 -2
  4. package/dist/agents/codex/app-server-client.js +3 -0
  5. package/dist/agents/codex/diagnostics.js +76 -44
  6. package/dist/agents/codex-runner.js +23 -19
  7. package/dist/agents/ecagent-runner.js +5 -13
  8. package/dist/aun/group/operations.js +0 -1
  9. package/dist/aun/msg/history.js +1 -0
  10. package/dist/aun/msg/p2p.js +0 -1
  11. package/dist/aun/outbox.js +50 -1
  12. package/dist/channels/aun.js +28 -2
  13. package/dist/channels/daemon.js +257 -2
  14. package/dist/cli/agent-command.js +58 -24
  15. package/dist/cli/agent.js +1 -1
  16. package/dist/cli/aun-commands.js +365 -29
  17. package/dist/cli/contact.js +8 -2
  18. package/dist/cli/daemon-commands.js +81 -12
  19. package/dist/cli/debug-mode.js +154 -0
  20. package/dist/cli/handoff-command.js +3 -0
  21. package/dist/cli/index.js +81 -64
  22. package/dist/cli/init.js +2 -2
  23. package/dist/cli/managed-command-guard.js +5 -0
  24. package/dist/cli/memory-command.js +8 -1
  25. package/dist/cli/queue-command.js +9 -4
  26. package/dist/cli/session-command.js +194 -0
  27. package/dist/cli/skill-command.js +78 -2
  28. package/dist/cli/task-context.js +1 -0
  29. package/dist/cli/thread-command.js +620 -0
  30. package/dist/cli/trigger-command.js +4 -2
  31. package/dist/config/builtin-roles.js +1 -0
  32. package/dist/config/config-field-policy.js +2 -3
  33. package/dist/config/config-manager.js +7 -2
  34. package/dist/config/resolved-config-op.js +3 -0
  35. package/dist/config-store.js +15 -0
  36. package/dist/core/auth/agent-delegation.js +25 -1
  37. package/dist/core/auth/debug-context.js +139 -0
  38. package/dist/core/auth/operation-authorizer.js +24 -1
  39. package/dist/core/auth/operation-catalog.js +33 -0
  40. package/dist/core/background-session/session-cleanup.js +212 -0
  41. package/dist/core/background-session/types.js +10 -0
  42. package/dist/core/bootstrap-messages.js +1 -1
  43. package/dist/core/bootstrap-service.js +6 -3
  44. package/dist/core/command/command-handler.js +60 -24
  45. package/dist/core/command/memory-slash.js +6 -2
  46. package/dist/core/command/menu-catalog.js +48 -1
  47. package/dist/core/command/menu-handler.js +92 -16
  48. package/dist/core/command/menu-protocol.js +24 -1
  49. package/dist/core/command/skill-slash.js +322 -0
  50. package/dist/core/command/slash-gate.js +6 -4
  51. package/dist/core/command/slash-handler.js +193 -13
  52. package/dist/core/command/topic-slash.js +528 -0
  53. package/dist/core/evolagent-registry.js +20 -5
  54. package/dist/core/handoff/runtime.js +5 -11
  55. package/dist/core/handoff/store.js +5 -0
  56. package/dist/core/ipc-registration.js +139 -2
  57. package/dist/core/memory/consolidation-context.js +49 -16
  58. package/dist/core/memory/consolidation-ops-records.js +35 -0
  59. package/dist/core/memory/consolidation-queue-format.js +21 -0
  60. package/dist/core/memory/consolidation.js +174 -180
  61. package/dist/core/message/aun-ipc-sender.js +161 -6
  62. package/dist/core/message/im-renderer.js +149 -6
  63. package/dist/core/message/message-bridge.js +13 -3
  64. package/dist/core/message/message-log.js +1 -0
  65. package/dist/core/message/peer-mode.js +6 -1
  66. package/dist/core/message/response-engine.js +349 -67
  67. package/dist/core/message/subtask-tool-projection.js +140 -0
  68. package/dist/core/model/catalog-service.js +17 -12
  69. package/dist/core/permission/ec-command-parser.js +126 -7
  70. package/dist/core/session/session-idle-monitor.js +249 -22
  71. package/dist/core/session/session-manager.js +54 -9
  72. package/dist/core/skill/command-parser.js +13 -0
  73. package/dist/core/skill/index-generator.js +4 -1
  74. package/dist/core/skill/startup.js +41 -0
  75. package/dist/core/skill/switch-resolver.js +55 -29
  76. package/dist/core/thread/command-parser.js +125 -0
  77. package/dist/core/thread/index.js +1 -0
  78. package/dist/core/thread/ipc-operation.js +95 -0
  79. package/dist/core/thread/thread-context-storage.js +131 -0
  80. package/dist/core/thread/thread-operation.js +258 -0
  81. package/dist/core/thread/thread-route.js +33 -0
  82. package/dist/core/thread/thread-service.js +777 -0
  83. package/dist/core/thread/types.js +1 -0
  84. package/dist/eck/message-renderer.js +35 -0
  85. package/dist/index.js +8 -5
  86. package/dist/ipc.js +89 -10
  87. package/dist/paths.js +23 -0
  88. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  89. package/dist/trigger/authorization.js +57 -28
  90. package/dist/trigger/feedback.js +7 -1
  91. package/dist/trigger/parser.js +63 -1
  92. package/dist/trigger/validation.js +14 -0
  93. package/dist/utils/error-utils.js +22 -1
  94. package/kits/docs/context-assembly.md +1 -0
  95. package/kits/docs/evolcore/INDEX.md +1 -0
  96. package/kits/docs/evolcore/agent-create.md +39 -28
  97. package/kits/docs/evolcore/agent.md +19 -4
  98. package/kits/docs/evolcore/ec-capabilities.md +4 -0
  99. package/kits/docs/evolcore/group-collaboration.md +3 -5
  100. package/kits/docs/evolcore/group.md +3 -4
  101. package/kits/docs/evolcore/memory-consolidation.md +1 -1
  102. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  103. package/kits/docs/evolcore/menu-protocol.md +7 -0
  104. package/kits/docs/evolcore/msg.md +5 -8
  105. package/kits/docs/evolcore/thread-chain.md +267 -0
  106. package/kits/docs/prompt-loading-architecture.md +1 -0
  107. package/kits/eck_manifest.json +61 -26
  108. package/kits/eck_message_manifest.json +169 -10
  109. package/kits/rules/05-venue.md +1 -1
  110. package/kits/rules/07-memory-and-thread.md +202 -0
  111. package/kits/schemas/relation-config.schema.9.json +18 -3
  112. package/kits/schemas/role-config.schema.1.json +6 -6
  113. package/kits/schemas/single-session.schema.1.json +6 -6
  114. package/kits/schemas/single-session.schema.2.json +6 -6
  115. package/kits/schemas/single-session.schema.3.json +6 -6
  116. package/kits/skills/.switches.json +12 -1
  117. package/kits/skills/ec-group/SKILL.md +234 -0
  118. package/kits/skills/ec-skill/SKILL.md +10 -1
  119. package/kits/skills/ec-skill/scripts/cli.mjs +15 -1
  120. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  121. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  122. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  123. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  124. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  125. package/kits/skills/thread-ops/SKILL.md +492 -0
  126. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  127. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  128. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  129. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  130. package/kits/templates/bootstrap-welcome.md +7 -7
  131. package/kits/templates/memory-consolidation/forget.md +5 -0
  132. package/kits/templates/memory-consolidation/generalize.md +5 -0
  133. package/kits/templates/memory-consolidation/profile.md +5 -0
  134. package/kits/templates/memory-consolidation/promote.md +5 -0
  135. package/kits/templates/message-fragments/item.md +2 -1
  136. package/kits/templates/message-fragments/thread-context.md +6 -0
  137. package/kits/templates/message-fragments/trigger-session.md +2 -0
  138. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  139. package/kits/templates/roles/member.json +2 -0
  140. package/kits/templates/roles/visitor.json +2 -0
  141. package/kits/templates/system-fragments/bootstrap.md +66 -72
  142. package/kits/templates/system-fragments/channel.md +3 -6
  143. package/kits/templates/system-fragments/commands.md +1 -0
  144. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  145. package/kits/templates/system-fragments/reply.md +3 -2
  146. package/kits/templates/system-fragments/session.md +5 -14
  147. package/package.json +3 -1
  148. package/kits/docs/evolcore/group-fs.md +0 -17
  149. package/kits/docs/venues/aun-group.md +0 -45
  150. package/kits/docs/venues/group.md +0 -43
@@ -45,6 +45,7 @@ import { handleSlashCommand } from './slash-handler.js';
45
45
  import { resolveChatMode } from '../message/peer-mode.js';
46
46
  import { execMenuAction as menuExecMenuAction, execMenuForEcweb as menuExecMenuForEcweb, execMenuForControl as menuExecMenuForControl, execMenuQuery as menuExecMenuQuery, execMenuUpdate as menuExecMenuUpdate, getMenuItems as menuGetMenuItems, getSubMenuItems as menuGetSubMenuItems, buildTopicForkSourcePayload, } from './menu-handler.js';
47
47
  import { getMenuCatalog, execMenuValueBatch, validateMenuValueBatchArgs } from './menu-catalog.js';
48
+ import { resolveCurrentSessionLoopTarget } from '../session/session-idle-monitor.js';
48
49
  export { isProcessLevelOwner } from './menu-handler.js';
49
50
  const CLI_EXEC_TIMEOUT_MS = 15_000;
50
51
  const CLI_EXEC_MAX_OUTPUT = 128 * 1024;
@@ -130,6 +131,8 @@ export class CommandHandler {
130
131
  primaryRunnerKey;
131
132
  agentRegistry;
132
133
  memoryConsolidationLauncher;
134
+ /** /skill refresh 的 daemon 侧入口:重建指定 agent 的技能索引 */
135
+ skillIndexRefresher;
133
136
  agentApplicationService;
134
137
  triggerSchedulerResolver;
135
138
  triggerSchedulerFallback;
@@ -145,9 +148,10 @@ export class CommandHandler {
145
148
  diagService;
146
149
  setDiagService(service) { this.diagService = service; }
147
150
  async handleMenuClientOperation(argv, sessionId, delegationToken, delegationCommandHash, send) {
148
- const validation = this.agentDelegationRegistry?.validate(delegationToken, sessionId, delegationCommandHash);
149
- if (!validation?.ok)
150
- return { ok: false, code: validation?.code ?? 'DELEGATION_REQUIRED', error: validation?.reason ?? 'Task delegation is required' };
151
+ const validation = this.agentDelegationRegistry?.validate(delegationToken, sessionId, delegationCommandHash)
152
+ ?? { ok: false, code: 'DELEGATION_REQUIRED', reason: 'Task delegation registry is not configured', diagnosticCause: 'missing_token' };
153
+ if (!validation.ok)
154
+ return { ok: false, code: validation.code ?? 'DELEGATION_REQUIRED', error: validation.reason ?? 'Task delegation is required' };
151
155
  const grant = validation.grant;
152
156
  const session = await this.sessionManager.getSessionById(sessionId);
153
157
  if (!session || session.selfAID !== grant.selfAid || argv[0] !== 'request' || argv[1] !== grant.selfAid)
@@ -541,17 +545,21 @@ export class CommandHandler {
541
545
  reason: input.reason,
542
546
  });
543
547
  }
544
- triggerRoleContext(selfAid, channel, peerId, chatType) {
548
+ triggerRoleContext(selfAid, channel, peerId, chatType, conversationId) {
545
549
  const owning = this.getOwningAgent(channel);
546
550
  if (!owning || owning.aid !== selfAid)
547
551
  return {};
552
+ const resolvedChatType = chatType === 'group' ? 'group' : 'private';
553
+ const resolvedConversationId = resolvedChatType === 'group'
554
+ ? conversationId || peerId
555
+ : peerId;
548
556
  const role = resolvePeerRoleDetail({
549
557
  selfAid,
550
558
  channelKey: channel,
551
559
  channelType: this.resolveChannelType(channel),
552
- chatType: chatType === 'group' ? 'group' : 'private',
560
+ chatType: resolvedChatType,
553
561
  actorId: peerId,
554
- conversationId: peerId,
562
+ conversationId: resolvedConversationId,
555
563
  }).effectiveRole;
556
564
  return { role: role || 'none', baseagent: owning.baseagent };
557
565
  }
@@ -596,12 +604,18 @@ export class CommandHandler {
596
604
  const now = Date.now();
597
605
  const id = `trig_${now}_${crypto.randomBytes(4).toString('hex')}`;
598
606
  const name = parsed.name ?? `trigger-${now.toString(36)}`;
607
+ const originChatType = chatType === 'group' ? 'group' : 'private';
608
+ const originIsGroup = originChatType === 'group';
609
+ const originConversationId = originIsGroup ? channelId : peerId;
599
610
  const feedbackChannel = parsed.targetChannel ?? channel;
611
+ const hasExplicitTarget = parsed.targetChannel !== undefined
612
+ || parsed.targetChannelId !== undefined
613
+ || parsed.targetChatType !== undefined;
600
614
  if (parsed.targetChannel && !this.adapters.has(parsed.targetChannel)) {
601
615
  return { error: `目标渠道不存在或未启用:${parsed.targetChannel}` };
602
616
  }
603
617
  const targetChannelType = this.resolveChannelType(feedbackChannel);
604
- const feedbackChannelId = parsed.targetChannelId ?? peerId;
618
+ const feedbackChannelId = parsed.targetChannelId ?? originConversationId;
605
619
  const schedulerAid = parsed.agentId
606
620
  ?? this.getOwningAgent(feedbackChannel)?.aid
607
621
  ?? tryParseChannelKey(feedbackChannel)?.selfAID
@@ -613,19 +627,12 @@ export class CommandHandler {
613
627
  if (targetChannelType === 'aun' && parsed.targetChannelId && !parsed.targetChatType) {
614
628
  return { error: 'AUN 目标必须显式指定 --target-chat-type private 或 group' };
615
629
  }
616
- const targetDelivery = targetChannelType === 'aun'
617
- ? parsed.targetChatType === 'group'
618
- ? { chatType: 'group', groupId: feedbackChannelId }
619
- : parsed.targetChatType === 'private'
620
- ? { chatType: 'private' }
621
- : { chatType: 'private' }
622
- : undefined;
623
630
  const target = {
624
631
  channelKey: feedbackChannel,
625
632
  channelId: feedbackChannelId,
626
633
  session: parsed.targetSession,
627
634
  threadId: parsed.targetSession === 'thread' ? parsed.targetThreadId : undefined,
628
- chatType: targetDelivery?.chatType,
635
+ chatType: parsed.targetChatType ?? (hasExplicitTarget ? undefined : originChatType),
629
636
  };
630
637
  if (target.session === 'thread') {
631
638
  const adapter = this.adapters.get(feedbackChannel);
@@ -634,19 +641,20 @@ export class CommandHandler {
634
641
  if (!target.threadId)
635
642
  return { error: '--target-session thread 需要 --target-thread-id' };
636
643
  }
637
- const originIsGroup = chatType === 'group';
638
644
  const activeSession = originIsGroup
639
645
  ? undefined
640
646
  : this.sessionManager.getActiveSessionSync(channel, peerId, this.resolveChannelType(channel), schedulerAid);
641
647
  const origin = {
642
648
  channelKey: channel,
643
649
  channelType: this.resolveChannelType(channel),
644
- channelId: peerId,
645
- session: !originIsGroup && threadId ? 'thread' : 'main',
646
- threadId: !originIsGroup ? threadId : undefined,
650
+ channelId: originConversationId,
651
+ session: threadId ? 'thread' : 'main',
652
+ threadId,
647
653
  peerId,
648
654
  sessionKey: activeSession?.sessionKey,
649
- delivery: { chatType: 'private' },
655
+ delivery: originIsGroup
656
+ ? { chatType: 'group', groupId: originConversationId }
657
+ : { chatType: 'private' },
650
658
  };
651
659
  if (!isAdmin && parsed.feedbackStrategy === 'target' && !sameFeedbackTarget(target, origin)) {
652
660
  return { error: '无权限:非 admin 只能把 trigger 投递到来源会话' };
@@ -668,7 +676,7 @@ export class CommandHandler {
668
676
  timeoutMs: parsed.scriptTimeoutMs ?? 30_000,
669
677
  } : undefined;
670
678
  const permissionMode = this.triggerPermissionFromParsed(parsed);
671
- const triggerRole = this.triggerRoleContext(schedulerAid, channel, peerId, chatType);
679
+ const triggerRole = this.triggerRoleContext(schedulerAid, channel, peerId, originChatType, originConversationId);
672
680
  const permissionError = this.validateTriggerPermissionModeForRole(permissionMode, isAdmin, triggerRole.role, schedulerAid, isDaemonOwner, canApprovePersistentFullAccess);
673
681
  if (permissionError)
674
682
  return { error: permissionError };
@@ -933,10 +941,21 @@ export class CommandHandler {
933
941
  setStatsCollector(collector) {
934
942
  this.statsCollector = collector;
935
943
  }
936
- /** 注入记忆整理入口(daemon 的统一整理队列),供 /memory consolidate 使用。返回 null 表示已入队。 */
944
+ /**
945
+ * 注入记忆整理入口(daemon 的统一整理队列),供 /memory consolidate 使用。
946
+ * 已入队时返回带 queued 的占位结果;返回 null 表示拉起失败。
947
+ */
937
948
  setMemoryConsolidationLauncher(launcher) {
938
949
  this.memoryConsolidationLauncher = launcher;
939
950
  }
951
+ /** 注入 /skill refresh 的索引重建入口(daemon 启动时调用)。 */
952
+ setSkillIndexRefresher(refresher) {
953
+ this.skillIndexRefresher = refresher;
954
+ }
955
+ /** 供 slash-handler 读取。 */
956
+ getSkillIndexRefresher() {
957
+ return this.skillIndexRefresher;
958
+ }
940
959
  registerAdapter(adapter) {
941
960
  this.adapters.set(adapter.channelName, adapter);
942
961
  }
@@ -1076,7 +1095,7 @@ export class CommandHandler {
1076
1095
  getMenuItems(role, chatType = 'private', scope = 'agent', authSubject) {
1077
1096
  return menuGetMenuItems.call(this, role, chatType, scope, authSubject);
1078
1097
  }
1079
- async getMenuCatalog(channel, channelId, subject, request, channelScope = 'agent', paircodeAllowed) {
1098
+ async getMenuCatalog(channel, channelId, subject, request, channelScope = 'agent', paircodeAllowed, transportReplyContext) {
1080
1099
  return await getMenuCatalog({
1081
1100
  subject,
1082
1101
  channelType: this.resolveChannelType(channel),
@@ -1084,6 +1103,19 @@ export class CommandHandler {
1084
1103
  channelId,
1085
1104
  request,
1086
1105
  ...(paircodeAllowed === undefined ? {} : { paircodeAllowed }),
1106
+ resolveLoopTarget: async (context) => {
1107
+ const target = await resolveCurrentSessionLoopTarget(this.sessionManager, {
1108
+ channel,
1109
+ channelId: context.channelId,
1110
+ selfAid: context.self ?? '',
1111
+ ...(transportReplyContext?.threadId && context.channelId === channelId
1112
+ ? { threadId: transportReplyContext.threadId }
1113
+ : {}),
1114
+ });
1115
+ if (target.peerKey !== context.peerKey)
1116
+ throw { code: 'INVALID_CONTEXT', message: '当前会话关系不匹配' };
1117
+ return { level: target.level, sessionId: target.session.id };
1118
+ },
1087
1119
  });
1088
1120
  }
1089
1121
  async execMenuValueBatch(channel, channelId, subject, args, request, channelScope = 'agent', transportReplyContext) {
@@ -1577,7 +1609,11 @@ export class CommandHandler {
1577
1609
  if (!isAdmin && updated.execution.type === 'script') {
1578
1610
  return { ok: false, error: '无权限:只有 owner/admin 可以修改 script trigger' };
1579
1611
  }
1580
- const triggerRole = this.triggerRoleContext(definition.agentAid, definition.origin?.channelKey || channel, definition.origin?.peerId || peerId);
1612
+ const originChatType = definition.origin?.delivery?.chatType;
1613
+ const originConversationId = originChatType === 'group'
1614
+ ? definition.origin?.channelId
1615
+ : definition.origin?.peerId;
1616
+ const triggerRole = this.triggerRoleContext(definition.agentAid, definition.origin?.channelKey || channel, definition.origin?.peerId || peerId, originChatType, originConversationId);
1581
1617
  const permissionError = this.validateTriggerPermissionModeForRole(updated.execution.permissionMode, isAdmin, triggerRole.role, definition.agentAid, isDaemonOwner, canApprovePersistentFullAccess);
1582
1618
  if (permissionError) {
1583
1619
  return {
@@ -8,6 +8,7 @@
8
8
  * 不支持跨关系浏览:relation 永远是发起命令的这段关系。
9
9
  */
10
10
  import { CONSOLIDATION_MODES, MODE_SCOPE } from '../memory/consolidation-context.js';
11
+ import { formatQueueDetails } from '../memory/consolidation-queue-format.js';
11
12
  import { MEMORY_SCOPES, isMemoryScope, memoryOverview, listMemoryFiles, formatMemoryFiles, readMemoryIndex, showMemoryFile, formatPendingNotes, showPendingNote, reindexScope, dropNote, archiveMemoryFile, restoreMemoryFile, } from '../memory/memory-admin.js';
12
13
  const USAGE = [
13
14
  '用法:',
@@ -263,8 +264,11 @@ async function consolidate(ctx, target, rawMode, rawScope, needScope, isPayload)
263
264
  ...(scopeOverride ? { scopeOverride } : {}),
264
265
  });
265
266
  const label = `${mode}${scopeOverride ? `(${scopeOverride})` : ''}`;
266
- if (!launched || launched.queued) {
267
- return result(`⏳ 整理请求已入队(${label}),前一个整理会话结束后自动开始`);
267
+ if (!launched)
268
+ return error(`拉起整理会话失败(${label}),详见 daemon 日志`);
269
+ if (launched.queued) {
270
+ return result(`⏳ 整理请求已入队(${label}),前一个整理会话结束后自动开始\n` +
271
+ formatQueueDetails(launched.queued).join('\n'));
268
272
  }
269
273
  return result(`✅ 已拉起整理会话(${label})${launched.executionSessionId ? `\n会话: ${launched.executionSessionId}` : ''}\n完成后会在当前对话反馈结果`);
270
274
  }
@@ -7,6 +7,7 @@ import { ConfigTarget, read as readConfig } from '../../config/config-manager.js
7
7
  import { readRolesConfig } from '../../config/roles.js';
8
8
  import { normalizeMenuError } from './menu-protocol.js';
9
9
  import { isValidAid } from '../../aun/aid/validation.js';
10
+ import { DEFAULT_SESSION_LOOP_SECONDS, MAX_SESSION_LOOP_SECONDS, MIN_SESSION_LOOP_SECONDS } from '../session/session-idle-monitor.js';
10
11
  const DEFAULT_DIRECTORY = { getGroup: groupInfo, getMembers: groupMembers };
11
12
  const HOST_BASEAGENTS = ['claude', 'codex', 'ecagent', 'gemini'];
12
13
  export function validateMenuValueBatchArgs(args) {
@@ -65,6 +66,19 @@ const relation = (id, name, label, order, cmd, operation, extra) => ({
65
66
  ...extra,
66
67
  });
67
68
  const ENTRIES = [
69
+ {
70
+ id: 'session.loop',
71
+ type: 'setting',
72
+ contexts: ['private_relation', 'group_relation'],
73
+ name: 'session',
74
+ label: '当前会话闲置触发',
75
+ section: 'relation_behavior',
76
+ order: 5,
77
+ query: { cmd: '/session', args: { view: 'loop' }, operation: 'session.loop.current', scope: 'relation' },
78
+ update: { cmd: '/session', args: { resource: 'loop' }, operation: 'session.loop.update', scope: 'relation' },
79
+ valueSchema: { type: 'boolean' },
80
+ control: 'toggle-with-number',
81
+ },
68
82
  relation('model.relation', 'model', '模型', 10, '/model', 'model.current', { options: { cmd: '/model', operation: 'model.list', scope: 'relation', mode: 'dynamic' }, update: { cmd: '/model', operation: 'model.use', scope: 'relation' }, control: 'select' }),
69
83
  relation('effort.relation', 'effort', '推理强度', 20, '/effort', 'model.effort.current', { options: { cmd: '/effort', operation: 'model.effort.current', scope: 'relation', mode: 'dynamic', dependsOn: ['model.relation'] }, update: { cmd: '/effort', operation: 'model.effort', scope: 'relation' }, control: 'select' }),
70
84
  relation('chatmode.relation', 'chatmode', '对话模式', 30, '/chatmode', 'chatmode.current', { options: { cmd: '/chatmode', operation: 'chatmode.current', scope: 'relation', mode: 'static', items: [{ value: 'interactive', label: '交互模式' }, { value: 'proactive', label: '主动模式' }] }, update: { cmd: '/chatmode', operation: 'chatmode.update', scope: 'relation' }, control: 'select' }),
@@ -272,6 +286,9 @@ export function bindMenuExecutionContextArgs(args, context) {
272
286
  if (!args && context.kind !== 'group_relation')
273
287
  return args;
274
288
  const { context: _context, ...bound } = args ?? {};
289
+ const isSessionLoop = bound.resource === 'loop' || bound.view === 'loop';
290
+ if (isSessionLoop)
291
+ return bound;
275
292
  if (context.kind === 'private_relation' || context.kind === 'group_relation') {
276
293
  if (bound.scope !== 'role')
277
294
  bound.scope = 'relation';
@@ -356,6 +373,12 @@ async function operationView(binding, context, options = {}) {
356
373
  function wireBinding(binding, context) {
357
374
  if (!binding || binding.supported === false)
358
375
  return undefined;
376
+ if (binding.operation.startsWith('session.loop.'))
377
+ return {
378
+ args: context.kind === 'group_relation'
379
+ ? { ...(binding.args ?? {}), context: { kind: context.kind, ...(context.groupId ? { groupId: context.groupId } : {}) } }
380
+ : binding.args ?? {},
381
+ };
359
382
  return {
360
383
  args: context.kind === 'group_relation'
361
384
  ? {
@@ -379,6 +402,27 @@ export async function getMenuCatalog(input) {
379
402
  operations[key] = view;
380
403
  }
381
404
  entries.push({ id: entry.id, type: entry.type, name: entry.name, label: entry.label, section: entry.section, order: entry.order, target: { scope: context.kind === 'agent' ? 'agent' : context.kind === 'process' ? 'process' : 'relation', ...(context.self ? { self: context.self } : {}), ...(context.peerKey ? { peerKey: context.peerKey } : {}) }, valueSchema: entry.valueSchema ?? { type: 'object' }, operations, wire: { name: entry.name, operations: { ...(wireBinding(entry.query, context) ? { query: wireBinding(entry.query, context) } : {}), ...(wireBinding(entry.options, context) ? { options: wireBinding(entry.options, context) } : {}), ...(wireBinding(entry.update, context) ? { update: wireBinding(entry.update, context) } : {}), ...(wireBinding(entry.action, context) ? { action: wireBinding(entry.action, context) } : {}) } }, ui: { control: entry.control ?? 'action' } });
405
+ if (entry.id === 'session.loop') {
406
+ const loopEntry = entries[entries.length - 1];
407
+ let target = { level: 'relation-main' };
408
+ if (input.resolveLoopTarget) {
409
+ try {
410
+ target = await input.resolveLoopTarget(context);
411
+ }
412
+ catch (error) {
413
+ const reason = normalizeMenuError(error);
414
+ for (const operation of Object.values(operations)) {
415
+ Object.assign(operation, { allowed: false, reason });
416
+ }
417
+ }
418
+ }
419
+ loopEntry.target = target;
420
+ loopEntry.label = target.level === 'topic-session' ? '当前话题闲置触发' : '关系主会话闲置触发';
421
+ loopEntry.ui = {
422
+ control: entry.control,
423
+ idleSeconds: { type: 'integer', minimum: MIN_SESSION_LOOP_SECONDS, maximum: MAX_SESSION_LOOP_SECONDS, default: DEFAULT_SESSION_LOOP_SECONDS, unit: 'seconds' },
424
+ };
425
+ }
382
426
  }
383
427
  const policyRevision = hash({ role: context.subject.role, self: context.subject.selfAid, peerKey: context.subject.peerKey, groupMembershipRole: context.groupMembershipRole, baselineRole: context.baselineRole, paircodeAllowed, roles: readRolesConfig(context.subject.selfAid) });
384
428
  const data = { $schema_version: 1, kind: 'menu.catalog', complete: true, catalogRevision: hash({ entries, context: context.contextKey, policyRevision }), policyRevision, generatedAt: Date.now(), context: { contextKey: context.contextKey, kind: context.kind, channelScope: context.channelScope, transport: context.transport, chatType: context.chatType, ...(context.self ? { self: context.self } : {}), ...(context.actor ? { actor: context.actor } : {}), ...(context.groupId ? { groupId: context.groupId } : {}), ...(context.peerKey ? { peerKey: context.peerKey } : {}), requesterRole: context.requesterRole, ...(context.groupMembershipRole ? { groupMembershipRole: context.groupMembershipRole } : {}), ...(context.baselineRole !== undefined ? { baselineRole: context.baselineRole } : {}) }, features: { batchValuesV1: true, verifiedExplicitGroupContext: context.kind === 'group_relation' }, entries };
@@ -420,7 +464,10 @@ export async function execMenuValueBatch(input) {
420
464
  if (allowed?.supported === false || !(allowed?.allowed))
421
465
  return { itemId, entryId, ok: false, error: allowed?.reason ?? { code: 'NOT_SUPPORTED', message: 'operation is not supported or denied' } };
422
466
  try {
423
- const result = await input.execQuery(entry.query.cmd, context.channelId, bindingArgs(entry.query, context), context);
467
+ const queryArgs = entry.id === 'session.loop'
468
+ ? { ...(entry.query.args ?? {}) }
469
+ : bindingArgs(entry.query, context);
470
+ const result = await input.execQuery(entry.query.cmd, context.channelId, queryArgs, context);
424
471
  return !('error' in result) ? { itemId, entryId, ok: true, data: result.data } : { itemId, entryId, ok: false, error: { code: normalizeMenuError({ code: result.code, message: result.error, data: result.data }).code, message: result.error, ...(result.data !== undefined ? { data: result.data } : {}) } };
425
472
  }
426
473
  catch (error) {
@@ -2,7 +2,7 @@ import { memoryHelpEntries } from './memory-slash.js';
2
2
  import { getBaseagentSnapshot, loadedBaseagentIds } from '../../agents/baseagent-availability.js';
3
3
  import { normalizePeer, writeScope } from '../model/config-scope.js';
4
4
  import { formatPeerKey } from '../relation/peer-identity.js';
5
- import { modelMatches, getCatalog, getBaseagentCatalog, resolveCatalogModel, assertCatalogSelectable, TRANSPORT_EFFORTS } from '../model/model-catalog.js';
5
+ import { modelMatches, getCatalog, getBaseagentCatalog, resolveCatalogModel, assertCatalogSelectable, CONFIGURABLE_EFFORTS, configurableEfforts } from '../model/model-catalog.js';
6
6
  import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
7
7
  import { hasModelSwitcher } from '../../agents/runner-types.js';
8
8
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
@@ -45,6 +45,7 @@ import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
45
45
  import { evolMenuResponseTransportMetadata, menuFailure, menuCommandForName, menuSuccess, normalizeMenuError, validateTaskActivityQueryArgs, validateConfigWriteScope, validateMenuRequest, } from './menu-protocol.js';
46
46
  import { parseMenuClientProtectedHeaders } from './menu-protocol.js';
47
47
  import { TaskActivityStore } from '../task/task-activity-store.js';
48
+ import { DEFAULT_SESSION_LOOP_SECONDS, MAX_SESSION_LOOP_SECONDS, MIN_SESSION_LOOP_SECONDS, refreshSessionLoopTarget, resolveCurrentSessionLoopTarget, sessionLoopPayload, validateIdleSeconds, writeSessionLoopConfig, } from '../session/session-idle-monitor.js';
48
49
  import { remoteAccessService, validateAndRenewRemoteAccessToken } from '../auth/remote-access-store.js';
49
50
  import { listDiagnosticLogOptions, listRawConfigResources, queryDiagnosticLogs, readRawConfigResource, } from './daemon-diagnostics.js';
50
51
  import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
@@ -63,7 +64,7 @@ import { normalizeTopicLabel, normalizeTopicStatus, resolveTopicArchiveFilter, }
63
64
  function getBaseagentVersion(cmd) {
64
65
  return cachedCliVersion(cmd);
65
66
  }
66
- const allEfforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
67
+ const allEfforts = CONFIGURABLE_EFFORTS;
67
68
  const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
68
69
  /** menu file: fetch 文件大小上限(与 /file 和 AUN sendFile 一致) */
69
70
  const FILE_FETCH_MAX_SIZE = MAX_AUN_ATTACHMENT_SIZE;
@@ -402,14 +403,14 @@ function tokenizeArgv(line) {
402
403
  return out;
403
404
  }
404
405
  async function getAvailableEfforts(agent, model) {
406
+ const baseagent = agent?.baseagent || agent?.name;
405
407
  const dynamic = await agent.getAvailableEfforts?.(model);
406
408
  if (Array.isArray(dynamic))
407
- return dynamic;
409
+ return configurableEfforts(baseagent, dynamic);
408
410
  // Keep Menu usable with legacy/embedded runner implementations that do not
409
411
  // expose the optional catalog method. Full runners still take the dynamic
410
412
  // path above, so this fallback cannot broaden their advertised capabilities.
411
- const baseagent = agent?.baseagent || agent?.name;
412
- return (TRANSPORT_EFFORTS[baseagent] ?? []);
413
+ return configurableEfforts(baseagent);
413
414
  }
414
415
  function isDaemonControlSubject(subject) {
415
416
  return subject.isDaemonOwner === true
@@ -1456,12 +1457,7 @@ export function getMenuItems(role, chatType = 'private', scope = 'agent', authSu
1456
1457
  desc: canUseModel ? '切换当前 Agent 使用的模型版本' : '查看当前 Agent 使用的模型版本',
1457
1458
  next: canUseModel ? { type: 'select', dynamic: true } : undefined,
1458
1459
  }] : []),
1459
- ...(canSetEffort ? [{ cmd: '/effort', label: '切换推理强度', desc: '调整模型推理深度,影响响应速度与质量', next: { type: 'select', items: [
1460
- { value: 'low', label: 'Low' },
1461
- { value: 'medium', label: 'Medium' },
1462
- { value: 'high', label: 'High' },
1463
- { value: 'max', label: 'Max' },
1464
- ] } }] : []),
1460
+ ...(canSetEffort ? [{ cmd: '/effort', label: '切换推理强度', desc: '调整模型推理深度,影响响应速度与质量', next: { type: 'select', dynamic: true } }] : []),
1465
1461
  ...(canSetChatmode ? [{ cmd: '/chatmode', label: '切换会话模式', desc: '控制 Agent 主动性(被动响应或主动推进)', next: { type: 'select', items: [
1466
1462
  { value: 'interactive', label: '交互模式', desc: '仅在收到消息时响应' },
1467
1463
  { value: 'proactive', label: '主动模式', desc: 'Agent 可主动推进任务' },
@@ -1966,7 +1962,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1966
1962
  }
1967
1963
  catch (error) {
1968
1964
  if (isDaemonControlSubject(subject)) {
1969
- return ['low', 'medium', 'high', 'auto'].map(e => ({ value: e, label: e === 'auto' ? 'auto (SDK默认)' : e }));
1965
+ return [...configurableEfforts(target.baseagent), 'auto'].map(e => ({ value: e, label: e === 'auto' ? 'auto (SDK默认)' : e }));
1970
1966
  }
1971
1967
  throw error;
1972
1968
  }
@@ -2066,6 +2062,26 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2066
2062
  fromControlChannel,
2067
2063
  subject: authSubject,
2068
2064
  });
2065
+ if (cmdBase === '/session' && args?.view === 'loop') {
2066
+ if (Object.keys(args).some(key => key !== 'view' && key !== 'context')) {
2067
+ return { error: 'session.loop query only accepts args.view', code: 'INVALID_ARGUMENT' };
2068
+ }
2069
+ try {
2070
+ const route = { channel, channelId, selfAid: subject.selfAid ?? '', threadId: transportReplyContext?.threadId };
2071
+ const target = await resolveCurrentSessionLoopTarget(this.sessionManager, route);
2072
+ const denied = await authorizeMenuIntent.call(this, {
2073
+ intent: { operation: 'session.loop.current', scope: 'relation', source, args: { sessionId: target.session.id, peer: target.peerKey } },
2074
+ identity, subject: { ...subject, sessionId: target.session.id }, session: target.session, channel, channelId, userId, fromControlChannel, source,
2075
+ });
2076
+ if (denied)
2077
+ return denied;
2078
+ const current = await refreshSessionLoopTarget(this.sessionManager, route, target);
2079
+ return { data: sessionLoopPayload(current) };
2080
+ }
2081
+ catch (error) {
2082
+ return menuResultFailure(error);
2083
+ }
2084
+ }
2069
2085
  const storageRequest = { type: 'menu.query', cmd, args };
2070
2086
  if (isAgentStorageMenuRequest(storageRequest)) {
2071
2087
  try {
@@ -2863,6 +2879,21 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2863
2879
  const cmdBase = cmd.trim().split(' ')[0];
2864
2880
  if (!cmdBase)
2865
2881
  return { error: '缺少命令', code: 'MISSING_CMD' };
2882
+ const isLoopUpdate = cmdBase === '/session' && args?.resource === 'loop';
2883
+ if (isLoopUpdate) {
2884
+ if (typeof value !== 'boolean' || Object.keys(args ?? {}).some(key => key !== 'resource' && key !== 'idleSeconds' && key !== 'context')) {
2885
+ return { error: 'session.loop requires a boolean value and only accepts resource / idleSeconds', code: 'INVALID_ARGUMENT' };
2886
+ }
2887
+ if (!value && Object.hasOwn(args ?? {}, 'idleSeconds')) {
2888
+ return { error: '关闭时不能提供 idleSeconds', code: 'INVALID_ARGUMENT' };
2889
+ }
2890
+ if (Object.hasOwn(args ?? {}, 'idleSeconds') && validateIdleSeconds(args?.idleSeconds) === undefined) {
2891
+ return { error: `idleSeconds 必须是 ${MIN_SESSION_LOOP_SECONDS}-${MAX_SESSION_LOOP_SECONDS} 的整数`, code: 'INVALID_ARGUMENT' };
2892
+ }
2893
+ }
2894
+ else if (typeof value !== 'string') {
2895
+ return { error: 'value 必须是字符串', code: 'INVALID_ARGUMENT' };
2896
+ }
2866
2897
  if (cmdBase === '/perm') {
2867
2898
  return { error: 'permissionMode 只能通过角色策略编辑器修改', code: 'ROLE_POLICY_MANAGED' };
2868
2899
  }
@@ -2888,9 +2919,6 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2888
2919
  }
2889
2920
  if (cmdBase === '/baseagent-models')
2890
2921
  return { error: 'Baseagent catalog only supports menu.query', code: 'NOT_SUPPORTED' };
2891
- const arg = value.trim();
2892
- if (!arg)
2893
- return { error: '缺少 value 参数', code: 'MISSING_VALUE' };
2894
2922
  const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
2895
2923
  const isAdmin = isManagementRole(identity.role);
2896
2924
  const subject = buildMenuAuthSubject(this, {
@@ -2902,6 +2930,29 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2902
2930
  fromControlChannel,
2903
2931
  subject: authSubject,
2904
2932
  });
2933
+ if (isLoopUpdate) {
2934
+ try {
2935
+ const route = { channel, channelId, selfAid: subject.selfAid ?? '', threadId: transportReplyContext?.threadId };
2936
+ const target = await resolveCurrentSessionLoopTarget(this.sessionManager, route);
2937
+ const denied = await authorizeMenuIntent.call(this, {
2938
+ intent: { operation: 'session.loop.update', scope: 'relation', source, args: { sessionId: target.session.id, peer: target.peerKey, value, ...(value ? { idleSeconds: args?.idleSeconds ?? DEFAULT_SESSION_LOOP_SECONDS } : {}) } },
2939
+ identity, subject: { ...subject, sessionId: target.session.id }, session: target.session, channel, channelId, userId, fromControlChannel, source,
2940
+ });
2941
+ if (denied)
2942
+ return denied;
2943
+ const current = await refreshSessionLoopTarget(this.sessionManager, route, target);
2944
+ const config = value === true
2945
+ ? { enabled: true, idleSeconds: args?.idleSeconds ?? DEFAULT_SESSION_LOOP_SECONDS }
2946
+ : { enabled: false };
2947
+ return { data: sessionLoopPayload(writeSessionLoopConfig(this.sessionManager, current, config)) };
2948
+ }
2949
+ catch (error) {
2950
+ return menuResultFailure(error);
2951
+ }
2952
+ }
2953
+ const arg = value.trim();
2954
+ if (!arg)
2955
+ return { error: '缺少 value 参数', code: 'MISSING_VALUE' };
2905
2956
  if (cmdBase === '/role') {
2906
2957
  const authorized = await authorizeRoleMenu(this, {
2907
2958
  kind: 'update', args, value: arg, identity, subject: authSubject, session, channel, channelId,
@@ -4123,6 +4174,21 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4123
4174
  }
4124
4175
  catch { }
4125
4176
  }
4177
+ // P1a.5.3: 若为话题会话,同步更新话题索引
4178
+ if (topic.threadId) {
4179
+ try {
4180
+ const { ThreadService } = await import('../../core/thread/index.js');
4181
+ const selfAid = topic.selfAID;
4182
+ const peer = topic.channelId;
4183
+ if (selfAid && peer) {
4184
+ const svc = new ThreadService(this.sessionManager);
4185
+ await svc.renameThread({ selfAid, peer, threadId: topic.threadId, name: newName });
4186
+ }
4187
+ }
4188
+ catch (err) {
4189
+ logger.debug('[MenuHandler] Thread index rename sync failed:', err);
4190
+ }
4191
+ }
4126
4192
  this.eventBus.publish({ type: 'session:renamed', sessionId: topic.id, oldName, newName });
4127
4193
  return {
4128
4194
  data: {
@@ -4781,6 +4847,14 @@ async function execMenuForSystemControl(payload, context) {
4781
4847
  request: payload.args.context,
4782
4848
  paircodeAllowed: context.authMethod === 'daemon-owner'
4783
4849
  && (context.source !== 'control' || context.encrypted),
4850
+ resolveLoopTarget: async (catalogContext) => {
4851
+ const target = await resolveCurrentSessionLoopTarget(this.sessionManager, {
4852
+ channel: targetChannel, channelId: catalogContext.channelId, selfAid: catalogContext.self ?? '',
4853
+ });
4854
+ if (target.peerKey !== catalogContext.peerKey)
4855
+ throw { code: 'INVALID_CONTEXT', message: '当前会话关系不匹配' };
4856
+ return { level: target.level, sessionId: target.session.id };
4857
+ },
4784
4858
  });
4785
4859
  return menuSuccess({ id, ...(name ? { name } : {}) }, data);
4786
4860
  }
@@ -4839,8 +4913,10 @@ async function execMenuForSystemControl(payload, context) {
4839
4913
  case 'menu.update': {
4840
4914
  if (!cmd)
4841
4915
  return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
4842
- if (!payload.value)
4916
+ if (payload.value === undefined || payload.value === null
4917
+ || (typeof payload.value === 'string' && payload.value.length === 0)) {
4843
4918
  return ecwebErr(id, name, 'MISSING_VALUE', '缺少 value');
4919
+ }
4844
4920
  const execution = await resolveExplicitMenuExecutionContext({
4845
4921
  subject: trustedSubject,
4846
4922
  channelType: 'aun',
@@ -4,6 +4,7 @@ import { loadDaemonConfig } from '../../config-store.js';
4
4
  import { readInstalledEvolcoreVersion } from '../../utils/evolcore-version.js';
5
5
  import { compareStableSemver, parseStableSemver } from '../../utils/stable-semver.js';
6
6
  import { logger } from '../../utils/logger.js';
7
+ import { validateIdleSeconds } from '../session/session-idle-monitor.js';
7
8
  export const MENU_REQUEST_TYPES = new Set([
8
9
  'menu.token.request',
9
10
  'menu.list',
@@ -334,9 +335,31 @@ export function validateMenuRequest(request) {
334
335
  if (scopeError)
335
336
  return scopeError;
336
337
  }
337
- if (type === 'menu.update' && (typeof request.value !== 'string' || request.value.length === 0)) {
338
+ const isSessionLoopUpdate = type === 'menu.update'
339
+ && (request.name === 'session' || request.cmd === '/session')
340
+ && request.args?.resource === 'loop';
341
+ if (isSessionLoopUpdate) {
342
+ if (typeof request.value !== 'boolean')
343
+ return { code: 'INVALID_ARGUMENT', message: 'session.loop value must be a boolean' };
344
+ const args = request.args;
345
+ const unknown = Object.keys(args ?? {}).find(key => !['resource', 'idleSeconds', 'context'].includes(key));
346
+ if (unknown)
347
+ return { code: 'INVALID_ARGUMENT', message: `session.loop update does not accept args.${unknown}` };
348
+ if (Object.hasOwn(args ?? {}, 'idleSeconds') && validateIdleSeconds(args?.idleSeconds) === undefined) {
349
+ return { code: 'INVALID_ARGUMENT', message: 'idleSeconds must be an integer between 30 and 86400' };
350
+ }
351
+ if (request.value === false && Object.hasOwn(args ?? {}, 'idleSeconds')) {
352
+ return { code: 'INVALID_ARGUMENT', message: 'idleSeconds is not allowed when session.loop is disabled' };
353
+ }
354
+ }
355
+ else if (type === 'menu.update' && (typeof request.value !== 'string' || request.value.length === 0)) {
338
356
  return { code: 'INVALID_ARGUMENT', message: 'Menu request value is required' };
339
357
  }
358
+ if (type === 'menu.query' && (request.name === 'session' || request.cmd === '/session')
359
+ && request.args?.view === 'loop'
360
+ && Object.keys(request.args).some(key => key !== 'view' && key !== 'context')) {
361
+ return { code: 'INVALID_ARGUMENT', message: 'session.loop query only accepts args.view' };
362
+ }
340
363
  if (type === 'menu.action' && (typeof request.action !== 'string' || request.action.trim().length === 0)) {
341
364
  return { code: 'INVALID_ARGUMENT', message: 'Menu request action is required' };
342
365
  }