evolcore 0.0.32 → 0.0.33

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 (150) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/bin/install-codex-managed-hooks.mjs +124 -37
  3. package/dist/agents/claude-runner.js +29 -2
  4. package/dist/agents/codex/app-server-client.js +3 -0
  5. package/dist/agents/codex/diagnostics.js +76 -44
  6. package/dist/agents/codex-runner.js +23 -19
  7. package/dist/agents/ecagent-runner.js +5 -13
  8. package/dist/aun/group/operations.js +0 -1
  9. package/dist/aun/msg/history.js +1 -0
  10. package/dist/aun/msg/p2p.js +0 -1
  11. package/dist/aun/outbox.js +50 -1
  12. package/dist/channels/aun.js +28 -2
  13. package/dist/channels/daemon.js +257 -2
  14. package/dist/cli/agent-command.js +58 -24
  15. package/dist/cli/agent.js +1 -1
  16. package/dist/cli/aun-commands.js +365 -29
  17. package/dist/cli/contact.js +8 -2
  18. package/dist/cli/daemon-commands.js +81 -12
  19. package/dist/cli/debug-mode.js +154 -0
  20. package/dist/cli/handoff-command.js +3 -0
  21. package/dist/cli/index.js +81 -64
  22. package/dist/cli/init.js +2 -2
  23. package/dist/cli/managed-command-guard.js +5 -0
  24. package/dist/cli/memory-command.js +8 -1
  25. package/dist/cli/queue-command.js +9 -4
  26. package/dist/cli/session-command.js +194 -0
  27. package/dist/cli/skill-command.js +78 -2
  28. package/dist/cli/task-context.js +1 -0
  29. package/dist/cli/thread-command.js +620 -0
  30. package/dist/cli/trigger-command.js +4 -2
  31. package/dist/config/builtin-roles.js +1 -0
  32. package/dist/config/config-field-policy.js +2 -3
  33. package/dist/config/config-manager.js +7 -2
  34. package/dist/config/resolved-config-op.js +3 -0
  35. package/dist/config-store.js +15 -0
  36. package/dist/core/auth/agent-delegation.js +25 -1
  37. package/dist/core/auth/debug-context.js +139 -0
  38. package/dist/core/auth/operation-authorizer.js +24 -1
  39. package/dist/core/auth/operation-catalog.js +33 -0
  40. package/dist/core/background-session/session-cleanup.js +212 -0
  41. package/dist/core/background-session/types.js +10 -0
  42. package/dist/core/bootstrap-messages.js +1 -1
  43. package/dist/core/bootstrap-service.js +6 -3
  44. package/dist/core/command/command-handler.js +60 -24
  45. package/dist/core/command/memory-slash.js +6 -2
  46. package/dist/core/command/menu-catalog.js +48 -1
  47. package/dist/core/command/menu-handler.js +92 -16
  48. package/dist/core/command/menu-protocol.js +24 -1
  49. package/dist/core/command/skill-slash.js +322 -0
  50. package/dist/core/command/slash-gate.js +6 -4
  51. package/dist/core/command/slash-handler.js +193 -13
  52. package/dist/core/command/topic-slash.js +528 -0
  53. package/dist/core/evolagent-registry.js +20 -5
  54. package/dist/core/handoff/runtime.js +5 -11
  55. package/dist/core/handoff/store.js +5 -0
  56. package/dist/core/ipc-registration.js +139 -2
  57. package/dist/core/memory/consolidation-context.js +49 -16
  58. package/dist/core/memory/consolidation-ops-records.js +35 -0
  59. package/dist/core/memory/consolidation-queue-format.js +21 -0
  60. package/dist/core/memory/consolidation.js +174 -180
  61. package/dist/core/message/aun-ipc-sender.js +161 -6
  62. package/dist/core/message/im-renderer.js +149 -6
  63. package/dist/core/message/message-bridge.js +13 -3
  64. package/dist/core/message/message-log.js +1 -0
  65. package/dist/core/message/peer-mode.js +6 -1
  66. package/dist/core/message/response-engine.js +349 -67
  67. package/dist/core/message/subtask-tool-projection.js +140 -0
  68. package/dist/core/model/catalog-service.js +17 -12
  69. package/dist/core/permission/ec-command-parser.js +126 -7
  70. package/dist/core/session/session-idle-monitor.js +249 -22
  71. package/dist/core/session/session-manager.js +54 -9
  72. package/dist/core/skill/command-parser.js +13 -0
  73. package/dist/core/skill/index-generator.js +4 -1
  74. package/dist/core/skill/startup.js +41 -0
  75. package/dist/core/skill/switch-resolver.js +55 -29
  76. package/dist/core/thread/command-parser.js +125 -0
  77. package/dist/core/thread/index.js +1 -0
  78. package/dist/core/thread/ipc-operation.js +95 -0
  79. package/dist/core/thread/thread-context-storage.js +131 -0
  80. package/dist/core/thread/thread-operation.js +258 -0
  81. package/dist/core/thread/thread-route.js +33 -0
  82. package/dist/core/thread/thread-service.js +777 -0
  83. package/dist/core/thread/types.js +1 -0
  84. package/dist/eck/message-renderer.js +35 -0
  85. package/dist/index.js +8 -5
  86. package/dist/ipc.js +89 -10
  87. package/dist/paths.js +23 -0
  88. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  89. package/dist/trigger/authorization.js +57 -28
  90. package/dist/trigger/feedback.js +7 -1
  91. package/dist/trigger/parser.js +63 -1
  92. package/dist/trigger/validation.js +14 -0
  93. package/dist/utils/error-utils.js +22 -1
  94. package/kits/docs/context-assembly.md +1 -0
  95. package/kits/docs/evolcore/INDEX.md +1 -0
  96. package/kits/docs/evolcore/agent-create.md +39 -28
  97. package/kits/docs/evolcore/agent.md +19 -4
  98. package/kits/docs/evolcore/ec-capabilities.md +4 -0
  99. package/kits/docs/evolcore/group-collaboration.md +3 -5
  100. package/kits/docs/evolcore/group.md +3 -4
  101. package/kits/docs/evolcore/memory-consolidation.md +1 -1
  102. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  103. package/kits/docs/evolcore/menu-protocol.md +7 -0
  104. package/kits/docs/evolcore/msg.md +5 -8
  105. package/kits/docs/evolcore/thread-chain.md +267 -0
  106. package/kits/docs/prompt-loading-architecture.md +1 -0
  107. package/kits/eck_manifest.json +61 -26
  108. package/kits/eck_message_manifest.json +169 -10
  109. package/kits/rules/05-venue.md +1 -1
  110. package/kits/rules/07-memory-and-thread.md +202 -0
  111. package/kits/schemas/relation-config.schema.9.json +18 -3
  112. package/kits/schemas/role-config.schema.1.json +6 -6
  113. package/kits/schemas/single-session.schema.1.json +6 -6
  114. package/kits/schemas/single-session.schema.2.json +6 -6
  115. package/kits/schemas/single-session.schema.3.json +6 -6
  116. package/kits/skills/.switches.json +12 -1
  117. package/kits/skills/ec-group/SKILL.md +234 -0
  118. package/kits/skills/ec-skill/SKILL.md +10 -1
  119. package/kits/skills/ec-skill/scripts/cli.mjs +15 -1
  120. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  121. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  122. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  123. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  124. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  125. package/kits/skills/thread-ops/SKILL.md +492 -0
  126. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  127. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  128. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  129. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  130. package/kits/templates/bootstrap-welcome.md +7 -7
  131. package/kits/templates/memory-consolidation/forget.md +5 -0
  132. package/kits/templates/memory-consolidation/generalize.md +5 -0
  133. package/kits/templates/memory-consolidation/profile.md +5 -0
  134. package/kits/templates/memory-consolidation/promote.md +5 -0
  135. package/kits/templates/message-fragments/item.md +2 -1
  136. package/kits/templates/message-fragments/thread-context.md +6 -0
  137. package/kits/templates/message-fragments/trigger-session.md +2 -0
  138. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  139. package/kits/templates/roles/member.json +2 -0
  140. package/kits/templates/roles/visitor.json +2 -0
  141. package/kits/templates/system-fragments/bootstrap.md +66 -72
  142. package/kits/templates/system-fragments/channel.md +3 -6
  143. package/kits/templates/system-fragments/commands.md +1 -0
  144. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  145. package/kits/templates/system-fragments/reply.md +3 -2
  146. package/kits/templates/system-fragments/session.md +5 -14
  147. package/package.json +3 -1
  148. package/kits/docs/evolcore/group-fs.md +0 -17
  149. package/kits/docs/venues/aun-group.md +0 -45
  150. package/kits/docs/venues/group.md +0 -43
@@ -11,6 +11,7 @@ import { authorizeAccess, authorizeOperation, buildAuthSubject } from '../auth/a
11
11
  import { buildTaskRuntimeEnv, ensureRuntimeLockDir, ensureSessionRuntimeDir } from '../../cli/task-context.js';
12
12
  import { memorySessionPolicyHook } from '../memory/memory-session-policy.js';
13
13
  import { IMRenderer } from './im-renderer.js';
14
+ import { projectSubtaskStarted, projectSubtaskTerminal } from './subtask-tool-projection.js';
14
15
  import { TaskActivityStore } from '../task/task-activity-store.js';
15
16
  import { createSendFileMarkerPattern } from './file-markers.js';
16
17
  import { createDecisionInferenceProvider, createTextInferenceProvider } from '../inference/text-inference.js';
@@ -19,9 +20,10 @@ import { logger } from '../../utils/logger.js';
19
20
  import { isHostChinese } from '../../utils/locale.js';
20
21
  import { ExecutionTimeoutError, OrphanedTargetSessionError, isExecutionFailure, getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
21
22
  import { readEvolcoreRuntimeFlavor, tagErrorTextWithEvolcoreVersion } from '../../utils/evolcore-version.js';
22
- import { describeProactiveSendCommandFailure, isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
23
+ import { describeProactiveSendCommandFailure, isExactEvolcoreSendCommandForSession, isThreadRoutedEvolcoreSendCommand } from '../permission/ec-command-parser.js';
24
+ import { resolveThreadRouteTarget } from '../thread/thread-route.js';
23
25
  import { summarizeToolInputForDisplay } from '../../utils/tool-summary.js';
24
- import { agentDataDir, agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
26
+ import { agentDataDir, agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths, debugSessionsDir } from '../../paths.js';
25
27
  import { renderKitSections, resolveSessionManifest, DEFAULT_MANIFEST_FILE } from '../../eck/kit-renderer.js';
26
28
  import { renderMessageBody } from '../../eck/message-renderer.js';
27
29
  import { syncGroupRulesContext } from '../../eck/group-rules-sync.js';
@@ -67,6 +69,10 @@ import { auditFullAccessEvent } from '../auth/authorization-audit.js';
67
69
  import { TaskRecoveryService, isTaskRecoveryCurrent } from '../task/task-recovery-service.js';
68
70
  const QUESTION_TTL_MS = 24 * 60 * 60 * 1000;
69
71
  /** Pending async questions outlive the originating turn, but never change its backend binding. */
72
+ /** 带 --thread 的跨话题发送:话题存在于本端索引且目标一致时视为合法发送。 */
73
+ function isThreadRoutedSendForSelf(toolName, toolInput, selfAid) {
74
+ return isThreadRoutedEvolcoreSendCommand(toolName, toolInput, selfAid, (scope, threadId, target) => !!selfAid && !!resolveThreadRouteTarget(selfAid, threadId, scope, target));
75
+ }
70
76
  export class AsyncQuestionService {
71
77
  deps;
72
78
  registered = new Map();
@@ -673,16 +679,8 @@ function getStreamErrorText(result) {
673
679
  return parts.join('\n');
674
680
  }
675
681
  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;
682
+ const raw = getStreamErrorText(result);
683
+ return getErrorMessage(new Error(raw), result.terminalReason, includeEmoji);
686
684
  }
687
685
  function streamHitContextLimit(result) {
688
686
  return result.terminalReason === 'prompt_too_long' ||
@@ -826,6 +824,8 @@ export class ResponseEngine {
826
824
  activeMonitors = new Map();
827
825
  /** sessionId → 当前正在处理任务的运行时上下文,供 in-task CLI 通过 IPC 查询。 */
828
826
  activeTaskRuntimeContexts = new Map();
827
+ /** sessionId → 本轮最近一次 runQuery 请求的模型;仅在任务活跃时对外可见(见 getOutboundDebug)。 */
828
+ activeRunnerModels = new Map();
829
829
  /** task.run started 后立即登记,覆盖运行时上下文尚未建立的 shutdown 窗口。 */
830
830
  activeTaskSpans = new Map();
831
831
  /** sessionId → 尚未收到 runner:task-notification 的子任务。 */
@@ -1404,6 +1404,7 @@ export class ResponseEngine {
1404
1404
  this.sessionManager.clearProcessingIfTask(sessionId, taskId);
1405
1405
  if (this.activeTaskRuntimeContexts.get(sessionId)?.taskId === taskId) {
1406
1406
  this.activeTaskRuntimeContexts.delete(sessionId);
1407
+ this.activeRunnerModels.delete(sessionId);
1407
1408
  }
1408
1409
  this.agentDelegationRegistry?.revokeTask(sessionId, taskId);
1409
1410
  }
@@ -1482,6 +1483,64 @@ export class ResponseEngine {
1482
1483
  getTaskRuntimeContext(sessionId) {
1483
1484
  return this.activeTaskRuntimeContexts.get(sessionId) ?? null;
1484
1485
  }
1486
+ /** debug 模式下写/删 debug-sessions 临时文件 */
1487
+ debugWriteSessionSnapshot(sessionId, ctx) {
1488
+ if (process.env.EVOLCORE_DEBUG_MODE !== '1')
1489
+ return;
1490
+ try {
1491
+ const dir = debugSessionsDir();
1492
+ if (!fs.existsSync(dir))
1493
+ fs.mkdirSync(dir, { recursive: true });
1494
+ const snapshot = {
1495
+ schemaVersion: 1,
1496
+ sessionId,
1497
+ selfAid: ctx.selfAid ?? '',
1498
+ channel: ctx.channel ?? '',
1499
+ channelKey: ctx.channelKey ?? '',
1500
+ channelId: ctx.channelId ?? '',
1501
+ peerKey: ctx.peerKey ?? '',
1502
+ chatType: ctx.chatType ?? 'private',
1503
+ threadId: ctx.threadId ?? '',
1504
+ sessionType: ctx.sessionType ?? 'main',
1505
+ baseagent: ctx.baseagent ?? '',
1506
+ writtenAt: Date.now(),
1507
+ };
1508
+ fs.writeFileSync(`${dir}/${sessionId}.json`, JSON.stringify(snapshot), 'utf-8');
1509
+ }
1510
+ catch (err) {
1511
+ logger.debug(`[Debug] failed to write debug session snapshot for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
1512
+ }
1513
+ }
1514
+ debugDeleteSessionSnapshot(sessionId) {
1515
+ if (process.env.EVOLCORE_DEBUG_MODE !== '1')
1516
+ return;
1517
+ try {
1518
+ const snapshotPath = `${debugSessionsDir()}/${sessionId}.json`;
1519
+ if (fs.existsSync(snapshotPath))
1520
+ fs.rmSync(snapshotPath, { force: true });
1521
+ }
1522
+ catch (err) {
1523
+ logger.debug(`[Debug] failed to delete debug session snapshot for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
1524
+ }
1525
+ }
1526
+ /**
1527
+ * 出站 payload.debug 的会话侧字段(不含 sentAt,由发送方盖戳)。
1528
+ * 供 agent 经 CLI(ec msg send / ec group send)委托发送的路径使用,
1529
+ * 与 IMRenderer 回复路径写入的 debug 同构。会话不存在返回 undefined。
1530
+ */
1531
+ async getOutboundDebug(sessionId) {
1532
+ const session = await this.sessionManager.getSessionById(sessionId);
1533
+ if (!session)
1534
+ return undefined;
1535
+ const model = this.activeTaskRuntimeContexts.has(sessionId)
1536
+ ? this.activeRunnerModels.get(sessionId)
1537
+ : undefined;
1538
+ return {
1539
+ sessionId: session.id,
1540
+ ...(session.agentSessionId ? { agentSessionId: session.agentSessionId } : {}),
1541
+ ...(model ? { model } : {}),
1542
+ };
1543
+ }
1485
1544
  setAgentDelegationRegistry(registry) {
1486
1545
  this.agentDelegationRegistry = registry;
1487
1546
  }
@@ -1796,6 +1855,71 @@ export class ResponseEngine {
1796
1855
  && message.source === 'trigger'
1797
1856
  && !!message.triggerMeta?.triggerId;
1798
1857
  }
1858
+ isTrustedBgSession(message) {
1859
+ return message.channel === 'daemon'
1860
+ && message.source === 'bg-session'
1861
+ && !!message.bgSessionMeta?.runId;
1862
+ }
1863
+ bgSessionTerminalMessages = new WeakSet();
1864
+ claimBgSessionTerminal(message) {
1865
+ const meta = message.bgSessionMeta;
1866
+ if (message.source !== 'bg-session' || !meta?.runId || this.bgSessionTerminalMessages.has(message)) {
1867
+ return undefined;
1868
+ }
1869
+ this.bgSessionTerminalMessages.add(message);
1870
+ return { meta, runId: meta.runId };
1871
+ }
1872
+ publishBgSessionError(message, error, opts = {}) {
1873
+ const terminal = this.claimBgSessionTerminal(message);
1874
+ if (!terminal)
1875
+ return;
1876
+ const { meta, runId } = terminal;
1877
+ this.eventBus.publish({
1878
+ type: 'bg-session:error',
1879
+ kind: meta.kind,
1880
+ runId,
1881
+ sessionId: meta.boundSessionId ?? '',
1882
+ messageId: opts.messageId ?? message.messageId ?? runId,
1883
+ error,
1884
+ targetChannel: message.channel,
1885
+ targetChannelId: message.channelId,
1886
+ causation: opts.causation ?? message.causation,
1887
+ });
1888
+ }
1889
+ publishBgSessionInterrupted(message, reason, causation) {
1890
+ const terminal = this.claimBgSessionTerminal(message);
1891
+ if (!terminal)
1892
+ return;
1893
+ const { meta, runId } = terminal;
1894
+ this.eventBus.publish({
1895
+ type: 'bg-session:interrupted',
1896
+ kind: meta.kind,
1897
+ runId,
1898
+ sessionId: meta.boundSessionId ?? '',
1899
+ messageId: message.messageId ?? runId,
1900
+ reason,
1901
+ targetChannel: message.channel,
1902
+ targetChannelId: message.channelId,
1903
+ causation: causation ?? message.causation,
1904
+ });
1905
+ }
1906
+ publishBgSessionCompleted(message, messageId, durationMs, causation) {
1907
+ const terminal = this.claimBgSessionTerminal(message);
1908
+ if (!terminal)
1909
+ return;
1910
+ const { meta, runId } = terminal;
1911
+ this.eventBus.publish({
1912
+ type: 'bg-session:completed',
1913
+ kind: meta.kind,
1914
+ runId,
1915
+ sessionId: meta.boundSessionId ?? '',
1916
+ messageId,
1917
+ durationMs,
1918
+ targetChannel: message.channel,
1919
+ targetChannelId: message.channelId,
1920
+ causation: causation ?? message.causation,
1921
+ });
1922
+ }
1799
1923
  /**
1800
1924
  * A fullaccess Trigger may run with no current relation access for its
1801
1925
  * origin actor, but only after its persisted owner provenance still
@@ -1859,6 +1983,8 @@ export class ResponseEngine {
1859
1983
  if (!selfAid || !actorId || !channelType || !originChannelKey)
1860
1984
  return undefined;
1861
1985
  const chatType = message.triggerMeta.originChatType ?? 'private';
1986
+ if (chatType === 'group' && !message.triggerMeta.originConversationId)
1987
+ return undefined;
1862
1988
  const conversationId = message.triggerMeta.originConversationId ?? actorId;
1863
1989
  const control = message.triggerMeta.originControl === true;
1864
1990
  let actor;
@@ -1889,6 +2015,7 @@ export class ResponseEngine {
1889
2015
  actorId,
1890
2016
  channelType,
1891
2017
  originChannelKey,
2018
+ conversationId: chatType === 'group' ? conversationId : actorId,
1892
2019
  control,
1893
2020
  peerKey: formatPeerKey(channelType, chatType === 'group' ? conversationId : actorId),
1894
2021
  };
@@ -1958,8 +2085,11 @@ export class ResponseEngine {
1958
2085
  /** Publish a terminal outcome before an early return from Trigger execution. */
1959
2086
  publishTriggerExecutionFailure(message, error, opts = {}) {
1960
2087
  const terminal = this.claimTriggerTerminal(message);
1961
- if (!terminal)
2088
+ if (!terminal) {
2089
+ // bg-session 路径:trigger terminal 不匹配时尝试 bg-session
2090
+ this.publishBgSessionError(message, error, { messageId: opts.messageId, causation: opts.causation });
1962
2091
  return;
2092
+ }
1963
2093
  const { trigger, runId } = terminal;
1964
2094
  const attemptId = trigger.attemptId;
1965
2095
  this.eventBus.publish({
@@ -1980,8 +2110,11 @@ export class ResponseEngine {
1980
2110
  }
1981
2111
  publishTriggerExecutionSkipped(message, reason, causation, interruption) {
1982
2112
  const terminal = this.claimTriggerTerminal(message);
1983
- if (!terminal)
2113
+ if (!terminal) {
2114
+ // bg-session 路径
2115
+ this.publishBgSessionInterrupted(message, reason, causation);
1984
2116
  return;
2117
+ }
1985
2118
  const { trigger, runId } = terminal;
1986
2119
  this.eventBus.publish({
1987
2120
  type: 'trigger:skipped',
@@ -2017,6 +2150,14 @@ export class ResponseEngine {
2017
2150
  logger.warn(`[ResponseEngine] Failed to close interrupted Trigger run=${message.triggerMeta?.runId ?? '<unknown>'}: ${error instanceof Error ? error.message : String(error)}`);
2018
2151
  });
2019
2152
  }
2153
+ else if (this.isTrustedBgSession(message)) {
2154
+ await adapter.send(envelope, {
2155
+ kind: 'status.interrupted',
2156
+ metadata: { reason },
2157
+ }).catch(error => {
2158
+ logger.warn(`[ResponseEngine] Failed to close interrupted bg-session run=${message.bgSessionMeta?.runId ?? '<unknown>'}: ${error instanceof Error ? error.message : String(error)}`);
2159
+ });
2160
+ }
2020
2161
  this.publishTriggerExecutionSkipped(message, reason, causation, interruption);
2021
2162
  }
2022
2163
  publishTriggerExecutionCompleted(message, messageId, durationMs, causation) {
@@ -2888,8 +3029,12 @@ export class ResponseEngine {
2888
3029
  ? { kind: 'restartResume', groupId: message.groupId, threadId: message.threadId }
2889
3030
  : undefined,
2890
3031
  previousTurnState: this.turnCoordinator.current(session),
2891
- isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, selfAid),
2892
- describeSendCommandFailure: describeProactiveSendCommandFailure,
3032
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, selfAid) || isThreadRoutedSendForSelf(toolName, toolInput, selfAid),
3033
+ describeSendCommandFailure: (toolName, toolInput) => describeProactiveSendCommandFailure(toolName, toolInput, {
3034
+ selfAid,
3035
+ channelId: session.channelId,
3036
+ chatType: chatType,
3037
+ }),
2893
3038
  logger,
2894
3039
  } : null;
2895
3040
  if (resolvedMode?.mode.beforeProcess && modeProcessCtx) {
@@ -2952,6 +3097,7 @@ export class ResponseEngine {
2952
3097
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
2953
3098
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
2954
3099
  this.activeTaskRuntimeContexts.delete(session.id);
3100
+ this.activeRunnerModels.delete(session.id); // F1修复
2955
3101
  }
2956
3102
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
2957
3103
  const daemonTrigger = this.isTrustedDaemonTrigger(message);
@@ -3301,9 +3447,9 @@ export class ResponseEngine {
3301
3447
  await adapter.send(withEnvelopeReplyContext(envelope, capturedReplyContext), { kind: 'result.text', text, isFinal: false });
3302
3448
  };
3303
3449
  const authChatType = configChatType;
3304
- const authConversationId = authChatType === 'group'
3450
+ const authConversationId = triggerExecutionIdentity?.conversationId ?? (authChatType === 'group'
3305
3451
  ? (session.metadata?.groupId || session.channelId || capturedChannelId)
3306
- : configActorId;
3452
+ : configActorId);
3307
3453
  const authPeerKey = configChannelType && authConversationId
3308
3454
  ? formatPeerKey(configChannelType, authConversationId)
3309
3455
  : undefined;
@@ -3552,16 +3698,31 @@ export class ResponseEngine {
3552
3698
  const pluginHook = pureSessionPolicyHook;
3553
3699
  if (!pluginHook)
3554
3700
  return undefined;
3555
- return (toolName, toolInput) => {
3701
+ return (toolName, toolInput, meta) => {
3556
3702
  const stBefore = modeState.get('proactive');
3557
3703
  const wasFirstPending = stBefore && !stBefore.firstToolDone;
3558
- const result = pluginHook(toolName, toolInput);
3704
+ const result = pluginHook(toolName, toolInput, meta);
3559
3705
  // 整理会话的拦截原因已经写清楚怎么改,不套 proactive 违规提醒
3560
3706
  if (result?.block && result.policyCode?.startsWith('memory_session_'))
3561
3707
  return result;
3562
3708
  if (result?.block) {
3563
3709
  // 拦截事件(首工具非表态 / 工具进展汇报未完成)
3564
3710
  snapshot.set(session.id, taskId, { policyHook: { triggered: true, blocked: true, toolName } });
3711
+ // 问题1/2修复:熔断码表示连续违规已超阈值,停止注入提醒,直接中断任务。
3712
+ // F5修复:向渠道发一条人类可读的错误说明,避免静默中断。
3713
+ if (result.policyCode === 'proactive_circuit_break') {
3714
+ void adapter.send(withEnvelopeReplyContext(envelope, capturedReplyContext), {
3715
+ kind: 'result.text',
3716
+ text: '⚠️ 任务执行遇到循环障碍,已自动中断。请重新描述你的需求。',
3717
+ isFinal: true,
3718
+ }).catch(err => {
3719
+ logger.warn(`[ResponseEngine] circuit break channel send failed: session=${session.id} error=${err instanceof Error ? err.message : String(err)}`);
3720
+ });
3721
+ void agent.interrupt(session.id).catch(error => {
3722
+ logger.debug(`[ResponseEngine] circuit break interrupt failed: session=${session.id} error=${error instanceof Error ? error.message : String(error)}`);
3723
+ });
3724
+ return result;
3725
+ }
3565
3726
  const errorMsg = `⚠️ proactive 模式违规:${result.reason ?? '请先发送必要说明'}。请重新执行正确的命令。`;
3566
3727
  agent.injectUserMessage?.(session.id, errorMsg);
3567
3728
  return result;
@@ -3605,6 +3766,8 @@ export class ResponseEngine {
3605
3766
  let previousFallbackState;
3606
3767
  let agentModel;
3607
3768
  let v2HandoffIds = [];
3769
+ /** T3: 本次注入的 Thread Context 条目(渲染后据此清空 inbox)。 */
3770
+ let threadContextItems = [];
3608
3771
  let v2HandoffResultRefs = [];
3609
3772
  let v2HandoffDirection;
3610
3773
  let handoffPromptRendered = false;
@@ -4137,6 +4300,16 @@ export class ResponseEngine {
4137
4300
  const rawPeerItems = message.source === 'owner-inject' ? [] : message.items && message.items.length > 0
4138
4301
  ? message.items
4139
4302
  : [{
4303
+ // T9.5/T10: trigger 消息填充专属 kind + triggerInfo(含 labels),走 msg-trigger-session/thread-check 渲染
4304
+ ...(message.source === 'trigger' && message.triggerMeta?.triggerId ? {
4305
+ kind: 'trigger',
4306
+ triggerInfo: {
4307
+ triggerId: message.triggerMeta.triggerId,
4308
+ triggerName: message.triggerMeta.triggerName,
4309
+ fireTime: message.triggerMeta.fireTime,
4310
+ labels: message.triggerMeta.labels,
4311
+ },
4312
+ } : {}),
4140
4313
  peerId: message.peerId, peerName: peerName || undefined,
4141
4314
  peerType: message.peerType,
4142
4315
  peerRole: message.batchRole || session.identity?.role || 'none',
@@ -4147,6 +4320,34 @@ export class ResponseEngine {
4147
4320
  mentions: message.mentions,
4148
4321
  mentionAids: message.mentionAids,
4149
4322
  }];
4323
+ // T3.2: 读取 Thread Context inbox —— 跨话题发送留在本端的注入提醒。
4324
+ // 注入一次后立即删除整个文件(T3.3),不依赖任何消费标记。
4325
+ threadContextItems = [];
4326
+ if (selfAid && message.source !== 'owner-inject') {
4327
+ try {
4328
+ const { ThreadService } = await import('../thread/index.js');
4329
+ const inbox = ThreadService.readInbox(selfAid, session.id);
4330
+ threadContextItems = inbox.map(entry => ({
4331
+ kind: 'thread-context',
4332
+ peerId: entry.originPeer,
4333
+ peerType: 'agent',
4334
+ peerRole: 'none',
4335
+ content: entry.content,
4336
+ timestamp: entry.timestamp,
4337
+ threadContext: {
4338
+ messageId: entry.messageId,
4339
+ originThreadId: entry.originThreadId,
4340
+ originPeer: entry.originPeer,
4341
+ originSessionId: entry.originSessionId,
4342
+ ...(entry.triggerId ? { triggerId: entry.triggerId } : {}),
4343
+ gapDetection: entry.gapDetection,
4344
+ },
4345
+ }));
4346
+ }
4347
+ catch (e) {
4348
+ logger.warn(`[ResponseEngine] ThreadContext inbox 读取失败: ${e?.message ?? e}`);
4349
+ }
4350
+ }
4150
4351
  const peerItems = (() => {
4151
4352
  if (message.handoffDelivery && this.handoffRuntime) {
4152
4353
  const items = this.handoffRuntime.buildPromptItems(message, peerRole);
@@ -4156,10 +4357,10 @@ export class ResponseEngine {
4156
4357
  : (item.handoff?.handoffId ? [item.handoff.handoffId] : [])))));
4157
4358
  v2HandoffResultRefs = items.flatMap(item => item.handoff?.resultRefs ?? []);
4158
4359
  v2HandoffDirection = message.handoffDelivery.direction;
4159
- return items;
4360
+ return [...threadContextItems, ...items];
4160
4361
  }
4161
4362
  }
4162
- return rawPeerItems;
4363
+ return [...threadContextItems, ...rawPeerItems];
4163
4364
  })();
4164
4365
  // 将匹配的 pending hint 暂时 claim,排在真实消息之前;渲染或启动
4165
4366
  // 失败时释放 claim,只有 runner 接受 prompt 后才持久化 consumed。
@@ -4263,6 +4464,16 @@ export class ResponseEngine {
4263
4464
  }
4264
4465
  }
4265
4466
  }
4467
+ // T3.3: 渲染成功、prompt 非空 —— 注入完成,删除 inbox(注入一次语义)。
4468
+ if (threadContextItems.length > 0 && selfAid) {
4469
+ try {
4470
+ const { ThreadService } = await import('../thread/index.js');
4471
+ ThreadService.clearInbox(selfAid, session.id);
4472
+ }
4473
+ catch (e) {
4474
+ logger.warn(`[ResponseEngine] ThreadContext inbox 清理失败: ${e?.message ?? e}`);
4475
+ }
4476
+ }
4266
4477
  // Pass the stable session directory explicitly so sandboxed helper
4267
4478
  // commands never fall back to a process, agent, or project directory
4268
4479
  // for transient state.
@@ -4273,6 +4484,10 @@ export class ResponseEngine {
4273
4484
  messageId: message.messageId,
4274
4485
  baseagent: normalizedBaseagent.canonical,
4275
4486
  channel: configChannelType,
4487
+ // 完整 channelKey(= adapter.channelName / inst.name,如 `aun#<selfAid>#main`)。
4488
+ // 必须与下面 issue() 的 `channel:` 用同一表达式,否则 debug 路径拿到的
4489
+ // origin.channelKey 会与真实任务路径不一致(见 D5)。
4490
+ channelKey: authorizationIdentity?.originChannelKey ?? message.channel,
4276
4491
  channelId: message.channelId,
4277
4492
  chatType: configChatType,
4278
4493
  selfAid,
@@ -4304,6 +4519,7 @@ export class ResponseEngine {
4304
4519
  causation: taskCausation,
4305
4520
  };
4306
4521
  this.activeTaskRuntimeContexts.set(session.id, taskRuntimeContext);
4522
+ this.debugWriteSessionSnapshot(session.id, taskRuntimeContext);
4307
4523
  runtimeEnv = buildTaskRuntimeEnv(taskRuntimeContext);
4308
4524
  if (this.agentDelegationRegistry && configActorId && selfAid && peerKey) {
4309
4525
  const delegationToken = this.agentDelegationRegistry.issue({
@@ -4409,6 +4625,10 @@ export class ResponseEngine {
4409
4625
  mimeType: image.mimeType,
4410
4626
  }));
4411
4627
  debugRunnerModel = modelOverride?.model || agentModel;
4628
+ if (debugRunnerModel)
4629
+ this.activeRunnerModels.set(session.id, debugRunnerModel);
4630
+ else
4631
+ this.activeRunnerModels.delete(session.id);
4412
4632
  const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, runAgentSessionId, runnerImages, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
4413
4633
  // The turn may be superseded while the runner is still creating its
4414
4634
  // backend query. Replay the interrupt after runQuery resolves, when
@@ -4704,6 +4924,7 @@ export class ResponseEngine {
4704
4924
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
4705
4925
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
4706
4926
  this.activeTaskRuntimeContexts.delete(session.id);
4927
+ this.activeRunnerModels.delete(session.id); // F1修复
4707
4928
  }
4708
4929
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
4709
4930
  logger.info(`[ResponseEngine] Stale turn stopped before publish: session=${session.id} task=${taskId}`);
@@ -4719,6 +4940,7 @@ export class ResponseEngine {
4719
4940
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
4720
4941
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
4721
4942
  this.activeTaskRuntimeContexts.delete(session.id);
4943
+ this.activeRunnerModels.delete(session.id); // F1修复
4722
4944
  }
4723
4945
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
4724
4946
  logger.info(`[ResponseEngine] Stale turn stopped before protocol rejection: session=${session.id} task=${taskId}`);
@@ -4870,9 +5092,10 @@ export class ResponseEngine {
4870
5092
  });
4871
5093
  }
4872
5094
  // 最终回复文本:suppressed 模式或无 text 事件时需要兜底添加
5095
+ // 错误诊断串已通过 system.error 单独发出时置 true,下游跳过 result.error 避免重复
5096
+ let sentErrorAsSystemError = false;
4873
5097
  const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
4874
5098
  const visibleFinalReplyText = stripContextLimitErrorLines(finalReplyText);
4875
- const isDiagnosticFallback = streamResult.isError && !streamResult.hasReceivedText;
4876
5099
  const isTriggerNoop = message.source === 'trigger'
4877
5100
  && typeof message.triggerMeta?.noopSentinel === 'string'
4878
5101
  && message.triggerMeta.noopSentinel.length > 0
@@ -4882,16 +5105,38 @@ export class ResponseEngine {
4882
5105
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
4883
5106
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
4884
5107
  this.activeTaskRuntimeContexts.delete(session.id);
5108
+ this.activeRunnerModels.delete(session.id); // F1修复
4885
5109
  }
4886
5110
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
4887
5111
  logger.info(`[ResponseEngine] Stale turn stopped before final flush: session=${session.id} task=${taskId}`);
4888
5112
  await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_turn', taskCausation);
4889
5113
  return;
4890
5114
  }
4891
- if (visibleFinalReplyText && !isDiagnosticFallback && !isTriggerNoop) {
5115
+ if ((visibleFinalReplyText || streamResult.syntheticDiagnosticText) && !isTriggerNoop) {
4892
5116
  // `hasReceivedText` 是任务级计数。工具调用前的文字已经显示,并不表示最后一轮
4893
5117
  // (通常来自 complete.result)的文字也已显示;用本轮投影标记避免终态丢失。
4894
- if (shouldSuppress() || !streamResult.hasProjectedCurrentReplyText) {
5118
+ //
5119
+ // 终态路由按"本轮是否以错误收场"(isError)判定,不再按"有没有收到过文本"判定。
5120
+ // 原因:上游会把错误原文当正文送进来——Claude Code 用 `<synthetic>` assistant 表达
5121
+ // 传输/API 故障(runner 已标 synthetic,见上方 text 分支),网关也会把上下文超限错误
5122
+ // 伪装成 text event。这两种情况下 hasReceivedText / lastReplyText 都非空,旧判据
5123
+ // (isError && !lastReplyText)拦不住,错误串会被当成模型的最终回复发出去
5124
+ // (2026-10-11 403「无法连接到积分服务器」案例)。
5125
+ // 以错误收场的轮次一律走 system.error:前端收到的单条消息即可判断来源,
5126
+ // 不需要等后续的 result.error。
5127
+ const errorText = visibleFinalReplyText || streamResult.syntheticDiagnosticText || getStreamErrorMessage(streamResult, false);
5128
+ if (streamResult.isError) {
5129
+ // 用原始诊断文本(含错误码)判断可重试性,不用已过 stripContextLimitErrorLines 的展示文本
5130
+ const recoverable = isRetryableError(new Error(getStreamErrorText(streamResult)));
5131
+ await adapter.send(envelope, {
5132
+ kind: 'system.error',
5133
+ text: errorText,
5134
+ subtype: streamResult.subtype ?? 'api_error',
5135
+ recoverable,
5136
+ }).catch(() => { });
5137
+ sentErrorAsSystemError = true;
5138
+ }
5139
+ else if (visibleFinalReplyText && (shouldSuppress() || !streamResult.hasProjectedCurrentReplyText)) {
4895
5140
  await renderer.addText(visibleFinalReplyText, undefined, undefined, streamResult.assistantUuid);
4896
5141
  }
4897
5142
  }
@@ -4901,6 +5146,7 @@ export class ResponseEngine {
4901
5146
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
4902
5147
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
4903
5148
  this.activeTaskRuntimeContexts.delete(session.id);
5149
+ this.activeRunnerModels.delete(session.id); // F1修复
4904
5150
  }
4905
5151
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
4906
5152
  logger.info(`[ResponseEngine] session ${session.id} processing cleared task=${taskId}`);
@@ -4991,12 +5237,17 @@ export class ResponseEngine {
4991
5237
  // DaemonChannel settles a trigger when it receives a terminal
4992
5238
  // payload. Send the metadata-bearing status first so failed runs do
4993
5239
  // not lose their tool counts and recovery diagnostics.
5240
+ // system.error 已在 finalReplyText 分支把错误文本发给了前端,不再重复发 result.error
4994
5241
  if (this.isTrustedDaemonTrigger(message)) {
4995
5242
  await adapter.send(envelope, { kind: 'status.error', metadata: terminalMetadata }).catch(() => { });
4996
- await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
5243
+ if (!sentErrorAsSystemError) {
5244
+ await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
5245
+ }
4997
5246
  }
4998
5247
  else {
4999
- await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
5248
+ if (!sentErrorAsSystemError) {
5249
+ await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
5250
+ }
5000
5251
  await adapter.send(envelope, { kind: 'status.error', metadata: terminalMetadata }).catch(() => { });
5001
5252
  }
5002
5253
  this.touchAgentActivity(channelKey);
@@ -5249,6 +5500,7 @@ export class ResponseEngine {
5249
5500
  }
5250
5501
  if (!interruptReason) {
5251
5502
  this.publishTriggerExecutionCompleted(message, messageId, durationMs, taskCausation);
5503
+ this.publishBgSessionCompleted(message, messageId, durationMs, taskCausation);
5252
5504
  }
5253
5505
  if (!this.turnCoordinator.canPublish(turnLease))
5254
5506
  return;
@@ -5338,6 +5590,7 @@ export class ResponseEngine {
5338
5590
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
5339
5591
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
5340
5592
  this.activeTaskRuntimeContexts.delete(session.id);
5593
+ this.activeRunnerModels.delete(session.id); // F1修复
5341
5594
  }
5342
5595
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
5343
5596
  logger.info(`[ResponseEngine] session ${session.id} processing cleared (on error) task=${taskId}`);
@@ -5889,6 +6142,9 @@ export class ResponseEngine {
5889
6142
  });
5890
6143
  // 追踪最后一轮 assistant 回复文本(tool_use 之后的纯文本)
5891
6144
  let lastReplyText = '';
6145
+ // 上游合成错误帧的原文(runner 标记 synthetic 的 text)。与正文通道分开存放:
6146
+ // 它只能作为终态错误文案的来源,不得进入 lastReplyText / 投影 / hasReceivedText。
6147
+ let syntheticDiagnosticText = '';
5892
6148
  // callId → description 映射,用于 tool_result 回显描述
5893
6149
  const toolDescByCallId = new Map();
5894
6150
  const toolInputByCallId = new Map();
@@ -6113,6 +6369,7 @@ export class ResponseEngine {
6113
6369
  toolUseId: event.toolUseId,
6114
6370
  description: event.description,
6115
6371
  subagentType: event.subagentType,
6372
+ taskType: event.taskType,
6116
6373
  skipTranscript: event.skipTranscript,
6117
6374
  });
6118
6375
  this.eventBus.publish({
@@ -6131,15 +6388,21 @@ export class ResponseEngine {
6131
6388
  causation,
6132
6389
  });
6133
6390
  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}`);
6391
+ // 子任务就是来源工具调用的执行过程:来源卡片在本轮可见时归拢进它的 tool_result,
6392
+ // 不另出卡片;否则(如 Codex 子智能体无来源工具)退回合成卡片。见 subtask-tool-projection.ts。
6393
+ const folded = renderer.beginParentSubtask(event.toolUseId);
6394
+ tasks.get(event.taskId).projection = folded ? 'parent' : 'synthetic';
6395
+ if (!folded) {
6396
+ renderer.addSubtaskActivity(projectSubtaskStarted({
6397
+ taskId: event.taskId,
6398
+ taskKind: event.taskKind,
6399
+ description: event.description,
6400
+ taskType: event.taskType,
6401
+ subagentType: event.subagentType,
6402
+ toolUseId: event.toolUseId,
6403
+ workflowName: event.workflowName,
6404
+ }));
6405
+ }
6143
6406
  }
6144
6407
  continue;
6145
6408
  }
@@ -6155,6 +6418,7 @@ export class ResponseEngine {
6155
6418
  }
6156
6419
  if (event.type === 'task_notification') {
6157
6420
  const tasks = this.activeRunnerTasks.get(session.id);
6421
+ const started = tasks?.get(event.taskId);
6158
6422
  tasks?.delete(event.taskId);
6159
6423
  if (tasks?.size === 0)
6160
6424
  this.activeRunnerTasks.delete(session.id);
@@ -6175,19 +6439,26 @@ export class ResponseEngine {
6175
6439
  causation,
6176
6440
  });
6177
6441
  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}`);
6442
+ const terminal = { status: event.status, summary: event.summary, durationMs: event.usage?.durationMs };
6443
+ const toolUseId = event.toolUseId ?? started?.toolUseId;
6444
+ // 归拢的子任务:终态合并进来源工具的 tool_result(前台等真实结果、后台放出扣住的占位结果)。
6445
+ const merged = started?.projection === 'parent' && renderer.endParentSubtask(toolUseId, terminal);
6446
+ if (!merged) {
6447
+ // 没归拢上:开始时就是合成卡片,则终态配同一张合成卡片;
6448
+ // 否则来源卡片已在本轮之外,补发一条同 call_id 的终态结果更新那张卡片。
6449
+ // taskType/subagentType 只在 task_started 事件上,从已登记的任务元数据补齐。
6450
+ const toParent = started?.projection !== 'synthetic' && !!toolUseId;
6451
+ renderer.addSubtaskActivity(projectSubtaskTerminal({
6452
+ taskId: event.taskId,
6453
+ taskKind: event.taskKind,
6454
+ description: event.description ?? started?.description,
6455
+ taskType: started?.taskType,
6456
+ subagentType: event.subagentType ?? started?.subagentType,
6457
+ toolUseId,
6458
+ ...terminal,
6459
+ ...(toParent ? { callId: toolUseId, toolName: renderer.toolNameForCall(toolUseId) } : {}),
6460
+ }));
6461
+ }
6191
6462
  }
6192
6463
  continue;
6193
6464
  }
@@ -6224,21 +6495,30 @@ export class ResponseEngine {
6224
6495
  if (!isCurrentlyBackground) {
6225
6496
  // 流式文本
6226
6497
  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 });
6498
+ // 上游合成错误帧(Claude Code 的 `<synthetic>` assistant)会把 API/传输故障的
6499
+ // 错误原文当成正文送进来。这段字是诊断回显,不是模型正文——出身由 runner 标注,
6500
+ // 不靠字符串或计数猜。不进入正文通道(lastReplyText / hasReceivedText / 投影),
6501
+ // 只单独留存,供终态错误分支取用。
6502
+ if (event.synthetic) {
6503
+ syntheticDiagnosticText = event.text;
6238
6504
  }
6239
- if (visibleText && !shouldSuppress()) {
6240
- await renderer.addText(visibleText, event.outputTokens, event.turn, event.assistantUuid, event.phase);
6241
- hasProjectedCurrentReplyText = true;
6505
+ else {
6506
+ // 网关偶尔会把上下文超限错误伪装成普通 text event。不要向事件总线或
6507
+ // 渲染器投影错误行;lastReplyText 仍保留原文本,以便流结束后触发恢复。
6508
+ const visibleText = stripContextLimitErrorLines(event.text);
6509
+ if (visibleText) {
6510
+ hasReceivedText = true;
6511
+ if (visibleText.trim())
6512
+ madeProgress = true;
6513
+ }
6514
+ lastReplyText += event.text;
6515
+ if (visibleText) {
6516
+ this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: visibleText, isFinal: false });
6517
+ }
6518
+ if (visibleText && !shouldSuppress()) {
6519
+ await renderer.addText(visibleText, event.outputTokens, event.turn, event.assistantUuid, event.phase);
6520
+ hasProjectedCurrentReplyText = true;
6521
+ }
6242
6522
  }
6243
6523
  }
6244
6524
  // compact 完成
@@ -6330,6 +6610,7 @@ export class ResponseEngine {
6330
6610
  state: modeHooks.state,
6331
6611
  sessionState: modeHooks.sessionState,
6332
6612
  turn: turnLease,
6613
+ modelTurn: typeof event.turn === 'number' ? event.turn : undefined,
6333
6614
  toolName: event.name,
6334
6615
  toolInput: event.input || {},
6335
6616
  injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
@@ -6344,7 +6625,7 @@ export class ResponseEngine {
6344
6625
  });
6345
6626
  },
6346
6627
  getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
6347
- isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID),
6628
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID) || isThreadRoutedSendForSelf(toolName, toolInput, proactiveSelfAid || session.selfAID),
6348
6629
  logger,
6349
6630
  });
6350
6631
  }
@@ -6464,7 +6745,7 @@ export class ResponseEngine {
6464
6745
  });
6465
6746
  },
6466
6747
  getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
6467
- isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID),
6748
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID) || isThreadRoutedSendForSelf(toolName, toolInput, proactiveSelfAid || session.selfAID),
6468
6749
  logger,
6469
6750
  });
6470
6751
  }
@@ -6502,6 +6783,7 @@ export class ResponseEngine {
6502
6783
  errors: [event.error],
6503
6784
  lastReplyText,
6504
6785
  fullText: '',
6786
+ syntheticDiagnosticText,
6505
6787
  };
6506
6788
  }
6507
6789
  // 完成事件
@@ -6516,7 +6798,7 @@ export class ResponseEngine {
6516
6798
  logger.info(`[ResponseEngine] Auto-filled session name: ${event.sessionTitle}`);
6517
6799
  }
6518
6800
  // 记录完成状态 + 最后一轮回复文本(后续 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 };
6801
+ 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
6802
  if (!event.isError)
6521
6803
  madeProgress = true;
6522
6804
  // thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
@@ -6567,7 +6849,7 @@ export class ResponseEngine {
6567
6849
  logger.info(`[ResponseEngine] Auto-filled session name: ${event.sessionTitle}`);
6568
6850
  }
6569
6851
  // 记录完成状态
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 };
6852
+ 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
6853
  if (event.subtype === 'success')
6572
6854
  madeProgress = true;
6573
6855
  }