evolcore 0.0.12 → 0.0.14

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 (116) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/ec-safe-output.js +26 -7
  4. package/bin/install-codex-managed-hooks.mjs +201 -0
  5. package/dist/agents/claude-runner.js +105 -11
  6. package/dist/agents/codex-app-server-client.js +176 -7
  7. package/dist/agents/codex-runner.js +248 -84
  8. package/dist/agents/ecagent-runner.js +21 -2
  9. package/dist/agents/gemini-runner.js +9 -4
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/store.js +13 -7
  12. package/dist/aun/group-fs-download.js +56 -0
  13. package/dist/aun/group-identity.js +1 -2
  14. package/dist/aun/msg/group.js +11 -33
  15. package/dist/aun/msg/index.js +1 -2
  16. package/dist/aun/msg/managed-operation.js +176 -55
  17. package/dist/aun/msg/upload.js +16 -1
  18. package/dist/aun/outbox.js +31 -2
  19. package/dist/aun/rpc/client.js +66 -0
  20. package/dist/aun/rpc/index.js +1 -2
  21. package/dist/aun/storage/{upload.js → client.js} +11 -2
  22. package/dist/aun/storage/index.js +1 -2
  23. package/dist/channels/aun.js +514 -150
  24. package/dist/channels/daemon.js +2 -0
  25. package/dist/cli/agent.js +9 -96
  26. package/dist/cli/aun-commands.js +1 -1
  27. package/dist/cli/cli-argv.js +10 -0
  28. package/dist/cli/config.js +9 -17
  29. package/dist/cli/ctl-command.js +1 -1
  30. package/dist/cli/daemon-commands.js +39 -24
  31. package/dist/cli/fs-command.js +65 -19
  32. package/dist/cli/init.js +77 -8
  33. package/dist/cli/queue-command.js +42 -0
  34. package/dist/cli/restart-monitor.js +3 -14
  35. package/dist/cli/task-context.js +172 -0
  36. package/dist/cli/trigger-command.js +7 -0
  37. package/dist/config/access-policy.js +1 -2
  38. package/dist/config/builtin-roles.js +7 -0
  39. package/dist/config/config-manager.js +27 -3
  40. package/dist/config/contact-book-store.js +7 -2
  41. package/dist/config/peer-role-resolver.js +6 -2
  42. package/dist/config/role-migration-startup.js +53 -0
  43. package/dist/config/role-store.js +1 -1
  44. package/dist/core/auth/agent-delegation.js +1 -1
  45. package/dist/core/auth/auth-gateway.js +30 -1
  46. package/dist/core/auth/authorization-audit.js +47 -0
  47. package/dist/core/auth/operation-authorizer.js +1 -0
  48. package/dist/core/auth/operation-catalog.js +10 -1
  49. package/dist/core/bootstrap-messages.js +9 -2
  50. package/dist/core/bootstrap-service.js +8 -1
  51. package/dist/core/command/command-handler.js +201 -51
  52. package/dist/core/command/menu-catalog.js +4 -0
  53. package/dist/core/command/menu-handler.js +85 -18
  54. package/dist/core/command/menu-protocol.js +23 -2
  55. package/dist/core/command/slash-handler.js +58 -10
  56. package/dist/core/event-catalog.js +8 -0
  57. package/dist/core/handoff/runtime.js +12 -1
  58. package/dist/core/handoff/store.js +5 -1
  59. package/dist/core/interaction-router.js +10 -0
  60. package/dist/core/message/im-renderer.js +34 -1
  61. package/dist/core/message/message-bridge.js +68 -13
  62. package/dist/core/message/message-log.js +2 -0
  63. package/dist/core/message/message-queue.js +267 -39
  64. package/dist/core/message/message-utils.js +81 -4
  65. package/dist/core/message/response-engine.js +242 -48
  66. package/dist/core/message/stream-debouncer.js +10 -2
  67. package/dist/core/permission/approval-gateway.js +1 -0
  68. package/dist/core/permission/ec-command-parser.js +649 -18
  69. package/dist/core/permission/index.js +1 -1
  70. package/dist/core/permission/mode.js +15 -0
  71. package/dist/core/permission/readonly-shell-query.js +148 -8
  72. package/dist/core/permission/sandbox-runtime.js +21 -0
  73. package/dist/core/permission/tool-policy.js +266 -56
  74. package/dist/core/protected-paths.js +107 -2
  75. package/dist/core/runtime-lock.js +101 -0
  76. package/dist/core/session/session-manager.js +21 -2
  77. package/dist/core/session/session-mapper.js +20 -1
  78. package/dist/eck/detect.js +18 -1
  79. package/dist/eck/group-rules-sync.js +15 -44
  80. package/dist/index.js +191 -89
  81. package/dist/ipc.js +16 -3
  82. package/dist/paths.js +3 -0
  83. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  84. package/dist/response-system/modes/single-session/index.js +3 -0
  85. package/dist/trigger/feedback.js +64 -11
  86. package/dist/trigger/history.js +42 -7
  87. package/dist/trigger/manager.js +21 -1
  88. package/dist/trigger/parser.js +14 -2
  89. package/dist/trigger/scheduler.js +1 -0
  90. package/dist/trigger/validation.js +67 -6
  91. package/dist/utils/ecweb-supervisor.js +332 -0
  92. package/dist/utils/error-utils.js +17 -0
  93. package/dist/utils/logger.js +37 -4
  94. package/kits/docs/evolcore/trigger.md +17 -1
  95. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  96. package/kits/schemas/role-config.schema.1.json +1 -0
  97. package/kits/templates/roles/admin.json +7 -0
  98. package/kits/templates/roles/member.json +8 -1
  99. package/kits/templates/roles/visitor.json +2 -0
  100. package/package.json +1 -1
  101. package/dist/aun/msg/payload-type.js +0 -27
  102. package/dist/aun/rpc/caller.js +0 -42
  103. package/dist/aun/rpc/connection.js +0 -25
  104. package/dist/aun/storage/manage.js +0 -10
  105. package/dist/config/access-policy-domain.js +0 -18
  106. package/dist/config/config-batch-get.js +0 -11
  107. package/dist/config/owner-policy.js +0 -4
  108. package/dist/config/role-config-v4-startup.js +0 -32
  109. package/dist/config/role-config-v5-startup.js +0 -27
  110. package/dist/core/auth/trigger-authorization.js +0 -15
  111. package/dist/core/interaction-registration.js +0 -10
  112. package/dist/core/permission/execution-sandbox.js +0 -16
  113. package/dist/core/relation/peer-key.js +0 -1
  114. package/dist/core/session/session-key.js +0 -24
  115. package/dist/eck/baseagent-caps.js +0 -18
  116. package/dist/eck/rules-loader.js +0 -28
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // 【重要】最先加载环境变量,确保后续模块初始化时可用
2
2
  import dotenv from 'dotenv';
3
3
  import path from 'path';
4
- import { ensureDataDirs, resolvePaths, getPackageRoot, agentMdPath, eckDebugDir, daemonControlDir, daemonQueuePath } from './paths.js';
4
+ import { ensureDataDirs, resolvePaths, getPackageRoot, agentMdPath, eckDebugDir, daemonControlDir, daemonQueuePath, daemonMessageDedupPath } from './paths.js';
5
5
  // 立即加载 .env 文件(在其他模块导入之前)
6
6
  try {
7
7
  dotenv.config({ path: path.join(resolvePaths().root, '.env') });
@@ -14,16 +14,13 @@ import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-f
14
14
  import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
15
15
  import { EcagentSessionFileAdapter } from './core/session/adapters/ecagent-session-file-adapter.js';
16
16
  import { loadDefaults, loadAllAgents, migrateIdentitiesIfNeeded, loadDaemonConfig, initializeEckSnapshotsConfig } from './config-store.js';
17
- import { ConfigTarget, initConfigManager, onConfigWrite, read as readConfig } from './config/config-manager.js';
18
- import { ensureRoleConfigV4OnStartup } from './config/role-config-v4-startup.js';
19
- import { ensureRoleConfigV5OnStartup } from './config/role-config-v5-startup.js';
17
+ import { ConfigTarget, initConfigManager, onConfigWrite, read as readConfig, shouldFailFastForMissingOwners } from './config/config-manager.js';
18
+ import { ensureRoleConfigV4OnStartup, ensureRoleConfigV5OnStartup } from './config/role-migration-startup.js';
20
19
  import { ensureContactBookV2OnStartup } from './config/contact-book-v2-startup.js';
21
20
  import { resolvePeerDisplayLabel } from './config/contact-book.js';
22
21
  import { expirePendingContactRequests } from './config/contact-request-service.js';
23
- import { shouldFailFastForMissingOwners } from './config/owner-policy.js';
24
22
  import { isManagementRole } from './config/builtin-roles.js';
25
23
  import { checkRoleAccess, getFirstStaticAgentOwner, resolvePeerRoleDetail, roleToSessionIdentity } from './config/peer-role-resolver.js';
26
- import { isCrossAgentTriggerOperationAllowed, isCrossAgentTriggerReadOperation } from './core/auth/trigger-authorization.js';
27
24
  import { ensureStartupConfigSnapshot, isConfigSnapshotsEnabled, retentionCleanup, readCurrent, readWVersion, diffWorkingVsVersion, paramDiff, incrementSuccessCount } from './config/snapshot.js';
28
25
  import { appendBootLog, selfDiagnose } from './config/boot-log.js';
29
26
  import { CONFIG_SCHEMA_VERSION } from './types.js';
@@ -41,7 +38,7 @@ import { BindService } from './utils/aid-bind.js';
41
38
  import { DingtalkChannelPlugin, registerPendingDingtalkContactBind } from './channels/dingtalk.js';
42
39
  import { QQBotChannelPlugin, registerPendingQQBotContactBind } from './channels/qqbot.js';
43
40
  import { WecomChannelPlugin, registerPendingWecomContactBind } from './channels/wecom.js';
44
- import { buildEnvelope } from './core/message/message-utils.js';
41
+ import { buildEnvelope, isDeliveryTargetForChannel, replyContextFromSession } from './core/message/message-utils.js';
45
42
  import { ResponseEngine } from './core/message/response-engine.js';
46
43
  import { MessageQueue } from './core/message/message-queue.js';
47
44
  import { MessageBridge } from './core/message/message-bridge.js';
@@ -58,10 +55,9 @@ import { getEventCatalog } from './core/event-catalog.js';
58
55
  import { StatsCollector } from './utils/stats.js';
59
56
  import { AidStatsCollector } from './utils/stats.js';
60
57
  import { PermissionGateway } from './core/permission/approval-gateway.js';
61
- import { InteractionRouter } from './core/interaction-router.js';
62
- import { registerAdapterInteractions } from './core/interaction-registration.js';
58
+ import { InteractionRouter, registerAdapterInteractions } from './core/interaction-router.js';
63
59
  import { AgentDelegationRegistry, authorizeDelegatedAunMsgSend } from './core/auth/agent-delegation.js';
64
- import { authorizeOperation, buildAuthSubject } from './core/auth/auth-gateway.js';
60
+ import { authorizeOperation, buildAuthSubject, isCrossAgentTriggerOperationAllowed, isCrossAgentTriggerReadOperation, } from './core/auth/auth-gateway.js';
65
61
  import { parsePeerKey } from './core/relation/peer-identity.js';
66
62
  import { isHClassPath } from './core/protected-paths.js';
67
63
  import { ChannelLoader, tryParseChannelKey } from './core/channel-loader.js';
@@ -72,7 +68,8 @@ import { AgentReloadBusyError, AgentReloadCoordinator } from './core/agent-reloa
72
68
  import { buildReloadHooks } from './core/channel-loader.js';
73
69
  import { IpcServer } from './ipc.js';
74
70
  import { logger, setLogLevel } from './utils/logger.js';
75
- import { ecwebVersionRequirementError, fetchEcwebPairCode } from './utils/ecweb-utils.js';
71
+ import { fetchEcwebPairCode } from './utils/ecweb-utils.js';
72
+ import { createEcwebSupervisor } from './utils/ecweb-supervisor.js';
76
73
  import { writeMain, removeAll, isMainWinner, scanInstances } from './utils/instance-registry.js';
77
74
  import { detectDuplicates } from './core/evolagent-registry.js';
78
75
  import { loadKitManifest, cleanEckDebug, invalidateKitCache } from './eck/kit-renderer.js';
@@ -90,14 +87,13 @@ import { applyTriggerPatch } from './trigger/patch.js';
90
87
  import { validateModelSelectionForRole } from './core/model/model-permission.js';
91
88
  import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride, } from './core/role/runtime-policy.js';
92
89
  import { atomicWriteJson } from './core/session/session-fs-store.js';
90
+ import { ensureProcessManagedTempDir } from './cli/task-context.js';
93
91
  import { appendMessageLog, buildOutboundEntry, classifyAunPayloadForLog } from './core/message/message-log.js';
94
92
  import { normalizeAunMentionEntries } from './aun/msg/mention-schema.js';
95
93
  import { MAIN_PACKAGE_NAME } from './product.js';
96
94
  import fs from 'fs';
97
95
  import crypto from 'crypto';
98
96
  import { fileURLToPath } from 'url';
99
- import { spawn } from 'child_process';
100
- import * as platform from './utils/cross-platform.js';
101
97
  const controlMenuDeduper = new MenuRequestDeduper();
102
98
  /** 出站 payload 摘要(用于 channel-out.log) */
103
99
  function summarizeOutboundPayload(payload) {
@@ -176,6 +172,61 @@ function normalizeMessageSource(value) {
176
172
  ? value
177
173
  : 'daemon';
178
174
  }
175
+ /**
176
+ * Preserve the authenticated AUN inbound route for control-plane replies.
177
+ * Control callbacks must not reconstruct group/private semantics from an AID.
178
+ */
179
+ function controlReplyContext(opts, metadata) {
180
+ const supplied = opts.replyContext?.delivery;
181
+ if (supplied) {
182
+ const matchesInbound = opts.chatType === 'group'
183
+ ? supplied.chatType === 'group' && supplied.groupId === opts.groupId
184
+ : supplied.chatType === 'private';
185
+ if (!matchesInbound) {
186
+ throw Object.assign(new Error('AUN control reply route does not match its authenticated inbound context'), {
187
+ code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
188
+ });
189
+ }
190
+ }
191
+ const delivery = supplied
192
+ ?? (opts.chatType === 'group'
193
+ ? (opts.groupId ? { chatType: 'group', groupId: opts.groupId } : undefined)
194
+ : { chatType: 'private' });
195
+ if (!delivery)
196
+ throw Object.assign(new Error('AUN control reply is missing a trusted groupId'), { code: 'AUN_OUTBOUND_ROUTE_REQUIRED' });
197
+ return {
198
+ ...(opts.replyContext ?? {}),
199
+ delivery,
200
+ metadata: {
201
+ ...(opts.replyContext?.metadata ?? {}),
202
+ ...(opts.encrypted === undefined ? {} : { encrypted: opts.encrypted }),
203
+ ...(opts.protectedHeaders ? { protectedHeaders: opts.protectedHeaders } : {}),
204
+ ...(metadata ?? {}),
205
+ },
206
+ };
207
+ }
208
+ function isValidAunDeliveryForChannel(delivery, channelId) {
209
+ return isDeliveryTargetForChannel(delivery, channelId);
210
+ }
211
+ function restartChannelType(channel) {
212
+ return channel?.split('#', 1)[0].toLowerCase();
213
+ }
214
+ /**
215
+ * Restore a persisted restart route without inferring AUN group semantics from
216
+ * an address. Legacy non-AUN entries may remain route-less; AUN entries do not.
217
+ */
218
+ export function resolvePersistedRestartDelivery(opts) {
219
+ const requiresAunRoute = restartChannelType(opts.channel) === 'aun'
220
+ || restartChannelType(opts.adapterChannel) === 'aun';
221
+ if (!requiresAunRoute) {
222
+ return isDeliveryTargetForChannel(opts.delivery, opts.channelId)
223
+ ? { valid: true, delivery: opts.delivery }
224
+ : { valid: true };
225
+ }
226
+ return isDeliveryTargetForChannel(opts.delivery, opts.channelId)
227
+ ? { valid: true, delivery: opts.delivery }
228
+ : { valid: false };
229
+ }
179
230
  function daemonConversationWatchdogMs(settings) {
180
231
  const idleTimeoutSec = settings.idleMonitor?.timeout;
181
232
  const idleMs = typeof idleTimeoutSec === 'number' && Number.isFinite(idleTimeoutSec) && idleTimeoutSec > 0
@@ -205,6 +256,10 @@ function originFromActorSession(session, authenticatedPeerId, authenticatedChann
205
256
  threadId: sameConversation && !isGroup ? (session.threadId || undefined) : undefined,
206
257
  peerId,
207
258
  sessionKey: sameConversation && !isGroup ? session.sessionKey : undefined,
259
+ // A group-created trigger intentionally feeds back to the authenticated
260
+ // actor's private conversation (the origin channelId is peerId), so the
261
+ // route remains explicit even though the source session was a group.
262
+ delivery: { chatType: 'private' },
208
263
  };
209
264
  }
210
265
  function seedUpgradeCheckTrigger(manager, owner) {
@@ -402,6 +457,20 @@ export async function sendSystemPayload(adapter, envelope, payload) {
402
457
  }
403
458
  async function runBindBootstrapDaemon(daemonCfg) {
404
459
  logger.warn('[bind-bootstrap] starting control AID + IPC only');
460
+ // Bootstrap exits before the normal trigger scheduler is constructed. Keep
461
+ // the daemon-owned upgrade check present during a fresh installation too;
462
+ // the regular startup path will reseed this definition idempotently.
463
+ const bootstrapControlAid = daemonCfg.aid || '__daemon__';
464
+ try {
465
+ seedUpgradeCheckTrigger(new TriggerDefinitionManager(bootstrapControlAid), {
466
+ aid: bootstrapControlAid,
467
+ originPeerId: bootstrapControlAid,
468
+ baseagent: 'codex',
469
+ });
470
+ }
471
+ catch (error) {
472
+ logger.warn(`[bind-bootstrap] failed to seed daemon upgrade trigger: ${error instanceof Error ? error.message : String(error)}`);
473
+ }
405
474
  const bindService = new BindService({
406
475
  receiverAid: daemonCfg.aid,
407
476
  getAvailableBaseagents: detectAvailableBaseagentsForBind,
@@ -437,7 +506,7 @@ async function runBindBootstrapDaemon(daemonCfg) {
437
506
  }
438
507
  const response = parsed ? await bindService.handleRequest(parsed, opts.peerId) : null;
439
508
  if (response) {
440
- await controlChannel.sendMessage(opts.channelId, JSON.stringify(response));
509
+ await controlChannel.sendMessage(opts.channelId, JSON.stringify(response), controlReplyContext(opts));
441
510
  }
442
511
  });
443
512
  const controlToken = crypto.randomBytes(32).toString('base64url');
@@ -595,6 +664,9 @@ function readFastaunVersion() {
595
664
  }
596
665
  }
597
666
  async function main() {
667
+ // Service managers do not necessarily export TMPDIR. Establish the private
668
+ // managed root before any runner or task runtime is initialized.
669
+ ensureProcessManagedTempDir();
598
670
  // 启动信息:目录类型 + 版本号 + 代码最新时间戳
599
671
  {
600
672
  const pkgRoot = getPackageRoot();
@@ -789,6 +861,23 @@ async function main() {
789
861
  '如需远程管理,请在 daemon.json 配置 owners: [<你的 AID>]');
790
862
  }
791
863
  const paths = resolvePaths();
864
+ // ECWeb has one lifecycle owner: the daemon. The CLI may have started an
865
+ // instance just before spawning us; the supervisor adopts it when healthy,
866
+ // otherwise starts one and keeps it alive for the daemon's lifetime.
867
+ let ecwebSupervisor = null;
868
+ if (daemonCfg.ecweb?.enabled) {
869
+ ecwebSupervisor = createEcwebSupervisor({
870
+ port: daemonCfg.ecweb.port ?? 42705,
871
+ logger,
872
+ });
873
+ try {
874
+ await ecwebSupervisor.start();
875
+ onShutdown(() => ecwebSupervisor?.stop());
876
+ }
877
+ catch (error) {
878
+ logger.warn(`ECWeb supervisor 启动失败(不影响 daemon 主流程): ${error?.message || error}`);
879
+ }
880
+ }
792
881
  if (bindBootstrapMode && daemonCfg.aid && loadAllAgents().agents.length === 0) {
793
882
  await runBindBootstrapDaemon(daemonCfg);
794
883
  return;
@@ -1091,7 +1180,7 @@ async function main() {
1091
1180
  if (!adapter)
1092
1181
  return;
1093
1182
  if (text) {
1094
- await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: id, replyContext: opts }), { kind: 'system.notice', text, subtype: 'health' });
1183
+ await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: id, delivery: opts?.delivery, replyContext: opts }), { kind: 'system.notice', text, subtype: 'health' });
1095
1184
  }
1096
1185
  };
1097
1186
  return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
@@ -1120,6 +1209,7 @@ async function main() {
1120
1209
  await processor.processMessage(message);
1121
1210
  }, {
1122
1211
  persistencePath: daemonQueuePath(),
1212
+ dedupPath: daemonMessageDedupPath(),
1123
1213
  });
1124
1214
  // 设置中断回调(精确中断正在处理的 agent)
1125
1215
  messageQueue.setInterruptCallback(async (sessionKey, baseagent, evolagentName, reason = 'new_message') => {
@@ -1160,12 +1250,20 @@ async function main() {
1160
1250
  source: 'msg',
1161
1251
  handoffTrace: { version: 2, handoff_id: handoff.handoff_id },
1162
1252
  };
1163
- const result = targetSession.chatType === 'group'
1253
+ const delivery = handoff.request.delivery;
1254
+ if (!isValidAunDeliveryForChannel(delivery, targetSession.channelId)) {
1255
+ return {
1256
+ ok: false,
1257
+ error: 'handoff is missing a valid immutable AUN delivery route',
1258
+ };
1259
+ }
1260
+ const result = delivery.chatType === 'group'
1164
1261
  ? typeof channel.sendDaemonGroupMsg === 'function'
1165
1262
  ? await channel.sendDaemonGroupMsg({
1166
- groupId: targetSession.channelId,
1263
+ groupId: delivery.groupId,
1167
1264
  payload: { ...handoff.request.payload, [AUN_HANDOFF_MARKER_FIELD]: handoff.handoff_id },
1168
1265
  encrypt: handoff.request.encrypt,
1266
+ causation: handoff.causation,
1169
1267
  log,
1170
1268
  })
1171
1269
  : { ok: false, error: 'AUN channel does not support daemon group sends' }
@@ -1279,6 +1377,14 @@ async function main() {
1279
1377
  if (target && !getTriggerChannel(definition.agentAid, target.channelKey)) {
1280
1378
  throw new Error(`agent ${definition.agentAid} has no configured channel ${target.channelKey}`);
1281
1379
  }
1380
+ const channelType = tryParseChannelKey(target?.channelKey ?? '')?.type
1381
+ ?? (target?.channelKey?.split('#', 1)[0] || undefined);
1382
+ const validAunRoute = definition.feedback.strategy === 'target'
1383
+ ? definition.feedback.target?.chatType === 'private' || definition.feedback.target?.chatType === 'group'
1384
+ : !!definition.origin && isValidAunDeliveryForChannel(definition.origin.delivery, definition.origin.channelId);
1385
+ if (target && channelType === 'aun' && !validAunRoute) {
1386
+ throw Object.assign(new Error('AUN trigger feedback target requires a valid explicit delivery route'), { code: 'AUN_OUTBOUND_ROUTE_REQUIRED' });
1387
+ }
1282
1388
  };
1283
1389
  const validateTriggerDefinitionForActor = (definition, actor) => {
1284
1390
  if (definition.execution.type === 'script' && !actor.management) {
@@ -1520,9 +1626,13 @@ async function main() {
1520
1626
  && (candidate.adapter.channelKey === target.channelKey || candidate.adapter.channelName === target.channelKey)
1521
1627
  && tryParseChannelKey(candidate.adapter.channelKey)?.selfAID === input.trigger.agentAid));
1522
1628
  const channel = inst?.channel;
1523
- if (!channel || typeof channel.isGroupId !== 'function')
1629
+ if (!channel)
1630
+ return null;
1631
+ // Trigger targets carry their transport classification explicitly. A
1632
+ // channelId is only an address and is never parsed to choose the RPC.
1633
+ const targetChatType = target.chatType;
1634
+ if (!targetChatType)
1524
1635
  return null;
1525
- const targetChatType = channel.isGroupId(target.channelId) ? 'group' : 'private';
1526
1636
  if (targetChatType === 'group' && typeof channel.sendDaemonGroupMsg !== 'function')
1527
1637
  return null;
1528
1638
  if (targetChatType === 'private' && typeof channel.sendDaemonMsg !== 'function')
@@ -1634,11 +1744,20 @@ async function main() {
1634
1744
  // 0. 包装 adapter.send,记录所有出站到 channel-out.log
1635
1745
  const originalSend = inst.adapter.send.bind(inst.adapter);
1636
1746
  inst.adapter.send = async (envelope, payload) => {
1637
- logger.channelOut({ channel: inst.adapter.channelName, channelId: envelope.channelId, taskId: envelope.taskId, payload: summarizeOutboundPayload(payload) });
1747
+ const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelKey)
1748
+ ?? agentRegistry.resolveByChannel(inst.adapter.channelName);
1749
+ logger.channelOut({
1750
+ channel: inst.adapter.channelName,
1751
+ channelId: envelope.channelId,
1752
+ taskId: envelope.taskId,
1753
+ correlationId: envelope.operationId ?? envelope.taskId,
1754
+ sessionId: envelope.sessionId,
1755
+ agentAid: owningAgent?.aid,
1756
+ agentName: envelope.agentName,
1757
+ payload: summarizeOutboundPayload(payload),
1758
+ });
1638
1759
  const result = await originalSend(envelope, payload);
1639
1760
  if (shouldCountSentPayload(payload)) {
1640
- const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelKey)
1641
- ?? agentRegistry.resolveByChannel(inst.adapter.channelName);
1642
1761
  if ((inst.channelType || inst.adapter.channelName) !== 'aun') {
1643
1762
  try {
1644
1763
  const logPayload = outboundPayloadToLogText(payload);
@@ -1748,8 +1867,8 @@ async function main() {
1748
1867
  });
1749
1868
  }
1750
1869
  // 5. 撤回消息 → 中断执行中任务
1751
- inst.channel.onRecall?.((messageId) => {
1752
- msgBridge.cancel(messageId);
1870
+ inst.channel.onRecall?.((messageId, selfAID) => {
1871
+ msgBridge.cancel(messageId, selfAID);
1753
1872
  });
1754
1873
  }
1755
1874
  // ── 注册所有渠道实例 ──
@@ -1858,6 +1977,7 @@ async function main() {
1858
1977
  channel: adapter.channelName,
1859
1978
  channelId: ownerAid,
1860
1979
  agentName,
1980
+ delivery: { chatType: 'private' },
1861
1981
  });
1862
1982
  sendSystemPayload(adapter, envelope, {
1863
1983
  kind: 'system.notice',
@@ -1896,16 +2016,31 @@ async function main() {
1896
2016
  logger.info(`[Restart] Pending notification waits for channel connection: ${pending.channel}`);
1897
2017
  return;
1898
2018
  }
1899
- const replyContext = pending.rootId
1900
- ? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId }
1901
- : undefined;
2019
+ const pendingDelivery = pending.delivery;
2020
+ const restartDelivery = resolvePersistedRestartDelivery({
2021
+ channel: pending.channel,
2022
+ adapterChannel: adapter.channelName,
2023
+ channelId: pending.channelId,
2024
+ delivery: pendingDelivery,
2025
+ });
2026
+ if (!restartDelivery.valid) {
2027
+ logger.warn(`[Restart] Discarding pending notification without a valid persisted delivery route: channel=${pending.channel} channelId=${pending.channelId}`);
2028
+ fs.rmSync(pendingFile, { force: true });
2029
+ return;
2030
+ }
2031
+ const pendingRouteContext = restartDelivery.delivery ? { delivery: restartDelivery.delivery } : {};
2032
+ const replyContext = {
2033
+ ...(pendingRouteContext ?? {}),
2034
+ ...(pending.rootId ? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId } : {}),
2035
+ };
1902
2036
  const owningAgent = agentRegistry.resolveByChannel(adapter.channelKey);
1903
2037
  const envelope = buildEnvelope({
1904
2038
  taskId: `system-restart-${process.pid}`,
1905
2039
  channel: adapter.channelKey,
1906
2040
  channelId: pending.channelId,
1907
2041
  agentName: owningAgent?.aid || 'evolcore',
1908
- replyContext,
2042
+ ...(restartDelivery.delivery ? { delivery: restartDelivery.delivery } : {}),
2043
+ replyContext: Object.keys(replyContext).length > 0 ? replyContext : undefined,
1909
2044
  });
1910
2045
  await sendSystemPayload(adapter, envelope, {
1911
2046
  kind: 'system.notice',
@@ -2115,53 +2250,6 @@ async function main() {
2115
2250
  catch (e) {
2116
2251
  logger.warn(`控制 AID 首连失败(后台自动重连,不影响 daemon 主流程): ${e?.message || e}`);
2117
2252
  }
2118
- // ── ECWeb 自动启动 ──
2119
- // 如果 config.ecweb.enabled,自动拉起 ecweb 服务
2120
- if (daemonCfg.ecweb?.enabled) {
2121
- const ecwebPath = path.join(resolvePaths().root, 'ecweb');
2122
- const ecwebEntry = path.join(ecwebPath, 'dist', 'index.js');
2123
- if (fs.existsSync(ecwebEntry)) {
2124
- let ecwebVersion;
2125
- try {
2126
- const pkg = JSON.parse(fs.readFileSync(path.join(ecwebPath, 'package.json'), 'utf-8'));
2127
- ecwebVersion = pkg.version;
2128
- }
2129
- catch { }
2130
- const versionError = ecwebVersionRequirementError(ecwebVersion);
2131
- if (versionError) {
2132
- logger.error(`ECWeb 未启动: ${versionError}`);
2133
- }
2134
- else {
2135
- try {
2136
- const ecwebProc = spawn(process.execPath, [ecwebEntry], {
2137
- cwd: ecwebPath,
2138
- detached: true,
2139
- stdio: 'ignore',
2140
- windowsHide: platform.isWindows,
2141
- env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
2142
- });
2143
- ecwebProc.unref();
2144
- logger.info(`✓ ECWeb 已启动 (PID: ${ecwebProc.pid})`);
2145
- onShutdown(() => {
2146
- try {
2147
- if (ecwebProc.pid)
2148
- platform.killProcess(ecwebProc.pid, true);
2149
- logger.info(`✓ ECWeb 已停止`);
2150
- }
2151
- catch { }
2152
- });
2153
- }
2154
- catch (e) {
2155
- logger.warn(`ECWeb 启动失败: ${e?.message || e}`);
2156
- }
2157
- }
2158
- }
2159
- else {
2160
- // 正式安装时 ECWeb 是独立的全局 ec-web 包,由 ec start/restart 的
2161
- // 启动链路管理;runtime 目录没有内嵌构建产物是正常状态。
2162
- logger.debug(`ECWeb runtime entry absent (managed externally): ${ecwebEntry}`);
2163
- }
2164
- }
2165
2253
  // 控制 AID 接收 owner 指令:
2166
2254
  // 1. /pair — ECWeb 配对码(文本快路径)
2167
2255
  // 2. menu.* JSON — 路由到 cmdHandler.execMenuForControl(进程级 + 全量权限)
@@ -2176,22 +2264,20 @@ async function main() {
2176
2264
  catch {
2177
2265
  parsed = null;
2178
2266
  }
2267
+ const menuResponseContext = () => controlReplyContext(opts, evolMenuResponseTransportMetadata());
2179
2268
  if (bindService && parsed?.type === 'bind.request') {
2180
2269
  const response = await bindService.handleRequest(parsed, opts.peerId);
2181
2270
  if (response) {
2182
2271
  // 用 sendStructured 直发 typed payload(payload.type='bind.response'),
2183
2272
  // 不能用 sendMessage——它会把内容包成 {type:'text', text:...},App 无法识别。
2184
2273
  // encrypted 跟随入站请求:bind.response 与 bind.request 的加密/明文对称。
2185
- await controlChannel.sendStructured(opts.channelId, response, { metadata: { encrypted: opts.encrypted } });
2274
+ await controlChannel.sendStructured(opts.channelId, response, controlReplyContext(opts));
2186
2275
  }
2187
2276
  return;
2188
2277
  }
2189
2278
  const menuControl = parseMenuControl(text);
2190
2279
  const isRoleMenu = menuControl.isMenu
2191
2280
  && (menuControl.name === 'role' || menuControl.request.cmd === '/role');
2192
- const menuResponseContext = () => ({
2193
- metadata: evolMenuResponseTransportMetadata(),
2194
- });
2195
2281
  if (menuControl.isMenu && !hasValidMenuId(menuControl)) {
2196
2282
  logger.warn(`[ControlMenu] dropped malformed request without id type=${menuControl.type}`);
2197
2283
  return;
@@ -2216,7 +2302,7 @@ async function main() {
2216
2302
  else {
2217
2303
  reply = 'ECWeb 未运行或暂不可达。请在主机运行 ec watch web 启动后重试。';
2218
2304
  }
2219
- await controlChannel.sendMessage(opts.channelId, reply);
2305
+ await controlChannel.sendMessage(opts.channelId, reply, controlReplyContext(opts));
2220
2306
  return;
2221
2307
  }
2222
2308
  // menu.* JSON 路由:owner 已在上方校验,转交 execMenuForControl(fromControlChannel=true)
@@ -2235,6 +2321,16 @@ async function main() {
2235
2321
  return;
2236
2322
  }
2237
2323
  const deduped = await controlMenuDeduper.execute([opts.channelId, opts.peerId, menuControl.id].join('\u001f'), menuPayloadFingerprint(menuControl.raw), () => cmdHandler.execMenuForControl(parsed, opts.peerId));
2324
+ if ('retry' in deduped) {
2325
+ if (deduped.replayed) {
2326
+ logger.warn(`[MenuControl] category=request-replay request=${menuControl.id}`
2327
+ + ` sender=${opts.peerId} replay=true`);
2328
+ }
2329
+ if (deduped.retry) {
2330
+ logger.warn(`[MenuControl] category=request-retry request=${menuControl.id}`
2331
+ + ` sender=${opts.peerId} retry=true`);
2332
+ }
2333
+ }
2238
2334
  const response = 'conflict' in deduped
2239
2335
  ? menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, { code: 'CONFLICT', message: 'Request ID was already used with a different payload' })
2240
2336
  : deduped.value;
@@ -2244,7 +2340,7 @@ async function main() {
2244
2340
  return;
2245
2341
  }
2246
2342
  // owner 发的其他内容:提示可用指令
2247
- await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取 ECWeb 登录配对码)');
2343
+ await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取 ECWeb 登录配对码)', controlReplyContext(opts));
2248
2344
  }
2249
2345
  catch (e) {
2250
2346
  logger.warn(`控制 AID 消息处理失败: ${e?.message || e}`);
@@ -2296,6 +2392,7 @@ async function main() {
2296
2392
  channel: other.adapter.channelKey,
2297
2393
  channelId: ownerId,
2298
2394
  agentName: owningAgent?.aid || 'evolcore',
2395
+ delivery: { chatType: 'private' },
2299
2396
  });
2300
2397
  sendSystemPayload(other.adapter, envelope, {
2301
2398
  kind: 'system.error',
@@ -2385,11 +2482,13 @@ async function main() {
2385
2482
  channelType: session.channelType || parsedResumeKey?.type,
2386
2483
  selfAID: resumeSelfAID,
2387
2484
  channelId: session.channelId,
2485
+ groupId: session.chatType === 'group' ? (session.metadata?.groupId || session.channelId) : undefined,
2486
+ chatType: session.chatType === 'group' ? 'group' : 'private',
2388
2487
  content: '服务已重启,请继续之前未完成的任务。',
2389
2488
  timestamp: Date.now(),
2390
2489
  peerId: '',
2391
2490
  threadId: session.threadId || undefined,
2392
- replyContext: session.metadata?.replyContext,
2491
+ replyContext: replyContextFromSession(session),
2393
2492
  };
2394
2493
  // 清除状态后入队(processMessage 会重新标记)
2395
2494
  sessionManager.clearProcessing(session.id);
@@ -2721,10 +2820,9 @@ async function main() {
2721
2820
  if (!ch) {
2722
2821
  return { ok: false, error: `AUN channel not found for ${params.aid}`, code: 'AUN_CHANNEL_NOT_FOUND' };
2723
2822
  }
2724
- const targetIsGroup = typeof ch.isGroupId === 'function' && ch.isGroupId(params.to);
2725
- if ((params.scope === 'group') !== targetIsGroup) {
2726
- return { ok: false, error: 'AUN target does not match the delegated send scope', code: 'UNSUPPORTED_TARGET' };
2727
- }
2823
+ // The IPC command already carries the caller-selected transport scope.
2824
+ // Never reclassify the target by parsing its AID string: a private AID may
2825
+ // look group-like and a group identifier may use a future format.
2728
2826
  const encrypt = typeof params.encrypt === 'boolean'
2729
2827
  ? params.encrypt
2730
2828
  : (typeof ch.getDefaultEncrypt === 'function' ? ch.getDefaultEncrypt() : true);
@@ -3180,7 +3278,7 @@ async function main() {
3180
3278
  const agentName = handle?.name;
3181
3279
  if (!agentName)
3182
3280
  return [];
3183
- return messageQueue.getQueueItemsByAgent(agentName);
3281
+ return messageQueue.getQueueItemsByAgent(agentName, { after: params.after, before: params.before });
3184
3282
  });
3185
3283
  ipcServer.setQueueActionExecutor(async (params) => {
3186
3284
  const handle = agentRegistry.get(params.agent);
@@ -3261,8 +3359,12 @@ async function main() {
3261
3359
  const handle = agentRegistry.get(authorization.aid);
3262
3360
  if (!handle?.name)
3263
3361
  return { ok: false, code: 'AGENT_NOT_FOUND', error: 'current Agent not found' };
3264
- if (!params.action)
3265
- return { ok: true, items: messageQueue.getQueueItemsByAgent(handle.name) };
3362
+ if (!params.action) {
3363
+ return { ok: true, items: messageQueue.getQueueItemsByAgent(handle.name, {
3364
+ after: params.after,
3365
+ before: params.before,
3366
+ }) };
3367
+ }
3266
3368
  if (params.action === 'clear')
3267
3369
  return { ok: true, cleared: messageQueue.clearByAgent(handle.name) };
3268
3370
  if (params.action === 'cancel') {
package/dist/ipc.js CHANGED
@@ -15,6 +15,7 @@ import { hashCurrentDelegatedCommand } from './core/auth/agent-delegation.js';
15
15
  import { normalizeAunMentionEntries } from './aun/msg/mention-schema.js';
16
16
  const isWindows = process.platform === 'win32';
17
17
  const isNamedPipe = (p) => isWindows && p.startsWith('\\\\.\\pipe\\');
18
+ const finiteTimestamp = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : undefined);
18
19
  const LOCAL_CONTROL_COMMANDS = new Set([
19
20
  'bind.begin',
20
21
  'bind.status',
@@ -32,6 +33,12 @@ const LOCAL_CONTROL_COMMANDS = new Set([
32
33
  'menu.exec',
33
34
  'monitor-snapshot',
34
35
  ]);
36
+ function requiresLocalControlToken(cmd) {
37
+ // Managed bootstrap completion is authenticated by the session delegation
38
+ // ticket. A standalone CLI invocation still needs the daemon control token.
39
+ return LOCAL_CONTROL_COMMANDS.has(String(cmd.type ?? ''))
40
+ || (cmd.type === 'evolagent.bootstrapComplete' && typeof cmd.sessionId !== 'string');
41
+ }
35
42
  export function summarizeMonitorAgents(agents, aids) {
36
43
  const connectedAgents = new Set(aids.filter(aid => aid.status === 'connected').map(aid => aid.agentName || aid.aid));
37
44
  const enabledAgents = agents.filter(agent => agent.status !== 'disabled');
@@ -379,7 +386,7 @@ export class IpcServer {
379
386
  }
380
387
  }
381
388
  async handleCommand(cmd) {
382
- if (LOCAL_CONTROL_COMMANDS.has(cmd.type) && !this.hasValidControlToken(cmd.controlToken)) {
389
+ if (requiresLocalControlToken(cmd) && !this.hasValidControlToken(cmd.controlToken)) {
383
390
  return { ok: false, code: 'INVALID_CONTROL_TOKEN', error: 'valid local control token is required' };
384
391
  }
385
392
  switch (cmd.type) {
@@ -520,7 +527,11 @@ export class IpcServer {
520
527
  // 纯查询
521
528
  if (!this.queueSnapshotProvider)
522
529
  return { ok: false, error: 'queue-snapshot not configured' };
523
- return { ok: true, items: this.queueSnapshotProvider({ agent: cmd.agent }) };
530
+ return { ok: true, items: this.queueSnapshotProvider({
531
+ agent: cmd.agent,
532
+ after: finiteTimestamp(cmd.after),
533
+ before: finiteTimestamp(cmd.before),
534
+ }) };
524
535
  }
525
536
  // 操作:clear / cancel / interrupt
526
537
  if (!this.queueActionExecutor)
@@ -546,6 +557,8 @@ export class IpcServer {
546
557
  action: cmd.action,
547
558
  messageId: typeof cmd.messageId === 'string' ? cmd.messageId : undefined,
548
559
  sessionKey: typeof cmd.sessionKey === 'string' ? cmd.sessionKey : undefined,
560
+ after: finiteTimestamp(cmd.after),
561
+ before: finiteTimestamp(cmd.before),
549
562
  sessionId: cmd.sessionId,
550
563
  delegationToken: typeof cmd.delegationToken === 'string' ? cmd.delegationToken : undefined,
551
564
  delegationCommandHash: typeof cmd.delegationCommandHash === 'string' ? cmd.delegationCommandHash : undefined,
@@ -1037,7 +1050,7 @@ export class IpcServer {
1037
1050
  export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
1038
1051
  let authenticatedCmd = cmd;
1039
1052
  if (!process.env.EVOLCORE_SESSION_ID
1040
- && LOCAL_CONTROL_COMMANDS.has(cmd.type)
1053
+ && requiresLocalControlToken(cmd)
1041
1054
  && typeof cmd.controlToken !== 'string') {
1042
1055
  try {
1043
1056
  const controlToken = fs.readFileSync(resolvePaths().controlToken, 'utf8').trim();
package/dist/paths.js CHANGED
@@ -149,6 +149,9 @@ export function daemonControlDir() {
149
149
  export function daemonQueuePath() {
150
150
  return path.join(daemonDataDir(), 'message-queue.json');
151
151
  }
152
+ export function daemonMessageDedupPath() {
153
+ return path.join(daemonDataDir(), 'message-dedup.json');
154
+ }
152
155
  export function channelStateDir(channelKey) {
153
156
  if (!channelKey || channelKey === '.' || channelKey === '..') {
154
157
  throw new Error('channelKey is required');