evolcore 0.0.39 → 0.0.40

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 (102) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +31 -20
  3. package/bin/ec-safe-output.js +14 -2
  4. package/dist/agents/baseagent-availability.js +1 -0
  5. package/dist/agents/claude-runner.js +32 -46
  6. package/dist/agents/codex-runner.js +51 -27
  7. package/dist/agents/ecagent-runner.js +18 -7
  8. package/dist/agents/gemini-runner.js +1 -2
  9. package/dist/agents/runner-types.js +28 -6
  10. package/dist/aun/aid/client.js +1 -1
  11. package/dist/aun/aid/control-aid.js +23 -98
  12. package/dist/aun/aid/ownership-operation.js +1 -3
  13. package/dist/aun/group/invite-service.js +6 -0
  14. package/dist/aun/msg/p2p.js +8 -12
  15. package/dist/channels/aun.js +66 -48
  16. package/dist/channels/dingtalk.js +8 -2
  17. package/dist/channels/feishu.js +6 -1
  18. package/dist/channels/wecom.js +7 -3
  19. package/dist/cli/agent-create.js +23 -4
  20. package/dist/cli/aid-bind-command.js +4 -1
  21. package/dist/cli/daemon-commands.js +242 -17
  22. package/dist/cli/index.js +25 -13
  23. package/dist/cli/memory-command.js +33 -14
  24. package/dist/cli/restart-monitor.js +35 -13
  25. package/dist/cli/task-context.js +1 -0
  26. package/dist/config/config-field-policy.js +1 -0
  27. package/dist/contracts/ecweb-service-protocol.js +44 -0
  28. package/dist/core/agent-runtime.js +6 -0
  29. package/dist/core/auth/agent-delegation.js +7 -4
  30. package/dist/core/auth/auth-gateway.js +3 -1
  31. package/dist/core/auth/authorization-audit.js +5 -1
  32. package/dist/core/auth/debug-context.js +5 -1
  33. package/dist/core/auth/operation-catalog.js +16 -0
  34. package/dist/core/command/command-handler.js +7 -1
  35. package/dist/core/command/menu-catalog.js +48 -12
  36. package/dist/core/command/menu-handler.js +72 -4
  37. package/dist/core/command/menu-protocol.js +20 -1
  38. package/dist/core/command/slash-gate.js +17 -3
  39. package/dist/core/command/slash-handler.js +90 -7
  40. package/dist/core/event-catalog.js +2 -0
  41. package/dist/core/interaction-router.js +2 -1
  42. package/dist/core/ipc-registration.js +26 -7
  43. package/dist/core/memory/batch-meta.js +2 -0
  44. package/dist/core/memory/consolidation-context.js +42 -51
  45. package/dist/core/memory/consolidation-ops-records.js +39 -7
  46. package/dist/core/memory/consolidation-trigger.js +71 -64
  47. package/dist/core/memory/consolidation.js +226 -20
  48. package/dist/core/memory/memory-writer.js +160 -34
  49. package/dist/core/memory/relation-idle-monitor.js +287 -0
  50. package/dist/core/memory/relation-sweep.js +206 -0
  51. package/dist/core/message/aun-ipc-sender.js +5 -0
  52. package/dist/core/message/message-bridge.js +64 -55
  53. package/dist/core/message/message-log.js +17 -0
  54. package/dist/core/message/message-queue.js +6 -0
  55. package/dist/core/message/outbound.js +70 -0
  56. package/dist/core/message/peer-mode.js +7 -2
  57. package/dist/core/message/response-engine.js +341 -73
  58. package/dist/core/ownership/owner-resolver.js +384 -178
  59. package/dist/core/permission/ec-command-parser.js +19 -3
  60. package/dist/core/permission/tool-policy.js +7 -7
  61. package/dist/core/role/runtime-policy.js +64 -0
  62. package/dist/core/session/session-manager.js +25 -5
  63. package/dist/core/system-session/memory.js +3 -18
  64. package/dist/ecweb/basic-settings-service.js +709 -0
  65. package/dist/ecweb/providers-service.js +172 -0
  66. package/dist/index.js +92 -4
  67. package/dist/ipc.js +78 -11
  68. package/dist/stats/db.js +33 -0
  69. package/dist/stats/index.js +2 -2
  70. package/dist/stats/query.js +246 -0
  71. package/dist/stats/writer.js +41 -1
  72. package/dist/trigger/application-service.js +12 -0
  73. package/dist/trigger/authorization.js +2 -2
  74. package/dist/trigger/history.js +36 -0
  75. package/dist/trigger/scheduler.js +3 -0
  76. package/dist/utils/ecweb-supervisor.js +31 -10
  77. package/dist/utils/error-utils.js +8 -4
  78. package/dist/utils/evolcore-version.js +3 -2
  79. package/dist/utils/host-info.js +52 -0
  80. package/ecagent/dist/harness/agent-harness.js +4 -1
  81. package/kits/docs/evolcore/INDEX.md +1 -1
  82. package/kits/docs/evolcore/agent.md +1 -1
  83. package/kits/docs/evolcore/aid.md +29 -12
  84. package/kits/docs/evolcore/config.md +9 -7
  85. package/kits/docs/evolcore/memory-architecture.md +3 -4
  86. package/kits/docs/evolcore/memory-consolidation.md +28 -7
  87. package/kits/docs/evolcore/memory-tags.md +1 -1
  88. package/kits/docs/evolcore/memory.md +1 -1
  89. package/kits/docs/evolcore/menu-client.md +1 -1
  90. package/kits/docs/evolcore/menu-protocol.md +6 -0
  91. package/kits/docs/evolcore/msg.md +1 -1
  92. package/kits/eck_manifest.json +3 -3
  93. package/kits/rules/04-relation.md +3 -2
  94. package/kits/schemas/agent-config.schema.13.json +5 -1
  95. package/kits/templates/memory-consolidation/generalize.md +32 -4
  96. package/kits/templates/memory-consolidation/notes.md +2 -2
  97. package/kits/templates/memory-consolidation/promote.md +10 -2
  98. package/kits/templates/system-fragments/baseagent.md +5 -0
  99. package/kits/templates/system-fragments/memory.md +12 -3
  100. package/kits/templates/system-fragments/session.md +1 -1
  101. package/kits/{rules/07-memory-and-thread.md → templates/system-fragments/thread.md} +17 -80
  102. package/package.json +3 -3
@@ -0,0 +1,206 @@
1
+ /**
2
+ * relation-sweep.ts — 关系整理链
3
+ *
4
+ * 一个关系空闲 / 兜底到期后,对它依次跑:
5
+ *
6
+ * notes → generalize → promote(仅 owner 关系)→ profile → forget(关系层)
7
+ *
8
+ * 每一步在上一步收尾之后才判定要不要做(notes 新产出的 to-generalize 要被第 2 步看到),
9
+ * 没有候选的步骤跳过,一个候选都没有就一个会话都不起。所有步骤走同一个整理队列,同 agent 串行。
10
+ * 触发来源记为 `relation-idle`,静默不反馈。
11
+ *
12
+ * 链中途收到新入站不中断:兜底本就发生在聊天进行中,主会话只写 pending,链写 consolidated。
13
+ *
14
+ * 群聊 notes 会按发言人用 `--peer` 把记忆写进发言人自己的关系。那些关系不一定有入站
15
+ * (发言人从不私聊),所以链结束时把它们交给 onSweepFinished 的 touchedPeerKeys,
16
+ * 由调用方各自补一轮整理(不再继续传播)。
17
+ */
18
+ import { logger } from '../../utils/logger.js';
19
+ import { RELATION_SWEEP_ORDER, isRelationModeDue } from './consolidation-trigger.js';
20
+ /** 单步整理会话迟迟不收尾时,链最多等这么久就放弃(整理会话自身 15 分钟超时,留余量)。 */
21
+ const DEFAULT_STEP_WATCHDOG_MS = 30 * 60_000;
22
+ /**
23
+ * 写进会话 metadata 的 relationSweepId 带上步骤序号:`<链 id>#<步骤序号>`。
24
+ * 看门狗放弃的那一步,会话之后才收尾时,回调里的序号对不上当前步骤,直接忽略,
25
+ * 不会被误认成下一步的收尾而让链提前推进。
26
+ */
27
+ function stepTag(sweepId, step) {
28
+ return `${sweepId}#${step}`;
29
+ }
30
+ function parseStepTag(tag) {
31
+ const idx = tag.lastIndexOf('#');
32
+ if (idx <= 0)
33
+ return undefined;
34
+ const step = Number(tag.slice(idx + 1));
35
+ if (!Number.isInteger(step))
36
+ return undefined;
37
+ return { sweepId: tag.slice(0, idx), step };
38
+ }
39
+ let sweepSeq = 0;
40
+ export class RelationSweepRunner {
41
+ deps;
42
+ sweeps = new Map();
43
+ isModeDue;
44
+ stepWatchdogMs;
45
+ constructor(deps) {
46
+ this.deps = deps;
47
+ this.isModeDue = deps.isModeDue ?? isRelationModeDue;
48
+ this.stepWatchdogMs = deps.stepWatchdogMs ?? DEFAULT_STEP_WATCHDOG_MS;
49
+ }
50
+ /** 开始一条整理链。保证之后恰好调用一次 onSweepFinished。 */
51
+ start(request) {
52
+ const id = `sweep-${Date.now().toString(36)}-${(++sweepSeq).toString(36)}`;
53
+ const state = {
54
+ id,
55
+ request,
56
+ remaining: [...RELATION_SWEEP_ORDER],
57
+ ran: [],
58
+ touched: new Set(),
59
+ step: 0,
60
+ };
61
+ this.sweeps.set(id, state);
62
+ void this.deps.resolveOwner(request.selfAid)
63
+ .catch(() => undefined)
64
+ .then(ownerAid => {
65
+ state.ownerAid = ownerAid;
66
+ return this.advance(state);
67
+ });
68
+ }
69
+ /** 整理会话收尾回调(来自 finalizer 的 onSessionFinalized)。 */
70
+ onSessionFinalized(info) {
71
+ const state = this.currentStep(info.relationSweepId, info.sessionId);
72
+ if (!state)
73
+ return;
74
+ if (state.request.propagate) {
75
+ for (const peerKey of info.touchedPeerKeys) {
76
+ if (peerKey !== state.request.peerKey)
77
+ state.touched.add(peerKey);
78
+ }
79
+ }
80
+ this.stepDone(state);
81
+ }
82
+ /** 排队后拉起失败的通知(来自 ConsolidationQueue.setLaunchFailedListener)。 */
83
+ onLaunchFailed(params) {
84
+ const state = this.currentStep(params.relationSweepId);
85
+ if (!state)
86
+ return;
87
+ this.stepDone(state);
88
+ }
89
+ /** 回调对应的是某条链**正在等的那一步**时返回该链;过期步骤、无关会话返回 undefined。 */
90
+ currentStep(tag, sessionId) {
91
+ if (tag) {
92
+ const parsed = parseStepTag(tag);
93
+ if (!parsed)
94
+ return undefined;
95
+ const state = this.sweeps.get(parsed.sweepId);
96
+ if (!state || !state.waiting || state.step !== parsed.step)
97
+ return undefined;
98
+ return state;
99
+ }
100
+ // 收尾时读不到会话 metadata(会话记录已丢失):按会话 ID 认领
101
+ if (!sessionId)
102
+ return undefined;
103
+ for (const state of this.sweeps.values()) {
104
+ if (state.waiting && state.waitingSessionId === sessionId)
105
+ return state;
106
+ }
107
+ return undefined;
108
+ }
109
+ /** 测试与诊断用:在跑的链数量。 */
110
+ get activeCount() {
111
+ return this.sweeps.size;
112
+ }
113
+ stepDone(state) {
114
+ if (state.watchdog)
115
+ clearTimeout(state.watchdog);
116
+ state.watchdog = undefined;
117
+ state.waiting = undefined;
118
+ state.waitingSessionId = undefined;
119
+ void this.advance(state);
120
+ }
121
+ async advance(state) {
122
+ const { selfAid, peerKey, route } = state.request;
123
+ while (state.remaining.length > 0) {
124
+ const mode = state.remaining.shift();
125
+ const agent = this.deps.getAgent(selfAid);
126
+ const memoryConfig = agent?.config?.memory;
127
+ let due = false;
128
+ try {
129
+ due = this.isModeDue({ mode, selfAid, peerKey, ownerAid: state.ownerAid, config: memoryConfig });
130
+ }
131
+ catch (error) {
132
+ logger.warn(`[Memory] 关系整理链判定失败(peerKey=${peerKey},mode=${mode}): ${error?.message}`);
133
+ }
134
+ if (!due)
135
+ continue;
136
+ if (!agent?.baseagent)
137
+ break;
138
+ const taskCtx = {
139
+ selfAid,
140
+ peerKey,
141
+ baseagent: agent.baseagent,
142
+ ...(route ? {
143
+ channel: route.channel,
144
+ channelId: route.channelId,
145
+ peerId: route.peerId,
146
+ chatType: route.chatType,
147
+ ...(route.channelKey ? { channelKey: route.channelKey } : {}),
148
+ ...(route.threadId ? { threadId: route.threadId } : {}),
149
+ } : {}),
150
+ };
151
+ // 先挂上等待标记再入队:会话可能在 enqueue 返回前就收尾,收尾回调要能认出这一步
152
+ const token = ++state.step;
153
+ state.waiting = mode;
154
+ let launched = null;
155
+ try {
156
+ launched = await this.deps.enqueue({
157
+ taskCtx,
158
+ mode,
159
+ triggeredBy: 'relation-idle',
160
+ ownerAid: state.ownerAid,
161
+ forgetAfterDays: memoryConfig?.forgetAfterDays,
162
+ ...(mode === 'forget' ? { scopeOverride: 'relation' } : {}),
163
+ relationSweepId: stepTag(state.id, token),
164
+ });
165
+ }
166
+ catch (error) {
167
+ logger.warn(`[Memory] 关系整理链入队失败(peerKey=${peerKey},mode=${mode}): ${error?.message}`);
168
+ }
169
+ if (state.step !== token || state.waiting !== mode) {
170
+ // enqueue 返回前这一步已经收尾,后续由那次 stepDone 推进
171
+ if (launched)
172
+ state.ran.push(mode);
173
+ return;
174
+ }
175
+ if (!launched) {
176
+ // 立即拉起失败:这一步作废,继续下一步
177
+ state.waiting = undefined;
178
+ continue;
179
+ }
180
+ state.ran.push(mode);
181
+ if (launched.executionSessionId)
182
+ state.waitingSessionId = launched.executionSessionId;
183
+ state.watchdog = setTimeout(() => {
184
+ logger.warn(`[Memory] 关系整理链等待收尾超时,跳过该步(peerKey=${peerKey},mode=${mode})`);
185
+ this.stepDone(state);
186
+ }, this.stepWatchdogMs);
187
+ state.watchdog.unref?.();
188
+ return;
189
+ }
190
+ this.finish(state);
191
+ }
192
+ finish(state) {
193
+ if (!this.sweeps.delete(state.id))
194
+ return;
195
+ const { selfAid, peerKey } = state.request;
196
+ if (state.ran.length > 0) {
197
+ logger.info(`[Memory] 关系整理链结束(aid=${selfAid},peerKey=${peerKey}):${state.ran.join(' → ')}`);
198
+ }
199
+ try {
200
+ this.deps.onSweepFinished({ selfAid, peerKey, ran: state.ran, touchedPeerKeys: [...state.touched] });
201
+ }
202
+ catch (error) {
203
+ logger.warn(`[Memory] 关系整理链结束回调失败(peerKey=${peerKey}): ${error?.message}`);
204
+ }
205
+ }
206
+ }
@@ -15,6 +15,7 @@ import { resolveThreadRouteTarget } from '../thread/thread-route.js';
15
15
  import { isTopicThreadId, parseMainTopicId, safeFormatMainTopicId, sessionLocalTopicId } from '../thread/topic-id.js';
16
16
  import { ThreadService } from '../thread/thread-service.js';
17
17
  import { getAunRequestRegistry, requestTtlSeconds } from './aun-request-registry.js';
18
+ const TASK_ID_MESSAGE_PAYLOAD_TYPES = new Set(['text', 'link', 'image', 'video', 'voice', 'file']);
18
19
  /** payload.debug 只是诊断字段:取不到时照常发送,不带 debug。 */
19
20
  async function resolveOutboundDebug(resolver, sessionId) {
20
21
  if (!resolver || !sessionId)
@@ -408,6 +409,10 @@ export function registerAunIpcSender({ ipcServer, channelInstances, agentDelegat
408
409
  if (!payload) {
409
410
  return { ok: false, error: 'message payload is required', code: 'INVALID_PAYLOAD' };
410
411
  }
412
+ if (params.rawPayload !== true && runtime?.taskId
413
+ && typeof payload.type === 'string' && TASK_ID_MESSAGE_PAYLOAD_TYPES.has(payload.type)) {
414
+ payload = { ...payload, task_id: runtime.taskId };
415
+ }
411
416
  // 托管 argv 通道(runManagedSend)先前不带 log 元数据:群聊 sendDaemonGroupMsg
412
417
  // 自身会写日志,私聊 sendDaemonMsg 只在 args.log 存在时才写,于是同一份
413
418
  // `ec msg send` 在私聊下不落 messages.jsonl,话题历史里查不到已发出的消息。
@@ -19,7 +19,7 @@ import { findContactRequestNote, expirePendingContactRequests, reviewContactRequ
19
19
  import { isValidAid } from '../../aun/aid/validation.js';
20
20
  import { PeerIdentityCache, formatPeerKey } from '../relation/peer-identity.js';
21
21
  import { ownerResolver } from '../ownership/owner-resolver.js';
22
- import { isTopicSession, sessionLocalTopicId } from '../thread/topic-id.js';
22
+ import { isMainSession, isTopicSession, sessionLocalTopicId } from '../thread/topic-id.js';
23
23
  import { resolveEffective } from '../../config/config-manager.js';
24
24
  import { createDecisionInferenceProvider } from '../inference/text-inference.js';
25
25
  import { AutoMentionGate } from './auto-mention-gate.js';
@@ -39,8 +39,7 @@ import { recordCausationSpan } from '../causation/audit.js';
39
39
  import { renderActionAsText } from '../interaction-router.js';
40
40
  import { planApprovalRoute } from '../permission/approval-gateway.js';
41
41
  import { guardGroupSlashCommand, parseFullAccessCommand } from '../command/slash-gate.js';
42
- import { createAgentPolicyAuthorization } from '../role/runtime-policy.js';
43
- import { auditFullAccessEvent } from '../auth/authorization-audit.js';
42
+ import { createAgentPolicyAuthorization, createSessionFullAccessAuthorization, hasSessionFullAccessOverride } from '../role/runtime-policy.js';
44
43
  import { DIAGNOSTIC_AGENT_AID, DiagnosticSessionRouter, parseDiagnosticEvent, isDiagnosticSessionActive } from '../command/diag-service.js';
45
44
  import { consumeAdmissionReplyBudget, clearAdmissionReplyBudgets, evaluateInboundAdmissionPreflight, evaluateOrdinaryInboundAdmission, } from './message-admission.js';
46
45
  const CONTACT_REJECTION_NOTICE = '[rejection] Not accepting messages from you.';
@@ -279,6 +278,43 @@ export class MessageBridge {
279
278
  return { handled: true, status: 'not-first-owner', reply: '仅 Agent 配置中的首个 Owner 可在 AUN 私聊发送 /bind 发起 ownership 绑定。' };
280
279
  }
281
280
  try {
281
+ const current = await ownerResolver.refresh(selfAid, { force: true });
282
+ if (current.consensusStatus === 'consensus') {
283
+ return current.ownerAid === actorId
284
+ ? { handled: true, status: 'already-bound', reply: '协议 Owner 已绑定,无需重复操作。' }
285
+ : {
286
+ handled: true,
287
+ status: 'owner-already-bound',
288
+ reply: `该 Agent 已绑定协议 Owner ${current.ownerAid ?? '(身份未知)'},暂不支持重新绑定。`,
289
+ };
290
+ }
291
+ const taskStatus = current.task?.status;
292
+ const hasActiveSubmission = taskStatus === 'pending'
293
+ || (['requesting', 'submitted'].includes(current.requestStatus ?? '')
294
+ && (!taskStatus || taskStatus === 'completed'));
295
+ const interruptedSubmissionNeedsCheck = current.requestStatus === 'interrupted'
296
+ && current.consensusStatus === 'single_sided';
297
+ if (hasActiveSubmission || interruptedSubmissionNeedsCheck) {
298
+ const requestedOwner = current.requestedOwnerAid ?? current.task?.owner_aid;
299
+ if (interruptedSubmissionNeedsCheck) {
300
+ return {
301
+ handled: true,
302
+ status: 'ownership-unverified',
303
+ reply: '上一笔协议 Owner 绑定请求状态尚未确认,请先核验状态,暂不重复发起。',
304
+ };
305
+ }
306
+ return requestedOwner && requestedOwner !== actorId
307
+ ? {
308
+ handled: true,
309
+ status: 'ownership-pending-conflict',
310
+ reply: `已有发往 ${requestedOwner} 的协议 Owner 绑定请求正在处理中,请先完成或核验该请求。`,
311
+ }
312
+ : {
313
+ handled: true,
314
+ status: 'ownership-pending',
315
+ reply: '协议 Owner 绑定请求正在处理中,请在 Evol 中确认;无需重复发送 /bind。',
316
+ };
317
+ }
282
318
  const state = await ownerResolver.bind(selfAid, actorId);
283
319
  const alreadyBound = state.consensusStatus === 'consensus' && state.ownerAid === actorId;
284
320
  if (!alreadyBound && state.task && ['rejected', 'stopped', 'error'].includes(state.task.status)) {
@@ -1150,9 +1186,9 @@ export class MessageBridge {
1150
1186
  return;
1151
1187
  }
1152
1188
  }
1153
- // /fa is an execution prefix, not a session-mode switch or a normal
1154
- // quick command. Only this trusted bridge path can construct the
1155
- // internal per-call authorization carried into ResponseEngine.
1189
+ // /fa is a persistent current-session control. The slash handler owns
1190
+ // its storage and authorization; the bridge only enforces source and
1191
+ // group boundaries before normal command routing.
1156
1192
  const fullAccessCommand = parseFullAccessCommand(content);
1157
1193
  if (fullAccessCommand.matched) {
1158
1194
  const groupGuard = guardGroupSlashCommand(content, chatType);
@@ -1160,51 +1196,14 @@ export class MessageBridge {
1160
1196
  await this.sendSlashControlReply(channelName, msg, sendReply, groupGuard, adapter);
1161
1197
  return;
1162
1198
  }
1163
- const reject = async (text) => {
1164
- logger.warn(`[MessageBridge] fullaccess command rejected: channel=${channelName} `
1165
- + `actor=${authSubject.principalId ?? authSubject.actorId ?? '<none>'} reason=${text}`);
1166
- auditFullAccessEvent({
1167
- event: 'fullaccess.command.rejected',
1168
- source: 'fullaccess-command',
1169
- decision: 'deny',
1170
- actorId: authSubject.principalId ?? authSubject.actorId,
1171
- processRole: authSubject.processRole,
1172
- messageId: msg.messageId,
1173
- reason: text,
1174
- });
1175
- await sendReply(msg.channelId, `❌ ${text}`, msg.replyContext);
1176
- };
1177
1199
  if (msg.source !== undefined && msg.source !== 'user') {
1178
- await reject('fullaccess 只能由用户直接发送的 /fa 或 /fullaccess 命令发起');
1179
- return;
1180
- }
1181
- if (!fullAccessCommand.prompt) {
1182
- await reject('用法:/fa <提示词> 或 /fullaccess <提示词>');
1200
+ await sendReply(msg.channelId, '❌ /fa 只能由用户直接发送', msg.replyContext);
1183
1201
  return;
1184
1202
  }
1185
- if (!authSubject.isDaemonOwner || !authSubject.principalId) {
1186
- await reject('仅可信 daemon-owner 可以使用 fullaccess');
1203
+ if (fullAccessCommand.action === 'invalid') {
1204
+ await sendReply(msg.channelId, '❌ 用法:/fa、/fa on 或 /fa off', msg.replyContext);
1187
1205
  return;
1188
1206
  }
1189
- if (!msg.messageLogContent)
1190
- msg.messageLogContent = content;
1191
- content = fullAccessCommand.prompt;
1192
- executionPermissionOverride = {
1193
- permissionMode: 'fullaccess',
1194
- processRole: 'fullaccess-run',
1195
- dataScope: 'daemon',
1196
- source: 'fullaccess-command',
1197
- authorizedBy: authSubject.principalId,
1198
- };
1199
- logger.warn(`[MessageBridge] fullaccess command accepted: channel=${channelName} `
1200
- + `actor=${authSubject.principalId} message=${msg.messageId ?? '<none>'}`);
1201
- auditFullAccessEvent({
1202
- event: 'fullaccess.command.accepted',
1203
- source: 'fullaccess-command',
1204
- actorId: authSubject.principalId,
1205
- processRole: authSubject.processRole,
1206
- messageId: msg.messageId,
1207
- });
1208
1207
  }
1209
1208
  // 0. 仅阻止协议遥测进入 session / model / messages.jsonl;协议本身已在
1210
1209
  // channel/main/events/AUN/handoff 专用链路中处理和记录。
@@ -1248,6 +1247,7 @@ export class MessageBridge {
1248
1247
  // 2. 命令快速路径(去除引用前缀后检查,兼容话题中引用上文的情况)
1249
1248
  const contentForCmd = content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
1250
1249
  const cmdContent = contentForCmd || content;
1250
+ const parsedFullAccessControl = parseFullAccessCommand(cmdContent);
1251
1251
  let autoMode = false;
1252
1252
  if (resolvedChannelType === 'aun' && chatType === 'group' && selfAid && msg.source !== 'handoff') {
1253
1253
  try {
@@ -1281,7 +1281,10 @@ export class MessageBridge {
1281
1281
  // A prompt such as `/fa /status` must not be re-routed as the
1282
1282
  // ordinary `/status` quick command after the prefix is removed.
1283
1283
  && !executionPermissionOverride
1284
- && !parseFullAccessCommand(cmdContent).matched
1284
+ && (!parsedFullAccessControl.matched
1285
+ || parsedFullAccessControl.action === 'status'
1286
+ || parsedFullAccessControl.action === 'on'
1287
+ || parsedFullAccessControl.action === 'off')
1285
1288
  && this.cmdHandler.isCommand(cmdContent);
1286
1289
  if (isCmd && /^\/new(?:\s|$)/i.test(cmdContent)) {
1287
1290
  // /new changes the active main-session boundary. Serialize it with
@@ -1333,14 +1336,6 @@ export class MessageBridge {
1333
1336
  return sendReply(msg.channelId, text, msg.replyContext);
1334
1337
  }, msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext, msg.messageId, msg.selfAID, authSubject))
1335
1338
  return;
1336
- // Commands retain their normal route. Only authenticated direct model
1337
- // input can obtain the Agent policy's per-call authorization.
1338
- if (!executionPermissionOverride && selfAid && owningAgent?.aid === selfAid) {
1339
- executionPermissionOverride = createAgentPolicyAuthorization({
1340
- agentAid: selfAid, actor: roleDetail.actor, chatType,
1341
- source: msg.source, peerType: msg.peerType,
1342
- });
1343
- }
1344
1339
  // 3. session 解析(使用 Channel 层填充的 chatType)
1345
1340
  if (!(await this.canCreateThreadSession(channelName, msg, chatType))) {
1346
1341
  // 静默丢弃:绝不向群里注入回复。
@@ -1481,6 +1476,20 @@ export class MessageBridge {
1481
1476
  causation: inboundCausation,
1482
1477
  });
1483
1478
  session = renewResult.session;
1479
+ if (!executionPermissionOverride && selfAid && owningAgent?.aid === selfAid) {
1480
+ executionPermissionOverride = createSessionFullAccessAuthorization({
1481
+ session,
1482
+ actor: roleDetail.actor,
1483
+ source: msg.source,
1484
+ peerType: msg.peerType,
1485
+ });
1486
+ if (!executionPermissionOverride && isMainSession(session) && !hasSessionFullAccessOverride(session)) {
1487
+ executionPermissionOverride = createAgentPolicyAuthorization({
1488
+ agentAid: selfAid, actor: roleDetail.actor, chatType,
1489
+ source: msg.source, peerType: msg.peerType,
1490
+ });
1491
+ }
1492
+ }
1484
1493
  if ('renewed' in renewResult && renewResult.renewed) {
1485
1494
  const notice = '已自动切换到新会话';
1486
1495
  try {
@@ -295,6 +295,20 @@ export function findMessageLogOperationMessageId(chatDir, operationId) {
295
295
  export function resolveChatDir(sessionsDir, channelType, channelId, selfAID) {
296
296
  return chatDirPath(sessionsDir, channelType, channelId, selfAID);
297
297
  }
298
+ let inboundLogListener;
299
+ export function setInboundLogListener(listener) {
300
+ inboundLogListener = listener;
301
+ }
302
+ function notifyInbound(chatDir, entry) {
303
+ if (entry.dir !== 'in' || !inboundLogListener)
304
+ return;
305
+ try {
306
+ inboundLogListener(chatDir, entry);
307
+ }
308
+ catch (e) {
309
+ logger.warn(`[MessageLog] Inbound listener failed: ${e}`);
310
+ }
311
+ }
298
312
  /** Append conversation history only. Protocol telemetry stays in its dedicated logs/stores. */
299
313
  export function appendMessageLog(chatDir, entry) {
300
314
  if (isTransientProtocolMessage(entry))
@@ -311,7 +325,9 @@ export function appendMessageLog(chatDir, entry) {
311
325
  }
312
326
  catch (e) {
313
327
  logger.warn(`[MessageLog] Failed to write message log: ${e}`);
328
+ return;
314
329
  }
330
+ notifyInbound(chatDir, entry);
315
331
  }
316
332
  /** Strict conversation-history append used by handoff reply binding. */
317
333
  export function appendMessageLogStrict(chatDir, entry, durable = false) {
@@ -323,6 +339,7 @@ export function appendMessageLogStrict(chatDir, entry, durable = false) {
323
339
  (durable ? appendJsonlDurable : appendJsonl)(messageLogPath(chatDir), entry);
324
340
  if (entry.dir === 'in')
325
341
  rememberMessageId(chatDir, entry.msgId);
342
+ notifyInbound(chatDir, entry);
326
343
  }
327
344
  export function buildInboundEntry(opts) {
328
345
  const ts = opts.timestamp || Date.now();
@@ -719,6 +719,7 @@ export class MessageQueue {
719
719
  sessionId: sessionKey,
720
720
  reason: 'daemon_restart',
721
721
  agentName,
722
+ agentAid: this.activeStates.get(queueKey)?.selfAID,
722
723
  taskId: this.activeStates.get(queueKey)?.taskId,
723
724
  generation: this.activeStates.get(queueKey)?.generation,
724
725
  causation: item.message.causation,
@@ -1154,6 +1155,7 @@ export class MessageQueue {
1154
1155
  sessionId: sessionKey,
1155
1156
  reason: 'new_message',
1156
1157
  agentName: this.processingAgent.get(queueKey),
1158
+ agentAid: this.activeStates.get(queueKey)?.selfAID,
1157
1159
  taskId: this.activeStates.get(queueKey)?.taskId,
1158
1160
  generation: this.activeStates.get(queueKey)?.generation,
1159
1161
  causation: this.activeBatches.get(queueKey)?.message.causation,
@@ -1755,6 +1757,7 @@ export class MessageQueue {
1755
1757
  sessionId: sessionKey,
1756
1758
  reason,
1757
1759
  agentName,
1760
+ agentAid: activeState?.selfAID,
1758
1761
  taskId: activeState?.taskId,
1759
1762
  generation: activeState?.generation,
1760
1763
  causation: active?.message.causation,
@@ -2043,6 +2046,7 @@ export class MessageQueue {
2043
2046
  sessionId: sessionKey,
2044
2047
  reason: 'stop',
2045
2048
  agentName: name,
2049
+ agentAid: activeState?.selfAID,
2046
2050
  taskId: this.activeStates.get(queueKey)?.taskId,
2047
2051
  generation: this.activeStates.get(queueKey)?.generation,
2048
2052
  causation: this.activeBatches.get(queueKey)?.message.causation,
@@ -2317,6 +2321,7 @@ export class MessageQueue {
2317
2321
  sessionId: sessionKey,
2318
2322
  reason: 'new_message',
2319
2323
  agentName: targetAgentName,
2324
+ agentAid: this.activeStates.get(targetQueueKey)?.selfAID,
2320
2325
  taskId: this.activeStates.get(targetQueueKey)?.taskId,
2321
2326
  generation: this.activeStates.get(targetQueueKey)?.generation,
2322
2327
  causation: this.activeBatches.get(targetQueueKey)?.message.causation,
@@ -2336,6 +2341,7 @@ export class MessageQueue {
2336
2341
  this.eventBus?.publish({
2337
2342
  type: 'task:interrupted', sessionId, reason: 'new_message',
2338
2343
  agentName: this.processingAgent.get(queueKey),
2344
+ agentAid: this.activeStates.get(queueKey)?.selfAID,
2339
2345
  taskId: this.activeStates.get(queueKey)?.taskId,
2340
2346
  generation: this.activeStates.get(queueKey)?.generation,
2341
2347
  causation: active.message.causation,
@@ -1,5 +1,70 @@
1
1
  import { isDeliveryTargetForChannel } from './message-utils.js';
2
2
  import { readEvolcoreRuntimeFlavor, readEvolcoreStartedAtIso, readInstalledEvolcoreVersion, tagErrorTextWithEvolcoreVersion, } from '../../utils/evolcore-version.js';
3
+ import { getErrorRawText } from '../../utils/error-utils.js';
4
+ function appendRawErrorText(text, raw) {
5
+ if (!raw || text === raw || text.includes(`原因:${raw}`))
6
+ return text;
7
+ return `${text}\n原因:${raw}`;
8
+ }
9
+ function resolveRawErrorText(raw, fallback) {
10
+ return getErrorRawText(raw) || getErrorRawText(fallback);
11
+ }
12
+ /** Ensure every outbound error carries the best available diagnostic text. */
13
+ export function normalizeOutboundErrorPayload(payload) {
14
+ switch (payload.kind) {
15
+ case 'result.error':
16
+ case 'system.error':
17
+ case 'command.error': {
18
+ const raw = resolveRawErrorText(payload.raw, payload.text);
19
+ if (!raw)
20
+ return payload;
21
+ return {
22
+ ...payload,
23
+ text: appendRawErrorText(payload.text, raw),
24
+ raw,
25
+ };
26
+ }
27
+ case 'status.error': {
28
+ const metadata = payload.metadata;
29
+ if (!metadata)
30
+ return payload;
31
+ const raw = resolveRawErrorText(metadata.raw, metadata.message);
32
+ if (!raw)
33
+ return payload;
34
+ return {
35
+ ...payload,
36
+ metadata: {
37
+ ...metadata,
38
+ message: typeof metadata.message === 'string'
39
+ ? appendRawErrorText(metadata.message, raw)
40
+ : metadata.message,
41
+ raw,
42
+ },
43
+ };
44
+ }
45
+ case 'status.timeout': {
46
+ const metadata = payload.metadata;
47
+ if (!metadata)
48
+ return payload;
49
+ const message = metadata.message ?? (typeof metadata.totalExecutionMs === 'number'
50
+ ? `任务超过总执行时限(${Math.round(metadata.totalExecutionMs / 1000)}秒),已自动中断`
51
+ : typeof metadata.idleSec === 'number' && metadata.idleSec > 0
52
+ ? `任务超时(${metadata.idleSec}秒无响应),已自动中断`
53
+ : '任务超时,已自动中断');
54
+ const raw = resolveRawErrorText(metadata.raw, metadata.reason) || message;
55
+ return {
56
+ ...payload,
57
+ metadata: {
58
+ ...metadata,
59
+ message: appendRawErrorText(message, raw),
60
+ raw,
61
+ },
62
+ };
63
+ }
64
+ default:
65
+ return payload;
66
+ }
67
+ }
3
68
  /** 出站 payload 摘要(用于 channel-out.log)。 */
4
69
  export function summarizeOutboundPayload(payload) {
5
70
  if (!payload)
@@ -18,6 +83,8 @@ export function summarizeOutboundPayload(payload) {
18
83
  case 'command.error':
19
84
  case 'result.error':
20
85
  summary.text = payload.text;
86
+ if (payload.raw !== undefined)
87
+ summary.raw = payload.raw;
21
88
  break;
22
89
  case 'result.file':
23
90
  summary.filePath = payload.filePath;
@@ -26,6 +93,8 @@ export function summarizeOutboundPayload(payload) {
26
93
  case 'system.error':
27
94
  summary.subtype = payload.subtype;
28
95
  summary.text = payload.text;
96
+ if (payload.raw !== undefined)
97
+ summary.raw = payload.raw;
29
98
  break;
30
99
  case 'interaction':
31
100
  summary.interactionId = payload.interaction?.id;
@@ -47,6 +116,7 @@ export function summarizeOutboundPayload(payload) {
47
116
  }
48
117
  /** Add the current EvolCore version marker to user-visible error payloads. */
49
118
  export function tagErrorPayloadWithEvolcoreVersion(payload) {
119
+ payload = normalizeOutboundErrorPayload(payload);
50
120
  switch (payload.kind) {
51
121
  case 'result.error':
52
122
  case 'system.error':
@@ -51,10 +51,15 @@ export function isPrivateChatmodeRoleLocked(field, role) {
51
51
  * - 人类且非 owner/admin(含角色未知)→ proactive
52
52
  * - 人类且 owner/admin → 分层配置(未配置时取出厂默认)
53
53
  */
54
- export function chatmodeForPeerIdentity(identity, role) {
54
+ export function chatmodeForPeerIdentity(identity, role, configContext = {}) {
55
55
  if (identity.isMachine)
56
56
  return 'proactive';
57
- return resolveChatMode({ role: role ?? undefined, chatType: 'private', peerType: identity.type ?? 'human' });
57
+ return resolveChatMode({
58
+ ...configContext,
59
+ role: role ?? undefined,
60
+ chatType: 'private',
61
+ peerType: identity.type ?? 'human',
62
+ });
58
63
  }
59
64
  export function resolveChatModeForField(params) {
60
65
  return resolveChatMode({