evolcore 0.0.10 → 0.0.11

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 (62) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +3 -3
  3. package/dist/agents/baseagent.js +4 -0
  4. package/dist/agents/claude-runner.js +123 -42
  5. package/dist/agents/codex-app-server-client.js +33 -9
  6. package/dist/agents/codex-runner.js +58 -8
  7. package/dist/agents/ecagent-runner.js +17 -2
  8. package/dist/agents/request-identity.js +55 -0
  9. package/dist/aun/outbox.js +28 -31
  10. package/dist/channels/aun.js +131 -128
  11. package/dist/cli/agent-command.js +16 -9
  12. package/dist/cli/agent.js +82 -19
  13. package/dist/cli/daemon-commands.js +21 -2
  14. package/dist/cli/index.js +76 -61
  15. package/dist/cli/init-cancel.js +208 -0
  16. package/dist/cli/init-channel.js +343 -195
  17. package/dist/cli/init.js +21 -9
  18. package/dist/config/builtin-roles.js +1 -0
  19. package/dist/config/gateway-config.js +26 -10
  20. package/dist/core/agent-reload-coordinator.js +53 -0
  21. package/dist/core/auth/operation-authorizer.js +32 -147
  22. package/dist/core/auth/operation-catalog.js +80 -0
  23. package/dist/core/bootstrap-messages.js +50 -0
  24. package/dist/core/bootstrap-service.js +85 -10
  25. package/dist/core/channel-loader.js +23 -6
  26. package/dist/core/command/agent-control.js +14 -11
  27. package/dist/core/command/menu-handler.js +67 -76
  28. package/dist/core/command/slash-handler.js +4 -4
  29. package/dist/core/evolagent-registry.js +125 -35
  30. package/dist/core/evolagent.js +8 -3
  31. package/dist/core/inference/text-inference.js +38 -4
  32. package/dist/core/message/message-bridge.js +1 -1
  33. package/dist/core/message/message-log.js +22 -0
  34. package/dist/core/message/message-queue.js +19 -4
  35. package/dist/core/model/model-catalog.js +143 -24
  36. package/dist/core/model/model-diagnostics.js +28 -10
  37. package/dist/core/permission/index.js +1 -0
  38. package/dist/core/permission/readonly-shell-query.js +532 -0
  39. package/dist/core/permission/shell-environment.js +46 -0
  40. package/dist/core/permission/tool-policy.js +231 -93
  41. package/dist/core/protected-paths.js +10 -7
  42. package/dist/core/runner-reload-transaction.js +57 -0
  43. package/dist/index.js +262 -84
  44. package/dist/ipc.js +29 -11
  45. package/dist/utils/aid-bind.js +3 -8
  46. package/dist/utils/log-writer.js +6 -10
  47. package/dist/utils/logger.js +5 -5
  48. package/kits/docs/evolcore/msg.md +13 -0
  49. package/kits/rules/01-overview.md +9 -0
  50. package/kits/schemas/agent-config.schema.3.json +1 -1
  51. package/kits/schemas/agent-config.schema.4.json +1 -1
  52. package/kits/schemas/relation-config.schema.2.json +1 -1
  53. package/kits/schemas/role-config.schema.1.json +1 -1
  54. package/kits/templates/roles/admin.json +5 -0
  55. package/kits/templates/roles/member.json +17 -0
  56. package/kits/templates/roles/visitor.json +8 -0
  57. package/kits/templates/system-fragments/bootstrap.md +12 -6
  58. package/kits/templates/system-fragments/channel.md +6 -0
  59. package/kits/templates/system-fragments/session.md +2 -0
  60. package/package.json +2 -1
  61. package/skills/eclink/SKILL.md +15 -3
  62. package/skills/eclink/agents/openai.yaml +3 -3
@@ -5,7 +5,7 @@ import { logger } from '../utils/logger.js';
5
5
  import { agentPersonalDir } from '../paths.js';
6
6
  import { invalidateAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
7
7
  import { loadAllAgents, ensureAgentDirSkeleton, loadAgent, validateAgentConfig, } from '../config-store.js';
8
- import { resolveEffective } from '../config/config-manager.js';
8
+ import { ConfigTarget, read as readConfig, resolveEffective } from '../config/config-manager.js';
9
9
  // ── Channel Fingerprint ───────────────────────────────────────────────────
10
10
  // 用于检测多 agent 之间复用同一外部凭证的冲突(appId、aid、token 等)。
11
11
  // 格式:{type}:{primaryKey}
@@ -84,18 +84,20 @@ export class EvolAgentRegistry {
84
84
  this.skipped = [];
85
85
  const { agents: rawAgents, skipped, invalidAgents = [] } = loadAllAgents({ includeInvalid: true });
86
86
  this.skipped = skipped;
87
- for (const raw of rawAgents) {
87
+ for (const expandedRaw of rawAgents) {
88
88
  try {
89
- const merged = resolveEffective({ self: raw.aid });
89
+ const raw = readConfig(ConfigTarget.Agent, { self: expandedRaw.aid }, { cache: true }) ?? expandedRaw;
90
+ const merged = resolveEffective({ self: expandedRaw.aid }, { expand: true });
90
91
  const agent = new EvolAgent(raw, merged);
91
- ensureAgentDirSkeleton(raw.aid);
92
+ ensureAgentDirSkeleton(expandedRaw.aid);
92
93
  this.agents.set(agent.aid, agent);
93
94
  }
94
95
  catch (e) {
95
- logger.warn(`[EvolAgentRegistry] failed to construct agent ${raw.aid}: ${e}`);
96
+ logger.warn(`[EvolAgentRegistry] failed to construct agent ${expandedRaw.aid}: ${e}`);
96
97
  }
97
98
  }
98
- for (const { agent: raw, reason } of invalidAgents) {
99
+ for (const { agent: expandedRaw, reason } of invalidAgents) {
100
+ const raw = readConfig(ConfigTarget.Agent, { self: expandedRaw.aid }, { cache: true }) ?? expandedRaw;
99
101
  this.registerErrorAgent(raw, reason);
100
102
  }
101
103
  this.detectAndFlagConflicts();
@@ -119,7 +121,7 @@ export class EvolAgentRegistry {
119
121
  }
120
122
  resolveMergedForErrorAgent(raw, reason) {
121
123
  try {
122
- return resolveEffective({ self: raw.aid });
124
+ return resolveEffective({ self: raw.aid }, { expand: true });
123
125
  }
124
126
  catch (e) {
125
127
  const message = e instanceof Error ? e.message : String(e);
@@ -225,9 +227,9 @@ export class EvolAgentRegistry {
225
227
  logger.info(`[EvolAgentRegistry] agent ${aid} already loaded, skipping`);
226
228
  return this.agents.get(aid);
227
229
  }
228
- let raw = null;
230
+ let expandedRaw = null;
229
231
  try {
230
- raw = loadAgent(aid);
232
+ expandedRaw = loadAgent(aid);
231
233
  }
232
234
  catch (e) {
233
235
  const reason = e instanceof Error ? e.message : String(e);
@@ -239,22 +241,23 @@ export class EvolAgentRegistry {
239
241
  channels: [],
240
242
  }, reason);
241
243
  }
242
- if (!raw) {
244
+ if (!expandedRaw) {
243
245
  logger.warn(`[EvolAgentRegistry] loadNewAgent: ${aid}/config.json not found`);
244
246
  return null;
245
247
  }
246
- const errs = validateAgentConfig(raw);
248
+ const errs = validateAgentConfig(expandedRaw);
247
249
  if (errs.length > 0) {
248
250
  const reason = errs.join('; ');
249
251
  logger.warn(`[EvolAgentRegistry] loadNewAgent ${aid}: ${reason}`);
250
- return this.registerErrorAgent(raw, reason);
252
+ return this.registerErrorAgent(expandedRaw, reason);
251
253
  }
252
- const conflict = this.checkConflictForReload(raw, aid);
254
+ const conflict = this.checkConflictForReload(expandedRaw, aid);
253
255
  if (conflict) {
254
256
  logger.warn(`[EvolAgentRegistry] loadNewAgent ${aid}: ${conflict}`);
255
- return this.registerErrorAgent(raw, `Channel conflict: ${conflict}`);
257
+ return this.registerErrorAgent(expandedRaw, `Channel conflict: ${conflict}`);
256
258
  }
257
- const merged = resolveEffective({ self: aid });
259
+ const raw = readConfig(ConfigTarget.Agent, { self: aid }, { cache: true }) ?? expandedRaw;
260
+ const merged = resolveEffective({ self: aid }, { expand: true });
258
261
  const agent = new EvolAgent(raw, merged);
259
262
  ensureAgentDirSkeleton(aid);
260
263
  this.agents.set(aid, agent);
@@ -268,13 +271,14 @@ export class EvolAgentRegistry {
268
271
  const oldAgent = this.agents.get(aidOrName);
269
272
  if (!oldAgent)
270
273
  throw new Error(`Agent "${aidOrName}" not found`);
271
- const raw = loadAgent(oldAgent.aid);
272
- if (!raw)
274
+ const expandedRaw = loadAgent(oldAgent.aid);
275
+ if (!expandedRaw)
273
276
  throw new Error(`Agent ${oldAgent.aid}/config.json missing on reload`);
274
- const errs = validateAgentConfig(raw);
277
+ const errs = validateAgentConfig(expandedRaw);
275
278
  if (errs.length > 0)
276
279
  throw new Error(`Invalid config after edit: ${errs.join('; ')}`);
277
- const merged = resolveEffective({ self: raw.aid });
280
+ const raw = readConfig(ConfigTarget.Agent, { self: oldAgent.aid }, { cache: true }) ?? expandedRaw;
281
+ const merged = resolveEffective({ self: raw.aid }, { expand: true });
278
282
  if (oldAgent.status === 'disabled' && raw.enabled !== false) {
279
283
  oldAgent.swapConfig(raw, merged);
280
284
  const hotLoad = globalThis.__evolcore_hotLoadAgent;
@@ -289,32 +293,69 @@ export class EvolAgentRegistry {
289
293
  }
290
294
  if (oldAgent.status !== 'disabled' && raw.enabled === false) {
291
295
  let prepared = false;
296
+ let runnerTransaction;
297
+ const disconnectedChannels = [];
298
+ const previousConfig = oldAgent.captureConfigSnapshot();
299
+ const previousStatus = oldAgent.status;
300
+ const previousError = oldAgent.error;
292
301
  try {
293
302
  await hooks.prepareHandoffReload?.(oldAgent.aid);
294
303
  prepared = true;
304
+ runnerTransaction = await hooks.stageAgentRunners?.(new EvolAgent(raw, merged));
295
305
  for (const ch of oldAgent.channelInstanceNames()) {
296
306
  try {
297
307
  await hooks.drainChannel(ch);
298
308
  }
299
309
  catch { }
300
- try {
301
- await hooks.disconnectChannel(ch);
302
- }
303
- catch { }
310
+ await hooks.disconnectChannel(ch);
311
+ disconnectedChannels.push(ch);
304
312
  }
305
313
  oldAgent.swapConfig(raw, merged);
306
314
  oldAgent.status = 'disabled';
307
315
  this.channelIndex.clear();
308
316
  this.buildChannelIndex();
317
+ runnerTransaction?.commit();
318
+ try {
319
+ await runnerTransaction?.finalize();
320
+ }
321
+ catch (error) {
322
+ logger.warn(`[Reload] Failed to dispose previous runners after disabling "${aidOrName}": ${error instanceof Error ? error.message : String(error)}`);
323
+ }
324
+ prepared = false;
309
325
  logger.info(`[Reload] Agent "${aidOrName}" disabled`);
310
326
  return;
311
327
  }
312
328
  catch (error) {
329
+ const rollbackErrors = [];
330
+ oldAgent.swapConfig(previousConfig.rawAgent, previousConfig.merged);
331
+ oldAgent.invalidatePersonaCache();
332
+ oldAgent.status = previousStatus;
333
+ oldAgent.error = previousError;
334
+ await runnerTransaction?.rollback().catch(rollbackError => {
335
+ rollbackErrors.push(`runners: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
336
+ });
337
+ for (const ch of disconnectedChannels) {
338
+ try {
339
+ await hooks.startChannel(oldAgent, ch);
340
+ }
341
+ catch (restartError) {
342
+ rollbackErrors.push(`restart ${ch}: ${restartError instanceof Error ? restartError.message : String(restartError)}`);
343
+ }
344
+ }
345
+ this.channelIndex.clear();
346
+ this.buildChannelIndex();
313
347
  if (prepared) {
314
348
  try {
315
349
  await hooks.completeHandoffReload?.(oldAgent.aid);
316
350
  }
317
- catch { }
351
+ catch (resumeError) {
352
+ rollbackErrors.push(`handoff: ${resumeError instanceof Error ? resumeError.message : String(resumeError)}`);
353
+ }
354
+ }
355
+ if (rollbackErrors.length > 0) {
356
+ oldAgent.status = 'error';
357
+ oldAgent.error = `Disable failed; rollback incomplete: ${rollbackErrors.join('; ')}`;
358
+ logger.error(`[Reload] Disable rollback incomplete for "${aidOrName}": ${rollbackErrors.join('; ')}`);
318
359
  }
319
360
  throw error;
320
361
  }
@@ -324,13 +365,18 @@ export class EvolAgentRegistry {
324
365
  throw new Error(`Channel conflict: ${conflict}`);
325
366
  const removedSuccessfully = [];
326
367
  const addedSuccessfully = [];
368
+ let runnerTransaction;
327
369
  let prepared = false;
370
+ const previousConfig = oldAgent.captureConfigSnapshot();
371
+ const previousStatus = oldAgent.status;
372
+ const previousError = oldAgent.error;
328
373
  try {
329
374
  await hooks.prepareHandoffReload?.(oldAgent.aid);
330
375
  prepared = true;
376
+ runnerTransaction = await hooks.stageAgentRunners?.(new EvolAgent(raw, merged));
331
377
  const oldChannels = new Set(oldAgent.channelInstanceNames());
332
378
  const aunKey = oldAgent.effectiveChannelName('aun', 'main');
333
- const otherKeys = raw.channels.filter(c => c.type !== 'aun').map(c => oldAgent.effectiveChannelName(c.type, c.name));
379
+ const otherKeys = merged.channels.filter(c => c.type !== 'aun').map(c => oldAgent.effectiveChannelName(c.type, c.name));
334
380
  const newChannels = new Set([aunKey, ...otherKeys]);
335
381
  const toRemove = [...oldChannels].filter(c => !newChannels.has(c));
336
382
  const toAdd = [...newChannels].filter(c => !oldChannels.has(c));
@@ -338,7 +384,7 @@ export class EvolAgentRegistry {
338
384
  const credentialsChanged = [];
339
385
  for (const ch of kept) {
340
386
  const oldInst = oldAgent.findChannelInstance(ch);
341
- const newInst = findInstanceByKey(raw, oldAgent, ch);
387
+ const newInst = findInstanceByKey(merged, oldAgent, ch);
342
388
  if (oldInst && newInst && JSON.stringify(oldInst) !== JSON.stringify(newInst)) {
343
389
  credentialsChanged.push(ch);
344
390
  }
@@ -360,28 +406,72 @@ export class EvolAgentRegistry {
360
406
  oldAgent.status = 'running';
361
407
  this.channelIndex.clear();
362
408
  this.buildChannelIndex();
363
- await hooks.completeHandoffReload?.(oldAgent.aid);
364
- prepared = false;
409
+ runnerTransaction?.commit();
410
+ try {
411
+ await hooks.completeHandoffReload?.(oldAgent.aid);
412
+ }
413
+ catch (error) {
414
+ logger.warn(`[Reload] Handoff recovery failed after commit for "${aidOrName}": ${error instanceof Error ? error.message : String(error)}`);
415
+ }
416
+ finally {
417
+ prepared = false;
418
+ }
419
+ try {
420
+ await hooks.afterReload?.(oldAgent);
421
+ }
422
+ catch (error) {
423
+ logger.warn(`[Reload] Post-reload hook failed after commit for "${aidOrName}": ${error instanceof Error ? error.message : String(error)}`);
424
+ }
425
+ try {
426
+ await runnerTransaction?.finalize();
427
+ }
428
+ catch (error) {
429
+ logger.warn(`[Reload] Failed to dispose previous runners for "${aidOrName}": ${error instanceof Error ? error.message : String(error)}`);
430
+ }
365
431
  }
366
432
  catch (err) {
367
- logger.error(`[Reload] Failed: ${err}. Attempting rollback for "${aidOrName}".`);
368
- for (const ch of addedSuccessfully) {
433
+ logger.error(`[Reload] Failed before commit: ${err}. Attempting rollback for "${aidOrName}".`);
434
+ const rollbackErrors = [];
435
+ for (const ch of [...addedSuccessfully].reverse()) {
369
436
  try {
370
437
  await hooks.disconnectChannel(ch);
371
438
  }
372
- catch { }
439
+ catch (error) {
440
+ rollbackErrors.push(`disconnect ${ch}: ${error instanceof Error ? error.message : String(error)}`);
441
+ }
373
442
  }
443
+ oldAgent.swapConfig(previousConfig.rawAgent, previousConfig.merged);
444
+ oldAgent.invalidatePersonaCache();
445
+ await runnerTransaction?.rollback().catch(error => {
446
+ rollbackErrors.push(`runners: ${error instanceof Error ? error.message : String(error)}`);
447
+ });
448
+ for (const ch of [...new Set(removedSuccessfully)]) {
449
+ try {
450
+ await hooks.startChannel(oldAgent, ch);
451
+ }
452
+ catch (error) {
453
+ rollbackErrors.push(`restart ${ch}: ${error instanceof Error ? error.message : String(error)}`);
454
+ }
455
+ }
456
+ this.channelIndex.clear();
457
+ this.buildChannelIndex();
374
458
  if (prepared) {
375
459
  try {
376
460
  await hooks.completeHandoffReload?.(oldAgent.aid);
377
461
  }
378
462
  catch (resumeError) {
379
- logger.error(`[Reload] Failed to resume Handoff dispatcher for "${aidOrName}":`, resumeError);
463
+ rollbackErrors.push(`handoff: ${resumeError instanceof Error ? resumeError.message : String(resumeError)}`);
380
464
  }
381
465
  }
382
- void removedSuccessfully;
383
- oldAgent.status = 'error';
384
- oldAgent.error = `Reload failed (rollback partial): ${err instanceof Error ? err.message : String(err)}`;
466
+ if (rollbackErrors.length === 0) {
467
+ oldAgent.status = previousStatus;
468
+ oldAgent.error = previousError;
469
+ }
470
+ else {
471
+ oldAgent.status = 'error';
472
+ oldAgent.error = `Reload failed; rollback incomplete: ${rollbackErrors.join('; ')}`;
473
+ logger.error(`[Reload] Rollback incomplete for "${aidOrName}": ${rollbackErrors.join('; ')}`);
474
+ }
385
475
  throw err;
386
476
  }
387
477
  }
@@ -52,7 +52,7 @@ export class EvolAgent {
52
52
  const ba = this.baseagent;
53
53
  // 动态读取配置(基于 fileCache + mtime),使配置变更立即体现在显示中
54
54
  try {
55
- const effective = resolveEffective({ self: this.aid }, { cache: true });
55
+ const effective = resolveEffective({ self: this.aid }, { cache: true, expand: true });
56
56
  const block = effective.baseagents?.[ba];
57
57
  return block?.model;
58
58
  }
@@ -66,7 +66,7 @@ export class EvolAgent {
66
66
  const ba = this.baseagent;
67
67
  // 动态读取配置(基于 fileCache + mtime),使配置变更立即体现在显示中
68
68
  try {
69
- const effective = resolveEffective({ self: this.aid }, { cache: true });
69
+ const effective = resolveEffective({ self: this.aid }, { cache: true, expand: true });
70
70
  const block = effective.baseagents?.[ba];
71
71
  if (ba === 'codex')
72
72
  return block?.effort ?? block?.reasoning;
@@ -257,7 +257,8 @@ export class EvolAgent {
257
257
  this.persist();
258
258
  }
259
259
  setLifecycle(value) {
260
- this.rawAgent = withLifecycleForWrite(this.rawAgent, value);
260
+ const current = cfgRead(ConfigTarget.Agent, { self: this.aid });
261
+ this.rawAgent = withLifecycleForWrite(current || this.rawAgent, value);
261
262
  this.rawAgent.$schema_version = Math.max(this.rawAgent.$schema_version || 0, this.merged.$schema_version || 0);
262
263
  this.merged.lifecycle = value;
263
264
  delete this.merged.initialized;
@@ -318,6 +319,10 @@ export class EvolAgent {
318
319
  this.rawAgent = rawAgent;
319
320
  this.merged = merged;
320
321
  }
322
+ /** Reload uses the current object references as an in-memory rollback snapshot. */
323
+ captureConfigSnapshot() {
324
+ return { rawAgent: this.rawAgent, merged: this.merged };
325
+ }
321
326
  // ── 内部辅助 ─────────────────────────────────────────────────────────
322
327
  /**
323
328
  * 找 rawAgent.channels 里的可变实例,用于写入。
@@ -1,4 +1,5 @@
1
1
  import { normalizeBaseagent, resolveAnthropicDirectConfig, resolveEcagentConfig, resolveOpenaiDirectConfig, } from '../../agents/baseagent.js';
2
+ import { buildModelRequestHeaders } from '../../agents/request-identity.js';
2
3
  function apiEndpoint(baseUrl, resource) {
3
4
  const configured = baseUrl?.trim();
4
5
  if (!configured)
@@ -119,12 +120,25 @@ export class EcagentTextInferenceProvider {
119
120
  baseagent = 'ecagent';
120
121
  constructor(config) {
121
122
  this.config = config;
123
+ this.config = {
124
+ ...config,
125
+ headers: buildModelRequestHeaders({
126
+ baseagent: 'ecagent',
127
+ baseUrl: config.baseUrl,
128
+ agentAid: config.evolcoreAgentAid,
129
+ configuredHeaders: config.headers,
130
+ }),
131
+ };
122
132
  }
123
133
  async completeText(request) {
124
134
  const response = await fetch(apiEndpoint(this.config.baseUrl, 'chat/completions'), {
125
135
  method: 'POST',
126
136
  signal: request.signal,
127
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.config.apiKey}` },
137
+ headers: {
138
+ 'Content-Type': 'application/json',
139
+ Authorization: `Bearer ${this.config.apiKey}`,
140
+ ...this.config.headers,
141
+ },
128
142
  body: JSON.stringify({
129
143
  model: request.model,
130
144
  messages: [{ role: 'system', content: request.system }, { role: 'user', content: request.input }],
@@ -150,15 +164,35 @@ export function createTextInferenceProvider(baseagent, config) {
150
164
  const canonical = normalizeBaseagent(baseagent).canonical;
151
165
  if (canonical === 'claude') {
152
166
  const resolved = resolveAnthropicDirectConfig(config.baseagents?.claude);
153
- return resolved ? new AnthropicTextInferenceProvider(resolved) : undefined;
167
+ return resolved ? new AnthropicTextInferenceProvider({
168
+ ...resolved,
169
+ headers: buildModelRequestHeaders({
170
+ baseagent: 'claude',
171
+ baseUrl: resolved.baseUrl,
172
+ agentAid: config.aid,
173
+ configuredHeaders: resolved.headers,
174
+ }),
175
+ }) : undefined;
154
176
  }
155
177
  if (canonical === 'codex') {
156
178
  const resolved = resolveOpenaiDirectConfig(config.baseagents?.codex);
157
- return resolved ? new OpenAITextInferenceProvider(resolved) : undefined;
179
+ return resolved ? new OpenAITextInferenceProvider({
180
+ ...resolved,
181
+ headers: buildModelRequestHeaders({
182
+ baseagent: 'codex',
183
+ baseUrl: resolved.baseUrl,
184
+ agentAid: config.aid,
185
+ configuredHeaders: resolved.headers,
186
+ }),
187
+ }) : undefined;
158
188
  }
159
189
  if (canonical === 'ecagent') {
160
190
  try {
161
- const resolved = resolveEcagentConfig({ agents: { ecagent: config.baseagents?.ecagent } }, config.baseagents?.ecagent);
191
+ const override = {
192
+ ...(config.baseagents?.ecagent ?? {}),
193
+ evolcoreAgentAid: config.aid,
194
+ };
195
+ const resolved = resolveEcagentConfig({ agents: { ecagent: override } }, override);
162
196
  return new EcagentTextInferenceProvider(resolved);
163
197
  }
164
198
  catch {
@@ -760,7 +760,7 @@ export class MessageBridge {
760
760
  channelKey,
761
761
  channelType: msg.channelType || effectiveChannelType,
762
762
  channelId: msg.channelId,
763
- recipientId: actorId,
763
+ recipientId: roleDetail.actor.principalId || actorId,
764
764
  recipientName: msg.peerName,
765
765
  source: 'inbound',
766
766
  });
@@ -229,6 +229,27 @@ function formatTimestampMs(epochMs) {
229
229
  export function messageLogPath(chatDir) {
230
230
  return path.join(chatDir, MESSAGE_LOG_FILE);
231
231
  }
232
+ export function hasMessageLogOperation(chatDir, operationId) {
233
+ const file = messageLogPath(chatDir);
234
+ if (!fs.existsSync(file))
235
+ return false;
236
+ try {
237
+ return fs.readFileSync(file, 'utf-8').split('\n').some(line => {
238
+ if (!line.trim())
239
+ return false;
240
+ try {
241
+ const entry = JSON.parse(line);
242
+ return entry?.dir === 'out' && entry.operationId === operationId;
243
+ }
244
+ catch {
245
+ return false;
246
+ }
247
+ });
248
+ }
249
+ catch {
250
+ return false;
251
+ }
252
+ }
232
253
  export function resolveChatDir(sessionsDir, channelType, channelId, selfAID) {
233
254
  return chatDirPath(sessionsDir, channelType, channelId, selfAID);
234
255
  }
@@ -323,6 +344,7 @@ export function buildOutboundEntry(opts) {
323
344
  transport: opts.transport,
324
345
  peerType: opts.peerType,
325
346
  source: opts.source ?? 'daemon',
347
+ operationId: opts.operationId,
326
348
  handoff_trace: opts.handoff_trace,
327
349
  };
328
350
  }
@@ -1361,9 +1361,9 @@ export class MessageQueue {
1361
1361
  isAgentMuted(agentName) {
1362
1362
  return this.mutedAgents.has(agentName);
1363
1363
  }
1364
- /** 中断指定 agent 所有正在处理中的会话。 */
1365
- interruptByAgent(agentName) {
1364
+ beginAgentInterrupt(agentName) {
1366
1365
  let interrupted = 0;
1366
+ const barriers = [];
1367
1367
  for (const [queueKey, name] of this.processingAgent) {
1368
1368
  if ((name || DEFAULT_AGENT_NAME) === agentName) {
1369
1369
  interrupted++;
@@ -1378,11 +1378,26 @@ export class MessageQueue {
1378
1378
  causation: this.activeBatches.get(queueKey)?.message.causation,
1379
1379
  });
1380
1380
  if (this.interruptCallback) {
1381
- void this.triggerInterrupt(queueKey, sessionKey, activeState?.baseagent, name, 'stop');
1381
+ barriers.push(this.triggerInterrupt(queueKey, sessionKey, activeState?.baseagent, name, 'stop'));
1382
1382
  }
1383
1383
  }
1384
1384
  }
1385
- return interrupted;
1385
+ return {
1386
+ count: interrupted,
1387
+ barrier: Promise.allSettled(barriers).then(() => undefined),
1388
+ };
1389
+ }
1390
+ /** 中断指定 agent 所有正在处理中的会话。 */
1391
+ interruptByAgent(agentName) {
1392
+ const interruption = this.beginAgentInterrupt(agentName);
1393
+ void interruption.barrier;
1394
+ return interruption.count;
1395
+ }
1396
+ /** 中断指定 agent 的会话,并等待 runner 中断屏障完成。 */
1397
+ async interruptByAgentAndWait(agentName) {
1398
+ const interruption = this.beginAgentInterrupt(agentName);
1399
+ await interruption.barrier;
1400
+ return interruption.count;
1386
1401
  }
1387
1402
  // ── Queue query/management methods ──
1388
1403
  /**