evolcore 0.0.12 → 0.0.14

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 (116) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/ec-safe-output.js +26 -7
  4. package/bin/install-codex-managed-hooks.mjs +201 -0
  5. package/dist/agents/claude-runner.js +105 -11
  6. package/dist/agents/codex-app-server-client.js +176 -7
  7. package/dist/agents/codex-runner.js +248 -84
  8. package/dist/agents/ecagent-runner.js +21 -2
  9. package/dist/agents/gemini-runner.js +9 -4
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/store.js +13 -7
  12. package/dist/aun/group-fs-download.js +56 -0
  13. package/dist/aun/group-identity.js +1 -2
  14. package/dist/aun/msg/group.js +11 -33
  15. package/dist/aun/msg/index.js +1 -2
  16. package/dist/aun/msg/managed-operation.js +176 -55
  17. package/dist/aun/msg/upload.js +16 -1
  18. package/dist/aun/outbox.js +31 -2
  19. package/dist/aun/rpc/client.js +66 -0
  20. package/dist/aun/rpc/index.js +1 -2
  21. package/dist/aun/storage/{upload.js → client.js} +11 -2
  22. package/dist/aun/storage/index.js +1 -2
  23. package/dist/channels/aun.js +514 -150
  24. package/dist/channels/daemon.js +2 -0
  25. package/dist/cli/agent.js +9 -96
  26. package/dist/cli/aun-commands.js +1 -1
  27. package/dist/cli/cli-argv.js +10 -0
  28. package/dist/cli/config.js +9 -17
  29. package/dist/cli/ctl-command.js +1 -1
  30. package/dist/cli/daemon-commands.js +39 -24
  31. package/dist/cli/fs-command.js +65 -19
  32. package/dist/cli/init.js +77 -8
  33. package/dist/cli/queue-command.js +42 -0
  34. package/dist/cli/restart-monitor.js +3 -14
  35. package/dist/cli/task-context.js +172 -0
  36. package/dist/cli/trigger-command.js +7 -0
  37. package/dist/config/access-policy.js +1 -2
  38. package/dist/config/builtin-roles.js +7 -0
  39. package/dist/config/config-manager.js +27 -3
  40. package/dist/config/contact-book-store.js +7 -2
  41. package/dist/config/peer-role-resolver.js +6 -2
  42. package/dist/config/role-migration-startup.js +53 -0
  43. package/dist/config/role-store.js +1 -1
  44. package/dist/core/auth/agent-delegation.js +1 -1
  45. package/dist/core/auth/auth-gateway.js +30 -1
  46. package/dist/core/auth/authorization-audit.js +47 -0
  47. package/dist/core/auth/operation-authorizer.js +1 -0
  48. package/dist/core/auth/operation-catalog.js +10 -1
  49. package/dist/core/bootstrap-messages.js +9 -2
  50. package/dist/core/bootstrap-service.js +8 -1
  51. package/dist/core/command/command-handler.js +201 -51
  52. package/dist/core/command/menu-catalog.js +4 -0
  53. package/dist/core/command/menu-handler.js +85 -18
  54. package/dist/core/command/menu-protocol.js +23 -2
  55. package/dist/core/command/slash-handler.js +58 -10
  56. package/dist/core/event-catalog.js +8 -0
  57. package/dist/core/handoff/runtime.js +12 -1
  58. package/dist/core/handoff/store.js +5 -1
  59. package/dist/core/interaction-router.js +10 -0
  60. package/dist/core/message/im-renderer.js +34 -1
  61. package/dist/core/message/message-bridge.js +68 -13
  62. package/dist/core/message/message-log.js +2 -0
  63. package/dist/core/message/message-queue.js +267 -39
  64. package/dist/core/message/message-utils.js +81 -4
  65. package/dist/core/message/response-engine.js +242 -48
  66. package/dist/core/message/stream-debouncer.js +10 -2
  67. package/dist/core/permission/approval-gateway.js +1 -0
  68. package/dist/core/permission/ec-command-parser.js +649 -18
  69. package/dist/core/permission/index.js +1 -1
  70. package/dist/core/permission/mode.js +15 -0
  71. package/dist/core/permission/readonly-shell-query.js +148 -8
  72. package/dist/core/permission/sandbox-runtime.js +21 -0
  73. package/dist/core/permission/tool-policy.js +266 -56
  74. package/dist/core/protected-paths.js +107 -2
  75. package/dist/core/runtime-lock.js +101 -0
  76. package/dist/core/session/session-manager.js +21 -2
  77. package/dist/core/session/session-mapper.js +20 -1
  78. package/dist/eck/detect.js +18 -1
  79. package/dist/eck/group-rules-sync.js +15 -44
  80. package/dist/index.js +191 -89
  81. package/dist/ipc.js +16 -3
  82. package/dist/paths.js +3 -0
  83. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  84. package/dist/response-system/modes/single-session/index.js +3 -0
  85. package/dist/trigger/feedback.js +64 -11
  86. package/dist/trigger/history.js +42 -7
  87. package/dist/trigger/manager.js +21 -1
  88. package/dist/trigger/parser.js +14 -2
  89. package/dist/trigger/scheduler.js +1 -0
  90. package/dist/trigger/validation.js +67 -6
  91. package/dist/utils/ecweb-supervisor.js +332 -0
  92. package/dist/utils/error-utils.js +17 -0
  93. package/dist/utils/logger.js +37 -4
  94. package/kits/docs/evolcore/trigger.md +17 -1
  95. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  96. package/kits/schemas/role-config.schema.1.json +1 -0
  97. package/kits/templates/roles/admin.json +7 -0
  98. package/kits/templates/roles/member.json +8 -1
  99. package/kits/templates/roles/visitor.json +2 -0
  100. package/package.json +1 -1
  101. package/dist/aun/msg/payload-type.js +0 -27
  102. package/dist/aun/rpc/caller.js +0 -42
  103. package/dist/aun/rpc/connection.js +0 -25
  104. package/dist/aun/storage/manage.js +0 -10
  105. package/dist/config/access-policy-domain.js +0 -18
  106. package/dist/config/config-batch-get.js +0 -11
  107. package/dist/config/owner-policy.js +0 -4
  108. package/dist/config/role-config-v4-startup.js +0 -32
  109. package/dist/config/role-config-v5-startup.js +0 -27
  110. package/dist/core/auth/trigger-authorization.js +0 -15
  111. package/dist/core/interaction-registration.js +0 -10
  112. package/dist/core/permission/execution-sandbox.js +0 -16
  113. package/dist/core/relation/peer-key.js +0 -1
  114. package/dist/core/session/session-key.js +0 -24
  115. package/dist/eck/baseagent-caps.js +0 -18
  116. package/dist/eck/rules-loader.js +0 -28
@@ -6,7 +6,7 @@ import { hasModelSwitcher } from '../../agents/runner-types.js';
6
6
  import { getCodexEfforts } from '../../agents/codex-runner.js';
7
7
  import { execCodexCliSync, resolveCodexCliPath } from '../../utils/codex-cli.js';
8
8
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
9
- import { buildEnvelope } from '../message/message-utils.js';
9
+ import { buildEnvelope, isDeliveryTarget, isDeliveryTargetForChannel, replyContextFromSession } from '../message/message-utils.js';
10
10
  import path from 'path';
11
11
  import fs from 'fs';
12
12
  import os from 'os';
@@ -28,7 +28,7 @@ import { normalizeCliArgv, parseCliIntent, parseLegacyCliCommand, validateCliArg
28
28
  import { auditCommandAuthorization, hashArgv } from '../auth/authorization-audit.js';
29
29
  import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
30
30
  import { evaluateRoleOperationCapability } from '../auth/operation-authorizer.js';
31
- import { splitConfigBatchGetArgv } from '../../config/config-batch-get.js';
31
+ import { splitConfigBatchGetArgv } from '../../cli/cli-argv.js';
32
32
  import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
33
33
  import { PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
34
34
  import { resolveRuntimePermissionMode, resolveRuntimeStringField, validateRuntimeStringFieldOverride, } from '../role/runtime-policy.js';
@@ -36,6 +36,7 @@ import { isManagementRole } from '../../config/builtin-roles.js';
36
36
  import { SYSTEM_CONTROL_CHANNEL } from '../system-channels.js';
37
37
  import { logger } from '../../utils/logger.js';
38
38
  import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
39
+ import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
39
40
  import { evolMenuResponseTransportMetadata, menuFailure, menuCommandForName, menuSuccess, normalizeMenuError, validateConfigWriteScope, validateMenuRequest, } from './menu-protocol.js';
40
41
  import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
41
42
  import { handleConnectMenu, connectMenuOperation } from './connect-menu.js';
@@ -1013,8 +1014,13 @@ function buildRelationIntentArgs(params) {
1013
1014
  return out;
1014
1015
  }
1015
1016
  function buildMenuAuthSubject(owner, params) {
1016
- if (params.subject)
1017
- return params.subject;
1017
+ if (params.subject) {
1018
+ return {
1019
+ ...params.subject,
1020
+ ...(params.requestId && !params.subject.requestId ? { requestId: params.requestId } : {}),
1021
+ ...(params.session?.id && !params.subject.sessionId ? { sessionId: params.session.id } : {}),
1022
+ };
1023
+ }
1018
1024
  const agent = owner.getOwningAgent?.(params.channel);
1019
1025
  const selfAid = agent?.aid || params.session?.selfAID;
1020
1026
  const channelType = owner.resolveChannelType?.(params.channel) || params.session?.channelType;
@@ -1025,6 +1031,8 @@ function buildMenuAuthSubject(owner, params) {
1025
1031
  return buildAuthSubject({
1026
1032
  selfAid,
1027
1033
  actorId: params.userId,
1034
+ requestId: params.requestId,
1035
+ sessionId: params.session?.id,
1028
1036
  channel: params.channel,
1029
1037
  channelType: channelType || params.channel.split('#')[0],
1030
1038
  channelId: params.channelId,
@@ -1114,7 +1122,7 @@ function buildRoleMenuContext(owner, channel, subject) {
1114
1122
  channel: contactAdapter.channelName,
1115
1123
  channelId: applicantAid,
1116
1124
  agentName: owningAgent?.name ?? '<unknown>',
1117
- replyContext: { metadata: evolMenuResponseTransportMetadata() },
1125
+ replyContext: { delivery: { chatType: 'private' }, metadata: evolMenuResponseTransportMetadata() },
1118
1126
  }), {
1119
1127
  kind: 'custom',
1120
1128
  channelType: 'aun',
@@ -2710,6 +2718,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2710
2718
  const subject = buildMenuAuthSubject(this, {
2711
2719
  identity: authIdentity,
2712
2720
  session: authSession,
2721
+ requestId,
2713
2722
  explicitChatType,
2714
2723
  channel,
2715
2724
  channelId,
@@ -2927,6 +2936,10 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2927
2936
  if (triggerThread !== undefined && triggerThread !== 'per_run' && triggerThread !== 'by_run' && triggerThread !== 'by_trigger') {
2928
2937
  return { error: `无效 triggerThread: ${triggerThread}`, code: 'INVALID_ARGS' };
2929
2938
  }
2939
+ const targetChatType = args.targetChatType;
2940
+ if (targetChatType !== undefined && targetChatType !== 'private' && targetChatType !== 'group') {
2941
+ return { error: `无效 targetChatType: ${targetChatType}`, code: 'INVALID_ARGS' };
2942
+ }
2930
2943
  const parsed = {
2931
2944
  scheduleType: args.scheduleType,
2932
2945
  scheduleValue: String(args.scheduleValue ?? ''),
@@ -2937,6 +2950,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2937
2950
  name: args.name,
2938
2951
  targetChannel: args.targetChannel,
2939
2952
  targetChannelId: args.targetChannelId,
2953
+ targetChatType,
2940
2954
  targetThreadId: args.targetThreadId,
2941
2955
  agentId: args.agentId,
2942
2956
  model: args.model,
@@ -3313,13 +3327,56 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3313
3327
  }
3314
3328
  if (action === 'restart') {
3315
3329
  const suppressRealRestart = shouldSuppressRealRestart();
3330
+ let restartReplyContext;
3331
+ try {
3332
+ restartReplyContext = session
3333
+ ? replyContextFromSession(session, transportReplyContext)
3334
+ : transportReplyContext;
3335
+ }
3336
+ catch (error) {
3337
+ const code = error?.code;
3338
+ if (channel.split('#', 1)[0].toLowerCase() !== 'aun'
3339
+ || (code !== 'AUN_OUTBOUND_ROUTE_REQUIRED' && code !== 'AUN_OUTBOUND_ROUTE_MISMATCH')) {
3340
+ throw error;
3341
+ }
3342
+ return {
3343
+ error: '当前 AUN 会话缺少有效的持久化出站路由,请从可信入站上下文重试',
3344
+ code,
3345
+ };
3346
+ }
3347
+ const restartDelivery = restartReplyContext?.delivery
3348
+ ?? (explicitChatType === 'group'
3349
+ ? { chatType: 'group', groupId: channelId }
3350
+ : explicitChatType === 'private'
3351
+ ? { chatType: 'private' }
3352
+ : undefined);
3353
+ if (channel.split('#', 1)[0].toLowerCase() === 'aun'
3354
+ && !isDeliveryTargetForChannel(restartDelivery, channelId)) {
3355
+ return {
3356
+ error: '无法确定有效的重启通知出站路由,请从可信会话上下文重试',
3357
+ code: isDeliveryTarget(restartDelivery)
3358
+ ? 'AUN_OUTBOUND_ROUTE_MISMATCH'
3359
+ : 'AUN_OUTBOUND_ROUTE_REQUIRED',
3360
+ };
3361
+ }
3316
3362
  if (!suppressRealRestart) {
3317
- const restartInfo = { channel, channelId, timestamp: Date.now() };
3363
+ const restartInfo = {
3364
+ channel,
3365
+ channelId,
3366
+ timestamp: Date.now(),
3367
+ ...(restartDelivery ? { delivery: restartDelivery } : {}),
3368
+ };
3318
3369
  const controlDir = daemonControlDir();
3319
3370
  fs.mkdirSync(controlDir, { recursive: true });
3320
3371
  fs.writeFileSync(path.join(controlDir, 'restart-pending.json'), JSON.stringify(restartInfo));
3321
- spawnDetachedNode([path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
3372
+ // Use the canonical CLI restart path so the restart itself performs
3373
+ // the version check and automatic upgrades before replacing the daemon.
3374
+ // Do not inherit managed-session credentials: otherwise cmdRestart can
3375
+ // delegate back to this daemon and recursively enqueue another restart.
3376
+ spawnDetachedNode([path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart'], {
3322
3377
  EVOLCORE_HOME: resolvePaths().root,
3378
+ EVOLCORE_SESSION_ID: '',
3379
+ [AGENT_DELEGATION_TOKEN_ENV]: '',
3323
3380
  });
3324
3381
  }
3325
3382
  else {
@@ -3386,6 +3443,17 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3386
3443
  ? (session?.metadata?.groupId || channelId)
3387
3444
  : userId;
3388
3445
  const cliPeerKey = cliChannelType && cliPeerKeyId ? formatPeerKey(cliChannelType, cliPeerKeyId) : undefined;
3446
+ const subject = buildMenuAuthSubject(this, {
3447
+ identity,
3448
+ session,
3449
+ requestId,
3450
+ explicitChatType,
3451
+ channel,
3452
+ channelId,
3453
+ userId,
3454
+ fromControlChannel: fromControlChannel ?? false,
3455
+ subject: authSubject,
3456
+ });
3389
3457
  if (Array.isArray(args?.argv)) {
3390
3458
  argv = withDefaultRelationContext(argv, { self: cliSelfAid, peer: cliPeerKey });
3391
3459
  }
@@ -3404,6 +3472,8 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3404
3472
  await auditCommandAuthorization({
3405
3473
  ts: Date.now(), source: 'menu.cli', operation: parsed.intent.operation,
3406
3474
  scope: parsed.intent.scope, dangerous: true, actorId: userId,
3475
+ requestId, sessionId: subject.sessionId, agentAid: subject.selfAid,
3476
+ permissionMode: subject.permissionMode,
3407
3477
  channel, channelId, role: identity.role, decision: 'deny', code: 'NOT_ALLOWED',
3408
3478
  reason: 'Unrecognized CLI command', taskId: requestId, argvHash: hashArgv(intentArgv),
3409
3479
  name: 'cli', action: 'exec', args: { argv: [...requestedArgv] },
@@ -3412,16 +3482,6 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3412
3482
  }
3413
3483
  parsedCommands.push(parsed);
3414
3484
  }
3415
- const subject = buildMenuAuthSubject(this, {
3416
- identity,
3417
- session,
3418
- explicitChatType,
3419
- channel,
3420
- channelId,
3421
- userId,
3422
- fromControlChannel: fromControlChannel ?? false,
3423
- subject: authSubject,
3424
- });
3425
3485
  const selfAid = subject.selfAid;
3426
3486
  const peerKey = subject.peerKey;
3427
3487
  const isDaemonOwner = subject.isDaemonOwner;
@@ -3446,6 +3506,10 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3446
3506
  allowExplicitRelationTarget: true,
3447
3507
  auditAllowed: false,
3448
3508
  auditMetadata: {
3509
+ requestId,
3510
+ sessionId: subject.sessionId,
3511
+ agentAid: subject.selfAid,
3512
+ permissionMode: subject.permissionMode,
3449
3513
  taskId: requestId,
3450
3514
  argvHash: hashArgv(authArgv),
3451
3515
  name: 'cli',
@@ -3466,6 +3530,8 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3466
3530
  ts: Date.now(), source: 'menu.cli', operation: parsed.intent.operation,
3467
3531
  scope: parsed.intent.scope, dangerous: parsed.intent.dangerous ?? false,
3468
3532
  actorId: userId, selfAid, peerKey,
3533
+ requestId, sessionId: subject.sessionId, agentAid: selfAid,
3534
+ permissionMode: subject.permissionMode,
3469
3535
  channel, channelId, role: identity.role, isDaemonOwner,
3470
3536
  fromControlChannel: fromControlChannel ?? false, decision: 'allow',
3471
3537
  matchedRule: decision.command?.matchedRule, taskId: requestId, argvHash: hashArgv(argv),
@@ -3509,7 +3575,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3509
3575
  return { error: '通道不存在', code: 'EXEC_FAILED' };
3510
3576
  if (!adapter.capabilities?.file)
3511
3577
  return { error: '通道不支持文件发送', code: 'NOT_SUPPORTED' };
3512
- const sessionReplyContext = session ? this.getReplyContext(session) : undefined;
3578
+ const sessionReplyContext = session ? replyContextFromSession(session, this.getReplyContext(session)) : undefined;
3513
3579
  const replyCtx = transportReplyContext
3514
3580
  ? {
3515
3581
  ...(sessionReplyContext ?? {}),
@@ -3593,6 +3659,7 @@ async function execMenuForSystemControl(payload, context) {
3593
3659
  const trustedSubject = buildAuthSubject({
3594
3660
  selfAid: targetAid,
3595
3661
  actorId: context.actorAid,
3662
+ requestId: id !== undefined && id !== null && String(id) ? String(id) : undefined,
3596
3663
  channel: targetChannel,
3597
3664
  channelType: 'aun',
3598
3665
  channelId: context.actorAid || SYSTEM_CONTROL_CHANNEL,
@@ -269,6 +269,9 @@ export class MenuRequestDeduper {
269
269
  ttlMs;
270
270
  maxEntries;
271
271
  entries = new Map();
272
+ /** Retain failed request identities briefly so a subsequent identical
273
+ * request is observable as a retry instead of an unrelated first attempt. */
274
+ failures = new Map();
272
275
  constructor(ttlMs = 30_000, maxEntries = 2_048) {
273
276
  this.ttlMs = ttlMs;
274
277
  this.maxEntries = maxEntries;
@@ -279,7 +282,13 @@ export class MenuRequestDeduper {
279
282
  if (existing) {
280
283
  if (existing.fingerprint !== fingerprint)
281
284
  return { conflict: true };
282
- return { value: await existing.promise, replayed: true };
285
+ return { value: await existing.promise, replayed: true, retry: false };
286
+ }
287
+ const previousFailure = this.failures.get(key);
288
+ if (previousFailure) {
289
+ if (previousFailure.fingerprint !== fingerprint)
290
+ return { conflict: true };
291
+ this.failures.delete(key);
283
292
  }
284
293
  const promise = operation();
285
294
  const entry = { fingerprint, promise, expiresAt: Date.now() + this.ttlMs };
@@ -288,10 +297,12 @@ export class MenuRequestDeduper {
288
297
  try {
289
298
  const value = await promise;
290
299
  entry.expiresAt = Date.now() + this.ttlMs;
291
- return { value, replayed: false };
300
+ return { value, replayed: false, retry: !!previousFailure };
292
301
  }
293
302
  catch (error) {
294
303
  this.entries.delete(key);
304
+ this.failures.set(key, { fingerprint, expiresAt: Date.now() + this.ttlMs });
305
+ this.enforceLimit();
295
306
  throw error;
296
307
  }
297
308
  }
@@ -301,6 +312,10 @@ export class MenuRequestDeduper {
301
312
  if (entry.expiresAt <= now)
302
313
  this.entries.delete(key);
303
314
  }
315
+ for (const [key, entry] of this.failures) {
316
+ if (entry.expiresAt <= now)
317
+ this.failures.delete(key);
318
+ }
304
319
  }
305
320
  enforceLimit() {
306
321
  while (this.entries.size > this.maxEntries) {
@@ -309,6 +324,12 @@ export class MenuRequestDeduper {
309
324
  break;
310
325
  this.entries.delete(oldest);
311
326
  }
327
+ while (this.failures.size > this.maxEntries) {
328
+ const oldest = this.failures.keys().next().value;
329
+ if (!oldest)
330
+ break;
331
+ this.failures.delete(oldest);
332
+ }
312
333
  }
313
334
  }
314
335
  export class MenuDiagnosticLimiter {
@@ -1,6 +1,6 @@
1
1
  import { hasModelSwitcher, hasPermissionController } from '../../agents/runner-types.js';
2
2
  import { getCodexEfforts } from '../../agents/codex-runner.js';
3
- import { buildEnvelope } from '../message/message-utils.js';
3
+ import { buildEnvelope, isDeliveryTargetForChannel } from '../message/message-utils.js';
4
4
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
5
5
  import { logger } from '../../utils/logger.js';
6
6
  import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
@@ -2492,17 +2492,50 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2492
2492
  // 执行重启逻辑(共用于卡片回调和文本确认)
2493
2493
  const executeRestart = async () => {
2494
2494
  const suppressRealRestart = shouldSuppressRealRestart();
2495
- let replyContext;
2496
- if (threadId) {
2497
- const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
2498
- if (threadSession)
2499
- replyContext = this.getReplyContext(threadSession);
2495
+ let restartReplyContext = replyContext;
2496
+ try {
2497
+ if (!restartReplyContext?.delivery && threadId) {
2498
+ const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
2499
+ if (threadSession)
2500
+ restartReplyContext = this.getReplyContext(threadSession);
2501
+ }
2502
+ else if (!restartReplyContext?.delivery && activeSession) {
2503
+ restartReplyContext = this.getReplyContext(activeSession);
2504
+ }
2505
+ }
2506
+ catch (error) {
2507
+ const code = error?.code;
2508
+ if (channel.split('#', 1)[0].toLowerCase() !== 'aun'
2509
+ || (code !== 'AUN_OUTBOUND_ROUTE_REQUIRED' && code !== 'AUN_OUTBOUND_ROUTE_MISMATCH')) {
2510
+ throw error;
2511
+ }
2512
+ logger.warn(`[System] Refusing restart from invalid persisted AUN session route: channel=${channel} channelId=${channelId} code=${code}`);
2513
+ return false;
2514
+ }
2515
+ // The authenticated inbound transport context takes precedence over a
2516
+ // potentially stale session snapshot. When no session exists, the
2517
+ // explicit command chat type is the only acceptable local fallback.
2518
+ if (restartReplyContext?.delivery === undefined) {
2519
+ restartReplyContext = {
2520
+ ...(restartReplyContext ?? {}),
2521
+ ...(chatType === 'group'
2522
+ ? { delivery: { chatType: 'group', groupId: channelId } }
2523
+ : chatType === 'private'
2524
+ ? { delivery: { chatType: 'private' } }
2525
+ : {}),
2526
+ };
2527
+ }
2528
+ if (channel.split('#', 1)[0].toLowerCase() === 'aun'
2529
+ && !isDeliveryTargetForChannel(restartReplyContext?.delivery, channelId)) {
2530
+ logger.warn(`[System] Refusing restart without a valid explicit delivery route: channel=${channel} channelId=${channelId}`);
2531
+ return false;
2500
2532
  }
2501
2533
  const restartInfo = {
2502
2534
  channel,
2503
2535
  channelId,
2504
2536
  timestamp: Date.now(),
2505
- ...(replyContext?.replyToMessageId ? { rootId: replyContext.replyToMessageId } : {}),
2537
+ ...(restartReplyContext?.delivery ? { delivery: restartReplyContext.delivery } : {}),
2538
+ ...(restartReplyContext?.replyToMessageId ? { rootId: restartReplyContext.replyToMessageId } : {}),
2506
2539
  };
2507
2540
  if (!suppressRealRestart) {
2508
2541
  const controlDir = daemonControlDir();
@@ -2527,7 +2560,11 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2527
2560
  channelId,
2528
2561
  agentName: 'system',
2529
2562
  chatmode: 'interactive',
2530
- replyContext,
2563
+ replyContext: restartReplyContext ?? (chatType === 'group'
2564
+ ? { delivery: { chatType: 'group', groupId: channelId } }
2565
+ : chatType === 'private'
2566
+ ? { delivery: { chatType: 'private' } }
2567
+ : undefined),
2531
2568
  });
2532
2569
  await adapter.send(envelope, { kind: 'command.result', text: '🔄 服务正在重启,请稍候...(约 5 秒后恢复)' });
2533
2570
  // 等待消息发送完成后再延迟 kill
@@ -2570,7 +2607,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2570
2607
  return { kind: 'command.result', text: sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。' };
2571
2608
  }
2572
2609
  }
2573
- await executeRestart();
2610
+ const restarted = await executeRestart();
2611
+ if (!restarted) {
2612
+ return { kind: 'command.error', text: '❌ 无法确定重启通知的出站路由,请从可信会话上下文重试' };
2613
+ }
2574
2614
  // executeRestart 内部已经发送了反馈消息,这里返回 null 避免重复发送
2575
2615
  return null;
2576
2616
  }
@@ -2728,7 +2768,15 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2728
2768
  }
2729
2769
  // 发送文件
2730
2770
  try {
2731
- const replyCtx = !isCrossChannel && sendSession ? this.getReplyContext(sendSession) : undefined;
2771
+ const replyCtx = isCrossChannel
2772
+ ? { delivery: { chatType: 'private' } }
2773
+ : sendSession
2774
+ ? this.getReplyContext(sendSession)
2775
+ : chatType === 'group'
2776
+ ? { delivery: { chatType: 'group', groupId: channelId } }
2777
+ : chatType === 'private'
2778
+ ? { delivery: { chatType: 'private' } }
2779
+ : undefined;
2732
2780
  await targetAdapter.send(buildEnvelope({ channel: targetAdapter.channelName, channelId: targetChannelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath });
2733
2781
  const sizeStr = stat.size < 1024 ? `${stat.size} B`
2734
2782
  : stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
@@ -313,6 +313,10 @@ const CATALOG = [
313
313
  { path: 'sessionId', type: 'string' },
314
314
  { path: 'toolName', type: 'string' },
315
315
  { path: 'input', type: 'object' },
316
+ { path: 'callId', type: 'string', optional: true },
317
+ { path: 'correlationId', type: 'string', optional: true },
318
+ { path: 'agentAid', type: 'string', optional: true },
319
+ { path: 'permissionMode', type: 'string', optional: true },
316
320
  { path: 'timestamp', type: 'number', optional: true },
317
321
  ],
318
322
  },
@@ -326,6 +330,10 @@ const CATALOG = [
326
330
  { path: 'toolName', type: 'string' },
327
331
  { path: 'isError', type: 'boolean', optional: true },
328
332
  { path: 'agentName', type: 'string', optional: true },
333
+ { path: 'callId', type: 'string', optional: true },
334
+ { path: 'correlationId', type: 'string', optional: true },
335
+ { path: 'agentAid', type: 'string', optional: true },
336
+ { path: 'permissionMode', type: 'string', optional: true },
329
337
  { path: 'timestamp', type: 'number', optional: true },
330
338
  ],
331
339
  },
@@ -6,6 +6,7 @@ import { HandoffStore } from './store.js';
6
6
  import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
7
7
  import { recordCausationSpan } from '../causation/audit.js';
8
8
  import { logger } from '../../utils/logger.js';
9
+ import { replyContextFromSession } from '../message/message-utils.js';
9
10
  const HANDOFF_STALE_WARN_MS = 72 * 60 * 60 * 1000;
10
11
  const HANDOFF_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
11
12
  const HANDOFF_LARGE_AGGREGATE_COUNT = 10;
@@ -79,7 +80,12 @@ export class HandoffRuntime {
79
80
  if (!origin || origin.selfAID !== input.selfAid)
80
81
  throw new Error('origin session not found');
81
82
  const targetChannelKey = formatChannelKey({ type: 'aun', selfAID: input.selfAid, name: 'main' });
82
- const targetChatType = input.targetChatType ?? 'private';
83
+ if (!input.targetChatType) {
84
+ throw Object.assign(new Error('AUN handoff requires an explicit target chat type'), {
85
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
86
+ });
87
+ }
88
+ const targetChatType = input.targetChatType;
83
89
  const target = await this.sessionManager.getOrCreateSession(targetChannelKey, input.to, origin.projectPath, input.thread, {
84
90
  channelKey: targetChannelKey,
85
91
  peerId: input.to,
@@ -112,6 +118,9 @@ export class HandoffRuntime {
112
118
  targetSessionId: target.id,
113
119
  payload: input.payload,
114
120
  encrypt: input.encrypt,
121
+ delivery: targetChatType === 'group'
122
+ ? { chatType: 'group', groupId: input.to }
123
+ : { chatType: 'private' },
115
124
  returnPolicy: input.explicitReturnPolicy ?? 'required',
116
125
  originAuthorization: input.originAuthorization,
117
126
  causation: handoffCausation,
@@ -209,6 +218,8 @@ export class HandoffRuntime {
209
218
  messageId,
210
219
  timestamp: Date.now(),
211
220
  source: 'handoff',
221
+ groupId: origin.chatType === 'group' ? (origin.metadata?.groupId || origin.channelId) : undefined,
222
+ replyContext: replyContextFromSession(origin),
212
223
  boundSessionId: origin.id,
213
224
  causation: instance.return_causation
214
225
  ? deriveCausation(instance.return_causation)
@@ -115,7 +115,11 @@ export class HandoffStore {
115
115
  origin_session_id: input.originSessionId,
116
116
  origin_message_id: input.originMessageId,
117
117
  target_session_id: input.targetSessionId,
118
- request: { payload: clonePayload(input.payload), encrypt: input.encrypt },
118
+ request: {
119
+ payload: clonePayload(input.payload),
120
+ encrypt: input.encrypt,
121
+ ...(input.delivery ? { delivery: input.delivery } : {}),
122
+ },
119
123
  ...(input.originAuthorization
120
124
  ? { origin_authorization: { ...input.originAuthorization } }
121
125
  : {}),
@@ -228,6 +228,16 @@ export class InteractionRouter {
228
228
  return undefined;
229
229
  }
230
230
  }
231
+ /** Bind an adapter response to its runtime-owned channel identity domain. */
232
+ export function registerAdapterInteractions(adapter, router) {
233
+ const channelKey = adapter.channelKey;
234
+ adapter.onInteraction?.(async (response) => {
235
+ return router.handleAsync({ ...response, channelKey });
236
+ });
237
+ adapter.onInteractionInvalidated?.((interactionId, reason) => {
238
+ return router.cancelFromChannel(interactionId, reason);
239
+ });
240
+ }
231
241
  /** 把 CommandCard 渲染为文本提示,channel 不支持卡片时使用 */
232
242
  export function renderCommandCardAsText(card) {
233
243
  const lines = [card.title];
@@ -59,7 +59,21 @@ export class IMRenderer {
59
59
  /** 推入 AgentEvent,按 chatmode 投影 */
60
60
  emit(event) {
61
61
  try {
62
- logger.event({ source: 'runner', taskId: this.opts.envelope.taskId, channelId: this.opts.envelope.channelId, event });
62
+ const correlatedEvent = event.type === 'tool_use' || event.type === 'tool_result'
63
+ ? { ...event, correlationId: event.correlationId ?? event.callId }
64
+ : event;
65
+ logger.event({
66
+ source: 'runner',
67
+ taskId: this.opts.envelope.taskId,
68
+ sessionId: this.opts.envelope.sessionId,
69
+ channelId: this.opts.envelope.channelId,
70
+ agentAid: this.opts.agentAid,
71
+ agentName: this.opts.envelope.agentName,
72
+ ...(correlatedEvent.type === 'tool_use' || correlatedEvent.type === 'tool_result'
73
+ ? { correlationId: correlatedEvent.correlationId, toolName: correlatedEvent.name }
74
+ : {}),
75
+ event: correlatedEvent,
76
+ });
63
77
  }
64
78
  catch {
65
79
  // logger.event 失败不影响业务
@@ -244,6 +258,25 @@ export class IMRenderer {
244
258
  durationMs: opts.durationMs,
245
259
  });
246
260
  }
261
+ /** 发送不进入 activity/thought 历史的非终态 warning status。 */
262
+ async sendWarningStatus(message, subtype, details) {
263
+ const payload = {
264
+ kind: 'status.progress',
265
+ metadata: {
266
+ activityType: 'progress',
267
+ message,
268
+ severity: 'warn',
269
+ ...(subtype !== undefined && { subtype }),
270
+ ...(details?.missingItems !== undefined && { missingItems: details.missingItems }),
271
+ },
272
+ };
273
+ try {
274
+ await this.opts.send(payload);
275
+ }
276
+ catch (error) {
277
+ logger.warn(`[IMRenderer] warning status send failed: ${error instanceof Error ? error.message : String(error)}`);
278
+ }
279
+ }
247
280
  /** 添加任务生命周期边界事件,复用 activity.batch 投影路径。 */
248
281
  addLifecycle(phase, metadata, text) {
249
282
  const item = {