evolcore 0.0.13 → 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 (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/install-codex-managed-hooks.mjs +201 -0
  4. package/dist/agents/claude-runner.js +53 -5
  5. package/dist/agents/codex-app-server-client.js +123 -2
  6. package/dist/agents/codex-runner.js +149 -30
  7. package/dist/agents/ecagent-runner.js +17 -1
  8. package/dist/agents/gemini-runner.js +9 -4
  9. package/dist/aun/msg/managed-operation.js +63 -3
  10. package/dist/channels/aun.js +144 -15
  11. package/dist/channels/daemon.js +2 -0
  12. package/dist/cli/aun-commands.js +1 -1
  13. package/dist/cli/fs-command.js +46 -9
  14. package/dist/cli/task-context.js +172 -0
  15. package/dist/config/builtin-roles.js +2 -0
  16. package/dist/config/config-manager.js +6 -2
  17. package/dist/config/contact-book-store.js +7 -2
  18. package/dist/core/auth/auth-gateway.js +1 -0
  19. package/dist/core/auth/authorization-audit.js +32 -0
  20. package/dist/core/auth/operation-catalog.js +3 -3
  21. package/dist/core/bootstrap-service.js +7 -1
  22. package/dist/core/command/command-handler.js +3 -0
  23. package/dist/core/event-catalog.js +2 -0
  24. package/dist/core/message/im-renderer.js +15 -1
  25. package/dist/core/message/message-bridge.js +5 -2
  26. package/dist/core/message/response-engine.js +138 -10
  27. package/dist/core/permission/ec-command-parser.js +556 -4
  28. package/dist/core/permission/tool-policy.js +17 -29
  29. package/dist/core/runtime-lock.js +101 -0
  30. package/dist/index.js +30 -3
  31. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  32. package/dist/response-system/modes/single-session/index.js +3 -0
  33. package/dist/trigger/history.js +42 -7
  34. package/dist/utils/error-utils.js +7 -0
  35. package/dist/utils/logger.js +37 -4
  36. package/kits/templates/roles/admin.json +2 -0
  37. package/kits/templates/roles/member.json +1 -0
  38. package/package.json +1 -1
@@ -65,7 +65,7 @@ const MSG_READ_OPERATIONS = [
65
65
  description,
66
66
  sources: ['agent-tool'],
67
67
  }));
68
- const GROUP_FS_READ_OPERATIONS = ['ls', 'stat', 'lstat', 'cat', 'find', 'df'].map(kind => ({
68
+ const GROUP_FS_READ_OPERATIONS = ['ls', 'stat', 'lstat', 'cat', 'find', 'df', 'getfacl'].map(kind => ({
69
69
  id: `ec.fs.${kind}`,
70
70
  category: 'read',
71
71
  dangerous: false,
@@ -73,9 +73,9 @@ const GROUP_FS_READ_OPERATIONS = ['ls', 'stat', 'lstat', 'cat', 'find', 'df'].ma
73
73
  description: `Read current group filesystem (${kind})`,
74
74
  sources: ['agent-tool'],
75
75
  }));
76
- const GROUP_FS_WRITE_OPERATIONS = ['mkdir', 'upload', 'copy', 'move', 'remove'].map(kind => ({
76
+ const GROUP_FS_WRITE_OPERATIONS = [['mkdir', 'write-own'], ['upload', 'write-own'], ['copy', 'write-own'], ['move', 'write-own'], ['remove', 'write-own'], ['setfacl', 'write-agent']].map(([kind, category]) => ({
77
77
  id: `ec.fs.${kind}`,
78
- category: 'write-own',
78
+ category,
79
79
  dangerous: false,
80
80
  defaultScopes: ['relation'],
81
81
  description: `Write current group filesystem (${kind})`,
@@ -140,7 +140,13 @@ export class BootstrapService {
140
140
  this.inFlight.delete(key);
141
141
  return false;
142
142
  }
143
- const channelId = ctx.channelId || this.defaultChannelIdForConnection(channelType, recipientId);
143
+ // The bootstrap prompt is deliberately a private message to the configured
144
+ // owner. An inbound owner message may have arrived from a group, in which
145
+ // case ctx.channelId is the group AID and must not be paired with the
146
+ // private delivery route below (that would call message.send(to=group)).
147
+ const channelId = channelType === 'aun'
148
+ ? recipientId
149
+ : ctx.channelId || this.defaultChannelIdForConnection(channelType, recipientId);
144
150
  if (!channelId) {
145
151
  this.inFlight.delete(key);
146
152
  return false;
@@ -1974,6 +1974,9 @@ export class CommandHandler {
1974
1974
  ...(parsed.command.recursive !== undefined ? { recursive: parsed.command.recursive } : {}),
1975
1975
  ...(parsed.command.parents !== undefined ? { parents: parsed.command.parents } : {}),
1976
1976
  ...(parsed.command.overwrite !== undefined ? { overwrite: parsed.command.overwrite } : {}),
1977
+ ...(parsed.command.aclGrantee ? { aclGrantee: parsed.command.aclGrantee } : {}),
1978
+ ...(parsed.command.aclPerms ? { aclPerms: parsed.command.aclPerms } : {}),
1979
+ ...(parsed.command.aclRemove !== undefined ? { aclRemove: parsed.command.aclRemove } : {}),
1977
1980
  };
1978
1981
  const decision = authorizeOperation({
1979
1982
  source: 'agent-tool',
@@ -314,6 +314,7 @@ const CATALOG = [
314
314
  { path: 'toolName', type: 'string' },
315
315
  { path: 'input', type: 'object' },
316
316
  { path: 'callId', type: 'string', optional: true },
317
+ { path: 'correlationId', type: 'string', optional: true },
317
318
  { path: 'agentAid', type: 'string', optional: true },
318
319
  { path: 'permissionMode', type: 'string', optional: true },
319
320
  { path: 'timestamp', type: 'number', optional: true },
@@ -330,6 +331,7 @@ const CATALOG = [
330
331
  { path: 'isError', type: 'boolean', optional: true },
331
332
  { path: 'agentName', type: 'string', optional: true },
332
333
  { path: 'callId', type: 'string', optional: true },
334
+ { path: 'correlationId', type: 'string', optional: true },
333
335
  { path: 'agentAid', type: 'string', optional: true },
334
336
  { path: 'permissionMode', type: 'string', optional: true },
335
337
  { path: 'timestamp', type: 'number', optional: true },
@@ -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 失败不影响业务
@@ -872,7 +872,7 @@ export class MessageBridge {
872
872
  return;
873
873
  }
874
874
  // 普通消息保留原始日志;控制 payload 使用脱敏元数据日志。
875
- logger.channelIn({ channel: channelName, channelId: msg.channelId, peerId: msg.peerId, peerName: msg.peerName, chatType: msg.chatType, msgId: msg.messageId, threadId: msg.threadId, content, images: msg.images?.length ?? 0, mentions: msg.mentions, replyContext: msg.replyContext });
875
+ logger.channelIn({ channel: channelName, channelId: msg.channelId, selfAid: msg.selfAID, peerId: msg.peerId, peerName: msg.peerName, chatType: msg.chatType, msgId: msg.messageId, correlationId: msg.messageId, threadId: msg.threadId, content, images: msg.images?.length ?? 0, mentions: msg.mentions, replyContext: msg.replyContext });
876
876
  const accessDecision = authorizeAccess(authSubject);
877
877
  if (!accessDecision.allow) {
878
878
  logger.warn(`[MessageBridge] Access denied before command routing: channel=${channelName} channelId=${msg.channelId} actor=${actorId ?? '<none>'} role=${identity.role} reason=${accessDecision.reason}`);
@@ -954,7 +954,8 @@ export class MessageBridge {
954
954
  }
955
955
  }
956
956
  if (msg.source !== 'handoff' && await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
957
- logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId: `cmd-${msg.messageId || Date.now()}`, payload: { kind: 'command.result', text } });
957
+ const taskId = `cmd-${msg.messageId || Date.now()}`;
958
+ logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId, correlationId: taskId, sessionId: msg.replyContext?.sessionId, agentAid: msg.selfAID, payload: { kind: 'command.result', text } });
958
959
  return sendReply(msg.channelId, text, msg.replyContext);
959
960
  }, msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext, msg.messageId, msg.selfAID, authSubject))
960
961
  return;
@@ -1468,9 +1469,11 @@ export class MessageBridge {
1468
1469
  logger.channelIn({
1469
1470
  channel,
1470
1471
  channelId: this.shortHash(msg.channelId),
1472
+ selfAid: msg.selfAID,
1471
1473
  peerId: this.shortHash(msg.peerId),
1472
1474
  chatType: msg.chatType,
1473
1475
  msgId: msg.messageId,
1476
+ correlationId: msg.messageId,
1474
1477
  control: {
1475
1478
  id: parsed.id,
1476
1479
  type: parsed.type,
@@ -3,7 +3,7 @@ import fs from 'fs';
3
3
  import os from 'os';
4
4
  import crypto from 'crypto';
5
5
  import { BaseagentRunnerUnavailableError, hasCompact, autoCompactWindowForContextWindow, autoCompactWindowForModel, isClaudeContextUsageModel, numericToken } from '../../agents/runner-types.js';
6
- import { buildTaskRuntimeEnv } from '../../cli/task-context.js';
6
+ import { buildTaskRuntimeEnv, ensureRuntimeLockDir, ensureSessionRuntimeDir } from '../../cli/task-context.js';
7
7
  import { IMRenderer } from './im-renderer.js';
8
8
  import { createSendFileMarkerPattern } from './file-markers.js';
9
9
  import { createTextInferenceProvider } from '../inference/text-inference.js';
@@ -953,7 +953,7 @@ export class ResponseEngine {
953
953
  }
954
954
  agent.registerStream(streamKey, retryStream);
955
955
  return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
956
- undefined, turnLease, opts.permissionMode);
956
+ undefined, turnLease, opts.permissionMode, opts.proactiveSelfAid);
957
957
  }
958
958
  // Dropping the whole session hides the root cause and can replay side
959
959
  // effects in unattended trigger tasks. Surface an actionable failure
@@ -1992,6 +1992,7 @@ export class ResponseEngine {
1992
1992
  const renderer = new IMRenderer({
1993
1993
  adapter,
1994
1994
  envelope,
1995
+ agentAid: session.selfAID,
1995
1996
  flushDelay: (options?.flushDelay ?? this.agentRegistry?.resolveByChannel(channelKey)?.config?.flush_delay ?? 3) * 1000,
1996
1997
  suppressActivityItems: isProactive ? false : middleOutputMode !== 'all',
1997
1998
  suppressIntermediateText: isProactive ? false : middleOutputMode === 'none',
@@ -2170,6 +2171,7 @@ export class ResponseEngine {
2170
2171
  ? (anomaly) => {
2171
2172
  recordTriggerExecutionAnomaly(triggerRunId, {
2172
2173
  ...anomaly,
2174
+ correlationId: anomaly.correlationId ?? anomaly.requestId,
2173
2175
  agentAid: anomaly.agentAid ?? session.selfAID ?? message.selfAID,
2174
2176
  sessionId: anomaly.sessionId ?? session.id,
2175
2177
  permissionMode: anomaly.permissionMode ?? effectivePermissionMode,
@@ -2758,6 +2760,12 @@ export class ResponseEngine {
2758
2760
  this.handoffRuntime.completeTargetContext(selfAid, handoffId);
2759
2761
  }
2760
2762
  }
2763
+ // The runner may already own a stronger per-session directory (Codex
2764
+ // registers one before constructing its thread). Other runners use a
2765
+ // private child of the process-provided TMPDIR. Both paths are passed
2766
+ // explicitly so sandboxed helper commands never fall back to a shared
2767
+ // agent or project directory for transient state.
2768
+ const sessionRuntimeDir = agent.getManagedTempDir?.(session.id) ?? ensureSessionRuntimeDir(session.id);
2761
2769
  const taskRuntimeContext = {
2762
2770
  taskId,
2763
2771
  sessionId: session.id,
@@ -2771,6 +2779,8 @@ export class ResponseEngine {
2771
2779
  peerType: message.peerType || session.metadata?.peerType || undefined,
2772
2780
  peerRole,
2773
2781
  threadId: session.threadId || undefined,
2782
+ sessionRuntimeDir,
2783
+ runtimeLockDir: ensureRuntimeLockDir(sessionRuntimeDir),
2774
2784
  handoffIds: v2HandoffIds.length > 0 && v2HandoffDirection === 'target' ? v2HandoffIds : undefined,
2775
2785
  causation: taskCausation,
2776
2786
  };
@@ -2841,7 +2851,7 @@ export class ResponseEngine {
2841
2851
  const processAttempt = this.processEventStream(stream, session, agent, renderer, (eventType, toolName) => {
2842
2852
  resetTimer(eventType, toolName);
2843
2853
  attemptTimeout?.reset();
2844
- }, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode);
2854
+ }, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, session.selfAID || message.selfAID);
2845
2855
  streamResult = attemptTimeout
2846
2856
  ? await Promise.race([processAttempt, attemptTimeout.promise])
2847
2857
  : await processAttempt;
@@ -2999,6 +3009,7 @@ export class ResponseEngine {
2999
3009
  resetTimer,
3000
3010
  shouldSuppress,
3001
3011
  proactive,
3012
+ proactiveSelfAid: session.selfAID || message.selfAID,
3002
3013
  permissionMode: effectivePermissionMode,
3003
3014
  turnLease,
3004
3015
  });
@@ -3041,6 +3052,7 @@ export class ResponseEngine {
3041
3052
  resetTimer,
3042
3053
  shouldSuppress,
3043
3054
  proactive,
3055
+ proactiveSelfAid: session.selfAID || message.selfAID,
3044
3056
  permissionMode: effectivePermissionMode,
3045
3057
  turnLease,
3046
3058
  });
@@ -4022,7 +4034,7 @@ export class ResponseEngine {
4022
4034
  */
4023
4035
  async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress, proactive,
4024
4036
  /** [迁移点4/5] 响应模式插件 + 状态,用于调 onToolUse/onComplete 钩子 */
4025
- modeHooks, causation, turnLease, permissionMode) {
4037
+ modeHooks, causation, turnLease, permissionMode, proactiveSelfAid) {
4026
4038
  // Per-session agent name for stats bucketing
4027
4039
  const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
4028
4040
  const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
@@ -4038,6 +4050,7 @@ export class ResponseEngine {
4038
4050
  let toolUseCount = 0;
4039
4051
  const openToolUseIds = new Set();
4040
4052
  const anonymousToolUseIds = new Map();
4053
+ const anonymousOpenIdByCorrelation = new Map();
4041
4054
  const finalizeResult = () => ({
4042
4055
  ...completeResult,
4043
4056
  hasReceivedText,
@@ -4053,7 +4066,28 @@ export class ResponseEngine {
4053
4066
  let lastReplyText = '';
4054
4067
  // callId → description 映射,用于 tool_result 回显描述
4055
4068
  const toolDescByCallId = new Map();
4069
+ const toolInputByCallId = new Map();
4070
+ const toolInputByCorrelationId = new Map();
4071
+ const anonymousToolInputsByName = new Map();
4056
4072
  const ctlQueueReadCallIds = new Set();
4073
+ const removePendingToolInput = (pending) => {
4074
+ for (const [callId, value] of toolInputByCallId) {
4075
+ if (value === pending)
4076
+ toolInputByCallId.delete(callId);
4077
+ }
4078
+ for (const [correlationId, value] of toolInputByCorrelationId) {
4079
+ if (value === pending)
4080
+ toolInputByCorrelationId.delete(correlationId);
4081
+ }
4082
+ const anonymous = anonymousToolInputsByName.get(pending.name);
4083
+ if (anonymous) {
4084
+ const remaining = anonymous.filter(value => value !== pending);
4085
+ if (remaining.length > 0)
4086
+ anonymousToolInputsByName.set(pending.name, remaining);
4087
+ else
4088
+ anonymousToolInputsByName.delete(pending.name);
4089
+ }
4090
+ };
4057
4091
  try {
4058
4092
  for await (let event of stream) {
4059
4093
  if (turnLease && !this.turnCoordinator.isCurrent(turnLease)) {
@@ -4085,16 +4119,44 @@ export class ResponseEngine {
4085
4119
  else {
4086
4120
  const syntheticId = `__anonymous_tool_${toolUseCount}`;
4087
4121
  openToolUseIds.add(syntheticId);
4122
+ if (event.correlationId)
4123
+ anonymousOpenIdByCorrelation.set(event.correlationId, syntheticId);
4088
4124
  const pending = anonymousToolUseIds.get(event.name) ?? [];
4089
4125
  pending.push(syntheticId);
4090
4126
  anonymousToolUseIds.set(event.name, pending);
4091
4127
  }
4092
4128
  }
4093
4129
  else if (event.type === 'tool_result') {
4094
- if (event.callId) {
4130
+ // A result carrying both identifiers may be stale or replayed. Do not
4131
+ // close the active call until both identifiers resolve to the same
4132
+ // pending tool invocation; otherwise a mismatched result can make the
4133
+ // protocol look complete even though the real call is still open.
4134
+ const hasBothResultIdentifiers = !!event.callId && !!event.correlationId;
4135
+ const byCallId = event.callId ? toolInputByCallId.get(event.callId) : undefined;
4136
+ const byCorrelationId = event.correlationId
4137
+ ? toolInputByCorrelationId.get(event.correlationId)
4138
+ : undefined;
4139
+ const identifiersMatch = !hasBothResultIdentifiers
4140
+ || (!!byCallId && !!byCorrelationId && byCallId === byCorrelationId);
4141
+ if (event.callId && identifiersMatch) {
4095
4142
  openToolUseIds.delete(event.callId);
4096
4143
  }
4097
- else {
4144
+ else if (!event.callId && event.correlationId && byCorrelationId) {
4145
+ const syntheticId = anonymousOpenIdByCorrelation.get(event.correlationId);
4146
+ if (syntheticId) {
4147
+ openToolUseIds.delete(syntheticId);
4148
+ anonymousOpenIdByCorrelation.delete(event.correlationId);
4149
+ }
4150
+ else {
4151
+ for (const [callId, pending] of toolInputByCallId) {
4152
+ if (pending === byCorrelationId) {
4153
+ openToolUseIds.delete(callId);
4154
+ break;
4155
+ }
4156
+ }
4157
+ }
4158
+ }
4159
+ else if (!event.callId) {
4098
4160
  const names = event.name ? [event.name] : [...anonymousToolUseIds.keys()];
4099
4161
  for (const name of names) {
4100
4162
  const pending = anonymousToolUseIds.get(name);
@@ -4350,16 +4412,32 @@ export class ResponseEngine {
4350
4412
  toolName: event.name,
4351
4413
  input: event.input,
4352
4414
  ...(event.callId ? { callId: event.callId } : {}),
4415
+ ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
4353
4416
  ...(session.selfAID ? { agentAid: session.selfAID } : {}),
4354
4417
  ...(permissionMode ? { permissionMode } : {}),
4355
4418
  timestamp: Date.now(),
4356
4419
  causation,
4357
4420
  });
4421
+ const desc = summarizeToolInput(event.name, event.input || {});
4422
+ const pendingInput = { name: event.name, input: event.input || {} };
4423
+ if (event.callId)
4424
+ toolInputByCallId.set(event.callId, pendingInput);
4425
+ if (event.correlationId)
4426
+ toolInputByCorrelationId.set(event.correlationId, pendingInput);
4427
+ else if (event.callId) {
4428
+ // Renderer/event-log normalization uses callId as the fallback
4429
+ // correlation. Keep the same alias here so a result carrying both
4430
+ // fields with that fallback still matches the original call.
4431
+ toolInputByCorrelationId.set(event.callId, pendingInput);
4432
+ }
4433
+ if (!event.callId && !event.correlationId) {
4434
+ const pending = anonymousToolInputsByName.get(event.name) ?? [];
4435
+ pending.push(pendingInput);
4436
+ anonymousToolInputsByName.set(event.name, pending);
4437
+ }
4358
4438
  if (!shouldSuppress()) {
4359
- const desc = summarizeToolInput(event.name, event.input || {});
4360
- if (event.callId) {
4439
+ if (event.callId)
4361
4440
  toolDescByCallId.set(event.callId, desc);
4362
- }
4363
4441
  renderer.addToolCall(event.name, event.input, event.callId, desc, event.turn, event.outputTokens);
4364
4442
  }
4365
4443
  if (event.callId && isCtlQueueReadCommand(event.name, event.input || {})) {
@@ -4375,7 +4453,7 @@ export class ResponseEngine {
4375
4453
  toolInput: event.input || {},
4376
4454
  injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
4377
4455
  getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
4378
- isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', session.selfAID),
4456
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID),
4379
4457
  logger,
4380
4458
  });
4381
4459
  }
@@ -4402,11 +4480,61 @@ export class ResponseEngine {
4402
4480
  isError: event.isError,
4403
4481
  agentName: agentNameForStats,
4404
4482
  ...(event.callId ? { callId: event.callId } : {}),
4483
+ ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
4405
4484
  ...(session.selfAID ? { agentAid: session.selfAID } : {}),
4406
4485
  ...(permissionMode ? { permissionMode } : {}),
4407
4486
  timestamp: Date.now(),
4408
4487
  causation,
4409
4488
  });
4489
+ // A proactive send gate advances only after the matching command
4490
+ // completed successfully and returned an explicit send receipt.
4491
+ let original;
4492
+ const byCallId = event.callId ? toolInputByCallId.get(event.callId) : undefined;
4493
+ const byCorrelationId = event.correlationId
4494
+ ? toolInputByCorrelationId.get(event.correlationId)
4495
+ : undefined;
4496
+ // Both identifiers must describe the same call. This avoids
4497
+ // accepting a stale/replayed correlation as the result of a new
4498
+ // call, or vice versa.
4499
+ const hasBothResultIdentifiers = !!event.callId && !!event.correlationId;
4500
+ if (hasBothResultIdentifiers
4501
+ && (!byCallId || !byCorrelationId || byCallId !== byCorrelationId)) {
4502
+ logger.warn(`[ResponseEngine] Ignoring mismatched tool result identifiers: session=${session.id}`);
4503
+ }
4504
+ else if (!hasBothResultIdentifiers) {
4505
+ original = byCallId ?? byCorrelationId;
4506
+ }
4507
+ else {
4508
+ original = byCallId;
4509
+ }
4510
+ if (!original && !event.callId && !event.correlationId) {
4511
+ const pending = anonymousToolInputsByName.get(event.name);
4512
+ // Without either identifier, FIFO is unsafe when same-named calls
4513
+ // overlap or complete out of order. Leave the gate unchanged until
4514
+ // a unique pending call can be matched.
4515
+ if (pending?.length === 1) {
4516
+ original = pending[0];
4517
+ anonymousToolInputsByName.delete(event.name);
4518
+ }
4519
+ }
4520
+ if (original)
4521
+ removePendingToolInput(original);
4522
+ if (original && modeHooks?.mode?.onToolResult) {
4523
+ const proactiveState = modeHooks.state.get('proactive');
4524
+ await modeHooks.mode.onToolResult({
4525
+ session,
4526
+ state: modeHooks.state,
4527
+ toolName: original.name,
4528
+ toolInput: original.input,
4529
+ result: event.result,
4530
+ isError: event.isError,
4531
+ error: event.error,
4532
+ injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
4533
+ getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
4534
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID),
4535
+ logger,
4536
+ });
4537
+ }
4410
4538
  // 从 tool_use 阶段缓存的描述中回溯
4411
4539
  const cachedDesc = event.callId ? toolDescByCallId.get(event.callId) : undefined;
4412
4540
  if (event.isError && !shouldSuppress()) {