evolcore 0.0.21 → 0.0.23

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 (112) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/codex-managed-hook.mjs +3 -0
  6. package/bin/install-codex-managed-hooks.mjs +376 -96
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +1 -1
  9. package/dist/agents/claude-runner.js +44 -226
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +154 -0
  14. package/dist/agents/codex-runner.js +1292 -219
  15. package/dist/agents/ecagent-runner.js +10 -31
  16. package/dist/agents/gemini-runner.js +2 -6
  17. package/dist/aun/aid/store.js +2 -0
  18. package/dist/aun/menu-client.js +272 -0
  19. package/dist/aun/msg/group.js +65 -4
  20. package/dist/aun/msg/index.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +51 -4
  22. package/dist/aun/outbox.js +7 -0
  23. package/dist/aun/rpc/client.js +8 -1
  24. package/dist/channels/aun.js +240 -36
  25. package/dist/channels/wechat.js +81 -10
  26. package/dist/cli/aun-commands.js +48 -4
  27. package/dist/cli/command-log.js +21 -2
  28. package/dist/cli/daemon-commands.js +169 -51
  29. package/dist/cli/index.js +20 -0
  30. package/dist/cli/init.js +2 -10
  31. package/dist/cli/menu-client.js +95 -0
  32. package/dist/cli/model.js +23 -8
  33. package/dist/cli/task-context.js +16 -2
  34. package/dist/cli/trigger-command.js +22 -5
  35. package/dist/config/config-field-policy.js +3 -0
  36. package/dist/config/config-manager.js +32 -5
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/schema-migration.js +14 -2
  39. package/dist/core/auth/agent-delegation.js +15 -0
  40. package/dist/core/auth/auth-gateway.js +8 -0
  41. package/dist/core/auth/authorization-audit.js +70 -6
  42. package/dist/core/auth/operation-catalog.js +4 -0
  43. package/dist/core/auth/remote-access-store.js +135 -0
  44. package/dist/core/bootstrap-messages.js +8 -0
  45. package/dist/core/bootstrap-service.js +93 -25
  46. package/dist/core/command/command-handler.js +62 -5
  47. package/dist/core/command/daemon-diagnostics.js +448 -0
  48. package/dist/core/command/menu-catalog.js +3 -0
  49. package/dist/core/command/menu-handler.js +150 -33
  50. package/dist/core/command/menu-protocol.js +120 -1
  51. package/dist/core/command/slash-gate.js +3 -3
  52. package/dist/core/command/slash-handler.js +83 -40
  53. package/dist/core/data-migration.js +11 -1
  54. package/dist/core/event-catalog.js +32 -0
  55. package/dist/core/handoff/runtime.js +23 -3
  56. package/dist/core/message/im-renderer.js +7 -3
  57. package/dist/core/message/message-bridge.js +60 -2
  58. package/dist/core/message/message-log.js +33 -0
  59. package/dist/core/message/message-queue.js +41 -15
  60. package/dist/core/message/response-engine.js +246 -114
  61. package/dist/core/model/catalog-service.js +308 -0
  62. package/dist/core/model/model-catalog.js +124 -334
  63. package/dist/core/model/model-diagnostics.js +4 -5
  64. package/dist/core/model/model-fallback.js +9 -16
  65. package/dist/core/model/model-permission.js +1 -1
  66. package/dist/core/permission/approval-gateway.js +16 -1
  67. package/dist/core/permission/ec-command-parser.js +272 -70
  68. package/dist/core/permission/protected-paths.js +74 -14
  69. package/dist/core/permission/sandbox-runtime.js +4 -2
  70. package/dist/core/permission/tool-error-code.js +12 -0
  71. package/dist/core/permission/tool-policy.js +290 -30
  72. package/dist/core/runtime-lock.js +13 -8
  73. package/dist/core/session/session-manager.js +30 -0
  74. package/dist/core/session/session-renew.js +18 -1
  75. package/dist/core/session/session-turn-coordinator.js +5 -1
  76. package/dist/index.js +104 -34
  77. package/dist/ipc.js +218 -96
  78. package/dist/paths.js +18 -0
  79. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  80. package/dist/stats/price-resolver.js +4 -0
  81. package/dist/trigger/feedback.js +14 -2
  82. package/dist/trigger/manager.js +3 -1
  83. package/dist/trigger/parser.js +86 -13
  84. package/dist/trigger/patch.js +104 -1
  85. package/dist/trigger/scheduler.js +20 -3
  86. package/dist/utils/ecweb-supervisor.js +11 -2
  87. package/dist/utils/ecweb-utils.js +8 -26
  88. package/dist/utils/error-utils.js +7 -0
  89. package/dist/utils/logger.js +9 -4
  90. package/dist/utils/npm-ops.js +47 -15
  91. package/dist/utils/tool-summary.js +59 -0
  92. package/dist/utils/windows-shell-trust.js +201 -0
  93. package/kits/docs/evolcore/INDEX.md +3 -2
  94. package/kits/docs/evolcore/agent-create.md +159 -0
  95. package/kits/docs/evolcore/agent.md +6 -0
  96. package/kits/docs/evolcore/group-collaboration.md +256 -0
  97. package/kits/docs/evolcore/group-rules.md +1 -19
  98. package/kits/docs/evolcore/group.md +3 -1
  99. package/kits/docs/evolcore/menu-client.md +34 -0
  100. package/kits/docs/evolcore/trigger.md +6 -3
  101. package/kits/docs/prompt-loading-architecture.md +6 -0
  102. package/kits/eck_message_manifest.json +6 -6
  103. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  104. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  105. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  106. package/kits/schemas/_meta.json +3 -2
  107. package/kits/schemas/agent-config.schema.12.json +427 -0
  108. package/kits/templates/message-fragments/item.md +1 -1
  109. package/kits/templates/roles/visitor.json +1 -0
  110. package/kits/templates/system-fragments/bootstrap.md +10 -2
  111. package/kits/templates/system-fragments/commands.md +2 -2
  112. package/package.json +11 -3
@@ -37,7 +37,7 @@ import { resolveAgentLifecycle } from '../../config/lifecycle.js';
37
37
  import { authorizationConfigRevision, checkRoleAccess, getFirstStaticAgentOwner, listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, roleToSessionIdentity, } from '../../config/peer-role-resolver.js';
38
38
  import { insertUsageEvent, insertContextBreakdown, insertModelCalls } from '../../stats/writer.js';
39
39
  import { normalizeUsage } from '../../stats/normalizer.js';
40
- import { resolvePrices } from '../../stats/price-resolver.js';
40
+ import { resolvePrices, roundCostForOutput } from '../../stats/price-resolver.js';
41
41
  import { getBudgetStatus } from '../../stats/budget.js';
42
42
  import { formatUsageSubjectKey, getRoleBudgetStatus } from '../../stats/role-budget.js';
43
43
  import { snapshot } from './response-snapshot.js';
@@ -47,7 +47,7 @@ import { registerBuiltinModes } from '../../response-system/modes/index.js';
47
47
  import { deriveSessionTitle, shouldAutoFillSessionTitle } from '../session/session-title.js';
48
48
  import { SessionTurnCoordinator } from '../session/session-turn-coordinator.js';
49
49
  import { recordTriggerExecutionAnomaly } from '../../trigger/anomaly-store.js';
50
- import { classifyToolErrorCode } from '../permission/tool-error-code.js';
50
+ import { classifyToolErrorCode, normalizeToolErrorCode } from '../permission/tool-error-code.js';
51
51
  import { normalizeExecutionPermissionMode } from '../permission/mode.js';
52
52
  import { buildToolLifecycleEventKey } from '../audit/event-key.js';
53
53
  import { isFullAccessEnabled, loadDaemonConfig } from '../../config-store.js';
@@ -325,44 +325,71 @@ function withFallbackModel(override, candidate) {
325
325
  ...(candidate.effort ? { effort: candidate.effort } : { effortMode: 'model_default' }),
326
326
  };
327
327
  }
328
+ /**
329
+ * Keep the per-task fullaccess fact in the user-message prompt rather than in
330
+ * the long-lived system/developer prompt. This avoids changing a runner's
331
+ * system-prompt cache while still giving the model an explicit, auditable
332
+ * execution-mode signal for the privileged task.
333
+ */
334
+ const FULL_ACCESS_MESSAGE_PROMPT_CONTEXT = '【当前权限模式提升为fullaccess,可不受限制执行命令,仅当前轮 task 有效】';
335
+ /**
336
+ * Put the one-task permission notice between the rendered message envelope and
337
+ * the user's literal input. The normal message fragment ends its envelope in
338
+ * `›`; matching the dated normal-message form avoids placing the notice inside
339
+ * an owner-hint envelope when one is present before the real inbound message.
340
+ */
341
+ function insertMessagePromptContext(prompt, context, userContent) {
342
+ if (!context)
343
+ return prompt;
344
+ const headers = [...prompt.matchAll(/‹\d{4}-\d{2}-\d{2} [^\n]*· from:[^\n]*›/gu)];
345
+ const renderedHeader = headers[0];
346
+ const headerEnd = renderedHeader
347
+ ? (renderedHeader.index ?? 0) + renderedHeader[0].length
348
+ : -1;
349
+ if (headerEnd >= 0) {
350
+ const remainder = prompt.slice(headerEnd).replace(/^\n+/u, '');
351
+ return `${prompt.slice(0, headerEnd)}\n${context}${remainder ? `\n${remainder}` : ''}`;
352
+ }
353
+ if (userContent?.trim()) {
354
+ const contentStart = prompt.indexOf(userContent);
355
+ if (contentStart >= 0) {
356
+ return `${prompt.slice(0, contentStart)}${context}\n${prompt.slice(contentStart)}`;
357
+ }
358
+ }
359
+ // Internal recovery prompts have no message envelope; keep their context at
360
+ // the front so the runner still sees it for the same task.
361
+ return `${context}\n\n${prompt}`;
362
+ }
328
363
  function getStreamErrorText(result) {
329
- return [
364
+ const parts = [
330
365
  result.errors?.join('\n'),
331
- result.lastReplyText,
332
- result.fullText,
333
366
  result.subtype,
334
367
  result.terminalReason,
335
368
  ]
336
- .filter((part) => typeof part === 'string' && part.trim().length > 0)
337
- .join('\n');
369
+ .filter((part) => typeof part === 'string' && part.trim().length > 0);
370
+ // Some older runners put the terminal diagnostic in `result`, but never
371
+ // treat streamed assistant text as an error merely because the turn failed.
372
+ const fullText = result.fullText.trim();
373
+ const fullTextLooksLikeDiagnostic = fullText.length > 0 && (isRetryableError(new Error(fullText))
374
+ || isContextTooLongText(fullText));
375
+ if (result.isError && !result.hasReceivedText && fullText
376
+ && (parts.length === 0 || fullTextLooksLikeDiagnostic)) {
377
+ parts.unshift(fullText);
378
+ }
379
+ return parts.join('\n');
338
380
  }
339
381
  function getStreamErrorMessage(result, includeEmoji = false) {
340
382
  const raw = getStreamErrorText(result) || '任务执行失败';
341
383
  const mapped = getErrorMessage(new Error(raw), result.terminalReason, includeEmoji);
342
384
  const generic = includeEmoji ? '❌ 处理消息时出错,请稍后重试' : '处理消息时出错,请稍后重试';
343
- if (!result.terminalReason && raw && mapped === generic) {
385
+ // Unknown runner-specific terminal reasons do not have a friendly mapping.
386
+ // Preserve their diagnostic text instead of collapsing it to the generic
387
+ // "处理消息时出错" message.
388
+ if (raw && mapped === generic) {
344
389
  return includeEmoji ? `❌ ${raw}` : raw;
345
390
  }
346
391
  return mapped;
347
392
  }
348
- function getRuntimeErrorMessage(raw, includeEmoji = false) {
349
- const fallback = raw || '任务执行失败';
350
- const mapped = getErrorMessage(new Error(fallback), undefined, includeEmoji);
351
- const generic = includeEmoji ? '❌ 处理消息时出错,请稍后重试' : '处理消息时出错,请稍后重试';
352
- if (mapped === generic)
353
- return includeEmoji ? `❌ ${fallback}` : fallback;
354
- return mapped;
355
- }
356
- function normalizeComparableText(text) {
357
- return text.replace(/\s+/g, ' ').trim();
358
- }
359
- function isPendingTextSameAsStreamError(pendingText, errorText) {
360
- const pending = normalizeComparableText(pendingText);
361
- const error = normalizeComparableText(errorText);
362
- if (!pending || !error || pending.length < 20)
363
- return false;
364
- return pending === error || pending.includes(error) || error.includes(pending);
365
- }
366
393
  function streamHitContextLimit(result) {
367
394
  return result.terminalReason === 'prompt_too_long' ||
368
395
  isContextTooLongText(result.lastReplyText) ||
@@ -446,12 +473,6 @@ function getContextTooLongHint(agent) {
446
473
  }
447
474
  return '上下文过长,请精简提问,或使用 /new 新建会话后继续';
448
475
  }
449
- function getContextCompactFailedHint(agent) {
450
- if (canCompactAgent(agent)) {
451
- return '上下文过长,自动压缩失败,请手动输入 /compact 重试';
452
- }
453
- return '上下文过长,请精简提问,或使用 /new 新建会话后继续';
454
- }
455
476
  function canCompactAgent(agent) {
456
477
  return hasCompact(agent) && agent.capabilities?.compact !== false;
457
478
  }
@@ -637,20 +658,44 @@ export class ResponseEngine {
637
658
  // 监听中断事件,标记被中断的 session
638
659
  this.eventBus.subscribe('task:interrupted', (event) => {
639
660
  if ('sessionId' in event && event.sessionId) {
661
+ const eventIdentity = event;
662
+ const eventTaskId = eventIdentity.taskId;
663
+ const activeTask = this.activeTaskSpans.get(event.sessionId);
664
+ // A delayed interruption from an older task must never invalidate a
665
+ // newer task that has already claimed the same session.
666
+ if (eventTaskId && activeTask && activeTask.taskId !== eventTaskId)
667
+ return;
640
668
  const reason = (event.reason || 'new_message');
641
- this.turnCoordinator.invalidate(event.sessionId, reason);
669
+ const currentTurn = this.turnCoordinator.invalidate(event.sessionId, reason, { taskId: eventTaskId, generation: eventIdentity.generation });
670
+ if ((eventTaskId || eventIdentity.generation !== undefined) && !currentTurn)
671
+ return;
642
672
  this.interruptedSessions.set(event.sessionId, reason);
643
673
  this.cancelRetryDelays(event.sessionId);
644
- this.agentDelegationRegistry?.revokeSession(event.sessionId);
674
+ // The event can be delayed relative to a newer task on the same
675
+ // session. Revoke only when the producer supplied the matching task;
676
+ // the interrupt path below performs the authoritative task-scoped
677
+ // revocation for legacy events without a taskId.
678
+ const interruptedTaskId = eventTaskId;
679
+ if (interruptedTaskId && activeTask?.taskId === interruptedTaskId) {
680
+ this.agentDelegationRegistry?.revokeTask(event.sessionId, interruptedTaskId);
681
+ }
645
682
  }
646
683
  });
647
684
  this.eventBus.subscribe('task:completed', event => {
648
- if ('sessionId' in event && event.sessionId)
649
- this.activeTaskSpans.delete(event.sessionId);
685
+ if ('sessionId' in event && event.sessionId) {
686
+ const taskId = event.taskId;
687
+ const active = this.activeTaskSpans.get(event.sessionId);
688
+ if (!taskId || active?.taskId === taskId)
689
+ this.activeTaskSpans.delete(event.sessionId);
690
+ }
650
691
  });
651
692
  this.eventBus.subscribe('task:error', event => {
652
- if ('sessionId' in event && event.sessionId)
653
- this.activeTaskSpans.delete(event.sessionId);
693
+ if ('sessionId' in event && event.sessionId) {
694
+ const taskId = event.taskId;
695
+ const active = this.activeTaskSpans.get(event.sessionId);
696
+ if (!taskId || active?.taskId === taskId)
697
+ this.activeTaskSpans.delete(event.sessionId);
698
+ }
654
699
  });
655
700
  // 初始化响应模式协调器,注册内置模式(interactive/proactive)
656
701
  const registry = new ResponseModeRegistry();
@@ -1018,7 +1063,7 @@ export class ResponseEngine {
1018
1063
  return true;
1019
1064
  }
1020
1065
  async retryAfterContextRecovery(prompt, opts) {
1021
- const { streamKey, renderer, agent, session, absoluteProjectPath, effectiveSystemPrompt, modelOverride, runtimeEnv, resetTimer, shouldSuppress, proactive, turnLease, beforeRunQuery, } = opts;
1066
+ const { streamKey, renderer, agent, session, absoluteProjectPath, effectiveSystemPrompt, modelOverride, runtimeEnv, resetTimer, shouldSuppress, proactive, turnLease, beforeRunQuery, messagePromptContext, } = opts;
1022
1067
  if (!session.agentSessionId || !canCompactAgent(agent)) {
1023
1068
  throw new Error('CONTEXT_COMPACT_FAILED');
1024
1069
  }
@@ -1057,7 +1102,7 @@ export class ResponseEngine {
1057
1102
  await this.emitOperationalNotice(renderer, '✅ 压缩完成,继续处理...', 'info', 'compact-retry');
1058
1103
  await renderer.flush();
1059
1104
  beforeRunQuery?.();
1060
- const retryStream = await agent.runQuery(session.id, prompt, absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, recoveryModelOverride, runtimeEnv);
1105
+ const retryStream = await agent.runQuery(session.id, insertMessagePromptContext(prompt, messagePromptContext), absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, recoveryModelOverride, runtimeEnv);
1061
1106
  if (await this.cancelStaleRunnerStart(agent, session.id, streamKey, turnLease, turnLease.taskId)) {
1062
1107
  throw new Error('RUNNER_START_SUPERSEDED');
1063
1108
  }
@@ -1274,7 +1319,7 @@ export class ResponseEngine {
1274
1319
  causation: opts.causation ?? message.causation,
1275
1320
  });
1276
1321
  }
1277
- publishTriggerExecutionSkipped(message, reason, causation) {
1322
+ publishTriggerExecutionSkipped(message, reason, causation, interruption) {
1278
1323
  const terminal = this.claimTriggerTerminal(message);
1279
1324
  if (!terminal)
1280
1325
  return;
@@ -1287,6 +1332,7 @@ export class ResponseEngine {
1287
1332
  attemptId: trigger.attemptId,
1288
1333
  originTriggerId: trigger.triggerId,
1289
1334
  reason,
1335
+ ...(interruption ?? {}),
1290
1336
  targetChannel: message.channel,
1291
1337
  targetChannelId: message.channelId,
1292
1338
  fireTime: trigger.fireTime,
@@ -1294,16 +1340,25 @@ export class ResponseEngine {
1294
1340
  });
1295
1341
  }
1296
1342
  /** Close the internal daemon conversation before returning from an interrupted Trigger turn. */
1297
- async publishTriggerExecutionInterrupted(message, adapter, envelope, reason, causation) {
1343
+ async publishTriggerExecutionInterrupted(message, adapter, envelope, reason, causation, generation) {
1344
+ const interruption = reason === 'stale_generation'
1345
+ ? {
1346
+ reasonCode: 'stale_generation',
1347
+ decisionSource: 'infrastructure',
1348
+ executionState: 'interrupted',
1349
+ ...(generation?.expected !== undefined ? { generation: generation.expected } : {}),
1350
+ ...(generation?.current !== undefined ? { currentGeneration: generation.current } : {}),
1351
+ }
1352
+ : undefined;
1298
1353
  if (this.isTrustedDaemonTrigger(message)) {
1299
1354
  await adapter.send(envelope, {
1300
1355
  kind: 'status.interrupted',
1301
- metadata: { reason },
1356
+ metadata: { reason, ...(interruption ?? {}) },
1302
1357
  }).catch(error => {
1303
1358
  logger.warn(`[ResponseEngine] Failed to close interrupted Trigger run=${message.triggerMeta?.runId ?? '<unknown>'}: ${error instanceof Error ? error.message : String(error)}`);
1304
1359
  });
1305
1360
  }
1306
- this.publishTriggerExecutionSkipped(message, reason, causation);
1361
+ this.publishTriggerExecutionSkipped(message, reason, causation, interruption);
1307
1362
  }
1308
1363
  publishTriggerExecutionCompleted(message, messageId, durationMs, causation) {
1309
1364
  const terminal = this.claimTriggerTerminal(message);
@@ -1331,11 +1386,11 @@ export class ResponseEngine {
1331
1386
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
1332
1387
  '/stop', '/pause', '/resume', '/clear', '/compact', '/renew', '/del', '/perm', '/file', '/check',
1333
1388
  '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode',
1334
- '/aid', '/upgrade', '/evolagent',
1389
+ '/aid', '/upgrade', '/evolagent', '/pair ',
1335
1390
  ];
1336
1391
  /** 判断消息内容是否为已知命令 */
1337
1392
  isKnownCommand(content) {
1338
- return content === '/s' ||
1393
+ return content === '/s' || content === '/pair' ||
1339
1394
  ResponseEngine.COMMAND_PREFIXES.some(cmd => content.startsWith(cmd));
1340
1395
  }
1341
1396
  /**
@@ -2130,6 +2185,8 @@ export class ResponseEngine {
2130
2185
  this.eventBus.publish({
2131
2186
  type: 'task:error',
2132
2187
  sessionId: session.id,
2188
+ taskId,
2189
+ generation: turnLease?.generation,
2133
2190
  error: timeoutError.message,
2134
2191
  errorType,
2135
2192
  agentName: agentNameForStats,
@@ -2255,7 +2312,10 @@ export class ResponseEngine {
2255
2312
  const contentPreview = formatInboundMessageLogText(message.content);
2256
2313
  logger.info(`[${message.channel}] ${message.channelId}: ${contentPreview}${imageInfo}${modeInfo}${e2eeInfo}`);
2257
2314
  // 构建 peer 标识(优先 peerName,退化到 peerId / channelId)
2258
- const peerName = session.metadata?.peerName ?? message.peerName;
2315
+ // Group sessions are shared by multiple senders. The current inbound
2316
+ // message is authoritative; session metadata is only a fallback for
2317
+ // legacy paths that do not carry a per-message display name.
2318
+ const peerName = message.peerName ?? session.metadata?.peerName;
2259
2319
  const peerId = session.metadata?.peerId ?? message.peerId ?? message.channelId;
2260
2320
  const peerShort = peerId ? peerId.split('.')[0].split(':')[0] : '?';
2261
2321
  const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
@@ -2266,7 +2326,16 @@ export class ResponseEngine {
2266
2326
  turnLease = await this.turnCoordinator.begin(session, taskId);
2267
2327
  // 记录开始处理
2268
2328
  const taskEncrypt = message.replyContext?.metadata?.encrypted != null ? !!(message.replyContext.metadata.encrypted) : undefined;
2269
- this.eventBus.publish({ type: 'task:started', sessionId: session.id, agentName: agentNameForStats, encrypt: taskEncrypt, chatmode, causation: taskCausation });
2329
+ this.eventBus.publish({
2330
+ type: 'task:started',
2331
+ sessionId: session.id,
2332
+ taskId,
2333
+ generation: turnLease.generation,
2334
+ agentName: agentNameForStats,
2335
+ encrypt: taskEncrypt,
2336
+ chatmode,
2337
+ causation: taskCausation,
2338
+ });
2270
2339
  this.touchAgentActivity(channelKey);
2271
2340
  // Upgrade the channel acknowledgement at task start, before compaction and
2272
2341
  // runner setup, so processing feedback remains visible for the whole run.
@@ -2292,6 +2361,7 @@ export class ResponseEngine {
2292
2361
  adapter,
2293
2362
  envelope,
2294
2363
  agentAid: session.selfAID,
2364
+ permissionMode: () => effectivePermissionMode,
2295
2365
  flushDelay: (options?.flushDelay ?? this.agentRegistry?.resolveByChannel(channelKey)?.config?.flush_delay ?? 3) * 1000,
2296
2366
  suppressActivityItems: isProactive ? false : middleOutputMode !== 'all',
2297
2367
  suppressIntermediateText: isProactive ? false : middleOutputMode === 'none',
@@ -2640,6 +2710,7 @@ export class ResponseEngine {
2640
2710
  : body;
2641
2711
  // 先用裸文本兜底;vars 构造完成后用消息渲染层重算(见下方 effectivePrompt 重赋值)。
2642
2712
  let effectivePrompt = wrapPrompt(message.content);
2713
+ let fullAccessMessagePromptContext;
2643
2714
  let renderResult;
2644
2715
  let effectiveSystemPrompt;
2645
2716
  let modelOverride;
@@ -2735,6 +2806,9 @@ export class ResponseEngine {
2735
2806
  // records all observe the same normalized value.
2736
2807
  permissionContext.permissionMode = effectivePermissionMode;
2737
2808
  permissionContext.executionPermission = fullAccessAuthorization;
2809
+ fullAccessMessagePromptContext = fullAccessAuthorization
2810
+ ? FULL_ACCESS_MESSAGE_PROMPT_CONTEXT
2811
+ : undefined;
2738
2812
  // 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
2739
2813
  // 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
2740
2814
  // 多对端并发各自独立解析、各自传参,无共享状态可被污染。
@@ -2829,7 +2903,9 @@ export class ResponseEngine {
2829
2903
  agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
2830
2904
  primaryModel = modelOverride?.model || agentModel;
2831
2905
  if (!hasExplicitTriggerModel && primaryModel) {
2832
- const fallbackResolution = eligibleFallbackModels({
2906
+ const fallbackResolution = await eligibleFallbackModels({
2907
+ selfAid: selfAid || undefined,
2908
+ catalog: agent.getModelCatalog?.bind(agent),
2833
2909
  baseagent: normalizedBaseagent.canonical,
2834
2910
  currentModel: primaryModel,
2835
2911
  resolveModelId: typeof agent.resolveModelId === 'function'
@@ -3108,7 +3184,7 @@ export class ResponseEngine {
3108
3184
  }];
3109
3185
  const peerItems = (() => {
3110
3186
  if (message.handoffDelivery && this.handoffRuntime) {
3111
- const items = this.handoffRuntime.buildPromptItems(message);
3187
+ const items = this.handoffRuntime.buildPromptItems(message, peerRole);
3112
3188
  if (items.length > 0) {
3113
3189
  v2HandoffIds = Array.from(new Set(items.flatMap(item => (item.handoff?.handoffIds?.length
3114
3190
  ? item.handoff.handoffIds
@@ -3169,6 +3245,12 @@ export class ResponseEngine {
3169
3245
  if (v2HandoffIds.length > 0 && renderResult?.body.trim())
3170
3246
  handoffPromptRendered = true;
3171
3247
  }
3248
+ // Keep this notice in the per-turn message prompt. Injecting it into
3249
+ // the system prompt would alter long-lived runner prompt caches and
3250
+ // could make a one-turn grant look persistent.
3251
+ if (fullAccessMessagePromptContext && effectivePrompt.trim()) {
3252
+ effectivePrompt = insertMessagePromptContext(effectivePrompt, fullAccessMessagePromptContext, message.content);
3253
+ }
3172
3254
  // 空消息防护:在 agent 调用之前检查 prompt 是否为空
3173
3255
  // 防止空消息(或纯空格消息)浪费 API 调用
3174
3256
  if (!effectivePrompt.trim()) {
@@ -3205,12 +3287,14 @@ export class ResponseEngine {
3205
3287
  peerName: peerName || undefined,
3206
3288
  peerType: message.peerType || session.metadata?.peerType || undefined,
3207
3289
  peerRole,
3290
+ permissionMode: effectivePermissionMode,
3208
3291
  ...(fullAccessAuthorization ? {
3209
3292
  processRole: fullAccessAuthorization.processRole,
3210
3293
  dataScope: fullAccessAuthorization.dataScope,
3211
3294
  authorizedBy: fullAccessAuthorization.authorizedBy,
3212
3295
  executionSource: fullAccessAuthorization.source,
3213
3296
  } : {}),
3297
+ daemonRuntimeEpoch: this.agentDelegationRegistry?.getRuntimeEpoch(),
3214
3298
  threadId: session.threadId || undefined,
3215
3299
  sessionRuntimeDir,
3216
3300
  runtimeLockDir: ensureRuntimeLockDir(sessionRuntimeDir),
@@ -3476,6 +3560,7 @@ export class ResponseEngine {
3476
3560
  proactiveSelfAid: selfAid,
3477
3561
  permissionMode: effectivePermissionMode,
3478
3562
  turnLease,
3563
+ messagePromptContext: fullAccessMessagePromptContext,
3479
3564
  beforeRunQuery: fullAccessAuthorization
3480
3565
  ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
3481
3566
  : undefined,
@@ -3522,6 +3607,7 @@ export class ResponseEngine {
3522
3607
  proactiveSelfAid: selfAid,
3523
3608
  permissionMode: effectivePermissionMode,
3524
3609
  turnLease,
3610
+ messagePromptContext: fullAccessMessagePromptContext,
3525
3611
  beforeRunQuery: fullAccessAuthorization
3526
3612
  ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
3527
3613
  : undefined,
@@ -3547,7 +3633,10 @@ export class ResponseEngine {
3547
3633
  }
3548
3634
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
3549
3635
  logger.info(`[ResponseEngine] Stale turn stopped before publish: session=${session.id} task=${taskId}`);
3550
- await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_generation', taskCausation);
3636
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_generation', taskCausation, {
3637
+ expected: turnLease?.generation,
3638
+ current: this.turnCoordinator.current(session).generation,
3639
+ });
3551
3640
  return;
3552
3641
  }
3553
3642
  if (!commitDecision.ok) {
@@ -3559,7 +3648,10 @@ export class ResponseEngine {
3559
3648
  }
3560
3649
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
3561
3650
  logger.info(`[ResponseEngine] Stale turn stopped before protocol rejection: session=${session.id} task=${taskId}`);
3562
- await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_generation', taskCausation);
3651
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_generation', taskCausation, {
3652
+ expected: turnLease?.generation,
3653
+ current: this.turnCoordinator.current(session).generation,
3654
+ });
3563
3655
  return;
3564
3656
  }
3565
3657
  const reason = streamResult.protocolIncompleteReason || commitDecision.reason;
@@ -3761,7 +3853,10 @@ export class ResponseEngine {
3761
3853
  if (!commitDecision.ok) {
3762
3854
  logger.info(`[ResponseEngine] Turn commit rejected after flush: session=${session.id} task=${taskId} reason=${commitDecision.reason}`);
3763
3855
  if (commitDecision.reason === 'stale_generation') {
3764
- await this.publishTriggerExecutionInterrupted(message, adapter, envelope, commitDecision.reason, taskCausation);
3856
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, commitDecision.reason, taskCausation, {
3857
+ expected: turnLease?.generation,
3858
+ current: this.turnCoordinator.current(session).generation,
3859
+ });
3765
3860
  }
3766
3861
  else {
3767
3862
  this.publishTriggerExecutionFailure(message, `turn_commit_rejected:${commitDecision.reason}`, {
@@ -3848,6 +3943,8 @@ export class ResponseEngine {
3848
3943
  this.eventBus.publish({
3849
3944
  type: 'task:error',
3850
3945
  sessionId: session.id,
3946
+ taskId,
3947
+ generation: turnLease?.generation,
3851
3948
  error: errorSummary,
3852
3949
  errorType,
3853
3950
  agentName: agentNameForStats,
@@ -3965,12 +4062,18 @@ export class ResponseEngine {
3965
4062
  cache_read_tokens: sum.cache_read_tokens,
3966
4063
  cache_creation_tokens: sum.cache_creation_tokens,
3967
4064
  // 顶层 cost_usd/cost_cny 保持向后兼容 = 网关实际价
3968
- cost_usd: sum.cost_gateway_usd,
3969
- cost_cny: sum.cost_gateway_cny,
4065
+ cost_usd: roundCostForOutput(sum.cost_gateway_usd),
4066
+ cost_cny: roundCostForOutput(sum.cost_gateway_cny),
3970
4067
  call_count: sum.calls,
3971
4068
  cost: {
3972
- official: { usd: sum.cost_official_usd, cny: sum.cost_official_cny },
3973
- gateway: { usd: sum.cost_gateway_usd, cny: sum.cost_gateway_cny },
4069
+ official: {
4070
+ usd: roundCostForOutput(sum.cost_official_usd),
4071
+ cny: roundCostForOutput(sum.cost_official_cny),
4072
+ },
4073
+ gateway: {
4074
+ usd: roundCostForOutput(sum.cost_gateway_usd),
4075
+ cny: roundCostForOutput(sum.cost_gateway_cny),
4076
+ },
3974
4077
  },
3975
4078
  };
3976
4079
  }
@@ -3988,10 +4091,13 @@ export class ResponseEngine {
3988
4091
  }
3989
4092
  else {
3990
4093
  // cost 同时给原价(official)与网关实际价(gateway);顶层 cost_usd/cost_cny 保持向后兼容 = 网关价。
3991
- const gatewayUsd = turnCost.gateway?.usd ?? turnCost.official?.usd ?? 0;
3992
- const gatewayCny = turnCost.gateway?.cny ?? turnCost.official?.cny ?? 0;
4094
+ const gatewayUsd = roundCostForOutput(turnCost.gateway?.usd ?? turnCost.official?.usd ?? 0);
4095
+ const gatewayCny = roundCostForOutput(turnCost.gateway?.cny ?? turnCost.official?.cny ?? 0);
3993
4096
  const turnCostBlock = {
3994
- official: { usd: turnCost.official?.usd ?? 0, cny: turnCost.official?.cny ?? 0 },
4097
+ official: {
4098
+ usd: roundCostForOutput(turnCost.official?.usd ?? 0),
4099
+ cny: roundCostForOutput(turnCost.official?.cny ?? 0),
4100
+ },
3995
4101
  gateway: { usd: gatewayUsd, cny: gatewayCny },
3996
4102
  };
3997
4103
  // 最后一次访问:本轮可能有多次大模型调用(numTurns>1),整轮的 turnCostBlock 不等于
@@ -4006,10 +4112,13 @@ export class ResponseEngine {
4006
4112
  model: lastModel, turns: 1,
4007
4113
  });
4008
4114
  const lp = resolvePrices(resolveRoot(), lastEvent, agent.getGatewayPricing?.());
4009
- const lpGwUsd = lp.gateway?.usd ?? lp.official?.usd ?? 0;
4010
- const lpGwCny = lp.gateway?.cny ?? lp.official?.cny ?? 0;
4115
+ const lpGwUsd = roundCostForOutput(lp.gateway?.usd ?? lp.official?.usd ?? 0);
4116
+ const lpGwCny = roundCostForOutput(lp.gateway?.cny ?? lp.official?.cny ?? 0);
4011
4117
  lastModelCall = { ...lastModelCall, cost: {
4012
- official: { usd: lp.official?.usd ?? 0, cny: lp.official?.cny ?? 0 },
4118
+ official: {
4119
+ usd: roundCostForOutput(lp.official?.usd ?? 0),
4120
+ cny: roundCostForOutput(lp.official?.cny ?? 0),
4121
+ },
4013
4122
  gateway: { usd: lpGwUsd, cny: lpGwCny },
4014
4123
  } };
4015
4124
  }
@@ -4057,6 +4166,8 @@ export class ResponseEngine {
4057
4166
  this.eventBus.publish({
4058
4167
  type: 'task:completed',
4059
4168
  sessionId: session.id,
4169
+ taskId,
4170
+ generation: turnLease.generation,
4060
4171
  channel: message.channel,
4061
4172
  channelId: message.channelId,
4062
4173
  terminalReason: streamResult.terminalReason,
@@ -4188,6 +4299,8 @@ export class ResponseEngine {
4188
4299
  this.eventBus.publish({
4189
4300
  type: 'task:error',
4190
4301
  sessionId: session.id,
4302
+ taskId,
4303
+ generation: turnLease?.generation,
4191
4304
  error: errorMsg,
4192
4305
  errorType,
4193
4306
  agentName: agentNameForStats,
@@ -4208,14 +4321,15 @@ export class ResponseEngine {
4208
4321
  }
4209
4322
  // 发送用户友好的错误消息
4210
4323
  // 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送错误提示
4211
- // processEventStream 已通过 renderer 发过错误时也跳过
4324
+ // 普通渠道可跳过 renderer 已发送的重复错误;daemon trigger 仍必须
4325
+ // 发送结构化终态,否则 DaemonChannel 会一直等到 watchdog。
4212
4326
  const retryExhaustedCount = getRetryExhaustedCount(error);
4213
4327
  const modelFallbackExhaustedMessage = getModelFallbackExhaustedMessage(error);
4214
4328
  const retryInputAlreadySubmitted = wasRetryInputAlreadySubmitted(error);
4215
4329
  if (isUserInterrupt) {
4216
4330
  logger.info(`[ResponseEngine] User interrupt by new_message, skip sending error message`);
4217
4331
  }
4218
- else if (error?._errorAlreadySent && !retryExhaustedCount && !isTimeout && !isTotalExecutionTimeout) {
4332
+ else if (error?._errorAlreadySent && !daemonTrigger && !retryExhaustedCount && !isTimeout && !isTotalExecutionTimeout) {
4219
4333
  logger.info(`[ResponseEngine] Error already sent via renderer, skip sending duplicate message`);
4220
4334
  }
4221
4335
  else {
@@ -4633,11 +4747,17 @@ export class ResponseEngine {
4633
4747
  if (event.type === 'complete') {
4634
4748
  event = normalizeCompleteAgentEvent(event);
4635
4749
  }
4636
- if (event.type === 'tool_result' && event.isError && !event.errorCode) {
4637
- event = {
4638
- ...event,
4639
- errorCode: classifyToolErrorCode({ error: event.error, result: event.result }),
4640
- };
4750
+ if (event.type === 'tool_result' && event.isError) {
4751
+ const classificationMissing = !normalizeToolErrorCode(event.errorCode)
4752
+ && event.decisionSource !== 'policy'
4753
+ && event.decisionSource !== 'approval';
4754
+ const errorCode = classifyToolErrorCode({
4755
+ errorCode: event.errorCode,
4756
+ decisionSource: event.decisionSource,
4757
+ });
4758
+ if (event.errorCode !== errorCode) {
4759
+ event = { ...event, errorCode, ...(classificationMissing ? { classificationMissing: true } : {}) };
4760
+ }
4641
4761
  }
4642
4762
  // 每收到事件重置空闲超时
4643
4763
  const toolName = event.type === 'tool_use' ? event.name : undefined;
@@ -4740,7 +4860,13 @@ export class ResponseEngine {
4740
4860
  eventDetail = ` tool=${event.name}${desc ? ` desc="${desc}"` : ''}`;
4741
4861
  }
4742
4862
  else if (event.type === 'tool_result') {
4743
- eventDetail = ` tool=${event.name} ok=${!event.isError}`;
4863
+ const decision = event.decision ?? (event.isError ? 'error' : 'allow');
4864
+ const executed = event.executed ?? !event.isError;
4865
+ const executionState = event.executionState
4866
+ ?? (decision === 'deny' ? 'blocked' : event.isError ? 'failed' : 'completed');
4867
+ eventDetail = ` tool=${event.name} ok=${decision === 'allow' && executed}`
4868
+ + ` decision=${decision} executed=${executed} executionState=${executionState}`
4869
+ + (event.policyCode ? ` policy=${event.policyCode}` : '');
4744
4870
  }
4745
4871
  const frameworkEvents = new Set(['session_id', 'state_changed', 'status']);
4746
4872
  if (frameworkEvents.has(event.type)) {
@@ -5006,6 +5132,16 @@ export class ResponseEngine {
5006
5132
  toolName: event.name,
5007
5133
  toolInput: event.input || {},
5008
5134
  injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
5135
+ recordReminder: reminder => {
5136
+ this.eventBus.publish({
5137
+ type: 'runner:proactive-reminder',
5138
+ sessionId: session.id,
5139
+ ...reminder,
5140
+ injection: 'requested',
5141
+ timestamp: Date.now(),
5142
+ causation,
5143
+ });
5144
+ },
5009
5145
  getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
5010
5146
  isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID),
5011
5147
  logger,
@@ -5031,6 +5167,7 @@ export class ResponseEngine {
5031
5167
  sessionId: session.id,
5032
5168
  callId: event.callId,
5033
5169
  correlationId: event.correlationId,
5170
+ requestId: event.requestId,
5034
5171
  });
5035
5172
  this.eventBus.publish({
5036
5173
  type: 'tool:result',
@@ -5039,16 +5176,29 @@ export class ResponseEngine {
5039
5176
  sessionId: session.id,
5040
5177
  toolName: event.name,
5041
5178
  isError: event.isError,
5042
- ...(event.isError ? { errorCode: classifyToolErrorCode({ errorCode: event.errorCode, error: event.error, result: event.result }) } : {}),
5179
+ ...(event.isError ? {
5180
+ errorCode: classifyToolErrorCode({
5181
+ errorCode: event.errorCode,
5182
+ decisionSource: event.decisionSource,
5183
+ }),
5184
+ ...((event.classificationMissing || (!normalizeToolErrorCode(event.errorCode)
5185
+ && event.decisionSource !== 'policy'
5186
+ && event.decisionSource !== 'approval')) ? { classificationMissing: true } : {}),
5187
+ } : {}),
5043
5188
  ...(event.callId ? { callId: event.callId } : {}),
5044
5189
  ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
5190
+ ...(event.requestId ? { requestId: event.requestId } : {}),
5045
5191
  agentName: lifecycleAgentName,
5046
5192
  agentAid: lifecycleAgentAid ?? 'unknown',
5047
5193
  permissionMode: lifecyclePermissionMode,
5048
- decision: event.isError ? 'error' : 'allow',
5049
- decisionSource: 'runner',
5050
- executed: true,
5051
- executionState: event.isError ? 'failed' : 'completed',
5194
+ decision: event.decision ?? (event.isError ? 'error' : 'allow'),
5195
+ decisionSource: event.decisionSource ?? 'runner',
5196
+ ...(event.policyCode ? { policyCode: event.policyCode } : {}),
5197
+ ...(event.reason ? { reason: event.reason } : {}),
5198
+ executed: event.executed ?? (event.decision === 'deny' ? false : true),
5199
+ executionState: event.executionState ?? (event.decision === 'deny'
5200
+ ? 'blocked'
5201
+ : event.isError ? 'failed' : 'completed'),
5052
5202
  timestamp: Date.now(),
5053
5203
  causation,
5054
5204
  });
@@ -5095,7 +5245,23 @@ export class ResponseEngine {
5095
5245
  result: event.result,
5096
5246
  isError: event.isError,
5097
5247
  error: event.error,
5248
+ decision: event.decision,
5249
+ decisionSource: event.decisionSource,
5250
+ executed: event.executed,
5251
+ executionState: event.executionState,
5252
+ policyCode: event.policyCode,
5253
+ requestId: event.requestId,
5098
5254
  injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
5255
+ recordReminder: reminder => {
5256
+ this.eventBus.publish({
5257
+ type: 'runner:proactive-reminder',
5258
+ sessionId: session.id,
5259
+ ...reminder,
5260
+ injection: 'requested',
5261
+ timestamp: Date.now(),
5262
+ causation,
5263
+ });
5264
+ },
5099
5265
  getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
5100
5266
  isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID),
5101
5267
  logger,
@@ -5123,8 +5289,10 @@ export class ResponseEngine {
5123
5289
  // 运行时错误(Codex: turn.failed / item error)
5124
5290
  if (event.type === 'error') {
5125
5291
  logger.warn(`[ResponseEngine] error event: ${event.errorType}: ${event.error}`);
5126
- // 记录错误文本到 lastReplyText,供后续 isPromptTooLong 检测
5127
- lastReplyText += event.error || '';
5292
+ // Keep runtime diagnostics separate from assistant prose. Appending
5293
+ // an error to lastReplyText makes the terminal flush treat the error
5294
+ // as user-visible model output and can also make it look like a
5295
+ // duplicate of a pending text event.
5128
5296
  completeResult = {
5129
5297
  ...completeResult,
5130
5298
  isError: true,
@@ -5134,14 +5302,6 @@ export class ResponseEngine {
5134
5302
  lastReplyText,
5135
5303
  fullText: '',
5136
5304
  };
5137
- // 上下文过长的错误不在此处输出 notice,留给外层 isPromptTooLong 触发 auto-compact
5138
- const isContextError = isContextTooLongText(event.error || '');
5139
- const isRetryableRuntimeError = isRetryableError(new Error(event.error || ''));
5140
- const isRecoverableModelError = classifyError(new Error(event.error || '')) === ErrorType.MODEL_UNAVAILABLE;
5141
- if (!isContextError && !isRetryableRuntimeError && !isRecoverableModelError && !hasErrorResult && !shouldSuppress()) {
5142
- hasErrorResult = true;
5143
- renderer.addNotice(getRuntimeErrorMessage(event.error || '任务执行失败', false), 'warn', 'runtime-error', true);
5144
- }
5145
5305
  }
5146
5306
  // 完成事件
5147
5307
  // SDK 可能产生多个 complete 事件(如 subagent 或 auto-compact 二次查询),
@@ -5160,34 +5320,6 @@ export class ResponseEngine {
5160
5320
  madeProgress = true;
5161
5321
  // thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
5162
5322
  // 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
5163
- // 失败且无前置错误输出:显示 errors 摘要
5164
- // 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
5165
- // 上下文过长的错误留给外层 isPromptTooLong 触发 auto-compact,不在此处输出
5166
- const interruptReason = this.interruptedSessions.get(session.id);
5167
- const isUserInterrupt = isNonErrorInterruptReason(interruptReason)
5168
- || isExpectedTerminalInterrupt(event.terminalReason);
5169
- const isContextTooLong = event.terminalReason === 'prompt_too_long'
5170
- || isContextTooLongText(event.errors?.join(' ') || '')
5171
- || isContextTooLongText(lastReplyText);
5172
- const completeErrorText = getStreamErrorText(completeResult);
5173
- const isRetryableCompleteError = event.isError && !isContextTooLong && completeErrorText
5174
- ? isRetryableError(new Error(completeErrorText))
5175
- : false;
5176
- const isRecoverableModelError = event.isError && !isContextTooLong && completeErrorText
5177
- ? classifyError(new Error(completeErrorText)) === ErrorType.MODEL_UNAVAILABLE
5178
- : false;
5179
- if (event.isError && completeErrorText && isPendingTextSameAsStreamError(renderer.getRemainingText(), completeErrorText)) {
5180
- renderer.discardPendingText();
5181
- }
5182
- if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong && !isRetryableCompleteError && !isRecoverableModelError) {
5183
- // 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
5184
- const userFriendlyMessage = event.terminalReason === 'prompt_too_long'
5185
- ? getContextTooLongHint(agent)
5186
- : event.terminalReason === 'context_compact_failed'
5187
- ? getContextCompactFailedHint(agent)
5188
- : getStreamErrorMessage(completeResult, false);
5189
- renderer.addNotice(userFriendlyMessage, 'warn', 'task-error', true);
5190
- }
5191
5323
  // 中间 complete:flush 掉已有 activities(不带 isFinal),让中间结果及时显示
5192
5324
  // 最终文本留给流结束后的统一 flush(true)
5193
5325
  if (renderer.hasContent()) {