evolcore 0.0.27 → 0.0.29-test.0

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 (195) hide show
  1. package/CHANGELOG.md +38 -15
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +29 -14
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +13 -2
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex/diagnostics.js +4 -3
  9. package/dist/agents/codex-runner.js +444 -106
  10. package/dist/agents/gemini-runner.js +35 -17
  11. package/dist/aun/group/history.js +130 -90
  12. package/dist/aun/group/operations.js +1 -3
  13. package/dist/aun/menu/client.js +1 -0
  14. package/dist/aun/menu/transport.js +5 -3
  15. package/dist/aun/msg/managed-operation.js +70 -0
  16. package/dist/aun/msg/p2p.js +1 -3
  17. package/dist/aun/msg/upload.js +2 -2
  18. package/dist/aun/outbox.js +1 -37
  19. package/dist/channels/aun.js +262 -86
  20. package/dist/channels/daemon.js +18 -1
  21. package/dist/channels/feishu.js +1 -2
  22. package/dist/cli/agent-command.js +23 -3
  23. package/dist/cli/agent-create.js +68 -2
  24. package/dist/cli/agent.js +12 -2
  25. package/dist/cli/aun-commands.js +87 -13
  26. package/dist/cli/cli-argv.js +26 -0
  27. package/dist/cli/daemon-commands.js +116 -149
  28. package/dist/cli/data-command.js +4 -4
  29. package/dist/cli/diag.js +1 -1
  30. package/dist/cli/handoff-command.js +2 -0
  31. package/dist/cli/index.js +18 -1
  32. package/dist/cli/managed-command-guard.js +10 -0
  33. package/dist/cli/memory-command.js +441 -0
  34. package/dist/cli/menu-client.js +4 -0
  35. package/dist/cli/restart-monitor.js +127 -23
  36. package/dist/cli/skill-command.js +41 -0
  37. package/dist/cli/task-context.js +2 -23
  38. package/dist/cli/trigger-command.js +9 -1
  39. package/dist/cli/watch-msg-diag.js +209 -0
  40. package/dist/config/config-field-policy.js +52 -3
  41. package/dist/config/config-manager.js +17 -4
  42. package/dist/config/contact-book-store.js +2 -2
  43. package/dist/config/mention-mode.js +7 -2
  44. package/dist/config/merge.js +28 -9
  45. package/dist/config/resolved-config-op.js +1 -1
  46. package/dist/config/runtime-env.js +137 -0
  47. package/dist/config/schema-registry.js +11 -1
  48. package/dist/config-store.js +10 -10
  49. package/dist/core/audit/event-key.js +3 -1
  50. package/dist/core/audit/log-integrity.js +4 -0
  51. package/dist/core/auth/agent-delegation.js +2 -1
  52. package/dist/core/auth/auth-gateway.js +13 -6
  53. package/dist/core/auth/authorization-audit.js +54 -19
  54. package/dist/core/auth/operation-authorizer.js +17 -0
  55. package/dist/core/auth/operation-catalog.js +9 -0
  56. package/dist/core/causation/context.js +5 -1
  57. package/dist/core/command/agent-control.js +1 -2
  58. package/dist/core/command/command-handler.js +80 -24
  59. package/dist/core/command/diag-service.js +292 -27
  60. package/dist/core/command/memory-slash.js +270 -0
  61. package/dist/core/command/menu-catalog.js +1 -0
  62. package/dist/core/command/menu-handler.js +331 -79
  63. package/dist/core/command/menu-protocol.js +45 -4
  64. package/dist/core/command/slash-gate.js +10 -10
  65. package/dist/core/command/slash-handler.js +230 -79
  66. package/dist/core/data-migration.js +282 -505
  67. package/dist/core/event-catalog.js +26 -0
  68. package/dist/core/evolagent-registry.js +6 -0
  69. package/dist/core/evolagent.js +0 -10
  70. package/dist/core/handoff/store.js +1 -20
  71. package/dist/core/inference/text-inference.js +57 -1
  72. package/dist/core/memory/consolidation-context.js +505 -0
  73. package/dist/core/memory/consolidation-ops-records.js +124 -0
  74. package/dist/core/memory/consolidation-trigger.js +394 -0
  75. package/dist/core/memory/index-manager.js +308 -0
  76. package/dist/core/memory/launch-consolidation.js +395 -0
  77. package/dist/core/memory/memory-admin.js +234 -0
  78. package/dist/core/memory/memory-document.js +100 -0
  79. package/dist/core/memory/memory-session-policy.js +41 -0
  80. package/dist/core/memory/memory-signature.js +257 -0
  81. package/dist/core/memory/memory-writer.js +432 -0
  82. package/dist/core/memory/staleness.js +117 -0
  83. package/dist/core/message/auto-mention-gate.js +454 -0
  84. package/dist/core/message/im-renderer.js +90 -20
  85. package/dist/core/message/message-bridge.js +333 -23
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/message-queue.js +114 -27
  88. package/dist/core/message/pending-hints.js +329 -161
  89. package/dist/core/message/response-engine.js +596 -90
  90. package/dist/core/permission/ec-command-parser.js +1044 -87
  91. package/dist/core/permission/protected-paths.js +376 -126
  92. package/dist/core/permission/sandbox-runtime.js +40 -12
  93. package/dist/core/permission/tool-error-code.js +96 -13
  94. package/dist/core/permission/tool-policy.js +277 -24
  95. package/dist/core/role/runtime-policy.js +3 -7
  96. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  97. package/dist/core/session/session-fs-store.js +0 -76
  98. package/dist/core/session/session-idle-monitor.js +355 -0
  99. package/dist/core/session/session-manager.js +482 -17
  100. package/dist/core/session/session-mapper.js +33 -16
  101. package/dist/core/session/session-renew.js +217 -155
  102. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  103. package/dist/core/skill/global-watcher.js +282 -0
  104. package/dist/core/skill/index-generator.js +84 -0
  105. package/dist/core/skill/startup.js +73 -0
  106. package/dist/core/skill/switch-resolver.js +115 -0
  107. package/dist/core/task/task-activity-store.js +219 -0
  108. package/dist/eck/kit-renderer.js +30 -3
  109. package/dist/eck/manifest-engine.js +26 -4
  110. package/dist/index.js +722 -171
  111. package/dist/ipc.js +564 -2
  112. package/dist/paths.js +30 -9
  113. package/dist/product.js +1 -0
  114. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  115. package/dist/response-system/modes/single-session/index.js +3 -0
  116. package/dist/trigger/feedback.js +1 -0
  117. package/dist/trigger/manager.js +0 -10
  118. package/dist/trigger/scheduler.js +11 -0
  119. package/dist/trigger/validation.js +22 -0
  120. package/dist/utils/atomic-write.js +98 -0
  121. package/dist/utils/avatar-upload.js +59 -18
  122. package/dist/utils/cross-platform.js +87 -16
  123. package/dist/utils/error-dict.json +6 -3
  124. package/dist/utils/error-utils.js +27 -0
  125. package/dist/utils/logger.js +32 -10
  126. package/dist/utils/media-cache.js +84 -0
  127. package/dist/utils/msg-diag.js +97 -0
  128. package/dist/utils/npm-ops.js +119 -4
  129. package/dist/utils/package-version-info.js +244 -0
  130. package/dist/utils/welcome.js +37 -8
  131. package/dist/utils/windows-shell-trust.js +37 -0
  132. package/kits/docs/AUTHORING.md +314 -0
  133. package/kits/docs/GUIDE.md +2 -0
  134. package/kits/docs/INDEX.md +2 -0
  135. package/kits/docs/aun/CHEATSHEET.md +4 -0
  136. package/kits/docs/aun/SDK.md +60 -0
  137. package/kits/docs/channels/aun.md +8 -0
  138. package/kits/docs/context-assembly.md +10 -6
  139. package/kits/docs/evolcore/INDEX.md +17 -3
  140. package/kits/docs/evolcore/agent-create.md +24 -0
  141. package/kits/docs/evolcore/agent.md +5 -2
  142. package/kits/docs/evolcore/config.md +1 -1
  143. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  144. package/kits/docs/evolcore/group.md +34 -18
  145. package/kits/docs/evolcore/memory-architecture.md +78 -0
  146. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  147. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  148. package/kits/docs/evolcore/memory-tags.md +63 -0
  149. package/kits/docs/evolcore/memory.md +110 -0
  150. package/kits/docs/evolcore/menu-client.md +27 -11
  151. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  152. package/kits/docs/evolcore/menu-protocol.md +213 -0
  153. package/kits/docs/evolcore/msg.md +2 -0
  154. package/kits/docs/evolcore/trigger.md +7 -5
  155. package/kits/docs/path-registry.md +6 -8
  156. package/kits/docs/prompt-loading-architecture.md +5 -3
  157. package/kits/docs/venues/group.md +5 -4
  158. package/kits/eck_manifest.json +294 -88
  159. package/kits/eck_manifest.memory.json +257 -0
  160. package/kits/rules/03-identity.md +8 -8
  161. package/kits/rules/05-venue.md +4 -2
  162. package/kits/rules/06-channel.md +1 -1
  163. package/kits/schemas/_meta.json +276 -47
  164. package/kits/schemas/agent-config.schema.12.json +38 -2
  165. package/kits/schemas/agent-config.schema.13.json +591 -0
  166. package/kits/schemas/daemon.schema.6.json +9 -9
  167. package/kits/schemas/defaults.schema.5.json +29 -0
  168. package/kits/schemas/relation-config.schema.8.json +14 -1
  169. package/kits/schemas/relation-config.schema.9.json +129 -0
  170. package/kits/schemas/role-config.schema.1.json +3 -3
  171. package/kits/skills/.switches.json +3 -0
  172. package/kits/skills/ec-skill/SKILL.md +217 -0
  173. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  174. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  175. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  176. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  177. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  178. package/kits/templates/decision/auto-mention.md +25 -0
  179. package/kits/templates/decision/session-renew.md +22 -0
  180. package/kits/templates/memory-consolidation/forget.md +104 -0
  181. package/kits/templates/memory-consolidation/generalize.md +76 -0
  182. package/kits/templates/memory-consolidation/notes.md +133 -0
  183. package/kits/templates/memory-consolidation/profile.md +58 -0
  184. package/kits/templates/memory-consolidation/promote.md +72 -0
  185. package/kits/templates/memory-consolidation/workflow.md +75 -0
  186. package/kits/templates/system-fragments/bootstrap.md +14 -5
  187. package/kits/templates/system-fragments/commands.md +1 -0
  188. package/kits/templates/system-fragments/group-history.md +6 -0
  189. package/kits/templates/system-fragments/identity.md +0 -3
  190. package/kits/templates/system-fragments/memory.md +79 -0
  191. package/kits/templates/system-fragments/reply.md +1 -0
  192. package/kits/templates/system-fragments/venue.md +1 -1
  193. package/package.json +4 -6
  194. package/dist/trigger/legacy-session-history.js +0 -19
  195. package/kits/templates/system-fragments/group-unread-history.md +0 -7
@@ -5,12 +5,15 @@ import os from 'os';
5
5
  import crypto from 'crypto';
6
6
  import { BaseagentRunnerUnavailableError, hasCompact, autoCompactWindowForModel, isClaudeContextUsageModel, numericToken, formatAsyncQuestionAnswer, parseAsyncUserQuestions, } from '../../agents/runner-types.js';
7
7
  import { loadedBaseagentIds } from '../../agents/baseagent-availability.js';
8
+ import { isDiagnosticSessionActive } from '../command/diag-service.js';
8
9
  import { evaluateInboundAdmissionPreflight, evaluateOrdinaryInboundAdmission } from './message-admission.js';
9
10
  import { authorizeAccess, authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
10
11
  import { buildTaskRuntimeEnv, ensureRuntimeLockDir, ensureSessionRuntimeDir } from '../../cli/task-context.js';
12
+ import { memorySessionPolicyHook } from '../memory/memory-session-policy.js';
11
13
  import { IMRenderer } from './im-renderer.js';
14
+ import { TaskActivityStore } from '../task/task-activity-store.js';
12
15
  import { createSendFileMarkerPattern } from './file-markers.js';
13
- import { createTextInferenceProvider } from '../inference/text-inference.js';
16
+ import { createDecisionInferenceProvider, createTextInferenceProvider } from '../inference/text-inference.js';
14
17
  import { StreamIdleMonitor } from './stream-idle-monitor.js';
15
18
  import { logger } from '../../utils/logger.js';
16
19
  import { isHostChinese } from '../../utils/locale.js';
@@ -19,10 +22,10 @@ import { readEvolcoreRuntimeFlavor, tagErrorTextWithEvolcoreVersion } from '../.
19
22
  import { describeProactiveSendCommandFailure, isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
20
23
  import { summarizeToolInputForDisplay } from '../../utils/tool-summary.js';
21
24
  import { agentDataDir, agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
22
- import { renderKitSections } from '../../eck/kit-renderer.js';
25
+ import { renderKitSections, resolveSessionManifest, DEFAULT_MANIFEST_FILE } from '../../eck/kit-renderer.js';
23
26
  import { renderMessageBody } from '../../eck/message-renderer.js';
24
27
  import { syncGroupRulesContext } from '../../eck/group-rules-sync.js';
25
- import { consumeHints, hintsToSubMessages, composeHintFallback } from './pending-hints.js';
28
+ import { claimHints, commitHintClaim, releaseHintClaim, peekHints, hintsToSubMessages, composeHintFallback, OBSERVER_HINT_FAILURE_MESSAGES, } from './pending-hints.js';
26
29
  import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
27
30
  import { recordCausationLink, recordCausationSpan } from '../causation/audit.js';
28
31
  import { normalizeBaseagent } from '../../agents/baseagent.js';
@@ -32,6 +35,7 @@ import { buildEnvelope, isInteractionSendAccepted, replyContextFromSession, send
32
35
  import { isSystemOrServicePeer, resolveChatMode } from './peer-mode.js';
33
36
  import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
34
37
  import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
38
+ import { isValidAid } from '../../aun/aid/identity.js';
35
39
  // Re-export 工具函数(向后兼容,让其他模块可以从 response-engine 导入)
36
40
  export { buildEnvelope, sendInteractionPayload } from './message-utils.js';
37
41
  import { constrainResolvedModelForRole, isModelAllowedForRoleBaseagent } from '../model/model-permission.js';
@@ -53,10 +57,11 @@ import { registerBuiltinModes } from '../../response-system/modes/index.js';
53
57
  import { deriveSessionTitle, shouldAutoFillSessionTitle } from '../session/session-title.js';
54
58
  import { SessionTurnCoordinator } from '../session/session-turn-coordinator.js';
55
59
  import { recordTriggerExecutionAnomaly } from '../../trigger/anomaly-store.js';
56
- import { classifyToolErrorCode, normalizeToolErrorCode } from '../permission/tool-error-code.js';
60
+ import { normalizeToolErrorClassification } from '../permission/tool-error-code.js';
57
61
  import { normalizeExecutionPermissionMode } from '../permission/mode.js';
58
62
  import { buildToolLifecycleEventKey } from '../audit/event-key.js';
59
- import { isFullAccessEnabled, loadDaemonConfig } from '../../config-store.js';
63
+ import { aunMessageTranscriptUuid } from '../session/session-transcript.js';
64
+ import { loadDaemonConfig } from '../../config-store.js';
60
65
  import { assertAgentPolicyAuthorization } from '../role/runtime-policy.js';
61
66
  import { auditFullAccessEvent } from '../auth/authorization-audit.js';
62
67
  import { TaskRecoveryService, isTaskRecoveryCurrent } from '../task/task-recovery-service.js';
@@ -888,6 +893,13 @@ export class ResponseEngine {
888
893
  const config = resolveEffective({ self: aid }, { cache: true, expand: true });
889
894
  return createTextInferenceProvider(baseagent, config);
890
895
  }
896
+ getDecisionInferenceProvider(selfAID) {
897
+ const aid = selfAID;
898
+ if (!aid)
899
+ return undefined;
900
+ const config = resolveEffective({ self: aid }, { cache: true, expand: true });
901
+ return createDecisionInferenceProvider(config.decision_model);
902
+ }
891
903
  getAvailableBaseagentsForOwner(evolName) {
892
904
  return loadedBaseagentIds(this.agentMap.keys(), evolName);
893
905
  }
@@ -1059,6 +1071,198 @@ export class ResponseEngine {
1059
1071
  hasChannel: (session) => !!this.resolveChannelInfo(session.metadata?.channelKey || session.channel),
1060
1072
  });
1061
1073
  }
1074
+ observerHintConsumed;
1075
+ observerHintFailed;
1076
+ observerHintFailureReported = new WeakMap();
1077
+ scheduledObserverHints = new Set();
1078
+ setObserverHintConsumed(fn) {
1079
+ this.observerHintConsumed = fn;
1080
+ }
1081
+ setObserverHintFailed(fn) {
1082
+ this.observerHintFailed = fn;
1083
+ }
1084
+ observerHintStatusHint(hint, requestId) {
1085
+ return requestId && !hint.promoteRequestId ? { ...hint, promoteRequestId: requestId } : hint;
1086
+ }
1087
+ reportObserverHintFailure(message, hint, target, code, requestId, action) {
1088
+ const reported = this.observerHintFailureReported.get(message) ?? new Set();
1089
+ if (reported.has(hint.id))
1090
+ return;
1091
+ reported.add(hint.id);
1092
+ this.observerHintFailureReported.set(message, reported);
1093
+ const statusAction = action ?? (hint.promoteRequestId ? 'promote' : 'add');
1094
+ const statusRequestId = requestId ?? hint.promoteRequestId ?? (statusAction === 'add' ? message.observerHintRequestId : undefined);
1095
+ const statusHint = this.observerHintStatusHint(hint, statusAction === 'promote' ? statusRequestId : undefined);
1096
+ try {
1097
+ this.observerHintFailed?.(statusHint, target, {
1098
+ code, message: OBSERVER_HINT_FAILURE_MESSAGES[code], action: statusAction, requestId: statusRequestId,
1099
+ });
1100
+ }
1101
+ catch (error) {
1102
+ logger.warn(`[ResponseEngine] observer hint failure status failed id=${hint.id}: ${error}`);
1103
+ }
1104
+ }
1105
+ reportObserverHintFailureForMessage(message, code, requestId) {
1106
+ if (message.source !== 'owner-inject' || !message.observerHintId || !message.selfAID)
1107
+ return;
1108
+ const hint = peekHints(resolvePaths().sessionsDir, 'aun', message.channelId, message.selfAID, message.threadId, (message.chatType || 'private'), undefined, message.channel).find(item => item.id === message.observerHintId);
1109
+ if (!hint)
1110
+ return;
1111
+ this.reportObserverHintFailure(message, hint, {
1112
+ channelKey: message.channel,
1113
+ channelId: message.channelId,
1114
+ chatType: (message.chatType || 'private'),
1115
+ threadId: message.threadId,
1116
+ }, code, requestId, hint.promoteRequestId ? 'promote' : 'add');
1117
+ }
1118
+ observerHintFailureCodeForError(error) {
1119
+ const code = error && typeof error === 'object' && 'code' in error
1120
+ ? error.code : undefined;
1121
+ const knownCodes = [
1122
+ 'SESSION_NOT_FOUND', 'INTERRUPT_FAILED', 'INTERRUPT_TIMEOUT', 'QUEUE_FAILED',
1123
+ 'QUEUE_PERSIST_FAILED', 'RENDER_FAILED', 'RUNNER_START_FAILED',
1124
+ 'RUNNER_START_SUPERSEDED', 'HINT_UNAVAILABLE',
1125
+ ];
1126
+ if (typeof code === 'string' && knownCodes.includes(code)) {
1127
+ return code;
1128
+ }
1129
+ const text = error instanceof Error ? error.message : String(error);
1130
+ if (text === 'RUNNER_START_SUPERSEDED')
1131
+ return 'RUNNER_START_SUPERSEDED';
1132
+ if (text.includes('interrupt barrier timed out'))
1133
+ return 'INTERRUPT_TIMEOUT';
1134
+ if (text.includes('interrupt'))
1135
+ return 'INTERRUPT_FAILED';
1136
+ if (text.includes('queue is unavailable'))
1137
+ return 'QUEUE_FAILED';
1138
+ if (text.includes('enqueue') || text.includes('queue'))
1139
+ return 'QUEUE_PERSIST_FAILED';
1140
+ if (text.includes('renderMessageBody'))
1141
+ return 'RENDER_FAILED';
1142
+ if (text.includes('target session') || text.includes('SESSION_NOT_FOUND'))
1143
+ return 'SESSION_NOT_FOUND';
1144
+ return 'RUNNER_START_FAILED';
1145
+ }
1146
+ reportObserverHintFailureForClaim(message, session, claim, code) {
1147
+ for (const hint of claim.hints) {
1148
+ this.reportObserverHintFailure(message, hint, {
1149
+ channelKey: session.channel,
1150
+ channelId: claim.channelId,
1151
+ chatType: claim.chatType,
1152
+ threadId: claim.threadId || undefined,
1153
+ }, code, hint.promoteRequestId ?? message.observerHintRequestId, hint.promoteRequestId ? 'promote' : 'add');
1154
+ }
1155
+ }
1156
+ observerHintScheduleFailure(target, code) {
1157
+ const hint = peekHints(resolvePaths().sessionsDir, 'aun', target.channelId, target.selfAid, target.threadId, target.chatType, undefined, target.channelKey).find(item => item.id === target.hintId);
1158
+ if (!hint)
1159
+ return;
1160
+ const statusAction = hint.promoteRequestId ? 'promote' : 'add';
1161
+ const statusHint = this.observerHintStatusHint(hint, statusAction === 'promote' ? target.requestId : undefined);
1162
+ try {
1163
+ this.observerHintFailed?.(statusHint, {
1164
+ channelKey: target.channelKey,
1165
+ channelId: target.channelId,
1166
+ chatType: target.chatType,
1167
+ threadId: target.threadId,
1168
+ }, {
1169
+ code, message: OBSERVER_HINT_FAILURE_MESSAGES[code], action: statusAction,
1170
+ requestId: target.requestId ?? hint.promoteRequestId,
1171
+ });
1172
+ }
1173
+ catch (error) {
1174
+ logger.warn(`[ResponseEngine] observer hint schedule failure status failed id=${hint.id}: ${error}`);
1175
+ }
1176
+ }
1177
+ async observerTargetSession(target) {
1178
+ const session = target.threadId
1179
+ ? await this.sessionManager.getThreadSession(target.channelKey, target.channelId, target.threadId)
1180
+ : this.sessionManager.getActiveSessionSync(target.channelKey, target.channelId, 'aun', target.selfAid);
1181
+ if (!session || session.deletedAt || session.selfAID !== target.selfAid || session.channel !== target.channelKey
1182
+ || session.channelId !== target.channelId || session.chatType !== target.chatType
1183
+ || (session.threadId || '') !== (target.threadId || '')
1184
+ || !this.sessionManager.hasBoundAgentSession(session)
1185
+ || (target.chatType === 'group' && (session.metadata?.groupId !== target.channelId
1186
+ || !session.metadata?.observerLastActorId)))
1187
+ return undefined;
1188
+ return session;
1189
+ }
1190
+ async hasObserverTargetSession(target) {
1191
+ return !!(await this.observerTargetSession(target));
1192
+ }
1193
+ async scheduleObserverHint(target) {
1194
+ const session = await this.observerTargetSession(target);
1195
+ if (!session) {
1196
+ this.observerHintScheduleFailure(target, 'SESSION_NOT_FOUND');
1197
+ return 'SESSION_NOT_FOUND';
1198
+ }
1199
+ if (!this.messageQueue) {
1200
+ this.observerHintScheduleFailure(target, 'QUEUE_FAILED');
1201
+ throw new Error('Observer hint queue is unavailable');
1202
+ }
1203
+ const scheduledKey = `${session.id}\u0000${target.hintId}`;
1204
+ if (this.scheduledObserverHints.has(scheduledKey))
1205
+ return 'duplicate';
1206
+ this.scheduledObserverHints.add(scheduledKey);
1207
+ try {
1208
+ await this.messageQueue.withSessionBarrier(session.id, async (snapshot) => {
1209
+ if (snapshot.active && !snapshot.dispatching) {
1210
+ let timeout;
1211
+ try {
1212
+ try {
1213
+ await Promise.race([
1214
+ this.messageQueue.interruptForPriority(session.id),
1215
+ new Promise((_, reject) => {
1216
+ timeout = setTimeout(() => reject(new Error('Observer hint interrupt barrier timed out')), 30_000);
1217
+ timeout.unref?.();
1218
+ }),
1219
+ ]);
1220
+ }
1221
+ catch (error) {
1222
+ const code = error instanceof Error
1223
+ && error.message === 'Observer hint interrupt barrier timed out'
1224
+ ? 'INTERRUPT_TIMEOUT' : 'INTERRUPT_FAILED';
1225
+ this.observerHintScheduleFailure(target, code);
1226
+ throw error;
1227
+ }
1228
+ }
1229
+ finally {
1230
+ if (timeout)
1231
+ clearTimeout(timeout);
1232
+ }
1233
+ }
1234
+ const receivedAt = Date.now();
1235
+ const actorId = target.chatType === 'group'
1236
+ ? session.metadata.observerLastActorId : target.channelId;
1237
+ try {
1238
+ await this.messageQueue.enqueuePersisted(session.id, {
1239
+ channel: session.channel, channelType: 'aun', channelId: session.channelId,
1240
+ selfAID: session.selfAID, baseagent: session.baseagent, threadId: session.threadId,
1241
+ chatType: target.chatType, groupId: target.chatType === 'group' ? target.channelId : undefined,
1242
+ peerId: actorId, peerName: session.metadata?.observerLastPeerName || session.metadata?.peerName,
1243
+ peerType: session.metadata?.observerLastPeerType || session.metadata?.peerType,
1244
+ replyContext: replyContextFromSession(session),
1245
+ boundSessionId: session.id, content: '', source: 'owner-inject', observerHintId: target.hintId,
1246
+ ...(target.requestId ? { observerHintRequestId: target.requestId } : {}),
1247
+ messageId: `owner-hint-${crypto.randomUUID()}`, receivedAt, timestamp: receivedAt,
1248
+ }, session.projectPath, {
1249
+ priority: true, interruptible: false, interruptSamePeer: false,
1250
+ agentName: this.agentRegistry?.resolveByChannel(session.channel)?.name,
1251
+ selfAID: target.selfAid, sessionKeyField: session.sessionKey,
1252
+ });
1253
+ }
1254
+ catch (error) {
1255
+ this.observerHintScheduleFailure(target, 'QUEUE_PERSIST_FAILED');
1256
+ throw error;
1257
+ }
1258
+ });
1259
+ return 'queued';
1260
+ }
1261
+ catch (error) {
1262
+ this.scheduledObserverHints.delete(scheduledKey);
1263
+ throw error;
1264
+ }
1265
+ }
1062
1266
  async recoverTask(request) {
1063
1267
  if (!this.taskRecoveryService) {
1064
1268
  return { ok: false, status: 'not_recoverable', sessionId: request.sessionId, message: '消息队列尚未初始化' };
@@ -1159,7 +1363,7 @@ export class ResponseEngine {
1159
1363
  else
1160
1364
  await this.interruptSession(sessionId, error instanceof OrphanedTargetSessionError ? 'execution_error' : 'timeout');
1161
1365
  }
1162
- async interruptSession(sessionId, reason) {
1366
+ async interruptSession(sessionId, reason, strict = false) {
1163
1367
  this.clearPauseSession(sessionId);
1164
1368
  // A retry delay has no active runner stream to abort. Wake it before any
1165
1369
  // session-store I/O so /stop and a newer message take effect immediately.
@@ -1239,6 +1443,8 @@ export class ResponseEngine {
1239
1443
  })();
1240
1444
  const runnerInterrupt = agent.interrupt(sessionId).catch(error => {
1241
1445
  logger.debug(`[ResponseEngine] interrupt barrier runner failure: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
1446
+ if (strict)
1447
+ throw error;
1242
1448
  });
1243
1449
  await Promise.all([terminalStatus, runnerInterrupt]);
1244
1450
  }
@@ -1393,32 +1599,30 @@ export class ResponseEngine {
1393
1599
  const globalCm = agent.config?.chatmode ?? this.globalSettings.chatmode;
1394
1600
  return agent.getContext(channelName, chatType, globalCm);
1395
1601
  }
1396
- /**
1397
- * 观察者插话(v0.3):消费当前 (对端, thread) 的待用提示,转成 owner-hint SubMessage。
1398
- * 一次性语义:consumeHints 回放算有效集后清该 thread(其它 thread 残留则保留,否则删文件)。
1399
- * 仅 aun 渠道(pending-hints 落在 sessions/aun/<self>/<对端>/)。
1400
- */
1602
+ /** Claim matching hints for this turn; only commit after the runner accepts its prompt. */
1401
1603
  consumeOwnerHints(session, message) {
1604
+ if (message.source !== 'owner-inject' && message.source !== 'user' && message.source !== undefined)
1605
+ return { items: [] };
1402
1606
  const channelType = session.channelType || message.channelType || session.channel;
1403
1607
  if (channelType !== 'aun')
1404
- return [];
1608
+ return { items: [] };
1405
1609
  const selfAID = session.selfAID || message.selfAID;
1406
1610
  if (!selfAID)
1407
- return [];
1611
+ return { items: [] };
1408
1612
  // 会话定位键:私聊=对端 AID,群聊=groupId(均为 session.channelId)。
1409
1613
  const peerChannelId = session.channelId;
1410
1614
  if (!peerChannelId)
1411
- return [];
1615
+ return { items: [] };
1412
1616
  try {
1413
- const hints = consumeHints(resolvePaths().sessionsDir, 'aun', peerChannelId, selfAID, session.threadId);
1414
- if (hints.length === 0)
1415
- return [];
1416
- logger.info(`[ResponseEngine] consumed ${hints.length} owner-hint(s) for ${peerChannelId} thread=${session.threadId || 'main'}`);
1417
- return hintsToSubMessages(hints);
1617
+ const claim = claimHints(resolvePaths().sessionsDir, 'aun', peerChannelId, selfAID, session.threadId, (message.chatType || session.chatType || 'private'), message.source === 'owner-inject' ? 'interrupt' : 'next', message.observerHintId, session.channel);
1618
+ if (claim.hints.length === 0)
1619
+ return { items: [] };
1620
+ logger.info(`[ResponseEngine] claimed ${claim.hints.length} owner-hint(s) for ${peerChannelId} thread=${session.threadId || 'main'}`);
1621
+ return { items: hintsToSubMessages(claim.hints), claim };
1418
1622
  }
1419
1623
  catch (e) {
1420
1624
  logger.warn(`[ResponseEngine] consumeOwnerHints failed: ${e instanceof Error ? e.message : String(e)}`);
1421
- return [];
1625
+ return { items: [] };
1422
1626
  }
1423
1627
  }
1424
1628
  /**
@@ -1594,8 +1798,8 @@ export class ResponseEngine {
1594
1798
  }
1595
1799
  /**
1596
1800
  * A fullaccess Trigger may run with no current relation access for its
1597
- * origin actor, but only after the daemon gate and persisted owner
1598
- * provenance still validate. This is intentionally stricter than merely
1801
+ * origin actor, but only after its persisted owner provenance still
1802
+ * validates. This is intentionally stricter than merely
1599
1803
  * checking the message's requested permission mode.
1600
1804
  */
1601
1805
  isCurrentFullAccessTrigger(message) {
@@ -1609,8 +1813,7 @@ export class ResponseEngine {
1609
1813
  return false;
1610
1814
  }
1611
1815
  try {
1612
- return isFullAccessEnabled()
1613
- && new Set(loadDaemonConfig().owners ?? []).has(trigger.authorizedBy);
1816
+ return new Set(loadDaemonConfig().owners ?? []).has(trigger.authorizedBy);
1614
1817
  }
1615
1818
  catch {
1616
1819
  return false;
@@ -1618,7 +1821,7 @@ export class ResponseEngine {
1618
1821
  }
1619
1822
  /**
1620
1823
  * The authorization envelope is immutable for one logical run, but the
1621
- * daemon gate, approving owner, and runner profile are live policy. Check
1824
+ * approving owner and runner profile are live policy. Check
1622
1825
  * them immediately before every Runner invocation, including retries.
1623
1826
  */
1624
1827
  assertCurrentFullAccessExecutionAuthorization(authorization, agent) {
@@ -1639,9 +1842,6 @@ export class ResponseEngine {
1639
1842
  else if (authorization.source !== 'fullaccess-command' && authorization.source !== 'trigger') {
1640
1843
  throw new Error('fullaccess execution authorization has an unsupported source');
1641
1844
  }
1642
- if (!isFullAccessEnabled()) {
1643
- throw new Error('fullaccess execution is disabled by daemon.json');
1644
- }
1645
1845
  const currentOwners = new Set(loadDaemonConfig().owners ?? []);
1646
1846
  if (!currentOwners.has(authorization.authorizedBy)) {
1647
1847
  throw new Error('fullaccess execution authorization is no longer owned');
@@ -1831,7 +2031,7 @@ export class ResponseEngine {
1831
2031
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
1832
2032
  '/stop', '/pause', '/resume', '/clear', '/compact', '/renew', '/del', '/perm', '/file', '/check',
1833
2033
  '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode',
1834
- '/aid', '/upgrade', '/evolagent', '/pair ',
2034
+ '/aid', '/upgrade', '/evolagent', '/pair ', '/memory',
1835
2035
  ];
1836
2036
  /** 判断消息内容是否为已知命令 */
1837
2037
  isKnownCommand(content) {
@@ -1849,9 +2049,15 @@ export class ResponseEngine {
1849
2049
  }
1850
2050
  catch (error) {
1851
2051
  outerFailureReason = error instanceof Error ? error.message : String(error);
2052
+ if (message.source === 'owner-inject') {
2053
+ this.reportObserverHintFailureForMessage(message, this.observerHintFailureCodeForError(error));
2054
+ }
1852
2055
  throw error;
1853
2056
  }
1854
2057
  finally {
2058
+ if (message.source === 'owner-inject' && message.observerHintId && message.boundSessionId) {
2059
+ this.scheduledObserverHints.delete(`${message.boundSessionId}\u0000${message.observerHintId}`);
2060
+ }
1855
2061
  if (fallbackFullAccessAudit && !this.fullAccessTerminalMessages.has(message)) {
1856
2062
  auditFullAccessEvent({
1857
2063
  event: fallbackFullAccessAudit.authorization.source === 'trigger'
@@ -1878,6 +2084,28 @@ export class ResponseEngine {
1878
2084
  async processMessageWithMonitors(message) {
1879
2085
  const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
1880
2086
  const totalExecutionMs = this.totalExecutionLimitMs();
2087
+ // A discarded handoff must be dropped before any bound-session lookup.
2088
+ // Restored origin deliveries may point at a session that has already been
2089
+ // replaced; resolving it first would reactivate the stale session path.
2090
+ if (message.handoffDelivery
2091
+ && this.handoffRuntime
2092
+ && this.handoffRuntime.isDiscardedDelivery(message)) {
2093
+ logger.info(`[ResponseEngine] Skip stale handoff delivery before session resolution:`
2094
+ + ` direction=${message.handoffDelivery.direction}`
2095
+ + ` handoff=${message.handoffDelivery.handoffId}`);
2096
+ return;
2097
+ }
2098
+ // Includes persisted queues, recovery triggers and delayed owner replies.
2099
+ // A closed diagnostic must not activate its old main session again.
2100
+ const diagnosticBoundId = message.boundSessionId ?? message.triggerMeta?.boundSessionId;
2101
+ if (diagnosticBoundId) {
2102
+ const bound = await this.sessionManager.getSessionById(diagnosticBoundId);
2103
+ if (bound && (bound.metadata?.diagnostic || bound.metadata?.diagnosticStatus === 'cancelled')
2104
+ && !isDiagnosticSessionActive(bound, this.sessionManager.getChatDir(bound))) {
2105
+ logger.info(`[ResponseEngine] Skip closed diagnostic session ${bound.id}`);
2106
+ return;
2107
+ }
2108
+ }
1881
2109
  if (message.taskRecovery) {
1882
2110
  const current = message.boundSessionId
1883
2111
  ? await this.sessionManager.getSessionById(message.boundSessionId)
@@ -1887,17 +2115,16 @@ export class ResponseEngine {
1887
2115
  return;
1888
2116
  }
1889
2117
  }
1890
- if (message.handoffDelivery
1891
- && this.handoffRuntime
1892
- && this.handoffRuntime.isDiscardedDelivery(message)) {
1893
- logger.info(`[ResponseEngine] Skip stale handoff delivery before session resolution:`
1894
- + ` direction=${message.handoffDelivery.direction}`
1895
- + ` handoff=${message.handoffDelivery.handoffId}`);
1896
- return;
1897
- }
1898
2118
  // 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
1899
2119
  // message.channel 现在存实例名(channelName),可直接用于精确路由
1900
2120
  const { session, absoluteProjectPath } = await this.resolveSession(message);
2121
+ if (message.source === 'owner-inject' && (!message.observerHintId || !peekHints(resolvePaths().sessionsDir, 'aun', session.channelId, session.selfAID || '', session.threadId, session.chatType, 'interrupt', session.channel).some(item => item.id === message.observerHintId))) {
2122
+ this.reportObserverHintFailureForMessage(message, 'HINT_UNAVAILABLE');
2123
+ return;
2124
+ }
2125
+ if ((session.metadata?.diagnostic || session.metadata?.diagnosticStatus === 'cancelled')
2126
+ && !isDiagnosticSessionActive(session, this.sessionManager.getChatDir(session)))
2127
+ return;
1901
2128
  const accessChannelKey = this.messageChannelKey(message, session);
1902
2129
  const accessChannelInfo = this.resolveChannelInfo(accessChannelKey);
1903
2130
  const accessAdapter = accessChannelInfo?.adapter;
@@ -1986,6 +2213,21 @@ export class ResponseEngine {
1986
2213
  this.publishTriggerExecutionFailure(message, 'access_denied');
1987
2214
  return;
1988
2215
  }
2216
+ if (message.chatType === 'group' && (message.source === 'user' || !message.source)) {
2217
+ const actorId = message.items?.filter(item => item.peerId).at(-1)?.peerId || message.peerId;
2218
+ if (actorId) {
2219
+ try {
2220
+ await this.sessionManager.patchSessionMetadata(session.id, {
2221
+ observerLastActorId: actorId,
2222
+ observerLastPeerName: message.peerName,
2223
+ observerLastPeerType: message.peerType,
2224
+ });
2225
+ }
2226
+ catch (error) {
2227
+ logger.warn(`[ResponseEngine] observer group actor snapshot failed: ${error}`);
2228
+ }
2229
+ }
2230
+ }
1989
2231
  // thread(feishu) pending strategy: inject replyContext so first reply creates the thread
1990
2232
  if (message.triggerMeta?.pendingThread && message.triggerMeta?.rootMessageId) {
1991
2233
  message.replyContext = {
@@ -2017,6 +2259,7 @@ export class ResponseEngine {
2017
2259
  catch (error) {
2018
2260
  if (error instanceof BaseagentRunnerUnavailableError) {
2019
2261
  logger.error(`[ResponseEngine] baseagent mismatch blocked: session=${session.id} channel=${channelKey} requested=${session.baseagent} owner=${error.evolagentName} available=${error.availableBaseagents.join(',') || '<none>'}`);
2262
+ this.reportObserverHintFailureForMessage(message, 'RUNNER_START_FAILED');
2020
2263
  await this.sendBaseagentMismatch(channelKey, message.channelId, session, error, message.replyContext);
2021
2264
  this.publishTriggerExecutionFailure(message, `baseagent_unavailable:${error.baseagent}`);
2022
2265
  return;
@@ -2213,8 +2456,7 @@ export class ResponseEngine {
2213
2456
  && commandAuthorization.authorizedBy.length > 0) {
2214
2457
  let currentOwner = false;
2215
2458
  try {
2216
- currentOwner = isFullAccessEnabled()
2217
- && new Set(loadDaemonConfig().owners ?? []).has(commandAuthorization.authorizedBy);
2459
+ currentOwner = new Set(loadDaemonConfig().owners ?? []).has(commandAuthorization.authorizedBy);
2218
2460
  if (commandAuthorization.source === 'agent-policy')
2219
2461
  assertAgentPolicyAuthorization(commandAuthorization);
2220
2462
  }
@@ -2330,6 +2572,8 @@ export class ResponseEngine {
2330
2572
  const isBootstrapping = lifecycle === 'bootstrapping';
2331
2573
  // Per-method agent name for stats bucketing (agent.name or '<unknown>')
2332
2574
  const agentNameForStats = owningAgentForTask?.name ?? taskAgentAid ?? '<unknown>';
2575
+ let ownerHintClaim;
2576
+ let observerHintFailureCode;
2333
2577
  if (!channelInfo) {
2334
2578
  logger.error(`[ResponseEngine] Unknown channel: ${channelKey}`);
2335
2579
  this.publishTriggerExecutionFailure(message, `unknown_channel:${channelKey}`);
@@ -2384,6 +2628,29 @@ export class ResponseEngine {
2384
2628
  }
2385
2629
  // 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
2386
2630
  const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
2631
+ let taskActivityStatus = 'running';
2632
+ const taskActivityStore = runnerSelfAid ? new TaskActivityStore(runnerSelfAid) : undefined;
2633
+ if (taskActivityStore) {
2634
+ try {
2635
+ const activityChatType = chatType === 'group' ? 'group' : 'private';
2636
+ const activityChannelId = activityChatType === 'group'
2637
+ ? (message.groupId || session.metadata?.groupId || message.channelId)
2638
+ : (message.peerId || message.channelId);
2639
+ taskActivityStore.createTask({
2640
+ taskId,
2641
+ sessionId: session.id,
2642
+ channel: message.channelType || session.channelType || message.channel,
2643
+ channelId: activityChannelId,
2644
+ chatType: activityChatType,
2645
+ ...(activityChatType === 'group' ? { groupId: activityChannelId } : {}),
2646
+ ...(activityChatType === 'private' ? { peerId: activityChannelId } : {}),
2647
+ taskStatus: taskActivityStatus,
2648
+ });
2649
+ }
2650
+ catch (error) {
2651
+ logger.warn(`[ResponseEngine] task activity store initialization failed: ${error instanceof Error ? error.message : String(error)}`);
2652
+ }
2653
+ }
2387
2654
  const taskPauseController = this.getPauseController(session.id);
2388
2655
  let turnLease;
2389
2656
  const inputCausation = normalizeCausation(message.causation) ?? createRootCausation();
@@ -2479,10 +2746,12 @@ export class ResponseEngine {
2479
2746
  // this inbound actor. Do not fall back to a stale session owner/admin
2480
2747
  // after an alias or Agent-role revocation; only use the persisted session
2481
2748
  // identity when role resolution itself was unavailable.
2482
- const peerRole = authorizationIdentity?.role
2483
- ?? (peerRoleDetail
2484
- ? (peerRoleDetail.effectiveRole ?? 'none')
2485
- : (session.identity?.role ?? 'none'));
2749
+ const peerRole = message.autoMentionBatch && message.items?.some(item => item.peerRole && item.peerRole !== peerRoleDetail?.effectiveRole)
2750
+ ? 'visitor'
2751
+ : authorizationIdentity?.role
2752
+ ?? (peerRoleDetail
2753
+ ? (peerRoleDetail.effectiveRole ?? 'none')
2754
+ : (session.identity?.role ?? 'none'));
2486
2755
  const effectiveAgentConfig = (() => {
2487
2756
  try {
2488
2757
  return resolveEffective({ self: selfAid || undefined, peerKey }, { cache: true });
@@ -2565,6 +2834,9 @@ export class ResponseEngine {
2565
2834
  outputState.middleOutputMode = middleOutputMode;
2566
2835
  // 诊断日志:记录 inbound message_id 和生成的 task_id 的对应关系
2567
2836
  logger.info(`[ResponseEngine] Task created: inboundMsgId=${message.messageId ?? 'none'} taskId=${taskId} sessionId=${session.id} chatmode=${chatmode} mode=${resolvedMode?.mode.id ?? 'override/fallback'}`);
2837
+ {
2838
+ import('../../utils/msg-diag.js').then(m => m.diagLog('task_created', { aid: session.selfAID ?? '', msgId: message.messageId ?? '', taskId, text: message.content ?? '' })).catch(() => { });
2839
+ }
2568
2840
  // 构建带 taskId/chatmode 的 ReplyContext(本次任务所有出站消息共用)
2569
2841
  const taskReplyContext = () => {
2570
2842
  const base = this.getReplyContext(message);
@@ -2588,6 +2860,19 @@ export class ResponseEngine {
2588
2860
  },
2589
2861
  modeConfig: resolvedMode.context.modeConfig,
2590
2862
  state: modeState,
2863
+ sessionState: resolvedMode.context.sessionState,
2864
+ continuation: message.taskRecovery
2865
+ ? {
2866
+ kind: 'taskRecovery',
2867
+ taskId: message.taskRecovery.taskId,
2868
+ generation: message.taskRecovery.generation,
2869
+ groupId: message.groupId,
2870
+ threadId: message.threadId,
2871
+ }
2872
+ : message.restartResume?.submitted
2873
+ ? { kind: 'restartResume', groupId: message.groupId, threadId: message.threadId }
2874
+ : undefined,
2875
+ previousTurnState: this.turnCoordinator.current(session),
2591
2876
  isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, selfAid),
2592
2877
  describeSendCommandFailure: describeProactiveSendCommandFailure,
2593
2878
  logger,
@@ -2824,7 +3109,18 @@ export class ResponseEngine {
2824
3109
  this.publishTriggerExecutionSkipped(message, 'recovery_stale');
2825
3110
  return;
2826
3111
  }
2827
- turnLease = await this.turnCoordinator.begin(session, taskId, undefined, configActorId ? {
3112
+ const sourceMessageId = message.sourceMessageIds === undefined
3113
+ ? message.messageId
3114
+ : message.sourceMessageIds.length === 1 ? message.sourceMessageIds[0] : undefined;
3115
+ const inputId = normalizeBaseagent(agent.name).canonical === 'claude'
3116
+ && configChatType === 'private'
3117
+ && (message.channelType ?? session.channelType) === 'aun'
3118
+ && message.source !== 'trigger'
3119
+ && !message.taskRecovery
3120
+ && sourceMessageId
3121
+ ? aunMessageTranscriptUuid(sourceMessageId)
3122
+ : undefined;
3123
+ turnLease = await this.turnCoordinator.begin(session, taskId, inputId, configActorId ? {
2828
3124
  peerId: configActorId,
2829
3125
  encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
2830
3126
  baseagent: session.baseagent,
@@ -2834,7 +3130,11 @@ export class ResponseEngine {
2834
3130
  triggerAttemptId: message.triggerMeta?.attemptId,
2835
3131
  } : undefined);
2836
3132
  // 记录开始处理
2837
- const taskEncrypt = message.replyContext?.metadata?.encrypted != null ? !!(message.replyContext.metadata.encrypted) : undefined;
3133
+ const taskEncrypt = typeof message.encrypted === 'boolean'
3134
+ ? message.encrypted
3135
+ : message.replyContext?.metadata?.encrypted != null
3136
+ ? !!message.replyContext.metadata.encrypted
3137
+ : undefined;
2838
3138
  this.eventBus.publish({
2839
3139
  type: 'task:started',
2840
3140
  sessionId: session.id,
@@ -2866,6 +3166,9 @@ export class ResponseEngine {
2866
3166
  });
2867
3167
  // 创建 IMRenderer(统一 interactive/proactive 两条路径)
2868
3168
  let firstReply = true;
3169
+ // 出站 debug.model:每次 runQuery 前更新(含备用模型切换)。单独声明是因为
3170
+ // send 闭包早于下方 modelOverride/agentModel 的 let 声明,直接引用会撞 TDZ。
3171
+ let debugRunnerModel;
2869
3172
  const renderer = new IMRenderer({
2870
3173
  adapter,
2871
3174
  envelope,
@@ -2874,8 +3177,32 @@ export class ResponseEngine {
2874
3177
  flushDelay: (options?.flushDelay ?? this.agentRegistry?.resolveByChannel(channelKey)?.config?.flush_delay ?? 3) * 1000,
2875
3178
  suppressActivityItems: isProactive ? false : middleOutputMode !== 'all',
2876
3179
  suppressIntermediateText: isProactive ? false : middleOutputMode === 'none',
3180
+ deferTextUntilBoundary: (message.channelType ?? session.channelType) === 'aun' && session.chatType === 'private',
2877
3181
  operationalNoticesAsText: !isProactive && middleOutputMode !== 'none',
2878
3182
  fileMarkerPattern: options?.fileMarkerPattern,
3183
+ recordActivity: item => {
3184
+ if (!taskActivityStore || (turnLease && !this.turnCoordinator.canPublish(turnLease)))
3185
+ return;
3186
+ try {
3187
+ const activityChatType = chatType === 'group' ? 'group' : 'private';
3188
+ const activityChannelId = activityChatType === 'group'
3189
+ ? (message.groupId || session.metadata?.groupId || message.channelId)
3190
+ : (message.peerId || message.channelId);
3191
+ taskActivityStore.append({
3192
+ taskId,
3193
+ sessionId: session.id,
3194
+ channel: message.channelType || session.channelType || message.channel,
3195
+ channelId: activityChannelId,
3196
+ chatType: activityChatType,
3197
+ ...(activityChatType === 'group' ? { groupId: activityChannelId } : {}),
3198
+ ...(activityChatType === 'private' ? { peerId: activityChannelId } : {}),
3199
+ item,
3200
+ });
3201
+ }
3202
+ catch (error) {
3203
+ logger.warn(`[ResponseEngine] task activity record failed: ${error instanceof Error ? error.message : String(error)}`);
3204
+ }
3205
+ },
2879
3206
  send: async (payload) => {
2880
3207
  if (turnLease && !this.turnCoordinator.canPublish(turnLease)) {
2881
3208
  snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'suppressed-stale-turn' });
@@ -2907,8 +3234,13 @@ export class ResponseEngine {
2907
3234
  }
2908
3235
  opts.sessionId = session.id;
2909
3236
  if (payload.kind === 'result.text' && payload.isFinal) {
2910
- opts.title = '\u2705 \u6700\u7ec8\u56de\u590d:';
3237
+ opts.ext = { ...opts.ext, isFinal: true };
2911
3238
  }
3239
+ opts.debug = {
3240
+ sessionId: session.id,
3241
+ ...(session.agentSessionId ? { agentSessionId: session.agentSessionId } : {}),
3242
+ ...(debugRunnerModel ? { model: debugRunnerModel } : {}),
3243
+ };
2912
3244
  opts.metadata = withTaskMetadata({
2913
3245
  ...(opts.metadata ?? {}),
2914
3246
  ...(payload.kind === 'result.text' ? payload.metadata : {}),
@@ -3080,7 +3412,12 @@ export class ResponseEngine {
3080
3412
  const runModeConfig = resolvedMode?.mode.configureRun && modeProcessCtx
3081
3413
  ? resolvedMode.mode.configureRun(modeProcessCtx)
3082
3414
  : undefined;
3083
- const pureSessionPolicyHook = runModeConfig?.policyHook;
3415
+ const modePolicyHook = runModeConfig?.policyHook;
3416
+ // 整理会话:写工具全拦、Bash 只放行 ec memory create|update|archive,先于模式策略判定
3417
+ const isMemorySession = (session.sessionType ?? session.metadata?.sessionType) === 'memory';
3418
+ const pureSessionPolicyHook = isMemorySession
3419
+ ? (toolName, toolInput) => memorySessionPolicyHook(toolName, toolInput) ?? modePolicyHook?.(toolName, toolInput)
3420
+ : modePolicyHook;
3084
3421
  // 设置权限审批的交互上下文(支持交互卡片)
3085
3422
  const permissionContext = {
3086
3423
  sendPrompt: permissionPromptForOrigin,
@@ -3203,6 +3540,9 @@ export class ResponseEngine {
3203
3540
  const stBefore = modeState.get('proactive');
3204
3541
  const wasFirstPending = stBefore && !stBefore.firstToolDone;
3205
3542
  const result = pluginHook(toolName, toolInput);
3543
+ // 整理会话的拦截原因已经写清楚怎么改,不套 proactive 违规提醒
3544
+ if (result?.block && result.policyCode?.startsWith('memory_session_'))
3545
+ return result;
3206
3546
  if (result?.block) {
3207
3547
  // 拦截事件(首工具非表态 / 工具进展汇报未完成)
3208
3548
  snapshot.set(session.id, taskId, { policyHook: { triggered: true, blocked: true, toolName } });
@@ -3264,10 +3604,9 @@ export class ResponseEngine {
3264
3604
  if (!isProactive && channelInfo.adapter.capabilities?.file)
3265
3605
  capParts.push('文件发送');
3266
3606
  // Personal state is exposed to the manifest as vars. The manifest owns
3267
- // loading persona.md and working.md into the system prompt.
3607
+ // loading persona.md into the system prompt.
3268
3608
  const owningAgent = owningAgentForTask;
3269
3609
  const persona = isActive ? (owningAgent?.getPersona?.() || undefined) : undefined;
3270
- const working = isActive ? (owningAgent?.getWorkingMemory?.() || undefined) : undefined;
3271
3610
  // 计算 peerKey:群聊固定按 groupId/channelId,私聊按发送者 peerId。
3272
3611
  // 这样单条和积压合并批次不会因队列状态不同而切换关系级配置。
3273
3612
  const normalizedBaseagent = normalizeBaseagent(agent.name);
@@ -3609,6 +3948,10 @@ export class ResponseEngine {
3609
3948
  },
3610
3949
  }).allow;
3611
3950
  };
3951
+ // 会话原型与整理模式:必须在 kitCtx 构造之前读出,manifest 的 when 条件
3952
+ // 依赖它们选段(缺了会 fail-closed —— 模式手册全不加载,且不报错)。
3953
+ const renderedSessionType = session.sessionType ?? session.metadata?.sessionType ?? 'main';
3954
+ const memoryMode = session.metadata?.memoryMode;
3612
3955
  // Kit renderer: 组装上下文
3613
3956
  const pkgRoot = getPackageRoot();
3614
3957
  const kitCtx = {
@@ -3626,16 +3969,36 @@ export class ResponseEngine {
3626
3969
  // evolcore 运行模式:dev=源码仓库 | install=全局安装包
3627
3970
  evolcoreMode: readEvolcoreRuntimeFlavor(),
3628
3971
  // 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
3972
+ // AGENT_DIR 是 manifest 的合法别名(buildPathMappings 有注册),
3973
+ // 缺了它引用 $AGENT_DIR 的段(如技能索引)会整段静默丢弃。
3974
+ AGENT_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid) : undefined,
3629
3975
  PERSONAL_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'personal') : undefined,
3630
3976
  RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
3631
3977
  VENUES_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'venues') : undefined,
3632
3978
  selfAid: selfAid || undefined,
3979
+ // ownerAid:当前 agent 的第一个 owner AID,用于 owner 作用域记忆路径展开。
3980
+ // 来源:getFirstStaticAgentOwner 读 config.json owners[0];AID 格式不合法时置 undefined。
3981
+ ownerAid: (() => {
3982
+ const raw = selfAid ? getFirstStaticAgentOwner(selfAid) : undefined;
3983
+ if (!raw)
3984
+ return undefined;
3985
+ return isValidAid(raw) ? raw : undefined;
3986
+ })(),
3987
+ // 记忆系统开关变量(来自 effectiveAgentConfig.memory,默认全部启用)。
3988
+ // enableRelation 是总开关:relation 是所有记忆的唯一出生地。
3989
+ // memory.enableRelation 在 relation-config schema 里声明了 x-merge-logic: AND,
3990
+ // mergeLayers 会自动处理 agent 级与 relation 级的 AND 合并,无需手动读两层。
3991
+ memoryEnableRelation: effectiveAgentConfig?.memory?.enableRelation ?? true,
3992
+ memoryEnableAgent: effectiveAgentConfig?.memory?.enableAgent ?? true,
3993
+ memoryEnableOwner: effectiveAgentConfig?.memory?.enableOwner ?? true,
3994
+ // 会话原型与整理模式(manifest 的 when 条件靠它们选段)
3995
+ sessionType: renderedSessionType,
3996
+ memoryMode,
3633
3997
  selfName: selfName || undefined,
3634
3998
  agentMdPath: selfAid ? agentMdPath(selfAid) : undefined,
3635
3999
  lifecycle,
3636
4000
  isBootstrapping,
3637
4001
  hasPersona: !!persona,
3638
- hasWorkingMemory: !!working,
3639
4002
  peerId: peerIdRaw || undefined,
3640
4003
  peerKey,
3641
4004
  peerName: peerName || undefined,
@@ -3686,9 +4049,10 @@ export class ResponseEngine {
3686
4049
  // 默认值不在此硬编码,出厂默认由模式 schema 声明、经 buildState 落入 proactive.*。
3687
4050
  proactivePreTool1stMsgChk: proactive?.preTool1stMsgChk ?? false,
3688
4051
  proactiveToolUseReminder: proactive?.toolUseReminder ?? false,
3689
- proactiveFirstSendRequired: proactive?.firstSendRequired ?? false,
4052
+ // 明确续跑已表态时,提示词和运行时门禁都不再要求重复开场。
4053
+ proactiveFirstSendRequired: proactive?.firstSendRequired === true && !proactive.firstToolDone,
3690
4054
  proactiveToolReportRequired: proactive?.toolReportRequired ?? false,
3691
- proactiveToolReportInterval: proactive?.toolReportInterval ?? 10,
4055
+ proactiveToolReportInterval: proactive?.toolReportInterval ?? 20,
3692
4056
  proactiveSendTargetLabel: chatType === 'group' ? '群里' : '对方',
3693
4057
  readonly: effectivePermissionMode === 'readonly',
3694
4058
  baseAgent: normalizedBaseagent.canonical,
@@ -3704,15 +4068,20 @@ export class ResponseEngine {
3704
4068
  },
3705
4069
  sessionId: session.id,
3706
4070
  };
3707
- // 按会话原型(sessionType)选 manifest 文件:config.sessionManifests 映射,缺省回退主 manifest。
3708
- const sessionType = session.sessionType ?? 'main';
4071
+ // 确保技能索引是最新的(懒加载机制)
4072
+ if (selfAid) {
4073
+ const { ensureSkillIndexUpToDate } = await import('../skill/startup.js');
4074
+ ensureSkillIndexUpToDate(resolveAgentDir(selfAid), selfAid);
4075
+ }
4076
+ // 按会话原型(sessionType)选 manifest 文件:config.sessionManifests 覆盖 → 内置映射 → 主 manifest。
4077
+ const sessionType = renderedSessionType;
3709
4078
  const sessionManifests = this.agentRegistry?.resolveByChannel(channelKey)?.config?.sessionManifests;
3710
4079
  // Bootstrap always uses the lifecycle-gated main manifest. A custom
3711
4080
  // session manifest (for example the auxiliary manifest with `always`
3712
4081
  // sections) must not reintroduce normal rules or command context.
3713
4082
  const manifestFile = isBootstrapping
3714
- ? 'eck_manifest.json'
3715
- : sessionManifests?.[sessionType] ?? 'eck_manifest.json';
4083
+ ? DEFAULT_MANIFEST_FILE
4084
+ : resolveSessionManifest(sessionType, sessionManifests);
3716
4085
  const kitContext = renderKitSections(kitCtx, manifestFile);
3717
4086
  effectiveSystemPrompt = [options?.systemPromptAppend, kitContext].filter(Boolean).join('\n') || undefined;
3718
4087
  // ── Stats: context_breakdown 旁路采集(各段估算 token 数,字符数/4 近似) ──
@@ -3747,9 +4116,9 @@ export class ResponseEngine {
3747
4116
  catch { /* non-fatal */ }
3748
4117
  // 消息渲染层:用 message manifest 逐条渲染(时间 + 群聊发送者),组装成最终正文。
3749
4118
  // 单条消息构造单元素 items;批量合并的消息 message.items 已由队列填充。
3750
- const hasContent = message.content.trim() || (message.items && message.items.length > 0);
4119
+ const hasContent = !!message.observerHintId || message.content.trim() || !!message.images?.length || (message.items && message.items.length > 0);
3751
4120
  if (hasContent) {
3752
- const rawPeerItems = message.items && message.items.length > 0
4121
+ const rawPeerItems = message.source === 'owner-inject' ? [] : message.items && message.items.length > 0
3753
4122
  ? message.items
3754
4123
  : [{
3755
4124
  peerId: message.peerId, peerName: peerName || undefined,
@@ -3776,10 +4145,17 @@ export class ResponseEngine {
3776
4145
  }
3777
4146
  return rawPeerItems;
3778
4147
  })();
3779
- // 观察者插话(v0.3):消费 (对端, thread) 的待用提示,包成 owner-hint item 排在对端消息前。
3780
- // 一次性语义:consumeOwnerHints 读取并删除(见 pending-hints.ts)。在 try 外消费,
3781
- // 这样即便 renderMessageBody 抛错走 raw 兜底,也把提示原文拼进去——绝不静默丢提示。
3782
- const hintItems = this.consumeOwnerHints(session, message);
4148
+ // 将匹配的 pending hint 暂时 claim,排在真实消息之前;渲染或启动
4149
+ // 失败时释放 claim,只有 runner 接受 prompt 后才持久化 consumed。
4150
+ const hintResult = this.consumeOwnerHints(session, message);
4151
+ ownerHintClaim = hintResult.claim;
4152
+ const hintItems = hintResult.items;
4153
+ if (message.source === 'owner-inject' && hintItems.length === 0) {
4154
+ if (turnLease)
4155
+ await this.turnCoordinator.finish(session, turnLease, 'cancelled', 'owner_hint_unavailable');
4156
+ this.sessionManager.clearProcessingIfTask(session.id, taskId);
4157
+ return;
4158
+ }
3783
4159
  const renderItems = hintItems.length > 0 ? [...hintItems, ...peerItems] : peerItems;
3784
4160
  const fallbackContent = (() => {
3785
4161
  if (!message.restartResume?.submitted || peerItems.length === 0)
@@ -3820,8 +4196,16 @@ export class ResponseEngine {
3820
4196
  effectivePrompt = wrapPrompt(composeHintFallback(hintItems, fallbackContent));
3821
4197
  }
3822
4198
  catch (e) {
3823
- logger.warn(`[ResponseEngine] renderMessageBody failed, using raw content: ${e instanceof Error ? e.message : String(e)}`);
3824
- effectivePrompt = wrapPrompt(composeHintFallback(hintItems, fallbackContent));
4199
+ logger.warn(`[ResponseEngine] renderMessageBody failed: ${e instanceof Error ? e.message : String(e)}`);
4200
+ if (ownerHintClaim) {
4201
+ observerHintFailureCode = 'RENDER_FAILED';
4202
+ this.reportObserverHintFailureForClaim(message, session, ownerHintClaim, observerHintFailureCode);
4203
+ releaseHintClaim(ownerHintClaim);
4204
+ ownerHintClaim = undefined;
4205
+ }
4206
+ if (message.source === 'owner-inject')
4207
+ throw e;
4208
+ effectivePrompt = wrapPrompt(fallbackContent);
3825
4209
  }
3826
4210
  if (v2HandoffIds.length > 0 && renderResult?.body.trim())
3827
4211
  handoffPromptRendered = true;
@@ -3835,6 +4219,12 @@ export class ResponseEngine {
3835
4219
  // 空消息防护:在 agent 调用之前检查 prompt 是否为空
3836
4220
  // 防止空消息(或纯空格消息)浪费 API 调用
3837
4221
  if (!effectivePrompt.trim()) {
4222
+ if (ownerHintClaim) {
4223
+ observerHintFailureCode = 'HINT_UNAVAILABLE';
4224
+ this.reportObserverHintFailureForClaim(message, session, ownerHintClaim, observerHintFailureCode);
4225
+ releaseHintClaim(ownerHintClaim);
4226
+ ownerHintClaim = undefined;
4227
+ }
3838
4228
  logger.info(`[ResponseEngine] Skip agent call: empty prompt after render. session=${session.id} task=${taskId}`);
3839
4229
  if (turnLease)
3840
4230
  await this.turnCoordinator.finish(session, turnLease, 'cancelled', 'empty_prompt');
@@ -3870,9 +4260,18 @@ export class ResponseEngine {
3870
4260
  chatType: configChatType,
3871
4261
  selfAid,
3872
4262
  peerId: configActorId || undefined,
4263
+ peerKey,
3873
4264
  peerName: peerName || undefined,
3874
4265
  peerType: message.peerType || session.metadata?.peerType || undefined,
3875
4266
  peerRole,
4267
+ // 会话原型与整理模式:ipc 侧靠 sessionType 拦非整理会话的写命令,
4268
+ // 靠 memoryMode 校验 --scope 是否与本模式匹配。
4269
+ sessionType: renderedSessionType,
4270
+ memoryMode: session.metadata?.memoryMode ?? undefined,
4271
+ memoryScope: session.metadata?.memoryScope ?? undefined,
4272
+ consolidationBatchDir: session.metadata?.consolidationBatchDir
4273
+ ?? session.metadata?.consolidationLogBase
4274
+ ?? undefined,
3876
4275
  permissionMode: effectivePermissionMode,
3877
4276
  ...(fullAccessAuthorization ? {
3878
4277
  processRole: fullAccessAuthorization.processRole,
@@ -3932,6 +4331,7 @@ export class ResponseEngine {
3932
4331
  let attemptTimeout;
3933
4332
  try {
3934
4333
  logger.info(`[ResponseEngine] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${runAttempt} apiRetries=${retryScheduler.apiRetryAttemptsMade} agentSessionId=${runAgentSessionId ?? 'none'} topicBinding=${session.threadId ? 'authoritative' : 'legacy'}`);
4334
+ import('../../utils/msg-diag.js').then(m => m.diagLog('task_start', { aid: session.selfAID ?? '', msgId: message.messageId ?? '', taskId, sessionId: runAgentSessionId ?? undefined, text: message.content ?? '' })).catch(() => { });
3935
4335
  if (fullAccessAuthorization) {
3936
4336
  this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent);
3937
4337
  if (fullAccessAuditStartedAt === undefined) {
@@ -3984,13 +4384,41 @@ export class ResponseEngine {
3984
4384
  modelOverride = { ...modelOverride,
3985
4385
  enableRequestUserInput: effectiveAgentConfig?.baseagents?.codex?.enableRequestUserInput ?? true };
3986
4386
  }
3987
- const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, runAgentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
4387
+ const sourceImages = renderResult?.images.length ? renderResult.images : message.images;
4388
+ const runnerImages = sourceImages?.map(image => ({
4389
+ data: image.data,
4390
+ mimeType: image.mimeType,
4391
+ }));
4392
+ debugRunnerModel = modelOverride?.model || agentModel;
4393
+ const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, runAgentSessionId, runnerImages, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
3988
4394
  // The turn may be superseded while the runner is still creating its
3989
4395
  // backend query. Replay the interrupt after runQuery resolves, when
3990
4396
  // every runner is required to have an addressable cancellation handle.
3991
4397
  if (await this.cancelStaleRunnerStart(agent, session.id, streamKey, turnLease, taskId)) {
3992
4398
  throw new Error('RUNNER_START_SUPERSEDED');
3993
4399
  }
4400
+ if (ownerHintClaim) {
4401
+ const accepted = ownerHintClaim;
4402
+ if (commitHintClaim(accepted)) {
4403
+ for (const hint of accepted.hints) {
4404
+ try {
4405
+ this.observerHintConsumed?.(hint, {
4406
+ channelKey: session.channel, channelId: accepted.channelId,
4407
+ chatType: accepted.chatType, threadId: accepted.threadId || undefined,
4408
+ }, {
4409
+ action: hint.promoteRequestId ? 'promote' : 'add',
4410
+ requestId: hint.promoteRequestId ?? message.observerHintRequestId ?? (hint.mode === 'interrupt' ? hint.id : undefined),
4411
+ });
4412
+ }
4413
+ catch (error) {
4414
+ logger.warn(`[ResponseEngine] observer hint status failed id=${hint.id}: ${error}`);
4415
+ }
4416
+ }
4417
+ }
4418
+ else
4419
+ releaseHintClaim(accepted);
4420
+ ownerHintClaim = undefined;
4421
+ }
3994
4422
  agent.registerStream(streamKey, stream);
3995
4423
  streamRegistered = true;
3996
4424
  if (shouldTimeoutRetryAttempt) {
@@ -4000,7 +4428,11 @@ export class ResponseEngine {
4000
4428
  const processAttempt = this.processEventStream(stream, session, agent, renderer, (eventType, toolName) => {
4001
4429
  resetTimer(eventType, toolName);
4002
4430
  attemptTimeout?.reset();
4003
- }, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, selfAid, async (request) => {
4431
+ }, shouldSuppress, proactive, resolvedMode ? {
4432
+ mode: resolvedMode.mode,
4433
+ state: modeState,
4434
+ sessionState: resolvedMode.context.sessionState,
4435
+ } : undefined, taskCausation, turnLease, effectivePermissionMode, selfAid, async (request) => {
4004
4436
  if (!this.asyncQuestions)
4005
4437
  throw new Error('Async question service is unavailable');
4006
4438
  if (permissionContext.approvalInteractionPolicy === 'deny') {
@@ -4166,6 +4598,7 @@ export class ResponseEngine {
4166
4598
  }
4167
4599
  }
4168
4600
  catch (error) {
4601
+ observerHintFailureCode = this.observerHintFailureCodeForError(error);
4169
4602
  if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && canCompactAgent(agent)) {
4170
4603
  streamResult = await this.recoverFromContextLimit(streamResult, {
4171
4604
  streamKey,
@@ -4420,6 +4853,11 @@ export class ResponseEngine {
4420
4853
  // 最终回复文本:suppressed 模式或无 text 事件时需要兜底添加
4421
4854
  const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
4422
4855
  const visibleFinalReplyText = stripContextLimitErrorLines(finalReplyText);
4856
+ const isDiagnosticFallback = streamResult.isError && !streamResult.hasReceivedText;
4857
+ const isTriggerNoop = message.source === 'trigger'
4858
+ && typeof message.triggerMeta?.noopSentinel === 'string'
4859
+ && message.triggerMeta.noopSentinel.length > 0
4860
+ && visibleFinalReplyText.trim() === message.triggerMeta.noopSentinel;
4423
4861
  if (!this.turnCoordinator.isCurrent(turnLease)) {
4424
4862
  agent.cleanupStream(streamKey);
4425
4863
  this.sessionManager.clearProcessingIfTask(session.id, taskId);
@@ -4431,7 +4869,7 @@ export class ResponseEngine {
4431
4869
  await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_turn', taskCausation);
4432
4870
  return;
4433
4871
  }
4434
- if (visibleFinalReplyText) {
4872
+ if (visibleFinalReplyText && !isDiagnosticFallback && !isTriggerNoop) {
4435
4873
  // `hasReceivedText` 是任务级计数。工具调用前的文字已经显示,并不表示最后一轮
4436
4874
  // (通常来自 complete.result)的文字也已显示;用本轮投影标记避免终态丢失。
4437
4875
  if (shouldSuppress() || !streamResult.hasProjectedCurrentReplyText) {
@@ -4447,6 +4885,7 @@ export class ResponseEngine {
4447
4885
  }
4448
4886
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
4449
4887
  logger.info(`[ResponseEngine] session ${session.id} processing cleared task=${taskId}`);
4888
+ import('../../utils/msg-diag.js').then(m => m.diagLog('task_done', { aid: session.selfAID ?? '', msgId: message.messageId ?? '', taskId, text: message.content ?? '' })).catch(() => { });
4450
4889
  // 降级模型回复末尾追加标记(代码层硬注入,不依赖模型输出)
4451
4890
  if (usedFallback) {
4452
4891
  const curFbState = this.modelFallbackMap.get(session.id);
@@ -4518,6 +4957,7 @@ export class ResponseEngine {
4518
4957
  // 这不是真正的失败,不应把诊断串暴露给用户,也不计入错误统计。
4519
4958
  const isExpectedInterrupt = isNonErrorInterruptReason(interruptReason)
4520
4959
  || isExpectedTerminalInterrupt(streamResult.terminalReason);
4960
+ taskActivityStatus = isExpectedInterrupt ? 'interrupted' : 'error';
4521
4961
  if (!isExpectedInterrupt && !permissionTerminalFailure) {
4522
4962
  const terminalMetadata = {
4523
4963
  errorType: rawSubtype,
@@ -4607,6 +5047,7 @@ export class ResponseEngine {
4607
5047
  }
4608
5048
  else {
4609
5049
  // 真正的成功
5050
+ taskActivityStatus = 'completed';
4610
5051
  if (!this.turnCoordinator.canPublish(turnLease)) {
4611
5052
  logger.info(`[ResponseEngine] Suppressing stale success path: session=${session.id} task=${taskId}`);
4612
5053
  await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_turn', taskCausation);
@@ -4809,6 +5250,7 @@ export class ResponseEngine {
4809
5250
  durationMs: Date.now() - startTime,
4810
5251
  agentName: agentNameForStats,
4811
5252
  numTurns: streamResult.numTurns,
5253
+ encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
4812
5254
  timestamp: Date.now(),
4813
5255
  causation: taskCausation,
4814
5256
  });
@@ -4847,6 +5289,15 @@ export class ResponseEngine {
4847
5289
  });
4848
5290
  }
4849
5291
  catch (error) {
5292
+ if (error instanceof Error && (error.message === 'TOTAL_EXECUTION_TIMEOUT' || error.message === 'SDK_TIMEOUT')) {
5293
+ taskActivityStatus = 'timeout';
5294
+ }
5295
+ else if (isNonErrorInterruptReason(this.interruptedSessions.get(session.id))) {
5296
+ taskActivityStatus = 'interrupted';
5297
+ }
5298
+ else {
5299
+ taskActivityStatus = 'error';
5300
+ }
4850
5301
  fullAccessExecutionFailed = true;
4851
5302
  fullAccessExecutionFailureReason = error instanceof Error ? error.message : String(error);
4852
5303
  const authoritativeTimeoutError = timeoutControl?.claim();
@@ -5064,6 +5515,37 @@ export class ResponseEngine {
5064
5515
  }
5065
5516
  }
5066
5517
  finally {
5518
+ if (taskActivityStore) {
5519
+ try {
5520
+ taskActivityStore.updateStatus(taskId, taskActivityStatus);
5521
+ }
5522
+ catch (error) {
5523
+ logger.warn(`[ResponseEngine] task activity status update failed: ${error instanceof Error ? error.message : String(error)}`);
5524
+ }
5525
+ }
5526
+ if (turnLease && resolvedMode?.mode.onTurnEnd) {
5527
+ try {
5528
+ const current = this.turnCoordinator.current(session);
5529
+ await resolvedMode.mode.onTurnEnd({
5530
+ session,
5531
+ state: modeState,
5532
+ sessionState: resolvedMode.context.sessionState,
5533
+ turn: turnLease,
5534
+ terminal: current.active ? undefined : current.lastTerminal,
5535
+ logger,
5536
+ });
5537
+ }
5538
+ catch (error) {
5539
+ logger.warn(`[ResponseEngine] onTurnEnd failed: session=${session.id} task=${taskId} error=${String(error)}`);
5540
+ }
5541
+ }
5542
+ if (ownerHintClaim) {
5543
+ // A runner error, interruption, or pre-run failure leaves the durable
5544
+ // hint pending for the next attempt.
5545
+ this.reportObserverHintFailureForClaim(message, session, ownerHintClaim, observerHintFailureCode ?? 'RUNNER_START_FAILED');
5546
+ releaseHintClaim(ownerHintClaim);
5547
+ ownerHintClaim = undefined;
5548
+ }
5067
5549
  if (fullAccessAuthorization) {
5068
5550
  const startedAt = fullAccessAuditStartedAt;
5069
5551
  const executed = fullAccessRunnerInvoked;
@@ -5212,6 +5694,30 @@ export class ResponseEngine {
5212
5694
  * 解析会话和项目路径
5213
5695
  */
5214
5696
  async resolveSession(message) {
5697
+ if (message.source === 'owner-inject') {
5698
+ const bound = message.boundSessionId
5699
+ ? await this.sessionManager.getSessionById(message.boundSessionId) : undefined;
5700
+ if (!message.observerHintId || !bound || bound.deletedAt || !bound.agentSessionId
5701
+ || bound.channel !== message.channel || bound.channelId !== message.channelId
5702
+ || bound.selfAID !== message.selfAID || bound.chatType !== message.chatType
5703
+ || (bound.threadId || '') !== (message.threadId || '')) {
5704
+ throw new Error('Observer hint target session is no longer available');
5705
+ }
5706
+ // An enqueued priority hint is bound to exactly the session selected at
5707
+ // submission. A later /new, /renew, thread switch or missing provider
5708
+ // file must never silently resume a stale/different backend session.
5709
+ const current = await this.observerTargetSession({
5710
+ channelKey: bound.channel, channelId: bound.channelId,
5711
+ chatType: bound.chatType, threadId: bound.threadId,
5712
+ selfAid: bound.selfAID,
5713
+ });
5714
+ if (current?.id !== bound.id || current.agentSessionId !== bound.agentSessionId) {
5715
+ throw new Error('Observer hint target session is no longer available');
5716
+ }
5717
+ bound.identity = this.inferDirectSessionIdentity(message, this.agentRegistry?.resolveByChannel(bound.channel));
5718
+ return { session: bound, absoluteProjectPath: path.isAbsolute(bound.projectPath)
5719
+ ? bound.projectPath : path.resolve(process.cwd(), bound.projectPath) };
5720
+ }
5215
5721
  // 话题会话创建时写入创建者和 replyContext(threadId 路由);主会话不写(避免群聊覆盖)
5216
5722
  const metadata = message.threadId
5217
5723
  ? {
@@ -5245,7 +5751,7 @@ export class ResponseEngine {
5245
5751
  // valid mapped owner/admin to role=none at the execution boundary.
5246
5752
  if (resolvedIdentity)
5247
5753
  bound.identity = resolvedIdentity;
5248
- if (bound.threadId) {
5754
+ if (bound.threadId || bound.metadata?.diagnostic) {
5249
5755
  const absoluteProjectPath = path.isAbsolute(bound.projectPath)
5250
5756
  ? bound.projectPath : path.resolve(process.cwd(), bound.projectPath);
5251
5757
  return { session: bound, absoluteProjectPath };
@@ -5271,7 +5777,7 @@ export class ResponseEngine {
5271
5777
  logger.warn(`[ResponseEngine] Bound session ${boundSessionId} not found, falling back to latest`);
5272
5778
  }
5273
5779
  }
5274
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, message.topicName, message.peerId, message.chatType, resolvedBaseagent, message.selfAID, message.channelType, message.peerType, resolvedIdentity, message.lable);
5780
+ const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, message.topicName, message.peerId, message.chatType, resolvedBaseagent, message.selfAID, message.channelType, message.peerType, resolvedIdentity, message.label ?? message.lable, message.topicStatus);
5275
5781
  this.ensureSessionBaseagent(session, resolvedBaseagent);
5276
5782
  // 群名解析:群会话首次取群显示名(group.get),缓存到 metadata,供信封渲染。
5277
5783
  // 渠道私有方法 getGroupName 自带进程缓存 + 容错;取不到不阻塞(groupName 保持空,模板回退 groupId)。
@@ -5398,16 +5904,14 @@ export class ResponseEngine {
5398
5904
  event = normalizeCompleteAgentEvent(event);
5399
5905
  }
5400
5906
  if (event.type === 'tool_result' && event.isError) {
5401
- const classificationMissing = !normalizeToolErrorCode(event.errorCode)
5402
- && event.decisionSource !== 'policy'
5403
- && event.decisionSource !== 'approval';
5404
- const errorCode = classifyToolErrorCode({
5405
- errorCode: event.errorCode,
5406
- decisionSource: event.decisionSource,
5407
- });
5408
- if (event.errorCode !== errorCode) {
5409
- event = { ...event, errorCode, ...(classificationMissing ? { classificationMissing: true } : {}) };
5410
- }
5907
+ const classification = normalizeToolErrorClassification(event);
5908
+ event = {
5909
+ ...event,
5910
+ errorCode: classification.errorCode,
5911
+ errorDetailCode: classification.errorDetailCode,
5912
+ classificationMissing: classification.classificationMissing || event.classificationMissing,
5913
+ classificationConflict: classification.classificationConflict || event.classificationConflict,
5914
+ };
5411
5915
  }
5412
5916
  // 每收到事件重置空闲超时
5413
5917
  const toolName = event.type === 'tool_use' ? event.name : undefined;
@@ -5705,7 +6209,7 @@ export class ResponseEngine {
5705
6209
  this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: visibleText, isFinal: false });
5706
6210
  }
5707
6211
  if (visibleText && !shouldSuppress()) {
5708
- await renderer.addText(visibleText, event.outputTokens, event.turn, event.assistantUuid);
6212
+ await renderer.addText(visibleText, event.outputTokens, event.turn, event.assistantUuid, event.phase);
5709
6213
  hasProjectedCurrentReplyText = true;
5710
6214
  }
5711
6215
  }
@@ -5785,7 +6289,7 @@ export class ResponseEngine {
5785
6289
  if (!shouldSuppress()) {
5786
6290
  if (event.callId)
5787
6291
  toolDescByCallId.set(event.callId, desc);
5788
- renderer.addToolCall(event.name, event.input, event.callId, desc, event.turn, event.outputTokens);
6292
+ renderer.addToolCall(event.name, event.input, event.callId, desc, event.turn, event.outputTokens, event.assistantUuid);
5789
6293
  }
5790
6294
  if (event.callId && isCtlQueueReadCommand(event.name, event.input || {})) {
5791
6295
  ctlQueueReadCallIds.add(event.callId);
@@ -5796,6 +6300,8 @@ export class ResponseEngine {
5796
6300
  modeHooks.mode.onToolUse({
5797
6301
  session,
5798
6302
  state: modeHooks.state,
6303
+ sessionState: modeHooks.sessionState,
6304
+ turn: turnLease,
5799
6305
  toolName: event.name,
5800
6306
  toolInput: event.input || {},
5801
6307
  injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
@@ -5844,14 +6350,12 @@ export class ResponseEngine {
5844
6350
  toolName: event.name,
5845
6351
  isError: event.isError,
5846
6352
  ...(event.isError ? {
5847
- errorCode: classifyToolErrorCode({
5848
- errorCode: event.errorCode,
5849
- decisionSource: event.decisionSource,
5850
- }),
5851
- ...((event.classificationMissing || (!normalizeToolErrorCode(event.errorCode)
5852
- && event.decisionSource !== 'policy'
5853
- && event.decisionSource !== 'approval')) ? { classificationMissing: true } : {}),
6353
+ errorCode: event.errorCode,
6354
+ ...(event.errorDetailCode ? { errorDetailCode: event.errorDetailCode } : {}),
6355
+ ...(event.classificationMissing ? { classificationMissing: true } : {}),
6356
+ ...(event.classificationConflict ? { classificationConflict: true } : {}),
5854
6357
  } : {}),
6358
+ ...(event.exitCode !== undefined ? { exitCode: event.exitCode } : {}),
5855
6359
  ...(event.callId ? { callId: event.callId } : {}),
5856
6360
  ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
5857
6361
  ...(event.requestId ? { requestId: event.requestId } : {}),
@@ -5907,6 +6411,8 @@ export class ResponseEngine {
5907
6411
  await modeHooks.mode.onToolResult({
5908
6412
  session,
5909
6413
  state: modeHooks.state,
6414
+ sessionState: modeHooks.sessionState,
6415
+ turn: turnLease,
5910
6416
  toolName: original.name,
5911
6417
  toolInput: original.input,
5912
6418
  result: event.result,
@@ -5941,10 +6447,10 @@ export class ResponseEngine {
5941
6447
  let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
5942
6448
  // 移除 XML 风格的错误标签
5943
6449
  errorMsg = errorMsg.replace(/<tool_use_error>(.*?)<\/tool_use_error>/gs, '$1');
5944
- renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, event.durationMs, cachedDesc, event);
6450
+ renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, event.durationMs, cachedDesc, event, event.assistantUuid);
5945
6451
  }
5946
6452
  else if (!event.isError && !shouldSuppress()) {
5947
- renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, event.durationMs, cachedDesc);
6453
+ renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, event.durationMs, cachedDesc, undefined, event.assistantUuid);
5948
6454
  }
5949
6455
  }
5950
6456
  // 非终态运行时告警走 status notify,不进入 activity/thought 历史。