evolcore 0.0.28 → 0.0.29

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 (183) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +19 -9
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +6 -2
  7. package/dist/agents/codex/diagnostics.js +4 -3
  8. package/dist/agents/codex-runner.js +396 -95
  9. package/dist/agents/gemini-runner.js +35 -17
  10. package/dist/aun/group/history.js +130 -90
  11. package/dist/aun/group/operations.js +1 -3
  12. package/dist/aun/menu/transport.js +5 -3
  13. package/dist/aun/msg/managed-operation.js +70 -0
  14. package/dist/aun/msg/p2p.js +1 -3
  15. package/dist/aun/msg/upload.js +2 -2
  16. package/dist/aun/outbox.js +1 -37
  17. package/dist/channels/aun.js +249 -89
  18. package/dist/channels/daemon.js +18 -1
  19. package/dist/channels/feishu.js +1 -2
  20. package/dist/cli/agent-command.js +23 -3
  21. package/dist/cli/agent-create.js +68 -2
  22. package/dist/cli/agent.js +12 -2
  23. package/dist/cli/aun-commands.js +87 -13
  24. package/dist/cli/cli-argv.js +26 -0
  25. package/dist/cli/daemon-commands.js +40 -10
  26. package/dist/cli/data-command.js +4 -4
  27. package/dist/cli/handoff-command.js +2 -0
  28. package/dist/cli/index.js +18 -1
  29. package/dist/cli/managed-command-guard.js +10 -0
  30. package/dist/cli/memory-command.js +441 -0
  31. package/dist/cli/restart-monitor.js +62 -6
  32. package/dist/cli/skill-command.js +41 -0
  33. package/dist/cli/task-context.js +2 -23
  34. package/dist/cli/trigger-command.js +9 -1
  35. package/dist/cli/watch-msg-diag.js +209 -0
  36. package/dist/config/config-field-policy.js +52 -3
  37. package/dist/config/config-manager.js +17 -4
  38. package/dist/config/contact-book-store.js +2 -2
  39. package/dist/config/mention-mode.js +7 -2
  40. package/dist/config/merge.js +28 -9
  41. package/dist/config/resolved-config-op.js +1 -1
  42. package/dist/config/runtime-env.js +137 -0
  43. package/dist/config/schema-registry.js +11 -1
  44. package/dist/config-store.js +10 -10
  45. package/dist/core/audit/event-key.js +3 -1
  46. package/dist/core/audit/log-integrity.js +4 -0
  47. package/dist/core/auth/agent-delegation.js +2 -1
  48. package/dist/core/auth/auth-gateway.js +13 -6
  49. package/dist/core/auth/authorization-audit.js +54 -19
  50. package/dist/core/auth/operation-authorizer.js +17 -0
  51. package/dist/core/auth/operation-catalog.js +9 -0
  52. package/dist/core/causation/context.js +5 -1
  53. package/dist/core/command/agent-control.js +1 -2
  54. package/dist/core/command/command-handler.js +51 -23
  55. package/dist/core/command/memory-slash.js +270 -0
  56. package/dist/core/command/menu-catalog.js +1 -0
  57. package/dist/core/command/menu-handler.js +315 -60
  58. package/dist/core/command/menu-protocol.js +42 -4
  59. package/dist/core/command/slash-gate.js +10 -10
  60. package/dist/core/command/slash-handler.js +206 -64
  61. package/dist/core/data-migration.js +282 -505
  62. package/dist/core/event-catalog.js +26 -0
  63. package/dist/core/evolagent-registry.js +6 -0
  64. package/dist/core/evolagent.js +0 -10
  65. package/dist/core/handoff/store.js +1 -20
  66. package/dist/core/inference/text-inference.js +57 -1
  67. package/dist/core/memory/consolidation-context.js +505 -0
  68. package/dist/core/memory/consolidation-ops-records.js +124 -0
  69. package/dist/core/memory/consolidation-trigger.js +394 -0
  70. package/dist/core/memory/index-manager.js +308 -0
  71. package/dist/core/memory/launch-consolidation.js +395 -0
  72. package/dist/core/memory/memory-admin.js +234 -0
  73. package/dist/core/memory/memory-document.js +100 -0
  74. package/dist/core/memory/memory-session-policy.js +41 -0
  75. package/dist/core/memory/memory-signature.js +257 -0
  76. package/dist/core/memory/memory-writer.js +432 -0
  77. package/dist/core/memory/staleness.js +117 -0
  78. package/dist/core/message/auto-mention-gate.js +454 -0
  79. package/dist/core/message/im-renderer.js +69 -16
  80. package/dist/core/message/message-bridge.js +287 -21
  81. package/dist/core/message/message-log.js +1 -0
  82. package/dist/core/message/message-queue.js +114 -27
  83. package/dist/core/message/pending-hints.js +329 -161
  84. package/dist/core/message/response-engine.js +567 -80
  85. package/dist/core/permission/ec-command-parser.js +1029 -80
  86. package/dist/core/permission/protected-paths.js +376 -126
  87. package/dist/core/permission/sandbox-runtime.js +40 -12
  88. package/dist/core/permission/tool-error-code.js +96 -13
  89. package/dist/core/permission/tool-policy.js +276 -23
  90. package/dist/core/role/runtime-policy.js +3 -7
  91. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  92. package/dist/core/session/session-fs-store.js +0 -76
  93. package/dist/core/session/session-idle-monitor.js +355 -0
  94. package/dist/core/session/session-manager.js +478 -17
  95. package/dist/core/session/session-mapper.js +33 -16
  96. package/dist/core/session/session-renew.js +217 -155
  97. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  98. package/dist/core/skill/global-watcher.js +282 -0
  99. package/dist/core/skill/index-generator.js +84 -0
  100. package/dist/core/skill/startup.js +73 -0
  101. package/dist/core/skill/switch-resolver.js +115 -0
  102. package/dist/core/task/task-activity-store.js +219 -0
  103. package/dist/eck/kit-renderer.js +30 -3
  104. package/dist/eck/manifest-engine.js +26 -4
  105. package/dist/index.js +708 -18
  106. package/dist/ipc.js +564 -2
  107. package/dist/paths.js +30 -9
  108. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  109. package/dist/response-system/modes/single-session/index.js +3 -0
  110. package/dist/trigger/feedback.js +1 -0
  111. package/dist/trigger/manager.js +0 -10
  112. package/dist/trigger/scheduler.js +11 -0
  113. package/dist/trigger/validation.js +22 -0
  114. package/dist/utils/avatar-upload.js +59 -18
  115. package/dist/utils/error-dict.json +6 -3
  116. package/dist/utils/error-utils.js +27 -0
  117. package/dist/utils/logger.js +32 -10
  118. package/dist/utils/media-cache.js +84 -0
  119. package/dist/utils/msg-diag.js +97 -0
  120. package/dist/utils/welcome.js +37 -8
  121. package/kits/docs/AUTHORING.md +314 -0
  122. package/kits/docs/GUIDE.md +2 -0
  123. package/kits/docs/INDEX.md +2 -0
  124. package/kits/docs/aun/CHEATSHEET.md +4 -0
  125. package/kits/docs/aun/SDK.md +60 -0
  126. package/kits/docs/context-assembly.md +10 -6
  127. package/kits/docs/evolcore/INDEX.md +17 -3
  128. package/kits/docs/evolcore/agent-create.md +24 -0
  129. package/kits/docs/evolcore/agent.md +5 -2
  130. package/kits/docs/evolcore/config.md +1 -1
  131. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  132. package/kits/docs/evolcore/group.md +34 -18
  133. package/kits/docs/evolcore/memory-architecture.md +78 -0
  134. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  135. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  136. package/kits/docs/evolcore/memory-tags.md +63 -0
  137. package/kits/docs/evolcore/memory.md +110 -0
  138. package/kits/docs/evolcore/menu-client.md +27 -11
  139. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  140. package/kits/docs/evolcore/menu-protocol.md +213 -0
  141. package/kits/docs/evolcore/msg.md +2 -0
  142. package/kits/docs/evolcore/trigger.md +7 -5
  143. package/kits/docs/path-registry.md +6 -8
  144. package/kits/docs/prompt-loading-architecture.md +5 -3
  145. package/kits/docs/venues/group.md +5 -4
  146. package/kits/eck_manifest.json +294 -88
  147. package/kits/eck_manifest.memory.json +257 -0
  148. package/kits/rules/03-identity.md +8 -8
  149. package/kits/rules/05-venue.md +4 -2
  150. package/kits/rules/06-channel.md +1 -1
  151. package/kits/schemas/_meta.json +276 -47
  152. package/kits/schemas/agent-config.schema.12.json +38 -2
  153. package/kits/schemas/agent-config.schema.13.json +591 -0
  154. package/kits/schemas/daemon.schema.6.json +9 -9
  155. package/kits/schemas/defaults.schema.5.json +29 -0
  156. package/kits/schemas/relation-config.schema.8.json +14 -1
  157. package/kits/schemas/relation-config.schema.9.json +129 -0
  158. package/kits/schemas/role-config.schema.1.json +3 -3
  159. package/kits/skills/.switches.json +3 -0
  160. package/kits/skills/ec-skill/SKILL.md +217 -0
  161. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  162. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  163. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  164. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  165. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  166. package/kits/templates/decision/auto-mention.md +25 -0
  167. package/kits/templates/decision/session-renew.md +22 -0
  168. package/kits/templates/memory-consolidation/forget.md +104 -0
  169. package/kits/templates/memory-consolidation/generalize.md +76 -0
  170. package/kits/templates/memory-consolidation/notes.md +133 -0
  171. package/kits/templates/memory-consolidation/profile.md +58 -0
  172. package/kits/templates/memory-consolidation/promote.md +72 -0
  173. package/kits/templates/memory-consolidation/workflow.md +75 -0
  174. package/kits/templates/system-fragments/bootstrap.md +14 -5
  175. package/kits/templates/system-fragments/commands.md +1 -0
  176. package/kits/templates/system-fragments/group-history.md +6 -0
  177. package/kits/templates/system-fragments/identity.md +0 -3
  178. package/kits/templates/system-fragments/memory.md +79 -0
  179. package/kits/templates/system-fragments/reply.md +1 -0
  180. package/kits/templates/system-fragments/venue.md +1 -1
  181. package/package.json +4 -6
  182. package/dist/trigger/legacy-session-history.js +0 -19
  183. 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,6 +114,7 @@ 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
120
  import { sanitizeMenuLogValue } from './core/command/menu-protocol.js';
@@ -318,6 +332,44 @@ function originFromActorSession(session, authenticatedPeerId, authenticatedChann
318
332
  delivery: { chatType: 'private' },
319
333
  };
320
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
+ }
321
373
  function seedUpgradeCheckTrigger(manager, owner) {
322
374
  const now = Date.now();
323
375
  const scriptName = 'upgrade-check.cjs';
@@ -442,6 +494,52 @@ function seedTaskRecoveryTrigger(manager, owner) {
442
494
  atomicWriteJson(manager.definitionPath(definition.id), definition);
443
495
  fs.rmSync(manager.activePath(definition.id), { force: true });
444
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
+ }
445
543
  function removeLegacyAgentUpgradeCheck(manager) {
446
544
  let existing;
447
545
  try {
@@ -874,7 +972,16 @@ async function main() {
874
972
  if (relationSharedMigration.copied > 0) {
875
973
  logger.info(`[migrate] Synced ${relationSharedMigration.copied} relation shared file(s) across ${relationSharedMigration.relations} relation(s)`);
876
974
  }
975
+ migrateLegacyConsolidationBatches();
877
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
+ }
878
985
  // ── 配置体系初始化(schema 字段不相交硬约束校验)──
879
986
  try {
880
987
  initConfigManager();
@@ -1139,20 +1246,185 @@ async function main() {
1139
1246
  logger.info(`[PermissionGateway] Cancelled ${cancelled} stale management approval(s) after ${target} config write: agent=${selector.self}`);
1140
1247
  }
1141
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
+ };
1142
1411
  eventBus.subscribe('task:interrupted', (event) => {
1143
1412
  if ('sessionId' in event && event.sessionId) {
1144
1413
  permissionGateway.cancelAll(event.sessionId, event.reason || 'interrupted');
1145
1414
  }
1415
+ void finishConsolidationSession(event.sessionId, false);
1146
1416
  });
1147
1417
  eventBus.subscribe('task:error', (event) => {
1148
1418
  if ('sessionId' in event && event.sessionId) {
1149
1419
  permissionGateway.cancelAll(event.sessionId, 'task_error', false);
1150
1420
  }
1421
+ void finishConsolidationSession(event.sessionId, false);
1151
1422
  });
1152
1423
  eventBus.subscribe('task:completed', (event) => {
1153
1424
  if ('sessionId' in event && event.sessionId) {
1154
1425
  permissionGateway.cancelAll(event.sessionId, 'task_completed', false);
1155
1426
  }
1427
+ void finishConsolidationSession(event.sessionId, true);
1156
1428
  });
1157
1429
  // 交互路由器
1158
1430
  const interactionRouter = new InteractionRouter();
@@ -1270,6 +1542,16 @@ async function main() {
1270
1542
  }, primaryRunnerKey);
1271
1543
  responseEngine.setAgentDelegationRegistry(agentDelegationRegistry);
1272
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
+ });
1273
1555
  // 回填 processor 和 messageQueue 的引用
1274
1556
  cmdHandler.setProcessor(processor);
1275
1557
  // Inject EvolAgentRegistry (methods added by T6/T7)
@@ -1295,13 +1577,13 @@ async function main() {
1295
1577
  dedupPath: daemonMessageDedupPath(),
1296
1578
  });
1297
1579
  // 设置中断回调(精确中断正在处理的 agent)
1298
- messageQueue.setInterruptCallback(async (sessionKey, baseagent, evolagentName, reason = 'new_message', timeout) => {
1580
+ messageQueue.setInterruptCallback(async (sessionKey, baseagent, evolagentName, reason = 'new_message', timeout, strict) => {
1299
1581
  void baseagent;
1300
1582
  void evolagentName;
1301
1583
  if (timeout)
1302
1584
  await responseEngine.timeoutSession(sessionKey, timeout);
1303
1585
  else
1304
- await responseEngine.interruptSession(sessionKey, reason);
1586
+ await responseEngine.interruptSession(sessionKey, reason, strict);
1305
1587
  });
1306
1588
  messageQueue.setEventBus(eventBus);
1307
1589
  // Restore persisted input before Trigger schedulers start. Trigger-owned work
@@ -1369,6 +1651,37 @@ async function main() {
1369
1651
  cmdHandler.setHandoffRuntime(handoffRuntime);
1370
1652
  // Trigger runtime: daemon-level script + feedback scheduler.
1371
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());
1372
1685
  const targetSessionLocks = new TargetSessionLockRegistry();
1373
1686
  const triggerAudit = new TriggerAuditLogger();
1374
1687
  const triggerScriptExecutor = new TriggerScriptExecutor();
@@ -1376,6 +1689,81 @@ async function main() {
1376
1689
  watchdogMs: daemonConversationWatchdogMs(globalSettings),
1377
1690
  totalExecutionMs: taskExecutionTtlMs,
1378
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);
1379
1767
  const triggerControlToken = crypto.randomBytes(32).toString('base64url');
1380
1768
  fs.writeFileSync(resolvePaths().controlToken, triggerControlToken, { mode: 0o600 });
1381
1769
  try {
@@ -1456,6 +1844,43 @@ async function main() {
1456
1844
  baseagent: agent.baseagent,
1457
1845
  };
1458
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;
1459
1884
  const validateTriggerFeedbackChannels = (definition) => {
1460
1885
  if (definition.feedback.strategy === 'silent' || definition.feedback.strategy === 'event_session')
1461
1886
  return;
@@ -1496,8 +1921,6 @@ async function main() {
1496
1921
  const hasDaemonTriggerAuthority = (subject) => (subject.isDaemonOwner || subject.processRole === 'daemon-service' || subject.dataScope === 'daemon');
1497
1922
  const validateTriggerDefinitionForActor = (definition, actor) => {
1498
1923
  if (definition.execution.permissionMode === 'fullaccess') {
1499
- if (!isFullAccessEnabled())
1500
- throw new Error('fullaccess is disabled by daemon.json');
1501
1924
  if (!actor.daemonOwner && !actor.fullAccess) {
1502
1925
  throw new Error('persistent fullaccess Trigger requires DaemonOwner or fullaccess-run');
1503
1926
  }
@@ -1556,9 +1979,6 @@ async function main() {
1556
1979
  }
1557
1980
  };
1558
1981
  const authorizeTriggerExecution = (definition) => {
1559
- if (definition.execution.permissionMode === 'fullaccess' && !isFullAccessEnabled()) {
1560
- return { allowed: false, reason: 'fullaccess is disabled by daemon.json' };
1561
- }
1562
1982
  if (definition.execution.permissionMode === 'fullaccess') {
1563
1983
  if (!definition.authorizedBy) {
1564
1984
  return { allowed: false, reason: 'fullaccess Trigger approval provenance is missing' };
@@ -1575,7 +1995,7 @@ async function main() {
1575
1995
  // task. Its origin remains useful for audit and feedback routing, but a
1576
1996
  // relation lookup on the origin actor would make a cross-Agent Trigger
1577
1997
  // persist successfully and then fail on every fire after creation.
1578
- // Approval provenance and the feature flag above remain mandatory; only
1998
+ // Approval provenance remains mandatory; only
1579
1999
  // the ordinary creator relation gate is bypassed for this mode.
1580
2000
  if (definition.execution.permissionMode === 'fullaccess') {
1581
2001
  return { allowed: true };
@@ -1763,10 +2183,22 @@ async function main() {
1763
2183
  if (opts.seedUpgradeCheck) {
1764
2184
  seedUpgradeCheckTrigger(manager, owner);
1765
2185
  seedTaskRecoveryTrigger(manager, owner);
2186
+ seedSessionIdleCheckTrigger(manager, owner);
1766
2187
  }
1767
2188
  else {
1768
2189
  removeLegacyAgentUpgradeCheck(manager);
1769
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
+ }
1770
2202
  const state = new TriggerRunStateStore(manager);
1771
2203
  const agentTriggerAudit = triggerAudit.withHistory(manager.history);
1772
2204
  const dispatcher = new TriggerFeedbackDispatcher({
@@ -2048,6 +2480,12 @@ async function main() {
2048
2480
  };
2049
2481
  });
2050
2482
  }
2483
+ if (inst.channelType === 'aun') {
2484
+ inst.channel.setObserverHintRuntime({
2485
+ hasSession: target => responseEngine.hasObserverTargetSession(target),
2486
+ schedule: target => responseEngine.scheduleObserverHint(target),
2487
+ });
2488
+ }
2051
2489
  // 5. 撤回消息 → 中断执行中任务
2052
2490
  inst.channel.onRecall?.((messageId, selfAID) => {
2053
2491
  msgBridge.cancel(messageId, selfAID);
@@ -2068,6 +2506,19 @@ async function main() {
2068
2506
  agent.status = 'running';
2069
2507
  }
2070
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
+ }
2071
2522
  if (agentRegistry.list().some((info) => info.status === 'running')) {
2072
2523
  agentRuntimeState = 'running';
2073
2524
  agentRuntimeError = undefined;
@@ -2278,10 +2729,23 @@ async function main() {
2278
2729
  })
2279
2730
  .join(', ');
2280
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}`)));
2281
2745
  const markChannelConnected = async (inst) => {
2282
2746
  const name = inst.adapter.channelName;
2283
2747
  const key = inst.adapter.channelKey;
2284
- if (connectedChannels.has(name))
2748
+ if (connectedChannels.has(key))
2285
2749
  return;
2286
2750
  connectedChannels.add(name);
2287
2751
  connectedChannels.add(key);
@@ -2294,6 +2758,11 @@ async function main() {
2294
2758
  });
2295
2759
  // Run async operations in parallel
2296
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
+ }
2297
2766
  await Promise.all([
2298
2767
  bootstrapService.tryStartBootstrap({
2299
2768
  adapter: inst.adapter,
@@ -2441,6 +2910,7 @@ async function main() {
2441
2910
  logger.warn(`[startup] trigger scheduler start failed for ${agent.aid}: ${e}`);
2442
2911
  })),
2443
2912
  ]);
2913
+ sessionIdleMonitor.activate();
2444
2914
  // ── 控制 AID(daemon 进程身份):pureIdentity 接入 AUN,独立于 evolagent ──
2445
2915
  // 证书缺失检测/生成在 CLI 侧(ec start)完成。daemon 是后台进程无终端,
2446
2916
  // 这里只做兜底:证书缺失时 warn 并继续(AUNChannel 内部后台重连),绝不阻塞。
@@ -2500,11 +2970,11 @@ async function main() {
2500
2970
  parsed = null;
2501
2971
  }
2502
2972
  const menuResponseContext = () => ({
2503
- ...controlReplyContext(opts, evolMenuResponseTransportMetadata(opts.protectedHeaders?.sdk_version)),
2973
+ ...controlReplyContext(opts, evolMenuResponseTransportMetadata(opts.protectedHeaders?.sdk_version, opts.protectedHeaders?.menu_volatile_v1)),
2504
2974
  ...(opts.peerDeviceId ? { toDeviceId: opts.peerDeviceId } : {}),
2505
2975
  });
2506
2976
  const sendControlMenuResponse = async (response) => {
2507
- 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);
2508
2978
  const request = activeMenuRequest ?? {
2509
2979
  type: 'menu.unknown',
2510
2980
  id: response.id,
@@ -2715,6 +3185,19 @@ async function main() {
2715
3185
  }
2716
3186
  const recoverHandoffsAndStartRestored = async () => {
2717
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);
2718
3201
  const outcomes = await Promise.allSettled(recoveryAids.map(aid => handoffRuntime.recover([aid])));
2719
3202
  outcomes.forEach((outcome, index) => {
2720
3203
  if (outcome.status === 'rejected') {
@@ -2723,6 +3206,8 @@ async function main() {
2723
3206
  });
2724
3207
  }
2725
3208
  finally {
3209
+ restoringStartupQueue = false;
3210
+ startupObserverRecoveries.length = 0;
2726
3211
  messageQueue.startRestored();
2727
3212
  }
2728
3213
  };
@@ -2976,6 +3461,22 @@ async function main() {
2976
3461
  aidStatsCollector.recordOutbound(params.aid, params.toPeer, Buffer.byteLength(params.text || '', 'utf-8'), params.text, false, params.encrypt, params.chatmode, 'send');
2977
3462
  });
2978
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
+ }
2979
3480
  ipcServer.setTaskRuntimeContextAuthorizer(({ sessionId, delegationToken, delegationCommandHash }) => {
2980
3481
  // Codex persistent threads carry a command-scoped authorization. Looking
2981
3482
  // up fresh routing context must not consume the subsequent CLI operation.
@@ -3440,6 +3941,119 @@ async function main() {
3440
3941
  },
3441
3942
  });
3442
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
+ });
3443
4057
  // Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
3444
4058
  globalThis.__evolcore_reloadHooks = reloadHooks;
3445
4059
  // Hot-load handler: dynamically add a new agent at runtime
@@ -4035,6 +4649,22 @@ async function main() {
4035
4649
  if (!argv.includes('--owner') && !argv.includes('--owners'))
4036
4650
  return fail('INITIALIZE_OWNER_MISMATCH', 'Explicit Owner required');
4037
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
+ }
4038
4668
  if (!plan.owners.includes(actor))
4039
4669
  return fail('INITIALIZE_OWNER_MISMATCH', 'The authenticated requesting Owner must be retained');
4040
4670
  return { ok: true, result: agentInitializationService.start(plan, actor, parsed.dryRun) };
@@ -4072,6 +4702,8 @@ async function main() {
4072
4702
  description: value('--description'),
4073
4703
  type: value('--type'),
4074
4704
  tags,
4705
+ avatarFile: value('--avatar-file'),
4706
+ avatarUrl: value('--avatar-url'),
4075
4707
  force: argv.includes('--force'),
4076
4708
  });
4077
4709
  const validationError = validateAgentCreatePlan(plan);
@@ -4131,7 +4763,33 @@ async function main() {
4131
4763
  type: plan.type,
4132
4764
  tags: plan.tags,
4133
4765
  force: argv.includes('--force'),
4766
+ avatarFile: value('--avatar-file'),
4767
+ avatarUrl: value('--avatar-url'),
4134
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
+ }
4135
4793
  try {
4136
4794
  const result = agentApplicationService
4137
4795
  ? await agentApplicationService.create(createOptions)
@@ -4247,6 +4905,36 @@ async function main() {
4247
4905
  return { ok: false, code: 'INVALID_LOCAL_FILE', error: error instanceof Error ? error.message : String(error) };
4248
4906
  }
4249
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
+ });
4250
4938
  ipcServer.setTriggerExecutor(async (cmd) => {
4251
4939
  const schedulerFor = (agentAid) => {
4252
4940
  const scheduler = triggerSchedulers.get(agentAid);
@@ -4516,8 +5204,6 @@ async function main() {
4516
5204
  throw new Error(`trigger not found: ${cmd.triggerId}`);
4517
5205
  if (cmd.enabled && existing.execution.permissionMode === 'fullaccess') {
4518
5206
  rejectUnapprovedFullAccessConfiguration(existing, actor);
4519
- if (!isFullAccessEnabled())
4520
- throw new Error('fullaccess is disabled by daemon.json');
4521
5207
  if (!actor.daemonOwner && !actor.fullAccess) {
4522
5208
  throw new Error('enabling a fullaccess Trigger requires a DaemonOwner or fullaccess-run');
4523
5209
  }
@@ -4652,6 +5338,10 @@ async function main() {
4652
5338
  // 断开插件系统的渠道
4653
5339
  removeGroupInviteConfigListener();
4654
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
+ });
4655
5345
  await shutdownStep('disconnect channels', () => channelLoader.disconnectAll(channelInstances));
4656
5346
  for (const inst of channelInstances) {
4657
5347
  const type = inst.channelType || inst.adapter.channelName;