evolcore 0.0.32 → 0.0.34

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 (214) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +135 -37
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +111 -15
  6. package/dist/agents/codex/app-server-client.js +3 -0
  7. package/dist/agents/codex/diagnostics.js +112 -54
  8. package/dist/agents/codex-runner.js +241 -63
  9. package/dist/agents/ecagent-runner.js +78 -33
  10. package/dist/agents/runner-types.js +5 -6
  11. package/dist/aun/aid/identity.js +1 -1
  12. package/dist/aun/group/operations.js +0 -1
  13. package/dist/aun/msg/history.js +1 -0
  14. package/dist/aun/msg/p2p.js +0 -1
  15. package/dist/aun/outbox.js +50 -1
  16. package/dist/channels/aun.js +44 -8
  17. package/dist/channels/daemon.js +261 -3
  18. package/dist/cli/agent-command.js +58 -24
  19. package/dist/cli/agent.js +1 -1
  20. package/dist/cli/aun-commands.js +368 -31
  21. package/dist/cli/contact.js +8 -2
  22. package/dist/cli/ctl-command.js +3 -1
  23. package/dist/cli/daemon-commands.js +197 -27
  24. package/dist/cli/debug-mode.js +154 -0
  25. package/dist/cli/fs-command.js +2 -1
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +98 -67
  28. package/dist/cli/init.js +2 -2
  29. package/dist/cli/integrity-command.js +196 -0
  30. package/dist/cli/managed-command-guard.js +5 -0
  31. package/dist/cli/memory-command.js +166 -146
  32. package/dist/cli/queue-command.js +9 -4
  33. package/dist/cli/session-command.js +194 -0
  34. package/dist/cli/skill-command.js +78 -2
  35. package/dist/cli/task-context.js +1 -0
  36. package/dist/cli/thread-command.js +622 -0
  37. package/dist/cli/trigger-command.js +10 -3
  38. package/dist/config/builtin-roles.js +1 -0
  39. package/dist/config/config-field-policy.js +6 -3
  40. package/dist/config/config-manager.js +26 -2
  41. package/dist/config/contact-book-store.js +5 -0
  42. package/dist/config/resolved-config-op.js +3 -0
  43. package/dist/config/role-service.js +13 -0
  44. package/dist/config/runtime-env.js +16 -0
  45. package/dist/config-store.js +45 -0
  46. package/dist/core/audit/log-integrity.js +3 -1
  47. package/dist/core/auth/agent-delegation.js +133 -36
  48. package/dist/core/auth/debug-context.js +139 -0
  49. package/dist/core/auth/delegation-diagnostics.js +414 -0
  50. package/dist/core/auth/operation-authorizer.js +25 -2
  51. package/dist/core/auth/operation-catalog.js +49 -0
  52. package/dist/core/background-session/session-cleanup.js +212 -0
  53. package/dist/core/background-session/types.js +10 -0
  54. package/dist/core/bootstrap-messages.js +1 -1
  55. package/dist/core/bootstrap-service.js +6 -3
  56. package/dist/core/command/agent-control.js +18 -26
  57. package/dist/core/command/command-handler.js +197 -27
  58. package/dist/core/command/daemon-diagnostics.js +1 -0
  59. package/dist/core/command/memory-slash.js +6 -2
  60. package/dist/core/command/menu-catalog.js +51 -1
  61. package/dist/core/command/menu-handler.js +114 -23
  62. package/dist/core/command/menu-protocol.js +25 -1
  63. package/dist/core/command/skill-slash.js +322 -0
  64. package/dist/core/command/slash-gate.js +6 -4
  65. package/dist/core/command/slash-handler.js +195 -13
  66. package/dist/core/command/topic-slash.js +528 -0
  67. package/dist/core/daemon-bootstrap.js +4 -7
  68. package/dist/core/evolagent-registry.js +20 -5
  69. package/dist/core/file-integrity/read.js +61 -0
  70. package/dist/core/file-integrity/store.js +262 -0
  71. package/dist/core/handoff/runtime.js +5 -11
  72. package/dist/core/handoff/store.js +5 -0
  73. package/dist/core/ipc-registration.js +175 -3
  74. package/dist/core/memory/batch-meta.js +132 -0
  75. package/dist/core/memory/consolidation-context.js +83 -29
  76. package/dist/core/memory/consolidation-ops-records.js +49 -16
  77. package/dist/core/memory/consolidation-queue-format.js +21 -0
  78. package/dist/core/memory/consolidation.js +264 -205
  79. package/dist/core/memory/index-manager.js +19 -28
  80. package/dist/core/memory/memory-admin.js +19 -14
  81. package/dist/core/memory/memory-document.js +53 -9
  82. package/dist/core/memory/memory-writer.js +30 -15
  83. package/dist/core/message/aun-ipc-sender.js +161 -6
  84. package/dist/core/message/im-renderer.js +169 -10
  85. package/dist/core/message/message-bridge.js +15 -4
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/peer-mode.js +6 -1
  88. package/dist/core/message/response-engine.js +929 -110
  89. package/dist/core/message/subtask-tool-projection.js +140 -0
  90. package/dist/core/model/catalog-service.js +17 -12
  91. package/dist/core/permission/ec-command-parser.js +217 -9
  92. package/dist/core/permission/protected-paths.js +5 -1
  93. package/dist/core/permission/readonly-shell-query.js +43 -5
  94. package/dist/core/permission/session-path-grants.js +334 -0
  95. package/dist/core/permission/tool-canonical.js +226 -0
  96. package/dist/core/permission/tool-policy.js +30 -62
  97. package/dist/core/role/runtime-policy.js +21 -10
  98. package/dist/core/session/session-fs-store.js +123 -8
  99. package/dist/core/session/session-idle-monitor.js +249 -22
  100. package/dist/core/session/session-manager.js +129 -14
  101. package/dist/core/skill/command-parser.js +13 -0
  102. package/dist/core/skill/index-generator.js +21 -4
  103. package/dist/core/skill/startup.js +41 -0
  104. package/dist/core/skill/switch-resolver.js +55 -29
  105. package/dist/core/system-session/grants.js +40 -0
  106. package/dist/core/system-session/memory.js +222 -0
  107. package/dist/core/system-session/registry.js +74 -0
  108. package/dist/core/system-session/types.js +14 -0
  109. package/dist/core/thread/command-parser.js +125 -0
  110. package/dist/core/thread/index.js +1 -0
  111. package/dist/core/thread/ipc-operation.js +95 -0
  112. package/dist/core/thread/thread-context-storage.js +131 -0
  113. package/dist/core/thread/thread-operation.js +258 -0
  114. package/dist/core/thread/thread-route.js +33 -0
  115. package/dist/core/thread/thread-service.js +770 -0
  116. package/dist/core/thread/types.js +1 -0
  117. package/dist/eck/kit-renderer.js +7 -2
  118. package/dist/eck/manifest-engine.js +24 -4
  119. package/dist/eck/message-renderer.js +35 -0
  120. package/dist/index.js +19 -7
  121. package/dist/ipc.js +140 -50
  122. package/dist/paths.js +93 -0
  123. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  124. package/dist/stats/billing.js +20 -8
  125. package/dist/trigger/authorization.js +57 -28
  126. package/dist/trigger/feedback.js +13 -2
  127. package/dist/trigger/parser.js +70 -1
  128. package/dist/trigger/system-definitions.js +3 -3
  129. package/dist/trigger/validation.js +14 -0
  130. package/dist/utils/cross-platform.js +11 -2
  131. package/dist/utils/error-dict.json +18 -4
  132. package/dist/utils/error-utils.js +143 -1
  133. package/dist/utils/instance-registry.js +13 -8
  134. package/dist/utils/log-writer.js +12 -0
  135. package/dist/utils/process-introspect.js +11 -0
  136. package/kits/docs/context-assembly.md +1 -1
  137. package/kits/docs/evolcore/INDEX.md +1 -0
  138. package/kits/docs/evolcore/agent-create.md +40 -28
  139. package/kits/docs/evolcore/agent.md +21 -6
  140. package/kits/docs/evolcore/config.md +2 -2
  141. package/kits/docs/evolcore/ctl.md +2 -2
  142. package/kits/docs/evolcore/ec-capabilities.md +5 -1
  143. package/kits/docs/evolcore/fs-architecture.md +4 -5
  144. package/kits/docs/evolcore/fs.md +1 -1
  145. package/kits/docs/evolcore/group-collaboration.md +4 -6
  146. package/kits/docs/evolcore/group.md +11 -12
  147. package/kits/docs/evolcore/memory-architecture.md +4 -5
  148. package/kits/docs/evolcore/memory-consolidation.md +19 -15
  149. package/kits/docs/evolcore/memory-tags.md +3 -1
  150. package/kits/docs/evolcore/memory.md +24 -12
  151. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  152. package/kits/docs/evolcore/menu-protocol.md +7 -0
  153. package/kits/docs/evolcore/model.md +16 -0
  154. package/kits/docs/evolcore/msg.md +8 -11
  155. package/kits/docs/evolcore/thread-chain.md +267 -0
  156. package/kits/docs/prompt-loading-architecture.md +1 -1
  157. package/kits/eck_manifest.json +55 -40
  158. package/kits/eck_manifest.memory.json +0 -12
  159. package/kits/eck_message_manifest.json +169 -10
  160. package/kits/rules/01-overview.md +11 -0
  161. package/kits/rules/03-identity.md +6 -3
  162. package/kits/rules/05-venue.md +1 -1
  163. package/kits/rules/06-channel.md +1 -1
  164. package/kits/rules/07-memory-and-thread.md +199 -0
  165. package/kits/schemas/_meta.json +7 -1
  166. package/kits/schemas/agent-config.schema.13.json +1 -1
  167. package/kits/schemas/daemon.schema.7.json +140 -0
  168. package/kits/schemas/relation-config.schema.9.json +18 -3
  169. package/kits/schemas/role-config.schema.1.json +6 -6
  170. package/kits/schemas/single-session.schema.1.json +6 -6
  171. package/kits/schemas/single-session.schema.2.json +6 -6
  172. package/kits/schemas/single-session.schema.3.json +6 -6
  173. package/kits/skills/.switches.json +12 -1
  174. package/kits/skills/ec-group/SKILL.md +234 -0
  175. package/kits/skills/ec-skill/SKILL.md +10 -1
  176. package/kits/skills/ec-skill/scripts/cli.mjs +16 -2
  177. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  178. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  179. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  180. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  181. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  182. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  183. package/kits/skills/thread-ops/SKILL.md +492 -0
  184. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  185. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  186. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  187. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  188. package/kits/templates/bootstrap-welcome.md +7 -7
  189. package/kits/templates/memory-consolidation/forget.md +7 -2
  190. package/kits/templates/memory-consolidation/generalize.md +9 -4
  191. package/kits/templates/memory-consolidation/notes.md +11 -11
  192. package/kits/templates/memory-consolidation/profile.md +8 -3
  193. package/kits/templates/memory-consolidation/promote.md +8 -3
  194. package/kits/templates/memory-consolidation/workflow.md +6 -6
  195. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  196. package/kits/templates/message-fragments/item.md +2 -1
  197. package/kits/templates/message-fragments/thread-context.md +6 -0
  198. package/kits/templates/message-fragments/trigger-session.md +2 -0
  199. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  200. package/kits/templates/roles/member.json +2 -0
  201. package/kits/templates/roles/visitor.json +2 -0
  202. package/kits/templates/system-fragments/bootstrap.md +67 -72
  203. package/kits/templates/system-fragments/channel.md +3 -6
  204. package/kits/templates/system-fragments/commands.md +1 -0
  205. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  206. package/kits/templates/system-fragments/memory.md +16 -9
  207. package/kits/templates/system-fragments/reply.md +7 -6
  208. package/kits/templates/system-fragments/session.md +5 -14
  209. package/package.json +6 -3
  210. package/dist/core/memory/memory-session-policy.js +0 -41
  211. package/dist/core/memory/memory-signature.js +0 -257
  212. package/kits/docs/evolcore/group-fs.md +0 -17
  213. package/kits/docs/venues/aun-group.md +0 -45
  214. package/kits/docs/venues/group.md +0 -43
@@ -3,25 +3,29 @@ import path from 'path';
3
3
  import fs from 'fs';
4
4
  import os from 'os';
5
5
  import crypto from 'crypto';
6
+ import { MAIN_CLI_BIN } from '../../product.js';
6
7
  import { BaseagentRunnerUnavailableError, hasCompact, autoCompactWindowForModel, isClaudeContextUsageModel, numericToken, formatAsyncQuestionAnswer, parseAsyncUserQuestions, } from '../../agents/runner-types.js';
7
8
  import { loadedBaseagentIds } from '../../agents/baseagent-availability.js';
8
9
  import { isDiagnosticSessionActive } from '../command/diag-service.js';
9
10
  import { evaluateInboundAdmissionPreflight, evaluateOrdinaryInboundAdmission } from './message-admission.js';
10
11
  import { authorizeAccess, authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
11
12
  import { buildTaskRuntimeEnv, ensureRuntimeLockDir, ensureSessionRuntimeDir } from '../../cli/task-context.js';
12
- import { memorySessionPolicyHook } from '../memory/memory-session-policy.js';
13
+ import { BUILTIN_SESSION_PATH_GRANTS, mergeRuntimeGrantsForSession, resolveGrantedReadRoots, } from '../permission/session-path-grants.js';
14
+ import { buildSystemSessionContext, getSystemSessionModule, isSystemSessionType, readSystemSessionMarker } from '../system-session/registry.js';
13
15
  import { IMRenderer } from './im-renderer.js';
16
+ import { projectSubtaskStarted, projectSubtaskTerminal } from './subtask-tool-projection.js';
14
17
  import { TaskActivityStore } from '../task/task-activity-store.js';
15
18
  import { createSendFileMarkerPattern } from './file-markers.js';
16
19
  import { createDecisionInferenceProvider, createTextInferenceProvider } from '../inference/text-inference.js';
17
20
  import { StreamIdleMonitor } from './stream-idle-monitor.js';
18
21
  import { logger } from '../../utils/logger.js';
19
22
  import { isHostChinese } from '../../utils/locale.js';
20
- import { ExecutionTimeoutError, OrphanedTargetSessionError, isExecutionFailure, getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
23
+ import { ExecutionTimeoutError, OrphanedTargetSessionError, isExecutionFailure, getErrorMessage, getErrorRawText, getToolResultErrorText, classifyError, normalizeCoreErrorReason, normalizeCoreErrorSubtype, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
21
24
  import { readEvolcoreRuntimeFlavor, tagErrorTextWithEvolcoreVersion } from '../../utils/evolcore-version.js';
22
- import { describeProactiveSendCommandFailure, isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
25
+ import { describeProactiveSendCommandFailure, isExactEvolcoreSendCommandForSession, isThreadRoutedEvolcoreSendCommand, parseCodexToolCommand, resolveCodexShellCarrierArgv, resolveCodexShellCarrierString, } from '../permission/ec-command-parser.js';
26
+ import { resolveThreadRouteTarget } from '../thread/thread-route.js';
23
27
  import { summarizeToolInputForDisplay } from '../../utils/tool-summary.js';
24
- import { agentDataDir, agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
28
+ import { agentDataDir, agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths, debugSessionsDir } from '../../paths.js';
25
29
  import { renderKitSections, resolveSessionManifest, DEFAULT_MANIFEST_FILE } from '../../eck/kit-renderer.js';
26
30
  import { renderMessageBody } from '../../eck/message-renderer.js';
27
31
  import { syncGroupRulesContext } from '../../eck/group-rules-sync.js';
@@ -67,6 +71,10 @@ import { auditFullAccessEvent } from '../auth/authorization-audit.js';
67
71
  import { TaskRecoveryService, isTaskRecoveryCurrent } from '../task/task-recovery-service.js';
68
72
  const QUESTION_TTL_MS = 24 * 60 * 60 * 1000;
69
73
  /** Pending async questions outlive the originating turn, but never change its backend binding. */
74
+ /** 带 --thread 的跨话题发送:话题存在于本端索引且目标一致时视为合法发送。 */
75
+ function isThreadRoutedSendForSelf(toolName, toolInput, selfAid) {
76
+ return isThreadRoutedEvolcoreSendCommand(toolName, toolInput, selfAid, (scope, threadId, target) => !!selfAid && !!resolveThreadRouteTarget(selfAid, threadId, scope, target));
77
+ }
70
78
  export class AsyncQuestionService {
71
79
  deps;
72
80
  registered = new Map();
@@ -408,6 +416,381 @@ export class PauseController {
408
416
  function isShowActivitiesMode(value) {
409
417
  return value === 'all' || value === 'text' || value === 'none';
410
418
  }
419
+ const TOOL_INPUT_DIAGNOSTIC_MAX_LENGTH = 96_000;
420
+ const TOOL_INPUT_DIAGNOSTIC_MAX_FINDINGS = 3;
421
+ const TOOL_INPUT_DIAGNOSTIC_TIMEOUT_MS = 1_000;
422
+ function isToolResultFailure(event) {
423
+ return event.isError === true
424
+ || event.executionState === 'failed'
425
+ || event.executionState === 'blocked'
426
+ || event.decision === 'deny'
427
+ || event.decision === 'error'
428
+ || (typeof event.exitCode === 'number' && Number.isFinite(event.exitCode) && event.exitCode !== 0);
429
+ }
430
+ function isStringArgv(value) {
431
+ return Array.isArray(value) && value.every(item => typeof item === 'string');
432
+ }
433
+ function boundedDiagnosticInput(value) {
434
+ return value.length <= TOOL_INPUT_DIAGNOSTIC_MAX_LENGTH;
435
+ }
436
+ function findApplyPatchArgumentsOffset(value) {
437
+ const match = value.trimStart().match(/^(?:"([^"\r\n]+)"|'([^'\r\n]+)'|([^\s;&|<>"']+))(?=\s|[;&|<>]|$)/u);
438
+ const executable = match?.[1] ?? match?.[2] ?? match?.[3];
439
+ if (!executable)
440
+ return -1;
441
+ const basename = executable.replaceAll('\\', '/').split('/').at(-1)?.toLowerCase();
442
+ if (basename !== 'apply_patch' && basename !== 'apply_patch.exe')
443
+ return -1;
444
+ return value.length - value.trimStart().length + match[0].length;
445
+ }
446
+ function findUnquotedDiagnosticText(value, needle, startOffset) {
447
+ let quote = null;
448
+ let escaped = false;
449
+ for (let index = startOffset; index < value.length; index++) {
450
+ const char = value[index];
451
+ if (escaped) {
452
+ escaped = false;
453
+ continue;
454
+ }
455
+ if (quote === 'single') {
456
+ if (char === "'")
457
+ quote = null;
458
+ continue;
459
+ }
460
+ if (quote === 'double') {
461
+ if (char === '\\')
462
+ escaped = true;
463
+ else if (char === '"')
464
+ quote = null;
465
+ continue;
466
+ }
467
+ if (char === '\\') {
468
+ escaped = true;
469
+ continue;
470
+ }
471
+ if (char === "'") {
472
+ quote = 'single';
473
+ continue;
474
+ }
475
+ if (char === '"') {
476
+ quote = 'double';
477
+ continue;
478
+ }
479
+ if (value.startsWith(needle, index))
480
+ return index;
481
+ }
482
+ return -1;
483
+ }
484
+ function diagnosticLineEnd(value, startOffset) {
485
+ const newline = value.indexOf('\n', startOffset);
486
+ return newline < 0 ? value.length : newline;
487
+ }
488
+ function parseDiagnosticHeredoc(value, argumentsOffset) {
489
+ const openOffset = findUnquotedDiagnosticText(value, '<<', argumentsOffset);
490
+ if (openOffset < 0 || openOffset >= diagnosticLineEnd(value, argumentsOffset)
491
+ || value[openOffset + 2] === '<')
492
+ return undefined;
493
+ let cursor = openOffset + 2;
494
+ const stripTabs = value[cursor] === '-';
495
+ if (stripTabs)
496
+ cursor++;
497
+ while (value[cursor] === ' ' || value[cursor] === '\t')
498
+ cursor++;
499
+ const quote = value[cursor] === "'" || value[cursor] === '"' ? value[cursor] : undefined;
500
+ if (quote)
501
+ cursor++;
502
+ const delimiterStart = cursor;
503
+ while (cursor < value.length) {
504
+ const char = value[cursor];
505
+ if (quote ? char === quote : char === ' ' || char === '\t' || char === '\r' || char === '\n')
506
+ break;
507
+ cursor++;
508
+ }
509
+ const delimiter = value.slice(delimiterStart, cursor);
510
+ if (!delimiter)
511
+ return undefined;
512
+ const firstNewline = value.indexOf('\n', cursor);
513
+ if (firstNewline < 0)
514
+ return {};
515
+ let lineStart = firstNewline + 1;
516
+ let trailing;
517
+ while (lineStart <= value.length) {
518
+ const end = diagnosticLineEnd(value, lineStart);
519
+ const rawLine = value.slice(lineStart, end).replace(/\r$/u, '');
520
+ const line = stripTabs ? rawLine.replace(/^\t+/u, '') : rawLine;
521
+ if (line === delimiter) {
522
+ let extraOffset;
523
+ let next = end < value.length ? end + 1 : value.length;
524
+ while (next < value.length) {
525
+ const nextEnd = diagnosticLineEnd(value, next);
526
+ const nextLine = value.slice(next, nextEnd).trim();
527
+ if (nextLine !== '' && !nextLine.startsWith('#')) {
528
+ extraOffset = next;
529
+ break;
530
+ }
531
+ next = nextEnd < value.length ? nextEnd + 1 : value.length;
532
+ }
533
+ return { delimiterOffset: lineStart + rawLine.length - line.length, extraOffset };
534
+ }
535
+ if (line.startsWith(delimiter) && line.length > delimiter.length) {
536
+ trailing ??= { trailingOffset: lineStart + rawLine.length - line.length + delimiter.length, trailingEndOffset: end };
537
+ }
538
+ if (end >= value.length)
539
+ break;
540
+ lineStart = end + 1;
541
+ }
542
+ return trailing ?? {};
543
+ }
544
+ function findDiagnosticShellSeparator(value, startOffset, endOffset) {
545
+ let quote = null;
546
+ let escaped = false;
547
+ for (let index = startOffset; index < endOffset; index++) {
548
+ const char = value[index];
549
+ if (escaped) {
550
+ escaped = false;
551
+ continue;
552
+ }
553
+ if (quote === 'single') {
554
+ if (char === "'")
555
+ quote = null;
556
+ continue;
557
+ }
558
+ if (quote === 'double') {
559
+ if (char === '\\')
560
+ escaped = true;
561
+ else if (char === '"')
562
+ quote = null;
563
+ continue;
564
+ }
565
+ if (char === '\\') {
566
+ escaped = true;
567
+ continue;
568
+ }
569
+ if (char === "'") {
570
+ quote = 'single';
571
+ continue;
572
+ }
573
+ if (char === '"') {
574
+ quote = 'double';
575
+ continue;
576
+ }
577
+ if (char === ';' || char === '|' || char === '&' || char === '\n' || char === '\r')
578
+ return index;
579
+ }
580
+ return -1;
581
+ }
582
+ function diagnosticLocation(source, startOffset, endOffset = startOffset) {
583
+ const start = Math.max(0, Math.min(startOffset, source.value.length));
584
+ const end = Math.max(start, Math.min(endOffset, source.value.length));
585
+ const lineStart = Math.max(source.value.lastIndexOf('\n', Math.max(0, start - 1)) + 1, 0);
586
+ return {
587
+ field: source.field,
588
+ ...(source.argumentIndex !== undefined ? { argumentIndex: source.argumentIndex } : {}),
589
+ line: source.value.slice(0, start).split('\n').length,
590
+ column: [...source.value.slice(lineStart, start)].length + 1,
591
+ startOffset: start,
592
+ endOffset: end,
593
+ };
594
+ }
595
+ function diagnosticFinding(ruleId, title, advice, confidence, source, startOffset, endOffset) {
596
+ return { ruleId, title, advice, confidence, location: diagnosticLocation(source, startOffset, endOffset) };
597
+ }
598
+ function diagnosticCandidates(input) {
599
+ const candidates = [];
600
+ if (typeof input.command === 'string' && boundedDiagnosticInput(input.command)) {
601
+ const source = { field: 'command', value: input.command };
602
+ const carrier = resolveCodexShellCarrierString(input.command);
603
+ if (carrier) {
604
+ const commandOffset = input.command.indexOf(carrier.command);
605
+ candidates.push({
606
+ source,
607
+ command: carrier.command,
608
+ commandOffset: commandOffset >= 0 ? commandOffset : 0,
609
+ carrier: true,
610
+ });
611
+ }
612
+ else {
613
+ candidates.push({ source, command: input.command, commandOffset: 0, carrier: false });
614
+ }
615
+ }
616
+ if (isStringArgv(input.commandArgv) && input.commandArgv.every(boundedDiagnosticInput)) {
617
+ const carrier = resolveCodexShellCarrierArgv(input.commandArgv);
618
+ if (carrier) {
619
+ const argumentIndex = input.commandArgv.length - 1;
620
+ candidates.push({
621
+ source: { field: 'commandArgv', value: input.commandArgv[argumentIndex], argumentIndex },
622
+ command: carrier.command,
623
+ commandOffset: 0,
624
+ carrier: true,
625
+ });
626
+ }
627
+ else {
628
+ const argumentIndex = 0;
629
+ if (findApplyPatchArgumentsOffset(input.commandArgv[argumentIndex] ?? '') >= 0) {
630
+ candidates.push({
631
+ source: { field: 'commandArgv', value: input.commandArgv[argumentIndex], argumentIndex },
632
+ command: input.commandArgv[argumentIndex],
633
+ commandOffset: 0,
634
+ carrier: false,
635
+ });
636
+ }
637
+ }
638
+ }
639
+ return candidates;
640
+ }
641
+ function addHeredocDiagnosticFindings(findings, candidate) {
642
+ const argumentsOffset = findApplyPatchArgumentsOffset(candidate.command);
643
+ if (argumentsOffset < 0)
644
+ return;
645
+ const heredoc = parseDiagnosticHeredoc(candidate.command, argumentsOffset);
646
+ if (!heredoc)
647
+ return;
648
+ const map = (offset) => candidate.commandOffset + offset;
649
+ if (heredoc.trailingOffset !== undefined) {
650
+ findings.push(diagnosticFinding('heredoc-terminator-trailing-characters', 'heredoc 结束标记行包含尾随字符', '让结束标记单独占一行,不要在标记后追加引号、命令或其它字符。', '高', candidate.source, map(heredoc.trailingOffset), map(heredoc.trailingEndOffset ?? heredoc.trailingOffset)));
651
+ }
652
+ else if (heredoc.delimiterOffset === undefined) {
653
+ findings.push(diagnosticFinding('heredoc-terminator-missing', '未找到匹配的 heredoc 结束标记', '补上与起始标记完全一致、且单独占一行的结束标记。', '高', candidate.source, map(candidate.command.length), map(candidate.command.length)));
654
+ }
655
+ else if (heredoc.extraOffset !== undefined) {
656
+ findings.push(diagnosticFinding('heredoc-extra-command', 'heredoc 结束标记后仍有额外命令或文本', '将 apply_patch 与测试或其它命令拆成两次工具调用。', '高', candidate.source, map(heredoc.extraOffset), map(heredoc.extraOffset)));
657
+ }
658
+ }
659
+ function addApplyPatchCompositionFinding(findings, candidate) {
660
+ const argumentsOffset = findApplyPatchArgumentsOffset(candidate.command);
661
+ if (argumentsOffset < 0 || candidate.carrier)
662
+ return;
663
+ const executableMatch = candidate.command.trimStart().match(/^(?:"([^"]+)"|'([^']+)'|([^\s]+))/u);
664
+ const executable = executableMatch?.[1] ?? executableMatch?.[2] ?? executableMatch?.[3] ?? '';
665
+ const basename = executable.replaceAll('\\', '/').split('/').at(-1)?.toLowerCase();
666
+ if (basename !== 'apply_patch' && basename !== 'apply_patch.exe')
667
+ return;
668
+ const heredocOffset = findUnquotedDiagnosticText(candidate.command, '<<', argumentsOffset);
669
+ const endOffset = heredocOffset >= 0 ? heredocOffset : candidate.command.length;
670
+ const separator = findDiagnosticShellSeparator(candidate.command, argumentsOffset, endOffset);
671
+ if (separator < 0)
672
+ return;
673
+ findings.push(diagnosticFinding('apply-patch-composed-command', 'apply_patch 与其它 Shell 命令组合提交', '一次工具调用只提交完整的 apply_patch;测试、检查或其它命令另行调用。', '高', candidate.source, candidate.commandOffset + separator, candidate.commandOffset + separator + 1));
674
+ }
675
+ function addNestedShellDiagnosticFinding(findings, candidate) {
676
+ if (!candidate.carrier || findApplyPatchArgumentsOffset(candidate.command) < 0)
677
+ return;
678
+ findings.push(diagnosticFinding('nested-shell-escaping-risk', '命令包含嵌套 Shell 包装层', '尽量直接提交单层 Shell 命令;需要 heredoc 或 JSON 时,减少外层转义和包装。', '中', candidate.source, candidate.commandOffset, candidate.commandOffset + candidate.command.length));
679
+ }
680
+ function addJsonArrayDiagnosticFinding(findings, input) {
681
+ const rawArgv = isStringArgv(input.commandArgv) ? input.commandArgv : undefined;
682
+ const parsedArgv = rawArgv ?? (typeof input.command === 'string'
683
+ ? (() => {
684
+ const parsed = parseCodexToolCommand({ command: input.command });
685
+ return parsed.ok ? parsed.argv : undefined;
686
+ })()
687
+ : undefined);
688
+ if (!parsedArgv || parsedArgv.length < 6)
689
+ return;
690
+ if (parsedArgv[0].toLowerCase() !== MAIN_CLI_BIN || parsedArgv[1].toLowerCase() !== 'aid'
691
+ || parsedArgv[2].toLowerCase() !== 'agentmd' || parsedArgv[3].toLowerCase() !== 'set'
692
+ || parsedArgv[5].toLowerCase() !== 'tags')
693
+ return;
694
+ const value = parsedArgv[6];
695
+ const source = rawArgv
696
+ ? { field: 'commandArgv', value: rawArgv[6] ?? '', argumentIndex: 6 }
697
+ : { field: 'command', value: typeof input.command === 'string' ? input.command : '' };
698
+ if (value === undefined) {
699
+ findings.push(diagnosticFinding('json-array-argument-missing', 'tags 参数缺少完整 JSON 数组值', '将 tags 作为一个完整参数传入,例如 PowerShell 使用单引号包住 ["DSH","AUN"]。', '高', source, source.value.length));
700
+ return;
701
+ }
702
+ let valid = false;
703
+ try {
704
+ const decoded = JSON.parse(value);
705
+ valid = Array.isArray(decoded) && decoded.every(item => typeof item === 'string');
706
+ }
707
+ catch {
708
+ valid = false;
709
+ }
710
+ if (valid)
711
+ return;
712
+ const split = rawArgv !== undefined && rawArgv.length > 7 && !rawArgv[7].startsWith('--')
713
+ && (value.startsWith('[') || value.endsWith(','));
714
+ findings.push(diagnosticFinding(split ? 'json-array-argument-splitting' : 'json-array-argument-invalid', split ? 'tags 的 JSON 数组疑似被拆成多个参数' : 'tags 参数不是可解析的 JSON 字符串数组', '保证整个 JSON 数组位于同一个参数中;PowerShell 普通单引号字符串内不要再添加反斜杠转义。', split ? '中' : '高', source, split ? source.value.length : 0, source.value.length));
715
+ }
716
+ function addCommandArgvMismatchFinding(findings, input) {
717
+ if (typeof input.command !== 'string' || !isStringArgv(input.commandArgv))
718
+ return;
719
+ const commandArgv = input.commandArgv;
720
+ if (!boundedDiagnosticInput(input.command) || commandArgv.some(value => !boundedDiagnosticInput(value)))
721
+ return;
722
+ if (resolveCodexShellCarrierArgv(commandArgv) || resolveCodexShellCarrierString(input.command))
723
+ return;
724
+ const parsed = parseCodexToolCommand({ command: input.command });
725
+ if (!parsed.ok || parsed.argv.length === commandArgv.length
726
+ && parsed.argv.every((value, index) => value === commandArgv[index]))
727
+ return;
728
+ findings.push(diagnosticFinding('command-argv-mismatch', 'command 与 commandArgv 的解析结果不一致', '以执行器提供的结构化 commandArgv 为准,避免在外层重新拼接或改写命令文本。', '中', { field: 'command', value: input.command }, 0, input.command.length));
729
+ }
730
+ function deduplicateDiagnosticFindings(findings) {
731
+ const seen = new Set();
732
+ return findings.filter(item => {
733
+ const key = `${item.ruleId}:${item.location.field}:${item.location.argumentIndex ?? ''}:${item.location.startOffset}`;
734
+ if (seen.has(key))
735
+ return false;
736
+ seen.add(key);
737
+ return true;
738
+ }).slice(0, TOOL_INPUT_DIAGNOSTIC_MAX_FINDINGS);
739
+ }
740
+ function diagnoseToolInput(original, event, deadline) {
741
+ if (!isToolResultFailure(event) || !original.input || typeof original.input !== 'object')
742
+ return undefined;
743
+ const input = original.input;
744
+ if (typeof input.command === 'string' && !boundedDiagnosticInput(input.command))
745
+ return undefined;
746
+ if (Array.isArray(input.commandArgv)) {
747
+ if (input.commandArgv.length > 1_024 || !isStringArgv(input.commandArgv)
748
+ || input.commandArgv.reduce((length, value) => length + value.length, 0) > TOOL_INPUT_DIAGNOSTIC_MAX_LENGTH) {
749
+ return undefined;
750
+ }
751
+ }
752
+ const candidates = diagnosticCandidates(input);
753
+ if (Date.now() > deadline)
754
+ return undefined;
755
+ const findings = [];
756
+ for (const candidate of candidates) {
757
+ if (Date.now() > deadline)
758
+ return undefined;
759
+ addHeredocDiagnosticFindings(findings, candidate);
760
+ addApplyPatchCompositionFinding(findings, candidate);
761
+ addNestedShellDiagnosticFinding(findings, candidate);
762
+ }
763
+ if (Date.now() <= deadline)
764
+ addJsonArrayDiagnosticFinding(findings, input);
765
+ if (Date.now() <= deadline)
766
+ addCommandArgvMismatchFinding(findings, input);
767
+ if (Date.now() > deadline)
768
+ return undefined;
769
+ const deduplicated = deduplicateDiagnosticFindings(findings);
770
+ return deduplicated.length > 0 ? { findings: deduplicated } : undefined;
771
+ }
772
+ export function diagnoseToolInputAfterFailure(original, event) {
773
+ if (!original)
774
+ return undefined;
775
+ try {
776
+ return diagnoseToolInput(original, event, Date.now() + TOOL_INPUT_DIAGNOSTIC_TIMEOUT_MS);
777
+ }
778
+ catch {
779
+ return undefined;
780
+ }
781
+ }
782
+ function formatToolInputDiagnosticLocation(location) {
783
+ const field = location.argumentIndex === undefined
784
+ ? location.field
785
+ : `${location.field}[${location.argumentIndex}]`;
786
+ return `${field},第 ${location.line} 行第 ${location.column} 列,UTF-16 偏移 ${location.startOffset}-${location.endOffset}`;
787
+ }
788
+ export function formatToolInputDiagnostics(report) {
789
+ if (!report || report.findings.length === 0)
790
+ return '';
791
+ const lines = report.findings.map(item => `- [${item.confidence}] ${item.title}(${formatToolInputDiagnosticLocation(item.location)})\n 建议:${item.advice}`);
792
+ return `\n\n输入核验(辅助):\n${lines.join('\n')}`;
793
+ }
411
794
  function approvalActorRevision(actor, owners = [], admins = [], selfAid) {
412
795
  if (!actor)
413
796
  return undefined;
@@ -526,8 +909,10 @@ function markRetryHealthRecorded(error) {
526
909
  function wasRetryHealthRecorded(error) {
527
910
  return !!(error && typeof error === 'object' && error[RETRY_HEALTH_RECORDED_MARK] === true);
528
911
  }
529
- function markModelFallbackExhausted(error, message) {
912
+ function markModelFallbackExhausted(error, message, cause) {
530
913
  error[MODEL_FALLBACK_EXHAUSTED_MARK] = message;
914
+ if (cause !== undefined)
915
+ error.cause = cause;
531
916
  return error;
532
917
  }
533
918
  function getModelFallbackExhaustedMessage(error) {
@@ -548,7 +933,7 @@ function wasRetryInputAlreadySubmitted(error) {
548
933
  return !!(error && typeof error === 'object' && error[RETRY_INPUT_ALREADY_SUBMITTED_MARK] === true);
549
934
  }
550
935
  function formatRetryInputAlreadySubmittedMessage(error) {
551
- const reason = getErrorMessage(error, undefined, false) || 'Codex 暂时不可用';
936
+ const reason = getErrorRawText(error) || 'Codex 暂时不可用';
552
937
  return `⚠️ Codex 已接收本次消息,但本轮未能确认完成。为避免在会话中重复写入同一消息,系统未自动重试。请检查会话状态后重新发送。\n原因:${reason}`;
553
938
  }
554
939
  function isNonErrorInterruptReason(reason) {
@@ -561,7 +946,7 @@ function isExpectedTerminalInterrupt(reason) {
561
946
  return isAbortTerminalReason(reason);
562
947
  }
563
948
  function formatRetryableErrorFinalMessage(error, retries) {
564
- const reason = getErrorMessage(error, undefined, false) || 'API 暂时不可用';
949
+ const reason = getErrorRawText(error) || 'API 暂时不可用';
565
950
  return `❌ API 暂时不可用,已自动重试 ${retries} 次仍失败,任务已停止。\n原因:${reason}`;
566
951
  }
567
952
  /**
@@ -672,17 +1057,23 @@ function getStreamErrorText(result) {
672
1057
  }
673
1058
  return parts.join('\n');
674
1059
  }
1060
+ function getStreamRawErrorText(result, fallback) {
1061
+ const fullText = result.fullText.trim();
1062
+ const fullTextLooksLikeDiagnostic = fullText.length > 0 && (isRetryableError(new Error(fullText))
1063
+ || isContextTooLongText(fullText));
1064
+ const fallbackLooksLikeDiagnostic = typeof fallback === 'string' && fallback.trim().length > 0 && (isRetryableError(new Error(fallback))
1065
+ || isContextTooLongText(fallback));
1066
+ const candidates = [
1067
+ result.syntheticDiagnosticText,
1068
+ result.errors?.join('\n'),
1069
+ fullTextLooksLikeDiagnostic ? fullText : undefined,
1070
+ fallbackLooksLikeDiagnostic ? fallback : undefined,
1071
+ ];
1072
+ return candidates.find((part) => typeof part === 'string' && part.trim().length > 0)?.trim();
1073
+ }
675
1074
  function getStreamErrorMessage(result, includeEmoji = false) {
676
- const raw = getStreamErrorText(result) || '任务执行失败';
677
- const mapped = getErrorMessage(new Error(raw), result.terminalReason, includeEmoji);
678
- const generic = includeEmoji ? '❌ 处理消息时出错,请稍后重试' : '处理消息时出错,请稍后重试';
679
- // Unknown runner-specific terminal reasons do not have a friendly mapping.
680
- // Preserve their diagnostic text instead of collapsing it to the generic
681
- // "处理消息时出错" message.
682
- if (raw && mapped === generic) {
683
- return includeEmoji ? `❌ ${raw}` : raw;
684
- }
685
- return mapped;
1075
+ const raw = getStreamErrorText(result);
1076
+ return getErrorMessage(new Error(raw), result.terminalReason, includeEmoji);
686
1077
  }
687
1078
  function streamHitContextLimit(result) {
688
1079
  return result.terminalReason === 'prompt_too_long' ||
@@ -826,6 +1217,8 @@ export class ResponseEngine {
826
1217
  activeMonitors = new Map();
827
1218
  /** sessionId → 当前正在处理任务的运行时上下文,供 in-task CLI 通过 IPC 查询。 */
828
1219
  activeTaskRuntimeContexts = new Map();
1220
+ /** sessionId → 本轮最近一次 runQuery 请求的模型;仅在任务活跃时对外可见(见 getOutboundDebug)。 */
1221
+ activeRunnerModels = new Map();
829
1222
  /** task.run started 后立即登记,覆盖运行时上下文尚未建立的 shutdown 窗口。 */
830
1223
  activeTaskSpans = new Map();
831
1224
  /** sessionId → 尚未收到 runner:task-notification 的子任务。 */
@@ -993,7 +1386,7 @@ export class ResponseEngine {
993
1386
  // revocation for legacy events without a taskId.
994
1387
  const interruptedTaskId = eventTaskId;
995
1388
  if (interruptedTaskId && activeTask?.taskId === interruptedTaskId) {
996
- this.agentDelegationRegistry?.revokeTask(event.sessionId, interruptedTaskId);
1389
+ this.agentDelegationRegistry?.revokeTask(event.sessionId, interruptedTaskId, 'interrupted');
997
1390
  }
998
1391
  }
999
1392
  });
@@ -1337,7 +1730,8 @@ export class ResponseEngine {
1337
1730
  || this.isPauseRequested(sessionId) || this.retryDelayWaiters.has(sessionId))
1338
1731
  return { state: 'live', ...identity };
1339
1732
  const runner = this.getAgent(session.metadata?.channelKey || session.channel, session.baseagent, selfAid);
1340
- return { state: runner.hasActiveStream(sessionId) ? 'live' : 'missing', ...identity,
1733
+ const owned = runner.hasActiveOperation?.(sessionId, active.taskId) ?? runner.hasActiveStream(sessionId);
1734
+ return { state: owned ? 'live' : 'missing', ...identity,
1341
1735
  hasTerminalOwner: this.activeTimeoutHandlers.has(sessionId) };
1342
1736
  }
1343
1737
  // Never adopt or interrupt a newer task to clean up an older attempt.
@@ -1380,6 +1774,14 @@ export class ResponseEngine {
1380
1774
  const interruptedTaskId = interruptedTurn?.active?.taskId;
1381
1775
  this.interruptedSessions.set(sessionId, effectiveReason);
1382
1776
  this.messageQueue?.markActiveTerminal?.(sessionId, effectiveReason);
1777
+ const channelKey = session.metadata?.channelKey || session.channel;
1778
+ const agent = this.getAgent(channelKey, session.baseagent, session.selfAID);
1779
+ const cancelledQueuedTurn = interruptedTurn?.active
1780
+ ? agent.cancelQueuedTurn?.(sessionId, {
1781
+ taskId: interruptedTurn.active.taskId,
1782
+ generation: interruptedTurn.active.generation,
1783
+ }) ?? false
1784
+ : false;
1383
1785
  // 终态必须先于渠道发送和 runner I/O 落盘。否则任一外部调用卡住时,
1384
1786
  // shutdown 超时退出会再次留下 started/interrupting 的悬空任务。
1385
1787
  const runtimeContext = this.activeTaskRuntimeContexts.get(sessionId);
@@ -1404,13 +1806,12 @@ export class ResponseEngine {
1404
1806
  this.sessionManager.clearProcessingIfTask(sessionId, taskId);
1405
1807
  if (this.activeTaskRuntimeContexts.get(sessionId)?.taskId === taskId) {
1406
1808
  this.activeTaskRuntimeContexts.delete(sessionId);
1809
+ this.activeRunnerModels.delete(sessionId);
1407
1810
  }
1408
- this.agentDelegationRegistry?.revokeTask(sessionId, taskId);
1811
+ this.agentDelegationRegistry?.revokeTask(sessionId, taskId, 'interrupted');
1409
1812
  }
1410
1813
  this.closeActiveRunnerTasks(sessionId, effectiveReason, taskSpan?.causation ?? runtimeContext?.causation);
1411
1814
  await this.interactionRouter?.cancelAll(sessionId, effectiveReason);
1412
- const channelKey = session.metadata?.channelKey || session.channel;
1413
- const agent = this.getAgent(channelKey, session.baseagent, session.selfAID);
1414
1815
  const terminalStatus = (() => {
1415
1816
  // TimeoutController owns timeout delivery (including user-facing timeout
1416
1817
  // details) and already guarantees a single terminal status. Sending it
@@ -1441,7 +1842,9 @@ export class ResponseEngine {
1441
1842
  logger.debug(`[ResponseEngine] interrupt terminal status failure: session=${sessionId} task=${interruptedTaskId} error=${error instanceof Error ? error.message : String(error)}`);
1442
1843
  });
1443
1844
  })();
1444
- const runnerInterrupt = agent.interrupt(sessionId).catch(error => {
1845
+ const runnerInterrupt = (cancelledQueuedTurn
1846
+ ? Promise.resolve()
1847
+ : agent.interrupt(sessionId)).catch(error => {
1445
1848
  logger.debug(`[ResponseEngine] interrupt barrier runner failure: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
1446
1849
  if (strict)
1447
1850
  throw error;
@@ -1482,6 +1885,64 @@ export class ResponseEngine {
1482
1885
  getTaskRuntimeContext(sessionId) {
1483
1886
  return this.activeTaskRuntimeContexts.get(sessionId) ?? null;
1484
1887
  }
1888
+ /** debug 模式下写/删 debug-sessions 临时文件 */
1889
+ debugWriteSessionSnapshot(sessionId, ctx) {
1890
+ if (process.env.EVOLCORE_DEBUG_MODE !== '1')
1891
+ return;
1892
+ try {
1893
+ const dir = debugSessionsDir();
1894
+ if (!fs.existsSync(dir))
1895
+ fs.mkdirSync(dir, { recursive: true });
1896
+ const snapshot = {
1897
+ schemaVersion: 1,
1898
+ sessionId,
1899
+ selfAid: ctx.selfAid ?? '',
1900
+ channel: ctx.channel ?? '',
1901
+ channelKey: ctx.channelKey ?? '',
1902
+ channelId: ctx.channelId ?? '',
1903
+ peerKey: ctx.peerKey ?? '',
1904
+ chatType: ctx.chatType ?? 'private',
1905
+ threadId: ctx.threadId ?? '',
1906
+ sessionType: ctx.sessionType ?? 'main',
1907
+ baseagent: ctx.baseagent ?? '',
1908
+ writtenAt: Date.now(),
1909
+ };
1910
+ fs.writeFileSync(`${dir}/${sessionId}.json`, JSON.stringify(snapshot), 'utf-8');
1911
+ }
1912
+ catch (err) {
1913
+ logger.debug(`[Debug] failed to write debug session snapshot for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
1914
+ }
1915
+ }
1916
+ debugDeleteSessionSnapshot(sessionId) {
1917
+ if (process.env.EVOLCORE_DEBUG_MODE !== '1')
1918
+ return;
1919
+ try {
1920
+ const snapshotPath = `${debugSessionsDir()}/${sessionId}.json`;
1921
+ if (fs.existsSync(snapshotPath))
1922
+ fs.rmSync(snapshotPath, { force: true });
1923
+ }
1924
+ catch (err) {
1925
+ logger.debug(`[Debug] failed to delete debug session snapshot for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
1926
+ }
1927
+ }
1928
+ /**
1929
+ * 出站 payload.debug 的会话侧字段(不含 sentAt,由发送方盖戳)。
1930
+ * 供 agent 经 CLI(ec msg send / ec group send)委托发送的路径使用,
1931
+ * 与 IMRenderer 回复路径写入的 debug 同构。会话不存在返回 undefined。
1932
+ */
1933
+ async getOutboundDebug(sessionId) {
1934
+ const session = await this.sessionManager.getSessionById(sessionId);
1935
+ if (!session)
1936
+ return undefined;
1937
+ const model = this.activeTaskRuntimeContexts.has(sessionId)
1938
+ ? this.activeRunnerModels.get(sessionId)
1939
+ : undefined;
1940
+ return {
1941
+ sessionId: session.id,
1942
+ ...(session.agentSessionId ? { agentSessionId: session.agentSessionId } : {}),
1943
+ ...(model ? { model } : {}),
1944
+ };
1945
+ }
1485
1946
  setAgentDelegationRegistry(registry) {
1486
1947
  this.agentDelegationRegistry = registry;
1487
1948
  }
@@ -1700,9 +2161,15 @@ export class ResponseEngine {
1700
2161
  if (this.turnCoordinator.isCurrent(turnLease))
1701
2162
  return false;
1702
2163
  logger.info(`[ResponseEngine] Stale turn detected after agent.runQuery start; cancelling unconsumed stream: session=${sessionId} task=${taskId}`);
1703
- await agent.interrupt(sessionId).catch(interruptError => {
1704
- logger.warn(`[ResponseEngine] Failed to cancel stale runner start: session=${sessionId} task=${taskId} error=${interruptError instanceof Error ? interruptError.message : String(interruptError)}`);
1705
- });
2164
+ const cancelledQueuedTurn = agent.cancelQueuedTurn?.(sessionId, {
2165
+ taskId: turnLease.taskId,
2166
+ generation: turnLease.generation,
2167
+ }) ?? false;
2168
+ if (!cancelledQueuedTurn) {
2169
+ await agent.interrupt(sessionId).catch(interruptError => {
2170
+ logger.warn(`[ResponseEngine] Failed to cancel stale runner start: session=${sessionId} task=${taskId} error=${interruptError instanceof Error ? interruptError.message : String(interruptError)}`);
2171
+ });
2172
+ }
1706
2173
  agent.cleanupStream(streamKey);
1707
2174
  return true;
1708
2175
  }
@@ -1796,6 +2263,71 @@ export class ResponseEngine {
1796
2263
  && message.source === 'trigger'
1797
2264
  && !!message.triggerMeta?.triggerId;
1798
2265
  }
2266
+ isTrustedBgSession(message) {
2267
+ return message.channel === 'daemon'
2268
+ && message.source === 'bg-session'
2269
+ && !!message.bgSessionMeta?.runId;
2270
+ }
2271
+ bgSessionTerminalMessages = new WeakSet();
2272
+ claimBgSessionTerminal(message) {
2273
+ const meta = message.bgSessionMeta;
2274
+ if (message.source !== 'bg-session' || !meta?.runId || this.bgSessionTerminalMessages.has(message)) {
2275
+ return undefined;
2276
+ }
2277
+ this.bgSessionTerminalMessages.add(message);
2278
+ return { meta, runId: meta.runId };
2279
+ }
2280
+ publishBgSessionError(message, error, opts = {}) {
2281
+ const terminal = this.claimBgSessionTerminal(message);
2282
+ if (!terminal)
2283
+ return;
2284
+ const { meta, runId } = terminal;
2285
+ this.eventBus.publish({
2286
+ type: 'bg-session:error',
2287
+ kind: meta.kind,
2288
+ runId,
2289
+ sessionId: meta.boundSessionId ?? '',
2290
+ messageId: opts.messageId ?? message.messageId ?? runId,
2291
+ error,
2292
+ targetChannel: message.channel,
2293
+ targetChannelId: message.channelId,
2294
+ causation: opts.causation ?? message.causation,
2295
+ });
2296
+ }
2297
+ publishBgSessionInterrupted(message, reason, causation) {
2298
+ const terminal = this.claimBgSessionTerminal(message);
2299
+ if (!terminal)
2300
+ return;
2301
+ const { meta, runId } = terminal;
2302
+ this.eventBus.publish({
2303
+ type: 'bg-session:interrupted',
2304
+ kind: meta.kind,
2305
+ runId,
2306
+ sessionId: meta.boundSessionId ?? '',
2307
+ messageId: message.messageId ?? runId,
2308
+ reason,
2309
+ targetChannel: message.channel,
2310
+ targetChannelId: message.channelId,
2311
+ causation: causation ?? message.causation,
2312
+ });
2313
+ }
2314
+ publishBgSessionCompleted(message, messageId, durationMs, causation) {
2315
+ const terminal = this.claimBgSessionTerminal(message);
2316
+ if (!terminal)
2317
+ return;
2318
+ const { meta, runId } = terminal;
2319
+ this.eventBus.publish({
2320
+ type: 'bg-session:completed',
2321
+ kind: meta.kind,
2322
+ runId,
2323
+ sessionId: meta.boundSessionId ?? '',
2324
+ messageId,
2325
+ durationMs,
2326
+ targetChannel: message.channel,
2327
+ targetChannelId: message.channelId,
2328
+ causation: causation ?? message.causation,
2329
+ });
2330
+ }
1799
2331
  /**
1800
2332
  * A fullaccess Trigger may run with no current relation access for its
1801
2333
  * origin actor, but only after its persisted owner provenance still
@@ -1859,6 +2391,8 @@ export class ResponseEngine {
1859
2391
  if (!selfAid || !actorId || !channelType || !originChannelKey)
1860
2392
  return undefined;
1861
2393
  const chatType = message.triggerMeta.originChatType ?? 'private';
2394
+ if (chatType === 'group' && !message.triggerMeta.originConversationId)
2395
+ return undefined;
1862
2396
  const conversationId = message.triggerMeta.originConversationId ?? actorId;
1863
2397
  const control = message.triggerMeta.originControl === true;
1864
2398
  let actor;
@@ -1889,6 +2423,7 @@ export class ResponseEngine {
1889
2423
  actorId,
1890
2424
  channelType,
1891
2425
  originChannelKey,
2426
+ conversationId: chatType === 'group' ? conversationId : actorId,
1892
2427
  control,
1893
2428
  peerKey: formatPeerKey(channelType, chatType === 'group' ? conversationId : actorId),
1894
2429
  };
@@ -1958,8 +2493,11 @@ export class ResponseEngine {
1958
2493
  /** Publish a terminal outcome before an early return from Trigger execution. */
1959
2494
  publishTriggerExecutionFailure(message, error, opts = {}) {
1960
2495
  const terminal = this.claimTriggerTerminal(message);
1961
- if (!terminal)
2496
+ if (!terminal) {
2497
+ // bg-session 路径:trigger terminal 不匹配时尝试 bg-session
2498
+ this.publishBgSessionError(message, error, { messageId: opts.messageId, causation: opts.causation });
1962
2499
  return;
2500
+ }
1963
2501
  const { trigger, runId } = terminal;
1964
2502
  const attemptId = trigger.attemptId;
1965
2503
  this.eventBus.publish({
@@ -1980,8 +2518,11 @@ export class ResponseEngine {
1980
2518
  }
1981
2519
  publishTriggerExecutionSkipped(message, reason, causation, interruption) {
1982
2520
  const terminal = this.claimTriggerTerminal(message);
1983
- if (!terminal)
2521
+ if (!terminal) {
2522
+ // bg-session 路径
2523
+ this.publishBgSessionInterrupted(message, reason, causation);
1984
2524
  return;
2525
+ }
1985
2526
  const { trigger, runId } = terminal;
1986
2527
  this.eventBus.publish({
1987
2528
  type: 'trigger:skipped',
@@ -2017,6 +2558,14 @@ export class ResponseEngine {
2017
2558
  logger.warn(`[ResponseEngine] Failed to close interrupted Trigger run=${message.triggerMeta?.runId ?? '<unknown>'}: ${error instanceof Error ? error.message : String(error)}`);
2018
2559
  });
2019
2560
  }
2561
+ else if (this.isTrustedBgSession(message)) {
2562
+ await adapter.send(envelope, {
2563
+ kind: 'status.interrupted',
2564
+ metadata: { reason },
2565
+ }).catch(error => {
2566
+ logger.warn(`[ResponseEngine] Failed to close interrupted bg-session run=${message.bgSessionMeta?.runId ?? '<unknown>'}: ${error instanceof Error ? error.message : String(error)}`);
2567
+ });
2568
+ }
2020
2569
  this.publishTriggerExecutionSkipped(message, reason, causation, interruption);
2021
2570
  }
2022
2571
  publishTriggerExecutionCompleted(message, messageId, durationMs, causation) {
@@ -2888,8 +3437,12 @@ export class ResponseEngine {
2888
3437
  ? { kind: 'restartResume', groupId: message.groupId, threadId: message.threadId }
2889
3438
  : undefined,
2890
3439
  previousTurnState: this.turnCoordinator.current(session),
2891
- isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, selfAid),
2892
- describeSendCommandFailure: describeProactiveSendCommandFailure,
3440
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, selfAid) || isThreadRoutedSendForSelf(toolName, toolInput, selfAid),
3441
+ describeSendCommandFailure: (toolName, toolInput) => describeProactiveSendCommandFailure(toolName, toolInput, {
3442
+ selfAid,
3443
+ channelId: session.channelId,
3444
+ chatType: chatType,
3445
+ }),
2893
3446
  logger,
2894
3447
  } : null;
2895
3448
  if (resolvedMode?.mode.beforeProcess && modeProcessCtx) {
@@ -2952,8 +3505,9 @@ export class ResponseEngine {
2952
3505
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
2953
3506
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
2954
3507
  this.activeTaskRuntimeContexts.delete(session.id);
3508
+ this.activeRunnerModels.delete(session.id); // F1修复
2955
3509
  }
2956
- this.agentDelegationRegistry?.revokeTask(session.id, taskId);
3510
+ this.agentDelegationRegistry?.revokeTask(session.id, taskId, 'interrupted');
2957
3511
  const daemonTrigger = this.isTrustedDaemonTrigger(message);
2958
3512
  if (!daemonTrigger) {
2959
3513
  await adapter.send(envelope, statusPayload).catch(() => { });
@@ -2961,7 +3515,11 @@ export class ResponseEngine {
2961
3515
  await adapter.send(envelope, {
2962
3516
  kind: 'result.error',
2963
3517
  text: userMessage,
2964
- reason: isExecutionFailure(timeoutError) ? timeoutError.reason : isTotalExecutionTimeout ? 'total_execution_timeout' : 'timeout',
3518
+ ...(getErrorRawText(timeoutError) ? { raw: getErrorRawText(timeoutError) } : {}),
3519
+ subtype: normalizeCoreErrorSubtype(timeoutError, undefined, isTotalExecutionTimeout ? 'timeout' : 'timeout'),
3520
+ reason: isExecutionFailure(timeoutError)
3521
+ ? normalizeCoreErrorReason(timeoutError.reason)
3522
+ : 'timeout',
2965
3523
  }).catch(() => { });
2966
3524
  if (daemonTrigger) {
2967
3525
  await adapter.send(envelope, statusPayload).catch(() => { });
@@ -3301,9 +3859,9 @@ export class ResponseEngine {
3301
3859
  await adapter.send(withEnvelopeReplyContext(envelope, capturedReplyContext), { kind: 'result.text', text, isFinal: false });
3302
3860
  };
3303
3861
  const authChatType = configChatType;
3304
- const authConversationId = authChatType === 'group'
3862
+ const authConversationId = triggerExecutionIdentity?.conversationId ?? (authChatType === 'group'
3305
3863
  ? (session.metadata?.groupId || session.channelId || capturedChannelId)
3306
- : configActorId;
3864
+ : configActorId);
3307
3865
  const authPeerKey = configChannelType && authConversationId
3308
3866
  ? formatPeerKey(configChannelType, authConversationId)
3309
3867
  : undefined;
@@ -3429,11 +3987,53 @@ export class ResponseEngine {
3429
3987
  ? resolvedMode.mode.configureRun(modeProcessCtx)
3430
3988
  : undefined;
3431
3989
  const modePolicyHook = runModeConfig?.policyHook;
3432
- // 整理会话:写工具全拦、Bash 只放行 ec memory create|update|archive,先于模式策略判定
3433
- const isMemorySession = (session.sessionType ?? session.metadata?.sessionType) === 'memory';
3434
- const pureSessionPolicyHook = isMemorySession
3435
- ? (toolName, toolInput) => memorySessionPolicyHook(toolName, toolInput) ?? modePolicyHook?.(toolName, toolInput)
3436
- : modePolicyHook;
3990
+ // 系统会话:路径 / 工具由 system-session 模块判定,与角色体系无关。
3991
+ // 有标记就只走模块,不挂模式策略、不算授权表、不并 runtime grant。
3992
+ const sessionMetadata = session.metadata;
3993
+ const declaredSessionType = session.sessionType ?? (typeof sessionMetadata?.sessionType === 'string' ? sessionMetadata.sessionType : undefined);
3994
+ const systemSessionMarker = readSystemSessionMarker(sessionMetadata);
3995
+ if (isSystemSessionType(declaredSessionType) && !systemSessionMarker) {
3996
+ throw new Error(`系统会话 ${declaredSessionType} 缺少有效 systemSession 标记`);
3997
+ }
3998
+ if (systemSessionMarker && declaredSessionType && declaredSessionType !== systemSessionMarker.type) {
3999
+ throw new Error(`系统会话标记类型 ${systemSessionMarker.type} 与 sessionType ${declaredSessionType} 不一致`);
4000
+ }
4001
+ const systemSessionModule = systemSessionMarker
4002
+ ? getSystemSessionModule(systemSessionMarker.type)
4003
+ : undefined;
4004
+ const systemSessionContext = systemSessionModule && systemSessionMarker
4005
+ ? buildSystemSessionContext({
4006
+ marker: systemSessionMarker,
4007
+ sessionId: session.id,
4008
+ selfAid: selfAid || session.selfAID || '',
4009
+ projectPath: absoluteProjectPath,
4010
+ managedTempDir: sessionRuntimeDir,
4011
+ metadata: sessionMetadata,
4012
+ })
4013
+ : undefined;
4014
+ const systemSessionCheck = systemSessionModule && systemSessionContext
4015
+ ? (toolName, toolInput) => systemSessionModule.checkTool(toolName, toolInput, systemSessionContext)
4016
+ : undefined;
4017
+ // 沙箱声明与 runner 有关,但当前所有 runner 共用同一个结论;仍按 runner 问一次,
4018
+ // 以便将来某个 runner 需要不同取舍时只改模块。
4019
+ const systemSessionSandbox = systemSessionModule && systemSessionContext
4020
+ ? systemSessionModule.runnerLaunch(normalizeBaseagent(agent.name).canonical, systemSessionContext).sandbox
4021
+ : undefined;
4022
+ const pureSessionPolicyHook = systemSessionMarker ? undefined : modePolicyHook;
4023
+ // 路径授权(仅用户会话):身份就是角色名;运行时授予(ec ctl path grant)按 sessionId 再并一层。
4024
+ const pathGrantIdentity = systemSessionMarker ? undefined : (peerRole || undefined);
4025
+ const grantedReadRoots = (() => {
4026
+ if (!pathGrantIdentity)
4027
+ return [];
4028
+ const table = mergeRuntimeGrantsForSession(BUILTIN_SESSION_PATH_GRANTS, session.id, pathGrantIdentity);
4029
+ return resolveGrantedReadRoots(table, {
4030
+ selfAid,
4031
+ peerKey: authPeerKey,
4032
+ projectPath: absoluteProjectPath,
4033
+ identity: pathGrantIdentity,
4034
+ permissionMode: effectivePermissionMode,
4035
+ });
4036
+ })();
3437
4037
  // 设置权限审批的交互上下文(支持交互卡片)
3438
4038
  const permissionContext = {
3439
4039
  sendPrompt: permissionPromptForOrigin,
@@ -3447,10 +4047,12 @@ export class ResponseEngine {
3447
4047
  agentName: agentNameForStats,
3448
4048
  taskId,
3449
4049
  chatmode: isProactive ? 'proactive' : 'interactive',
3450
- role: peerRole,
4050
+ // 系统会话没有角色
4051
+ role: systemSessionMarker ? undefined : peerRole,
3451
4052
  chatType: authChatType,
3452
4053
  selfAid,
3453
4054
  managedTempDir: sessionRuntimeDir,
4055
+ grantedReadRoots,
3454
4056
  allowReadonlySourceDiagnostics: effectiveAgentConfig?.readonlySourceDiagnostics === true,
3455
4057
  peerKey: authPeerKey,
3456
4058
  causation: taskCausation,
@@ -3460,6 +4062,13 @@ export class ResponseEngine {
3460
4062
  recordExecutionAnomaly,
3461
4063
  pauseController: taskPauseController,
3462
4064
  preToolUsePolicyHook: pureSessionPolicyHook,
4065
+ systemSessionCheck,
4066
+ ...(systemSessionModule && systemSessionContext
4067
+ ? {
4068
+ systemSessionGrants: systemSessionModule.grants(systemSessionContext),
4069
+ ...(systemSessionSandbox ? { systemSessionSandbox } : {}),
4070
+ }
4071
+ : {}),
3463
4072
  armApprovedDelegationCommand: this.agentDelegationRegistry
3464
4073
  ? (sessionToken, commandHash) => this.agentDelegationRegistry.armApprovedCommand({
3465
4074
  sessionToken,
@@ -3468,6 +4077,11 @@ export class ResponseEngine {
3468
4077
  commandHash,
3469
4078
  })
3470
4079
  : undefined,
4080
+ bindSessionCredential: this.agentDelegationRegistry
4081
+ ? (sessionToken, threadId) => this.agentDelegationRegistry.registerSessionCredential({
4082
+ sessionToken, threadId, sessionId: session.id, taskId,
4083
+ })
4084
+ : undefined,
3471
4085
  turn: {
3472
4086
  sessionId: turnLease.sessionId,
3473
4087
  taskId,
@@ -3552,16 +4166,31 @@ export class ResponseEngine {
3552
4166
  const pluginHook = pureSessionPolicyHook;
3553
4167
  if (!pluginHook)
3554
4168
  return undefined;
3555
- return (toolName, toolInput) => {
4169
+ return (toolName, toolInput, meta) => {
3556
4170
  const stBefore = modeState.get('proactive');
3557
4171
  const wasFirstPending = stBefore && !stBefore.firstToolDone;
3558
- const result = pluginHook(toolName, toolInput);
4172
+ const result = pluginHook(toolName, toolInput, meta);
3559
4173
  // 整理会话的拦截原因已经写清楚怎么改,不套 proactive 违规提醒
3560
4174
  if (result?.block && result.policyCode?.startsWith('memory_session_'))
3561
4175
  return result;
3562
4176
  if (result?.block) {
3563
4177
  // 拦截事件(首工具非表态 / 工具进展汇报未完成)
3564
4178
  snapshot.set(session.id, taskId, { policyHook: { triggered: true, blocked: true, toolName } });
4179
+ // 问题1/2修复:熔断码表示连续违规已超阈值,停止注入提醒,直接中断任务。
4180
+ // F5修复:向渠道发一条人类可读的错误说明,避免静默中断。
4181
+ if (result.policyCode === 'proactive_circuit_break') {
4182
+ void adapter.send(withEnvelopeReplyContext(envelope, capturedReplyContext), {
4183
+ kind: 'result.text',
4184
+ text: '⚠️ 任务执行遇到循环障碍,已自动中断。请重新描述你的需求。',
4185
+ isFinal: true,
4186
+ }).catch(err => {
4187
+ logger.warn(`[ResponseEngine] circuit break channel send failed: session=${session.id} error=${err instanceof Error ? err.message : String(err)}`);
4188
+ });
4189
+ void agent.interrupt(session.id).catch(error => {
4190
+ logger.debug(`[ResponseEngine] circuit break interrupt failed: session=${session.id} error=${error instanceof Error ? error.message : String(error)}`);
4191
+ });
4192
+ return result;
4193
+ }
3565
4194
  const errorMsg = `⚠️ proactive 模式违规:${result.reason ?? '请先发送必要说明'}。请重新执行正确的命令。`;
3566
4195
  agent.injectUserMessage?.(session.id, errorMsg);
3567
4196
  return result;
@@ -3605,6 +4234,8 @@ export class ResponseEngine {
3605
4234
  let previousFallbackState;
3606
4235
  let agentModel;
3607
4236
  let v2HandoffIds = [];
4237
+ /** T3: 本次注入的 Thread Context 条目(渲染后据此清空 inbox)。 */
4238
+ let threadContextItems = [];
3608
4239
  let v2HandoffResultRefs = [];
3609
4240
  let v2HandoffDirection;
3610
4241
  let handoffPromptRendered = false;
@@ -3634,7 +4265,15 @@ export class ResponseEngine {
3634
4265
  const triggerPermissionModeOverride = message.triggerMeta?.permissionModeOverride;
3635
4266
  const commandFullAccess = message.executionPermissionOverride;
3636
4267
  const triggerFullAccess = triggerPermissionModeOverride === 'fullaccess';
3637
- if (commandFullAccess || triggerFullAccess) {
4268
+ if (systemSessionMarker) {
4269
+ // 系统会话不走角色 / trigger override / fullaccess:runner 模式由模块固定
4270
+ if (commandFullAccess || triggerFullAccess) {
4271
+ throw new Error('系统会话不支持 fullaccess');
4272
+ }
4273
+ effectivePermissionMode = systemSessionModule
4274
+ .runnerLaunch(normalizedBaseagent.canonical, systemSessionContext).runnerMode;
4275
+ }
4276
+ else if (commandFullAccess || triggerFullAccess) {
3638
4277
  if (commandFullAccess) {
3639
4278
  if (commandFullAccess.permissionMode !== 'fullaccess'
3640
4279
  || commandFullAccess.processRole !== 'fullaccess-run'
@@ -3848,10 +4487,13 @@ export class ResponseEngine {
3848
4487
  bootstrapAgentMdPath: agentMdPath(selfAid),
3849
4488
  };
3850
4489
  }
4490
+ // persona.md 是唯一的人格/交接文件(personal.md 已废弃并入)。
4491
+ // 该白名单让 owner 会话可直接用文件工具分段更新 `## Bootstrap 交接`,
4492
+ // 避免把长 Markdown 交接摘要塞进 `ec agent persona --text` 的 shell 转义。
3851
4493
  if (selfAid && peerRole === 'owner' && effectivePermissionMode !== 'readonly') {
3852
4494
  modelOverride = {
3853
4495
  ...(modelOverride || {}),
3854
- personalMdPath: path.join(resolveRoot(), 'agents', selfAid, 'personal', 'personal.md'),
4496
+ personalMdPath: path.join(resolveRoot(), 'agents', selfAid, 'personal', 'persona.md'),
3855
4497
  };
3856
4498
  }
3857
4499
  if (normalizedBaseagent.canonical === 'claude') {
@@ -4095,9 +4737,12 @@ export class ResponseEngine {
4095
4737
  // Bootstrap always uses the lifecycle-gated main manifest. A custom
4096
4738
  // session manifest (for example the auxiliary manifest with `always`
4097
4739
  // sections) must not reintroduce normal rules or command context.
4740
+ // 系统会话:manifest 由模块按 type/subtype 决定,不读 config
4098
4741
  const manifestFile = isBootstrapping
4099
4742
  ? DEFAULT_MANIFEST_FILE
4100
- : resolveSessionManifest(sessionType, sessionManifests);
4743
+ : systemSessionMarker
4744
+ ? getSystemSessionModule(systemSessionMarker.type).manifest(systemSessionMarker.subtype)
4745
+ : resolveSessionManifest(sessionType, sessionManifests);
4101
4746
  const kitContext = renderKitSections(kitCtx, manifestFile);
4102
4747
  effectiveSystemPrompt = [options?.systemPromptAppend, kitContext].filter(Boolean).join('\n') || undefined;
4103
4748
  // ── Stats: context_breakdown 旁路采集(各段估算 token 数,字符数/4 近似) ──
@@ -4137,6 +4782,16 @@ export class ResponseEngine {
4137
4782
  const rawPeerItems = message.source === 'owner-inject' ? [] : message.items && message.items.length > 0
4138
4783
  ? message.items
4139
4784
  : [{
4785
+ // T9.5/T10: trigger 消息填充专属 kind + triggerInfo(含 labels),走 msg-trigger-session/thread-check 渲染
4786
+ ...(message.source === 'trigger' && message.triggerMeta?.triggerId ? {
4787
+ kind: 'trigger',
4788
+ triggerInfo: {
4789
+ triggerId: message.triggerMeta.triggerId,
4790
+ triggerName: message.triggerMeta.triggerName,
4791
+ fireTime: message.triggerMeta.fireTime,
4792
+ labels: message.triggerMeta.labels,
4793
+ },
4794
+ } : {}),
4140
4795
  peerId: message.peerId, peerName: peerName || undefined,
4141
4796
  peerType: message.peerType,
4142
4797
  peerRole: message.batchRole || session.identity?.role || 'none',
@@ -4147,6 +4802,34 @@ export class ResponseEngine {
4147
4802
  mentions: message.mentions,
4148
4803
  mentionAids: message.mentionAids,
4149
4804
  }];
4805
+ // T3.2: 读取 Thread Context inbox —— 跨话题发送留在本端的注入提醒。
4806
+ // 注入一次后立即删除整个文件(T3.3),不依赖任何消费标记。
4807
+ threadContextItems = [];
4808
+ if (selfAid && message.source !== 'owner-inject') {
4809
+ try {
4810
+ const { ThreadService } = await import('../thread/index.js');
4811
+ const inbox = ThreadService.readInbox(selfAid, session.id);
4812
+ threadContextItems = inbox.map(entry => ({
4813
+ kind: 'thread-context',
4814
+ peerId: entry.originPeer,
4815
+ peerType: 'agent',
4816
+ peerRole: 'none',
4817
+ content: entry.content,
4818
+ timestamp: entry.timestamp,
4819
+ threadContext: {
4820
+ messageId: entry.messageId,
4821
+ originThreadId: entry.originThreadId,
4822
+ originPeer: entry.originPeer,
4823
+ originSessionId: entry.originSessionId,
4824
+ ...(entry.triggerId ? { triggerId: entry.triggerId } : {}),
4825
+ gapDetection: entry.gapDetection,
4826
+ },
4827
+ }));
4828
+ }
4829
+ catch (e) {
4830
+ logger.warn(`[ResponseEngine] ThreadContext inbox 读取失败: ${e?.message ?? e}`);
4831
+ }
4832
+ }
4150
4833
  const peerItems = (() => {
4151
4834
  if (message.handoffDelivery && this.handoffRuntime) {
4152
4835
  const items = this.handoffRuntime.buildPromptItems(message, peerRole);
@@ -4156,10 +4839,10 @@ export class ResponseEngine {
4156
4839
  : (item.handoff?.handoffId ? [item.handoff.handoffId] : [])))));
4157
4840
  v2HandoffResultRefs = items.flatMap(item => item.handoff?.resultRefs ?? []);
4158
4841
  v2HandoffDirection = message.handoffDelivery.direction;
4159
- return items;
4842
+ return [...threadContextItems, ...items];
4160
4843
  }
4161
4844
  }
4162
- return rawPeerItems;
4845
+ return [...threadContextItems, ...rawPeerItems];
4163
4846
  })();
4164
4847
  // 将匹配的 pending hint 暂时 claim,排在真实消息之前;渲染或启动
4165
4848
  // 失败时释放 claim,只有 runner 接受 prompt 后才持久化 consumed。
@@ -4263,6 +4946,16 @@ export class ResponseEngine {
4263
4946
  }
4264
4947
  }
4265
4948
  }
4949
+ // T3.3: 渲染成功、prompt 非空 —— 注入完成,删除 inbox(注入一次语义)。
4950
+ if (threadContextItems.length > 0 && selfAid) {
4951
+ try {
4952
+ const { ThreadService } = await import('../thread/index.js');
4953
+ ThreadService.clearInbox(selfAid, session.id);
4954
+ }
4955
+ catch (e) {
4956
+ logger.warn(`[ResponseEngine] ThreadContext inbox 清理失败: ${e?.message ?? e}`);
4957
+ }
4958
+ }
4266
4959
  // Pass the stable session directory explicitly so sandboxed helper
4267
4960
  // commands never fall back to a process, agent, or project directory
4268
4961
  // for transient state.
@@ -4273,6 +4966,10 @@ export class ResponseEngine {
4273
4966
  messageId: message.messageId,
4274
4967
  baseagent: normalizedBaseagent.canonical,
4275
4968
  channel: configChannelType,
4969
+ // 完整 channelKey(= adapter.channelName / inst.name,如 `aun#<selfAid>#main`)。
4970
+ // 必须与下面 issue() 的 `channel:` 用同一表达式,否则 debug 路径拿到的
4971
+ // origin.channelKey 会与真实任务路径不一致(见 D5)。
4972
+ channelKey: authorizationIdentity?.originChannelKey ?? message.channel,
4276
4973
  channelId: message.channelId,
4277
4974
  chatType: configChatType,
4278
4975
  selfAid,
@@ -4280,7 +4977,8 @@ export class ResponseEngine {
4280
4977
  peerKey,
4281
4978
  peerName: peerName || undefined,
4282
4979
  peerType: message.peerType || session.metadata?.peerType || undefined,
4283
- peerRole,
4980
+ // 系统会话没有角色
4981
+ ...(systemSessionMarker ? {} : { peerRole }),
4284
4982
  // 会话原型与整理模式:ipc 侧靠 sessionType 拦非整理会话的写命令,
4285
4983
  // 靠 memoryMode 校验 --scope 是否与本模式匹配。
4286
4984
  sessionType: renderedSessionType,
@@ -4304,6 +5002,7 @@ export class ResponseEngine {
4304
5002
  causation: taskCausation,
4305
5003
  };
4306
5004
  this.activeTaskRuntimeContexts.set(session.id, taskRuntimeContext);
5005
+ this.debugWriteSessionSnapshot(session.id, taskRuntimeContext);
4307
5006
  runtimeEnv = buildTaskRuntimeEnv(taskRuntimeContext);
4308
5007
  if (this.agentDelegationRegistry && configActorId && selfAid && peerKey) {
4309
5008
  const delegationToken = this.agentDelegationRegistry.issue({
@@ -4316,7 +5015,8 @@ export class ResponseEngine {
4316
5015
  chatType: configChatType,
4317
5016
  selfAid,
4318
5017
  peerKey,
4319
- issuedRole: peerRole,
5018
+ // 系统会话与角色体系无关:带标记、不签角色
5019
+ ...(systemSessionMarker ? { systemSession: systemSessionMarker } : { issuedRole: peerRole }),
4320
5020
  executionSource: fullAccessAuthorization?.source ?? (message.source === 'trigger' ? 'trigger' : 'user'),
4321
5021
  permissionMode: effectivePermissionMode,
4322
5022
  approvalInteractionPolicy,
@@ -4409,6 +5109,10 @@ export class ResponseEngine {
4409
5109
  mimeType: image.mimeType,
4410
5110
  }));
4411
5111
  debugRunnerModel = modelOverride?.model || agentModel;
5112
+ if (debugRunnerModel)
5113
+ this.activeRunnerModels.set(session.id, debugRunnerModel);
5114
+ else
5115
+ this.activeRunnerModels.delete(session.id);
4412
5116
  const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, runAgentSessionId, runnerImages, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
4413
5117
  // The turn may be superseded while the runner is still creating its
4414
5118
  // backend query. Replay the interrupt after runQuery resolves, when
@@ -4578,7 +5282,7 @@ export class ResponseEngine {
4578
5282
  const candidateHint = fallbackCandidates.length === 0
4579
5283
  ? '未配置可用备用模型'
4580
5284
  : '备用模型均不可用';
4581
- throw markModelFallbackExhausted(new Error(`model_not_found: ${primaryModel ?? 'runner_default'}`), `当前模型 ${primaryModel ?? '默认模型'} 不可用,${candidateHint}。请稍后重试;管理员可在 agents/defaults.json 的 baseagents.${normalizedBaseagent.canonical}.fallbackModels 配置同类备用模型。`);
5285
+ throw markModelFallbackExhausted(new Error(`model_not_found: ${primaryModel ?? 'runner_default'}`), `当前模型 ${primaryModel ?? '默认模型'} 不可用,${candidateHint}。请稍后重试;管理员可在 agents/defaults.json 的 baseagents.${normalizedBaseagent.canonical}.fallbackModels 配置同类备用模型。`, retryError);
4582
5286
  }
4583
5287
  if (retryAttemptTimedOut || isRetryableError(retryError)) {
4584
5288
  await recordRetryHealthError(retryError);
@@ -4704,8 +5408,9 @@ export class ResponseEngine {
4704
5408
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
4705
5409
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
4706
5410
  this.activeTaskRuntimeContexts.delete(session.id);
5411
+ this.activeRunnerModels.delete(session.id); // F1修复
4707
5412
  }
4708
- this.agentDelegationRegistry?.revokeTask(session.id, taskId);
5413
+ this.agentDelegationRegistry?.revokeTask(session.id, taskId, 'replaced');
4709
5414
  logger.info(`[ResponseEngine] Stale turn stopped before publish: session=${session.id} task=${taskId}`);
4710
5415
  await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_generation', taskCausation, {
4711
5416
  expected: turnLease?.generation,
@@ -4719,8 +5424,9 @@ export class ResponseEngine {
4719
5424
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
4720
5425
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
4721
5426
  this.activeTaskRuntimeContexts.delete(session.id);
5427
+ this.activeRunnerModels.delete(session.id); // F1修复
4722
5428
  }
4723
- this.agentDelegationRegistry?.revokeTask(session.id, taskId);
5429
+ this.agentDelegationRegistry?.revokeTask(session.id, taskId, 'replaced');
4724
5430
  logger.info(`[ResponseEngine] Stale turn stopped before protocol rejection: session=${session.id} task=${taskId}`);
4725
5431
  await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_generation', taskCausation, {
4726
5432
  expected: turnLease?.generation,
@@ -4870,9 +5576,11 @@ export class ResponseEngine {
4870
5576
  });
4871
5577
  }
4872
5578
  // 最终回复文本:suppressed 模式或无 text 事件时需要兜底添加
5579
+ // 错误诊断串已通过 system.error 单独发出时置 true,下游跳过 result.error 避免重复
5580
+ let sentErrorAsSystemError = false;
4873
5581
  const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
4874
5582
  const visibleFinalReplyText = stripContextLimitErrorLines(finalReplyText);
4875
- const isDiagnosticFallback = streamResult.isError && !streamResult.hasReceivedText;
5583
+ const streamRawErrorText = getStreamRawErrorText(streamResult, visibleFinalReplyText || undefined);
4876
5584
  const isTriggerNoop = message.source === 'trigger'
4877
5585
  && typeof message.triggerMeta?.noopSentinel === 'string'
4878
5586
  && message.triggerMeta.noopSentinel.length > 0
@@ -4882,16 +5590,42 @@ export class ResponseEngine {
4882
5590
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
4883
5591
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
4884
5592
  this.activeTaskRuntimeContexts.delete(session.id);
5593
+ this.activeRunnerModels.delete(session.id); // F1修复
4885
5594
  }
4886
- this.agentDelegationRegistry?.revokeTask(session.id, taskId);
5595
+ this.agentDelegationRegistry?.revokeTask(session.id, taskId, 'replaced');
4887
5596
  logger.info(`[ResponseEngine] Stale turn stopped before final flush: session=${session.id} task=${taskId}`);
4888
5597
  await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_turn', taskCausation);
4889
5598
  return;
4890
5599
  }
4891
- if (visibleFinalReplyText && !isDiagnosticFallback && !isTriggerNoop) {
5600
+ if ((visibleFinalReplyText || streamResult.syntheticDiagnosticText) && !isTriggerNoop) {
4892
5601
  // `hasReceivedText` 是任务级计数。工具调用前的文字已经显示,并不表示最后一轮
4893
5602
  // (通常来自 complete.result)的文字也已显示;用本轮投影标记避免终态丢失。
4894
- if (shouldSuppress() || !streamResult.hasProjectedCurrentReplyText) {
5603
+ //
5604
+ // 终态路由按"本轮是否以错误收场"(isError)判定,不再按"有没有收到过文本"判定。
5605
+ // 原因:上游会把错误原文当正文送进来——Claude Code 用 `<synthetic>` assistant 表达
5606
+ // 传输/API 故障(runner 已标 synthetic,见上方 text 分支),网关也会把上下文超限错误
5607
+ // 伪装成 text event。这两种情况下 hasReceivedText / lastReplyText 都非空,旧判据
5608
+ // (isError && !lastReplyText)拦不住,错误串会被当成模型的最终回复发出去
5609
+ // (2026-10-11 403「无法连接到积分服务器」案例)。
5610
+ // 以错误收场的轮次一律走 system.error:前端收到的单条消息即可判断来源,
5611
+ // 不需要等后续的 result.error。
5612
+ const errorText = visibleFinalReplyText || streamResult.syntheticDiagnosticText || getStreamErrorMessage(streamResult, false);
5613
+ if (streamResult.isError) {
5614
+ // 用原始诊断文本(含错误码)判断可重试性,不用已过 stripContextLimitErrorLines 的展示文本
5615
+ const recoverable = isRetryableError(new Error(getStreamErrorText(streamResult)));
5616
+ const subtype = normalizeCoreErrorSubtype(new Error(streamRawErrorText || getStreamErrorText(streamResult)), streamResult.subtype, streamResult.terminalReason);
5617
+ const reason = normalizeCoreErrorReason(streamResult.terminalReason);
5618
+ await adapter.send(envelope, {
5619
+ kind: 'system.error',
5620
+ text: errorText,
5621
+ ...(streamRawErrorText ? { raw: streamRawErrorText } : {}),
5622
+ subtype,
5623
+ ...(reason ? { reason } : {}),
5624
+ recoverable,
5625
+ }).catch(() => { });
5626
+ sentErrorAsSystemError = true;
5627
+ }
5628
+ else if (visibleFinalReplyText && (shouldSuppress() || !streamResult.hasProjectedCurrentReplyText)) {
4895
5629
  await renderer.addText(visibleFinalReplyText, undefined, undefined, streamResult.assistantUuid);
4896
5630
  }
4897
5631
  }
@@ -4901,8 +5635,9 @@ export class ResponseEngine {
4901
5635
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
4902
5636
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
4903
5637
  this.activeTaskRuntimeContexts.delete(session.id);
5638
+ this.activeRunnerModels.delete(session.id); // F1修复
4904
5639
  }
4905
- this.agentDelegationRegistry?.revokeTask(session.id, taskId);
5640
+ this.agentDelegationRegistry?.revokeTask(session.id, taskId, streamResult.isError ? 'failed' : 'completed');
4906
5641
  logger.info(`[ResponseEngine] session ${session.id} processing cleared task=${taskId}`);
4907
5642
  import('../../utils/msg-diag.js').then(m => m.diagLog('task_done', { aid: session.selfAID ?? '', msgId: message.messageId ?? '', taskId, text: message.content ?? '' })).catch(() => { });
4908
5643
  // 降级模型回复末尾追加标记(代码层硬注入,不依赖模型输出)
@@ -4971,6 +5706,10 @@ export class ResponseEngine {
4971
5706
  : getStreamErrorMessage(streamResult, false);
4972
5707
  const rawSubtype = streamResult.subtype || 'agent_error';
4973
5708
  const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
5709
+ const coreSubtype = normalizeCoreErrorSubtype(new Error(streamRawErrorText || errorSummary), rawSubtype, streamResult.terminalReason);
5710
+ const coreReason = permissionTerminalFailure
5711
+ ? normalizeCoreErrorReason(permissionTerminalFailure.reason)
5712
+ : normalizeCoreErrorReason(streamResult.terminalReason);
4974
5713
  // 用户主动打断(新消息/​/stop/​撤回)会让 SDK 流在工具调用中途被掐断,
4975
5714
  // 末尾 result message 形状异常并被标记为 error(含 SDK 内部 ede_diagnostic 串)。
4976
5715
  // 这不是真正的失败,不应把诊断串暴露给用户,也不计入错误统计。
@@ -4991,12 +5730,29 @@ export class ResponseEngine {
4991
5730
  // DaemonChannel settles a trigger when it receives a terminal
4992
5731
  // payload. Send the metadata-bearing status first so failed runs do
4993
5732
  // not lose their tool counts and recovery diagnostics.
5733
+ // system.error 已在 finalReplyText 分支把错误文本发给了前端,不再重复发 result.error
4994
5734
  if (this.isTrustedDaemonTrigger(message)) {
4995
5735
  await adapter.send(envelope, { kind: 'status.error', metadata: terminalMetadata }).catch(() => { });
4996
- await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
5736
+ if (!sentErrorAsSystemError) {
5737
+ await adapter.send(envelope, {
5738
+ kind: 'result.error',
5739
+ text: userErrorSummary,
5740
+ ...(streamRawErrorText ? { raw: streamRawErrorText } : {}),
5741
+ subtype: coreSubtype,
5742
+ ...(coreReason ? { reason: coreReason } : {}),
5743
+ }).catch(() => { });
5744
+ }
4997
5745
  }
4998
5746
  else {
4999
- await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
5747
+ if (!sentErrorAsSystemError) {
5748
+ await adapter.send(envelope, {
5749
+ kind: 'result.error',
5750
+ text: userErrorSummary,
5751
+ ...(streamRawErrorText ? { raw: streamRawErrorText } : {}),
5752
+ subtype: coreSubtype,
5753
+ ...(coreReason ? { reason: coreReason } : {}),
5754
+ }).catch(() => { });
5755
+ }
5000
5756
  await adapter.send(envelope, { kind: 'status.error', metadata: terminalMetadata }).catch(() => { });
5001
5757
  }
5002
5758
  this.touchAgentActivity(channelKey);
@@ -5249,6 +6005,7 @@ export class ResponseEngine {
5249
6005
  }
5250
6006
  if (!interruptReason) {
5251
6007
  this.publishTriggerExecutionCompleted(message, messageId, durationMs, taskCausation);
6008
+ this.publishBgSessionCompleted(message, messageId, durationMs, taskCausation);
5252
6009
  }
5253
6010
  if (!this.turnCoordinator.canPublish(turnLease))
5254
6011
  return;
@@ -5338,8 +6095,9 @@ export class ResponseEngine {
5338
6095
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
5339
6096
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
5340
6097
  this.activeTaskRuntimeContexts.delete(session.id);
6098
+ this.activeRunnerModels.delete(session.id); // F1修复
5341
6099
  }
5342
- this.agentDelegationRegistry?.revokeTask(session.id, taskId);
6100
+ this.agentDelegationRegistry?.revokeTask(session.id, taskId, this.interruptedSessions.has(session.id) ? 'interrupted' : 'failed');
5343
6101
  logger.info(`[ResponseEngine] session ${session.id} processing cleared (on error) task=${taskId}`);
5344
6102
  }
5345
6103
  catch { }
@@ -5486,10 +6244,29 @@ export class ResponseEngine {
5486
6244
  sessionId: session.id,
5487
6245
  metadata: withTaskMetadata(sendOpts?.metadata),
5488
6246
  };
6247
+ const errorReason = isExecutionFailure(error)
6248
+ ? normalizeCoreErrorReason(error.reason)
6249
+ : retryExhaustedCount
6250
+ ? 'retry_exhausted'
6251
+ : retryInputAlreadySubmitted
6252
+ ? 'input_already_submitted'
6253
+ : modelFallbackExhaustedMessage
6254
+ ? 'fallback_exhausted'
6255
+ : isTotalExecutionTimeout || isTimeout
6256
+ ? 'timeout'
6257
+ : undefined;
6258
+ const errorSubtype = error instanceof OrphanedTargetSessionError
6259
+ ? ErrorType.SESSION_UNAVAILABLE
6260
+ : modelFallbackExhaustedMessage
6261
+ ? ErrorType.MODEL_UNAVAILABLE
6262
+ : normalizeCoreErrorSubtype(error, undefined, isTotalExecutionTimeout || isTimeout ? 'timeout' : undefined);
6263
+ const rawErrorText = getErrorRawText(error);
5489
6264
  const errorPayload = {
5490
6265
  kind: 'result.error',
5491
6266
  text: userMessage,
5492
- reason: isExecutionFailure(error) ? error.reason : retryExhaustedCount ? 'retry_exhausted' : retryInputAlreadySubmitted ? 'input_already_submitted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
6267
+ subtype: errorSubtype,
6268
+ ...(rawErrorText ? { raw: rawErrorText } : {}),
6269
+ ...(errorReason ? { reason: errorReason } : {}),
5493
6270
  };
5494
6271
  if (!isUserInterrupt && daemonTrigger && procStatus === 'error') {
5495
6272
  // DaemonChannel settles a trigger on the first terminal payload. Put
@@ -5889,6 +6666,9 @@ export class ResponseEngine {
5889
6666
  });
5890
6667
  // 追踪最后一轮 assistant 回复文本(tool_use 之后的纯文本)
5891
6668
  let lastReplyText = '';
6669
+ // 上游合成错误帧的原文(runner 标记 synthetic 的 text)。与正文通道分开存放:
6670
+ // 它只能作为终态错误文案的来源,不得进入 lastReplyText / 投影 / hasReceivedText。
6671
+ let syntheticDiagnosticText = '';
5892
6672
  // callId → description 映射,用于 tool_result 回显描述
5893
6673
  const toolDescByCallId = new Map();
5894
6674
  const toolInputByCallId = new Map();
@@ -5922,10 +6702,11 @@ export class ResponseEngine {
5922
6702
  if (event.type === 'complete') {
5923
6703
  event = normalizeCompleteAgentEvent(event);
5924
6704
  }
5925
- if (event.type === 'tool_result' && event.isError) {
6705
+ if (event.type === 'tool_result' && isToolResultFailure(event)) {
5926
6706
  const classification = normalizeToolErrorClassification(event);
5927
6707
  event = {
5928
6708
  ...event,
6709
+ isError: true,
5929
6710
  errorCode: classification.errorCode,
5930
6711
  errorDetailCode: classification.errorDetailCode,
5931
6712
  classificationMissing: classification.classificationMissing || event.classificationMissing,
@@ -6071,7 +6852,20 @@ export class ResponseEngine {
6071
6852
  logger.info(`[ResponseEngine] Event: type=${event.type}${eventDetail}`);
6072
6853
  }
6073
6854
  // IMRenderer 旁路:proactive 模式逐事件投影为 thought(fire-and-forget)
6074
- renderer.emit(event);
6855
+ let diagnosticSuffix = '';
6856
+ let projectedEvent = event;
6857
+ if (event.type === 'tool_result' && isToolResultFailure(event)) {
6858
+ const byCallId = event.callId ? toolInputByCallId.get(event.callId) : undefined;
6859
+ const byCorrelationId = event.correlationId
6860
+ ? toolInputByCorrelationId.get(event.correlationId)
6861
+ : undefined;
6862
+ const original = event.callId && event.correlationId
6863
+ ? byCallId === byCorrelationId ? byCallId : undefined
6864
+ : byCallId ?? byCorrelationId;
6865
+ diagnosticSuffix = formatToolInputDiagnostics(diagnoseToolInputAfterFailure(original, event));
6866
+ projectedEvent = { ...event, error: getToolResultErrorText(event) + diagnosticSuffix };
6867
+ }
6868
+ renderer.emit(projectedEvent);
6075
6869
  if (event.type === 'text_start') {
6076
6870
  await renderer.sendTextStart(event.contentIndex, event.turn);
6077
6871
  continue;
@@ -6113,6 +6907,7 @@ export class ResponseEngine {
6113
6907
  toolUseId: event.toolUseId,
6114
6908
  description: event.description,
6115
6909
  subagentType: event.subagentType,
6910
+ taskType: event.taskType,
6116
6911
  skipTranscript: event.skipTranscript,
6117
6912
  });
6118
6913
  this.eventBus.publish({
@@ -6131,15 +6926,21 @@ export class ResponseEngine {
6131
6926
  causation,
6132
6927
  });
6133
6928
  if (!event.skipTranscript) {
6134
- renderer.addLifecycle('started', {
6135
- scope: 'runner_task',
6136
- taskId: event.taskId,
6137
- taskKind: event.taskKind,
6138
- toolUseId: event.toolUseId,
6139
- description: event.description,
6140
- subagentType: event.subagentType,
6141
- taskType: event.taskType,
6142
- }, `${event.taskKind === 'agent' ? 'Agent' : 'Background'} task started: ${event.description}`);
6929
+ // 子任务就是来源工具调用的执行过程:来源卡片在本轮可见时归拢进它的 tool_result,
6930
+ // 不另出卡片;否则(如 Codex 子智能体无来源工具)退回合成卡片。见 subtask-tool-projection.ts。
6931
+ const folded = renderer.beginParentSubtask(event.toolUseId);
6932
+ tasks.get(event.taskId).projection = folded ? 'parent' : 'synthetic';
6933
+ if (!folded) {
6934
+ renderer.addSubtaskActivity(projectSubtaskStarted({
6935
+ taskId: event.taskId,
6936
+ taskKind: event.taskKind,
6937
+ description: event.description,
6938
+ taskType: event.taskType,
6939
+ subagentType: event.subagentType,
6940
+ toolUseId: event.toolUseId,
6941
+ workflowName: event.workflowName,
6942
+ }));
6943
+ }
6143
6944
  }
6144
6945
  continue;
6145
6946
  }
@@ -6155,6 +6956,7 @@ export class ResponseEngine {
6155
6956
  }
6156
6957
  if (event.type === 'task_notification') {
6157
6958
  const tasks = this.activeRunnerTasks.get(session.id);
6959
+ const started = tasks?.get(event.taskId);
6158
6960
  tasks?.delete(event.taskId);
6159
6961
  if (tasks?.size === 0)
6160
6962
  this.activeRunnerTasks.delete(session.id);
@@ -6175,19 +6977,26 @@ export class ResponseEngine {
6175
6977
  causation,
6176
6978
  });
6177
6979
  if (!event.skipTranscript) {
6178
- const label = event.description || event.taskId;
6179
- renderer.addLifecycle('completed', {
6180
- scope: 'runner_task',
6181
- taskId: event.taskId,
6182
- taskKind: event.taskKind,
6183
- toolUseId: event.toolUseId,
6184
- description: event.description,
6185
- subagentType: event.subagentType,
6186
- status: event.status,
6187
- outputFile: event.outputFile,
6188
- summary: event.summary,
6189
- usage: event.usage,
6190
- }, `${event.taskKind === 'agent' ? 'Agent' : 'Background'} task ${event.status}: ${label}`);
6980
+ const terminal = { status: event.status, summary: event.summary, durationMs: event.usage?.durationMs };
6981
+ const toolUseId = event.toolUseId ?? started?.toolUseId;
6982
+ // 归拢的子任务:终态合并进来源工具的 tool_result(前台等真实结果、后台放出扣住的占位结果)。
6983
+ const merged = started?.projection === 'parent' && renderer.endParentSubtask(toolUseId, terminal);
6984
+ if (!merged) {
6985
+ // 没归拢上:开始时就是合成卡片,则终态配同一张合成卡片;
6986
+ // 否则来源卡片已在本轮之外,补发一条同 call_id 的终态结果更新那张卡片。
6987
+ // taskType/subagentType 只在 task_started 事件上,从已登记的任务元数据补齐。
6988
+ const toParent = started?.projection !== 'synthetic' && !!toolUseId;
6989
+ renderer.addSubtaskActivity(projectSubtaskTerminal({
6990
+ taskId: event.taskId,
6991
+ taskKind: event.taskKind,
6992
+ description: event.description ?? started?.description,
6993
+ taskType: started?.taskType,
6994
+ subagentType: event.subagentType ?? started?.subagentType,
6995
+ toolUseId,
6996
+ ...terminal,
6997
+ ...(toParent ? { callId: toolUseId, toolName: renderer.toolNameForCall(toolUseId) } : {}),
6998
+ }));
6999
+ }
6191
7000
  }
6192
7001
  continue;
6193
7002
  }
@@ -6224,21 +7033,30 @@ export class ResponseEngine {
6224
7033
  if (!isCurrentlyBackground) {
6225
7034
  // 流式文本
6226
7035
  if (event.type === 'text') {
6227
- // 网关偶尔会把上下文超限错误伪装成普通 text event。不要向事件总线或
6228
- // 渲染器投影错误行;lastReplyText 仍保留原文本,以便流结束后触发恢复。
6229
- const visibleText = stripContextLimitErrorLines(event.text);
6230
- if (visibleText) {
6231
- hasReceivedText = true;
6232
- if (visibleText.trim())
6233
- madeProgress = true;
6234
- }
6235
- lastReplyText += event.text;
6236
- if (visibleText) {
6237
- this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: visibleText, isFinal: false });
7036
+ // 上游合成错误帧(Claude Code 的 `<synthetic>` assistant)会把 API/传输故障的
7037
+ // 错误原文当成正文送进来。这段字是诊断回显,不是模型正文——出身由 runner 标注,
7038
+ // 不靠字符串或计数猜。不进入正文通道(lastReplyText / hasReceivedText / 投影),
7039
+ // 只单独留存,供终态错误分支取用。
7040
+ if (event.synthetic) {
7041
+ syntheticDiagnosticText = event.text;
6238
7042
  }
6239
- if (visibleText && !shouldSuppress()) {
6240
- await renderer.addText(visibleText, event.outputTokens, event.turn, event.assistantUuid, event.phase);
6241
- hasProjectedCurrentReplyText = true;
7043
+ else {
7044
+ // 网关偶尔会把上下文超限错误伪装成普通 text event。不要向事件总线或
7045
+ // 渲染器投影错误行;lastReplyText 仍保留原文本,以便流结束后触发恢复。
7046
+ const visibleText = stripContextLimitErrorLines(event.text);
7047
+ if (visibleText) {
7048
+ hasReceivedText = true;
7049
+ if (visibleText.trim())
7050
+ madeProgress = true;
7051
+ }
7052
+ lastReplyText += event.text;
7053
+ if (visibleText) {
7054
+ this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: visibleText, isFinal: false });
7055
+ }
7056
+ if (visibleText && !shouldSuppress()) {
7057
+ await renderer.addText(visibleText, event.outputTokens, event.turn, event.assistantUuid, event.phase);
7058
+ hasProjectedCurrentReplyText = true;
7059
+ }
6242
7060
  }
6243
7061
  }
6244
7062
  // compact 完成
@@ -6330,6 +7148,7 @@ export class ResponseEngine {
6330
7148
  state: modeHooks.state,
6331
7149
  sessionState: modeHooks.sessionState,
6332
7150
  turn: turnLease,
7151
+ modelTurn: typeof event.turn === 'number' ? event.turn : undefined,
6333
7152
  toolName: event.name,
6334
7153
  toolInput: event.input || {},
6335
7154
  injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
@@ -6344,7 +7163,7 @@ export class ResponseEngine {
6344
7163
  });
6345
7164
  },
6346
7165
  getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
6347
- isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID),
7166
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID) || isThreadRoutedSendForSelf(toolName, toolInput, proactiveSelfAid || session.selfAID),
6348
7167
  logger,
6349
7168
  });
6350
7169
  }
@@ -6464,21 +7283,20 @@ export class ResponseEngine {
6464
7283
  });
6465
7284
  },
6466
7285
  getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
6467
- isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID),
7286
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID) || isThreadRoutedSendForSelf(toolName, toolInput, proactiveSelfAid || session.selfAID),
6468
7287
  logger,
6469
7288
  });
6470
7289
  }
6471
7290
  // 从 tool_use 阶段缓存的描述中回溯
6472
7291
  const cachedDesc = event.callId ? toolDescByCallId.get(event.callId) : undefined;
6473
- if (event.isError && !shouldSuppress()) {
7292
+ const toolResultFailed = isToolResultFailure(event);
7293
+ if (toolResultFailed && !shouldSuppress()) {
6474
7294
  hasErrorResult = true;
6475
- let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
6476
- // 移除 XML 风格的错误标签
6477
- errorMsg = errorMsg.replace(/<tool_use_error>(.*?)<\/tool_use_error>/gs, '$1');
6478
- renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, event.durationMs, cachedDesc, event, event.assistantUuid);
7295
+ const errorMsg = getToolResultErrorText(event) + diagnosticSuffix;
7296
+ renderer.addToolResult(event.name || '\u5de5\u5177', false, event.result, errorMsg, event.callId, event.durationMs, cachedDesc, event, event.assistantUuid);
6479
7297
  }
6480
- else if (!event.isError && !shouldSuppress()) {
6481
- renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, event.durationMs, cachedDesc, undefined, event.assistantUuid);
7298
+ else if (!toolResultFailed && !shouldSuppress()) {
7299
+ renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, event.durationMs, cachedDesc, event, event.assistantUuid);
6482
7300
  }
6483
7301
  }
6484
7302
  // 非终态运行时告警走 status notify,不进入 activity/thought 历史。
@@ -6502,6 +7320,7 @@ export class ResponseEngine {
6502
7320
  errors: [event.error],
6503
7321
  lastReplyText,
6504
7322
  fullText: '',
7323
+ syntheticDiagnosticText,
6505
7324
  };
6506
7325
  }
6507
7326
  // 完成事件
@@ -6516,7 +7335,7 @@ export class ResponseEngine {
6516
7335
  logger.info(`[ResponseEngine] Auto-filled session name: ${event.sessionTitle}`);
6517
7336
  }
6518
7337
  // 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
6519
- completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, model: event.model, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage, lastModelCall: event.lastModelCall, modelCalls: event.modelCalls };
7338
+ completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, model: event.model, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage, lastModelCall: event.lastModelCall, modelCalls: event.modelCalls, syntheticDiagnosticText };
6520
7339
  if (!event.isError)
6521
7340
  madeProgress = true;
6522
7341
  // thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
@@ -6567,7 +7386,7 @@ export class ResponseEngine {
6567
7386
  logger.info(`[ResponseEngine] Auto-filled session name: ${event.sessionTitle}`);
6568
7387
  }
6569
7388
  // 记录完成状态
6570
- completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, model: event.model, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage, lastModelCall: event.lastModelCall };
7389
+ completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, syntheticDiagnosticText, numTurns: event.numTurns, ttftMs: event.ttftMs, model: event.model, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage, lastModelCall: event.lastModelCall };
6571
7390
  if (event.subtype === 'success')
6572
7391
  madeProgress = true;
6573
7392
  }