evolcore 0.0.10 → 0.0.11

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 (62) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +3 -3
  3. package/dist/agents/baseagent.js +4 -0
  4. package/dist/agents/claude-runner.js +123 -42
  5. package/dist/agents/codex-app-server-client.js +33 -9
  6. package/dist/agents/codex-runner.js +58 -8
  7. package/dist/agents/ecagent-runner.js +17 -2
  8. package/dist/agents/request-identity.js +55 -0
  9. package/dist/aun/outbox.js +28 -31
  10. package/dist/channels/aun.js +131 -128
  11. package/dist/cli/agent-command.js +16 -9
  12. package/dist/cli/agent.js +82 -19
  13. package/dist/cli/daemon-commands.js +21 -2
  14. package/dist/cli/index.js +76 -61
  15. package/dist/cli/init-cancel.js +208 -0
  16. package/dist/cli/init-channel.js +343 -195
  17. package/dist/cli/init.js +21 -9
  18. package/dist/config/builtin-roles.js +1 -0
  19. package/dist/config/gateway-config.js +26 -10
  20. package/dist/core/agent-reload-coordinator.js +53 -0
  21. package/dist/core/auth/operation-authorizer.js +32 -147
  22. package/dist/core/auth/operation-catalog.js +80 -0
  23. package/dist/core/bootstrap-messages.js +50 -0
  24. package/dist/core/bootstrap-service.js +85 -10
  25. package/dist/core/channel-loader.js +23 -6
  26. package/dist/core/command/agent-control.js +14 -11
  27. package/dist/core/command/menu-handler.js +67 -76
  28. package/dist/core/command/slash-handler.js +4 -4
  29. package/dist/core/evolagent-registry.js +125 -35
  30. package/dist/core/evolagent.js +8 -3
  31. package/dist/core/inference/text-inference.js +38 -4
  32. package/dist/core/message/message-bridge.js +1 -1
  33. package/dist/core/message/message-log.js +22 -0
  34. package/dist/core/message/message-queue.js +19 -4
  35. package/dist/core/model/model-catalog.js +143 -24
  36. package/dist/core/model/model-diagnostics.js +28 -10
  37. package/dist/core/permission/index.js +1 -0
  38. package/dist/core/permission/readonly-shell-query.js +532 -0
  39. package/dist/core/permission/shell-environment.js +46 -0
  40. package/dist/core/permission/tool-policy.js +231 -93
  41. package/dist/core/protected-paths.js +10 -7
  42. package/dist/core/runner-reload-transaction.js +57 -0
  43. package/dist/index.js +262 -84
  44. package/dist/ipc.js +29 -11
  45. package/dist/utils/aid-bind.js +3 -8
  46. package/dist/utils/log-writer.js +6 -10
  47. package/dist/utils/logger.js +5 -5
  48. package/kits/docs/evolcore/msg.md +13 -0
  49. package/kits/rules/01-overview.md +9 -0
  50. package/kits/schemas/agent-config.schema.3.json +1 -1
  51. package/kits/schemas/agent-config.schema.4.json +1 -1
  52. package/kits/schemas/relation-config.schema.2.json +1 -1
  53. package/kits/schemas/role-config.schema.1.json +1 -1
  54. package/kits/templates/roles/admin.json +5 -0
  55. package/kits/templates/roles/member.json +17 -0
  56. package/kits/templates/roles/visitor.json +8 -0
  57. package/kits/templates/system-fragments/bootstrap.md +12 -6
  58. package/kits/templates/system-fragments/channel.md +6 -0
  59. package/kits/templates/system-fragments/session.md +2 -0
  60. package/package.json +2 -1
  61. package/skills/eclink/SKILL.md +15 -3
  62. package/skills/eclink/agents/openai.yaml +3 -3
@@ -10,6 +10,7 @@ import { normalizePermissionMode } from '../core/permission/mode.js';
10
10
  import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
11
11
  import { logger } from '../utils/logger.js';
12
12
  import { resolveEcagentConfig } from './baseagent.js';
13
+ import { buildModelRequestHeaders } from './request-identity.js';
13
14
  const PROVIDER_ID = 'evolcore-gateway';
14
15
  const DEFAULT_SYSTEM_PROMPT = `You are ecagent, the coding agent built into EvolCore.
15
16
  Work directly in the current project. Inspect relevant files before changing them, keep edits scoped, and verify the result.
@@ -252,6 +253,7 @@ function createOpenAiTransport(config) {
252
253
  authorization: `Bearer ${config.apiKey}`,
253
254
  'content-type': 'application/json',
254
255
  ...Object.fromEntries(Object.entries(options?.headers ?? {}).filter((entry) => entry[1] !== null)),
256
+ ...config.headers,
255
257
  },
256
258
  body: JSON.stringify(payload),
257
259
  signal: controller.signal,
@@ -678,7 +680,15 @@ export class EcagentRunner {
678
680
  onCompactStart;
679
681
  modelCache;
680
682
  constructor(config, callbacks) {
681
- this.config = config;
683
+ this.config = {
684
+ ...config,
685
+ headers: buildModelRequestHeaders({
686
+ baseagent: 'ecagent',
687
+ baseUrl: config.baseUrl,
688
+ agentAid: config.evolcoreAgentAid,
689
+ configuredHeaders: config.headers,
690
+ }),
691
+ };
682
692
  this.model = config.model;
683
693
  this.effort = config.effort;
684
694
  this.onSessionIdUpdate = callbacks?.onSessionIdUpdate;
@@ -714,7 +724,11 @@ export class EcagentRunner {
714
724
  try {
715
725
  const signal = AbortSignal.timeout(MODEL_LIST_TIMEOUT_MS);
716
726
  const response = await fetch(`${this.config.baseUrl.replace(/\/+$/, '')}/models`, {
717
- headers: { authorization: `Bearer ${this.config.apiKey}` }, signal,
727
+ headers: {
728
+ authorization: `Bearer ${this.config.apiKey}`,
729
+ ...this.config.headers,
730
+ },
731
+ signal,
718
732
  });
719
733
  if (!response.ok)
720
734
  throw new Error(`Failed to list ecagent models: HTTP ${response.status}`);
@@ -948,6 +962,7 @@ export class EcagentRunner {
948
962
  return { block: true, reason: policy.reason || '当前会话策略拒绝此工具调用' };
949
963
  const preflight = evaluateToolPreflight(toolName, input, {
950
964
  sessionId,
965
+ selfAid: permissionContext?.selfAid,
951
966
  channel: permissionContext?.channel,
952
967
  userId: permissionContext?.userId,
953
968
  role: permissionContext?.role,
@@ -0,0 +1,55 @@
1
+ import { logger } from '../utils/logger.js';
2
+ import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
3
+ export const EVOLCORE_REQUEST_HEADERS = {
4
+ version: 'X-EvolCore-Version',
5
+ agentAid: 'X-EvolCore-Agent-AID',
6
+ baseagent: 'X-EvolCore-Base-Agent',
7
+ };
8
+ const RESERVED_HEADER_NAMES = new Set(Object.values(EVOLCORE_REQUEST_HEADERS).map(name => name.toLowerCase()));
9
+ const warnedReservedHeaders = new Set();
10
+ function officialHostname(baseagent) {
11
+ return baseagent === 'claude' ? 'api.anthropic.com' : 'api.openai.com';
12
+ }
13
+ export function isOfficialModelEndpoint(baseagent, baseUrl) {
14
+ const configured = baseUrl?.trim();
15
+ if (!configured)
16
+ return true;
17
+ try {
18
+ return new URL(configured).hostname.toLowerCase() === officialHostname(baseagent);
19
+ }
20
+ catch {
21
+ // Invalid URLs fail at the request layer. Treat them as configured here so
22
+ // identity behavior does not silently change if URL validation is relaxed.
23
+ return false;
24
+ }
25
+ }
26
+ function configuredHeadersWithoutReserved(baseagent, configuredHeaders) {
27
+ const headers = {};
28
+ for (const [name, value] of Object.entries(configuredHeaders ?? {})) {
29
+ const normalizedName = name.toLowerCase();
30
+ if (!RESERVED_HEADER_NAMES.has(normalizedName)) {
31
+ headers[name] = value;
32
+ continue;
33
+ }
34
+ const warningKey = `${baseagent}:${normalizedName}`;
35
+ if (!warnedReservedHeaders.has(warningKey)) {
36
+ warnedReservedHeaders.add(warningKey);
37
+ logger.warn(`[RequestIdentity] Ignoring configured reserved header ${JSON.stringify(name)} for ${baseagent}; EvolCore manages this header at runtime`);
38
+ }
39
+ }
40
+ return headers;
41
+ }
42
+ export function buildModelRequestHeaders(input) {
43
+ const headers = configuredHeadersWithoutReserved(input.baseagent, input.configuredHeaders);
44
+ if (isOfficialModelEndpoint(input.baseagent, input.baseUrl))
45
+ return headers;
46
+ headers[EVOLCORE_REQUEST_HEADERS.version] = readInstalledEvolcoreVersion();
47
+ const agentAid = input.agentAid?.trim();
48
+ if (agentAid)
49
+ headers[EVOLCORE_REQUEST_HEADERS.agentAid] = agentAid;
50
+ headers[EVOLCORE_REQUEST_HEADERS.baseagent] = input.baseagent;
51
+ return headers;
52
+ }
53
+ export function _resetRequestIdentityWarningsForTests() {
54
+ warnedReservedHeaders.clear();
55
+ }
@@ -1,7 +1,6 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
1
  import crypto from 'crypto';
4
2
  import { agentOutboxPath } from '../paths.js';
3
+ import { atomicRead, atomicWrite } from '../utils/atomic-write.js';
5
4
  const MAX_ENTRIES_PER_AID = 20;
6
5
  const DEFAULT_TTL = 300_000; // 5 minutes
7
6
  function outboxFile(aid) {
@@ -13,14 +12,14 @@ function generateId() {
13
12
  return `out-${ts}-${rand}`;
14
13
  }
15
14
  function isExpired(entry) {
15
+ if (entry.critical)
16
+ return false;
16
17
  return Date.now() - entry.ts > entry.ttl;
17
18
  }
18
19
  function readEntries(aid) {
19
20
  const file = outboxFile(aid);
20
- if (!fs.existsSync(file))
21
- return [];
22
21
  try {
23
- const content = fs.readFileSync(file, 'utf-8').trim();
22
+ const content = atomicRead(file)?.trim();
24
23
  if (!content)
25
24
  return [];
26
25
  return content.split('\n').map(line => {
@@ -38,22 +37,25 @@ function readEntries(aid) {
38
37
  }
39
38
  function writeEntries(aid, entries) {
40
39
  const file = outboxFile(aid);
41
- if (entries.length === 0) {
42
- try {
43
- fs.unlinkSync(file);
44
- }
45
- catch { }
46
- return;
47
- }
48
- fs.mkdirSync(path.dirname(file), { recursive: true });
49
- fs.writeFileSync(file, entries.map(e => JSON.stringify(e)).join('\n') + '\n');
40
+ const content = entries.length > 0
41
+ ? entries.map(e => JSON.stringify(e)).join('\n') + '\n'
42
+ : '';
43
+ atomicWrite(file, content);
50
44
  }
51
45
  export function enqueue(aid, opts) {
46
+ const existingEntries = readEntries(aid);
47
+ if (opts.dedupeKey) {
48
+ const existing = existingEntries.find(entry => entry.dedupeKey === opts.dedupeKey && !isExpired(entry));
49
+ if (existing)
50
+ return existing;
51
+ }
52
52
  const entry = {
53
53
  id: generateId(),
54
54
  ts: Date.now(),
55
55
  aid,
56
56
  channelId: opts.channelId,
57
+ dedupeKey: opts.dedupeKey,
58
+ critical: opts.critical,
57
59
  type: opts.type,
58
60
  contentKind: opts.contentKind,
59
61
  payload: opts.payload,
@@ -65,17 +67,18 @@ export function enqueue(aid, opts) {
65
67
  ttl: opts.ttl ?? DEFAULT_TTL,
66
68
  postSend: opts.postSend,
67
69
  };
68
- const file = outboxFile(aid);
69
- fs.mkdirSync(path.dirname(file), { recursive: true });
70
70
  // Enforce cap: read existing, drop oldest if over limit
71
- let entries = readEntries(aid);
72
- if (entries.length >= MAX_ENTRIES_PER_AID) {
73
- entries = entries.slice(entries.length - MAX_ENTRIES_PER_AID + 1);
74
- writeEntries(aid, [...entries, entry]);
71
+ let entries = existingEntries;
72
+ if (entries.filter(candidate => !candidate.critical).length >= MAX_ENTRIES_PER_AID) {
73
+ const dropIndex = entries.findIndex(candidate => !candidate.critical);
74
+ if (dropIndex >= 0)
75
+ entries.splice(dropIndex, 1);
76
+ entries = [...entries, entry];
75
77
  }
76
78
  else {
77
- fs.appendFileSync(file, JSON.stringify(entry) + '\n');
79
+ entries = [...entries, entry];
78
80
  }
81
+ writeEntries(aid, entries);
79
82
  return entry;
80
83
  }
81
84
  export function remove(aid, id) {
@@ -105,6 +108,9 @@ export function removeInteractionCards(aid, requestId) {
105
108
  export function load(aid) {
106
109
  return readEntries(aid).filter(e => !isExpired(e));
107
110
  }
111
+ export function findByDedupeKey(aid, dedupeKey) {
112
+ return load(aid).find(entry => entry.dedupeKey === dedupeKey);
113
+ }
108
114
  export function cleanup(aid) {
109
115
  const all = readEntries(aid);
110
116
  const valid = all.filter(e => !isExpired(e));
@@ -185,16 +191,7 @@ export async function drain(aid, sender) {
185
191
  }
186
192
  }
187
193
  export function hasPending(aid) {
188
- const file = outboxFile(aid);
189
- if (!fs.existsSync(file))
190
- return false;
191
- try {
192
- const stat = fs.statSync(file);
193
- return stat.size > 0;
194
- }
195
- catch {
196
- return false;
197
- }
194
+ return readEntries(aid).some(entry => !isExpired(entry));
198
195
  }
199
196
  /**
200
197
  * 当前 outbox 中待发送条目数。用于诊断发送管线堵塞:depth 持续增长说明
@@ -7,17 +7,18 @@ import { logger, localTimestamp } from '../utils/logger.js';
7
7
  import { LogWriter } from '../utils/log-writer.js';
8
8
  import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
9
9
  import { DEFAULT_FLUSH_DELAY_SECONDS } from '../types.js';
10
- import { resolvePaths, agentMdPath as agentMdPathFn, agentDir as agentDirPath, resolveRoot } from '../paths.js';
10
+ import { resolvePaths, agentDir as agentDirPath, resolveRoot } from '../paths.js';
11
11
  import { saveToUploads, sanitizeFileName, bufferToInboundImage, safeFetch } from '../utils/media-cache.js';
12
12
  import { appendAidEvent } from '../utils/instance-registry.js';
13
- import { appendMessageLog, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog } from '../core/message/message-log.js';
13
+ import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, hasMessageLogOperation } from '../core/message/message-log.js';
14
14
  import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
15
15
  import { chatDirPath } from '../core/session/session-fs-store.js';
16
16
  import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
17
17
  import { appendAidLifecycle } from '../aun/aid/identity.js';
18
18
  import { getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
19
19
  import { MAX_AUN_ATTACHMENT_SIZE, uploadBufferAndBuildPayload, uploadFileAndBuildPayload } from '../aun/msg/upload.js';
20
- import { loadAgent, saveAgent } from '../config-store.js';
20
+ import { loadAgent } from '../config-store.js';
21
+ import { normalizeAgentLifecycle } from '../config/lifecycle.js';
21
22
  import { resolveEffective } from '../config/config-manager.js';
22
23
  import { isManagementRole } from '../config/builtin-roles.js';
23
24
  import { mentionModeToDispatch } from '../config/mention-mode.js';
@@ -33,6 +34,7 @@ import { recordCausationSpan } from '../core/causation/audit.js';
33
34
  import { isExplicitGroupId } from '../aun/group-identity.js';
34
35
  import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
35
36
  import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
37
+ import { postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox } from '../core/bootstrap-messages.js';
36
38
  export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
37
39
  const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
38
40
  const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
@@ -200,7 +202,6 @@ export class AUNChannel {
200
202
  connected = false;
201
203
  traceWriter = null;
202
204
  eventBus = null;
203
- ownerBoundHandler = null;
204
205
  queuedHandler = null;
205
206
  pendingEchoMessages = new Map();
206
207
  isEchoSending = false;
@@ -833,9 +834,9 @@ export class AUNChannel {
833
834
  baseName: 'aun',
834
835
  logDir: resolvePaths().logs,
835
836
  rotation: 'hourly',
836
- retention: { hours: 12 },
837
+ retention: { hours: 24 },
837
838
  });
838
- logger.info(`${this.logPrefix()} Trace logging enabled (hourly rotation, 12h retention): ${this.traceWriter.activePath()}`);
839
+ logger.info(`${this.logPrefix()} Trace logging enabled (hourly rotation, 24h retention): ${this.traceWriter.activePath()}`);
839
840
  }
840
841
  this.aidState = {
841
842
  aid: config.aid,
@@ -1083,11 +1084,6 @@ export class AUNChannel {
1083
1084
  logger.info(`${this.logPrefix()} Connected as ${this._aid}`);
1084
1085
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
1085
1086
  appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
1086
- // Send welcome message to owner after first connection
1087
- // pureIdentity(控制 AID):跳过 evolagent onboarding(根除 warn 噪声 + 永不 agentmdPut)
1088
- if (!this.config.pureIdentity) {
1089
- await this.sendWelcomeMessage();
1090
- }
1091
1087
  }
1092
1088
  catch (e) {
1093
1089
  this.trace('OUT', 'client.connect.error', { error: String(e) });
@@ -1097,122 +1093,81 @@ export class AUNChannel {
1097
1093
  throw e;
1098
1094
  }
1099
1095
  }
1100
- async sendWelcomeMessage() {
1096
+ async preparePostBootstrapWelcome() {
1101
1097
  try {
1102
1098
  const aid = this.config.aid;
1103
1099
  const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
1104
- // Read initialized + owners from per-agent config.json
1105
- // (config.json 是 owner 的真相来源——auto-bind 后会更新这里,但 this.config 是
1106
- // channel 启动时的快照,不会自动同步)
1107
1100
  const agentConfig = loadAgent(aidName);
1108
1101
  if (!agentConfig) {
1109
1102
  logger.warn(`${this.logPrefix()} agent config not found for ${aidName}, skipping welcome message`);
1110
- return;
1111
- }
1112
- if (agentConfig.initialized === true) {
1113
- logger.info(`${this.logPrefix()} Agent already initialized, skipping welcome message`);
1114
- return;
1103
+ return false;
1115
1104
  }
1116
1105
  const owner = getFirstStaticAgentOwner(aidName);
1117
1106
  if (!owner) {
1118
- logger.info(`${this.logPrefix()} No owner configured, skipping welcome message (will retry after auto-bind)`);
1119
- return;
1107
+ logger.info(`${this.logPrefix()} No owner configured, skipping post-bootstrap welcome message`);
1108
+ return false;
1120
1109
  }
1121
- const agentMdLocalPath = agentMdPathFn(aidName);
1122
- const existingAgentMd = fs.existsSync(agentMdLocalPath) ? fs.readFileSync(agentMdLocalPath, 'utf-8') : '';
1123
- const existingFrontmatterMatch = existingAgentMd.match(/^---\n([\s\S]*?)\n---/);
1124
- const existingFrontmatter = existingFrontmatterMatch?.[1] ?? '';
1125
- // Fetch owner's agent.md to derive name and validate type
1126
- const ownerInfo = await this.fetchPeerInfo(owner);
1127
- if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
1128
- logger.warn(`${this.logPrefix()} Owner ${owner} type is "${ownerInfo.type}" (not human). Consider using a human AID as owner.`);
1110
+ const operationId = postBootstrapWelcomeOperationId(aidName);
1111
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aidName);
1112
+ if (hasMessageLogOperation(chatDir, operationId) || outbox.findByDedupeKey(aidName, operationId)) {
1113
+ return true;
1129
1114
  }
1130
- // Name: prefer existing agent.md name if user has customized it,
1131
- // otherwise generate "{ownerName}的Evol助手 ({aidLabel})" for disambiguation
1132
1115
  const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
1133
- const ownerDisplayName = (ownerInfo.name || ownerAidClean.split('.')[0]).slice(0, 12);
1134
- const currentNameMatch = existingFrontmatter.match(/^name:\s*"?([^"\n]+)/m);
1135
- const currentName = currentNameMatch?.[1]?.trim().replace(/"$/, '');
1136
- const aidLabel = aidName.split('.')[0];
1137
- let agentDisplayName;
1138
- if (currentName && currentName !== aidLabel) {
1139
- agentDisplayName = currentName;
1140
- }
1141
- else {
1142
- agentDisplayName = `${ownerDisplayName}的Evol助手 (${aidLabel})`;
1143
- }
1144
- // Preserve user-provided description (from `agent new --description`), fallback to default
1145
- const currentDescMatch = existingFrontmatter.match(/^description:\s*"?([^"\n]*)/m);
1146
- const currentDesc = currentDescMatch?.[1]?.trim().replace(/"$/, '');
1147
- const agentDescription = currentDesc
1148
- ? currentDesc
1149
- : 'EvolCore AI Agent Gateway - 连接 Claude/Codex 到消息通道';
1150
- // Generate new agent.md (no `initialized` frontmatter — that's now in config.json)
1151
- const newAgentMd = `---
1152
- aid: "${aid}"
1153
- name: "${agentDisplayName}"
1154
- type: "codeagent"
1155
- version: "1.0.0"
1156
- description: "${agentDescription}"
1157
- tags:
1158
- - evolcore
1159
- - ai-agent
1160
- - gateway
1161
- ---
1162
-
1163
- # ${agentDisplayName}
1164
-
1165
- EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1166
- `;
1167
- // Write locally and publish to AUN network (auto-sign)
1116
+ let ownerDisplayName = ownerAidClean.split('.')[0].slice(0, 12);
1168
1117
  try {
1169
- const { agentmdPut } = await import('../aun/aid/agentmd.js');
1170
- await agentmdPut(newAgentMd, { aid: aidName, store: this.store });
1171
- logger.info(`${this.logPrefix()} Published agent.md to AUN network`);
1172
- }
1173
- catch (e) {
1174
- logger.warn(`${this.logPrefix()} Failed to publish agent.md: ${e}`);
1175
- }
1176
- // Send welcome message
1177
- const { generateWelcomeMessage } = await import('../utils/welcome.js');
1178
- const welcomeText = generateWelcomeMessage({
1179
- channelType: 'aun',
1180
- agentName: agentDisplayName,
1181
- ownerName: ownerDisplayName,
1182
- includeBindingNote: true,
1183
- });
1184
- // First contact with Owner races against Owner's async cert fetch from
1185
- // gateway PKI; a 3s pause lets the cert propagate. persist_required asks
1186
- // the gateway to durably store the message so Owner can recover it via
1187
- // pull if the initial E2EE push still arrives before the cert resolves.
1188
- await new Promise(resolve => setTimeout(resolve, 3000));
1189
- if (!this.client) {
1190
- logger.warn(`${this.logPrefix()} Client disconnected before welcome message could be sent`);
1191
- return;
1192
- }
1193
- await this.callAndTrace('message.send', {
1194
- to: owner,
1195
- payload: { type: 'text', text: welcomeText },
1196
- encrypt: this.shouldEncrypt(owner),
1197
- persist_required: true,
1198
- });
1199
- logger.info(`${this.logPrefix()} Welcome message sent to owner: ${owner}`);
1200
- // Mark agent as initialized in config.json (replaces old agent.md frontmatter flag)
1201
- try {
1202
- const fresh = loadAgent(aidName);
1203
- if (fresh) {
1204
- fresh.initialized = true;
1205
- saveAgent(fresh);
1206
- logger.info(`${this.logPrefix()} Marked ${aidName} as initialized in config.json`);
1118
+ const ownerInfo = await this.fetchPeerInfo(owner);
1119
+ ownerDisplayName = (ownerInfo.name || ownerDisplayName).slice(0, 12);
1120
+ if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
1121
+ logger.warn(`${this.logPrefix()} Owner ${owner} type is "${ownerInfo.type}" (not human).`);
1207
1122
  }
1208
1123
  }
1209
1124
  catch (e) {
1210
- logger.warn(`${this.logPrefix()} Failed to update initialized flag in config.json: ${e}`);
1125
+ logger.warn(`${this.logPrefix()} Failed to resolve owner display name; using AID label: ${e}`);
1211
1126
  }
1127
+ preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName);
1128
+ logger.info(`${this.logPrefix()} Post-bootstrap welcome durably prepared for owner: ${owner}`);
1129
+ return true;
1212
1130
  }
1213
1131
  catch (e) {
1214
- logger.warn(`${this.logPrefix()} Failed to send welcome message: ${e}`);
1132
+ logger.warn(`${this.logPrefix()} Failed to prepare post-bootstrap welcome message: ${e}`);
1133
+ return false;
1134
+ }
1135
+ }
1136
+ async sendPostBootstrapWelcome() {
1137
+ if (!await this.preparePostBootstrapWelcome())
1138
+ return false;
1139
+ return this.reconcilePostBootstrapWelcome();
1140
+ }
1141
+ /**
1142
+ * Resume delivery of an already prepared completion welcome.
1143
+ *
1144
+ * Unlike sendPostBootstrapWelcome(), this method never creates a new
1145
+ * operation. That distinction keeps repeated `ec agent ready` calls on an
1146
+ * already-active agent from manufacturing another welcome message.
1147
+ */
1148
+ async reconcilePostBootstrapWelcome() {
1149
+ const configuredAid = this.config.aid;
1150
+ const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
1151
+ const operationId = postBootstrapWelcomeOperationId(aid);
1152
+ const entry = outbox.findByDedupeKey(aid, operationId);
1153
+ if (!entry) {
1154
+ const owner = getFirstStaticAgentOwner(aid);
1155
+ if (!owner)
1156
+ return false;
1157
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
1158
+ return hasMessageLogOperation(chatDir, operationId);
1215
1159
  }
1160
+ const agentConfig = loadAgent(aid);
1161
+ if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active') {
1162
+ logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
1163
+ return true;
1164
+ }
1165
+ if (!this.connected || !this.client)
1166
+ return true;
1167
+ const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
1168
+ if (sent)
1169
+ outbox.remove(aid, entry.id);
1170
+ return true;
1216
1171
  }
1217
1172
  // ── Event handlers ──────────────────────────────────────────
1218
1173
  /**
@@ -2445,31 +2400,12 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2445
2400
  // ── Public API (same interface as before) ───────────────────
2446
2401
  setEventBus(bus) {
2447
2402
  // 重新订阅前先解掉旧的——避免 reload/重连后 listener 累积
2448
- if (this.eventBus && this.ownerBoundHandler && typeof this.eventBus.unsubscribe === 'function') {
2449
- this.eventBus.unsubscribe('channel:owner-bound', this.ownerBoundHandler);
2450
- }
2451
2403
  if (this.eventBus && this.queuedHandler && typeof this.eventBus.unsubscribe === 'function') {
2452
2404
  this.eventBus.unsubscribe('task:queued', this.queuedHandler);
2453
2405
  }
2454
- this.ownerBoundHandler = null;
2455
2406
  this.queuedHandler = null;
2456
2407
  this.eventBus = bus;
2457
2408
  if (bus && typeof bus.subscribe === 'function') {
2458
- const handler = (event) => {
2459
- if (event.channelName !== this.config.channelName)
2460
- return;
2461
- // sendWelcomeMessage 内部读 config.json 中最新的 owners[0],并幂等检查 initialized
2462
- // 自身做 client 健康检查后再发
2463
- if (!this.client) {
2464
- logger.info(`${this.logPrefix()} owner-bound event received but client not connected; skip welcome retry`);
2465
- return;
2466
- }
2467
- this.sendWelcomeMessage().catch(e => {
2468
- logger.warn(`${this.logPrefix()} owner-bound welcome retry failed: ${e}`);
2469
- });
2470
- };
2471
- bus.subscribe('channel:owner-bound', handler);
2472
- this.ownerBoundHandler = handler;
2473
2409
  const queuedHandler = (event) => {
2474
2410
  if (event.channel !== this.config.channelName)
2475
2411
  return;
@@ -2928,12 +2864,25 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2928
2864
  finalText = `@${context.peerId} ` + finalText;
2929
2865
  }
2930
2866
  }
2867
+ const operationId = typeof context?.metadata?.operationId === 'string'
2868
+ ? context.metadata.operationId
2869
+ : undefined;
2870
+ if (operationId) {
2871
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
2872
+ if (hasMessageLogOperation(chatDir, operationId))
2873
+ return;
2874
+ }
2931
2875
  // Write-ahead: persist to outbox before attempting send
2932
2876
  const entry = outbox.enqueue(this.config.aid, {
2933
2877
  channelId,
2878
+ dedupeKey: operationId,
2879
+ critical: context?.metadata?.criticalDelivery === true,
2934
2880
  type: 'text',
2935
2881
  text: finalText,
2936
2882
  context,
2883
+ ttl: typeof context?.metadata?.outboxTtl === 'number'
2884
+ ? context.metadata.outboxTtl
2885
+ : undefined,
2937
2886
  });
2938
2887
  logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
2939
2888
  // 积压深度告警:outbox 待发条目累积说明发送速度跟不上,回复将出现明显延迟。
@@ -3187,6 +3136,20 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3187
3136
  const channelId = entry.channelId;
3188
3137
  const finalText = entry.text;
3189
3138
  const context = entry.context;
3139
+ const operationId = entry.dedupeKey
3140
+ || (typeof context?.metadata?.operationId === 'string' ? context.metadata.operationId : undefined);
3141
+ if (operationId) {
3142
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
3143
+ if (hasMessageLogOperation(chatDir, operationId)) {
3144
+ logger.info(`${this.logPrefix()} Durable operation already logged; skipping duplicate send: ${operationId}`);
3145
+ return true;
3146
+ }
3147
+ if (operationId === postBootstrapWelcomeOperationId(this.config.aid)) {
3148
+ const agentConfig = loadAgent(this.config.aid);
3149
+ if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active')
3150
+ return false;
3151
+ }
3152
+ }
3190
3153
  // 从 context.metadata.source 读取 source,默认为 'daemon'
3191
3154
  const source = context?.metadata?.source ?? 'daemon';
3192
3155
  const payload = { type: 'text', text: finalText };
@@ -3215,11 +3178,25 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3215
3178
  logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} source=${source} textLen=${finalText.length}`);
3216
3179
  const isGroup = this.isGroupId(channelId);
3217
3180
  const targetAid = channelId;
3181
+ if (operationId && entry.deliveryReceipt) {
3182
+ this.appendOutboundJsonl(channelId, {
3183
+ ...classifyAunPayloadForLog(payload),
3184
+ msgId: entry.deliveryReceipt.messageId,
3185
+ encrypt: entry.deliveryReceipt.encrypt,
3186
+ context,
3187
+ isGroup,
3188
+ source,
3189
+ transport: entry.deliveryReceipt.transport,
3190
+ });
3191
+ return true;
3192
+ }
3218
3193
  const encryptTarget = isGroup ? channelId : targetAid;
3219
3194
  const encrypt = context?.metadata?.encrypted != null
3220
3195
  ? !!(context.metadata.encrypted)
3221
3196
  : this.shouldEncrypt(encryptTarget);
3222
3197
  const params = { payload, encrypt };
3198
+ if (context?.metadata?.persistRequired === true)
3199
+ params.persist_required = true;
3223
3200
  try {
3224
3201
  if (isGroup) {
3225
3202
  params.group_id = channelId;
@@ -3237,6 +3214,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3237
3214
  }
3238
3215
  else {
3239
3216
  this.logAunSendAccepted('group.send', channelId, mid, encrypt, result, finalText);
3217
+ this.checkpointTextDelivery(entry, mid, encrypt, result);
3240
3218
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
3241
3219
  const statsContext = await this.groupStatsContext(channelId);
3242
3220
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode, 'send', statsContext);
@@ -3257,6 +3235,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3257
3235
  }
3258
3236
  else {
3259
3237
  this.logAunSendAccepted('message.send', this.peerLabel(targetAid), result.message_id, encrypt, result, finalText);
3238
+ this.checkpointTextDelivery(entry, result.message_id, encrypt, result);
3260
3239
  const causation = normalizeCausation(context?.metadata?.causation);
3261
3240
  if (causation) {
3262
3241
  registerAunCausation(result.message_id, this.config.aid, targetAid, causation);
@@ -3292,6 +3271,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3292
3271
  logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
3293
3272
  return false;
3294
3273
  }
3274
+ this.checkpointTextDelivery(entry, mid, false, result);
3295
3275
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
3296
3276
  const statsContext = await this.groupStatsContext(channelId);
3297
3277
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, false, context?.metadata?.chatmode, 'send', statsContext);
@@ -3310,6 +3290,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3310
3290
  logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
3311
3291
  return false;
3312
3292
  }
3293
+ this.checkpointTextDelivery(entry, mid, false, result);
3313
3294
  const causation = normalizeCausation(context?.metadata?.causation);
3314
3295
  if (causation) {
3315
3296
  registerAunCausation(mid, this.config.aid, targetAid, causation);
@@ -3341,6 +3322,18 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3341
3322
  }
3342
3323
  }
3343
3324
  }
3325
+ checkpointTextDelivery(entry, messageId, encrypt, result) {
3326
+ if (!entry.dedupeKey)
3327
+ return;
3328
+ entry.deliveryReceipt = {
3329
+ messageId,
3330
+ encrypt,
3331
+ transport: this.sendReceiptFromResult(result),
3332
+ };
3333
+ if (!outbox.replace(this.config.aid, entry)) {
3334
+ logger.warn(`${this.logPrefix()} Failed to checkpoint durable operation receipt: ${entry.dedupeKey}`);
3335
+ }
3336
+ }
3344
3337
  async deliverPayloadEntry(entry) {
3345
3338
  const interactionId = entry.postSend?.type === 'register_interaction_card'
3346
3339
  ? entry.postSend.requestId
@@ -3371,13 +3364,16 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3371
3364
  }
3372
3365
  /** 有效会话正文写入 messages.jsonl(message.send/group.send 成功后调用)。 */
3373
3366
  appendOutboundJsonl(channelId, descriptor) {
3367
+ const operationId = typeof descriptor.context?.metadata?.operationId === 'string'
3368
+ ? descriptor.context.metadata.operationId
3369
+ : undefined;
3374
3370
  try {
3375
3371
  const { content, msgType, payloadType, payloadSummary, msgId, encrypt, transport, context, isGroup, source = 'daemon' } = descriptor;
3376
3372
  const sessionsDir = resolvePaths().sessionsDir;
3377
3373
  const selfAID = this.config.aid;
3378
3374
  const chatDir = chatDirPath(sessionsDir, 'aun', channelId, selfAID);
3379
3375
  const chatmode = context?.metadata?.chatmode;
3380
- appendMessageLog(chatDir, buildOutboundEntry({
3376
+ const entry = buildOutboundEntry({
3381
3377
  from: selfAID,
3382
3378
  to: channelId,
3383
3379
  sessionId: context?.sessionId,
@@ -3397,9 +3393,16 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3397
3393
  payloadType,
3398
3394
  payloadSummary,
3399
3395
  source,
3400
- }));
3396
+ operationId,
3397
+ });
3398
+ if (operationId)
3399
+ appendMessageLogStrict(chatDir, entry);
3400
+ else
3401
+ appendMessageLog(chatDir, entry);
3401
3402
  }
3402
3403
  catch (e) {
3404
+ if (operationId)
3405
+ throw e;
3403
3406
  logger.debug(`${this.logPrefix()} appendOutboundJsonl failed: ${e}`);
3404
3407
  }
3405
3408
  }