evolcore 0.0.31 → 0.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/bin/install-codex-managed-hooks.mjs +124 -37
  3. package/dist/agents/claude-runner.js +29 -2
  4. package/dist/agents/codex/app-server-client.js +3 -0
  5. package/dist/agents/codex/diagnostics.js +76 -44
  6. package/dist/agents/codex-runner.js +51 -24
  7. package/dist/agents/ecagent-runner.js +9 -13
  8. package/dist/agents/gemini-runner.js +4 -0
  9. package/dist/aun/aid/store.js +0 -81
  10. package/dist/aun/group/fs-download.js +1 -10
  11. package/dist/aun/group/operations.js +0 -1
  12. package/dist/aun/msg/history.js +1 -0
  13. package/dist/aun/msg/managed-operation.js +130 -32
  14. package/dist/aun/msg/p2p.js +0 -1
  15. package/dist/aun/outbox.js +50 -1
  16. package/dist/aun/service-proxy.js +12 -18
  17. package/dist/aun/storage/index.js +29 -0
  18. package/dist/channels/aun.js +38 -137
  19. package/dist/channels/daemon.js +257 -2
  20. package/dist/cli/agent-command.js +64 -31
  21. package/dist/cli/agent-create.js +43 -24
  22. package/dist/cli/agent.js +16 -70
  23. package/dist/cli/aun-commands.js +512 -32
  24. package/dist/cli/contact.js +8 -2
  25. package/dist/cli/ctl-command.js +1 -2
  26. package/dist/cli/daemon-commands.js +82 -13
  27. package/dist/cli/debug-mode.js +154 -0
  28. package/dist/cli/diag.js +2 -2
  29. package/dist/cli/handoff-command.js +18 -5
  30. package/dist/cli/index.js +81 -64
  31. package/dist/cli/init.js +2 -2
  32. package/dist/cli/managed-command-guard.js +5 -0
  33. package/dist/cli/memory-command.js +93 -12
  34. package/dist/cli/model.js +14 -138
  35. package/dist/cli/queue-command.js +51 -0
  36. package/dist/cli/response.js +5 -54
  37. package/dist/cli/session-command.js +194 -0
  38. package/dist/cli/skill-command.js +85 -3
  39. package/dist/cli/stats.js +10 -4
  40. package/dist/cli/task-command.js +1 -0
  41. package/dist/cli/task-context.js +16 -0
  42. package/dist/cli/thread-command.js +620 -0
  43. package/dist/cli/trigger-command.js +76 -50
  44. package/dist/config/builtin-roles.js +9 -7
  45. package/dist/config/config-field-policy.js +2 -3
  46. package/dist/config/config-manager.js +7 -2
  47. package/dist/config/resolved-config-op.js +3 -0
  48. package/dist/config-store.js +15 -0
  49. package/dist/core/agent-ipc-registration.js +65 -22
  50. package/dist/core/auth/agent-delegation.js +42 -2
  51. package/dist/core/auth/debug-context.js +139 -0
  52. package/dist/core/auth/delegation-authorization.js +1 -0
  53. package/dist/core/auth/operation-authorizer.js +24 -1
  54. package/dist/core/auth/operation-catalog.js +44 -3
  55. package/dist/core/background-session/session-cleanup.js +212 -0
  56. package/dist/core/background-session/types.js +10 -0
  57. package/dist/core/bootstrap-messages.js +1 -1
  58. package/dist/core/bootstrap-service.js +6 -3
  59. package/dist/core/command/command-handler.js +224 -45
  60. package/dist/core/command/diag-service.js +5 -1
  61. package/dist/core/command/memory-slash.js +6 -2
  62. package/dist/core/command/menu-catalog.js +48 -1
  63. package/dist/core/command/menu-handler.js +92 -16
  64. package/dist/core/command/menu-protocol.js +24 -1
  65. package/dist/core/command/skill-slash.js +322 -0
  66. package/dist/core/command/slash-gate.js +6 -4
  67. package/dist/core/command/slash-handler.js +193 -13
  68. package/dist/core/command/topic-slash.js +528 -0
  69. package/dist/core/evolagent-registry.js +20 -5
  70. package/dist/core/handoff/ipc-registration.js +38 -0
  71. package/dist/core/handoff/runtime.js +5 -11
  72. package/dist/core/handoff/store.js +5 -0
  73. package/dist/core/ipc-registration.js +210 -9
  74. package/dist/core/memory/consolidation-context.js +49 -16
  75. package/dist/core/memory/consolidation-ops-records.js +35 -0
  76. package/dist/core/memory/consolidation-queue-format.js +21 -0
  77. package/dist/core/memory/consolidation.js +174 -180
  78. package/dist/core/message/aun-ipc-sender.js +207 -8
  79. package/dist/core/message/im-renderer.js +149 -6
  80. package/dist/core/message/message-bridge.js +13 -3
  81. package/dist/core/message/message-log.js +1 -0
  82. package/dist/core/message/message-queue.js +131 -3
  83. package/dist/core/message/peer-mode.js +6 -1
  84. package/dist/core/message/response-engine.js +379 -69
  85. package/dist/core/message/subtask-tool-projection.js +140 -0
  86. package/dist/core/model/catalog-service.js +17 -12
  87. package/dist/core/permission/ec-command-parser.js +126 -7
  88. package/dist/core/permission/sandbox-runtime.js +14 -1
  89. package/dist/core/session/session-idle-monitor.js +249 -22
  90. package/dist/core/session/session-manager.js +54 -9
  91. package/dist/core/skill/command-parser.js +13 -0
  92. package/dist/core/skill/index-generator.js +4 -1
  93. package/dist/core/skill/startup.js +41 -0
  94. package/dist/core/skill/switch-resolver.js +55 -29
  95. package/dist/core/task/task-recovery-service.js +2 -0
  96. package/dist/core/task/task-resume-operation.js +11 -0
  97. package/dist/core/thread/command-parser.js +125 -0
  98. package/dist/core/thread/index.js +1 -0
  99. package/dist/core/thread/ipc-operation.js +95 -0
  100. package/dist/core/thread/thread-context-storage.js +131 -0
  101. package/dist/core/thread/thread-operation.js +258 -0
  102. package/dist/core/thread/thread-route.js +33 -0
  103. package/dist/core/thread/thread-service.js +777 -0
  104. package/dist/core/thread/types.js +1 -0
  105. package/dist/eck/message-renderer.js +35 -0
  106. package/dist/index.js +10 -7
  107. package/dist/ipc.js +234 -17
  108. package/dist/paths.js +23 -0
  109. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  110. package/dist/trigger/authorization.js +59 -29
  111. package/dist/trigger/feedback.js +7 -1
  112. package/dist/trigger/ipc-service.js +49 -35
  113. package/dist/trigger/parser.js +63 -1
  114. package/dist/trigger/script-sandbox.js +58 -77
  115. package/dist/trigger/validation.js +14 -0
  116. package/dist/utils/error-utils.js +22 -1
  117. package/kits/docs/AUTHORING.md +1 -1
  118. package/kits/docs/channels/aun.md +8 -0
  119. package/kits/docs/context-assembly.md +1 -0
  120. package/kits/docs/evolcore/INDEX.md +3 -2
  121. package/kits/docs/evolcore/agent-create.md +39 -28
  122. package/kits/docs/evolcore/agent.md +19 -4
  123. package/kits/docs/evolcore/ec-capabilities.md +4 -0
  124. package/kits/docs/evolcore/group-collaboration.md +3 -5
  125. package/kits/docs/evolcore/group.md +3 -4
  126. package/kits/docs/evolcore/memory-consolidation.md +1 -1
  127. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  128. package/kits/docs/evolcore/menu-protocol.md +7 -0
  129. package/kits/docs/evolcore/msg.md +5 -8
  130. package/kits/docs/evolcore/task.md +2 -0
  131. package/kits/docs/evolcore/thread-chain.md +267 -0
  132. package/kits/docs/evolcore/trigger.md +2 -2
  133. package/kits/docs/prompt-loading-architecture.md +1 -0
  134. package/kits/eck_manifest.json +61 -26
  135. package/kits/eck_message_manifest.json +169 -10
  136. package/kits/rules/01-overview.md +0 -9
  137. package/kits/rules/05-venue.md +1 -1
  138. package/kits/rules/06-channel.md +7 -1
  139. package/kits/rules/07-memory-and-thread.md +202 -0
  140. package/kits/schemas/relation-config.schema.9.json +18 -3
  141. package/kits/schemas/role-config.schema.1.json +6 -6
  142. package/kits/schemas/single-session.schema.1.json +6 -6
  143. package/kits/schemas/single-session.schema.2.json +6 -6
  144. package/kits/schemas/single-session.schema.3.json +6 -6
  145. package/kits/skills/.switches.json +12 -1
  146. package/kits/skills/ec-group/SKILL.md +234 -0
  147. package/kits/skills/ec-skill/SKILL.md +10 -1
  148. package/kits/skills/ec-skill/scripts/cli.mjs +15 -1
  149. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  150. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  151. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  152. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  153. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  154. package/kits/skills/thread-ops/SKILL.md +492 -0
  155. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  156. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  157. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  158. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  159. package/kits/templates/bootstrap-welcome.md +7 -7
  160. package/kits/templates/memory-consolidation/forget.md +5 -0
  161. package/kits/templates/memory-consolidation/generalize.md +5 -0
  162. package/kits/templates/memory-consolidation/profile.md +5 -0
  163. package/kits/templates/memory-consolidation/promote.md +5 -0
  164. package/kits/templates/message-fragments/item.md +2 -1
  165. package/kits/templates/message-fragments/thread-context.md +6 -0
  166. package/kits/templates/message-fragments/trigger-session.md +2 -0
  167. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  168. package/kits/templates/roles/member.json +2 -0
  169. package/kits/templates/roles/visitor.json +2 -0
  170. package/kits/templates/system-fragments/bootstrap.md +66 -72
  171. package/kits/templates/system-fragments/channel.md +3 -6
  172. package/kits/templates/system-fragments/commands.md +2 -1
  173. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  174. package/kits/templates/system-fragments/reply.md +3 -2
  175. package/kits/templates/system-fragments/session.md +5 -14
  176. package/package.json +3 -1
  177. package/kits/docs/evolcore/group-fs.md +0 -17
  178. package/kits/docs/venues/aun-group.md +0 -45
  179. package/kits/docs/venues/group.md +0 -43
@@ -1,25 +1,142 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { ConfigTarget, read as readConfig } from '../../config/config-manager.js';
3
+ import { ConfigTarget, mutateConfig, read as readConfig } from '../../config/config-manager.js';
4
4
  import { formatPeerKey } from '../relation/peer-identity.js';
5
+ import { isSystemControlChannel } from '../system-channels.js';
5
6
  import { logger } from '../../utils/logger.js';
6
7
  import { atomicWriteJson } from './session-fs-store.js';
7
8
  import { createRootCausation } from '../causation/context.js';
8
- export const MIN_SESSION_IDLE_SECONDS = 30;
9
- export function readRelationSessionIdleSeconds(session) {
10
- if (!session.selfAID || !session.channelId)
9
+ export const DEFAULT_SESSION_LOOP_SECONDS = 300;
10
+ export const MIN_SESSION_LOOP_SECONDS = 30;
11
+ export const MAX_SESSION_LOOP_SECONDS = 86400;
12
+ export function normalizeSessionLoopConfig(value) {
13
+ if (!value || typeof value !== 'object' || Array.isArray(value))
11
14
  return undefined;
12
- try {
13
- const relation = readConfig(ConfigTarget.Relation, {
14
- self: session.selfAID,
15
- peerKey: formatPeerKey(session.channelType || session.channel, session.channelId),
16
- }, { cache: true, expand: true });
17
- return relation?.sessionIdleSec;
18
- }
19
- catch (error) {
20
- logger.warn(`[SessionIdleMonitor] Failed to read relation config for ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
15
+ const raw = value;
16
+ if (Object.keys(raw).some(key => key !== 'enabled' && key !== 'idleSeconds'))
17
+ return undefined;
18
+ if (raw.enabled === false && Object.keys(raw).every(key => key === 'enabled'))
19
+ return { enabled: false };
20
+ if (raw.enabled !== true || !isValidIdleSeconds(raw.idleSeconds))
21
+ return undefined;
22
+ return { enabled: true, idleSeconds: raw.idleSeconds };
23
+ }
24
+ export function validateIdleSeconds(value) {
25
+ return isValidIdleSeconds(value) ? value : undefined;
26
+ }
27
+ export function resolveSessionLoopTarget(session) {
28
+ if (!session || session.deletedAt || session.topicStatus === 'archived'
29
+ || session.metadata?.sessionType || isSystemControlChannel(session.channel)
30
+ || (session.chatType !== 'private' && session.chatType !== 'group'))
31
+ return undefined;
32
+ const peerId = session.chatType === 'group'
33
+ ? session.metadata?.groupId || session.channelId
34
+ : session.channelId;
35
+ if (!session.selfAID || !peerId)
21
36
  return undefined;
37
+ const peerKey = formatPeerKey(session.channelType || session.channel, peerId);
38
+ return { level: session.threadId ? 'topic-session' : 'relation-main', session, peerKey };
39
+ }
40
+ export function readSessionLoopConfig(target) {
41
+ let value;
42
+ if (target.level === 'topic-session')
43
+ value = target.session.metadata?.sessionLoop;
44
+ else {
45
+ try {
46
+ const relation = readConfig(ConfigTarget.Relation, {
47
+ self: target.session.selfAID,
48
+ peerKey: target.peerKey,
49
+ }, { cache: true, expand: true });
50
+ value = relation?.sessionLoop;
51
+ }
52
+ catch (error) {
53
+ logger.warn(`[SessionLoop] Failed to read configuration for ${target.session.id}; disabled: ${error instanceof Error ? error.message : String(error)}`);
54
+ return undefined;
55
+ }
56
+ }
57
+ const config = normalizeSessionLoopConfig(value);
58
+ if (value !== undefined && !config)
59
+ logger.warn(`[SessionLoop] Invalid configuration for ${target.session.id}; disabled`);
60
+ return config;
61
+ }
62
+ export function readSessionLoopConfigForSession(session) {
63
+ const target = resolveSessionLoopTarget(session);
64
+ return target ? readSessionLoopConfig(target) : undefined;
65
+ }
66
+ export function updateRelationSessionLoop(session, config) {
67
+ const target = resolveSessionLoopTarget(session);
68
+ if (!target || target.level !== 'relation-main')
69
+ throw sessionLoopError('INVALID_TARGET', 'relation main session is required');
70
+ const selector = { self: session.selfAID, peerKey: target.peerKey };
71
+ const normalized = normalizeSessionLoopConfig(config);
72
+ if (!normalized)
73
+ throw sessionLoopError('INVALID_ARGUMENT', 'invalid sessionLoop configuration');
74
+ mutateConfig(ConfigTarget.Relation, selector, current => {
75
+ current.sessionLoop = normalized;
76
+ });
77
+ }
78
+ export async function resolveCurrentSessionLoopTarget(store, route) {
79
+ let session;
80
+ try {
81
+ session = route.threadId
82
+ ? await store.getThreadSession(route.channel, route.channelId, route.threadId)
83
+ : await store.getActiveSession(route.channel, route.channelId);
84
+ }
85
+ catch {
86
+ throw sessionLoopError('SESSION_LOOKUP_FAILED', '无法读取当前会话,拒绝回退到其他会话');
87
+ }
88
+ if (!session || session.deletedAt)
89
+ throw sessionLoopError(route.threadId ? 'NOT_FOUND' : 'NO_ACTIVE_SESSION', '当前会话不存在');
90
+ const target = resolveSessionLoopTarget(session);
91
+ if (!target || !route.selfAid || session.selfAID !== route.selfAid
92
+ || session.channel !== route.channel || session.channelId !== route.channelId
93
+ || (session.threadId || '') !== (route.threadId || '')) {
94
+ throw sessionLoopError('INVALID_CONTEXT', '当前会话路由或归属不匹配');
95
+ }
96
+ return target;
97
+ }
98
+ export async function refreshSessionLoopTarget(store, route, expected) {
99
+ const current = await resolveCurrentSessionLoopTarget(store, route);
100
+ if (current.session.id !== expected.session.id || current.peerKey !== expected.peerKey || current.level !== expected.level) {
101
+ throw sessionLoopError('CONFLICT', '当前会话已发生变化,请重新查询');
22
102
  }
103
+ return current;
104
+ }
105
+ export function writeSessionLoopConfig(store, target, config) {
106
+ if (target.level === 'topic-session') {
107
+ const session = store.updateSessionLoop(target.session.id, config);
108
+ return { ...target, session };
109
+ }
110
+ const active = store.getActiveSessionSync(target.session.channel, target.session.channelId);
111
+ if (!active || active.deletedAt || active.id !== target.session.id || active.threadId) {
112
+ throw sessionLoopError('CONFLICT', '当前主会话已发生变化,请重新查询');
113
+ }
114
+ updateRelationSessionLoop(active, config);
115
+ return { ...target, session: active };
116
+ }
117
+ export function sessionLoopPayload(target) {
118
+ const config = readSessionLoopConfig(target);
119
+ return {
120
+ target: { level: target.level, sessionId: target.session.id },
121
+ sessionId: target.session.id,
122
+ value: config?.enabled === true,
123
+ ...(config?.enabled ? { idleSeconds: config.idleSeconds } : {}),
124
+ };
125
+ }
126
+ export function sessionLoopError(code, message) {
127
+ return Object.assign(new Error(message), { code });
128
+ }
129
+ function isValidIdleSeconds(value) {
130
+ return typeof value === 'number'
131
+ && Number.isFinite(value)
132
+ && Number.isSafeInteger(value)
133
+ && value >= MIN_SESSION_LOOP_SECONDS
134
+ && value <= MAX_SESSION_LOOP_SECONDS;
135
+ }
136
+ export const MIN_SESSION_IDLE_SECONDS = MIN_SESSION_LOOP_SECONDS;
137
+ export function readSessionLoopIdleSeconds(session) {
138
+ const config = readSessionLoopConfigForSession(session);
139
+ return config?.enabled ? config.idleSeconds : undefined;
23
140
  }
24
141
  /**
25
142
  * Publishes one session:idle event after a runner task ends, using the
@@ -71,7 +188,17 @@ export class SessionIdleMonitor {
71
188
  if (event.type !== 'session:switched')
72
189
  return;
73
190
  this.clearSession(event.fromSessionId);
74
- this.clearSession(event.toSessionId);
191
+ void this.activateMainSession(event.toSessionId);
192
+ };
193
+ onSessionCreated = (event) => {
194
+ if (event.type !== 'session:created' || event.threadId)
195
+ return;
196
+ for (const sessionId of this.sessions.keys())
197
+ void this.pruneInactiveMainSession(sessionId);
198
+ };
199
+ onSessionLoopChanged = (event) => {
200
+ if (event.type === 'session:loop-changed')
201
+ void this.refreshSession(event.sessionId, event.timestamp);
75
202
  };
76
203
  constructor(options) {
77
204
  this.options = options;
@@ -88,6 +215,8 @@ export class SessionIdleMonitor {
88
215
  this.options.eventBus.subscribe('task:started', this.onTaskStarted);
89
216
  this.options.eventBus.subscribe('session:deleted', this.onSessionDeleted);
90
217
  this.options.eventBus.subscribe('session:switched', this.onSessionSwitched);
218
+ this.options.eventBus.subscribe('session:created', this.onSessionCreated);
219
+ this.options.eventBus.subscribe('session:loop-changed', this.onSessionLoopChanged);
91
220
  }
92
221
  /** Arm restored timers only after event-trigger subscriptions are registered. */
93
222
  activate() {
@@ -108,6 +237,47 @@ export class SessionIdleMonitor {
108
237
  this.scheduleSession(sessionId);
109
238
  }
110
239
  }
240
+ async refreshSession(sessionId, changedAt = this.now()) {
241
+ const state = this.sessions.get(sessionId);
242
+ if (!state || !this.active)
243
+ return;
244
+ const generation = this.generations.get(sessionId) ?? 0;
245
+ try {
246
+ const session = await this.options.getSessionById(sessionId);
247
+ if (!session || !this.isEligibleSession(session))
248
+ return;
249
+ const seconds = await this.readIdleSeconds(session);
250
+ if (this.sessions.get(sessionId) !== state || (this.generations.get(sessionId) ?? 0) !== generation)
251
+ return;
252
+ if (state.idleSeconds === seconds)
253
+ return;
254
+ this.clearTimers(sessionId);
255
+ this.generations.set(sessionId, generation + 1);
256
+ state.idleSeconds = seconds;
257
+ state.armedAt = changedAt;
258
+ state.emitted = false;
259
+ this.persistState();
260
+ if (seconds !== undefined && !await this.options.isSessionBusy(sessionId))
261
+ this.scheduleSession(sessionId);
262
+ }
263
+ catch (error) {
264
+ this.warn(`Failed to refresh loop configuration for ${sessionId}`, error);
265
+ }
266
+ }
267
+ async refreshRelation(selfAid, peerKey, changedAt = this.now()) {
268
+ for (const sessionId of this.sessions.keys()) {
269
+ try {
270
+ const session = await this.options.getSessionById(sessionId);
271
+ const target = resolveSessionLoopTarget(session);
272
+ if (target?.level === 'relation-main' && target.peerKey === peerKey && session?.selfAID === selfAid) {
273
+ await this.refreshSession(sessionId, changedAt);
274
+ }
275
+ }
276
+ catch (error) {
277
+ this.warn(`Failed to refresh relation loop configuration for ${sessionId}`, error);
278
+ }
279
+ }
280
+ }
111
281
  stop() {
112
282
  this.active = false;
113
283
  this.clearAllTimers();
@@ -120,6 +290,8 @@ export class SessionIdleMonitor {
120
290
  this.options.eventBus.unsubscribe('task:started', this.onTaskStarted);
121
291
  this.options.eventBus.unsubscribe('session:deleted', this.onSessionDeleted);
122
292
  this.options.eventBus.unsubscribe('session:switched', this.onSessionSwitched);
293
+ this.options.eventBus.unsubscribe('session:created', this.onSessionCreated);
294
+ this.options.eventBus.unsubscribe('session:loop-changed', this.onSessionLoopChanged);
123
295
  }
124
296
  now() {
125
297
  return this.options.now?.() ?? Date.now();
@@ -172,15 +344,21 @@ export class SessionIdleMonitor {
172
344
  this.clearSession(sessionId);
173
345
  return;
174
346
  }
175
- if (!isSupportedSession(session))
347
+ if (!this.isEligibleSession(session)) {
348
+ this.clearTimers(sessionId);
176
349
  return;
350
+ }
177
351
  const idleSeconds = await this.readIdleSeconds(session);
178
352
  if (!this.isCurrentCycle(sessionId, state.lastEndedAt, generation))
179
353
  return;
354
+ if (this.timers.has(sessionId))
355
+ return;
180
356
  const idleMs = toIdleMs(idleSeconds);
357
+ state.idleSeconds = idleSeconds;
358
+ this.persistState();
181
359
  if (idleMs === undefined)
182
360
  return;
183
- const delay = Math.max(0, state.lastEndedAt + idleMs - this.now());
361
+ const delay = Math.max(0, (state.armedAt ?? state.lastEndedAt) + idleMs - this.now());
184
362
  const timer = setTimeout(() => {
185
363
  this.timers.delete(sessionId);
186
364
  void this.emitIfIdle(sessionId, state.lastEndedAt, generation);
@@ -209,14 +387,14 @@ export class SessionIdleMonitor {
209
387
  this.clearSession(sessionId);
210
388
  return;
211
389
  }
212
- if (!isSupportedSession(session))
390
+ if (!this.isEligibleSession(session))
213
391
  return;
214
392
  const idleMs = toIdleMs(await this.readIdleSeconds(session));
215
393
  if (!this.isCurrentCycle(sessionId, lastEndedAt, generation))
216
394
  return;
217
395
  if (idleMs === undefined)
218
396
  return;
219
- const remaining = lastEndedAt + idleMs - this.now();
397
+ const remaining = (this.sessions.get(sessionId)?.armedAt ?? lastEndedAt) + idleMs - this.now();
220
398
  if (remaining > 0) {
221
399
  this.scheduleSession(sessionId);
222
400
  return;
@@ -232,6 +410,8 @@ export class SessionIdleMonitor {
232
410
  }
233
411
  if (!this.isCurrentCycle(sessionId, lastEndedAt, generation))
234
412
  return;
413
+ if (!this.isEligibleSession(session))
414
+ return;
235
415
  if (busy) {
236
416
  // An active/queued task is not idle. Disarm this cycle and let its next
237
417
  // terminal event start a fresh one; repeatedly rearming here would poll
@@ -248,7 +428,8 @@ export class SessionIdleMonitor {
248
428
  if (!state || state.emitted)
249
429
  return;
250
430
  state.emitted = true;
251
- this.persistState();
431
+ if (!this.persistState())
432
+ return;
252
433
  this.options.eventBus.publish({
253
434
  type: 'session:idle',
254
435
  sessionId,
@@ -308,6 +489,49 @@ export class SessionIdleMonitor {
308
489
  return undefined;
309
490
  }
310
491
  }
492
+ isEligibleSession(session) {
493
+ return !!resolveSessionLoopTarget(session)
494
+ && (!!session.threadId || !this.options.isCurrentMainSession || this.options.isCurrentMainSession(session));
495
+ }
496
+ async disarmMainSession(sessionId) {
497
+ try {
498
+ const session = await this.options.getSessionById(sessionId);
499
+ if (!session || session.threadId)
500
+ return;
501
+ this.clearTimers(sessionId);
502
+ this.generations.set(sessionId, (this.generations.get(sessionId) ?? 0) + 1);
503
+ }
504
+ catch (error) {
505
+ this.warn(`Failed to disarm main session ${sessionId}`, error);
506
+ }
507
+ }
508
+ async activateMainSession(sessionId) {
509
+ try {
510
+ const session = await this.options.getSessionById(sessionId);
511
+ const state = this.sessions.get(sessionId);
512
+ if (!session || session.threadId || !state || !this.isEligibleSession(session))
513
+ return;
514
+ this.clearTimers(sessionId);
515
+ this.generations.set(sessionId, (this.generations.get(sessionId) ?? 0) + 1);
516
+ state.armedAt = this.now();
517
+ state.emitted = false;
518
+ this.persistState();
519
+ this.scheduleSession(sessionId);
520
+ }
521
+ catch (error) {
522
+ this.warn(`Failed to activate main session ${sessionId}`, error);
523
+ }
524
+ }
525
+ async pruneInactiveMainSession(sessionId) {
526
+ try {
527
+ const session = await this.options.getSessionById(sessionId);
528
+ if (session && !session.threadId && !this.isEligibleSession(session))
529
+ await this.disarmMainSession(sessionId);
530
+ }
531
+ catch (error) {
532
+ this.warn(`Failed to inspect main session ${sessionId}`, error);
533
+ }
534
+ }
311
535
  loadState() {
312
536
  try {
313
537
  if (!fs.existsSync(this.options.statePath))
@@ -330,6 +554,8 @@ export class SessionIdleMonitor {
330
554
  lastEndedAt,
331
555
  emitted,
332
556
  ...(typeof rawState.encrypted === 'boolean' ? { encrypted: rawState.encrypted } : {}),
557
+ ...(typeof rawState.armedAt === 'number' && Number.isFinite(rawState.armedAt) ? { armedAt: rawState.armedAt } : {}),
558
+ ...(typeof rawState.idleSeconds === 'number' ? { idleSeconds: rawState.idleSeconds } : {}),
333
559
  });
334
560
  }
335
561
  }
@@ -344,23 +570,24 @@ export class SessionIdleMonitor {
344
570
  lastEndedAt: state.lastEndedAt,
345
571
  emitted: state.emitted,
346
572
  ...(typeof state.encrypted === 'boolean' ? { encrypted: state.encrypted } : {}),
573
+ ...(state.armedAt !== undefined ? { armedAt: state.armedAt } : {}),
574
+ ...(state.idleSeconds !== undefined ? { idleSeconds: state.idleSeconds } : {}),
347
575
  };
348
576
  }
349
577
  try {
350
578
  fs.mkdirSync(path.dirname(this.options.statePath), { recursive: true });
351
579
  atomicWriteJson(this.options.statePath, { version: 2, sessions });
580
+ return true;
352
581
  }
353
582
  catch (error) {
354
583
  this.warn(`Failed to persist session idle state at ${this.options.statePath}`, error);
584
+ return false;
355
585
  }
356
586
  }
357
587
  warn(message, error) {
358
588
  logger.warn(`[SessionIdleMonitor] ${message}${error === undefined ? '' : `: ${error instanceof Error ? error.message : String(error)}`}`);
359
589
  }
360
590
  }
361
- function isSupportedSession(session) {
362
- return session.chatType === 'group' || session.chatType === 'private';
363
- }
364
591
  function toIdleMs(seconds) {
365
592
  if (seconds === undefined || !Number.isFinite(seconds) || seconds <= 0)
366
593
  return undefined;
@@ -1,3 +1,4 @@
1
+ import { normalizeSessionLoopConfig, resolveSessionLoopTarget, sessionLoopError } from './session-idle-monitor.js';
1
2
  import { ensureDir } from '../../utils/atomic-write.js';
2
3
  import { resolvePaths } from '../../paths.js';
3
4
  import { logger } from '../../utils/logger.js';
@@ -225,7 +226,7 @@ export function pageForkHistory(snapshot, before = snapshot.items.length, limit
225
226
  };
226
227
  }
227
228
  /** Metadata keys owned by ordinary topic/session routing writers. */
228
- const TOPIC_METADATA_PROTECTED_KEYS = new Set(['turnState', 'resumeAt', 'agentSessions']);
229
+ const TOPIC_METADATA_PROTECTED_KEYS = new Set(['turnState', 'resumeAt', 'agentSessions', 'sessionLoop']);
229
230
  const TOPIC_BACKEND_SCOPED_KEYS = ['resumeAt', 'agentSessions'];
230
231
  function topicMetadataOwnershipError(key) {
231
232
  const error = new Error(`topic metadata field is owned by a dedicated updater: ${key}`);
@@ -646,6 +647,15 @@ export class SessionManager {
646
647
  * @returns 是否真的写入了 .jsonl(去重命中时返回 false)
647
648
  */
648
649
  persistSession(session, intent, opts) {
650
+ if (!opts?.sessionLoopMutation) {
651
+ const latest = this.readMetaLatest(this.metaPathForSession(session));
652
+ const metadata = { ...(session.metadata ?? {}) };
653
+ if (latest?.metadata?.sessionLoop !== undefined)
654
+ metadata.sessionLoop = latest.metadata.sessionLoop;
655
+ else
656
+ delete metadata.sessionLoop;
657
+ session.metadata = metadata;
658
+ }
649
659
  // Topic session snapshots can be written by synchronous legacy paths
650
660
  // (mark/clear processing) after an async /renew has committed. Preserve
651
661
  // the newest backend fence unless this call is one of the two explicit
@@ -1308,6 +1318,8 @@ export class SessionManager {
1308
1318
  if (updates.name !== undefined)
1309
1319
  current.name = updates.name;
1310
1320
  if (updates.metadata !== undefined) {
1321
+ if (Object.hasOwn(updates.metadata, 'sessionLoop'))
1322
+ throw topicMetadataOwnershipError('sessionLoop');
1311
1323
  if (current.threadId) {
1312
1324
  for (const key of Object.keys(updates.metadata)) {
1313
1325
  if (TOPIC_METADATA_PROTECTED_KEYS.has(key))
@@ -1337,6 +1349,8 @@ export class SessionManager {
1337
1349
  return;
1338
1350
  const { current } = loaded;
1339
1351
  for (const key of Object.keys(patch)) {
1352
+ if (key === 'sessionLoop')
1353
+ throw topicMetadataOwnershipError(key);
1340
1354
  if (current.threadId && TOPIC_METADATA_PROTECTED_KEYS.has(key)) {
1341
1355
  throw topicMetadataOwnershipError(key);
1342
1356
  }
@@ -1351,6 +1365,21 @@ export class SessionManager {
1351
1365
  current.metadata = metadata;
1352
1366
  this.persistSession(current, 'sync');
1353
1367
  }
1368
+ updateSessionLoop(sessionId, config) {
1369
+ const normalized = normalizeSessionLoopConfig(config);
1370
+ if (!normalized)
1371
+ throw sessionLoopError('INVALID_ARGUMENT', 'invalid sessionLoop configuration');
1372
+ return this.mutateLatestTopicSession(sessionId, current => {
1373
+ const target = resolveSessionLoopTarget(current);
1374
+ if (!target || target.level !== 'topic-session')
1375
+ throw sessionLoopError('INVALID_CONTEXT', '话题会话不存在或已不可用');
1376
+ current.metadata = { ...(current.metadata ?? {}), sessionLoop: normalized };
1377
+ const changed = this.persistSession(current, 'none', { sessionLoopMutation: true });
1378
+ if (changed)
1379
+ this.eventBus.publish({ type: 'session:loop-changed', sessionId, timestamp: Date.now() });
1380
+ return current;
1381
+ });
1382
+ }
1354
1383
  /** Persist only the turn coordinator-owned metadata field. */
1355
1384
  async updateTurnState(sessionId, turnState) {
1356
1385
  const loaded = this.loadSessionForUpdate(sessionId);
@@ -1366,6 +1395,8 @@ export class SessionManager {
1366
1395
  this.persistSession(current, 'sync', { turnStateMutation: true });
1367
1396
  }
1368
1397
  getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType, label, topicStatus) {
1398
+ const topicMetadata = { ...(metadata ?? {}) };
1399
+ delete topicMetadata.sessionLoop;
1369
1400
  // 使用精确路径(channelType + selfAID)
1370
1401
  const chatDir = (channelType && selfAID)
1371
1402
  ? (() => { const d = chatDirPath(this.sessionsDir, channelType, channelId, selfAID, channel); fs.mkdirSync(d, { recursive: true }); fs.mkdirSync(path.join(d, '_threads'), { recursive: true }); return d; })()
@@ -1387,7 +1418,7 @@ export class SessionManager {
1387
1418
  let mutated = false;
1388
1419
  if (metadata) {
1389
1420
  const creatorPeerId = existing.metadata?.peerId;
1390
- existing.metadata = { ...(existing.metadata || {}), ...metadata };
1421
+ existing.metadata = { ...(existing.metadata || {}), ...topicMetadata };
1391
1422
  if (creatorPeerId && existing.metadata)
1392
1423
  existing.metadata.peerId = creatorPeerId;
1393
1424
  mutated = true;
@@ -1411,7 +1442,21 @@ export class SessionManager {
1411
1442
  const mainActive = readJsonFile(path.join(chatDir, 'active.json'));
1412
1443
  // 兼容旧字段名
1413
1444
  const mainBaseagent = mainActive?.baseagent || mainActive?.agentType;
1414
- const inheritedBaseagent = mainBaseagent || baseagent;
1445
+ // 最终兜底:从 agent config.json 读 active_baseagent(宿主终端下 active.json 可能不存在)
1446
+ const configBaseagent = (() => {
1447
+ if (mainBaseagent || baseagent)
1448
+ return undefined;
1449
+ if (!selfAID)
1450
+ return undefined;
1451
+ try {
1452
+ const cfg = readJsonFile(path.join(resolvePaths().agentsDir, selfAID, 'config.json'));
1453
+ return cfg?.active_baseagent || undefined;
1454
+ }
1455
+ catch {
1456
+ return undefined;
1457
+ }
1458
+ })();
1459
+ const inheritedBaseagent = mainBaseagent || baseagent || configBaseagent;
1415
1460
  if (!inheritedBaseagent) {
1416
1461
  throw new Error('[SessionManager] getOrCreateThreadSession: baseagent is empty');
1417
1462
  }
@@ -1426,7 +1471,7 @@ export class SessionManager {
1426
1471
  baseagent: inheritedBaseagent,
1427
1472
  sessionKey,
1428
1473
  chatType: effectiveChatType,
1429
- metadata,
1474
+ metadata: topicMetadata,
1430
1475
  name: name || '话题会话',
1431
1476
  label: normalizeTopicLabel(label),
1432
1477
  topicStatus: topicStatus ? normalizeTopicStatus(topicStatus) : 'pending',
@@ -1671,10 +1716,10 @@ export class SessionManager {
1671
1716
  return !!session && !session.threadId;
1672
1717
  });
1673
1718
  }
1674
- async getThreadSession(channel, channelId, threadId) {
1719
+ async getThreadSession(channel, channelId, threadId, selfAID) {
1675
1720
  let chatDir;
1676
1721
  try {
1677
- chatDir = this.resolveChatDirSafe(channel, channelId);
1722
+ chatDir = this.resolveChatDirSafe(channel, channelId, channel === 'aun' ? 'aun' : undefined, selfAID);
1678
1723
  }
1679
1724
  catch {
1680
1725
  return undefined;
@@ -1687,9 +1732,6 @@ export class SessionManager {
1687
1732
  const session = this.readMetaLatest(metaPath);
1688
1733
  if (!session)
1689
1734
  return undefined;
1690
- // Do not consult provider-file existence to derive topic binding state;
1691
- // the topic JSONL boundary is authoritative and may intentionally point
1692
- // at a backend that needs an explicit /renew or repair operation.
1693
1735
  const validSessionId = normalizeBackendSessionId(session.agentSessionId);
1694
1736
  return { ...session, agentSessionId: validSessionId };
1695
1737
  }
@@ -1761,6 +1803,8 @@ export class SessionManager {
1761
1803
  return target;
1762
1804
  }
1763
1805
  updateMetadata(sessionId, metadata) {
1806
+ if (Object.hasOwn(metadata, 'sessionLoop'))
1807
+ throw topicMetadataOwnershipError('sessionLoop');
1764
1808
  const loaded = this.loadSessionForUpdate(sessionId);
1765
1809
  if (!loaded)
1766
1810
  return;
@@ -2123,6 +2167,7 @@ export class SessionManager {
2123
2167
  delete metadata.agentSessions;
2124
2168
  delete metadata.resumeAt;
2125
2169
  delete metadata.replyContext;
2170
+ delete metadata.sessionLoop;
2126
2171
  metadata.__sessionRenewedAt = Date.now();
2127
2172
  const session = {
2128
2173
  id: generateSessionId(),
@@ -0,0 +1,13 @@
1
+ import { isValidAid } from '../../aun/aid/validation.js';
2
+ export function resolveSkillRefreshArgv(argv) {
3
+ if (argv[0] !== 'skill' || argv[1] !== 'refresh' || argv.length < 3 || argv.length > 4
4
+ || argv.some(value => typeof value !== 'string'))
5
+ throw new Error('Invalid skill refresh argv');
6
+ const positional = argv.slice(2).filter(value => value !== '--json');
7
+ if (positional.length !== 1)
8
+ throw new Error('Specify one Agent AID');
9
+ const aid = positional[0].replace(/^@/, '');
10
+ if (!isValidAid(aid))
11
+ throw new Error('Invalid Agent AID');
12
+ return aid;
13
+ }
@@ -42,6 +42,9 @@ function generateOneIndex(indexDir, filename, skills, label) {
42
42
  const sep = path.sep;
43
43
  const header = `[skills-${label}] 本 Agent 已启用 ${skills.length} 个${label === 'builtin' ? '内置' : '本机'}技能。
44
44
 
45
+ 加载方式: EC 技能用 Read 工具读取上面对应的 SKILL.md 全文后按内容执行;
46
+ 它们不是宿主工具自带的技能或斜杠命令,用 Skill/斜杠命令加载会报 Unknown skill/未知命令。
47
+
45
48
  路径规则:
46
49
  根目录: ${rootDir}
47
50
  技能目录 = 根目录 + "${sep}" + 技能名
@@ -66,7 +69,7 @@ function generateOneIndex(indexDir, filename, skills, label) {
66
69
  * 直接按目录读,不走 discoverSkillDirectories——后者按父目录扫描再按 name 匹配,
67
70
  * 同名技能(本机覆盖内置)会读到另一份的 description。
68
71
  */
69
- function readSkillDescription(skillDir) {
72
+ export function readSkillDescription(skillDir) {
70
73
  try {
71
74
  const text = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf-8');
72
75
  const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
@@ -2,6 +2,28 @@ import { generateSkillIndexes } from './index-generator.js';
2
2
  import { globalSkillWatcher } from './global-watcher.js';
3
3
  import { logger } from '../../utils/logger.js';
4
4
  import { invalidateKitCache } from '../../eck/kit-renderer.js';
5
+ import { isSkillWatchEnabled } from '../../config-store.js';
6
+ /**
7
+ * 监控开关(daemon.json `skills.watch`,默认 false),daemon 启动时读一次。
8
+ *
9
+ * 关闭时不创建任何 chokidar watcher:Windows 上 chokidar 每个文件/目录各占一个
10
+ * 目录句柄(本机实测 ~/.evolcore/skills 约 3400 个),子目录因此无法被资源管理器
11
+ * 删除或重命名。关闭后改完技能需执行 `ec skill refresh <aid>`(refreshSkillIndex)。
12
+ * 监控与手动刷新互不冲突,打开开关后两者可同时使用。
13
+ */
14
+ let watchEnabled;
15
+ function skillWatchEnabled() {
16
+ if (watchEnabled === undefined) {
17
+ try {
18
+ watchEnabled = isSkillWatchEnabled();
19
+ }
20
+ catch (e) {
21
+ logger.warn(`[skill] 读取 skills.watch 失败,按关闭处理: ${e instanceof Error ? e.message : String(e)}`);
22
+ watchEnabled = false;
23
+ }
24
+ }
25
+ return watchEnabled;
26
+ }
5
27
  /**
6
28
  * daemon 启动时,启动全局监控器。
7
29
  *
@@ -11,6 +33,10 @@ import { invalidateKitCache } from '../../eck/kit-renderer.js';
11
33
  * - agent 收到消息需要组装系统上下文时,检查并按需生成索引
12
34
  */
13
35
  export function startSkillWatcher() {
36
+ if (!skillWatchEnabled()) {
37
+ logger.info('[skill] 技能目录监控已关闭(daemon.json skills.watch=false),改动技能后请执行 ec skill refresh <aid>');
38
+ return;
39
+ }
14
40
  // 启动全局监控器(监控内置和本机技能目录)
15
41
  globalSkillWatcher.start();
16
42
  // 注册防抖回调:内置和本机技能变化时标记所有 agent 为脏并失效缓存
@@ -36,6 +62,8 @@ export function startSkillWatcher() {
36
62
  * 重复调用会 re-arm debounce timer 造成 2 秒自激循环。
37
63
  */
38
64
  export function watchAgentSkills(aid) {
65
+ if (!skillWatchEnabled())
66
+ return;
39
67
  globalSkillWatcher.watchAgent(aid);
40
68
  globalSkillWatcher.onDebouncedChange(`agent:${aid}`, () => {
41
69
  logger.info(`[skill] Agent ${aid} 技能目录有变化,标记为脏状态并失效缓存`);
@@ -65,6 +93,19 @@ export function ensureSkillIndexUpToDate(agentDir, aid) {
65
93
  }
66
94
  }
67
95
  }
96
+ /**
97
+ * 手动刷新指定 agent 的技能索引(`ec skill refresh <aid>` 的 daemon 侧实现)。
98
+ *
99
+ * 与懒加载入口不同:无条件重建,失败直接抛出(由 IPC 返回给调用方),
100
+ * 成功后清掉该 agent 的脏标记,避免下一轮对话再重建一次。
101
+ */
102
+ export function refreshSkillIndex(agentDir, aid) {
103
+ generateSkillIndexes(agentDir);
104
+ globalSkillWatcher.markAgentChecked(aid);
105
+ globalSkillWatcher.markAgentClean(aid);
106
+ invalidateKitCache();
107
+ logger.info(`[skill] 已手动刷新 agent ${aid} 的技能索引`);
108
+ }
68
109
  /**
69
110
  * 停止全局监控器(daemon 关闭时调用)
70
111
  */