evolclaw 2.8.0 → 2.8.2

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.
@@ -1,8 +1,10 @@
1
1
  import path from 'path';
2
2
  import { logger } from '../../utils/logger.js';
3
+ const DEFAULT_AGENT_NAME = '[default]';
3
4
  export class MessageQueue {
4
5
  queues = new Map();
5
6
  processing = new Set();
7
+ processingAgent = new Map(); // queueKey → agentName(处理中项目的 agent)
6
8
  externalLocks = new Map();
7
9
  handler;
8
10
  currentSessionKey;
@@ -77,18 +79,24 @@ export class MessageQueue {
77
79
  return Promise.resolve();
78
80
  }
79
81
  const queueKey = this.getQueueKey(sessionKey, projectPath);
80
- logger.debug(`[Queue] Enqueuing message for ${queueKey}`);
82
+ const agentName = options?.agentName || DEFAULT_AGENT_NAME;
83
+ logger.debug(`[Queue] Enqueuing message for ${queueKey} (agent=${agentName})`);
81
84
  return new Promise((resolve, reject) => {
82
85
  if (!this.queues.has(queueKey)) {
83
86
  this.queues.set(queueKey, []);
84
87
  }
85
- this.queues.get(queueKey).push({ message, projectPath, resolve, reject });
88
+ this.queues.get(queueKey).push({ message, projectPath, agentName, resolve, reject });
86
89
  // 根据 interruptible 选项决定是否触发中断
87
90
  if (this.processing.has(queueKey)) {
88
91
  if (options?.interruptible !== false) {
89
92
  // 单聊:保留中断行为
90
93
  logger.debug(`[Queue] ${queueKey} is processing, triggering interrupt`);
91
- this.eventBus?.publish({ type: 'message:interrupted', sessionId: sessionKey, reason: 'new_message' });
94
+ this.eventBus?.publish({
95
+ type: 'message:interrupted',
96
+ sessionId: sessionKey,
97
+ reason: 'new_message',
98
+ agentName: this.processingAgent.get(queueKey),
99
+ });
92
100
  if (this.interruptCallback) {
93
101
  this.interruptCallback(sessionKey, this.currentAgentId).catch(() => { });
94
102
  }
@@ -118,6 +126,7 @@ export class MessageQueue {
118
126
  if (!queue || queue.length === 0) {
119
127
  logger.debug(`[Queue] Queue ${queueKey} is empty, stopping`);
120
128
  this.processing.delete(queueKey);
129
+ this.processingAgent.delete(queueKey);
121
130
  this.currentSessionKey = undefined;
122
131
  this.currentProjectPath = undefined;
123
132
  this.activeMessageIds.clear();
@@ -129,6 +138,7 @@ export class MessageQueue {
129
138
  this.currentSessionKey = queueKey;
130
139
  this.currentProjectPath = merged.projectPath;
131
140
  this.currentAgentId = merged.message.agentId;
141
+ this.processingAgent.set(queueKey, merged.agentName);
132
142
  // 记录正在执行的 messageId(用于撤回中断)
133
143
  this.activeMessageIds.clear();
134
144
  for (const item of items) {
@@ -203,6 +213,7 @@ export class MessageQueue {
203
213
  return {
204
214
  message: merged,
205
215
  projectPath: last.projectPath,
216
+ agentName: last.agentName,
206
217
  resolve: () => { }, // 由调用方管理
207
218
  reject: () => { },
208
219
  };
@@ -226,6 +237,20 @@ export class MessageQueue {
226
237
  }
227
238
  return false;
228
239
  }
240
+ /**
241
+ * 检查指定 channel 下是否有任何 session 在处理。
242
+ * queueKey 格式为 `${sessionKey}::${projectPath}`,其中 sessionKey
243
+ * 形如 `${channelName}-${channelId}-${ts}`,因此匹配 `${channelName}-` 前缀。
244
+ */
245
+ isChannelProcessing(channelName) {
246
+ const prefix = `${channelName}-`;
247
+ for (const key of this.processing.keys()) {
248
+ if (key.startsWith(prefix) || key.startsWith(`${channelName}::`)) {
249
+ return true;
250
+ }
251
+ }
252
+ return false;
253
+ }
229
254
  cancel(messageId) {
230
255
  for (const queue of this.queues.values()) {
231
256
  const idx = queue.findIndex(q => q.message.messageId === messageId);
@@ -250,7 +275,12 @@ export class MessageQueue {
250
275
  // 从 queueKey 提取 sessionKey
251
276
  const sessionKey = this.currentSessionKey.split('::')[0];
252
277
  logger.info(`[Queue] Recalled active message ${messageId}, interrupting session ${sessionKey}`);
253
- this.eventBus?.publish({ type: 'message:interrupted', sessionId: sessionKey, reason: 'recalled' });
278
+ this.eventBus?.publish({
279
+ type: 'message:interrupted',
280
+ sessionId: sessionKey,
281
+ reason: 'recalled',
282
+ agentName: this.processingAgent.get(this.currentSessionKey),
283
+ });
254
284
  if (this.interruptCallback) {
255
285
  this.interruptCallback(sessionKey, this.currentAgentId).catch(() => { });
256
286
  }
@@ -293,4 +323,29 @@ export class MessageQueue {
293
323
  getGlobalProcessingCount() {
294
324
  return this.processing.size;
295
325
  }
326
+ /**
327
+ * 获取指定 agent 的待处理消息数量。
328
+ * agent 维度按 enqueue 时传入的 agentName 计数。
329
+ */
330
+ getQueueLengthByAgent(agentName) {
331
+ let total = 0;
332
+ for (const queue of this.queues.values()) {
333
+ for (const item of queue) {
334
+ if ((item.agentName || DEFAULT_AGENT_NAME) === agentName)
335
+ total++;
336
+ }
337
+ }
338
+ return total;
339
+ }
340
+ /**
341
+ * 获取指定 agent 的处理中队列数量。
342
+ */
343
+ getProcessingCountByAgent(agentName) {
344
+ let total = 0;
345
+ for (const a of this.processingAgent.values()) {
346
+ if ((a || DEFAULT_AGENT_NAME) === agentName)
347
+ total++;
348
+ }
349
+ return total;
350
+ }
296
351
  }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Reload Hooks
3
+ *
4
+ * Extracted from index.ts main() for testability. Builds the ReloadHooks
5
+ * implementation used by AgentRegistry.reload() to drain/disconnect/start
6
+ * channels during a hot reload.
7
+ */
8
+ import { logger } from '../utils/logger.js';
9
+ export function buildReloadHooks(deps) {
10
+ const { channelLoader, channelInstances, registerChannelInstance, messageQueue } = deps;
11
+ const drainDelayMs = deps.drainDelayMs ?? 500;
12
+ const drainTimeoutMs = deps.drainTimeoutMs ?? 30000;
13
+ return {
14
+ async drainChannel(channelName) {
15
+ logger.info(`[Reload] Draining channel: ${channelName}`);
16
+ if (messageQueue) {
17
+ // Real drain: poll until empty or timeout
18
+ const pollMs = 100;
19
+ const start = Date.now();
20
+ while (messageQueue.isChannelProcessing(channelName)) {
21
+ if (Date.now() - start > drainTimeoutMs) {
22
+ logger.warn(`[Reload] Drain timeout (${drainTimeoutMs}ms) for channel: ${channelName}, proceeding anyway`);
23
+ return;
24
+ }
25
+ await new Promise(r => setTimeout(r, pollMs));
26
+ }
27
+ logger.info(`[Reload] Drain complete: ${channelName}`);
28
+ }
29
+ else if (drainDelayMs > 0) {
30
+ await new Promise(r => setTimeout(r, drainDelayMs));
31
+ }
32
+ },
33
+ async disconnectChannel(channelName) {
34
+ const inst = channelInstances.find(i => i.adapter.channelName === channelName);
35
+ if (!inst) {
36
+ logger.warn(`[Reload] Channel ${channelName} not found, skipping disconnect`);
37
+ return;
38
+ }
39
+ try {
40
+ await inst.disconnect();
41
+ const idx = channelInstances.indexOf(inst);
42
+ if (idx >= 0)
43
+ channelInstances.splice(idx, 1);
44
+ logger.info(`[Reload] Disconnected channel: ${channelName}`);
45
+ }
46
+ catch (e) {
47
+ logger.error(`[Reload] Failed to disconnect ${channelName}: ${e}`);
48
+ throw e;
49
+ }
50
+ },
51
+ async startChannel(agent, channelName) {
52
+ const channels = agent.config.channels;
53
+ let channelType = null;
54
+ for (const [type, raw] of Object.entries(channels)) {
55
+ const instances = Array.isArray(raw) ? raw : [raw];
56
+ for (const inst of instances) {
57
+ const name = inst.name ?? type;
58
+ if (name === channelName) {
59
+ channelType = type;
60
+ break;
61
+ }
62
+ }
63
+ if (channelType)
64
+ break;
65
+ }
66
+ if (!channelType) {
67
+ const msg = `[Reload] Channel ${channelName} not found in agent ${agent.name} config`;
68
+ logger.error(msg);
69
+ throw new Error(msg);
70
+ }
71
+ const partialConfig = {
72
+ agents: agent.config.agents,
73
+ channels: { [channelType]: channels[channelType] },
74
+ projects: agent.config.projects,
75
+ };
76
+ const newInstances = await channelLoader.createAll(partialConfig);
77
+ const newInst = newInstances.find(i => i.adapter.channelName === channelName);
78
+ if (!newInst) {
79
+ throw new Error(`[Reload] Failed to create instance ${channelName}`);
80
+ }
81
+ registerChannelInstance(newInst);
82
+ await newInst.connect();
83
+ channelInstances.push(newInst);
84
+ logger.info(`[Reload] Started channel: ${channelName}`);
85
+ },
86
+ };
87
+ }
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ClaudeSessionFileAdapter } from './core/session/adapters/claude-session-file-adapter.js';
2
2
  import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-file-adapter.js';
3
3
  import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
4
- import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig, isOwner, isAdmin, validateConfigIntegrity, validateChannelInstanceNames, getOwner, getDefaultSessionMode } from './config.js';
4
+ import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig, isOwner, isAdmin, validateConfigIntegrity, validateChannelInstanceNames, getOwner, getDefaultSessionMode, setOwner as setOwnerInGlobalConfig, setChannelShowActivities as setShowActivitiesInGlobalConfig } from './config.js';
5
5
  import { SessionManager } from './core/session/session-manager.js';
6
6
  import { ClaudeAgentPlugin } from './agents/claude-runner.js';
7
7
  import { CodexAgentPlugin } from './agents/codex-runner.js';
@@ -23,10 +23,12 @@ import { PermissionGateway } from './core/permission.js';
23
23
  import { InteractionRouter } from './core/interaction-router.js';
24
24
  import { ChannelLoader } from './core/channel-loader.js';
25
25
  import { AgentLoader } from './core/agent-loader.js';
26
+ import { EvolAgentRegistry } from './core/evolagent-registry.js';
27
+ import { buildReloadHooks } from './utils/reload-hooks.js';
26
28
  import { IpcServer } from './ipc.js';
27
29
  import { logger, setLogLevel } from './utils/logger.js';
28
- import { detectDuplicates } from './utils/channel-fingerprint.js';
29
- import { loadPromptTemplates } from './prompts/templates.js';
30
+ import { detectDuplicates } from './core/evolagent-registry.js';
31
+ import { loadPromptTemplates } from './agents/templates.js';
30
32
  import path from 'path';
31
33
  import fs from 'fs';
32
34
  async function main() {
@@ -82,13 +84,45 @@ async function main() {
82
84
  if (anthropic.baseUrl) {
83
85
  logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
84
86
  }
87
+ // EvolAgent Registry
88
+ const agentRegistry = new EvolAgentRegistry(paths.agentsDir, {
89
+ setOwner: (channelName, userId) => {
90
+ setOwnerInGlobalConfig(config, channelName, userId);
91
+ },
92
+ setShowActivities: (channelName, mode) => {
93
+ setShowActivitiesInGlobalConfig(config, channelName, mode);
94
+ },
95
+ });
96
+ agentRegistry.loadAll(config);
97
+ const agentInfos = agentRegistry.list();
98
+ const evolagentCount = agentInfos.filter(i => !i.isDefault).length;
99
+ if (evolagentCount > 0) {
100
+ logger.info(`✓ Loaded ${evolagentCount} evolagent(s)`);
101
+ for (const info of agentInfos) {
102
+ if (info.isDefault)
103
+ continue;
104
+ if (info.status === 'error') {
105
+ logger.error(` ✗ [${info.name}] ${info.error}`);
106
+ }
107
+ else if (info.status === 'disabled') {
108
+ logger.info(` ○ [${info.name}] disabled`);
109
+ }
110
+ else {
111
+ logger.info(` ● [${info.name}] ${info.baseagent} @ ${path.basename(info.projectPath)}`);
112
+ }
113
+ }
114
+ }
115
+ // Store for IPC access (T10 will wire this)
116
+ // M4: removed dead globalThis.__evolclaw_agentRegistry assignment
85
117
  // 创建事件总线
86
118
  const eventBus = new EventBus();
87
119
  logger.info('✓ Event bus initialized');
88
120
  // 统计收集器(近 1 小时滚动统计)
89
121
  const statsCollector = new StatsCollector(eventBus);
90
122
  // 初始化数据库(带 ownerResolver)
91
- const sessionManager = new SessionManager(undefined, eventBus, (channel, userId) => isOwner(config, channel, userId), (channel, userId) => isAdmin(config, channel, userId));
123
+ // Registry-first: agent-owned channels resolve via EvolAgent.isOwner/isAdmin,
124
+ // default-agent channels fall back to global config (evolclaw.json).
125
+ const sessionManager = new SessionManager(undefined, eventBus, (channel, userId) => agentRegistry.isOwner(channel, userId, (ch, uid) => isOwner(config, ch, uid)), (channel, userId) => agentRegistry.isAdmin(channel, userId, (ch, uid) => isAdmin(config, ch, uid)));
92
126
  // sessionMode 解析:全局 chatmode 配置 > 默认 'interactive'
93
127
  sessionManager.setSessionModeResolver((_channel, chatType) => {
94
128
  return getDefaultSessionMode(config, chatType);
@@ -143,8 +177,48 @@ async function main() {
143
177
  channelLoader.register(new DingtalkChannelPlugin());
144
178
  channelLoader.register(new QQBotChannelPlugin());
145
179
  channelLoader.register(new WecomChannelPlugin());
146
- const channelInstances = await channelLoader.createAll(config);
147
- logger.info(`✓ Created ${channelInstances.length} channel instance(s)`);
180
+ // Create channel instances: default (from evolclaw.json) + each evolagent
181
+ const defaultInstances = await channelLoader.createAll(config);
182
+ const evolagentInstances = [];
183
+ for (const agent of agentRegistry.runnableAgents()) {
184
+ // Rewrite channel instance names with agent prefix to avoid collisions
185
+ // with DefaultAgent and other EvolAgents.
186
+ // Rule (EvolAgent only):
187
+ // - explicit name → `${agent.name}-${type}-${name}`
188
+ // - omitted name → `${agent.name}-${type}`
189
+ const rewrittenChannels = {};
190
+ for (const [type, raw] of Object.entries(agent.config.channels || {})) {
191
+ if (type === 'defaultChannel') {
192
+ rewrittenChannels[type] = raw;
193
+ continue;
194
+ }
195
+ const instances = Array.isArray(raw) ? raw : [raw];
196
+ const rewritten = instances.map((inst) => {
197
+ if (!inst || typeof inst !== 'object')
198
+ return inst;
199
+ const effName = agent.effectiveChannelName(type, inst.name);
200
+ return { ...inst, name: effName };
201
+ });
202
+ // Preserve original shape (array vs single object)
203
+ rewrittenChannels[type] = Array.isArray(raw) ? rewritten : rewritten[0];
204
+ }
205
+ const agentConfig = {
206
+ agents: agent.config.agents,
207
+ channels: rewrittenChannels,
208
+ projects: agent.config.projects,
209
+ };
210
+ try {
211
+ const instances = await channelLoader.createAll(agentConfig);
212
+ evolagentInstances.push(...instances);
213
+ }
214
+ catch (e) {
215
+ logger.error(`[EvolAgent] Failed to create channels for ${agent.name}: ${e}`);
216
+ agent.status = 'error';
217
+ agent.error = `Channel creation failed: ${e}`;
218
+ }
219
+ }
220
+ const channelInstances = [...defaultInstances, ...evolagentInstances];
221
+ logger.info(`✓ Created ${channelInstances.length} channel instance(s)${evolagentInstances.length > 0 ? ` (${defaultInstances.length} default + ${evolagentInstances.length} agent)` : ''}`);
148
222
  // 启动迁移:将 sessions.channel 从 channelType 回填为实例名
149
223
  sessionManager.migrateChannelToInstanceName();
150
224
  // 创建命令处理器
@@ -166,6 +240,13 @@ async function main() {
166
240
  }, defaultAgent);
167
241
  // 回填 processor 和 messageQueue 的引用
168
242
  cmdHandler.setProcessor(processor);
243
+ // Inject EvolAgentRegistry (methods added by T6/T7)
244
+ if (processor.setAgentRegistry) {
245
+ processor.setAgentRegistry(agentRegistry);
246
+ }
247
+ if (cmdHandler.setAgentRegistry) {
248
+ cmdHandler.setAgentRegistry(agentRegistry);
249
+ }
169
250
  // 设置交互路由器
170
251
  processor.setInteractionRouter(interactionRouter);
171
252
  // 设置 compact 开始回调(对所有支持的 agent)
@@ -203,12 +284,18 @@ async function main() {
203
284
  };
204
285
  // ── MessageBridge:Channel ↔ Core 消息桥梁 ──
205
286
  const msgBridge = new MessageBridge(config, sessionManager, processor, messageQueue, cmdHandler, eventBus);
287
+ msgBridge.setAgentRegistry(agentRegistry);
206
288
  // ── Channel instance registration (shared by startup and hot-load) ──
207
289
  function registerChannelInstance(inst) {
208
290
  // 1. 项目路径提供器
209
291
  if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
210
292
  inst.channel.onProjectPathRequest(async (channelId) => {
211
- const session = await sessionManager.getOrCreateSession(inst.adapter.channelName, channelId, config.projects?.defaultPath || process.cwd(), undefined, undefined, undefined, undefined);
293
+ // Effective default path: agent's projectPath if agent-owned, else global
294
+ const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelName);
295
+ const effectiveDefault = (owningAgent && !owningAgent.isDefault)
296
+ ? owningAgent.projectPath
297
+ : (config.projects?.defaultPath || process.cwd());
298
+ const session = await sessionManager.getOrCreateSession(inst.adapter.channelName, channelId, effectiveDefault, undefined, undefined, undefined, undefined);
212
299
  return path.isAbsolute(session.projectPath)
213
300
  ? session.projectPath
214
301
  : path.resolve(process.cwd(), session.projectPath);
@@ -348,20 +435,20 @@ async function main() {
348
435
  for (const inst of channelInstances) {
349
436
  registerChannelInstance(inst);
350
437
  }
351
- // ── 设置热加载回调 ──
352
- cmdHandler.setHotLoadChannel(async (inst) => {
353
- registerChannelInstance(inst);
354
- channelInstances.push(inst);
355
- await inst.connect();
356
- eventBus.publish({
357
- type: 'channel:connected',
358
- channel: (inst.channelType || inst.adapter.channelName).toLowerCase(),
359
- channelName: inst.adapter.channelName,
360
- timestamp: Date.now(),
361
- });
362
- });
363
438
  // ── 连接所有渠道 ──
364
439
  const connected = await channelLoader.connectAll(channelInstances);
440
+ // Bind connected adapters to their owning agents
441
+ // I1: only mark 'running' if a channel actually connected for that agent
442
+ const connectedSet = new Set(connected);
443
+ for (const inst of channelInstances) {
444
+ const agent = agentRegistry.resolveByChannel(inst.adapter.channelName);
445
+ if (!agent || agent.status === 'error')
446
+ continue;
447
+ agent.channels.set(inst.adapter.channelName, inst.adapter);
448
+ if (agent.status === 'stopped' && connectedSet.has(inst.adapter.channelName)) {
449
+ agent.status = 'running';
450
+ }
451
+ }
365
452
  // 预填充 Feishu 已知 thread_id(重启后避免误判话题创建)
366
453
  for (const inst of channelInstances) {
367
454
  const channelType = inst.channelType || inst.adapter.channelName;
@@ -396,7 +483,7 @@ async function main() {
396
483
  continue; // 跳过同类型通道
397
484
  if (notified.has(otherType))
398
485
  continue; // 同类型已通知过
399
- const ownerId = getOwner(config, other.adapter.channelName);
486
+ const ownerId = agentRegistry.getOwner(other.adapter.channelName) ?? getOwner(config, other.adapter.channelName);
400
487
  if (!ownerId)
401
488
  continue;
402
489
  notified.add(otherType);
@@ -511,6 +598,18 @@ async function main() {
511
598
  },
512
599
  };
513
600
  }, async (cmd, sessionId) => cmdHandler.handleCtl(cmd, sessionId));
601
+ // M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
602
+ ipcServer.setAgentRegistry(agentRegistry);
603
+ // ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
604
+ const reloadHooks = buildReloadHooks({
605
+ channelLoader,
606
+ channelInstances,
607
+ registerChannelInstance,
608
+ messageQueue,
609
+ });
610
+ // Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
611
+ globalThis.__evolclaw_reloadHooks = reloadHooks;
612
+ // I3: start IPC server LAST, after all hook setup, to eliminate race window
514
613
  ipcServer.start();
515
614
  // 运行时配置文件监控
516
615
  const configPath = resolvePaths().config;
package/dist/ipc.js CHANGED
@@ -8,11 +8,16 @@ export class IpcServer {
8
8
  getStatus;
9
9
  commandExecutor;
10
10
  server = null;
11
+ agentRegistry;
11
12
  constructor(socketPath, getStatus, commandExecutor) {
12
13
  this.socketPath = socketPath;
13
14
  this.getStatus = getStatus;
14
15
  this.commandExecutor = commandExecutor;
15
16
  }
17
+ /** Inject EvolAgentRegistry for evolagent.* IPC handlers */
18
+ setAgentRegistry(registry) {
19
+ this.agentRegistry = registry;
20
+ }
16
21
  start() {
17
22
  // Remove stale socket file (Unix only — named pipes auto-cleanup on process exit)
18
23
  if (!isNamedPipe(this.socketPath)) {
@@ -82,6 +87,48 @@ export class IpcServer {
82
87
  return { ok: false, error: 'missing cmd or sessionId' };
83
88
  return await this.commandExecutor(slashCmd, sessionId);
84
89
  }
90
+ case 'evolagent.list': {
91
+ if (!this.agentRegistry)
92
+ return { ok: false, error: 'EvolAgentRegistry not available' };
93
+ return { ok: true, agents: this.agentRegistry.list() };
94
+ }
95
+ case 'evolagent.show': {
96
+ if (!this.agentRegistry)
97
+ return { ok: false, error: 'EvolAgentRegistry not available' };
98
+ const name = cmd.name;
99
+ if (!name || typeof name !== 'string')
100
+ return { ok: false, error: 'missing name' };
101
+ const agent = this.agentRegistry.get(name);
102
+ if (!agent)
103
+ return { ok: false, error: `Agent "${name}" not found` };
104
+ const info = this.agentRegistry.list().find((i) => i.name === name);
105
+ // I7: null-guard list().find() result
106
+ if (!info)
107
+ return { ok: false, error: `Agent "${name}" found but info missing (race?)` };
108
+ return { ok: true, agent: info };
109
+ }
110
+ case 'evolagent.reload': {
111
+ if (!this.agentRegistry)
112
+ return { ok: false, error: 'EvolAgentRegistry not available' };
113
+ const name = cmd.name;
114
+ if (!name || typeof name !== 'string')
115
+ return { ok: false, error: 'missing name' };
116
+ const hooks = globalThis.__evolclaw_reloadHooks;
117
+ if (!hooks)
118
+ return { ok: false, error: 'Reload hooks not initialized' };
119
+ try {
120
+ const a = this.agentRegistry.get(name);
121
+ if (!a)
122
+ return { ok: false, error: `Agent "${name}" not found` };
123
+ if (!this.agentRegistry.reload)
124
+ return { ok: false, error: 'EvolAgentRegistry.reload not available' };
125
+ await this.agentRegistry.reload(name, hooks);
126
+ return { ok: true, result: `Agent "${name}" reloaded` };
127
+ }
128
+ catch (e) {
129
+ return { ok: false, error: e?.message || String(e) };
130
+ }
131
+ }
85
132
  default:
86
133
  return { error: `unknown command: ${cmd.type}` };
87
134
  }
package/dist/paths.js CHANGED
@@ -32,6 +32,7 @@ export function resolvePaths() {
32
32
  pid: path.join(root, 'logs', 'evolclaw.pid'),
33
33
  dataDir: path.join(root, 'data'),
34
34
  logs: path.join(root, 'logs'),
35
+ agentsDir: path.join(root, 'agents'),
35
36
  lineStats: path.join(root, 'logs', 'line-stats.log'),
36
37
  readySignal: path.join(root, 'logs', 'ready.signal'),
37
38
  selfHealLog: path.join(root, 'logs', 'self-heal.md'),
@@ -49,6 +50,7 @@ export function ensureDataDirs() {
49
50
  const p = resolvePaths();
50
51
  fs.mkdirSync(p.dataDir, { recursive: true });
51
52
  fs.mkdirSync(p.logs, { recursive: true });
53
+ fs.mkdirSync(p.agentsDir, { recursive: true });
52
54
  }
53
55
  export function getPackageRoot() {
54
56
  // import.meta.dirname is available in Node.js 21.2+ and always returns
package/dist/types.js CHANGED
@@ -3,3 +3,5 @@
3
3
  // Array form: `name` is required to distinguish instances.
4
4
  /** Default permission mode applied to new sessions. Change here to affect all roles. */
5
5
  export const DEFAULT_PERMISSION_MODE = 'bypass';
6
+ /** Reserved agent name used for DefaultAgent (no agent.json file). */
7
+ export const DEFAULT_AGENT_NAME = '[default]';