evolcore 0.0.31 → 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 (179) hide show
  1. package/CHANGELOG.md +35 -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 +51 -24
  7. package/dist/agents/ecagent-runner.js +9 -13
  8. package/dist/agents/gemini-runner.js +4 -0
  9. package/dist/aun/aid/store.js +0 -81
  10. package/dist/aun/group/fs-download.js +1 -10
  11. package/dist/aun/group/operations.js +0 -1
  12. package/dist/aun/msg/history.js +1 -0
  13. package/dist/aun/msg/managed-operation.js +130 -32
  14. package/dist/aun/msg/p2p.js +0 -1
  15. package/dist/aun/outbox.js +50 -1
  16. package/dist/aun/service-proxy.js +12 -18
  17. package/dist/aun/storage/index.js +29 -0
  18. package/dist/channels/aun.js +38 -137
  19. package/dist/channels/daemon.js +257 -2
  20. package/dist/cli/agent-command.js +64 -31
  21. package/dist/cli/agent-create.js +43 -24
  22. package/dist/cli/agent.js +16 -70
  23. package/dist/cli/aun-commands.js +512 -32
  24. package/dist/cli/contact.js +8 -2
  25. package/dist/cli/ctl-command.js +1 -2
  26. package/dist/cli/daemon-commands.js +82 -13
  27. package/dist/cli/debug-mode.js +154 -0
  28. package/dist/cli/diag.js +2 -2
  29. package/dist/cli/handoff-command.js +18 -5
  30. package/dist/cli/index.js +81 -64
  31. package/dist/cli/init.js +2 -2
  32. package/dist/cli/managed-command-guard.js +5 -0
  33. package/dist/cli/memory-command.js +93 -12
  34. package/dist/cli/model.js +14 -138
  35. package/dist/cli/queue-command.js +51 -0
  36. package/dist/cli/response.js +5 -54
  37. package/dist/cli/session-command.js +194 -0
  38. package/dist/cli/skill-command.js +85 -3
  39. package/dist/cli/stats.js +10 -4
  40. package/dist/cli/task-command.js +1 -0
  41. package/dist/cli/task-context.js +16 -0
  42. package/dist/cli/thread-command.js +620 -0
  43. package/dist/cli/trigger-command.js +76 -50
  44. package/dist/config/builtin-roles.js +9 -7
  45. package/dist/config/config-field-policy.js +2 -3
  46. package/dist/config/config-manager.js +7 -2
  47. package/dist/config/resolved-config-op.js +3 -0
  48. package/dist/config-store.js +15 -0
  49. package/dist/core/agent-ipc-registration.js +65 -22
  50. package/dist/core/auth/agent-delegation.js +42 -2
  51. package/dist/core/auth/debug-context.js +139 -0
  52. package/dist/core/auth/delegation-authorization.js +1 -0
  53. package/dist/core/auth/operation-authorizer.js +24 -1
  54. package/dist/core/auth/operation-catalog.js +44 -3
  55. package/dist/core/background-session/session-cleanup.js +212 -0
  56. package/dist/core/background-session/types.js +10 -0
  57. package/dist/core/bootstrap-messages.js +1 -1
  58. package/dist/core/bootstrap-service.js +6 -3
  59. package/dist/core/command/command-handler.js +224 -45
  60. package/dist/core/command/diag-service.js +5 -1
  61. package/dist/core/command/memory-slash.js +6 -2
  62. package/dist/core/command/menu-catalog.js +48 -1
  63. package/dist/core/command/menu-handler.js +92 -16
  64. package/dist/core/command/menu-protocol.js +24 -1
  65. package/dist/core/command/skill-slash.js +322 -0
  66. package/dist/core/command/slash-gate.js +6 -4
  67. package/dist/core/command/slash-handler.js +193 -13
  68. package/dist/core/command/topic-slash.js +528 -0
  69. package/dist/core/evolagent-registry.js +20 -5
  70. package/dist/core/handoff/ipc-registration.js +38 -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 +210 -9
  74. package/dist/core/memory/consolidation-context.js +49 -16
  75. package/dist/core/memory/consolidation-ops-records.js +35 -0
  76. package/dist/core/memory/consolidation-queue-format.js +21 -0
  77. package/dist/core/memory/consolidation.js +174 -180
  78. package/dist/core/message/aun-ipc-sender.js +207 -8
  79. package/dist/core/message/im-renderer.js +149 -6
  80. package/dist/core/message/message-bridge.js +13 -3
  81. package/dist/core/message/message-log.js +1 -0
  82. package/dist/core/message/message-queue.js +131 -3
  83. package/dist/core/message/peer-mode.js +6 -1
  84. package/dist/core/message/response-engine.js +379 -69
  85. package/dist/core/message/subtask-tool-projection.js +140 -0
  86. package/dist/core/model/catalog-service.js +17 -12
  87. package/dist/core/permission/ec-command-parser.js +126 -7
  88. package/dist/core/permission/sandbox-runtime.js +14 -1
  89. package/dist/core/session/session-idle-monitor.js +249 -22
  90. package/dist/core/session/session-manager.js +54 -9
  91. package/dist/core/skill/command-parser.js +13 -0
  92. package/dist/core/skill/index-generator.js +4 -1
  93. package/dist/core/skill/startup.js +41 -0
  94. package/dist/core/skill/switch-resolver.js +55 -29
  95. package/dist/core/task/task-recovery-service.js +2 -0
  96. package/dist/core/task/task-resume-operation.js +11 -0
  97. package/dist/core/thread/command-parser.js +125 -0
  98. package/dist/core/thread/index.js +1 -0
  99. package/dist/core/thread/ipc-operation.js +95 -0
  100. package/dist/core/thread/thread-context-storage.js +131 -0
  101. package/dist/core/thread/thread-operation.js +258 -0
  102. package/dist/core/thread/thread-route.js +33 -0
  103. package/dist/core/thread/thread-service.js +777 -0
  104. package/dist/core/thread/types.js +1 -0
  105. package/dist/eck/message-renderer.js +35 -0
  106. package/dist/index.js +10 -7
  107. package/dist/ipc.js +234 -17
  108. package/dist/paths.js +23 -0
  109. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  110. package/dist/trigger/authorization.js +59 -29
  111. package/dist/trigger/feedback.js +7 -1
  112. package/dist/trigger/ipc-service.js +49 -35
  113. package/dist/trigger/parser.js +63 -1
  114. package/dist/trigger/script-sandbox.js +58 -77
  115. package/dist/trigger/validation.js +14 -0
  116. package/dist/utils/error-utils.js +22 -1
  117. package/kits/docs/AUTHORING.md +1 -1
  118. package/kits/docs/channels/aun.md +8 -0
  119. package/kits/docs/context-assembly.md +1 -0
  120. package/kits/docs/evolcore/INDEX.md +3 -2
  121. package/kits/docs/evolcore/agent-create.md +39 -28
  122. package/kits/docs/evolcore/agent.md +19 -4
  123. package/kits/docs/evolcore/ec-capabilities.md +4 -0
  124. package/kits/docs/evolcore/group-collaboration.md +3 -5
  125. package/kits/docs/evolcore/group.md +3 -4
  126. package/kits/docs/evolcore/memory-consolidation.md +1 -1
  127. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  128. package/kits/docs/evolcore/menu-protocol.md +7 -0
  129. package/kits/docs/evolcore/msg.md +5 -8
  130. package/kits/docs/evolcore/task.md +2 -0
  131. package/kits/docs/evolcore/thread-chain.md +267 -0
  132. package/kits/docs/evolcore/trigger.md +2 -2
  133. package/kits/docs/prompt-loading-architecture.md +1 -0
  134. package/kits/eck_manifest.json +61 -26
  135. package/kits/eck_message_manifest.json +169 -10
  136. package/kits/rules/01-overview.md +0 -9
  137. package/kits/rules/05-venue.md +1 -1
  138. package/kits/rules/06-channel.md +7 -1
  139. package/kits/rules/07-memory-and-thread.md +202 -0
  140. package/kits/schemas/relation-config.schema.9.json +18 -3
  141. package/kits/schemas/role-config.schema.1.json +6 -6
  142. package/kits/schemas/single-session.schema.1.json +6 -6
  143. package/kits/schemas/single-session.schema.2.json +6 -6
  144. package/kits/schemas/single-session.schema.3.json +6 -6
  145. package/kits/skills/.switches.json +12 -1
  146. package/kits/skills/ec-group/SKILL.md +234 -0
  147. package/kits/skills/ec-skill/SKILL.md +10 -1
  148. package/kits/skills/ec-skill/scripts/cli.mjs +15 -1
  149. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  150. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  151. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  152. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  153. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  154. package/kits/skills/thread-ops/SKILL.md +492 -0
  155. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  156. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  157. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  158. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  159. package/kits/templates/bootstrap-welcome.md +7 -7
  160. package/kits/templates/memory-consolidation/forget.md +5 -0
  161. package/kits/templates/memory-consolidation/generalize.md +5 -0
  162. package/kits/templates/memory-consolidation/profile.md +5 -0
  163. package/kits/templates/memory-consolidation/promote.md +5 -0
  164. package/kits/templates/message-fragments/item.md +2 -1
  165. package/kits/templates/message-fragments/thread-context.md +6 -0
  166. package/kits/templates/message-fragments/trigger-session.md +2 -0
  167. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  168. package/kits/templates/roles/member.json +2 -0
  169. package/kits/templates/roles/visitor.json +2 -0
  170. package/kits/templates/system-fragments/bootstrap.md +66 -72
  171. package/kits/templates/system-fragments/channel.md +3 -6
  172. package/kits/templates/system-fragments/commands.md +2 -1
  173. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  174. package/kits/templates/system-fragments/reply.md +3 -2
  175. package/kits/templates/system-fragments/session.md +5 -14
  176. package/package.json +3 -1
  177. package/kits/docs/evolcore/group-fs.md +0 -17
  178. package/kits/docs/venues/aun-group.md +0 -45
  179. 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,10 +2015,25 @@ 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
  };
1895
2022
  }
2023
+ resolveTaskApprovalInteractionPolicy(message) {
2024
+ if (message.taskRecovery) {
2025
+ const policy = message.taskRecovery.approvalInteractionPolicy;
2026
+ if (policy === 'interactive' || policy === 'deny')
2027
+ return policy;
2028
+ return message.taskRecovery.automatic ? 'deny' : 'interactive';
2029
+ }
2030
+ if (message.source === 'handoff' && message.handoffDelivery?.direction === 'origin') {
2031
+ return message.handoffDelivery.originAuthorization?.approvalInteractionPolicy === 'interactive'
2032
+ ? 'interactive'
2033
+ : 'deny';
2034
+ }
2035
+ return message.source === 'trigger' ? 'deny' : 'interactive';
2036
+ }
1896
2037
  resolveHandoffOriginExecutionIdentity(message, selfAid) {
1897
2038
  const authorization = message.handoffDelivery?.originAuthorization;
1898
2039
  if (message.source !== 'handoff'
@@ -1944,8 +2085,11 @@ export class ResponseEngine {
1944
2085
  /** Publish a terminal outcome before an early return from Trigger execution. */
1945
2086
  publishTriggerExecutionFailure(message, error, opts = {}) {
1946
2087
  const terminal = this.claimTriggerTerminal(message);
1947
- if (!terminal)
2088
+ if (!terminal) {
2089
+ // bg-session 路径:trigger terminal 不匹配时尝试 bg-session
2090
+ this.publishBgSessionError(message, error, { messageId: opts.messageId, causation: opts.causation });
1948
2091
  return;
2092
+ }
1949
2093
  const { trigger, runId } = terminal;
1950
2094
  const attemptId = trigger.attemptId;
1951
2095
  this.eventBus.publish({
@@ -1966,8 +2110,11 @@ export class ResponseEngine {
1966
2110
  }
1967
2111
  publishTriggerExecutionSkipped(message, reason, causation, interruption) {
1968
2112
  const terminal = this.claimTriggerTerminal(message);
1969
- if (!terminal)
2113
+ if (!terminal) {
2114
+ // bg-session 路径
2115
+ this.publishBgSessionInterrupted(message, reason, causation);
1970
2116
  return;
2117
+ }
1971
2118
  const { trigger, runId } = terminal;
1972
2119
  this.eventBus.publish({
1973
2120
  type: 'trigger:skipped',
@@ -2003,6 +2150,14 @@ export class ResponseEngine {
2003
2150
  logger.warn(`[ResponseEngine] Failed to close interrupted Trigger run=${message.triggerMeta?.runId ?? '<unknown>'}: ${error instanceof Error ? error.message : String(error)}`);
2004
2151
  });
2005
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
+ }
2006
2161
  this.publishTriggerExecutionSkipped(message, reason, causation, interruption);
2007
2162
  }
2008
2163
  publishTriggerExecutionCompleted(message, messageId, durationMs, causation) {
@@ -2701,6 +2856,7 @@ export class ResponseEngine {
2701
2856
  const triggerExecutionIdentity = this.resolveTriggerExecutionIdentity(message, selfAid);
2702
2857
  const authorizationIdentity = triggerExecutionIdentity
2703
2858
  ?? this.resolveHandoffOriginExecutionIdentity(message, selfAid);
2859
+ const approvalInteractionPolicy = this.resolveTaskApprovalInteractionPolicy(message);
2704
2860
  const configChannelType = authorizationIdentity?.channelType ?? currentChannelType;
2705
2861
  const configChatType = authorizationIdentity
2706
2862
  ? (message.triggerMeta?.originChatType ?? message.handoffDelivery?.originAuthorization?.chatType ?? 'private')
@@ -2873,8 +3029,12 @@ export class ResponseEngine {
2873
3029
  ? { kind: 'restartResume', groupId: message.groupId, threadId: message.threadId }
2874
3030
  : undefined,
2875
3031
  previousTurnState: this.turnCoordinator.current(session),
2876
- isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, selfAid),
2877
- 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
+ }),
2878
3038
  logger,
2879
3039
  } : null;
2880
3040
  if (resolvedMode?.mode.beforeProcess && modeProcessCtx) {
@@ -2937,6 +3097,7 @@ export class ResponseEngine {
2937
3097
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
2938
3098
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
2939
3099
  this.activeTaskRuntimeContexts.delete(session.id);
3100
+ this.activeRunnerModels.delete(session.id); // F1修复
2940
3101
  }
2941
3102
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
2942
3103
  const daemonTrigger = this.isTrustedDaemonTrigger(message);
@@ -3122,6 +3283,7 @@ export class ResponseEngine {
3122
3283
  : undefined;
3123
3284
  turnLease = await this.turnCoordinator.begin(session, taskId, inputId, configActorId ? {
3124
3285
  peerId: configActorId,
3286
+ approvalInteractionPolicy,
3125
3287
  encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
3126
3288
  baseagent: session.baseagent,
3127
3289
  agentSessionId: session.agentSessionId,
@@ -3285,9 +3447,9 @@ export class ResponseEngine {
3285
3447
  await adapter.send(withEnvelopeReplyContext(envelope, capturedReplyContext), { kind: 'result.text', text, isFinal: false });
3286
3448
  };
3287
3449
  const authChatType = configChatType;
3288
- const authConversationId = authChatType === 'group'
3450
+ const authConversationId = triggerExecutionIdentity?.conversationId ?? (authChatType === 'group'
3289
3451
  ? (session.metadata?.groupId || session.channelId || capturedChannelId)
3290
- : configActorId;
3452
+ : configActorId);
3291
3453
  const authPeerKey = configChannelType && authConversationId
3292
3454
  ? formatPeerKey(configChannelType, authConversationId)
3293
3455
  : undefined;
@@ -3439,7 +3601,7 @@ export class ResponseEngine {
3439
3601
  peerKey: authPeerKey,
3440
3602
  causation: taskCausation,
3441
3603
  approvalRouting,
3442
- approvalInteractionPolicy: authorizationIdentity ? 'deny' : 'interactive',
3604
+ approvalInteractionPolicy,
3443
3605
  permissionMode: effectivePermissionMode,
3444
3606
  recordExecutionAnomaly,
3445
3607
  pauseController: taskPauseController,
@@ -3536,16 +3698,31 @@ export class ResponseEngine {
3536
3698
  const pluginHook = pureSessionPolicyHook;
3537
3699
  if (!pluginHook)
3538
3700
  return undefined;
3539
- return (toolName, toolInput) => {
3701
+ return (toolName, toolInput, meta) => {
3540
3702
  const stBefore = modeState.get('proactive');
3541
3703
  const wasFirstPending = stBefore && !stBefore.firstToolDone;
3542
- const result = pluginHook(toolName, toolInput);
3704
+ const result = pluginHook(toolName, toolInput, meta);
3543
3705
  // 整理会话的拦截原因已经写清楚怎么改,不套 proactive 违规提醒
3544
3706
  if (result?.block && result.policyCode?.startsWith('memory_session_'))
3545
3707
  return result;
3546
3708
  if (result?.block) {
3547
3709
  // 拦截事件(首工具非表态 / 工具进展汇报未完成)
3548
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
+ }
3549
3726
  const errorMsg = `⚠️ proactive 模式违规:${result.reason ?? '请先发送必要说明'}。请重新执行正确的命令。`;
3550
3727
  agent.injectUserMessage?.(session.id, errorMsg);
3551
3728
  return result;
@@ -3589,6 +3766,8 @@ export class ResponseEngine {
3589
3766
  let previousFallbackState;
3590
3767
  let agentModel;
3591
3768
  let v2HandoffIds = [];
3769
+ /** T3: 本次注入的 Thread Context 条目(渲染后据此清空 inbox)。 */
3770
+ let threadContextItems = [];
3592
3771
  let v2HandoffResultRefs = [];
3593
3772
  let v2HandoffDirection;
3594
3773
  let handoffPromptRendered = false;
@@ -4121,6 +4300,16 @@ export class ResponseEngine {
4121
4300
  const rawPeerItems = message.source === 'owner-inject' ? [] : message.items && message.items.length > 0
4122
4301
  ? message.items
4123
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
+ } : {}),
4124
4313
  peerId: message.peerId, peerName: peerName || undefined,
4125
4314
  peerType: message.peerType,
4126
4315
  peerRole: message.batchRole || session.identity?.role || 'none',
@@ -4131,6 +4320,34 @@ export class ResponseEngine {
4131
4320
  mentions: message.mentions,
4132
4321
  mentionAids: message.mentionAids,
4133
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
+ }
4134
4351
  const peerItems = (() => {
4135
4352
  if (message.handoffDelivery && this.handoffRuntime) {
4136
4353
  const items = this.handoffRuntime.buildPromptItems(message, peerRole);
@@ -4140,10 +4357,10 @@ export class ResponseEngine {
4140
4357
  : (item.handoff?.handoffId ? [item.handoff.handoffId] : [])))));
4141
4358
  v2HandoffResultRefs = items.flatMap(item => item.handoff?.resultRefs ?? []);
4142
4359
  v2HandoffDirection = message.handoffDelivery.direction;
4143
- return items;
4360
+ return [...threadContextItems, ...items];
4144
4361
  }
4145
4362
  }
4146
- return rawPeerItems;
4363
+ return [...threadContextItems, ...rawPeerItems];
4147
4364
  })();
4148
4365
  // 将匹配的 pending hint 暂时 claim,排在真实消息之前;渲染或启动
4149
4366
  // 失败时释放 claim,只有 runner 接受 prompt 后才持久化 consumed。
@@ -4247,15 +4464,30 @@ export class ResponseEngine {
4247
4464
  }
4248
4465
  }
4249
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
+ }
4250
4477
  // Pass the stable session directory explicitly so sandboxed helper
4251
4478
  // commands never fall back to a process, agent, or project directory
4252
4479
  // for transient state.
4253
4480
  const taskRuntimeContext = {
4254
4481
  taskId,
4255
4482
  sessionId: session.id,
4483
+ projectPath: path.resolve(session.projectPath),
4256
4484
  messageId: message.messageId,
4257
4485
  baseagent: normalizedBaseagent.canonical,
4258
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,
4259
4491
  channelId: message.channelId,
4260
4492
  chatType: configChatType,
4261
4493
  selfAid,
@@ -4287,6 +4519,7 @@ export class ResponseEngine {
4287
4519
  causation: taskCausation,
4288
4520
  };
4289
4521
  this.activeTaskRuntimeContexts.set(session.id, taskRuntimeContext);
4522
+ this.debugWriteSessionSnapshot(session.id, taskRuntimeContext);
4290
4523
  runtimeEnv = buildTaskRuntimeEnv(taskRuntimeContext);
4291
4524
  if (this.agentDelegationRegistry && configActorId && selfAid && peerKey) {
4292
4525
  const delegationToken = this.agentDelegationRegistry.issue({
@@ -4300,7 +4533,9 @@ export class ResponseEngine {
4300
4533
  selfAid,
4301
4534
  peerKey,
4302
4535
  issuedRole: peerRole,
4536
+ executionSource: fullAccessAuthorization?.source ?? (message.source === 'trigger' ? 'trigger' : 'user'),
4303
4537
  permissionMode: effectivePermissionMode,
4538
+ approvalInteractionPolicy,
4304
4539
  ...(sessionRuntimeDir ? { sessionRuntimeDir } : {}),
4305
4540
  ...(fullAccessAuthorization ? { executionIdentity: fullAccessAuthorization } : {}),
4306
4541
  });
@@ -4390,6 +4625,10 @@ export class ResponseEngine {
4390
4625
  mimeType: image.mimeType,
4391
4626
  }));
4392
4627
  debugRunnerModel = modelOverride?.model || agentModel;
4628
+ if (debugRunnerModel)
4629
+ this.activeRunnerModels.set(session.id, debugRunnerModel);
4630
+ else
4631
+ this.activeRunnerModels.delete(session.id);
4393
4632
  const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, runAgentSessionId, runnerImages, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
4394
4633
  // The turn may be superseded while the runner is still creating its
4395
4634
  // backend query. Replay the interrupt after runQuery resolves, when
@@ -4685,6 +4924,7 @@ export class ResponseEngine {
4685
4924
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
4686
4925
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
4687
4926
  this.activeTaskRuntimeContexts.delete(session.id);
4927
+ this.activeRunnerModels.delete(session.id); // F1修复
4688
4928
  }
4689
4929
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
4690
4930
  logger.info(`[ResponseEngine] Stale turn stopped before publish: session=${session.id} task=${taskId}`);
@@ -4700,6 +4940,7 @@ export class ResponseEngine {
4700
4940
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
4701
4941
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
4702
4942
  this.activeTaskRuntimeContexts.delete(session.id);
4943
+ this.activeRunnerModels.delete(session.id); // F1修复
4703
4944
  }
4704
4945
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
4705
4946
  logger.info(`[ResponseEngine] Stale turn stopped before protocol rejection: session=${session.id} task=${taskId}`);
@@ -4851,9 +5092,10 @@ export class ResponseEngine {
4851
5092
  });
4852
5093
  }
4853
5094
  // 最终回复文本:suppressed 模式或无 text 事件时需要兜底添加
5095
+ // 错误诊断串已通过 system.error 单独发出时置 true,下游跳过 result.error 避免重复
5096
+ let sentErrorAsSystemError = false;
4854
5097
  const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
4855
5098
  const visibleFinalReplyText = stripContextLimitErrorLines(finalReplyText);
4856
- const isDiagnosticFallback = streamResult.isError && !streamResult.hasReceivedText;
4857
5099
  const isTriggerNoop = message.source === 'trigger'
4858
5100
  && typeof message.triggerMeta?.noopSentinel === 'string'
4859
5101
  && message.triggerMeta.noopSentinel.length > 0
@@ -4863,16 +5105,38 @@ export class ResponseEngine {
4863
5105
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
4864
5106
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
4865
5107
  this.activeTaskRuntimeContexts.delete(session.id);
5108
+ this.activeRunnerModels.delete(session.id); // F1修复
4866
5109
  }
4867
5110
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
4868
5111
  logger.info(`[ResponseEngine] Stale turn stopped before final flush: session=${session.id} task=${taskId}`);
4869
5112
  await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_turn', taskCausation);
4870
5113
  return;
4871
5114
  }
4872
- if (visibleFinalReplyText && !isDiagnosticFallback && !isTriggerNoop) {
5115
+ if ((visibleFinalReplyText || streamResult.syntheticDiagnosticText) && !isTriggerNoop) {
4873
5116
  // `hasReceivedText` 是任务级计数。工具调用前的文字已经显示,并不表示最后一轮
4874
5117
  // (通常来自 complete.result)的文字也已显示;用本轮投影标记避免终态丢失。
4875
- 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)) {
4876
5140
  await renderer.addText(visibleFinalReplyText, undefined, undefined, streamResult.assistantUuid);
4877
5141
  }
4878
5142
  }
@@ -4882,6 +5146,7 @@ export class ResponseEngine {
4882
5146
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
4883
5147
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
4884
5148
  this.activeTaskRuntimeContexts.delete(session.id);
5149
+ this.activeRunnerModels.delete(session.id); // F1修复
4885
5150
  }
4886
5151
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
4887
5152
  logger.info(`[ResponseEngine] session ${session.id} processing cleared task=${taskId}`);
@@ -4972,12 +5237,17 @@ export class ResponseEngine {
4972
5237
  // DaemonChannel settles a trigger when it receives a terminal
4973
5238
  // payload. Send the metadata-bearing status first so failed runs do
4974
5239
  // not lose their tool counts and recovery diagnostics.
5240
+ // system.error 已在 finalReplyText 分支把错误文本发给了前端,不再重复发 result.error
4975
5241
  if (this.isTrustedDaemonTrigger(message)) {
4976
5242
  await adapter.send(envelope, { kind: 'status.error', metadata: terminalMetadata }).catch(() => { });
4977
- 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
+ }
4978
5246
  }
4979
5247
  else {
4980
- 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
+ }
4981
5251
  await adapter.send(envelope, { kind: 'status.error', metadata: terminalMetadata }).catch(() => { });
4982
5252
  }
4983
5253
  this.touchAgentActivity(channelKey);
@@ -5230,6 +5500,7 @@ export class ResponseEngine {
5230
5500
  }
5231
5501
  if (!interruptReason) {
5232
5502
  this.publishTriggerExecutionCompleted(message, messageId, durationMs, taskCausation);
5503
+ this.publishBgSessionCompleted(message, messageId, durationMs, taskCausation);
5233
5504
  }
5234
5505
  if (!this.turnCoordinator.canPublish(turnLease))
5235
5506
  return;
@@ -5319,6 +5590,7 @@ export class ResponseEngine {
5319
5590
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
5320
5591
  if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
5321
5592
  this.activeTaskRuntimeContexts.delete(session.id);
5593
+ this.activeRunnerModels.delete(session.id); // F1修复
5322
5594
  }
5323
5595
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
5324
5596
  logger.info(`[ResponseEngine] session ${session.id} processing cleared (on error) task=${taskId}`);
@@ -5870,6 +6142,9 @@ export class ResponseEngine {
5870
6142
  });
5871
6143
  // 追踪最后一轮 assistant 回复文本(tool_use 之后的纯文本)
5872
6144
  let lastReplyText = '';
6145
+ // 上游合成错误帧的原文(runner 标记 synthetic 的 text)。与正文通道分开存放:
6146
+ // 它只能作为终态错误文案的来源,不得进入 lastReplyText / 投影 / hasReceivedText。
6147
+ let syntheticDiagnosticText = '';
5873
6148
  // callId → description 映射,用于 tool_result 回显描述
5874
6149
  const toolDescByCallId = new Map();
5875
6150
  const toolInputByCallId = new Map();
@@ -5932,14 +6207,23 @@ export class ResponseEngine {
5932
6207
  if (event.type === 'input_accepted') {
5933
6208
  inputAccepted = !turnLease || event.inputId === turnLease.inputId;
5934
6209
  logger.debug(`[ResponseEngine] Input accepted: session=${session.id} input=${event.inputId}`);
6210
+ // 通知队列:本批次输入已真正提交给模型,此后不可回队重放。
6211
+ this.messageQueue?.markInputSubmitted?.(session.id, turnLease?.taskId);
5935
6212
  continue;
5936
6213
  }
5937
6214
  if (event.type === 'text') {
5938
- if (event.text.trim())
6215
+ const hasText = !!event.text.trim();
6216
+ // 模型已产出文本 ⇒ 输入必然已被消费。覆盖不发 input_accepted 的 runner
6217
+ // (codex/ecagent/gemini),避免它们在 pre-submit 回队时重复用户轮次。
6218
+ if (hasText)
6219
+ this.messageQueue?.markInputSubmitted?.(session.id, turnLease?.taskId);
6220
+ if (hasText)
5939
6221
  textCount++;
5940
6222
  assistantUuid = event.assistantUuid ?? assistantUuid;
5941
6223
  }
5942
6224
  else if (event.type === 'tool_use') {
6225
+ // 产生工具调用同样证明输入已被消费,且此时重放会重复发起副作用。
6226
+ this.messageQueue?.markInputSubmitted?.(session.id, turnLease?.taskId);
5943
6227
  toolUseCount++;
5944
6228
  assistantUuid = event.assistantUuid ?? assistantUuid;
5945
6229
  if (event.callId) {
@@ -6085,6 +6369,7 @@ export class ResponseEngine {
6085
6369
  toolUseId: event.toolUseId,
6086
6370
  description: event.description,
6087
6371
  subagentType: event.subagentType,
6372
+ taskType: event.taskType,
6088
6373
  skipTranscript: event.skipTranscript,
6089
6374
  });
6090
6375
  this.eventBus.publish({
@@ -6103,15 +6388,21 @@ export class ResponseEngine {
6103
6388
  causation,
6104
6389
  });
6105
6390
  if (!event.skipTranscript) {
6106
- renderer.addLifecycle('started', {
6107
- scope: 'runner_task',
6108
- taskId: event.taskId,
6109
- taskKind: event.taskKind,
6110
- toolUseId: event.toolUseId,
6111
- description: event.description,
6112
- subagentType: event.subagentType,
6113
- taskType: event.taskType,
6114
- }, `${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
+ }
6115
6406
  }
6116
6407
  continue;
6117
6408
  }
@@ -6127,6 +6418,7 @@ export class ResponseEngine {
6127
6418
  }
6128
6419
  if (event.type === 'task_notification') {
6129
6420
  const tasks = this.activeRunnerTasks.get(session.id);
6421
+ const started = tasks?.get(event.taskId);
6130
6422
  tasks?.delete(event.taskId);
6131
6423
  if (tasks?.size === 0)
6132
6424
  this.activeRunnerTasks.delete(session.id);
@@ -6147,19 +6439,26 @@ export class ResponseEngine {
6147
6439
  causation,
6148
6440
  });
6149
6441
  if (!event.skipTranscript) {
6150
- const label = event.description || event.taskId;
6151
- renderer.addLifecycle('completed', {
6152
- scope: 'runner_task',
6153
- taskId: event.taskId,
6154
- taskKind: event.taskKind,
6155
- toolUseId: event.toolUseId,
6156
- description: event.description,
6157
- subagentType: event.subagentType,
6158
- status: event.status,
6159
- outputFile: event.outputFile,
6160
- summary: event.summary,
6161
- usage: event.usage,
6162
- }, `${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
+ }
6163
6462
  }
6164
6463
  continue;
6165
6464
  }
@@ -6196,21 +6495,30 @@ export class ResponseEngine {
6196
6495
  if (!isCurrentlyBackground) {
6197
6496
  // 流式文本
6198
6497
  if (event.type === 'text') {
6199
- // 网关偶尔会把上下文超限错误伪装成普通 text event。不要向事件总线或
6200
- // 渲染器投影错误行;lastReplyText 仍保留原文本,以便流结束后触发恢复。
6201
- const visibleText = stripContextLimitErrorLines(event.text);
6202
- if (visibleText) {
6203
- hasReceivedText = true;
6204
- if (visibleText.trim())
6205
- madeProgress = true;
6206
- }
6207
- lastReplyText += event.text;
6208
- if (visibleText) {
6209
- 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;
6210
6504
  }
6211
- if (visibleText && !shouldSuppress()) {
6212
- await renderer.addText(visibleText, event.outputTokens, event.turn, event.assistantUuid, event.phase);
6213
- 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
+ }
6214
6522
  }
6215
6523
  }
6216
6524
  // compact 完成
@@ -6302,6 +6610,7 @@ export class ResponseEngine {
6302
6610
  state: modeHooks.state,
6303
6611
  sessionState: modeHooks.sessionState,
6304
6612
  turn: turnLease,
6613
+ modelTurn: typeof event.turn === 'number' ? event.turn : undefined,
6305
6614
  toolName: event.name,
6306
6615
  toolInput: event.input || {},
6307
6616
  injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
@@ -6316,7 +6625,7 @@ export class ResponseEngine {
6316
6625
  });
6317
6626
  },
6318
6627
  getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
6319
- 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),
6320
6629
  logger,
6321
6630
  });
6322
6631
  }
@@ -6436,7 +6745,7 @@ export class ResponseEngine {
6436
6745
  });
6437
6746
  },
6438
6747
  getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
6439
- 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),
6440
6749
  logger,
6441
6750
  });
6442
6751
  }
@@ -6474,6 +6783,7 @@ export class ResponseEngine {
6474
6783
  errors: [event.error],
6475
6784
  lastReplyText,
6476
6785
  fullText: '',
6786
+ syntheticDiagnosticText,
6477
6787
  };
6478
6788
  }
6479
6789
  // 完成事件
@@ -6488,7 +6798,7 @@ export class ResponseEngine {
6488
6798
  logger.info(`[ResponseEngine] Auto-filled session name: ${event.sessionTitle}`);
6489
6799
  }
6490
6800
  // 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
6491
- 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 };
6492
6802
  if (!event.isError)
6493
6803
  madeProgress = true;
6494
6804
  // thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
@@ -6539,7 +6849,7 @@ export class ResponseEngine {
6539
6849
  logger.info(`[ResponseEngine] Auto-filled session name: ${event.sessionTitle}`);
6540
6850
  }
6541
6851
  // 记录完成状态
6542
- 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 };
6543
6853
  if (event.subtype === 'success')
6544
6854
  madeProgress = true;
6545
6855
  }