evolcore 0.0.10 → 0.0.12

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 (131) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +3 -3
  3. package/bin/codex-managed-hook.mjs +80 -0
  4. package/dist/agents/baseagent.js +4 -0
  5. package/dist/agents/claude-runner.js +158 -44
  6. package/dist/agents/codex-app-server-client.js +153 -23
  7. package/dist/agents/codex-runner.js +331 -43
  8. package/dist/agents/ecagent-runner.js +53 -6
  9. package/dist/agents/gemini-runner.js +1 -0
  10. package/dist/agents/request-identity.js +55 -0
  11. package/dist/aun/aid/client.js +11 -21
  12. package/dist/aun/aid/managed-operation.js +107 -0
  13. package/dist/aun/msg/group.js +81 -12
  14. package/dist/aun/msg/managed-operation.js +804 -0
  15. package/dist/aun/msg/mention-schema.js +20 -0
  16. package/dist/aun/msg/p2p.js +7 -0
  17. package/dist/aun/outbox.js +28 -31
  18. package/dist/channels/aun.js +418 -235
  19. package/dist/channels/daemon.js +13 -0
  20. package/dist/cli/agent-command.js +72 -17
  21. package/dist/cli/agent.js +249 -22
  22. package/dist/cli/aun-commands.js +270 -69
  23. package/dist/cli/bench.js +12 -3
  24. package/dist/cli/daemon-commands.js +52 -33
  25. package/dist/cli/fs-command.js +60 -0
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +106 -63
  28. package/dist/cli/init-cancel.js +208 -0
  29. package/dist/cli/init-channel.js +343 -195
  30. package/dist/cli/init.js +21 -9
  31. package/dist/cli/managed-command-guard.js +41 -0
  32. package/dist/cli/model.js +11 -4
  33. package/dist/cli/queue-command.js +11 -1
  34. package/dist/cli/response.js +71 -0
  35. package/dist/cli/restart-monitor.js +4 -18
  36. package/dist/cli/trigger-command.js +4 -0
  37. package/dist/config/builtin-roles.js +5 -0
  38. package/dist/config/contact-book-store.js +11 -3
  39. package/dist/config/contact-request-service.js +161 -15
  40. package/dist/config/gateway-config.js +26 -10
  41. package/dist/config/mention-mode.js +8 -24
  42. package/dist/core/agent-reload-coordinator.js +53 -0
  43. package/dist/core/auth/agent-delegation.js +0 -1
  44. package/dist/core/auth/operation-authorizer.js +62 -154
  45. package/dist/core/auth/operation-catalog.js +264 -22
  46. package/dist/core/bootstrap-messages.js +68 -0
  47. package/dist/core/bootstrap-service.js +144 -10
  48. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  49. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  50. package/dist/core/capability/skill-discovery.js +55 -0
  51. package/dist/core/channel-loader.js +24 -7
  52. package/dist/core/command/agent-control.js +14 -11
  53. package/dist/core/command/cli-intent-parser.js +9 -0
  54. package/dist/core/command/command-handler.js +467 -6
  55. package/dist/core/command/connect-menu.js +15 -0
  56. package/dist/core/command/group-menu.js +4 -4
  57. package/dist/core/command/menu-catalog.js +366 -0
  58. package/dist/core/command/menu-handler.js +213 -110
  59. package/dist/core/command/menu-protocol.js +71 -0
  60. package/dist/core/command/slash-gate.js +5 -2
  61. package/dist/core/command/slash-handler.js +45 -25
  62. package/dist/core/daemon-file-cache.js +2 -1
  63. package/dist/core/data-migration.js +9 -2
  64. package/dist/core/evolagent-registry.js +125 -35
  65. package/dist/core/evolagent.js +12 -5
  66. package/dist/core/handoff/dispatcher.js +5 -2
  67. package/dist/core/handoff/runtime.js +145 -21
  68. package/dist/core/handoff/store.js +24 -0
  69. package/dist/core/handoff/types.js +1 -0
  70. package/dist/core/inference/text-inference.js +38 -4
  71. package/dist/core/message/logical-queue-bridge.js +1 -0
  72. package/dist/core/message/mention-schema.js +126 -0
  73. package/dist/core/message/message-bridge.js +237 -79
  74. package/dist/core/message/message-log.js +23 -0
  75. package/dist/core/message/message-queue.js +20 -4
  76. package/dist/core/message/response-engine.js +54 -36
  77. package/dist/core/model/model-catalog.js +143 -24
  78. package/dist/core/model/model-diagnostics.js +28 -10
  79. package/dist/core/permission/ec-command-parser.js +156 -10
  80. package/dist/core/permission/index.js +1 -0
  81. package/dist/core/permission/readonly-shell-query.js +532 -0
  82. package/dist/core/permission/sandbox-runtime.js +22 -1
  83. package/dist/core/permission/shell-environment.js +46 -0
  84. package/dist/core/permission/tool-policy.js +292 -111
  85. package/dist/core/protected-paths.js +17 -9
  86. package/dist/core/runner-reload-transaction.js +57 -0
  87. package/dist/core/session/session-manager.js +21 -2
  88. package/dist/eck/manifest-engine.js +39 -13
  89. package/dist/index.js +644 -180
  90. package/dist/ipc.js +246 -19
  91. package/dist/response-system/coordinator.js +6 -4
  92. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  93. package/dist/response-system/modes/single-session/index.js +14 -1
  94. package/dist/response-system/selector.js +6 -5
  95. package/dist/trigger/anomaly-store.js +4 -0
  96. package/dist/trigger/script-executor.js +1 -0
  97. package/dist/utils/aid-bind.js +3 -8
  98. package/dist/utils/codex-app-server-registry.js +90 -0
  99. package/dist/utils/codex-cli.js +5 -1
  100. package/dist/utils/cross-platform.js +15 -0
  101. package/dist/utils/log-writer.js +6 -10
  102. package/dist/utils/logger.js +5 -5
  103. package/dist/utils/npm-ops.js +5 -12
  104. package/dist/utils/tool-summary.js +11 -0
  105. package/kits/docs/channels/aun.md +1 -1
  106. package/kits/docs/context-assembly.md +2 -2
  107. package/kits/docs/evolcore/agent.md +11 -6
  108. package/kits/docs/evolcore/aid.md +14 -0
  109. package/kits/docs/evolcore/msg.md +13 -0
  110. package/kits/docs/prompt-loading-architecture.md +1 -2
  111. package/kits/docs/venues/group.md +1 -1
  112. package/kits/eck_manifest.json +0 -12
  113. package/kits/rules/01-overview.md +9 -0
  114. package/kits/rules/04-relation.md +1 -1
  115. package/kits/rules/05-venue.md +2 -2
  116. package/kits/schemas/agent-config.schema.3.json +1 -1
  117. package/kits/schemas/agent-config.schema.4.json +1 -1
  118. package/kits/schemas/relation-config.schema.2.json +1 -1
  119. package/kits/schemas/role-config.schema.1.json +1 -1
  120. package/kits/schemas/single-session.schema.1.json +3 -3
  121. package/kits/schemas/single-session.schema.2.json +3 -3
  122. package/kits/templates/roles/admin.json +27 -0
  123. package/kits/templates/roles/member.json +44 -0
  124. package/kits/templates/roles/visitor.json +40 -2
  125. package/kits/templates/system-fragments/bootstrap.md +12 -6
  126. package/kits/templates/system-fragments/channel.md +6 -0
  127. package/kits/templates/system-fragments/session.md +2 -0
  128. package/package.json +3 -2
  129. package/skills/eclink/SKILL.md +15 -3
  130. package/skills/eclink/agents/openai.yaml +3 -3
  131. package/dist/core/command/evol-menu-version-gate.js +0 -39
@@ -8,7 +8,7 @@ import { formatChannelKey, tryParseChannelKey } from '../channel-loader.js';
8
8
  import { agentDir, resolvePaths } from '../../paths.js';
9
9
  import { listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, } from '../../config/peer-role-resolver.js';
10
10
  import { resolveContactView, resolvePeerDisplayLabel, resolvePrincipal } from '../../config/contact-book.js';
11
- import { findContactRequestNote, expirePendingContactRequests, reviewContactRequest, submitContactRequest, } from '../../config/contact-request-service.js';
11
+ import { findContactRequestNote, expirePendingContactRequests, reviewContactRequest, submitContactRequest, contactRequestSubmissionProtocolResult, queryOwnContactRequestStatus, } from '../../config/contact-request-service.js';
12
12
  import { isValidAid } from '../../aun/aid/validation.js';
13
13
  import { PeerIdentityCache } from '../relation/peer-identity.js';
14
14
  import { handlePendingDingtalkContactBindMessage, registerPendingDingtalkContactBind } from '../../channels/dingtalk.js';
@@ -17,8 +17,8 @@ import { handlePendingQQBotContactBindMessage, registerPendingQQBotContactBind }
17
17
  import { handlePendingWecomContactBindMessage, registerPendingWecomContactBind } from '../../channels/wecom.js';
18
18
  import { handlePendingWechatContactBindMessage, registerPendingWechatContactBind } from '../../channels/wechat.js';
19
19
  import { authorizeAccess, buildAuthSubject } from '../auth/auth-gateway.js';
20
- import { MenuDiagnosticLimiter, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, menuSuccess, normalizeMenuError, parseMenuControl, validateMenuRequest, } from '../command/menu-protocol.js';
21
- import { evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, isAunMenuTokenRequired } from '../command/evol-menu-version-gate.js';
20
+ import { MenuDiagnosticLimiter, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, menuSuccess, normalizeMenuError, parseMenuControl, validateMenuRequest, evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, isAunMenuTokenRequired, menuCommandForName, } from '../command/menu-protocol.js';
21
+ import { resolveExplicitMenuExecutionContext, validateMenuCatalogArgs, validateMenuValueBatchArgs } from '../command/menu-catalog.js';
22
22
  import { MenuTokenStore } from '../command/menu-token-store.js';
23
23
  import { SessionRenewService } from '../session/session-renew.js';
24
24
  import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
@@ -306,42 +306,171 @@ export class MessageBridge {
306
306
  return true;
307
307
  }
308
308
  }
309
+ return false;
310
+ }
311
+ async handleContactRequestProtocol(input) {
312
+ const { context, msg, content, owningAgent, adapter, sendReply } = input;
309
313
  const requestMatch = /^\s*\/request(?:\s+([\s\S]*?))?\s*$/i.exec(content);
310
- if (!requestMatch)
314
+ let protocolType;
315
+ let protocolPayload;
316
+ try {
317
+ const parsed = JSON.parse(content);
318
+ protocolPayload = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
319
+ ? parsed
320
+ : undefined;
321
+ protocolType = protocolPayload?.type === 'contact.request.query'
322
+ ? 'query'
323
+ : protocolPayload?.type === 'contact.request.submit' ? 'submit' : undefined;
324
+ }
325
+ catch { /* slash command */ }
326
+ if (!requestMatch && !protocolType)
311
327
  return false;
312
- if (context.channelType !== 'aun') {
313
- await this.sendAdmissionControlReply(context, msg, sendReply, '联系人申请仅支持 AUN 私聊。');
328
+ // Slash commands keep their human-facing response semantics. Structured
329
+ // App requests use contact.request.response and never fall through to the
330
+ // normal command/session pipeline.
331
+ if (requestMatch) {
332
+ // Blacklist remains the first gate even though slash controls are
333
+ // recognized before ordinary admission. Let the normal blocked path
334
+ // decide whether an agent/bot/service gets a limited rejection.
335
+ if (context.contactStatus === 'blocked')
336
+ return false;
337
+ if (context.channelType !== 'aun') {
338
+ await this.sendAdmissionControlReply(context, msg, sendReply, '联系人申请仅支持 AUN 私聊。');
339
+ return true;
340
+ }
341
+ if (context.chatType !== 'private') {
342
+ await this.sendAdmissionControlReply(context, msg, sendReply, '联系人申请仅支持 AUN 私聊。');
343
+ return true;
344
+ }
345
+ if (!context.primaryId || context.principal?.principalStatus !== 'native_aid') {
346
+ await this.sendAdmissionControlReply(context, msg, sendReply, '无法验证当前 AUN 身份,未提交联系人申请。');
347
+ return true;
348
+ }
349
+ try {
350
+ const submission = await submitContactRequest({
351
+ selfAid: context.selfAid,
352
+ applicantAid: context.primaryId,
353
+ note: requestMatch[1],
354
+ sourceChannelKey: context.channelKey,
355
+ });
356
+ if ((submission.code === 'submitted' || submission.code === 'resubmitted') && submission.requestId) {
357
+ void this.deliverContactReviewCard(context.selfAid, context.primaryId, submission, owningAgent)
358
+ .catch(error => logger.warn(`[MessageBridge] Contact review delivery failed: ${error instanceof Error ? error.message : String(error)}`));
359
+ }
360
+ await this.sendAdmissionControlReply(context, msg, sendReply, this.contactRequestReply(submission));
361
+ }
362
+ catch (error) {
363
+ logger.warn(`[MessageBridge] Contact request submission failed: ${error instanceof Error ? error.message : String(error)}`);
364
+ await this.sendAdmissionControlReply(context, msg, sendReply, '当前无法受理联系人申请,请稍后再试。');
365
+ }
314
366
  return true;
315
367
  }
316
- if (context.chatType !== 'private') {
317
- await this.sendAdmissionControlReply(context, msg, sendReply, '联系人申请仅支持 AUN 私聊。');
368
+ // The structured protocol is AUN-only. For a blocked external alias,
369
+ // return false so the existing blacklist response/silence policy applies;
370
+ // never emit a custom payload through a non-AUN adapter.
371
+ if (context.contactStatus === 'blocked' && context.channelType !== 'aun')
372
+ return false;
373
+ if (context.channelType !== 'aun')
374
+ return true;
375
+ if (context.chatType !== 'private')
376
+ return true;
377
+ if (context.principal?.principalStatus !== 'native_aid' || !context.primaryId) {
378
+ await this.sendContactRequestProtocolResponse(adapter, msg, {
379
+ status: 'NOT_ALLOWED', requestId: null, expiresAt: null,
380
+ }, owningAgent?.name);
318
381
  return true;
319
382
  }
320
- if (!context.primaryId) {
321
- await this.sendAdmissionControlReply(context, msg, sendReply, '无法验证当前 AUN 身份,未提交联系人申请。');
383
+ if (context.contactStatus === 'blocked') {
384
+ await this.sendContactRequestProtocolResponse(adapter, msg, {
385
+ status: 'NOT_ALLOWED', requestId: null, expiresAt: null,
386
+ }, owningAgent?.name);
322
387
  return true;
323
388
  }
324
389
  let result;
325
- try {
326
- result = await submitContactRequest({
327
- selfAid: context.selfAid,
328
- applicantAid: context.primaryId,
329
- note: requestMatch[1],
330
- sourceChannelKey: context.channelKey,
331
- });
332
- }
333
- catch (error) {
334
- logger.warn(`[MessageBridge] Contact request submission failed: self=${context.selfAid} applicant=${context.primaryId} error=${error instanceof Error ? error.message : String(error)}`);
335
- await this.sendAdmissionControlReply(context, msg, sendReply, '当前无法受理联系人申请,请稍后再试。');
336
- return true;
390
+ if (protocolType === 'query') {
391
+ try {
392
+ result = await queryOwnContactRequestStatus({
393
+ selfAid: context.selfAid,
394
+ applicantAid: context.primaryId,
395
+ requestId: protocolPayload && Object.prototype.hasOwnProperty.call(protocolPayload, 'request_id')
396
+ ? protocolPayload.request_id
397
+ : undefined,
398
+ });
399
+ }
400
+ catch (error) {
401
+ logger.warn(`[MessageBridge] Contact request query failed: ${error instanceof Error ? error.message : String(error)}`);
402
+ result = { status: 'NOT_ALLOWED', requestId: null, expiresAt: null };
403
+ }
337
404
  }
338
- if ((result.code === 'submitted' || result.code === 'resubmitted') && result.requestId) {
339
- void this.deliverContactReviewCard(context.selfAid, context.primaryId, result, owningAgent)
340
- .catch(error => logger.warn(`[MessageBridge] Contact review delivery failed: self=${context.selfAid} applicant=${context.primaryId} error=${error instanceof Error ? error.message : String(error)}`));
405
+ else {
406
+ try {
407
+ if (protocolPayload && Object.prototype.hasOwnProperty.call(protocolPayload, 'note')
408
+ && typeof protocolPayload?.note !== 'string') {
409
+ await this.sendContactRequestProtocolResponse(adapter, msg, {
410
+ status: 'NOT_ALLOWED', requestId: null, expiresAt: null,
411
+ }, owningAgent?.name);
412
+ return true;
413
+ }
414
+ const submission = await submitContactRequest({
415
+ selfAid: context.selfAid,
416
+ applicantAid: context.primaryId,
417
+ note: typeof protocolPayload?.note === 'string' ? protocolPayload.note : undefined,
418
+ sourceChannelKey: context.channelKey,
419
+ });
420
+ result = contactRequestSubmissionProtocolResult(submission);
421
+ if ((submission.code === 'submitted' || submission.code === 'resubmitted') && submission.requestId) {
422
+ void this.deliverContactReviewCard(context.selfAid, context.primaryId, submission, owningAgent)
423
+ .catch(error => logger.warn(`[MessageBridge] Contact review delivery failed: ${error instanceof Error ? error.message : String(error)}`));
424
+ }
425
+ }
426
+ catch (error) {
427
+ logger.warn(`[MessageBridge] Contact request submission failed: ${error instanceof Error ? error.message : String(error)}`);
428
+ result = { status: 'NOT_ALLOWED', requestId: null, expiresAt: null };
429
+ }
341
430
  }
342
- await this.sendAdmissionControlReply(context, msg, sendReply, this.contactRequestReply(result));
431
+ await this.sendContactRequestProtocolResponse(adapter, msg, result, owningAgent?.name);
343
432
  return true;
344
433
  }
434
+ contactRequestReply(result) {
435
+ switch (result.code) {
436
+ case 'submitted': return '联系人申请已提交,等待管理员审核。';
437
+ case 'resubmitted': return '联系人申请已更新,等待管理员审核。';
438
+ case 'already-contact': return '你已经是联系人。';
439
+ case 'owner': return '你已经是 Owner。';
440
+ case 'blocked': return CONTACT_REJECTION_NOTICE;
441
+ case 'rate-limited': return '联系人申请过于频繁,请稍后再试。';
442
+ case 'capacity': return '当前申请较多,请稍后再试。';
443
+ case 'no-owner': return '当前无法受理联系人申请。';
444
+ case 'invalid-applicant': return '无法验证当前 AUN 身份,未提交联系人申请。';
445
+ case 'invalid-note': return '申请说明不能超过 500 个字符。';
446
+ }
447
+ }
448
+ async sendContactRequestProtocolResponse(adapter, msg, result, agentName) {
449
+ if (!adapter?.send)
450
+ return;
451
+ await adapter.send(buildEnvelope({
452
+ taskId: `contact-request-${randomBytes(4).toString('hex')}`,
453
+ channel: adapter.channelName,
454
+ channelId: msg.channelId,
455
+ agentName: agentName ?? '<unknown>',
456
+ replyContext: {
457
+ ...(msg.replyContext ?? {}),
458
+ metadata: {
459
+ ...(msg.replyContext?.metadata ?? {}),
460
+ ...evolMenuResponseTransportMetadata(),
461
+ },
462
+ },
463
+ }), {
464
+ kind: 'custom',
465
+ channelType: 'aun',
466
+ payload: {
467
+ type: 'contact.request.response',
468
+ status: result.status,
469
+ request_id: result.requestId,
470
+ expires_at: result.expiresAt,
471
+ },
472
+ });
473
+ }
345
474
  async sendAdmissionControlReply(context, msg, sendReply, text) {
346
475
  if (!text || !consumeAdmissionReplyBudget(context))
347
476
  return false;
@@ -360,20 +489,6 @@ export class MessageBridge {
360
489
  }
361
490
  return '当前不接受陌生人消息。联系人申请仅支持 AUN 私聊。';
362
491
  }
363
- contactRequestReply(result) {
364
- switch (result.code) {
365
- case 'submitted': return '联系人申请已提交,等待管理员审核。';
366
- case 'resubmitted': return '联系人申请已更新,等待管理员审核。';
367
- case 'already-contact': return '你已经是联系人。';
368
- case 'owner': return '你已经是 Owner。';
369
- case 'blocked': return CONTACT_REJECTION_NOTICE;
370
- case 'rate-limited': return '联系人申请过于频繁,请稍后再试。';
371
- case 'capacity': return '当前申请较多,请稍后再试。';
372
- case 'no-owner': return '当前无法受理联系人申请。';
373
- case 'invalid-applicant': return '无法验证当前 AUN 身份,未提交联系人申请。';
374
- case 'invalid-note': return '申请说明不能超过 500 个字符。';
375
- }
376
- }
377
492
  async deliverContactReviewCard(selfAid, applicantAid, submission, owningAgent) {
378
493
  if (!this.interactionRouter || !submission.requestId || !submission.expiresAt || !submission.submittedAt) {
379
494
  return false;
@@ -463,7 +578,7 @@ export class MessageBridge {
463
578
  }
464
579
  if (reviewed.code === 'approved' || reviewed.code === 'rejected' || reviewed.code === 'blocked') {
465
580
  try {
466
- await this.notifyContactApplicant(route.adapter, applicantAid, reviewed.code, owningAgent?.name);
581
+ await this.notifyContactApplicant(route.adapter, applicantAid, reviewed.code, submission.requestId, submission.expiresAt, owningAgent?.name);
467
582
  }
468
583
  catch (error) {
469
584
  logger.warn(`[MessageBridge] Contact applicant notification failed: request=${submission.requestId} error=${error instanceof Error ? error.message : String(error)}`);
@@ -502,11 +617,22 @@ export class MessageBridge {
502
617
  : '当前无审核权限。';
503
618
  await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: ownerAid, agentName: agentName ?? '<unknown>' }), { kind: 'system.notice', text, subtype: 'contact-review' });
504
619
  }
505
- async notifyContactApplicant(adapter, applicantAid, code, agentName) {
506
- const text = code === 'approved' ? '你的联系人申请已通过。'
507
- : code === 'rejected' ? '你的联系人申请未通过。'
508
- : '你的联系人申请未通过。';
509
- await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: applicantAid, agentName: agentName ?? '<unknown>' }), { kind: 'system.notice', text, subtype: 'contact-request-result' });
620
+ async notifyContactApplicant(adapter, applicantAid, code, requestId, expiresAt, agentName) {
621
+ await adapter.send(buildEnvelope({
622
+ channel: adapter.channelName,
623
+ channelId: applicantAid,
624
+ agentName: agentName ?? '<unknown>',
625
+ replyContext: { metadata: evolMenuResponseTransportMetadata() },
626
+ }), {
627
+ kind: 'custom',
628
+ channelType: 'aun',
629
+ payload: {
630
+ type: 'contact.request.response',
631
+ status: code === 'approved' ? 'APPROVED' : code === 'rejected' ? 'REJECTED' : 'NOT_ALLOWED',
632
+ request_id: requestId,
633
+ expires_at: expiresAt,
634
+ },
635
+ });
510
636
  }
511
637
  getDebouncer(channelName, channelType) {
512
638
  let d = this.debouncers.get(channelName);
@@ -582,6 +708,16 @@ export class MessageBridge {
582
708
  actorId,
583
709
  trustedHandoffEcho,
584
710
  });
711
+ if (!trustedHandoffEcho && await this.handleContactRequestProtocol({
712
+ context: admissionPreflight.context,
713
+ msg,
714
+ content,
715
+ owningAgent,
716
+ adapter,
717
+ sendReply,
718
+ })) {
719
+ return;
720
+ }
585
721
  if (!admissionPreflight.allow) {
586
722
  const requestCommand = /^\s*\/request(?:\s|$)/i.test(content);
587
723
  if (admissionPreflight.reason === 'blocked' && admissionPreflight.context.primaryId) {
@@ -760,7 +896,7 @@ export class MessageBridge {
760
896
  channelKey,
761
897
  channelType: msg.channelType || effectiveChannelType,
762
898
  channelId: msg.channelId,
763
- recipientId: actorId,
899
+ recipientId: roleDetail.actor.principalId || actorId,
764
900
  recipientName: msg.peerName,
765
901
  source: 'inbound',
766
902
  });
@@ -900,7 +1036,7 @@ export class MessageBridge {
900
1036
  source: msg.source,
901
1037
  boundSessionId: session.id,
902
1038
  resolvedIdentity: identity,
903
- dispatchMode: msg.dispatchMode,
1039
+ mentionMode: msg.mentionMode,
904
1040
  causation: inboundCausation,
905
1041
  };
906
1042
  recordCausationSpan(inboundCausation, 'message.inbound', {
@@ -1027,28 +1163,6 @@ export class MessageBridge {
1027
1163
  });
1028
1164
  }
1029
1165
  // ── Menu Protocol ──
1030
- static MENU_NAME_MAP = {
1031
- pwd: '/pwd',
1032
- session: '/session',
1033
- topic: '/topic',
1034
- baseagent: '/baseagent',
1035
- model: '/model',
1036
- effort: '/effort',
1037
- chatmode: '/chatmode',
1038
- mentionmode: '/mentionmode',
1039
- group: '/group',
1040
- permission: '/perm',
1041
- activity: '/activity',
1042
- observable: '/observable',
1043
- system: '/system',
1044
- cli: '/cli',
1045
- agent: '/agent',
1046
- trigger: '/trigger',
1047
- file: '/file',
1048
- capability: '/capability',
1049
- role: '/role',
1050
- connect: '/connect',
1051
- };
1052
1166
  extractTopicName(msg) {
1053
1167
  const raw = msg.topicName
1054
1168
  ?? msg.replyContext?.title
@@ -1077,7 +1191,7 @@ export class MessageBridge {
1077
1191
  resolveCmd(name, cmd) {
1078
1192
  if (cmd)
1079
1193
  return cmd;
1080
- const mapped = MessageBridge.MENU_NAME_MAP[name];
1194
+ const mapped = menuCommandForName(name);
1081
1195
  if (!mapped)
1082
1196
  throw { code: 'NOT_SUPPORTED', message: `Unsupported menu name: ${name}` };
1083
1197
  return mapped;
@@ -1152,29 +1266,73 @@ export class MessageBridge {
1152
1266
  const args = request.args;
1153
1267
  switch (request.type) {
1154
1268
  case 'menu.list':
1269
+ if (args?.view === 'catalog') {
1270
+ validateMenuCatalogArgs(args);
1271
+ const data = await this.cmdHandler.getMenuCatalog(channel, msg.channelId, authSubject, args.context, msg.isControlChannel ? 'control' : 'agent');
1272
+ return menuSuccess(header, data);
1273
+ }
1274
+ if (args?.view !== undefined) {
1275
+ return menuFailure(header, { code: 'NOT_SUPPORTED', message: `Unsupported menu.list view: ${String(args.view)}` });
1276
+ }
1155
1277
  return menuSuccess(header, this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private', msg.isControlChannel ? 'control' : 'agent', authSubject));
1156
1278
  case 'menu.query': {
1157
- const result = await this.cmdHandler.execMenuQuery(this.resolveCmd(name, cmd), channel, msg.channelId, msg.peerId, args, msg.chatType, msg.isControlChannel ?? false, identity, authSubject);
1279
+ if (name === 'menu' && args?.view === 'values') {
1280
+ validateMenuValueBatchArgs(args);
1281
+ const data = await this.cmdHandler.execMenuValueBatch(channel, msg.channelId, authSubject, args, args.context, msg.isControlChannel ? 'control' : 'agent');
1282
+ return menuSuccess(header, data);
1283
+ }
1284
+ if (name === 'menu') {
1285
+ return menuFailure(header, { code: 'NOT_SUPPORTED', message: `Unsupported menu.query view: ${String(args?.view)}` });
1286
+ }
1287
+ const execution = await resolveExplicitMenuExecutionContext({
1288
+ subject: authSubject,
1289
+ channelType: authSubject.channelType,
1290
+ channelScope: msg.isControlChannel ? 'control' : 'agent',
1291
+ channelId: msg.channelId,
1292
+ args,
1293
+ });
1294
+ const result = await this.cmdHandler.execMenuQuery(this.resolveCmd(name, cmd), channel, execution.channelId, execution.subject.actorId, execution.args, execution.subject.chatType, execution.subject.fromControlChannel, execution.subject.identity, execution.subject);
1158
1295
  if ('error' in result)
1159
1296
  throw result;
1160
1297
  return menuSuccess(header, result.data);
1161
1298
  }
1162
1299
  case 'menu.options': {
1163
- let optionArgs = args;
1300
+ const execution = await resolveExplicitMenuExecutionContext({
1301
+ subject: authSubject,
1302
+ channelType: authSubject.channelType,
1303
+ channelScope: msg.isControlChannel ? 'control' : 'agent',
1304
+ channelId: msg.channelId,
1305
+ args,
1306
+ });
1307
+ let optionArgs = execution.args;
1164
1308
  if ((name === 'topic' || cmd === '/topic') && args?.mode === 'fork-turns' && msg.threadId && !args?.sourceThreadId) {
1165
- optionArgs = { ...(args ?? {}), sourceThreadId: msg.threadId };
1309
+ optionArgs = { ...(execution.args ?? {}), sourceThreadId: msg.threadId };
1166
1310
  }
1167
- const data = await this.cmdHandler.getSubMenuItems(this.resolveCmd(name, cmd), channel, msg.channelId, msg.peerId, optionArgs, identity, msg.chatType, msg.isControlChannel ?? false, authSubject) ?? [];
1311
+ const data = await this.cmdHandler.getSubMenuItems(this.resolveCmd(name, cmd), channel, execution.channelId, execution.subject.actorId, optionArgs, execution.subject.identity, execution.subject.chatType, execution.subject.fromControlChannel, execution.subject) ?? [];
1168
1312
  return menuSuccess(header, data);
1169
1313
  }
1170
1314
  case 'menu.update': {
1171
- const result = await this.cmdHandler.execMenuUpdate(this.resolveCmd(name, cmd), request.value, channel, msg.channelId, msg.peerId, identity, msg.isControlChannel ?? false, args, authSubject);
1315
+ const execution = await resolveExplicitMenuExecutionContext({
1316
+ subject: authSubject,
1317
+ channelType: authSubject.channelType,
1318
+ channelScope: msg.isControlChannel ? 'control' : 'agent',
1319
+ channelId: msg.channelId,
1320
+ args,
1321
+ });
1322
+ const result = await this.cmdHandler.execMenuUpdate(this.resolveCmd(name, cmd), request.value, channel, execution.channelId, execution.subject.actorId, execution.subject.identity, execution.subject.fromControlChannel, execution.args, execution.subject);
1172
1323
  if ('error' in result)
1173
1324
  throw result;
1174
1325
  return menuSuccess(header, result.data);
1175
1326
  }
1176
1327
  case 'menu.action': {
1177
- let actionArgs = args;
1328
+ const execution = await resolveExplicitMenuExecutionContext({
1329
+ subject: authSubject,
1330
+ channelType: authSubject.channelType,
1331
+ channelScope: msg.isControlChannel ? 'control' : 'agent',
1332
+ channelId: msg.channelId,
1333
+ args,
1334
+ });
1335
+ let actionArgs = execution.args;
1178
1336
  if ((name === 'topic' || cmd === '/topic') && request.action === 'fork' && msg.threadId) {
1179
1337
  if (typeof args?.targetThreadId === 'string' && args.targetThreadId !== msg.threadId) {
1180
1338
  return menuFailure(header, {
@@ -1182,11 +1340,11 @@ export class MessageBridge {
1182
1340
  message: 'args.targetThreadId 必须与消息 thread_id 一致',
1183
1341
  });
1184
1342
  }
1185
- actionArgs = { ...(args ?? {}), targetThreadId: msg.threadId };
1343
+ actionArgs = { ...(execution.args ?? {}), targetThreadId: msg.threadId };
1186
1344
  }
1187
1345
  const resolvedCmd = this.resolveCmd(name, cmd);
1188
1346
  const isFileFetch = resolvedCmd.trim().split(' ')[0] === '/file' && request.action === 'fetch';
1189
- const result = await this.cmdHandler.execMenuAction(resolvedCmd, request.action, actionArgs, channel, msg.channelId, msg.peerId, identity, msg.chatType, header.id, msg.isControlChannel ?? false, authSubject, 'menu', msg.replyContext, isFileFetch ? registerAfterResponse : undefined);
1347
+ const result = await this.cmdHandler.execMenuAction(resolvedCmd, request.action, actionArgs, channel, execution.channelId, execution.subject.actorId, execution.subject.identity, execution.subject.chatType, header.id, execution.subject.fromControlChannel, execution.subject, 'menu', msg.replyContext, isFileFetch ? registerAfterResponse : undefined);
1190
1348
  if ('error' in result)
1191
1349
  throw result;
1192
1350
  return menuSuccess(header, result.data);
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import { appendJsonl, chatDirPath } from '../session/session-fs-store.js';
4
4
  import { logger } from '../../utils/logger.js';
5
5
  const TRANSIENT_PROTOCOL_TYPE_PREFIXES = [
6
+ 'contact.request.',
6
7
  'menu.',
7
8
  'status.',
8
9
  'event.',
@@ -229,6 +230,27 @@ function formatTimestampMs(epochMs) {
229
230
  export function messageLogPath(chatDir) {
230
231
  return path.join(chatDir, MESSAGE_LOG_FILE);
231
232
  }
233
+ export function hasMessageLogOperation(chatDir, operationId) {
234
+ const file = messageLogPath(chatDir);
235
+ if (!fs.existsSync(file))
236
+ return false;
237
+ try {
238
+ return fs.readFileSync(file, 'utf-8').split('\n').some(line => {
239
+ if (!line.trim())
240
+ return false;
241
+ try {
242
+ const entry = JSON.parse(line);
243
+ return entry?.dir === 'out' && entry.operationId === operationId;
244
+ }
245
+ catch {
246
+ return false;
247
+ }
248
+ });
249
+ }
250
+ catch {
251
+ return false;
252
+ }
253
+ }
232
254
  export function resolveChatDir(sessionsDir, channelType, channelId, selfAID) {
233
255
  return chatDirPath(sessionsDir, channelType, channelId, selfAID);
234
256
  }
@@ -323,6 +345,7 @@ export function buildOutboundEntry(opts) {
323
345
  transport: opts.transport,
324
346
  peerType: opts.peerType,
325
347
  source: opts.source ?? 'daemon',
348
+ operationId: opts.operationId,
326
349
  handoff_trace: opts.handoff_trace,
327
350
  };
328
351
  }
@@ -1075,6 +1075,7 @@ export class MessageQueue {
1075
1075
  content: m.content, timestamp: m.timestamp,
1076
1076
  causation: m.causation,
1077
1077
  images: m.images && m.images.length > 0 ? m.images : undefined,
1078
+ mentions: m.mentions && m.mentions.length > 0 ? m.mentions : undefined,
1078
1079
  mentionAids: m.mentionAids && m.mentionAids.length > 0 ? m.mentionAids : undefined,
1079
1080
  });
1080
1081
  }
@@ -1361,9 +1362,9 @@ export class MessageQueue {
1361
1362
  isAgentMuted(agentName) {
1362
1363
  return this.mutedAgents.has(agentName);
1363
1364
  }
1364
- /** 中断指定 agent 所有正在处理中的会话。 */
1365
- interruptByAgent(agentName) {
1365
+ beginAgentInterrupt(agentName) {
1366
1366
  let interrupted = 0;
1367
+ const barriers = [];
1367
1368
  for (const [queueKey, name] of this.processingAgent) {
1368
1369
  if ((name || DEFAULT_AGENT_NAME) === agentName) {
1369
1370
  interrupted++;
@@ -1378,11 +1379,26 @@ export class MessageQueue {
1378
1379
  causation: this.activeBatches.get(queueKey)?.message.causation,
1379
1380
  });
1380
1381
  if (this.interruptCallback) {
1381
- void this.triggerInterrupt(queueKey, sessionKey, activeState?.baseagent, name, 'stop');
1382
+ barriers.push(this.triggerInterrupt(queueKey, sessionKey, activeState?.baseagent, name, 'stop'));
1382
1383
  }
1383
1384
  }
1384
1385
  }
1385
- return interrupted;
1386
+ return {
1387
+ count: interrupted,
1388
+ barrier: Promise.allSettled(barriers).then(() => undefined),
1389
+ };
1390
+ }
1391
+ /** 中断指定 agent 所有正在处理中的会话。 */
1392
+ interruptByAgent(agentName) {
1393
+ const interruption = this.beginAgentInterrupt(agentName);
1394
+ void interruption.barrier;
1395
+ return interruption.count;
1396
+ }
1397
+ /** 中断指定 agent 的会话,并等待 runner 中断屏障完成。 */
1398
+ async interruptByAgentAndWait(agentName) {
1399
+ const interruption = this.beginAgentInterrupt(agentName);
1400
+ await interruption.barrier;
1401
+ return interruption.count;
1386
1402
  }
1387
1403
  // ── Queue query/management methods ──
1388
1404
  /**