evolcore 0.0.27 → 0.0.29-test.0

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 (195) hide show
  1. package/CHANGELOG.md +38 -15
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +29 -14
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +13 -2
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex/diagnostics.js +4 -3
  9. package/dist/agents/codex-runner.js +444 -106
  10. package/dist/agents/gemini-runner.js +35 -17
  11. package/dist/aun/group/history.js +130 -90
  12. package/dist/aun/group/operations.js +1 -3
  13. package/dist/aun/menu/client.js +1 -0
  14. package/dist/aun/menu/transport.js +5 -3
  15. package/dist/aun/msg/managed-operation.js +70 -0
  16. package/dist/aun/msg/p2p.js +1 -3
  17. package/dist/aun/msg/upload.js +2 -2
  18. package/dist/aun/outbox.js +1 -37
  19. package/dist/channels/aun.js +262 -86
  20. package/dist/channels/daemon.js +18 -1
  21. package/dist/channels/feishu.js +1 -2
  22. package/dist/cli/agent-command.js +23 -3
  23. package/dist/cli/agent-create.js +68 -2
  24. package/dist/cli/agent.js +12 -2
  25. package/dist/cli/aun-commands.js +87 -13
  26. package/dist/cli/cli-argv.js +26 -0
  27. package/dist/cli/daemon-commands.js +116 -149
  28. package/dist/cli/data-command.js +4 -4
  29. package/dist/cli/diag.js +1 -1
  30. package/dist/cli/handoff-command.js +2 -0
  31. package/dist/cli/index.js +18 -1
  32. package/dist/cli/managed-command-guard.js +10 -0
  33. package/dist/cli/memory-command.js +441 -0
  34. package/dist/cli/menu-client.js +4 -0
  35. package/dist/cli/restart-monitor.js +127 -23
  36. package/dist/cli/skill-command.js +41 -0
  37. package/dist/cli/task-context.js +2 -23
  38. package/dist/cli/trigger-command.js +9 -1
  39. package/dist/cli/watch-msg-diag.js +209 -0
  40. package/dist/config/config-field-policy.js +52 -3
  41. package/dist/config/config-manager.js +17 -4
  42. package/dist/config/contact-book-store.js +2 -2
  43. package/dist/config/mention-mode.js +7 -2
  44. package/dist/config/merge.js +28 -9
  45. package/dist/config/resolved-config-op.js +1 -1
  46. package/dist/config/runtime-env.js +137 -0
  47. package/dist/config/schema-registry.js +11 -1
  48. package/dist/config-store.js +10 -10
  49. package/dist/core/audit/event-key.js +3 -1
  50. package/dist/core/audit/log-integrity.js +4 -0
  51. package/dist/core/auth/agent-delegation.js +2 -1
  52. package/dist/core/auth/auth-gateway.js +13 -6
  53. package/dist/core/auth/authorization-audit.js +54 -19
  54. package/dist/core/auth/operation-authorizer.js +17 -0
  55. package/dist/core/auth/operation-catalog.js +9 -0
  56. package/dist/core/causation/context.js +5 -1
  57. package/dist/core/command/agent-control.js +1 -2
  58. package/dist/core/command/command-handler.js +80 -24
  59. package/dist/core/command/diag-service.js +292 -27
  60. package/dist/core/command/memory-slash.js +270 -0
  61. package/dist/core/command/menu-catalog.js +1 -0
  62. package/dist/core/command/menu-handler.js +331 -79
  63. package/dist/core/command/menu-protocol.js +45 -4
  64. package/dist/core/command/slash-gate.js +10 -10
  65. package/dist/core/command/slash-handler.js +230 -79
  66. package/dist/core/data-migration.js +282 -505
  67. package/dist/core/event-catalog.js +26 -0
  68. package/dist/core/evolagent-registry.js +6 -0
  69. package/dist/core/evolagent.js +0 -10
  70. package/dist/core/handoff/store.js +1 -20
  71. package/dist/core/inference/text-inference.js +57 -1
  72. package/dist/core/memory/consolidation-context.js +505 -0
  73. package/dist/core/memory/consolidation-ops-records.js +124 -0
  74. package/dist/core/memory/consolidation-trigger.js +394 -0
  75. package/dist/core/memory/index-manager.js +308 -0
  76. package/dist/core/memory/launch-consolidation.js +395 -0
  77. package/dist/core/memory/memory-admin.js +234 -0
  78. package/dist/core/memory/memory-document.js +100 -0
  79. package/dist/core/memory/memory-session-policy.js +41 -0
  80. package/dist/core/memory/memory-signature.js +257 -0
  81. package/dist/core/memory/memory-writer.js +432 -0
  82. package/dist/core/memory/staleness.js +117 -0
  83. package/dist/core/message/auto-mention-gate.js +454 -0
  84. package/dist/core/message/im-renderer.js +90 -20
  85. package/dist/core/message/message-bridge.js +333 -23
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/message-queue.js +114 -27
  88. package/dist/core/message/pending-hints.js +329 -161
  89. package/dist/core/message/response-engine.js +596 -90
  90. package/dist/core/permission/ec-command-parser.js +1044 -87
  91. package/dist/core/permission/protected-paths.js +376 -126
  92. package/dist/core/permission/sandbox-runtime.js +40 -12
  93. package/dist/core/permission/tool-error-code.js +96 -13
  94. package/dist/core/permission/tool-policy.js +277 -24
  95. package/dist/core/role/runtime-policy.js +3 -7
  96. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  97. package/dist/core/session/session-fs-store.js +0 -76
  98. package/dist/core/session/session-idle-monitor.js +355 -0
  99. package/dist/core/session/session-manager.js +482 -17
  100. package/dist/core/session/session-mapper.js +33 -16
  101. package/dist/core/session/session-renew.js +217 -155
  102. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  103. package/dist/core/skill/global-watcher.js +282 -0
  104. package/dist/core/skill/index-generator.js +84 -0
  105. package/dist/core/skill/startup.js +73 -0
  106. package/dist/core/skill/switch-resolver.js +115 -0
  107. package/dist/core/task/task-activity-store.js +219 -0
  108. package/dist/eck/kit-renderer.js +30 -3
  109. package/dist/eck/manifest-engine.js +26 -4
  110. package/dist/index.js +722 -171
  111. package/dist/ipc.js +564 -2
  112. package/dist/paths.js +30 -9
  113. package/dist/product.js +1 -0
  114. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  115. package/dist/response-system/modes/single-session/index.js +3 -0
  116. package/dist/trigger/feedback.js +1 -0
  117. package/dist/trigger/manager.js +0 -10
  118. package/dist/trigger/scheduler.js +11 -0
  119. package/dist/trigger/validation.js +22 -0
  120. package/dist/utils/atomic-write.js +98 -0
  121. package/dist/utils/avatar-upload.js +59 -18
  122. package/dist/utils/cross-platform.js +87 -16
  123. package/dist/utils/error-dict.json +6 -3
  124. package/dist/utils/error-utils.js +27 -0
  125. package/dist/utils/logger.js +32 -10
  126. package/dist/utils/media-cache.js +84 -0
  127. package/dist/utils/msg-diag.js +97 -0
  128. package/dist/utils/npm-ops.js +119 -4
  129. package/dist/utils/package-version-info.js +244 -0
  130. package/dist/utils/welcome.js +37 -8
  131. package/dist/utils/windows-shell-trust.js +37 -0
  132. package/kits/docs/AUTHORING.md +314 -0
  133. package/kits/docs/GUIDE.md +2 -0
  134. package/kits/docs/INDEX.md +2 -0
  135. package/kits/docs/aun/CHEATSHEET.md +4 -0
  136. package/kits/docs/aun/SDK.md +60 -0
  137. package/kits/docs/channels/aun.md +8 -0
  138. package/kits/docs/context-assembly.md +10 -6
  139. package/kits/docs/evolcore/INDEX.md +17 -3
  140. package/kits/docs/evolcore/agent-create.md +24 -0
  141. package/kits/docs/evolcore/agent.md +5 -2
  142. package/kits/docs/evolcore/config.md +1 -1
  143. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  144. package/kits/docs/evolcore/group.md +34 -18
  145. package/kits/docs/evolcore/memory-architecture.md +78 -0
  146. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  147. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  148. package/kits/docs/evolcore/memory-tags.md +63 -0
  149. package/kits/docs/evolcore/memory.md +110 -0
  150. package/kits/docs/evolcore/menu-client.md +27 -11
  151. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  152. package/kits/docs/evolcore/menu-protocol.md +213 -0
  153. package/kits/docs/evolcore/msg.md +2 -0
  154. package/kits/docs/evolcore/trigger.md +7 -5
  155. package/kits/docs/path-registry.md +6 -8
  156. package/kits/docs/prompt-loading-architecture.md +5 -3
  157. package/kits/docs/venues/group.md +5 -4
  158. package/kits/eck_manifest.json +294 -88
  159. package/kits/eck_manifest.memory.json +257 -0
  160. package/kits/rules/03-identity.md +8 -8
  161. package/kits/rules/05-venue.md +4 -2
  162. package/kits/rules/06-channel.md +1 -1
  163. package/kits/schemas/_meta.json +276 -47
  164. package/kits/schemas/agent-config.schema.12.json +38 -2
  165. package/kits/schemas/agent-config.schema.13.json +591 -0
  166. package/kits/schemas/daemon.schema.6.json +9 -9
  167. package/kits/schemas/defaults.schema.5.json +29 -0
  168. package/kits/schemas/relation-config.schema.8.json +14 -1
  169. package/kits/schemas/relation-config.schema.9.json +129 -0
  170. package/kits/schemas/role-config.schema.1.json +3 -3
  171. package/kits/skills/.switches.json +3 -0
  172. package/kits/skills/ec-skill/SKILL.md +217 -0
  173. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  174. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  175. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  176. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  177. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  178. package/kits/templates/decision/auto-mention.md +25 -0
  179. package/kits/templates/decision/session-renew.md +22 -0
  180. package/kits/templates/memory-consolidation/forget.md +104 -0
  181. package/kits/templates/memory-consolidation/generalize.md +76 -0
  182. package/kits/templates/memory-consolidation/notes.md +133 -0
  183. package/kits/templates/memory-consolidation/profile.md +58 -0
  184. package/kits/templates/memory-consolidation/promote.md +72 -0
  185. package/kits/templates/memory-consolidation/workflow.md +75 -0
  186. package/kits/templates/system-fragments/bootstrap.md +14 -5
  187. package/kits/templates/system-fragments/commands.md +1 -0
  188. package/kits/templates/system-fragments/group-history.md +6 -0
  189. package/kits/templates/system-fragments/identity.md +0 -3
  190. package/kits/templates/system-fragments/memory.md +79 -0
  191. package/kits/templates/system-fragments/reply.md +1 -0
  192. package/kits/templates/system-fragments/venue.md +1 -1
  193. package/package.json +4 -6
  194. package/dist/trigger/legacy-session-history.js +0 -19
  195. package/kits/templates/system-fragments/group-unread-history.md +0 -7
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, daemonMessageDedupPath } from './paths.js';
4
+ import { ensureDataDirs, resolvePaths, agentDir, 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') });
@@ -13,7 +13,7 @@ import { ClaudeSessionFileAdapter } from './core/session/adapters/claude-session
13
13
  import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-file-adapter.js';
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
- import { autoMigrateIfNeeded, loadDefaults, loadAllAgents, migrateIdentitiesIfNeeded, loadDaemonConfig, initializeEckSnapshotsConfig, isFullAccessEnabled } from './config-store.js';
16
+ import { autoMigrateIfNeeded, loadDefaults, loadAllAgents, migrateIdentitiesIfNeeded, loadDaemonConfig, initializeEckSnapshotsConfig } from './config-store.js';
17
17
  import { migrateRelationSharedDirectories } from './core/relation/shared-data.js';
18
18
  import { DEFAULT_ECWEB_PORT, ecwebService } from './config/daemon-services.js';
19
19
  import { ConfigTarget, initConfigManager, onConfigWrite, read as readConfig, shouldFailFastForMissingOwners } from './config/config-manager.js';
@@ -28,6 +28,7 @@ import { ensureStartupConfigSnapshot, isConfigSnapshotsEnabled, retentionCleanup
28
28
  import { appendBootLog, selfDiagnose } from './config/boot-log.js';
29
29
  import { CONFIG_SCHEMA_VERSION } from './types.js';
30
30
  import { SessionManager } from './core/session/session-manager.js';
31
+ import { SessionIdleMonitor } from './core/session/session-idle-monitor.js';
31
32
  import { ClaudeAgentPlugin } from './agents/claude-runner.js';
32
33
  import { CodexAgentPlugin } from './agents/codex-runner.js';
33
34
  import { GeminiAgentPlugin } from './agents/gemini-runner.js';
@@ -44,9 +45,10 @@ import { WecomChannelPlugin, registerPendingWecomContactBind } from './channels/
44
45
  import { buildEnvelope, isDeliveryTargetForChannel, replyContextFromSession } from './core/message/message-utils.js';
45
46
  import { ResponseEngine } from './core/message/response-engine.js';
46
47
  import { MessageQueue } from './core/message/message-queue.js';
48
+ import { pendingInterruptHints } from './core/message/pending-hints.js';
47
49
  import { MessageBridge } from './core/message/message-bridge.js';
48
50
  import { evaluateOutboundContactAdmission } from './core/message/message-admission.js';
49
- import { enforceMenuResponseSize, evaluateMenuClientCompatibility, evolMenuResponseTransportMetadata, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, normalizeMenuResponseTiming, parseMenuControl, validateMenuRequest, withMenuProcessingTime, logMenuRequestCompleted, logMenuRequestReceived } from './core/command/menu-protocol.js';
51
+ import { enforceMenuResponseSize, evaluateMenuClientCompatibility, evolMenuResponseTransportMetadata, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, normalizeMenuResponseTiming, parseMenuControl, validateMenuRequest, withMenuProcessingTime, logMenuRequestCompleted, logMenuRequestReceived, MENU_RESPONSE_MAX_BYTES, MENU_RESPONSE_SAFETY_MARGIN_BYTES } from './core/command/menu-protocol.js';
50
52
  import { computeBuildId, readBuildIdentity, readEvolcoreRuntimeFlavor, readEvolcoreStartedAtIso, readInstalledEvolcoreVersion, tagErrorTextWithEvolcoreVersion, } from './utils/evolcore-version.js';
51
53
  import { recoverAllRoleMutationsSync } from './core/command/role-menu.js';
52
54
  import { HandoffRuntime } from './core/handoff/runtime.js';
@@ -58,6 +60,17 @@ import { executeLocalInitializationCommand, AgentInitializationService, hasSuppl
58
60
  import { loadAgent } from './config-store.js';
59
61
  import { AgentApplicationService } from './core/agent-application-service.js';
60
62
  import { EventBus } from './core/event-bus.js';
63
+ import { seedConsolidationCheckTrigger, removeConsolidationCheckTrigger } from './core/memory/consolidation-trigger.js';
64
+ import { migrateLegacyConsolidationBatches } from './core/memory/consolidation-context.js';
65
+ /** 由 checkIntervalHours 生成 cron 表达式(每小时检查的分钟位错开整点)。 */
66
+ function consolidationCheckCron(intervalHours) {
67
+ const hours = intervalHours && intervalHours > 0 ? Math.floor(intervalHours) : 1;
68
+ if (hours === 1)
69
+ return '17 * * * *';
70
+ if (hours >= 24)
71
+ return '17 3 * * *';
72
+ return `17 */${hours} * * *`;
73
+ }
61
74
  import { eventPatternIncludesInternal, getEventCatalog } from './core/event-catalog.js';
62
75
  import { StatsCollector } from './utils/stats.js';
63
76
  import { AidStatsCollector } from './utils/stats.js';
@@ -68,8 +81,8 @@ import { AgentDelegationRegistry, authorizeDelegatedAunMsgSend, hasTrustedFullAc
68
81
  import { authorizeOperation, buildAuthSubject, canApprovePersistentFullAccessTrigger, isCrossAgentTriggerOperationAllowed, } from './core/auth/auth-gateway.js';
69
82
  import { authorizationDenialData, formatAuthorizationDenial } from './core/auth/authorization-denial.js';
70
83
  import { auditFullAccessEvent } from './core/auth/authorization-audit.js';
71
- import { parsePeerKey } from './core/relation/peer-identity.js';
72
- import { isHClassPath, isSameOrDescendant } from './core/permission/protected-paths.js';
84
+ import { formatPeerKey, parsePeerKey } from './core/relation/peer-identity.js';
85
+ import { isHClassPath, isLClassPath, isSameOrDescendant } from './core/permission/protected-paths.js';
73
86
  import { ChannelLoader, tryParseChannelKey } from './core/channel-loader.js';
74
87
  import { AgentLoader } from './core/baseagent-loader.js';
75
88
  import { EvolAgentRegistry } from './core/evolagent-registry.js';
@@ -101,14 +114,13 @@ import { validateModelSelectionForRole } from './core/model/model-permission.js'
101
114
  import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride, } from './core/role/runtime-policy.js';
102
115
  import { atomicWriteJson } from './core/session/session-fs-store.js';
103
116
  import { DAEMON_RUNTIME_EPOCH_ENV, ensureProcessManagedTempDir } from './cli/task-context.js';
117
+ import { isLinkedInstall } from './utils/npm-ops.js';
104
118
  import { appendMessageLog, buildOutboundEntry, classifyAunPayloadForLog } from './core/message/message-log.js';
105
119
  import { normalizeAunMentionEntries } from './aun/msg/mention-schema.js';
106
- import { MAIN_PACKAGE_NAME } from './product.js';
107
120
  import { sanitizeMenuLogValue } from './core/command/menu-protocol.js';
108
121
  import fs from 'fs';
109
122
  import crypto from 'crypto';
110
123
  import { fileURLToPath } from 'url';
111
- import * as platform from './utils/cross-platform.js';
112
124
  const controlMenuDeduper = new MenuRequestDeduper();
113
125
  /** 出站 payload 摘要(用于 channel-out.log) */
114
126
  function summarizeOutboundPayload(payload) {
@@ -320,6 +332,44 @@ function originFromActorSession(session, authenticatedPeerId, authenticatedChann
320
332
  delivery: { chatType: 'private' },
321
333
  };
322
334
  }
335
+ /** 整理会话未调用 `ec memory feedback` 时的默认反馈模板。 */
336
+ function renderDefaultConsolidationFeedback(input) {
337
+ const triggerLabel = {
338
+ manual: '手动触发',
339
+ compress: '上下文压缩',
340
+ 'session-new': '新会话',
341
+ 'periodic-trigger': '定时检查',
342
+ };
343
+ const mode = input.mode ?? 'notes';
344
+ const cause = triggerLabel[input.triggeredBy] ?? input.triggeredBy;
345
+ if (!input.success) {
346
+ return [
347
+ '## 记忆整理失败',
348
+ '',
349
+ `**模式**:${mode}`,
350
+ `**触发**:${cause}`,
351
+ '',
352
+ '本次整理未完成,已回退未归档的改动。详情见整理批次:',
353
+ `\`${input.batchDir}\``,
354
+ ].join('\n');
355
+ }
356
+ const { created, updated, archived, errors } = input.summary;
357
+ const lines = [
358
+ '## 记忆整理完成',
359
+ '',
360
+ `**模式**:${mode} `,
361
+ `**触发**:${cause}`,
362
+ '',
363
+ '### 处理结果',
364
+ `- 新建记忆:${created} 条`,
365
+ `- 更新记忆:${updated} 条`,
366
+ `- 归档记忆:${archived} 条`,
367
+ ];
368
+ if (errors > 0)
369
+ lines.push(`- 错误:${errors} 条`);
370
+ lines.push('', `整理批次:\`${input.batchDir}\``);
371
+ return lines.join('\n');
372
+ }
323
373
  function seedUpgradeCheckTrigger(manager, owner) {
324
374
  const now = Date.now();
325
375
  const scriptName = 'upgrade-check.cjs';
@@ -372,160 +422,23 @@ function seedUpgradeCheckTrigger(manager, owner) {
372
422
  fs.mkdirSync(dir, { recursive: true });
373
423
  const scriptPath = path.join(dir, scriptName);
374
424
  const packageRoot = getPackageRoot();
375
- const cliPath = path.join(packageRoot, 'dist', 'cli', 'index.js');
376
- const packageJsonPath = path.join(packageRoot, 'package.json');
377
- const devMode = fs.existsSync(path.join(packageRoot, 'src', 'index.ts'));
378
- // Resolve the launch argv at generation time: in a checkout this points the
379
- // monitor at src/ through tsx, so a `npm run build` that wipes dist/ cannot
380
- // take the watchdog down with it.
381
- const monitorEntry = platform.resolveCliEntry(packageRoot);
382
- const stageParent = path.join(resolvePaths().root, 'data', 'monitor-stage');
425
+ // Keep this generated script check-only. It never starts a monitor or runs npm install.
383
426
  fs.writeFileSync(scriptPath, `#!/usr/bin/env node
384
- const fs = require('node:fs');
385
- const path = require('node:path');
386
- const { existsSync, readFileSync } = fs;
387
- const { spawn } = require('node:child_process');
388
-
389
- const CLI_PATH = ${JSON.stringify(monitorEntry.entry)};
390
- const CLI_ARGS = ${JSON.stringify(monitorEntry.args)};
391
- const PACKAGE_ROOT = ${JSON.stringify(packageRoot)};
392
- const STAGE_PARENT = ${JSON.stringify(stageParent)};
393
- const PACKAGE_JSON = ${JSON.stringify(packageJsonPath)};
394
- const PACKAGE_NAME = ${JSON.stringify(MAIN_PACKAGE_NAME)};
395
- const DEV_MODE = ${devMode ? 'true' : 'false'};
396
-
397
- function emit(outcome, text) {
398
- process.stdout.write(JSON.stringify({ outcome, text, files: [] }));
399
- }
400
-
401
- function compareVersions(a, b) {
402
- const pa = a.split('-')[0].split('.').map(Number);
403
- const pb = b.split('-')[0].split('.').map(Number);
404
- const count = Math.max(pa.length, pb.length);
405
- for (let i = 0; i < count; i += 1) {
406
- const av = Number.isFinite(pa[i]) ? pa[i] : 0;
407
- const bv = Number.isFinite(pb[i]) ? pb[i] : 0;
408
- if (av < bv) return -1;
409
- if (av > bv) return 1;
410
- }
411
- return 0;
412
- }
413
-
414
- async function latestVersion() {
415
- const controller = new AbortController();
416
- const timer = setTimeout(() => controller.abort(), 10_000);
417
- try {
418
- const response = await fetch('https://registry.npmjs.org/' + encodeURIComponent(PACKAGE_NAME) + '/latest', {
419
- signal: controller.signal,
420
- headers: { Accept: 'application/json' },
421
- });
422
- if (!response.ok) return null;
423
- const data = await response.json();
424
- return typeof data.version === 'string' ? data.version : null;
425
- } finally {
426
- clearTimeout(timer);
427
- }
428
- }
429
-
427
+ const { pathToFileURL } = require('node:url');
428
+ const entry = ${JSON.stringify(path.join(packageRoot, 'dist', 'utils', 'package-version-info.js'))};
430
429
  async function main() {
431
- if (DEV_MODE) {
432
- emit('noop', 'upgrade check skipped in dev mode');
433
- return;
434
- }
435
-
436
- let localVersion;
437
- try {
438
- localVersion = JSON.parse(readFileSync(PACKAGE_JSON, 'utf8')).version;
439
- } catch {
440
- emit('error', 'failed to read local ec version');
441
- return;
442
- }
443
- if (typeof localVersion !== 'string' || !localVersion) {
444
- emit('error', 'failed to read local ec version');
445
- return;
446
- }
447
-
448
- let remoteVersion;
449
- try {
450
- remoteVersion = await latestVersion();
451
- } catch {
452
- remoteVersion = null;
453
- }
454
- if (!remoteVersion) {
455
- emit('error', 'failed to check evolcore latest version');
456
- return;
457
- }
458
- if (compareVersions(localVersion, remoteVersion) >= 0) {
459
- emit('noop', 'evolcore is already up to date (' + localVersion + '; latest ' + remoteVersion + ')');
460
- return;
461
- }
462
- if (!existsSync(CLI_PATH)) {
463
- emit('error', 'restart-monitor entry not found: ' + CLI_PATH);
464
- return;
465
- }
466
-
467
- // Installed mode: copy the monitor out of the package before it upgrades that
468
- // same package. Mirrors prepareMonitorStage(); kept inline because this file
469
- // is generated standalone CJS. Any failure degrades to launching in place —
470
- // losing upgrade isolation beats losing the upgrade.
471
- let launchArgs = CLI_ARGS;
472
- const launchEnv = { ...process.env };
473
- if (!DEV_MODE && STAGE_PARENT) {
474
- try {
475
- const stageDir = path.join(STAGE_PARENT, 'upgrade-' + process.pid + '-' + Date.now());
476
- fs.mkdirSync(stageDir, { recursive: true });
477
- fs.cpSync(path.join(PACKAGE_ROOT, 'dist'), path.join(stageDir, 'dist'), { recursive: true });
478
- if (existsSync(PACKAGE_JSON)) fs.copyFileSync(PACKAGE_JSON, path.join(stageDir, 'package.json'));
479
- const modulesSource = path.join(PACKAGE_ROOT, 'node_modules');
480
- if (existsSync(modulesSource)) {
481
- const staged = path.join(stageDir, 'node_modules');
482
- try {
483
- // junction works unelevated on Windows; dir symlink elsewhere.
484
- fs.symlinkSync(modulesSource, staged, process.platform === 'win32' ? 'junction' : 'dir');
485
- fs.readdirSync(staged);
486
- } catch (linkError) {
487
- fs.rmSync(staged, { recursive: true, force: true });
488
- fs.cpSync(modulesSource, staged, { recursive: true });
489
- }
490
- }
491
- const stagedEntry = path.join(stageDir, 'dist', 'cli', 'index.js');
492
- if (existsSync(stagedEntry)) {
493
- launchArgs = [stagedEntry];
494
- launchEnv.EVOLCORE_TARGET_PACKAGE_ROOT = PACKAGE_ROOT;
495
- launchEnv.EVOLCORE_MONITOR_STAGE = stageDir;
496
- }
497
- } catch (stageError) {
498
- // Fall through with the in-place entry.
499
- }
500
- }
501
-
502
- try {
503
- const child = spawn(process.execPath, [...launchArgs, 'restart-monitor'], {
504
- detached: true,
505
- stdio: 'ignore',
506
- windowsHide: process.platform === 'win32',
507
- env: launchEnv,
508
- });
509
- child.unref();
510
- const pid = child.pid;
511
- // The daemon is still running here — the monitor is what stops it. So a
512
- // monitor that dies on load costs the upgrade, not the service; still,
513
- // report it accurately instead of claiming success.
514
- await new Promise(resolve => setTimeout(resolve, 3000));
515
- let alive = false;
516
- try { process.kill(pid, 0); alive = true; } catch (e) { alive = false; }
517
- if (!alive) {
518
- emit('error', 'restart-monitor exited during startup; upgrade to ' + remoteVersion + ' not applied');
519
- return;
520
- }
521
- emit('success', 'evolcore upgrade available: ' + localVersion + ' -> ' + remoteVersion + '; restart-monitor started');
522
- } catch (error) {
523
- emit('error', 'failed to start restart-monitor: ' + (error && error.message ? error.message : String(error)));
524
- }
430
+ const { refreshManagedPackageVersions } = await import(pathToFileURL(entry).href);
431
+ const results = await refreshManagedPackageVersions(undefined, { source: 'daily-check' });
432
+ const failed = results.some(result => result.status === 'unavailable');
433
+ process.stdout.write(JSON.stringify({
434
+ outcome: failed ? 'error' : 'success',
435
+ text: results.map(result => result.packageName + ': ' + result.status).join('; '),
436
+ files: [],
437
+ }));
525
438
  }
526
-
527
- main().catch((error) => {
528
- emit('error', 'upgrade check failed: ' + (error && error.message ? error.message : String(error)));
439
+ main().catch(() => {
440
+ process.stdout.write(JSON.stringify({ outcome: 'error', text: 'Version information refresh failed', files: [] }));
441
+ process.exitCode = 1;
529
442
  });
530
443
  `);
531
444
  // Remove the pre-Node script left by older installations so it cannot be
@@ -581,6 +494,52 @@ function seedTaskRecoveryTrigger(manager, owner) {
581
494
  atomicWriteJson(manager.definitionPath(definition.id), definition);
582
495
  fs.rmSync(manager.activePath(definition.id), { force: true });
583
496
  }
497
+ const SESSION_IDLE_CHECK_PROMPT = `本会话已闲置一段时间。请依据你的职责执行一次例行巡检:
498
+ 1. 判断群内是否存在成员有未完成的工作却停滞(如长时间无进展、被点名未回应、承诺后失联)。有,则向该成员催促一次并说明停滞事项,随后即止;无,则不做任何动作。大家都在正常工作或无待办时保持沉默。
499
+ 2. 巡检优先基于已有上下文判断,确需补充背景再查询历史,并遵守会话自身的发送规范。
500
+ 3. 判定无需任何对外动作时,本轮只输出 [[NOOP]],不发送任何消息、不发布巡检结论。注意:最终对外输出必须是且仅是 [[NOOP]] 这一行——不要在输出它之前先发送任何说明文字或巡检判定,判定过程仅在内部完成。
501
+ (本指令为通用模板,可按具体职责增删检查项。)`;
502
+ function seedSessionIdleCheckTrigger(manager, owner) {
503
+ const now = Date.now();
504
+ const definition = normalizeTriggerDefinition({
505
+ $schema_version: 3.1,
506
+ id: '__session-idle-check',
507
+ agentAid: owner.aid,
508
+ enabled: true,
509
+ name: '__session-idle-check',
510
+ description: 'System trigger: inspect configured idle group sessions.',
511
+ createdAt: now,
512
+ updatedAt: now,
513
+ origin: {
514
+ channelKey: 'daemon',
515
+ channelType: 'daemon',
516
+ channelId: owner.originPeerId || owner.aid,
517
+ session: 'main',
518
+ peerId: owner.originPeerId || owner.aid,
519
+ sessionKey: `daemon#${owner.originPeerId || owner.aid}#__session-idle-check__`,
520
+ },
521
+ source: {
522
+ type: 'event',
523
+ eventPattern: 'session:idle',
524
+ filter: { match: { chatType: 'group' } },
525
+ },
526
+ execution: {
527
+ type: 'target_session',
528
+ prompt: SESSION_IDLE_CHECK_PROMPT,
529
+ onError: 'retry',
530
+ noopSentinel: '[[NOOP]]',
531
+ },
532
+ feedback: { strategy: 'event_session' },
533
+ reliability: {
534
+ concurrency: 'forbid',
535
+ missedPolicy: 'run_once',
536
+ retry: { maxAttempts: 2, backoffMs: 30_000 },
537
+ },
538
+ });
539
+ fs.mkdirSync(manager.triggerDir(definition.id), { recursive: true });
540
+ atomicWriteJson(manager.definitionPath(definition.id), definition);
541
+ fs.rmSync(manager.activePath(definition.id), { force: true });
542
+ }
584
543
  function removeLegacyAgentUpgradeCheck(manager) {
585
544
  let existing;
586
545
  try {
@@ -1013,7 +972,16 @@ async function main() {
1013
972
  if (relationSharedMigration.copied > 0) {
1014
973
  logger.info(`[migrate] Synced ${relationSharedMigration.copied} relation shared file(s) across ${relationSharedMigration.relations} relation(s)`);
1015
974
  }
975
+ migrateLegacyConsolidationBatches();
1016
976
  // daemon.json 的旧 ecweb/serviceProxy 结构已在启动前归一化为 services[]。
977
+ // ── Skill 监控器启动(采用懒加载机制)──
978
+ try {
979
+ const { startSkillWatcher } = await import('./core/skill/startup.js');
980
+ startSkillWatcher();
981
+ }
982
+ catch (e) {
983
+ logger.warn(`[skill] 监控器启动失败: ${e instanceof Error ? e.message : String(e)}`);
984
+ }
1017
985
  // ── 配置体系初始化(schema 字段不相交硬约束校验)──
1018
986
  try {
1019
987
  initConfigManager();
@@ -1278,20 +1246,185 @@ async function main() {
1278
1246
  logger.info(`[PermissionGateway] Cancelled ${cancelled} stale management approval(s) after ${target} config write: agent=${selector.self}`);
1279
1247
  }
1280
1248
  });
1249
+ // 整理会话结束(成功 / 失败 / 中断):回撤、processing/ 归位、定稿 rebuild、forget 后置检查、反馈
1250
+ // releaseSlot / feedbackSender 由后面的块赋值(避免暂时性死区)。
1251
+ let releaseConsolidationSlotRef = () => { };
1252
+ let sendConsolidationFeedbackRef = async () => { };
1253
+ const finishConsolidationSession = async (sessionId, success) => {
1254
+ let releaseAid;
1255
+ try {
1256
+ if (!sessionId)
1257
+ return;
1258
+ const session = await sessionManager.getSessionById(sessionId);
1259
+ if (!session)
1260
+ return;
1261
+ const meta = (session.metadata ?? {});
1262
+ const batchDir = (meta.consolidationBatchDir ?? meta.consolidationLogBase);
1263
+ if (!batchDir)
1264
+ return; // 不是整理会话
1265
+ releaseAid = session.selfAID;
1266
+ const { onConsolidationComplete, rollbackConsolidation } = await import('./core/memory/launch-consolidation.js');
1267
+ const { appendOpsRecord, summarizeOpsRecords, collectAffectedScopes } = await import('./core/memory/consolidation-ops-records.js');
1268
+ // ① 失败时先回撤写操作,再做 processing/ 归位
1269
+ if (!success) {
1270
+ await rollbackConsolidation(batchDir, session.selfAID);
1271
+ }
1272
+ // ② processing/ 归位(成功→archive/notes,失败→pending)+ 写 checkpoint
1273
+ const completeResult = await onConsolidationComplete({
1274
+ success,
1275
+ batchDir,
1276
+ chatDir: meta.consolidationChatDir,
1277
+ memoryDir: meta.consolidationMemoryDir,
1278
+ });
1279
+ // ③ 收尾定稿 rebuild:对 ops.jsonl 里出现的所有 scope 各 rebuild 一次
1280
+ // 无论成败都做,确保索引和磁盘文件一致(失败回撤后索引也跟着还原)
1281
+ try {
1282
+ const affectedScopes = collectAffectedScopes(batchDir); // Map<scope, memoryDir>
1283
+ if (affectedScopes.size > 0) {
1284
+ const { rebuildIndex } = await import('./core/memory/index-manager.js');
1285
+ const selfAid = session.selfAID;
1286
+ const mc = selfAid ? agentRegistry?.get(selfAid)?.config?.memory : undefined;
1287
+ const maxBytes = mc?.indexMaxBytes ?? 5 * 1024;
1288
+ for (const [scope, memoryDir] of affectedScopes) {
1289
+ const consolidatedDir = path.join(memoryDir, 'consolidated');
1290
+ const indexPath = path.join(memoryDir, 'MEMORY.md');
1291
+ const invalidDir = path.join(memoryDir, 'archive', 'invalid');
1292
+ try {
1293
+ await rebuildIndex(consolidatedDir, indexPath, scope, invalidDir, undefined, maxBytes);
1294
+ }
1295
+ catch { /* 单个 scope rebuild 失败不影响其他 scope */ }
1296
+ }
1297
+ }
1298
+ }
1299
+ catch { /* rebuild 失败下次整理时补上 */ }
1300
+ // ④ 写 session-end 日志(落笔,记录最终状态)
1301
+ appendOpsRecord(batchDir, {
1302
+ ts: new Date().toISOString(),
1303
+ op: 'session-end',
1304
+ success,
1305
+ movedNotes: completeResult.moved,
1306
+ checkpointWritten: completeResult.checkpointWritten,
1307
+ });
1308
+ // ⑤ forget 后置检查:成功且有产出且不是 forget 模式本身
1309
+ if (success) {
1310
+ const memoryMode = meta.memoryMode;
1311
+ if (memoryMode && memoryMode !== 'forget') {
1312
+ const opsSummary = summarizeOpsRecords(batchDir);
1313
+ const hasOutput = completeResult.moved > 0 || opsSummary.created > 0 || opsSummary.updated > 0;
1314
+ if (hasOutput) {
1315
+ try {
1316
+ const { collectAffectedScopes: getScopes } = await import('./core/memory/consolidation-ops-records.js');
1317
+ const { collectCandidates, isDueForReview, deferCandidate } = await import('./core/memory/staleness.js');
1318
+ const selfAid = session.selfAID;
1319
+ if (selfAid) {
1320
+ const mc = agentRegistry?.get(selfAid)?.config?.memory;
1321
+ const thresholds = { forgetAfterDays: mc?.forgetAfterDays ?? 3 };
1322
+ const now = Date.now();
1323
+ const affectedScopes = getScopes(batchDir);
1324
+ for (const [scope, memoryDir] of affectedScopes) {
1325
+ const consolidatedDir = path.join(memoryDir, 'consolidated');
1326
+ let due = 0;
1327
+ try {
1328
+ for (const info of collectCandidates(consolidatedDir, thresholds, now)) {
1329
+ if (isDueForReview(info, thresholds, now))
1330
+ due++;
1331
+ else {
1332
+ try {
1333
+ deferCandidate(info.filePath, now);
1334
+ }
1335
+ catch { /* ignore */ }
1336
+ }
1337
+ }
1338
+ }
1339
+ catch {
1340
+ continue;
1341
+ }
1342
+ if (due > 0) {
1343
+ const ownerAid = mc ? agentRegistry?.get(selfAid)?.config?.owners?.[0] : undefined;
1344
+ // 构造最小 taskCtx,定时触发不需要 sessionId/peerKey
1345
+ const taskCtx = { selfAid, peerKey: meta.peerKey };
1346
+ void enqueueConsolidation({
1347
+ taskCtx,
1348
+ mode: 'forget',
1349
+ triggeredBy: 'periodic-trigger',
1350
+ ownerAid,
1351
+ forgetAfterDays: mc?.forgetAfterDays,
1352
+ scopeOverride: scope,
1353
+ }).catch(() => { });
1354
+ }
1355
+ }
1356
+ }
1357
+ }
1358
+ catch { /* forget 后置检查失败不影响主流程 */ }
1359
+ }
1360
+ }
1361
+ }
1362
+ // ⑥ 会话历史导出 → <batchDir>/history.jsonl
1363
+ const agentSessionId = session.agentSessionId;
1364
+ const projectPath = session.projectPath;
1365
+ const baseagent = session.baseagent ?? 'claude';
1366
+ if (agentSessionId && projectPath) {
1367
+ const fs = await import('fs');
1368
+ const historyPath = path.join(batchDir, 'history.jsonl');
1369
+ const runner = agentMap.get(`${session.selfAID}::${baseagent}`);
1370
+ if (runner?.getSessionMessages) {
1371
+ try {
1372
+ const messages = await runner.getSessionMessages(agentSessionId, projectPath);
1373
+ fs.default.writeFileSync(historyPath, messages.map((m) => JSON.stringify(m)).join('\n') + '\n', 'utf-8');
1374
+ }
1375
+ catch { /* 导出失败不影响主流程 */ }
1376
+ }
1377
+ else {
1378
+ fs.default.writeFileSync(historyPath, JSON.stringify({ note: `runner ${baseagent} does not support getSessionMessages` }) + '\n', 'utf-8');
1379
+ }
1380
+ }
1381
+ // ⑦ 反馈:非定时触发时,把整理结果投递回发起时的会话和话题
1382
+ const feedbackOrigin = meta.feedbackOrigin;
1383
+ const triggeredBy = meta.consolidationTriggeredBy;
1384
+ if (feedbackOrigin && triggeredBy && triggeredBy !== 'periodic-trigger' && session.selfAID) {
1385
+ try {
1386
+ const { readFeedbackText, summarizeOpsRecords } = await import('./core/memory/consolidation-ops-records.js');
1387
+ const customText = readFeedbackText(batchDir);
1388
+ const text = customText ?? renderDefaultConsolidationFeedback({
1389
+ mode: meta.memoryMode,
1390
+ triggeredBy,
1391
+ success,
1392
+ summary: summarizeOpsRecords(batchDir),
1393
+ batchDir,
1394
+ });
1395
+ await sendConsolidationFeedbackRef(feedbackOrigin, text, session.selfAID);
1396
+ }
1397
+ catch (e) {
1398
+ logger.warn(`[Memory] 整理反馈投递失败(session=${sessionId}): ${e?.message}`);
1399
+ }
1400
+ }
1401
+ }
1402
+ catch (e) {
1403
+ logger.warn(`[Memory] 整理收尾失败(session=${sessionId}): ${e?.message}`);
1404
+ }
1405
+ finally {
1406
+ // 释放该 agent 的并发位并推进队列(幂等:不在活跃集合里也无害)
1407
+ if (releaseAid)
1408
+ releaseConsolidationSlotRef(releaseAid);
1409
+ }
1410
+ };
1281
1411
  eventBus.subscribe('task:interrupted', (event) => {
1282
1412
  if ('sessionId' in event && event.sessionId) {
1283
1413
  permissionGateway.cancelAll(event.sessionId, event.reason || 'interrupted');
1284
1414
  }
1415
+ void finishConsolidationSession(event.sessionId, false);
1285
1416
  });
1286
1417
  eventBus.subscribe('task:error', (event) => {
1287
1418
  if ('sessionId' in event && event.sessionId) {
1288
1419
  permissionGateway.cancelAll(event.sessionId, 'task_error', false);
1289
1420
  }
1421
+ void finishConsolidationSession(event.sessionId, false);
1290
1422
  });
1291
1423
  eventBus.subscribe('task:completed', (event) => {
1292
1424
  if ('sessionId' in event && event.sessionId) {
1293
1425
  permissionGateway.cancelAll(event.sessionId, 'task_completed', false);
1294
1426
  }
1427
+ void finishConsolidationSession(event.sessionId, true);
1295
1428
  });
1296
1429
  // 交互路由器
1297
1430
  const interactionRouter = new InteractionRouter();
@@ -1409,6 +1542,16 @@ async function main() {
1409
1542
  }, primaryRunnerKey);
1410
1543
  responseEngine.setAgentDelegationRegistry(agentDelegationRegistry);
1411
1544
  const processor = responseEngine;
1545
+ responseEngine.setObserverHintConsumed((hint, target, meta) => {
1546
+ const observerChannel = channelInstances.find(candidate => candidate.adapter.channelKey === target.channelKey
1547
+ && candidate.channelType === 'aun')?.channel;
1548
+ observerChannel?.notifyObserverHintConsumed(hint, target, meta);
1549
+ });
1550
+ responseEngine.setObserverHintFailed((hint, target, failure) => {
1551
+ const observerChannel = channelInstances.find(candidate => candidate.adapter.channelKey === target.channelKey
1552
+ && candidate.channelType === 'aun')?.channel;
1553
+ observerChannel?.notifyObserverHintFailed(hint, target, failure);
1554
+ });
1412
1555
  // 回填 processor 和 messageQueue 的引用
1413
1556
  cmdHandler.setProcessor(processor);
1414
1557
  // Inject EvolAgentRegistry (methods added by T6/T7)
@@ -1434,13 +1577,13 @@ async function main() {
1434
1577
  dedupPath: daemonMessageDedupPath(),
1435
1578
  });
1436
1579
  // 设置中断回调(精确中断正在处理的 agent)
1437
- messageQueue.setInterruptCallback(async (sessionKey, baseagent, evolagentName, reason = 'new_message', timeout) => {
1580
+ messageQueue.setInterruptCallback(async (sessionKey, baseagent, evolagentName, reason = 'new_message', timeout, strict) => {
1438
1581
  void baseagent;
1439
1582
  void evolagentName;
1440
1583
  if (timeout)
1441
1584
  await responseEngine.timeoutSession(sessionKey, timeout);
1442
1585
  else
1443
- await responseEngine.interruptSession(sessionKey, reason);
1586
+ await responseEngine.interruptSession(sessionKey, reason, strict);
1444
1587
  });
1445
1588
  messageQueue.setEventBus(eventBus);
1446
1589
  // Restore persisted input before Trigger schedulers start. Trigger-owned work
@@ -1508,6 +1651,37 @@ async function main() {
1508
1651
  cmdHandler.setHandoffRuntime(handoffRuntime);
1509
1652
  // Trigger runtime: daemon-level script + feedback scheduler.
1510
1653
  const triggerSchedulers = new Map();
1654
+ const sessionIdleMonitor = new SessionIdleMonitor({
1655
+ eventBus,
1656
+ statePath: path.join(paths.dataDir, 'session-idle-state.json'),
1657
+ getSessionById: sessionId => sessionManager.getSessionById(sessionId),
1658
+ isSessionBusy: sessionId => messageQueue.isProcessing(sessionId) || messageQueue.getQueueLength(sessionId) > 0,
1659
+ getSessionIdleTriggerIds: () => [...triggerSchedulers.values()].flatMap(scheduler => scheduler.list({ all: true })
1660
+ .filter(definition => definition.source.type === 'event' && definition.source.eventPattern === 'session:idle')
1661
+ .map(definition => definition.id)),
1662
+ getSessionIdleSeconds: (session) => {
1663
+ if (!session.selfAID || !session.channelId)
1664
+ return undefined;
1665
+ try {
1666
+ const relation = readConfig(ConfigTarget.Relation, {
1667
+ self: session.selfAID,
1668
+ peerKey: formatPeerKey(session.channelType || session.channel, session.channelId),
1669
+ }, { cache: true, expand: true });
1670
+ return relation?.sessionIdleSec;
1671
+ }
1672
+ catch (error) {
1673
+ logger.warn(`[SessionIdleMonitor] Failed to read relation config for ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
1674
+ return undefined;
1675
+ }
1676
+ },
1677
+ getSessionEncrypt: sessionId => sessionManager.getSessionEncrypt(sessionId),
1678
+ });
1679
+ sessionIdleMonitor.start();
1680
+ onConfigWrite(({ target, selector }) => {
1681
+ if (target === ConfigTarget.Relation && selector.self)
1682
+ sessionIdleMonitor.refresh();
1683
+ });
1684
+ onShutdown(() => sessionIdleMonitor.stop());
1511
1685
  const targetSessionLocks = new TargetSessionLockRegistry();
1512
1686
  const triggerAudit = new TriggerAuditLogger();
1513
1687
  const triggerScriptExecutor = new TriggerScriptExecutor();
@@ -1515,6 +1689,81 @@ async function main() {
1515
1689
  watchdogMs: daemonConversationWatchdogMs(globalSettings),
1516
1690
  totalExecutionMs: taskExecutionTtlMs,
1517
1691
  });
1692
+ // ── 记忆整理队列:同 agent 串行 + 全局并发上限 ─────────────────────────
1693
+ //
1694
+ // 整理会话不再经 trigger 系统创建,改为 launchConsolidation 直接创建会话。
1695
+ // 并发控制由这里接管(原来靠 trigger 的 reliability.concurrency='forbid'):
1696
+ // - 同一 agent 同时最多 1 个整理会话
1697
+ // - 全局同时最多 MAX_CONCURRENT_CONSOLIDATION(5) 个 agent 在整理
1698
+ // - 超限的请求排队,前面的结束后自动启动下一个
1699
+ const MAX_CONCURRENT_CONSOLIDATION = 5;
1700
+ const activeConsolidationAgents = new Set(); // 正在整理的 agentAid
1701
+ const consolidationQueues = new Map(); // agentAid → 待办
1702
+ const startConsolidation = async (params) => {
1703
+ const aid = params.taskCtx.selfAid;
1704
+ if (!aid)
1705
+ return null;
1706
+ activeConsolidationAgents.add(aid);
1707
+ try {
1708
+ const { launchConsolidation } = await import('./core/memory/launch-consolidation.js');
1709
+ return await launchConsolidation(daemonChannel, params);
1710
+ }
1711
+ catch (e) {
1712
+ logger.warn(`[Memory] 拉起整理会话失败(aid=${aid},mode=${params.mode}): ${e?.message}`);
1713
+ // 启动失败不进收尾事件,直接释放并发位并推进队列
1714
+ activeConsolidationAgents.delete(aid);
1715
+ void drainConsolidationQueue();
1716
+ return null;
1717
+ }
1718
+ };
1719
+ /** 有空位就从队列里取等待最久的请求启动。 */
1720
+ const drainConsolidationQueue = async () => {
1721
+ while (activeConsolidationAgents.size < MAX_CONCURRENT_CONSOLIDATION) {
1722
+ // 找第一个队列非空、且该 agent 当前不活跃的请求
1723
+ let picked;
1724
+ for (const [aid, queue] of consolidationQueues) {
1725
+ if (queue.length === 0)
1726
+ continue;
1727
+ if (activeConsolidationAgents.has(aid))
1728
+ continue;
1729
+ picked = { aid, params: queue.shift() };
1730
+ break;
1731
+ }
1732
+ if (!picked)
1733
+ break;
1734
+ if (consolidationQueues.get(picked.aid)?.length === 0)
1735
+ consolidationQueues.delete(picked.aid);
1736
+ await startConsolidation(picked.params);
1737
+ }
1738
+ };
1739
+ /**
1740
+ * 整理请求统一入口。四种触发方式(手动 / compact / renew·new / 定时)都走这里。
1741
+ * 该 agent 不活跃且全局有空位 → 立即启动并返回结果;否则入队并返回 null。
1742
+ */
1743
+ const enqueueConsolidation = async (params) => {
1744
+ const aid = params.taskCtx.selfAid;
1745
+ if (!aid)
1746
+ return null;
1747
+ const agentBusy = activeConsolidationAgents.has(aid);
1748
+ const queue = consolidationQueues.get(aid) ?? [];
1749
+ const globalFull = activeConsolidationAgents.size >= MAX_CONCURRENT_CONSOLIDATION;
1750
+ if (!agentBusy && !globalFull && queue.length === 0) {
1751
+ return await startConsolidation(params);
1752
+ }
1753
+ queue.push(params);
1754
+ consolidationQueues.set(aid, queue);
1755
+ logger.info(`[Memory] 整理请求入队(aid=${aid},mode=${params.mode},` +
1756
+ `队列长度=${queue.length},全局活跃=${activeConsolidationAgents.size})`);
1757
+ return null;
1758
+ };
1759
+ /** 整理会话结束(成功/失败/中断)时调用:释放并发位,推进队列。 */
1760
+ const releaseConsolidationSlot = (aid) => {
1761
+ activeConsolidationAgents.delete(aid);
1762
+ void drainConsolidationQueue();
1763
+ };
1764
+ // 接到前面 finishConsolidationSession 的 holder(那个函数定义在本块之前)
1765
+ releaseConsolidationSlotRef = releaseConsolidationSlot;
1766
+ cmdHandler.setMemoryConsolidationLauncher(enqueueConsolidation);
1518
1767
  const triggerControlToken = crypto.randomBytes(32).toString('base64url');
1519
1768
  fs.writeFileSync(resolvePaths().controlToken, triggerControlToken, { mode: 0o600 });
1520
1769
  try {
@@ -1595,6 +1844,43 @@ async function main() {
1595
1844
  baseagent: agent.baseagent,
1596
1845
  };
1597
1846
  };
1847
+ // 整理会话反馈投递:用发起时抓取的渠道三元组直接调 adapter.send()。
1848
+ // 不查 sessionId——renew/`/new` 换了会话 ID 也不影响送达。
1849
+ const sendConsolidationFeedback = async (origin, text, selfAid) => {
1850
+ const { buildEnvelope } = await import('./core/message/message-utils.js');
1851
+ const channelKey = origin.channelKey;
1852
+ const channelId = origin.channelId;
1853
+ if (!channelKey || !channelId) {
1854
+ logger.warn(`[Memory] 反馈目标缺少 channelKey/channelId,跳过投递`);
1855
+ return;
1856
+ }
1857
+ const binding = getTriggerChannel(selfAid, channelKey);
1858
+ if (!binding) {
1859
+ logger.warn(`[Memory] 找不到渠道绑定(aid=${selfAid},channelKey=${channelKey}),跳过反馈`);
1860
+ return;
1861
+ }
1862
+ const threadId = origin.threadId;
1863
+ const chatType = origin.delivery?.chatType;
1864
+ const replyContext = {};
1865
+ if (threadId)
1866
+ replyContext.threadId = threadId;
1867
+ if (chatType)
1868
+ replyContext.delivery = { chatType };
1869
+ await binding.adapter.send(buildEnvelope({
1870
+ taskId: `memory-feedback-${Date.now()}`,
1871
+ channel: binding.adapter.channelKey,
1872
+ channelId,
1873
+ agentName: binding.agentName,
1874
+ replyContext: Object.keys(replyContext).length > 0 ? replyContext : undefined,
1875
+ }), {
1876
+ kind: 'result.text',
1877
+ text,
1878
+ isFinal: true,
1879
+ format: 'markdown',
1880
+ });
1881
+ logger.info(`[Memory] 整理反馈已投递(aid=${selfAid},channelId=${channelId})`);
1882
+ };
1883
+ sendConsolidationFeedbackRef = sendConsolidationFeedback;
1598
1884
  const validateTriggerFeedbackChannels = (definition) => {
1599
1885
  if (definition.feedback.strategy === 'silent' || definition.feedback.strategy === 'event_session')
1600
1886
  return;
@@ -1635,8 +1921,6 @@ async function main() {
1635
1921
  const hasDaemonTriggerAuthority = (subject) => (subject.isDaemonOwner || subject.processRole === 'daemon-service' || subject.dataScope === 'daemon');
1636
1922
  const validateTriggerDefinitionForActor = (definition, actor) => {
1637
1923
  if (definition.execution.permissionMode === 'fullaccess') {
1638
- if (!isFullAccessEnabled())
1639
- throw new Error('fullaccess is disabled by daemon.json');
1640
1924
  if (!actor.daemonOwner && !actor.fullAccess) {
1641
1925
  throw new Error('persistent fullaccess Trigger requires DaemonOwner or fullaccess-run');
1642
1926
  }
@@ -1695,9 +1979,6 @@ async function main() {
1695
1979
  }
1696
1980
  };
1697
1981
  const authorizeTriggerExecution = (definition) => {
1698
- if (definition.execution.permissionMode === 'fullaccess' && !isFullAccessEnabled()) {
1699
- return { allowed: false, reason: 'fullaccess is disabled by daemon.json' };
1700
- }
1701
1982
  if (definition.execution.permissionMode === 'fullaccess') {
1702
1983
  if (!definition.authorizedBy) {
1703
1984
  return { allowed: false, reason: 'fullaccess Trigger approval provenance is missing' };
@@ -1714,7 +1995,7 @@ async function main() {
1714
1995
  // task. Its origin remains useful for audit and feedback routing, but a
1715
1996
  // relation lookup on the origin actor would make a cross-Agent Trigger
1716
1997
  // persist successfully and then fail on every fire after creation.
1717
- // Approval provenance and the feature flag above remain mandatory; only
1998
+ // Approval provenance remains mandatory; only
1718
1999
  // the ordinary creator relation gate is bypassed for this mode.
1719
2000
  if (definition.execution.permissionMode === 'fullaccess') {
1720
2001
  return { allowed: true };
@@ -1902,10 +2183,22 @@ async function main() {
1902
2183
  if (opts.seedUpgradeCheck) {
1903
2184
  seedUpgradeCheckTrigger(manager, owner);
1904
2185
  seedTaskRecoveryTrigger(manager, owner);
2186
+ seedSessionIdleCheckTrigger(manager, owner);
1905
2187
  }
1906
2188
  else {
1907
2189
  removeLegacyAgentUpgradeCheck(manager);
1908
2190
  }
2191
+ // 记忆整理定时检查(每个 agent 一个)。记忆关闭时不注册。
2192
+ const memConfig = agentRegistry.get(owner.aid)?.config?.memory;
2193
+ if (memConfig?.enableRelation !== false) {
2194
+ seedConsolidationCheckTrigger(manager, {
2195
+ aid: owner.aid,
2196
+ cronExpression: consolidationCheckCron(memConfig?.checkIntervalHours),
2197
+ });
2198
+ }
2199
+ else {
2200
+ removeConsolidationCheckTrigger(manager);
2201
+ }
1909
2202
  const state = new TriggerRunStateStore(manager);
1910
2203
  const agentTriggerAudit = triggerAudit.withHistory(manager.history);
1911
2204
  const dispatcher = new TriggerFeedbackDispatcher({
@@ -2187,6 +2480,12 @@ async function main() {
2187
2480
  };
2188
2481
  });
2189
2482
  }
2483
+ if (inst.channelType === 'aun') {
2484
+ inst.channel.setObserverHintRuntime({
2485
+ hasSession: target => responseEngine.hasObserverTargetSession(target),
2486
+ schedule: target => responseEngine.scheduleObserverHint(target),
2487
+ });
2488
+ }
2190
2489
  // 5. 撤回消息 → 中断执行中任务
2191
2490
  inst.channel.onRecall?.((messageId, selfAID) => {
2192
2491
  msgBridge.cancel(messageId, selfAID);
@@ -2207,6 +2506,19 @@ async function main() {
2207
2506
  agent.status = 'running';
2208
2507
  }
2209
2508
  }
2509
+ // 为所有已置为 running 的 agent 注册技能目录监控(懒加载机制)
2510
+ // 必须在 agent.status = 'running' 之后执行,否则全部还是 'stopped'
2511
+ try {
2512
+ const { watchAgentSkills } = await import('./core/skill/startup.js');
2513
+ for (const info of agentRegistry.list()) {
2514
+ if (info.status === 'running') {
2515
+ watchAgentSkills(info.aid);
2516
+ }
2517
+ }
2518
+ }
2519
+ catch (e) {
2520
+ logger.warn(`[skill] agent 监控注册失败: ${e instanceof Error ? e.message : String(e)}`);
2521
+ }
2210
2522
  if (agentRegistry.list().some((info) => info.status === 'running')) {
2211
2523
  agentRuntimeState = 'running';
2212
2524
  agentRuntimeError = undefined;
@@ -2417,10 +2729,23 @@ async function main() {
2417
2729
  })
2418
2730
  .join(', ');
2419
2731
  };
2732
+ // ChannelLoader deliberately does not await onConnected. Keep the initial
2733
+ // recovery promises so restored ordinary messages cannot start ahead of
2734
+ // pending priority hints for the same session.
2735
+ const startupObserverRecoveries = [];
2736
+ let restoringStartupQueue = true;
2737
+ const queuePendingObserverHints = (aid, channelKey) => pendingInterruptHints(paths.sessionsDir, aid)
2738
+ .filter(pending => pending.channelKey === channelKey)
2739
+ .map(pending => responseEngine.scheduleObserverHint({ ...pending, selfAid: aid })
2740
+ .then(result => {
2741
+ if (result === 'SESSION_NOT_FOUND')
2742
+ logger.warn(`[ObserverHint] recovery target missing hint=${pending.hintId}`);
2743
+ })
2744
+ .catch(error => logger.warn(`[ObserverHint] recovery failed hint=${pending.hintId}: ${error}`)));
2420
2745
  const markChannelConnected = async (inst) => {
2421
2746
  const name = inst.adapter.channelName;
2422
2747
  const key = inst.adapter.channelKey;
2423
- if (connectedChannels.has(name))
2748
+ if (connectedChannels.has(key))
2424
2749
  return;
2425
2750
  connectedChannels.add(name);
2426
2751
  connectedChannels.add(key);
@@ -2433,6 +2758,11 @@ async function main() {
2433
2758
  });
2434
2759
  // Run async operations in parallel
2435
2760
  const agent = agentRegistry.resolveByChannel(inst.adapter.channelKey) ?? agentRegistry.resolveByChannel(name);
2761
+ if (inst.channelType === 'aun' && agent) {
2762
+ const recoveries = queuePendingObserverHints(agent.aid, key);
2763
+ if (restoringStartupQueue)
2764
+ startupObserverRecoveries.push(...recoveries);
2765
+ }
2436
2766
  await Promise.all([
2437
2767
  bootstrapService.tryStartBootstrap({
2438
2768
  adapter: inst.adapter,
@@ -2580,6 +2910,7 @@ async function main() {
2580
2910
  logger.warn(`[startup] trigger scheduler start failed for ${agent.aid}: ${e}`);
2581
2911
  })),
2582
2912
  ]);
2913
+ sessionIdleMonitor.activate();
2583
2914
  // ── 控制 AID(daemon 进程身份):pureIdentity 接入 AUN,独立于 evolagent ──
2584
2915
  // 证书缺失检测/生成在 CLI 侧(ec start)完成。daemon 是后台进程无终端,
2585
2916
  // 这里只做兜底:证书缺失时 warn 并继续(AUNChannel 内部后台重连),绝不阻塞。
@@ -2639,11 +2970,11 @@ async function main() {
2639
2970
  parsed = null;
2640
2971
  }
2641
2972
  const menuResponseContext = () => ({
2642
- ...controlReplyContext(opts, evolMenuResponseTransportMetadata(opts.protectedHeaders?.sdk_version)),
2973
+ ...controlReplyContext(opts, evolMenuResponseTransportMetadata(opts.protectedHeaders?.sdk_version, opts.protectedHeaders?.menu_volatile_v1)),
2643
2974
  ...(opts.peerDeviceId ? { toDeviceId: opts.peerDeviceId } : {}),
2644
2975
  });
2645
2976
  const sendControlMenuResponse = async (response) => {
2646
- const timedResponse = withMenuProcessingTime(enforceMenuResponseSize(response, 128 * 1024 - 1024), menuTiming);
2977
+ const timedResponse = withMenuProcessingTime(enforceMenuResponseSize(response, MENU_RESPONSE_MAX_BYTES - MENU_RESPONSE_SAFETY_MARGIN_BYTES), menuTiming);
2647
2978
  const request = activeMenuRequest ?? {
2648
2979
  type: 'menu.unknown',
2649
2980
  id: response.id,
@@ -2854,6 +3185,19 @@ async function main() {
2854
3185
  }
2855
3186
  const recoverHandoffsAndStartRestored = async () => {
2856
3187
  try {
3188
+ // Recovery must not require the gateway to connect: pending interrupt
3189
+ // hints are local durable work, while ordinary restored input may run
3190
+ // even when its channel is offline. Scheduling twice is deduplicated by
3191
+ // the ResponseEngine's per-session hint key.
3192
+ for (const inst of channelInstances) {
3193
+ if (inst.channelType !== 'aun')
3194
+ continue;
3195
+ const agent = agentRegistry.resolveByChannel(inst.adapter.channelKey);
3196
+ if (agent && recoveryAids.includes(agent.aid)) {
3197
+ startupObserverRecoveries.push(...queuePendingObserverHints(agent.aid, inst.adapter.channelKey));
3198
+ }
3199
+ }
3200
+ await Promise.all(startupObserverRecoveries);
2857
3201
  const outcomes = await Promise.allSettled(recoveryAids.map(aid => handoffRuntime.recover([aid])));
2858
3202
  outcomes.forEach((outcome, index) => {
2859
3203
  if (outcome.status === 'rejected') {
@@ -2862,6 +3206,8 @@ async function main() {
2862
3206
  });
2863
3207
  }
2864
3208
  finally {
3209
+ restoringStartupQueue = false;
3210
+ startupObserverRecoveries.length = 0;
2865
3211
  messageQueue.startRestored();
2866
3212
  }
2867
3213
  };
@@ -3115,6 +3461,22 @@ async function main() {
3115
3461
  aidStatsCollector.recordOutbound(params.aid, params.toPeer, Buffer.byteLength(params.text || '', 'utf-8'), params.text, false, params.encrypt, params.chatmode, 'send');
3116
3462
  });
3117
3463
  ipcServer.setTaskRuntimeContextProvider(({ sessionId }) => responseEngine.getTaskRuntimeContext(sessionId));
3464
+ // `ec memory ... --debug <sessionId>`:仅开发仓安装从会话文件重建上下文,正式安装不注册
3465
+ if (isLinkedInstall()) {
3466
+ ipcServer.setDebugTaskRuntimeContextProvider(async ({ sessionId }) => {
3467
+ const session = await sessionManager.getSessionById(sessionId);
3468
+ if (!session)
3469
+ return null;
3470
+ const { buildDebugTaskContext } = await import('./core/memory/launch-consolidation.js');
3471
+ const { ensureSessionRuntimeDir } = await import('./cli/task-context.js');
3472
+ return buildDebugTaskContext({
3473
+ session,
3474
+ getTrigger: (agentAid, triggerId) => new TriggerDefinitionManager(agentAid).get(triggerId),
3475
+ sessionRuntimeDir: ensureSessionRuntimeDir(session.id),
3476
+ });
3477
+ });
3478
+ logger.info('[Memory] 开发仓安装:已启用 ec memory --debug <sessionId>');
3479
+ }
3118
3480
  ipcServer.setTaskRuntimeContextAuthorizer(({ sessionId, delegationToken, delegationCommandHash }) => {
3119
3481
  // Codex persistent threads carry a command-scoped authorization. Looking
3120
3482
  // up fresh routing context must not consume the subsequent CLI operation.
@@ -3579,6 +3941,119 @@ async function main() {
3579
3941
  },
3580
3942
  });
3581
3943
  ipcServer.setAgentReloadExecutor((aid, options) => reloadCoordinator.reload(aid, options));
3944
+ // ── 记忆整理定时检查(v4):候选检查在 daemon 侧,定时由 cron script trigger 驱动 ──
3945
+ //
3946
+ // 脚本(consolidation-trigger.ts 生成)调 `memory.check-consolidation`,
3947
+ // 本 executor 跑候选检查并把命中的候选经 enqueueConsolidation 入队。
3948
+ ipcServer.setConsolidationCheckExecutor(async ({ selfAid }) => {
3949
+ const { checkConsolidationCandidates, shouldUseMetaIndex } = await import('./core/memory/consolidation-trigger.js');
3950
+ const agentConfig = agentRegistry.get(selfAid)?.config;
3951
+ if (agentConfig?.memory?.enableRelation === false)
3952
+ return { launched: [] };
3953
+ const ownerAid = agentConfig?.owners?.[0];
3954
+ // 规模决定扫描路径:agent 数 × 本 agent 关系数超过阈值才走 _meta.json 快路径。
3955
+ const agentCount = agentRegistry.runnableAgents().length;
3956
+ const relationsDir = path.join(agentDir(selfAid), 'relations');
3957
+ let relationCount = 0;
3958
+ try {
3959
+ relationCount = fs.existsSync(relationsDir)
3960
+ ? fs.readdirSync(relationsDir, { withFileTypes: true }).filter(d => d.isDirectory()).length
3961
+ : 0;
3962
+ }
3963
+ catch { /* ignore */ }
3964
+ const useMetaIndex = shouldUseMetaIndex(agentCount, relationCount);
3965
+ const candidates = checkConsolidationCandidates({
3966
+ selfAid,
3967
+ ownerAid,
3968
+ config: agentConfig?.memory,
3969
+ useMetaIndex,
3970
+ });
3971
+ const launched = [];
3972
+ for (const candidate of candidates) {
3973
+ // 定时没有会话上下文,用 agent 级最小上下文(selfAid + peerKey)
3974
+ const taskCtx = {
3975
+ selfAid,
3976
+ peerKey: candidate.peerKey,
3977
+ baseagent: undefined,
3978
+ sessionId: undefined,
3979
+ };
3980
+ try {
3981
+ await enqueueConsolidation({
3982
+ taskCtx,
3983
+ mode: candidate.mode,
3984
+ triggeredBy: 'periodic-trigger',
3985
+ ownerAid,
3986
+ forgetAfterDays: agentConfig?.memory?.forgetAfterDays,
3987
+ ...(candidate.scope ? { scopeOverride: candidate.scope } : {}),
3988
+ });
3989
+ launched.push({ mode: candidate.mode, ...(candidate.peerKey ? { peerKey: candidate.peerKey } : {}) });
3990
+ }
3991
+ catch (e) {
3992
+ logger.warn(`[Memory] 定时检查入队失败(aid=${selfAid},mode=${candidate.mode}): ${e?.message}`);
3993
+ }
3994
+ }
3995
+ if (launched.length > 0) {
3996
+ logger.info(`[Memory] 定时检查拉起 ${launched.length} 个整理会话(aid=${selfAid}): ` +
3997
+ launched.map(l => `${l.mode}${l.peerKey ? `(${l.peerKey})` : ''}`).join(', '));
3998
+ }
3999
+ return { launched };
4000
+ });
4001
+ // ── T17:compact / session-new 节点自动触发 relation 整理 ────────────────
4002
+ // 走统一的 enqueueConsolidation(notes 模式),与手动 consolidate 同一条队列。
4003
+ const triggerRelationConsolidation = async (taskCtx, triggeredBy) => {
4004
+ try {
4005
+ if (!taskCtx?.selfAid || !taskCtx?.peerKey)
4006
+ return;
4007
+ if (taskCtx.sessionType === 'memory')
4008
+ return;
4009
+ const mc = agentRegistry?.get(taskCtx.selfAid)?.config?.memory;
4010
+ if (mc?.enableRelation === false)
4011
+ return;
4012
+ const ownerAid = agentRegistry?.get(taskCtx.selfAid)?.config?.owners?.[0];
4013
+ await enqueueConsolidation({
4014
+ taskCtx,
4015
+ mode: 'notes',
4016
+ triggeredBy,
4017
+ ownerAid,
4018
+ });
4019
+ }
4020
+ catch (e) {
4021
+ logger.warn(`[MemoryT17] relation 整理触发失败(${triggeredBy}): ${e?.message}`);
4022
+ }
4023
+ };
4024
+ eventBus.subscribe('runner:compact-start', (event) => {
4025
+ const { sessionId } = event;
4026
+ if (!sessionId)
4027
+ return;
4028
+ const taskCtx = responseEngine.getTaskRuntimeContext(sessionId);
4029
+ void triggerRelationConsolidation(taskCtx, 'compress');
4030
+ });
4031
+ eventBus.subscribe('session:created', (event) => {
4032
+ const ev = event;
4033
+ if (!ev.sourceSessionId)
4034
+ return;
4035
+ // 优先用事件快照构造 taskCtx(不依赖内存 map,renew/`/new` 后旧会话任务可能已结束);
4036
+ // 快照缺失时回退到内存 map(compact 等路径仍在活动任务内)。
4037
+ let snapshot;
4038
+ if (ev.sourceChannel && ev.sourceChannelId && ev.sourceSelfAid) {
4039
+ // peerKey:群聊用群 ID(channelId),私聊用对端 AID(peerId)。
4040
+ // 与 response-engine 的构造保持一致(见 formatPeerKey 用法)。
4041
+ const peerKeyId = ev.sourceChatType === 'group' ? ev.sourceChannelId : (ev.sourcePeerId ?? ev.sourceChannelId);
4042
+ const peerKey = peerKeyId ? `${ev.sourceChannel}#${encodeURIComponent(peerKeyId)}` : undefined;
4043
+ snapshot = {
4044
+ sessionId: ev.sourceSessionId,
4045
+ selfAid: ev.sourceSelfAid,
4046
+ peerKey,
4047
+ channel: ev.sourceChannel,
4048
+ channelId: ev.sourceChannelId,
4049
+ peerId: ev.sourcePeerId ?? ev.sourceChannelId,
4050
+ chatType: ev.sourceChatType,
4051
+ threadId: ev.sourceThreadId,
4052
+ };
4053
+ }
4054
+ const taskCtx = snapshot ?? responseEngine.getTaskRuntimeContext(ev.sourceSessionId);
4055
+ void triggerRelationConsolidation(taskCtx, 'session-new');
4056
+ });
3582
4057
  // Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
3583
4058
  globalThis.__evolcore_reloadHooks = reloadHooks;
3584
4059
  // Hot-load handler: dynamically add a new agent at runtime
@@ -4174,6 +4649,22 @@ async function main() {
4174
4649
  if (!argv.includes('--owner') && !argv.includes('--owners'))
4175
4650
  return fail('INITIALIZE_OWNER_MISMATCH', 'Explicit Owner required');
4176
4651
  const plan = resolveInitializationPlan(argv.slice(1));
4652
+ if (plan.avatarFile?.startsWith('attachment:')) {
4653
+ const { readRelationAttachment } = await import('./utils/media-cache.js');
4654
+ plan.avatarFile = readRelationAttachment(taskAid, delegation.grant.peerKey, plan.avatarFile.slice('attachment:'.length)).attachment.filePath;
4655
+ }
4656
+ else if (plan.avatarFile) {
4657
+ const session = await sessionManager.getSessionById(sessionId);
4658
+ const candidate = path.resolve(plan.avatarFile);
4659
+ const roots = [session?.projectPath, delegation.grant.sessionRuntimeDir]
4660
+ .filter((root) => !!root && path.isAbsolute(root))
4661
+ .map(root => fs.realpathSync(root));
4662
+ const real = fs.realpathSync(candidate);
4663
+ if (!roots.some(root => isSameOrDescendant(real, root)) || isHClassPath(real) || isLClassPath(real)) {
4664
+ return fail('AVATAR_INVALID', '--avatar-file must be a current project/TMPDIR file or authorized relation attachment');
4665
+ }
4666
+ plan.avatarFile = real;
4667
+ }
4177
4668
  if (!plan.owners.includes(actor))
4178
4669
  return fail('INITIALIZE_OWNER_MISMATCH', 'The authenticated requesting Owner must be retained');
4179
4670
  return { ok: true, result: agentInitializationService.start(plan, actor, parsed.dryRun) };
@@ -4211,6 +4702,8 @@ async function main() {
4211
4702
  description: value('--description'),
4212
4703
  type: value('--type'),
4213
4704
  tags,
4705
+ avatarFile: value('--avatar-file'),
4706
+ avatarUrl: value('--avatar-url'),
4214
4707
  force: argv.includes('--force'),
4215
4708
  });
4216
4709
  const validationError = validateAgentCreatePlan(plan);
@@ -4270,7 +4763,33 @@ async function main() {
4270
4763
  type: plan.type,
4271
4764
  tags: plan.tags,
4272
4765
  force: argv.includes('--force'),
4766
+ avatarFile: value('--avatar-file'),
4767
+ avatarUrl: value('--avatar-url'),
4273
4768
  };
4769
+ if (createOptions.avatarFile) {
4770
+ try {
4771
+ if (createOptions.avatarFile.startsWith('attachment:')) {
4772
+ const { readRelationAttachment } = await import('./utils/media-cache.js');
4773
+ const source = readRelationAttachment(taskAid, delegation.grant.peerKey, createOptions.avatarFile.slice('attachment:'.length));
4774
+ createOptions.avatarFile = source.attachment.filePath;
4775
+ }
4776
+ else {
4777
+ const session = await sessionManager.getSessionById(sessionId);
4778
+ const candidate = path.resolve(createOptions.avatarFile);
4779
+ const roots = [session?.projectPath, delegation.grant.sessionRuntimeDir]
4780
+ .filter((root) => !!root && path.isAbsolute(root))
4781
+ .map(root => fs.realpathSync(root));
4782
+ const real = fs.realpathSync(candidate);
4783
+ if (!roots.some(root => isSameOrDescendant(real, root)) || isHClassPath(real) || isLClassPath(real)) {
4784
+ return fail('AVATAR_INVALID', '--avatar-file must be a current project/TMPDIR file or authorized relation attachment');
4785
+ }
4786
+ createOptions.avatarFile = real;
4787
+ }
4788
+ }
4789
+ catch (error) {
4790
+ return fail('AVATAR_INVALID', error instanceof Error ? error.message : String(error));
4791
+ }
4792
+ }
4274
4793
  try {
4275
4794
  const result = agentApplicationService
4276
4795
  ? await agentApplicationService.create(createOptions)
@@ -4386,6 +4905,36 @@ async function main() {
4386
4905
  return { ok: false, code: 'INVALID_LOCAL_FILE', error: error instanceof Error ? error.message : String(error) };
4387
4906
  }
4388
4907
  });
4908
+ ipcServer.setConsolidationLaunchExecutor(async (params) => {
4909
+ const selfAid = params.taskCtx.selfAid;
4910
+ if (!selfAid)
4911
+ throw new Error('无法确定当前 agent AID(selfAid 为空)');
4912
+ // 手动触发也走统一队列:不忙时立即启动并拿回结果,忙时入队。
4913
+ const launched = await enqueueConsolidation(params);
4914
+ if (!launched) {
4915
+ // 已入队:返回一个占位结果,告知调用方稍后执行
4916
+ return { triggerId: '', contextFile: '', contextFilePath: '', logFile: '', batchDir: '', queued: true };
4917
+ }
4918
+ // 写 session-start 记录
4919
+ {
4920
+ const { appendOpsRecord } = await import('./core/memory/consolidation-ops-records.js');
4921
+ appendOpsRecord(launched.batchDir, {
4922
+ ts: new Date().toISOString(),
4923
+ op: 'session-start',
4924
+ sessionId: launched.triggerId,
4925
+ mode: params.mode,
4926
+ triggeredBy: params.triggeredBy,
4927
+ });
4928
+ }
4929
+ // 手动触发:等 runner 分配 agentSessionId 后再返回(超时返回已拿到的部分)。
4930
+ // 会话刚创建时只有 executionSessionId,agentSessionId 要等 runner 初始化后才有。
4931
+ const { waitForConsolidationSession } = await import('./core/memory/launch-consolidation.js');
4932
+ const ids = await waitForConsolidationSession({
4933
+ executionSessionId: launched.executionSessionId,
4934
+ readAgentSessionId: async (id) => (await sessionManager.getSessionById(id))?.agentSessionId,
4935
+ });
4936
+ return { ...launched, ...ids };
4937
+ });
4389
4938
  ipcServer.setTriggerExecutor(async (cmd) => {
4390
4939
  const schedulerFor = (agentAid) => {
4391
4940
  const scheduler = triggerSchedulers.get(agentAid);
@@ -4655,8 +5204,6 @@ async function main() {
4655
5204
  throw new Error(`trigger not found: ${cmd.triggerId}`);
4656
5205
  if (cmd.enabled && existing.execution.permissionMode === 'fullaccess') {
4657
5206
  rejectUnapprovedFullAccessConfiguration(existing, actor);
4658
- if (!isFullAccessEnabled())
4659
- throw new Error('fullaccess is disabled by daemon.json');
4660
5207
  if (!actor.daemonOwner && !actor.fullAccess) {
4661
5208
  throw new Error('enabling a fullaccess Trigger requires a DaemonOwner or fullaccess-run');
4662
5209
  }
@@ -4791,6 +5338,10 @@ async function main() {
4791
5338
  // 断开插件系统的渠道
4792
5339
  removeGroupInviteConfigListener();
4793
5340
  await shutdownStep('stop group invitation review', () => groupInviteReviewService.dispose());
5341
+ await shutdownStep('stop global skill watcher', async () => {
5342
+ const { stopGlobalSkillWatcher } = await import('./core/skill/global-watcher.js');
5343
+ await stopGlobalSkillWatcher();
5344
+ });
4794
5345
  await shutdownStep('disconnect channels', () => channelLoader.disconnectAll(channelInstances));
4795
5346
  for (const inst of channelInstances) {
4796
5347
  const type = inst.channelType || inst.adapter.channelName;