evolcore 0.0.21 → 0.0.22

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 (64) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/bin/codex-managed-hook.mjs +3 -0
  3. package/bin/install-codex-managed-hooks.mjs +3 -1
  4. package/dist/agents/claude-runner.js +14 -0
  5. package/dist/agents/codex-app-server-client.js +31 -5
  6. package/dist/agents/codex-runner.js +926 -121
  7. package/dist/aun/outbox.js +7 -0
  8. package/dist/channels/aun.js +209 -35
  9. package/dist/cli/daemon-commands.js +29 -8
  10. package/dist/cli/task-context.js +4 -0
  11. package/dist/cli/trigger-command.js +13 -4
  12. package/dist/config/config-field-policy.js +3 -0
  13. package/dist/config/config-manager.js +32 -5
  14. package/dist/config/contact-book-store.js +25 -3
  15. package/dist/core/auth/agent-delegation.js +12 -0
  16. package/dist/core/auth/auth-gateway.js +8 -0
  17. package/dist/core/auth/authorization-audit.js +66 -6
  18. package/dist/core/bootstrap-messages.js +8 -0
  19. package/dist/core/bootstrap-service.js +93 -25
  20. package/dist/core/command/command-handler.js +21 -0
  21. package/dist/core/command/menu-handler.js +9 -0
  22. package/dist/core/command/menu-protocol.js +1 -1
  23. package/dist/core/command/slash-handler.js +41 -18
  24. package/dist/core/data-migration.js +11 -1
  25. package/dist/core/event-catalog.js +32 -0
  26. package/dist/core/handoff/runtime.js +23 -3
  27. package/dist/core/message/im-renderer.js +7 -3
  28. package/dist/core/message/message-bridge.js +60 -2
  29. package/dist/core/message/message-log.js +33 -0
  30. package/dist/core/message/message-queue.js +21 -0
  31. package/dist/core/message/response-engine.js +172 -41
  32. package/dist/core/permission/ec-command-parser.js +272 -70
  33. package/dist/core/permission/protected-paths.js +11 -10
  34. package/dist/core/permission/tool-error-code.js +12 -0
  35. package/dist/core/permission/tool-policy.js +46 -5
  36. package/dist/core/session/session-manager.js +30 -0
  37. package/dist/core/session/session-renew.js +18 -1
  38. package/dist/core/session/session-turn-coordinator.js +5 -1
  39. package/dist/index.js +64 -5
  40. package/dist/ipc.js +97 -17
  41. package/dist/paths.js +18 -0
  42. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  43. package/dist/stats/price-resolver.js +4 -0
  44. package/dist/trigger/feedback.js +14 -2
  45. package/dist/trigger/parser.js +10 -1
  46. package/dist/trigger/scheduler.js +20 -3
  47. package/dist/utils/logger.js +9 -4
  48. package/dist/utils/tool-summary.js +59 -0
  49. package/dist/utils/windows-shell-trust.js +201 -0
  50. package/kits/docs/evolcore/INDEX.md +2 -2
  51. package/kits/docs/evolcore/agent-create.md +146 -0
  52. package/kits/docs/evolcore/agent.md +6 -0
  53. package/kits/docs/evolcore/group-collaboration.md +251 -0
  54. package/kits/docs/evolcore/group-rules.md +1 -19
  55. package/kits/docs/evolcore/group.md +3 -1
  56. package/kits/docs/evolcore/trigger.md +6 -3
  57. package/kits/docs/prompt-loading-architecture.md +6 -0
  58. package/kits/eck_message_manifest.json +6 -6
  59. package/kits/schemas/_meta.json +3 -2
  60. package/kits/schemas/agent-config.schema.12.json +427 -0
  61. package/kits/templates/message-fragments/item.md +1 -1
  62. package/kits/templates/system-fragments/bootstrap.md +2 -1
  63. package/kits/templates/system-fragments/commands.md +2 -2
  64. package/package.json +2 -2
@@ -215,6 +215,13 @@ export function findByDedupeKey(aid, dedupeKey, opts = {}, queue = 'default') {
215
215
  : load(aid, queue);
216
216
  return entries.find(entry => entry.dedupeKey === dedupeKey);
217
217
  }
218
+ /** Find all durable entries belonging to an operation family. */
219
+ export function findByDedupePrefix(aid, dedupePrefix, opts = {}, queue = 'default') {
220
+ const entries = opts.includeTerminal
221
+ ? readEntries(aid, queue).filter(entry => !isExpired(entry))
222
+ : load(aid, queue);
223
+ return entries.filter(entry => typeof entry.dedupeKey === 'string' && entry.dedupeKey.startsWith(dedupePrefix));
224
+ }
218
225
  export function cleanup(aid, queue = 'default') {
219
226
  const all = readEntries(aid, queue);
220
227
  let expired = 0;
@@ -34,7 +34,8 @@ import { deriveCausation, normalizeCausation } from '../core/causation/context.j
34
34
  import { recordCausationSpan } from '../core/causation/audit.js';
35
35
  import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
36
36
  import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
37
- import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationId, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
37
+ import { toolInputForDisplay } from '../utils/tool-summary.js';
38
+ import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationPrefix, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
38
39
  import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
39
40
  import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
40
41
  import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
@@ -45,6 +46,9 @@ const AUN_INBOUND_DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1000;
45
46
  const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
46
47
  const AUN_ATTACHMENT_DOWNLOAD_ATTEMPTS = 3;
47
48
  const AUN_ATTACHMENT_RETRY_DELAYS_MS = [250, 750];
49
+ const OBSERVER_GROUP_MEMBERS_TTL_MS = 60_000;
50
+ const OBSERVER_GROUP_MEMBERS_PAGE_SIZE = 200;
51
+ const OBSERVER_GROUP_MEMBERS_MAX_PAGES = 100;
48
52
  const AUN_SEND_LOG_TEXT_MAX_LENGTH = 60;
49
53
  export function formatAunSendLogText(text) {
50
54
  return text.replace(/\s+/gu, ' ').trim().slice(0, AUN_SEND_LOG_TEXT_MAX_LENGTH);
@@ -1062,6 +1066,18 @@ export class AUNChannel {
1062
1066
  .replace(/[ \t]+/g, ' ')
1063
1067
  .trim();
1064
1068
  }
1069
+ /** Normalize a command after a client-rendered friendly mention label. */
1070
+ normalizeSlashCommandText(text) {
1071
+ const trimmed = text.trim();
1072
+ const leadingLabels = trimmed.match(/^(?:@[^\s]+\s+)+(?=\/)/u);
1073
+ const commandText = leadingLabels
1074
+ ? trimmed.slice(leadingLabels[0].length).trimStart()
1075
+ : trimmed;
1076
+ // Keep the command contract consistent for mixed friendly/canonical @
1077
+ // tokens: selector labels are removed above, then known AID-shaped tokens
1078
+ // anywhere in the command are removed by the existing helper.
1079
+ return this.stripAllMentions(commandText);
1080
+ }
1065
1081
  parsePayloadMentionsOrReject(payload, context, messageId, seq) {
1066
1082
  const raw = payload && typeof payload === 'object' && !Array.isArray(payload)
1067
1083
  ? payload.mentions
@@ -1277,6 +1293,8 @@ export class AUNChannel {
1277
1293
  /** 撤权可能先于 outbox 卡片投递完成;迟到卡片注册后立即撤回。 */
1278
1294
  invalidatedInteractions = new Map();
1279
1295
  mentionModeResolver;
1296
+ /** Agent-level policy controlling whether group slash commands require structured mentions. */
1297
+ groupSlashRequireMentionsResolver;
1280
1298
  static PROACTIVE_ALLOW_TYPES = new Set([
1281
1299
  'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
1282
1300
  'merge', 'link', 'location', 'personal_card',
@@ -1401,6 +1419,14 @@ export class AUNChannel {
1401
1419
  }
1402
1420
  }
1403
1421
  async _initClientInner() {
1422
+ this.observerGroupMembers.clear();
1423
+ for (const groupId of new Set([
1424
+ ...this.observerGroupMembershipEpoch.keys(),
1425
+ ...this.observerGroupMemberFetches.keys(),
1426
+ ])) {
1427
+ this.observerGroupMembershipEpoch.set(groupId, (this.observerGroupMembershipEpoch.get(groupId) ?? 0) + 1);
1428
+ }
1429
+ this.observerGroupMemberFetches.clear();
1404
1430
  // Clean up existing client if any
1405
1431
  if (this.client) {
1406
1432
  this.trace('OUT', 'client.close', { reason: 'initClient' });
@@ -1469,6 +1495,15 @@ export class AUNChannel {
1469
1495
  logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
1470
1496
  this.handleIncomingGroupMessage(data);
1471
1497
  });
1498
+ client.on('group.changed', (data) => {
1499
+ if (!isCurrentClient())
1500
+ return;
1501
+ const record = data && typeof data === 'object' ? data : {};
1502
+ const envelope = record.envelope && typeof record.envelope === 'object'
1503
+ ? record.envelope
1504
+ : {};
1505
+ this.invalidateObserverGroupMembers(envelope.group_id ?? record.group_id);
1506
+ });
1472
1507
  }
1473
1508
  const handleSdkState = (source) => (data) => {
1474
1509
  if (!isCurrentClient())
@@ -2034,7 +2069,7 @@ export class AUNChannel {
2034
2069
  // 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
2035
2070
  // (即便日后 from-owner 排除规则调整,也不会泄漏)。
2036
2071
  if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
2037
- this.forwardInbound(msg);
2072
+ void this.forwardInbound(msg);
2038
2073
  }
2039
2074
  // 回声过滤:自己发出的消息会被 gateway fanout 回来,
2040
2075
  // 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
@@ -2205,7 +2240,7 @@ export class AUNChannel {
2205
2240
  }
2206
2241
  // Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
2207
2242
  // forwardInbound 内部排除 self-echo 与 from-owner。
2208
- this.forwardInbound(msg);
2243
+ void this.forwardInbound(msg);
2209
2244
  logger.debug(`${this.logPrefix()}[DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
2210
2245
  if (!groupId || !senderAid) {
2211
2246
  this.acknowledgeImmediately(messageId, seq);
@@ -2316,9 +2351,13 @@ export class AUNChannel {
2316
2351
  const firstLineGroup = text.split('\n')[0] || '';
2317
2352
  const hasEvolCoreTraceGroup = /\[EvolCore\.(receive|reply|agent)\]/.test(text);
2318
2353
  const isEchoMsg = /echo/i.test(firstLineGroup) && !hasEvolCoreTraceGroup;
2354
+ const slashCommandText = this.normalizeSlashCommandText(text);
2319
2355
  // 命令判定:剥离所有 @ 后看是否 / 开头(多 @ 场景如 @a @b /status 也能正确识别)。
2320
2356
  // echo 消息走独立的 trace 流程,不参与命令语义判定。
2321
- const isCommandMsg = !isEchoMsg && this.stripAllMentions(text).startsWith('/');
2357
+ const isCommandMsg = !isEchoMsg && slashCommandText.startsWith('/');
2358
+ const groupSlashRequireMentions = this.groupSlashRequireMentionsResolver
2359
+ ? await this.groupSlashRequireMentionsResolver().catch(() => undefined) ?? true
2360
+ : true;
2322
2361
  if (isEchoMsg) {
2323
2362
  // 短 echo(≤10 字符)已在前面的快速通道命中并 return,这里只处理长 echo
2324
2363
  // >10 字符:追加 trace,存 pending echo,跳过 mention 过滤继续走 Agent 流程
@@ -2345,9 +2384,10 @@ export class AUNChannel {
2345
2384
  }
2346
2385
  else {
2347
2386
  // 非 echo 消息:mention 标记(不再过滤,交给响应模式决定)
2348
- // slash 命令在任何 mentionMode 下都强制走 mention-only 语义
2349
- // 即必须 @ 本 agent(或 @all)才处理,避免广播群里一条命令被全部 agent 各自执行。
2350
- const enforceMention = mentionMode === 'mention-only' || isCommandMsg;
2387
+ // slash 命令默认要求结构化 @;Agent 可通过
2388
+ // groupSlashRequireMentions=false 选择仅按正文命令格式处理。
2389
+ const enforceMention = (mentionMode === 'mention-only' && !isCommandMsg)
2390
+ || (isCommandMsg && groupSlashRequireMentions);
2351
2391
  const isMentioned = mentionedSelf || mentionedAll;
2352
2392
  // 过滤逻辑下移到响应层:这里只标记,不过滤
2353
2393
  // 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
@@ -2355,14 +2395,14 @@ export class AUNChannel {
2355
2395
  if (enforceMention && !isMentioned && !isOwnHandoff) {
2356
2396
  this.acknowledgeImmediately(messageId, seq);
2357
2397
  this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
2358
- logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
2398
+ logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} slashRequireMentions=${groupSlashRequireMentions} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
2359
2399
  return;
2360
2400
  }
2361
2401
  }
2362
2402
  // 命令消息:剥离所有 @(多 agent 被 @ 时各自拿到干净的 /status 各自执行);
2363
2403
  // 普通消息:仅在唯一 @ 是自己时剥离,保留其他 @ 供 agent 感知。
2364
2404
  const strippedText = isCommandMsg
2365
- ? this.stripAllMentions(text)
2405
+ ? slashCommandText
2366
2406
  : this.stripSelfMentionIfOnly(text, this._aid);
2367
2407
  // Detect attachments before the empty-text guard (顶层 + 嵌套)
2368
2408
  const rawAttachments = this.collectAllAttachments(payload);
@@ -2393,7 +2433,7 @@ export class AUNChannel {
2393
2433
  : mentionedSelf
2394
2434
  ? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
2395
2435
  : `${mentionMode}.no-mention`;
2396
- logger.info(`${this.logPrefix()} Group mention decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} mentionMode=${mentionMode} reason=${reason} structMentions=${JSON.stringify(payloadMentionEntries)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
2436
+ logger.info(`${this.logPrefix()} Group mention decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} mentionMode=${mentionMode} slashRequireMentions=${groupSlashRequireMentions} reason=${reason} structMentions=${JSON.stringify(payloadMentionEntries)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
2397
2437
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
2398
2438
  if (payloadType === 'action_card_reply')
2399
2439
  return;
@@ -2564,6 +2604,9 @@ export class AUNChannel {
2564
2604
  // observable / owners 不在此处缓存——由 daemon 注入 resolver,从 EvolAgent 的
2565
2605
  // in-memory merged config(启动/重启/热重载时统一更新的唯一缓存)读取,避免重复缓存。
2566
2606
  observerConfigResolver;
2607
+ observerGroupMembers = new Map();
2608
+ observerGroupMemberFetches = new Map();
2609
+ observerGroupMembershipEpoch = new Map();
2567
2610
  /** 注入观察者配置读取器(daemon 侧从 EvolAgent merged config 读)。 */
2568
2611
  setObserverConfigResolver(fn) {
2569
2612
  this.observerConfigResolver = fn;
@@ -2582,23 +2625,129 @@ export class AUNChannel {
2582
2625
  * data 为 SDK message.received / group.message_created 回调的整个对象,
2583
2626
  * 不拆解、不重组——SDK 信封结构变化不影响此处。
2584
2627
  */
2628
+ async resolveObserverGroupMembers(groupId) {
2629
+ if (!groupId || !this.client)
2630
+ return undefined;
2631
+ const cached = this.observerGroupMembers.get(groupId);
2632
+ if (cached && cached.expiresAt > Date.now())
2633
+ return cached.aids;
2634
+ const pending = this.observerGroupMemberFetches.get(groupId);
2635
+ if (pending)
2636
+ return pending;
2637
+ const epoch = this.observerGroupMembershipEpoch.get(groupId) ?? 0;
2638
+ let request;
2639
+ request = (async () => {
2640
+ try {
2641
+ const result = await this.callAndTrace('group.get_members', {
2642
+ group_id: groupId,
2643
+ page: 1,
2644
+ size: OBSERVER_GROUP_MEMBERS_PAGE_SIZE,
2645
+ });
2646
+ const rawMembers = Array.isArray(result?.members)
2647
+ ? result.members
2648
+ : Array.isArray(result?.items) ? result.items : undefined;
2649
+ if (!rawMembers)
2650
+ return undefined;
2651
+ const hasTotal = Number.isFinite(Number(result?.total));
2652
+ const hasMore = result?.has_more === true || result?.hasMore === true;
2653
+ if (!hasTotal && (hasMore || rawMembers.length >= OBSERVER_GROUP_MEMBERS_PAGE_SIZE))
2654
+ return undefined;
2655
+ const allMembers = [...rawMembers];
2656
+ const total = Number(result?.total);
2657
+ const pageSize = Number(result?.size) > 0 ? Number(result.size) : OBSERVER_GROUP_MEMBERS_PAGE_SIZE;
2658
+ const pageCount = Number.isFinite(total) && total > allMembers.length
2659
+ ? Math.ceil(total / pageSize)
2660
+ : 1;
2661
+ if (pageCount > OBSERVER_GROUP_MEMBERS_MAX_PAGES)
2662
+ return undefined;
2663
+ for (let page = 2; page <= pageCount; page++) {
2664
+ const next = await this.callAndTrace('group.get_members', {
2665
+ group_id: groupId,
2666
+ page,
2667
+ size: OBSERVER_GROUP_MEMBERS_PAGE_SIZE,
2668
+ });
2669
+ const nextMembers = Array.isArray(next?.members)
2670
+ ? next.members
2671
+ : Array.isArray(next?.items) ? next.items : undefined;
2672
+ if (!nextMembers)
2673
+ return undefined;
2674
+ allMembers.push(...nextMembers);
2675
+ }
2676
+ if (hasTotal && total > allMembers.length)
2677
+ return undefined;
2678
+ const aids = new Set();
2679
+ for (const member of allMembers) {
2680
+ const aid = typeof member === 'string'
2681
+ ? member.trim()
2682
+ : typeof member?.aid === 'string' ? member.aid.trim()
2683
+ : typeof member?.member_aid === 'string' ? member.member_aid.trim() : '';
2684
+ if (aid)
2685
+ aids.add(aid);
2686
+ }
2687
+ if ((this.observerGroupMembershipEpoch.get(groupId) ?? 0) === epoch) {
2688
+ this.observerGroupMembers.set(groupId, { aids, expiresAt: Date.now() + OBSERVER_GROUP_MEMBERS_TTL_MS });
2689
+ }
2690
+ return aids;
2691
+ }
2692
+ catch (error) {
2693
+ logger.debug(`${this.logPrefix()} observer group membership lookup failed group=${groupId}: ${error}`);
2694
+ return undefined;
2695
+ }
2696
+ finally {
2697
+ if (this.observerGroupMemberFetches.get(groupId) === request) {
2698
+ this.observerGroupMemberFetches.delete(groupId);
2699
+ }
2700
+ }
2701
+ })();
2702
+ this.observerGroupMemberFetches.set(groupId, request);
2703
+ return request;
2704
+ }
2705
+ invalidateObserverGroupMembers(groupId) {
2706
+ if (typeof groupId !== 'string' || !groupId.trim())
2707
+ return;
2708
+ const key = groupId.trim();
2709
+ this.observerGroupMembers.delete(key);
2710
+ this.observerGroupMembershipEpoch.set(key, (this.observerGroupMembershipEpoch.get(key) ?? 0) + 1);
2711
+ }
2712
+ async observerRecipients(owners, envelope) {
2713
+ if (owners.length === 0)
2714
+ return owners;
2715
+ const groupId = typeof envelope.group_id === 'string' ? envelope.group_id.trim() : '';
2716
+ const members = groupId ? await this.resolveObserverGroupMembers(groupId) : undefined;
2717
+ // Unknown membership is fail-open for observation: only an explicit member
2718
+ // response may suppress the redundant owner DM.
2719
+ return owners.filter(owner => !(members?.has(owner)));
2720
+ }
2585
2721
  forwardInbound(data) {
2586
2722
  if (!this.connected || !this.client)
2587
- return;
2723
+ return Promise.resolve();
2588
2724
  if (payloadTypeForObserver(data)?.startsWith('menu.'))
2589
- return;
2725
+ return Promise.resolve();
2590
2726
  const { observable, owners } = this.getObserverConfig();
2591
2727
  if (!observable || owners.length === 0)
2592
- return;
2593
- const env = (data?.envelope && typeof data.envelope === 'object') ? data.envelope : {};
2728
+ return Promise.resolve();
2729
+ const env = (data?.envelope && typeof data.envelope === 'object')
2730
+ ? { ...data.envelope }
2731
+ : {};
2732
+ if (!env.group_id && typeof data.group_id === 'string')
2733
+ env.group_id = data.group_id;
2594
2734
  const from = env.from ?? '';
2595
2735
  if (this._aid && from === this._aid)
2596
- return; // self-echo:已在出站转过
2736
+ return Promise.resolve(); // self-echo:已在出站转过
2597
2737
  // 排除来源 owner(不把"owner A 发来的"再转回 A),但仍转给其他 owner。
2598
- const recipientOwners = owners.filter(o => o !== from);
2599
- if (recipientOwners.length === 0)
2600
- return;
2601
- this.emitForward('inbound', data, recipientOwners);
2738
+ const candidateOwners = owners.filter(o => o !== from);
2739
+ if (candidateOwners.length === 0)
2740
+ return Promise.resolve();
2741
+ if (typeof env.group_id !== 'string' || !env.group_id.trim()) {
2742
+ this.emitForward('inbound', data, candidateOwners);
2743
+ return Promise.resolve();
2744
+ }
2745
+ return this.observerRecipients(candidateOwners, env)
2746
+ .then(recipientOwners => {
2747
+ if (recipientOwners.length > 0)
2748
+ this.emitForward('inbound', data, recipientOwners);
2749
+ })
2750
+ .catch(error => logger.debug(`${this.logPrefix()} observer inbound recipient lookup failed: ${error}`));
2602
2751
  }
2603
2752
  /**
2604
2753
  * 出站转发:Agent 经 AUN 真实发出的消息原样转发给 owner。
@@ -2607,20 +2756,33 @@ export class AUNChannel {
2607
2756
  */
2608
2757
  forwardOutbound(result) {
2609
2758
  if (!this.connected || !this.client)
2610
- return;
2759
+ return Promise.resolve();
2611
2760
  if (payloadTypeForObserver(result)?.startsWith('menu.'))
2612
- return;
2761
+ return Promise.resolve();
2613
2762
  const { observable, owners } = this.getObserverConfig();
2614
2763
  if (!observable || owners.length === 0)
2615
- return;
2616
- const env = (result?.envelope && typeof result.envelope === 'object') ? result.envelope : {};
2764
+ return Promise.resolve();
2765
+ const env = (result?.envelope && typeof result.envelope === 'object')
2766
+ ? { ...result.envelope }
2767
+ : {};
2768
+ if (!env.group_id && typeof result.group_id === 'string')
2769
+ env.group_id = result.group_id;
2617
2770
  const to = env.to ?? env.group_id ?? '';
2618
2771
  // 过滤:若对端本身是 owner,不转发给该 owner(避免"回复你"转给你自己);
2619
2772
  // 但仍转发给其他 owner。
2620
- const recipientOwners = owners.filter(o => o !== to);
2621
- if (recipientOwners.length === 0)
2622
- return;
2623
- this.emitForward('outbound', result, recipientOwners);
2773
+ const candidateOwners = owners.filter(o => o !== to);
2774
+ if (candidateOwners.length === 0)
2775
+ return Promise.resolve();
2776
+ if (typeof env.group_id !== 'string' || !env.group_id.trim()) {
2777
+ this.emitForward('outbound', result, candidateOwners);
2778
+ return Promise.resolve();
2779
+ }
2780
+ return this.observerRecipients(candidateOwners, env)
2781
+ .then(recipientOwners => {
2782
+ if (recipientOwners.length > 0)
2783
+ this.emitForward('outbound', result, recipientOwners);
2784
+ })
2785
+ .catch(error => logger.debug(`${this.logPrefix()} observer outbound recipient lookup failed: ${error}`));
2624
2786
  }
2625
2787
  /**
2626
2788
  * 实际投递 observer.forward 给每个 owner,使用 daemon 的全局 AUN 默认加密设置。
@@ -3104,6 +3266,9 @@ export class AUNChannel {
3104
3266
  setMentionModeResolver(resolver) {
3105
3267
  this.mentionModeResolver = resolver;
3106
3268
  }
3269
+ setGroupSlashRequireMentionsResolver(resolver) {
3270
+ this.groupSlashRequireMentionsResolver = resolver;
3271
+ }
3107
3272
  onRecall(handler) {
3108
3273
  this.recallHandler = handler;
3109
3274
  }
@@ -3711,9 +3876,15 @@ export class AUNChannel {
3711
3876
  return truncated;
3712
3877
  }
3713
3878
  buildActivityPayload(envelope, context, item) {
3714
- const activityItem = item && typeof item === 'object' && !Array.isArray(item)
3879
+ const rawItem = item && typeof item === 'object' && !Array.isArray(item)
3715
3880
  ? { ...item }
3716
3881
  : { kind: 'unknown', text: String(item ?? '') };
3882
+ const displayArguments = rawItem.kind === 'tool_call'
3883
+ ? toolInputForDisplay(rawItem.name, rawItem.arguments)
3884
+ : undefined;
3885
+ const activityItem = rawItem.kind === 'tool_call' && displayArguments !== rawItem.arguments
3886
+ ? { ...rawItem, arguments: displayArguments }
3887
+ : rawItem;
3717
3888
  return {
3718
3889
  ...this.buildTaskPayloadBase(envelope, context),
3719
3890
  type: 'activity',
@@ -5119,13 +5290,11 @@ export class AUNChannel {
5119
5290
  const owner = getFirstStaticAgentOwner(aid);
5120
5291
  if (!owner)
5121
5292
  return;
5122
- const operationIds = new Set([
5123
- bootstrapInitialMessageOperationId(aid),
5124
- postBootstrapWelcomeOperationId(aid),
5125
- ]);
5126
- const entries = [...operationIds]
5127
- .map(operationId => outbox.findByDedupeKey(aid, operationId, { includeTerminal: true }))
5128
- .filter((entry) => !!entry?.channelId);
5293
+ const completionEntry = outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid), { includeTerminal: true });
5294
+ const entries = [
5295
+ ...outbox.findByDedupePrefix(aid, bootstrapInitialMessageOperationPrefix(aid), { includeTerminal: true }),
5296
+ ...(completionEntry ? [completionEntry] : []),
5297
+ ].filter((entry) => !!entry?.channelId);
5129
5298
  for (const entry of entries) {
5130
5299
  const delivery = { chatType: 'private' };
5131
5300
  const nestedDelivery = entry.context?.delivery;
@@ -5915,6 +6084,11 @@ export class AUNChannelPlugin {
5915
6084
  }, { cache: true }).mentionMode;
5916
6085
  });
5917
6086
  }
6087
+ if (typeof channel.setGroupSlashRequireMentionsResolver === 'function') {
6088
+ channel.setGroupSlashRequireMentionsResolver(async () => {
6089
+ return resolveEffective({ self: aid }, { cache: true }).groupSlashRequireMentions ?? true;
6090
+ });
6091
+ }
5918
6092
  },
5919
6093
  };
5920
6094
  }
@@ -174,14 +174,35 @@ function sourceBuildNeedsRefresh() {
174
174
  const sourceMtime = Math.max(latestSourceMtime(path.join(packageRoot, 'src')), latestSourceMtime(path.join(packageRoot, 'ecagent', 'src')));
175
175
  return sourceMtime > fs.statSync(daemonEntry).mtimeMs;
176
176
  }
177
- function requireFreshSourceBuild() {
177
+ function runSourceBuild() {
178
+ return new Promise(resolve => {
179
+ console.log('🔧 源码比 daemon 编译产物更新,自动重新构建...');
180
+ const child = spawn(platform.isWindows ? 'npm.cmd' : 'npm', ['run', 'build'], {
181
+ cwd: getPackageRoot(),
182
+ stdio: 'inherit',
183
+ shell: platform.isWindows,
184
+ });
185
+ child.on('error', err => {
186
+ console.error(`❌ 无法启动构建进程: ${err.message}`);
187
+ resolve(false);
188
+ });
189
+ child.on('exit', code => {
190
+ if (code === 0) {
191
+ console.log('✅ 构建完成');
192
+ resolve(true);
193
+ }
194
+ else {
195
+ console.error(`❌ 自动构建失败 (exit ${code}),服务未启动。`);
196
+ console.error(' 请手动执行: npm run build 排查错误');
197
+ resolve(false);
198
+ }
199
+ });
200
+ });
201
+ }
202
+ async function requireFreshSourceBuild() {
178
203
  if (!sourceBuildNeedsRefresh())
179
204
  return true;
180
- console.error('❌ 源码比 daemon 编译产物更新,服务未启动。');
181
- console.error(' 请先执行: ec stop && npm run build');
182
- console.error(' 然后执行: ec start');
183
- process.exitCode = 2;
184
- return false;
205
+ return await runSourceBuild();
185
206
  }
186
207
  function requireCompletedDataMigration(root) {
187
208
  const requirement = inspectDataMigrationRequirement(root);
@@ -306,7 +327,7 @@ export async function cmdStart(opts = {}) {
306
327
  return;
307
328
  }
308
329
  ensureDataDirs();
309
- if (!requireFreshSourceBuild())
330
+ if (!await requireFreshSourceBuild())
310
331
  return;
311
332
  // 旧配置自动迁移(daemon.json → 新结构)
312
333
  const { autoMigrateIfNeeded } = await import('../config-store.js');
@@ -681,7 +702,7 @@ export async function cmdRestart(opts = {}) {
681
702
  if (initialRuntimeOrphans.length === 0 && rejectPidIsolatedLifecycle('restart', initialStatus, ping))
682
703
  return;
683
704
  }
684
- if (!requireFreshSourceBuild())
705
+ if (!await requireFreshSourceBuild())
685
706
  return;
686
707
  // 版本检查与自动升级
687
708
  console.log('📦 Checking for updates...');
@@ -6,6 +6,7 @@ import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  export const TASK_RUNTIME_CONTEXT_ENV = 'EVOLCORE_TASK_RUNTIME_CONTEXT';
8
8
  export const SESSION_RUNTIME_DIR_ENV = 'EVOLCORE_SESSION_RUNTIME_DIR';
9
+ export const DAEMON_RUNTIME_EPOCH_ENV = 'EVOLCORE_DAEMON_RUNTIME_EPOCH';
9
10
  export { RUNTIME_LOCK_DIR_ENV };
10
11
  const generatedProcessManagedTempDirs = new Set();
11
12
  let processTempCleanupRegistered = false;
@@ -32,11 +33,13 @@ function normalizeTaskRuntimeContext(value) {
32
33
  peerName: optionalString(value.peerName),
33
34
  peerType: optionalString(value.peerType),
34
35
  peerRole: optionalString(value.peerRole),
36
+ permissionMode: optionalString(value.permissionMode),
35
37
  processRole: optionalProcessRole(value.processRole),
36
38
  dataScope: optionalDataScope(value.dataScope),
37
39
  authorizedBy: optionalString(value.authorizedBy),
38
40
  executionSource: value.executionSource === 'fullaccess-command' || value.executionSource === 'trigger'
39
41
  ? value.executionSource : undefined,
42
+ daemonRuntimeEpoch: optionalString(value.daemonRuntimeEpoch),
40
43
  threadId: optionalString(value.threadId),
41
44
  sessionRuntimeDir: optionalAbsolutePath(value.sessionRuntimeDir),
42
45
  runtimeLockDir: optionalAbsolutePath(value.runtimeLockDir),
@@ -336,6 +339,7 @@ export function buildTaskRuntimeEnv(ctx) {
336
339
  const clean = normalizeTaskRuntimeContext(ctx);
337
340
  return {
338
341
  EVOLCORE_SESSION_ID: ctx.sessionId ?? '',
342
+ ...(ctx.daemonRuntimeEpoch ? { [DAEMON_RUNTIME_EPOCH_ENV]: ctx.daemonRuntimeEpoch } : {}),
339
343
  [TASK_RUNTIME_CONTEXT_ENV]: JSON.stringify(clean),
340
344
  // Every managed child receives the session directory as TMPDIR. This keeps
341
345
  // ordinary temporary files and runtime-only fallbacks out of shared agent
@@ -32,7 +32,7 @@ export async function cmdTrigger(args) {
32
32
  return;
33
33
  }
34
34
  if (process.env.EVOLCORE_SESSION_ID
35
- && (hasOption(rest, '--file') || hasOption(rest, '--prompt-file') || hasOption(rest, '--script-path'))) {
35
+ && (hasOption(rest, '--file') || hasOption(rest, '--prompt-file') || hasOption(rest, '--script-path') || hasOption(rest, '--script-file'))) {
36
36
  throw new Error('Managed tasks cannot read trigger definitions, prompts, or scripts from local files; use inline prompt content');
37
37
  }
38
38
  try {
@@ -123,7 +123,7 @@ Create 参数模式支持:
123
123
 
124
124
  Update 仅支持常用字段:
125
125
  --once | --delay | --at | --cron | --every | --event [--tz]
126
- --prompt <文本> | --prompt-file <路径> --name <名称>
126
+ --prompt <文本> | --prompt-file <路径> --script-file <路径> --name <名称>
127
127
  --model <模型> --effort <强度> --permission <权限模式|inherit>(inherit 清除覆盖)
128
128
  --max-runs <次数> --max-duration <时长>
129
129
  --concurrency <forbid|replace|allow> --missed-policy <skip|run-once|run-all>
@@ -399,19 +399,28 @@ function shellQuoteArg(value) {
399
399
  }
400
400
  async function updateTrigger(args, json) {
401
401
  const agentAid = resolveAgentAid(args);
402
- const triggerId = positional(args, 0, ['--agent', '--format', '--if-revision', '--prompt-file']);
402
+ const triggerId = positional(args, 0, ['--agent', '--format', '--if-revision', '--prompt-file', '--script-file']);
403
403
  const usesPromptFile = hasOption(args, '--prompt-file');
404
+ const usesScriptFile = hasOption(args, '--script-file');
405
+ const scriptFilePath = usesScriptFile ? requireFlag(args, '--script-file') : undefined;
404
406
  const { parseTriggerUpdateArgv } = await import('../trigger/parser.js');
405
407
  const parsed = parseTriggerUpdateArgv(triggerId, withPromptFromFile(triggerUpdateArgs(args)), { allowLongPrompt: usesPromptFile });
406
408
  if (!parsed.ok)
407
409
  throw new Error(parsed.error);
410
+ const scriptFileRequested = parsed.value.scriptFile !== undefined;
411
+ const { scriptFile: _scriptFile, ...patchFields } = parsed.value;
412
+ const patch = scriptFileRequested ? { ...patchFields, scriptFile: true } : patchFields;
408
413
  const expectedRevision = flagValue(args, '--if-revision');
414
+ const scriptFileBase64 = scriptFilePath
415
+ ? fs.readFileSync(scriptFilePath).toString('base64')
416
+ : undefined;
409
417
  const res = await request({
410
418
  type: 'trigger.update',
411
419
  agentAid,
412
420
  triggerId,
413
- patch: parsed.value,
421
+ patch,
414
422
  ...(usesPromptFile ? { promptFile: true } : {}),
423
+ ...(scriptFileBase64 !== undefined ? { scriptFileBase64 } : {}),
415
424
  ...(expectedRevision ? { expectedRevision } : {}),
416
425
  }, 30_000);
417
426
  if (json)
@@ -2,6 +2,7 @@ const BEHAVIOR_TOP_FIELDS = new Set([
2
2
  'active_baseagent',
3
3
  'chatmode',
4
4
  'mentionMode',
5
+ 'groupSlashRequireMentions',
5
6
  'responseMode',
6
7
  'responseModeParams',
7
8
  'flush_delay',
@@ -110,6 +111,8 @@ export function resolveConfigFieldRule(fieldPath) {
110
111
  return scalar(field, 'nonnegative-number');
111
112
  if (field === 'mentionMode')
112
113
  return scalar(field, 'mention-mode');
114
+ if (field === 'groupSlashRequireMentions')
115
+ return scalar(field, 'boolean');
113
116
  if (field === 'show_activities')
114
117
  return scalar(field, 'show-activities');
115
118
  if (field === 'enable_rich_content')
@@ -933,7 +933,10 @@ function acquireConfigMutationLock(file) {
933
933
  return () => releaseConfigMutationLock(lockDir, ownerFile, token);
934
934
  }
935
935
  catch (error) {
936
- if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY')
936
+ const lockMayAlreadyExist = error?.code === 'EEXIST'
937
+ || error?.code === 'ENOTEMPTY'
938
+ || (process.platform === 'win32' && error?.code === 'EPERM' && fs.existsSync(lockDir));
939
+ if (!lockMayAlreadyExist)
937
940
  throw error;
938
941
  if (reclaimStaleConfigMutationLock(lockDir, ownerFile))
939
942
  continue;
@@ -957,10 +960,10 @@ function reclaimStaleConfigMutationLock(lockDir, ownerFile) {
957
960
  pid = Number(JSON.parse(raw).pid);
958
961
  }
959
962
  catch {
960
- return false;
963
+ return reclaimCorruptConfigMutationLock(lockDir);
961
964
  }
962
965
  if (!Number.isInteger(pid) || pid <= 0)
963
- return false;
966
+ return reclaimCorruptConfigMutationLock(lockDir);
964
967
  try {
965
968
  process.kill(pid, 0);
966
969
  return false;
@@ -979,6 +982,24 @@ function reclaimStaleConfigMutationLock(lockDir, ownerFile) {
979
982
  }
980
983
  }
981
984
  }
985
+ function reclaimCorruptConfigMutationLock(lockDir) {
986
+ try {
987
+ const stat = fs.statSync(lockDir);
988
+ if (Date.now() - stat.mtimeMs < CONFIG_MUTATION_LOCK_WAIT_MS)
989
+ return false;
990
+ const reclaimed = `${lockDir}.reclaimed-corrupt-${crypto.randomBytes(8).toString('hex')}`;
991
+ fs.renameSync(lockDir, reclaimed);
992
+ fs.rmSync(reclaimed, { recursive: true, force: true });
993
+ return true;
994
+ }
995
+ catch (error) {
996
+ if (error?.code === 'ENOENT')
997
+ return true;
998
+ if (error?.code === 'EEXIST' || error?.code === 'ENOTEMPTY' || error?.code === 'EPERM')
999
+ return false;
1000
+ throw error;
1001
+ }
1002
+ }
982
1003
  function releaseConfigMutationLock(lockDir, ownerFile, token) {
983
1004
  try {
984
1005
  const owner = JSON.parse(fs.readFileSync(ownerFile, 'utf8'));
@@ -1289,9 +1310,13 @@ function stripStaticOwnerField(config) {
1289
1310
  return config;
1290
1311
  if (!Object.prototype.hasOwnProperty.call(config, 'owners')
1291
1312
  && !Object.prototype.hasOwnProperty.call(config, 'admins')
1292
- && !Object.prototype.hasOwnProperty.call(config, 'access'))
1313
+ && !Object.prototype.hasOwnProperty.call(config, 'access')
1314
+ && !Object.prototype.hasOwnProperty.call(config, 'groupSlashRequireMentions'))
1293
1315
  return config;
1294
- const { owners: _owners, admins: _admins, access: _access, ...rest } = config;
1316
+ // groupSlashRequireMentions is intentionally Agent-only. Relation/defaults
1317
+ // files allow forward-compatible unknown properties, so strip it before the
1318
+ // generic merge chain can treat a manually written value as an override.
1319
+ const { owners: _owners, admins: _admins, access: _access, groupSlashRequireMentions: _groupSlashRequireMentions, ...rest } = config;
1295
1320
  return rest;
1296
1321
  }
1297
1322
  function stripRelationRoleData(config) {
@@ -1330,6 +1355,7 @@ export function resolveEffective(sel, opts = {}) {
1330
1355
  baseagents: config.baseagents,
1331
1356
  chatmode: config.chatmode,
1332
1357
  mentionMode: config.mentionMode,
1358
+ groupSlashRequireMentions: config.groupSlashRequireMentions ?? true,
1333
1359
  model: config.model,
1334
1360
  responseMode: config.responseMode,
1335
1361
  responseModeParams: config.responseModeParams,
@@ -1361,6 +1387,7 @@ const EFFECTIVE_BEHAVIOR_FIELDS = [
1361
1387
  'baseagents',
1362
1388
  'chatmode',
1363
1389
  'mentionMode',
1390
+ 'groupSlashRequireMentions',
1364
1391
  'flush_delay',
1365
1392
  'debounce',
1366
1393
  'show_activities',