evolcore 0.0.10 → 0.0.12

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 (131) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +3 -3
  3. package/bin/codex-managed-hook.mjs +80 -0
  4. package/dist/agents/baseagent.js +4 -0
  5. package/dist/agents/claude-runner.js +158 -44
  6. package/dist/agents/codex-app-server-client.js +153 -23
  7. package/dist/agents/codex-runner.js +331 -43
  8. package/dist/agents/ecagent-runner.js +53 -6
  9. package/dist/agents/gemini-runner.js +1 -0
  10. package/dist/agents/request-identity.js +55 -0
  11. package/dist/aun/aid/client.js +11 -21
  12. package/dist/aun/aid/managed-operation.js +107 -0
  13. package/dist/aun/msg/group.js +81 -12
  14. package/dist/aun/msg/managed-operation.js +804 -0
  15. package/dist/aun/msg/mention-schema.js +20 -0
  16. package/dist/aun/msg/p2p.js +7 -0
  17. package/dist/aun/outbox.js +28 -31
  18. package/dist/channels/aun.js +418 -235
  19. package/dist/channels/daemon.js +13 -0
  20. package/dist/cli/agent-command.js +72 -17
  21. package/dist/cli/agent.js +249 -22
  22. package/dist/cli/aun-commands.js +270 -69
  23. package/dist/cli/bench.js +12 -3
  24. package/dist/cli/daemon-commands.js +52 -33
  25. package/dist/cli/fs-command.js +60 -0
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +106 -63
  28. package/dist/cli/init-cancel.js +208 -0
  29. package/dist/cli/init-channel.js +343 -195
  30. package/dist/cli/init.js +21 -9
  31. package/dist/cli/managed-command-guard.js +41 -0
  32. package/dist/cli/model.js +11 -4
  33. package/dist/cli/queue-command.js +11 -1
  34. package/dist/cli/response.js +71 -0
  35. package/dist/cli/restart-monitor.js +4 -18
  36. package/dist/cli/trigger-command.js +4 -0
  37. package/dist/config/builtin-roles.js +5 -0
  38. package/dist/config/contact-book-store.js +11 -3
  39. package/dist/config/contact-request-service.js +161 -15
  40. package/dist/config/gateway-config.js +26 -10
  41. package/dist/config/mention-mode.js +8 -24
  42. package/dist/core/agent-reload-coordinator.js +53 -0
  43. package/dist/core/auth/agent-delegation.js +0 -1
  44. package/dist/core/auth/operation-authorizer.js +62 -154
  45. package/dist/core/auth/operation-catalog.js +264 -22
  46. package/dist/core/bootstrap-messages.js +68 -0
  47. package/dist/core/bootstrap-service.js +144 -10
  48. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  49. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  50. package/dist/core/capability/skill-discovery.js +55 -0
  51. package/dist/core/channel-loader.js +24 -7
  52. package/dist/core/command/agent-control.js +14 -11
  53. package/dist/core/command/cli-intent-parser.js +9 -0
  54. package/dist/core/command/command-handler.js +467 -6
  55. package/dist/core/command/connect-menu.js +15 -0
  56. package/dist/core/command/group-menu.js +4 -4
  57. package/dist/core/command/menu-catalog.js +366 -0
  58. package/dist/core/command/menu-handler.js +213 -110
  59. package/dist/core/command/menu-protocol.js +71 -0
  60. package/dist/core/command/slash-gate.js +5 -2
  61. package/dist/core/command/slash-handler.js +45 -25
  62. package/dist/core/daemon-file-cache.js +2 -1
  63. package/dist/core/data-migration.js +9 -2
  64. package/dist/core/evolagent-registry.js +125 -35
  65. package/dist/core/evolagent.js +12 -5
  66. package/dist/core/handoff/dispatcher.js +5 -2
  67. package/dist/core/handoff/runtime.js +145 -21
  68. package/dist/core/handoff/store.js +24 -0
  69. package/dist/core/handoff/types.js +1 -0
  70. package/dist/core/inference/text-inference.js +38 -4
  71. package/dist/core/message/logical-queue-bridge.js +1 -0
  72. package/dist/core/message/mention-schema.js +126 -0
  73. package/dist/core/message/message-bridge.js +237 -79
  74. package/dist/core/message/message-log.js +23 -0
  75. package/dist/core/message/message-queue.js +20 -4
  76. package/dist/core/message/response-engine.js +54 -36
  77. package/dist/core/model/model-catalog.js +143 -24
  78. package/dist/core/model/model-diagnostics.js +28 -10
  79. package/dist/core/permission/ec-command-parser.js +156 -10
  80. package/dist/core/permission/index.js +1 -0
  81. package/dist/core/permission/readonly-shell-query.js +532 -0
  82. package/dist/core/permission/sandbox-runtime.js +22 -1
  83. package/dist/core/permission/shell-environment.js +46 -0
  84. package/dist/core/permission/tool-policy.js +292 -111
  85. package/dist/core/protected-paths.js +17 -9
  86. package/dist/core/runner-reload-transaction.js +57 -0
  87. package/dist/core/session/session-manager.js +21 -2
  88. package/dist/eck/manifest-engine.js +39 -13
  89. package/dist/index.js +644 -180
  90. package/dist/ipc.js +246 -19
  91. package/dist/response-system/coordinator.js +6 -4
  92. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  93. package/dist/response-system/modes/single-session/index.js +14 -1
  94. package/dist/response-system/selector.js +6 -5
  95. package/dist/trigger/anomaly-store.js +4 -0
  96. package/dist/trigger/script-executor.js +1 -0
  97. package/dist/utils/aid-bind.js +3 -8
  98. package/dist/utils/codex-app-server-registry.js +90 -0
  99. package/dist/utils/codex-cli.js +5 -1
  100. package/dist/utils/cross-platform.js +15 -0
  101. package/dist/utils/log-writer.js +6 -10
  102. package/dist/utils/logger.js +5 -5
  103. package/dist/utils/npm-ops.js +5 -12
  104. package/dist/utils/tool-summary.js +11 -0
  105. package/kits/docs/channels/aun.md +1 -1
  106. package/kits/docs/context-assembly.md +2 -2
  107. package/kits/docs/evolcore/agent.md +11 -6
  108. package/kits/docs/evolcore/aid.md +14 -0
  109. package/kits/docs/evolcore/msg.md +13 -0
  110. package/kits/docs/prompt-loading-architecture.md +1 -2
  111. package/kits/docs/venues/group.md +1 -1
  112. package/kits/eck_manifest.json +0 -12
  113. package/kits/rules/01-overview.md +9 -0
  114. package/kits/rules/04-relation.md +1 -1
  115. package/kits/rules/05-venue.md +2 -2
  116. package/kits/schemas/agent-config.schema.3.json +1 -1
  117. package/kits/schemas/agent-config.schema.4.json +1 -1
  118. package/kits/schemas/relation-config.schema.2.json +1 -1
  119. package/kits/schemas/role-config.schema.1.json +1 -1
  120. package/kits/schemas/single-session.schema.1.json +3 -3
  121. package/kits/schemas/single-session.schema.2.json +3 -3
  122. package/kits/templates/roles/admin.json +27 -0
  123. package/kits/templates/roles/member.json +44 -0
  124. package/kits/templates/roles/visitor.json +40 -2
  125. package/kits/templates/system-fragments/bootstrap.md +12 -6
  126. package/kits/templates/system-fragments/channel.md +6 -0
  127. package/kits/templates/system-fragments/session.md +2 -0
  128. package/package.json +3 -2
  129. package/skills/eclink/SKILL.md +15 -3
  130. package/skills/eclink/agents/openai.yaml +3 -3
  131. package/dist/core/command/evol-menu-version-gate.js +0 -39
@@ -52,7 +52,7 @@ export class EvolAgent {
52
52
  const ba = this.baseagent;
53
53
  // 动态读取配置(基于 fileCache + mtime),使配置变更立即体现在显示中
54
54
  try {
55
- const effective = resolveEffective({ self: this.aid }, { cache: true });
55
+ const effective = resolveEffective({ self: this.aid }, { cache: true, expand: true });
56
56
  const block = effective.baseagents?.[ba];
57
57
  return block?.model;
58
58
  }
@@ -66,7 +66,7 @@ export class EvolAgent {
66
66
  const ba = this.baseagent;
67
67
  // 动态读取配置(基于 fileCache + mtime),使配置变更立即体现在显示中
68
68
  try {
69
- const effective = resolveEffective({ self: this.aid }, { cache: true });
69
+ const effective = resolveEffective({ self: this.aid }, { cache: true, expand: true });
70
70
  const block = effective.baseagents?.[ba];
71
71
  if (ba === 'codex')
72
72
  return block?.effort ?? block?.reasoning;
@@ -257,7 +257,8 @@ export class EvolAgent {
257
257
  this.persist();
258
258
  }
259
259
  setLifecycle(value) {
260
- this.rawAgent = withLifecycleForWrite(this.rawAgent, value);
260
+ const current = cfgRead(ConfigTarget.Agent, { self: this.aid });
261
+ this.rawAgent = withLifecycleForWrite(current || this.rawAgent, value);
261
262
  this.rawAgent.$schema_version = Math.max(this.rawAgent.$schema_version || 0, this.merged.$schema_version || 0);
262
263
  this.merged.lifecycle = value;
263
264
  delete this.merged.initialized;
@@ -275,7 +276,8 @@ export class EvolAgent {
275
276
  */
276
277
  getPersona() {
277
278
  const personaPath = path.join(agentPersonalDir(this.aid), 'persona.md');
278
- return fileCache.get(personaPath, (raw) => (raw === null ? null : (raw.trim() || null)), { policy: 'mtime', group: this.agentFilesGroup() });
279
+ const raw = fileCache.getText(personaPath, { policy: 'mtime', group: this.agentFilesGroup() });
280
+ return raw === null ? null : (raw.trim() || null);
279
281
  }
280
282
  /**
281
283
  * 读取 personal/memory/working.md 内容。走 fileCache(mtime 门控):
@@ -284,7 +286,8 @@ export class EvolAgent {
284
286
  */
285
287
  getWorkingMemory() {
286
288
  const workingPath = path.join(agentPersonalDir(this.aid), 'memory', 'working.md');
287
- return fileCache.get(workingPath, (raw) => (raw === null ? null : (raw.trim() || null)), { policy: 'mtime', group: this.agentFilesGroup() });
289
+ const raw = fileCache.getText(workingPath, { policy: 'mtime', group: this.agentFilesGroup() });
290
+ return raw === null ? null : (raw.trim() || null);
288
291
  }
289
292
  /** 清除本 agent 身份层缓存(reload 后重新读取)。只失效自己的文件组,不波及他人。 */
290
293
  invalidatePersonaCache() {
@@ -318,6 +321,10 @@ export class EvolAgent {
318
321
  this.rawAgent = rawAgent;
319
322
  this.merged = merged;
320
323
  }
324
+ /** Reload uses the current object references as an in-memory rollback snapshot. */
325
+ captureConfigSnapshot() {
326
+ return { rawAgent: this.rawAgent, merged: this.merged };
327
+ }
321
328
  // ── 内部辅助 ─────────────────────────────────────────────────────────
322
329
  /**
323
330
  * 找 rawAgent.channels 里的可变实例,用于写入。
@@ -117,8 +117,9 @@ export class HandoffDispatcher {
117
117
  if (this.pausedAgents.has(selfAid))
118
118
  return false;
119
119
  const current = this.store.get(selfAid, next.handoff_id);
120
- if (!current || current.state !== 'queued' || current.attention_required)
121
- return current?.state === 'target_sent';
120
+ if (!current || current.state !== 'queued' || current.attention_required) {
121
+ return current?.state === 'target_sent' || current?.state === 'discarded';
122
+ }
122
123
  this.store.recordSendStarted(selfAid, current.handoff_id);
123
124
  try {
124
125
  const send = await this.sendWithinQueueDeadline(current);
@@ -171,6 +172,8 @@ export class HandoffDispatcher {
171
172
  await new Promise(resolve => setTimeout(resolve, this.retryDelaysMs[Math.min(attempt - 1, this.retryDelaysMs.length - 1)] ?? 0));
172
173
  }
173
174
  }
175
+ if (this.store.get(selfAid, next.handoff_id)?.state === 'discarded')
176
+ continue;
174
177
  if (!sent) {
175
178
  this.store.recordSendExhausted(selfAid, next.handoff_id);
176
179
  logger.error(`[Handoff] target send retries exhausted: ${next.handoff_id}`);
@@ -7,6 +7,7 @@ import { createRootCausation, deriveCausation, normalizeCausation } from '../cau
7
7
  import { recordCausationSpan } from '../causation/audit.js';
8
8
  import { logger } from '../../utils/logger.js';
9
9
  const HANDOFF_STALE_WARN_MS = 72 * 60 * 60 * 1000;
10
+ const HANDOFF_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
10
11
  const HANDOFF_LARGE_AGGREGATE_COUNT = 10;
11
12
  const HANDOFF_LARGE_AGGREGATE_CHARS = 50_000;
12
13
  export class HandoffTargetBlockedError extends Error {
@@ -43,6 +44,15 @@ export class TriggerHandoffRequiredForbiddenError extends Error {
43
44
  this.name = 'TriggerHandoffRequiredForbiddenError';
44
45
  }
45
46
  }
47
+ export class HandoffSessionReplacedError extends Error {
48
+ sessionId;
49
+ code = 'HANDOFF_SESSION_REPLACED';
50
+ constructor(sessionId) {
51
+ super(`handoff session was replaced during creation: ${sessionId}`);
52
+ this.sessionId = sessionId;
53
+ this.name = 'HandoffSessionReplacedError';
54
+ }
55
+ }
46
56
  export class HandoffRuntime {
47
57
  sessionManager;
48
58
  messageQueue;
@@ -52,6 +62,8 @@ export class HandoffRuntime {
52
62
  now;
53
63
  staleWarned = new Set();
54
64
  largeAggregateWarned = new Set();
65
+ lifecycleSequence = 0;
66
+ sessionDiscardSequence = new Map();
55
67
  constructor(sessionManager, messageQueue, sender, store = new HandoffStore(), options = {}) {
56
68
  this.sessionManager = sessionManager;
57
69
  this.messageQueue = messageQueue;
@@ -60,6 +72,7 @@ export class HandoffRuntime {
60
72
  this.dispatcher = new HandoffDispatcher(store, this.mutexes, sender, options);
61
73
  }
62
74
  async createOutbound(input) {
75
+ const operationSequence = ++this.lifecycleSequence;
63
76
  throwIfAborted(input.signal);
64
77
  const origin = await this.sessionManager.getSessionById(input.originSessionId);
65
78
  throwIfAborted(input.signal);
@@ -81,24 +94,29 @@ export class HandoffRuntime {
81
94
  if (input.explicitReturnPolicy === 'required' && input.causation?.trigger?.path.length) {
82
95
  throw new TriggerHandoffRequiredForbiddenError();
83
96
  }
84
- await this.dispatcher.expireTargetQueued(input.selfAid, target.id);
85
- throwIfAborted(input.signal);
86
- const head = this.store.listByTarget(input.selfAid, target.id, 'queued')[0];
87
- if (head?.attention_required) {
88
- throw new HandoffTargetBlockedError(head.handoff_id, head.attention_reason ?? 'UNKNOWN_ATTENTION');
89
- }
90
97
  const handoffCausation = deriveCausation(normalizeCausation(input.causation) ?? createRootCausation());
91
- const handoff = this.store.create({
92
- selfAid: input.selfAid,
93
- originSessionId: origin.id,
94
- originMessageId: input.originMessageId,
95
- targetSessionId: target.id,
96
- payload: input.payload,
97
- encrypt: input.encrypt,
98
- returnPolicy: input.explicitReturnPolicy ?? 'required',
99
- originAuthorization: input.originAuthorization,
100
- causation: handoffCausation,
101
- now: this.now?.(),
98
+ const handoff = await this.mutexes.forKey(this.targetKey(input.selfAid, target.id)).runExclusive(async () => {
99
+ const replacedSessionId = [origin.id, target.id].find(sessionId => ((this.sessionDiscardSequence.get(this.sessionKey(input.selfAid, sessionId)) ?? 0) > operationSequence));
100
+ if (replacedSessionId)
101
+ throw new HandoffSessionReplacedError(replacedSessionId);
102
+ this.dispatcher.expireQueued(input.selfAid, target.id);
103
+ throwIfAborted(input.signal);
104
+ const head = this.store.listByTarget(input.selfAid, target.id, 'queued')[0];
105
+ if (head?.attention_required) {
106
+ throw new HandoffTargetBlockedError(head.handoff_id, head.attention_reason ?? 'UNKNOWN_ATTENTION');
107
+ }
108
+ return this.store.create({
109
+ selfAid: input.selfAid,
110
+ originSessionId: origin.id,
111
+ originMessageId: input.originMessageId,
112
+ targetSessionId: target.id,
113
+ payload: input.payload,
114
+ encrypt: input.encrypt,
115
+ returnPolicy: input.explicitReturnPolicy ?? 'required',
116
+ originAuthorization: input.originAuthorization,
117
+ causation: handoffCausation,
118
+ now: this.now?.(),
119
+ });
102
120
  });
103
121
  recordCausationSpan(handoffCausation, 'handoff.request', {
104
122
  status: 'started',
@@ -106,6 +124,10 @@ export class HandoffRuntime {
106
124
  });
107
125
  await this.dispatcher.drain(input.selfAid, target.id);
108
126
  const dispatched = this.store.get(input.selfAid, handoff.handoff_id) ?? handoff;
127
+ const replacedSessionId = [origin.id, target.id].find(sessionId => ((this.sessionDiscardSequence.get(this.sessionKey(input.selfAid, sessionId)) ?? 0) > operationSequence));
128
+ if (dispatched.state === 'discarded' || replacedSessionId) {
129
+ throw new HandoffSessionReplacedError(replacedSessionId ?? target.id);
130
+ }
109
131
  if (dispatched.state === 'failed') {
110
132
  throw new HandoffDeliveryFailedError(dispatched.handoff_id);
111
133
  }
@@ -142,7 +164,9 @@ export class HandoffRuntime {
142
164
  }
143
165
  markBindingIncomplete(selfAid, handoffId) {
144
166
  const instance = this.store.get(selfAid, handoffId);
145
- if (instance && !instance.attention_required) {
167
+ if (instance
168
+ && (instance.state === 'target_context_pending' || instance.state === 'return_pending')
169
+ && !instance.attention_required) {
146
170
  this.store.markAttention(selfAid, handoffId, 'REPLY_BINDING_INCOMPLETE');
147
171
  }
148
172
  }
@@ -207,16 +231,31 @@ export class HandoffRuntime {
207
231
  role: origin.chatType === 'group' ? (origin.identity?.role ?? 'none') : undefined,
208
232
  sessionKeyField: origin.sessionKey,
209
233
  selfAID: origin.selfAID,
210
- onPersisted: () => this.store.recordOriginEnqueued(selfAid, handoffId, messageId),
234
+ onPersisted: () => {
235
+ if (this.store.get(selfAid, handoffId)?.state !== 'origin_queued') {
236
+ throw new Error(`handoff is no longer deliverable: ${handoffId}`);
237
+ }
238
+ this.store.recordOriginEnqueued(selfAid, handoffId, messageId);
239
+ },
211
240
  });
212
241
  }
213
242
  catch (error) {
214
- this.store.recordOriginEnqueueFailed(selfAid, handoffId, error instanceof Error ? error.message : String(error));
243
+ if (this.store.get(selfAid, handoffId)?.state === 'origin_queued') {
244
+ this.store.recordOriginEnqueueFailed(selfAid, handoffId, error instanceof Error ? error.message : String(error));
245
+ }
215
246
  }
216
247
  }
217
248
  buildPromptItem(message) {
218
249
  return this.buildPromptItems(message)[0] ?? null;
219
250
  }
251
+ isDiscardedDelivery(message) {
252
+ const delivery = message.handoffDelivery;
253
+ const selfAid = message.selfAID;
254
+ if (!delivery || !selfAid)
255
+ return false;
256
+ const ids = delivery.handoffIds?.length ? delivery.handoffIds : [delivery.handoffId];
257
+ return ids.length > 0 && ids.every(handoffId => this.store.get(selfAid, handoffId)?.state === 'discarded');
258
+ }
220
259
  buildPromptItems(message) {
221
260
  const delivery = message.handoffDelivery;
222
261
  const selfAid = message.selfAID;
@@ -367,9 +406,54 @@ export class HandoffRuntime {
367
406
  expired += this.dispatcher.expireQueued(selfAid).length;
368
407
  return expired;
369
408
  }
370
- async recover(selfAids) {
409
+ async discardSession(selfAid, sessionId) {
410
+ this.sessionDiscardSequence.set(this.sessionKey(selfAid, sessionId), ++this.lifecycleSequence);
411
+ const candidates = this.store.list(selfAid)
412
+ .filter(instance => (instance.origin_session_id === sessionId || instance.target_session_id === sessionId) && instance.state !== 'failed' && instance.state !== 'completed' && instance.state !== 'discarded');
413
+ let discarded = 0;
414
+ for (const instance of candidates) {
415
+ const result = await this.discardWithTargetLock(selfAid, instance.handoff_id, 'session_replaced', {
416
+ session_id: sessionId,
417
+ });
418
+ if (result)
419
+ discarded++;
420
+ }
421
+ return discarded;
422
+ }
423
+ async discardExpired(selfAids, retentionMs = HANDOFF_RETENTION_MS) {
424
+ const now = this.now?.() ?? Date.now();
425
+ let discarded = 0;
426
+ for (const selfAid of selfAids) {
427
+ const latestEventVersions = new Map();
428
+ for (const event of this.store.events(selfAid)) {
429
+ if (!event.handoff_id || event.to_version === undefined)
430
+ continue;
431
+ latestEventVersions.set(event.handoff_id, Math.max(latestEventVersions.get(event.handoff_id) ?? 0, event.to_version));
432
+ }
433
+ for (const instance of this.store.list(selfAid)) {
434
+ if (instance.state === 'failed' || instance.state === 'completed' || instance.state === 'discarded')
435
+ continue;
436
+ if (now - instance.created_at <= retentionMs)
437
+ continue;
438
+ if ((latestEventVersions.get(instance.handoff_id) ?? 0) !== instance.version)
439
+ continue;
440
+ const result = await this.discardWithTargetLock(selfAid, instance.handoff_id, 'retention_expired', {
441
+ retention_ms: retentionMs,
442
+ age_ms: now - instance.created_at,
443
+ }, instance.version, now);
444
+ if (result) {
445
+ discarded++;
446
+ logger.info(`[Handoff] discarded expired handoff: ${instance.handoff_id}`);
447
+ }
448
+ }
449
+ }
450
+ return discarded;
451
+ }
452
+ async recover(selfAids, options = {}) {
371
453
  const originEnqueues = [];
372
454
  for (const selfAid of selfAids) {
455
+ if (options.discardExpired !== false)
456
+ await this.discardExpired([selfAid]);
373
457
  // v1 initially represented return=none as return_pending and required an
374
458
  // empty manual return. It has already been shown to a target turn, so
375
459
  // finish it before version reconciliation instead of prompting again.
@@ -412,6 +496,40 @@ export class HandoffRuntime {
412
496
  }
413
497
  await Promise.all(originEnqueues);
414
498
  }
499
+ async discardWithTargetLock(selfAid, handoffId, reason, details, expectedVersion, now = this.now?.()) {
500
+ const snapshot = this.store.get(selfAid, handoffId);
501
+ if (!snapshot)
502
+ return false;
503
+ const discarded = await this.mutexes.forKey(this.targetKey(selfAid, snapshot.target_session_id)).runExclusive(async () => {
504
+ const current = this.store.get(selfAid, handoffId);
505
+ if (!current || current.state === 'failed' || current.state === 'completed' || current.state === 'discarded') {
506
+ return null;
507
+ }
508
+ if (expectedVersion !== undefined && current.version !== expectedVersion)
509
+ return null;
510
+ this.store.discard(selfAid, handoffId, reason, details, now);
511
+ return current;
512
+ });
513
+ if (!discarded)
514
+ return false;
515
+ await this.cancelOriginDelivery(discarded);
516
+ return true;
517
+ }
518
+ async cancelOriginDelivery(instance) {
519
+ if (instance.state !== 'origin_queued' && instance.state !== 'origin_delivered')
520
+ return;
521
+ const messageId = instance.origin_delivery_message_id
522
+ ?? `origin-deliver:${instance.handoff_id}:${instance.origin_session_id}`;
523
+ const queue = this.messageQueue;
524
+ try {
525
+ const queued = queue.cancelMessageByIdInSession?.(instance.origin_session_id, messageId) ?? false;
526
+ if (!queued)
527
+ await queue.cancelActiveAndWait?.(messageId);
528
+ }
529
+ catch (error) {
530
+ logger.warn(`[Handoff] failed to cancel discarded origin delivery ${messageId}:`, error);
531
+ }
532
+ }
415
533
  warnIfStale(instance) {
416
534
  if (instance.state !== 'target_sent' && instance.state !== 'return_pending')
417
535
  return;
@@ -425,6 +543,12 @@ export class HandoffRuntime {
425
543
  + ` ageHours=${Math.floor(ageMs / 3_600_000)}`
426
544
  + ` originSession=${instance.origin_session_id} targetSession=${instance.target_session_id}`);
427
545
  }
546
+ targetKey(selfAid, targetSessionId) {
547
+ return `${selfAid}\u0000${targetSessionId}`;
548
+ }
549
+ sessionKey(selfAid, sessionId) {
550
+ return `${selfAid}\u0000${sessionId}`;
551
+ }
428
552
  warnIfLargeAggregate(message, instances, direction) {
429
553
  const chars = message.content.length
430
554
  + instances.reduce((total, instance) => total + this.requestSummary(instance).length, 0);
@@ -9,6 +9,7 @@ import { HANDOFF_SCHEMA_VERSION, HANDOFF_QUERY_DEFAULT_LIMIT, HANDOFF_QUERY_MAX_
9
9
  import { normalizeCausation } from '../causation/context.js';
10
10
  const SAFE_ID_RE = /^[A-Za-z0-9._-]+$/;
11
11
  export const HANDOFF_ID_RE = /^h-[A-Za-z0-9._-]+$/;
12
+ const TERMINAL_HANDOFF_STATES = new Set(['failed', 'completed', 'discarded']);
12
13
  function safeSegment(value, field) {
13
14
  if (!value || !SAFE_ID_RE.test(value) || value === '.' || value === '..') {
14
15
  throw new Error(`invalid ${field}`);
@@ -514,6 +515,29 @@ export class HandoffStore {
514
515
  this.write(selfAid, instance);
515
516
  return instance;
516
517
  }
518
+ discard(selfAid, handoffId, reason, details = {}, now = Date.now()) {
519
+ const instance = this.get(selfAid, handoffId);
520
+ if (!instance)
521
+ throw new Error('handoff not found');
522
+ if (TERMINAL_HANDOFF_STATES.has(instance.state))
523
+ return instance;
524
+ const previousState = instance.state;
525
+ const previousVersion = instance.version;
526
+ instance.state = 'discarded';
527
+ instance.attention_required = false;
528
+ instance.attention_reason = null;
529
+ instance.version++;
530
+ instance.updated_at = now;
531
+ instance.completed_at = now;
532
+ this.appendEvent(selfAid, 'handoff_discarded', handoffId, {
533
+ fromVersion: previousVersion,
534
+ toVersion: instance.version,
535
+ details: { ...details, reason, previous_state: previousState },
536
+ now,
537
+ });
538
+ this.write(selfAid, instance);
539
+ return instance;
540
+ }
517
541
  requireState(selfAid, handoffId, expected) {
518
542
  const instance = this.get(selfAid, handoffId);
519
543
  if (!instance)
@@ -8,6 +8,7 @@ export const HANDOFF_STATES = [
8
8
  'origin_queued',
9
9
  'origin_delivered',
10
10
  'completed',
11
+ 'discarded',
11
12
  ];
12
13
  export const HANDOFF_QUERY_DEFAULT_LIMIT = 100;
13
14
  export const HANDOFF_QUERY_MAX_LIMIT = 500;
@@ -1,4 +1,5 @@
1
1
  import { normalizeBaseagent, resolveAnthropicDirectConfig, resolveEcagentConfig, resolveOpenaiDirectConfig, } from '../../agents/baseagent.js';
2
+ import { buildModelRequestHeaders } from '../../agents/request-identity.js';
2
3
  function apiEndpoint(baseUrl, resource) {
3
4
  const configured = baseUrl?.trim();
4
5
  if (!configured)
@@ -119,12 +120,25 @@ export class EcagentTextInferenceProvider {
119
120
  baseagent = 'ecagent';
120
121
  constructor(config) {
121
122
  this.config = config;
123
+ this.config = {
124
+ ...config,
125
+ headers: buildModelRequestHeaders({
126
+ baseagent: 'ecagent',
127
+ baseUrl: config.baseUrl,
128
+ agentAid: config.evolcoreAgentAid,
129
+ configuredHeaders: config.headers,
130
+ }),
131
+ };
122
132
  }
123
133
  async completeText(request) {
124
134
  const response = await fetch(apiEndpoint(this.config.baseUrl, 'chat/completions'), {
125
135
  method: 'POST',
126
136
  signal: request.signal,
127
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.config.apiKey}` },
137
+ headers: {
138
+ 'Content-Type': 'application/json',
139
+ Authorization: `Bearer ${this.config.apiKey}`,
140
+ ...this.config.headers,
141
+ },
128
142
  body: JSON.stringify({
129
143
  model: request.model,
130
144
  messages: [{ role: 'system', content: request.system }, { role: 'user', content: request.input }],
@@ -150,15 +164,35 @@ export function createTextInferenceProvider(baseagent, config) {
150
164
  const canonical = normalizeBaseagent(baseagent).canonical;
151
165
  if (canonical === 'claude') {
152
166
  const resolved = resolveAnthropicDirectConfig(config.baseagents?.claude);
153
- return resolved ? new AnthropicTextInferenceProvider(resolved) : undefined;
167
+ return resolved ? new AnthropicTextInferenceProvider({
168
+ ...resolved,
169
+ headers: buildModelRequestHeaders({
170
+ baseagent: 'claude',
171
+ baseUrl: resolved.baseUrl,
172
+ agentAid: config.aid,
173
+ configuredHeaders: resolved.headers,
174
+ }),
175
+ }) : undefined;
154
176
  }
155
177
  if (canonical === 'codex') {
156
178
  const resolved = resolveOpenaiDirectConfig(config.baseagents?.codex);
157
- return resolved ? new OpenAITextInferenceProvider(resolved) : undefined;
179
+ return resolved ? new OpenAITextInferenceProvider({
180
+ ...resolved,
181
+ headers: buildModelRequestHeaders({
182
+ baseagent: 'codex',
183
+ baseUrl: resolved.baseUrl,
184
+ agentAid: config.aid,
185
+ configuredHeaders: resolved.headers,
186
+ }),
187
+ }) : undefined;
158
188
  }
159
189
  if (canonical === 'ecagent') {
160
190
  try {
161
- const resolved = resolveEcagentConfig({ agents: { ecagent: config.baseagents?.ecagent } }, config.baseagents?.ecagent);
191
+ const override = {
192
+ ...(config.baseagents?.ecagent ?? {}),
193
+ evolcoreAgentAid: config.aid,
194
+ };
195
+ const resolved = resolveEcagentConfig({ agents: { ecagent: override } }, override);
162
196
  return new EcagentTextInferenceProvider(resolved);
163
197
  }
164
198
  catch {
@@ -110,6 +110,7 @@ export class LogicalQueueBridge {
110
110
  peerId: message.peerId,
111
111
  content: message.content,
112
112
  chatType: message.chatType ?? 'private',
113
+ mentions: message.mentions,
113
114
  isMentioned: message.isMentioned,
114
115
  mentionAids: message.mentionAids,
115
116
  };
@@ -0,0 +1,126 @@
1
+ export class MentionSchemaError extends Error {
2
+ code = 'INVALID_MENTION_SCHEMA';
3
+ index;
4
+ constructor(message, index) {
5
+ super(index === undefined ? message : `mentions[${index}]: ${message}`);
6
+ this.name = 'MentionSchemaError';
7
+ this.index = index;
8
+ }
9
+ }
10
+ function isRecord(value) {
11
+ return !!value && typeof value === 'object' && !Array.isArray(value);
12
+ }
13
+ function hasOwn(value, key) {
14
+ return Object.prototype.hasOwnProperty.call(value, key);
15
+ }
16
+ function assertMetadata(value, index) {
17
+ if (hasOwn(value, 'display') && typeof value.display !== 'string') {
18
+ throw new MentionSchemaError('display must be a string', index);
19
+ }
20
+ for (const key of ['offset', 'length']) {
21
+ if (!hasOwn(value, key))
22
+ continue;
23
+ const field = value[key];
24
+ if (typeof field !== 'number' || !Number.isInteger(field) || field < 0) {
25
+ throw new MentionSchemaError(`${key} must be a non-negative integer`, index);
26
+ }
27
+ }
28
+ for (const key of ['name', 'key']) {
29
+ if (hasOwn(value, key) && typeof value[key] !== 'string') {
30
+ throw new MentionSchemaError(`${key} must be a string`, index);
31
+ }
32
+ }
33
+ }
34
+ /**
35
+ * Validate and copy canonical mention entries.
36
+ *
37
+ * This is intentionally strict: the old string array format is rejected
38
+ * instead of being converted, because conversion can hide a producer bug and
39
+ * make routing appear to work with the wrong wire contract.
40
+ */
41
+ export function normalizeMentionEntries(raw) {
42
+ if (raw === undefined)
43
+ return [];
44
+ if (!Array.isArray(raw)) {
45
+ throw new MentionSchemaError('mentions must be an array');
46
+ }
47
+ const normalized = [];
48
+ for (const [index, value] of raw.entries()) {
49
+ if (typeof value === 'string') {
50
+ throw new MentionSchemaError('legacy string entries are not supported; use {aid} or {scope:"all"}', index);
51
+ }
52
+ if (!isRecord(value)) {
53
+ throw new MentionSchemaError('entry must be an object', index);
54
+ }
55
+ const hasScope = hasOwn(value, 'scope');
56
+ const hasAid = hasOwn(value, 'aid');
57
+ const hasUserId = hasOwn(value, 'userId');
58
+ const discriminatorCount = Number(hasScope) + Number(hasAid) + Number(hasUserId);
59
+ if (discriminatorCount !== 1) {
60
+ throw new MentionSchemaError('entry must contain exactly one of scope, aid, or userId', index);
61
+ }
62
+ assertMetadata(value, index);
63
+ if (hasScope) {
64
+ if (value.scope !== 'all') {
65
+ throw new MentionSchemaError('scope must be "all"', index);
66
+ }
67
+ normalized.push({ ...value, scope: 'all' });
68
+ continue;
69
+ }
70
+ if (hasAid) {
71
+ if (typeof value.aid !== 'string' || value.aid.length === 0 || value.aid.trim() !== value.aid) {
72
+ throw new MentionSchemaError('aid must be a non-empty trimmed string', index);
73
+ }
74
+ if (value.aid === 'all') {
75
+ throw new MentionSchemaError('all is a scope, not an aid; use {scope:"all"}', index);
76
+ }
77
+ normalized.push({ ...value, aid: value.aid });
78
+ continue;
79
+ }
80
+ if (typeof value.userId !== 'string' || value.userId.length === 0 || value.userId.trim() !== value.userId) {
81
+ throw new MentionSchemaError('userId must be a non-empty trimmed string', index);
82
+ }
83
+ if (value.userId === 'all') {
84
+ throw new MentionSchemaError('all is a scope, not a userId; use {scope:"all"}', index);
85
+ }
86
+ normalized.push({ ...value, userId: value.userId });
87
+ }
88
+ return normalized;
89
+ }
90
+ export function mentionEntryIsAll(entry) {
91
+ return 'scope' in entry && entry.scope === 'all';
92
+ }
93
+ /** Return all concrete target IDs, including channel-native userId entries. */
94
+ export function mentionEntryTargets(entries) {
95
+ const targets = [];
96
+ const seen = new Set();
97
+ for (const entry of entries) {
98
+ const target = 'aid' in entry && typeof entry.aid === 'string'
99
+ ? entry.aid
100
+ : 'userId' in entry && typeof entry.userId === 'string'
101
+ ? entry.userId
102
+ : undefined;
103
+ if (!target || target === 'all' || seen.has(target))
104
+ continue;
105
+ seen.add(target);
106
+ targets.push(target);
107
+ }
108
+ return targets;
109
+ }
110
+ export function hasMentionAll(entries) {
111
+ return entries.some(mentionEntryIsAll);
112
+ }
113
+ /** IDs used by renderers; keeps `all` visible as a derived value. */
114
+ export function mentionEntryAids(entries) {
115
+ const aids = mentionEntryTargets(entries);
116
+ if (hasMentionAll(entries))
117
+ aids.push('all');
118
+ return aids;
119
+ }
120
+ export function mentionEntryKey(entry) {
121
+ if ('scope' in entry && entry.scope === 'all')
122
+ return 'scope:all';
123
+ if ('aid' in entry)
124
+ return `aid:${entry.aid}`;
125
+ return `user:${entry.userId}`;
126
+ }