evolcore 0.0.2 → 0.0.4

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 (206) hide show
  1. package/CHANGELOG.md +93 -778
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +198 -48
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +203 -8
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/aun/outbox.js +8 -0
  14. package/dist/channels/aun.js +241 -116
  15. package/dist/channels/daemon.js +18 -4
  16. package/dist/channels/dingtalk.js +1005 -260
  17. package/dist/channels/feishu.js +184 -56
  18. package/dist/channels/qqbot.js +23 -1
  19. package/dist/channels/wechat.js +303 -155
  20. package/dist/channels/wecom.js +1132 -164
  21. package/dist/cli/agent-command.js +2 -1
  22. package/dist/cli/aun-commands.js +98 -43
  23. package/dist/cli/bench.js +2 -2
  24. package/dist/cli/contact.js +71 -0
  25. package/dist/cli/ctl-command.js +2 -2
  26. package/dist/cli/daemon-commands.js +77 -214
  27. package/dist/cli/handoff-command.js +4 -3
  28. package/dist/cli/help.js +9 -5
  29. package/dist/cli/index.js +132 -116
  30. package/dist/cli/init-channel.js +242 -129
  31. package/dist/cli/init.js +70 -31
  32. package/dist/cli/model.js +2 -1
  33. package/dist/cli/net-check.js +2 -2
  34. package/dist/cli/queue-command.js +30 -6
  35. package/dist/cli/raw-key-input.js +25 -0
  36. package/dist/cli/response.js +5 -6
  37. package/dist/cli/restart-monitor.js +25 -1
  38. package/dist/cli/stats.js +6 -4
  39. package/dist/cli/trigger-command.js +146 -25
  40. package/dist/cli/version.js +6 -1
  41. package/dist/config/access-policy-domain.js +38 -0
  42. package/dist/config/access-policy.js +134 -0
  43. package/dist/config/builtin-role-templates.js +42 -17
  44. package/dist/config/builtin-roles.js +31 -6
  45. package/dist/config/config-field-policy.js +17 -5
  46. package/dist/config/config-manager.js +409 -29
  47. package/dist/config/config-operation-service.js +35 -38
  48. package/dist/config/contact-alias.js +68 -0
  49. package/dist/config/contact-bind-code.js +274 -0
  50. package/dist/config/contact-book-store.js +622 -0
  51. package/dist/config/contact-book-v2-startup.js +35 -0
  52. package/dist/config/contact-book.js +185 -300
  53. package/dist/config/contact-operation-service.js +116 -0
  54. package/dist/config/contact-request-service.js +331 -0
  55. package/dist/config/peer-role-resolver.js +136 -55
  56. package/dist/config/role-ranks.js +18 -0
  57. package/dist/config/role-service.js +16 -19
  58. package/dist/config/role-store.js +16 -5
  59. package/dist/config/roles.js +10 -1
  60. package/dist/config/schema-registry.js +49 -24
  61. package/dist/config-store.js +14 -4
  62. package/dist/core/auth/agent-delegation.js +105 -11
  63. package/dist/core/auth/authorization-audit.js +16 -1
  64. package/dist/core/auth/operation-authorizer.js +10 -0
  65. package/dist/core/auth/operation-catalog.js +105 -1
  66. package/dist/core/auth/trigger-authorization.js +15 -0
  67. package/dist/core/bootstrap-service.js +2 -9
  68. package/dist/core/channel-loader.js +6 -2
  69. package/dist/core/command/command-handler.js +113 -21
  70. package/dist/core/command/connect-menu.js +599 -0
  71. package/dist/core/command/evol-menu-version-gate.js +38 -0
  72. package/dist/core/command/group-menu.js +421 -0
  73. package/dist/core/command/menu-handler.js +382 -100
  74. package/dist/core/command/menu-protocol.js +8 -2
  75. package/dist/core/command/menu-token-store.js +102 -0
  76. package/dist/core/command/role-menu.js +128 -29
  77. package/dist/core/command/slash-gate.js +1 -1
  78. package/dist/core/command/slash-handler.js +186 -49
  79. package/dist/core/daemon-file-cache.js +40 -6
  80. package/dist/core/event-catalog.js +99 -0
  81. package/dist/core/evolagent-registry.js +4 -40
  82. package/dist/core/evolagent.js +20 -9
  83. package/dist/core/handoff/runtime.js +162 -37
  84. package/dist/core/handoff/store.js +46 -2
  85. package/dist/core/handoff/types.js +1 -0
  86. package/dist/core/inference/text-inference.js +16 -74
  87. package/dist/core/message/file-markers.js +7 -0
  88. package/dist/core/message/im-renderer.js +20 -14
  89. package/dist/core/message/inbound-admission.js +124 -0
  90. package/dist/core/message/message-bridge.js +670 -57
  91. package/dist/core/message/message-log.js +1 -0
  92. package/dist/core/message/message-queue.js +195 -10
  93. package/dist/core/message/message-utils.js +8 -2
  94. package/dist/core/message/peer-mode.js +7 -8
  95. package/dist/core/message/response-engine.js +642 -96
  96. package/dist/core/message/send-receipt.js +24 -0
  97. package/dist/core/message/stream-debouncer.js +11 -2
  98. package/dist/core/permission/approval-gateway.js +17 -10
  99. package/dist/core/permission/ec-command-parser.js +101 -46
  100. package/dist/core/permission/tool-policy.js +115 -38
  101. package/dist/core/protected-paths.js +38 -11
  102. package/dist/core/session/session-fs-store.js +44 -3
  103. package/dist/core/session/session-manager.js +86 -8
  104. package/dist/core/session/session-mapper.js +2 -0
  105. package/dist/core/session/session-renew.js +125 -69
  106. package/dist/core/session/session-turn-coordinator.js +5 -1
  107. package/dist/eck/kit-renderer.js +12 -1
  108. package/dist/eck/message-renderer.js +79 -1
  109. package/dist/index.js +285 -94
  110. package/dist/ipc.js +114 -8
  111. package/dist/paths.js +3 -0
  112. package/dist/response-system/config-resolver.js +29 -0
  113. package/dist/response-system/coordinator.js +8 -32
  114. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  115. package/dist/response-system/index.js +1 -0
  116. package/dist/response-system/modes/single-session/index.js +7 -4
  117. package/dist/stats/billing.js +20 -8
  118. package/dist/trigger/manager.js +3 -0
  119. package/dist/trigger/parser.js +126 -11
  120. package/dist/trigger/patch.js +12 -2
  121. package/dist/trigger/scheduler.js +444 -40
  122. package/dist/trigger/validation.js +13 -0
  123. package/dist/utils/aid-bind.js +43 -29
  124. package/dist/utils/error-dict.json +7 -0
  125. package/dist/utils/evolcore-version.js +21 -0
  126. package/dist/utils/instance-registry.js +14 -7
  127. package/dist/utils/log-writer.js +46 -0
  128. package/dist/utils/logger.js +2 -2
  129. package/dist/utils/media-cache.js +4 -1
  130. package/dist/utils/model-prices.jsonl +6 -3
  131. package/dist/utils/restart-safety.js +31 -0
  132. package/dist/utils/stable-semver.js +21 -0
  133. package/dist/utils/stats.js +33 -9
  134. package/dist/utils/system-memory.js +62 -0
  135. package/kits/docs/INDEX.md +2 -1
  136. package/kits/docs/channels/aun.md +4 -13
  137. package/kits/docs/evolcore/INDEX.md +5 -3
  138. package/kits/docs/evolcore/agent.md +9 -1
  139. package/kits/docs/evolcore/aid.md +5 -2
  140. package/kits/docs/evolcore/config.md +47 -2
  141. package/kits/docs/evolcore/contact.md +61 -0
  142. package/kits/docs/evolcore/fs.md +9 -0
  143. package/kits/docs/evolcore/group-rules.md +46 -4
  144. package/kits/docs/evolcore/group.md +15 -6
  145. package/kits/docs/evolcore/model.md +4 -1
  146. package/kits/docs/evolcore/msg.md +12 -6
  147. package/kits/docs/evolcore/response.md +16 -21
  148. package/kits/docs/evolcore/rpc.md +2 -0
  149. package/kits/docs/evolcore/stats.md +15 -2
  150. package/kits/docs/evolcore/storage.md +1 -0
  151. package/kits/docs/evolcore/trigger.md +42 -10
  152. package/kits/docs/path-registry.md +36 -17
  153. package/kits/eck_manifest.json +12 -0
  154. package/kits/eck_message_manifest.json +12 -1
  155. package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
  156. package/kits/rules/01-overview.md +17 -7
  157. package/kits/rules/02-navigation.md +38 -18
  158. package/kits/rules/03-identity.md +28 -24
  159. package/kits/rules/04-relation.md +44 -28
  160. package/kits/rules/05-venue.md +31 -15
  161. package/kits/rules/06-channel.md +11 -7
  162. package/kits/schemas/_meta.json +21 -7
  163. package/kits/schemas/agent-config.schema.6.json +322 -0
  164. package/kits/schemas/agent-config.schema.7.json +303 -0
  165. package/kits/schemas/agent-config.schema.8.json +304 -0
  166. package/kits/schemas/agent-config.schema.9.json +374 -0
  167. package/kits/schemas/contact-book.schema.2.json +43 -0
  168. package/kits/schemas/contact-book.schema.3.json +67 -0
  169. package/kits/schemas/daemon.schema.1.json +3 -2
  170. package/kits/schemas/daemon.schema.2.json +101 -0
  171. package/kits/schemas/daemon.schema.3.json +123 -0
  172. package/kits/schemas/defaults.schema.2.json +85 -0
  173. package/kits/schemas/defaults.schema.3.json +73 -0
  174. package/kits/schemas/relation-config.schema.5.json +47 -0
  175. package/kits/schemas/relation-config.schema.6.json +46 -0
  176. package/kits/schemas/relation-config.schema.7.json +59 -0
  177. package/kits/schemas/role-config.schema.1.json +1 -0
  178. package/kits/schemas/role-registry.schema.1.json +2 -2
  179. package/kits/schemas/single-session.schema.2.json +57 -0
  180. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  181. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  182. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  183. package/kits/templates/roles/admin.json +47 -0
  184. package/kits/templates/roles/member.json +5 -0
  185. package/kits/templates/roles/owner.json +1 -0
  186. package/kits/templates/roles/visitor.json +5 -0
  187. package/kits/templates/system-fragments/channel.md +12 -2
  188. package/kits/templates/system-fragments/commands.md +3 -1
  189. package/kits/templates/system-fragments/identity.md +3 -1
  190. package/kits/templates/system-fragments/relation.md +1 -1
  191. package/kits/templates/system-fragments/session.md +6 -2
  192. package/package.json +7 -5
  193. package/MIGRATION-0.5.0.md +0 -378
  194. package/ROLE_ACCESS_CONTROL.md +0 -174
  195. package/assets/brand/evolcore/README.md +0 -19
  196. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  197. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  198. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  199. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  200. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  201. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  202. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  203. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  204. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  205. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  206. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -2,7 +2,8 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { EvolAgent } from './evolagent.js';
4
4
  import { logger } from '../utils/logger.js';
5
- import { agentMdPath, agentPersonalDir } from '../paths.js';
5
+ import { agentPersonalDir } from '../paths.js';
6
+ import { invalidateAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
6
7
  import { loadAllAgents, ensureAgentDirSkeleton, loadAgent, validateAgentConfig, } from '../config-store.js';
7
8
  import { resolveEffective } from '../config/config-manager.js';
8
9
  // ── Channel Fingerprint ───────────────────────────────────────────────────
@@ -129,7 +130,6 @@ export class EvolAgentRegistry {
129
130
  aid: raw.aid,
130
131
  enabled: raw.enabled,
131
132
  owners: raw.owners,
132
- admins: raw.admins,
133
133
  channels: Array.isArray(raw.channels) ? raw.channels : [],
134
134
  active_baseagent: raw.active_baseagent,
135
135
  baseagents: raw.baseagents,
@@ -211,9 +211,8 @@ export class EvolAgentRegistry {
211
211
  return [...this.agents.values()].map(a => this.toInfo(a));
212
212
  }
213
213
  invalidateAgentDisplayCache(aid) {
214
- this.displayNameCache.delete(aid);
214
+ invalidateAgentDisplayName(aid);
215
215
  this.personalNameCache.delete(aid);
216
- this.displayNamePending.delete(aid);
217
216
  }
218
217
  runnableAgents() {
219
218
  return [...this.agents.values()].filter(a => a.status === 'stopped');
@@ -443,43 +442,8 @@ export class EvolAgentRegistry {
443
442
  }
444
443
  return null;
445
444
  }
446
- displayNameCache = new Map();
447
- displayNamePending = new Set();
448
445
  resolveDisplayName(aid) {
449
- const cached = this.displayNameCache.get(aid);
450
- if (cached)
451
- return cached;
452
- try {
453
- const mdPath = agentMdPath(aid);
454
- if (fs.existsSync(mdPath)) {
455
- const content = fs.readFileSync(mdPath, 'utf-8');
456
- const fm = content.match(/^---\n([\s\S]*?)\n---/);
457
- if (fm) {
458
- const nm = fm[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
459
- if (nm?.[1]) {
460
- this.displayNameCache.set(aid, nm[1]);
461
- return nm[1];
462
- }
463
- }
464
- }
465
- }
466
- catch { }
467
- if (!this.displayNamePending.has(aid)) {
468
- this.displayNamePending.add(aid);
469
- import('../aun/aid/index.js').then(({ agentmdGet }) => {
470
- agentmdGet(aid).then(content => {
471
- if (typeof content === 'string') {
472
- const fm = content.match(/^---\n([\s\S]*?)\n---/);
473
- if (fm) {
474
- const nm = fm[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
475
- if (nm?.[1])
476
- this.displayNameCache.set(aid, nm[1]);
477
- }
478
- }
479
- }).catch(() => { }).finally(() => this.displayNamePending.delete(aid));
480
- }).catch(() => { this.displayNamePending.delete(aid); });
481
- }
482
- return undefined;
446
+ return resolveAgentDisplayName(aid);
483
447
  }
484
448
  personalNameCache = new Map();
485
449
  resolvePersonalName(aid) {
@@ -1,9 +1,11 @@
1
1
  import path from 'path';
2
- import { formatChannelKey } from './channel-loader.js';
2
+ import { formatChannelKey, tryParseChannelKey } from './channel-loader.js';
3
3
  import { agentPersonalDir } from '../paths.js';
4
4
  import { fileCache } from './daemon-file-cache.js';
5
5
  import { ConfigTarget, read as cfgRead, write as cfgWrite, ensureFile as cfgEnsure, resolveEffective } from '../config/config-manager.js';
6
6
  import { withLifecycleForWrite } from '../config/lifecycle.js';
7
+ import { isStaticAgentAdmin, isStaticAgentOwner } from '../config/peer-role-resolver.js';
8
+ import { resolvePrincipal } from '../config/contact-book.js';
7
9
  /**
8
10
  * EvolAgent —— 一个 self-agent 的运行时表示。
9
11
  *
@@ -123,19 +125,28 @@ export class EvolAgent {
123
125
  /**
124
126
  * Check if a user has owner role for this agent (private scope).
125
127
  */
126
- isOwner(_channelKey, userId) {
127
- if (this.merged.owners?.includes(userId))
128
- return true;
129
- return false;
128
+ isOwner(channelKey, userId) {
129
+ const principalId = this.managementPrincipal(channelKey, userId);
130
+ return !!principalId && isStaticAgentOwner(this.aid, principalId);
130
131
  }
131
132
  /**
132
133
  * Check if a user has admin role for this agent (private scope).
133
- * Uses static owners/admins from agent config.
134
+ * Owner remains in Agent config; admin is resolved from the direct AUN relation.
134
135
  */
135
- isAdmin(_channelKey, userId) {
136
- if (this.isOwner(_channelKey, userId))
136
+ isAdmin(channelKey, userId) {
137
+ const principalId = this.managementPrincipal(channelKey, userId);
138
+ if (!principalId)
139
+ return false;
140
+ if (isStaticAgentOwner(this.aid, principalId))
137
141
  return true;
138
- return this.merged.admins?.includes(userId) ?? false;
142
+ return isStaticAgentAdmin(this.aid, principalId);
143
+ }
144
+ managementPrincipal(channelKey, userId) {
145
+ const channelType = tryParseChannelKey(channelKey)?.type ?? String(channelKey || '').split('#')[0];
146
+ const resolution = resolvePrincipal(this.aid, channelType, userId, channelKey);
147
+ return resolution.principalStatus === 'native_aid' || resolution.principalStatus === 'mapped'
148
+ ? resolution.principalId ?? null
149
+ : null;
139
150
  }
140
151
  /**
141
152
  * Get the first owner of this agent (private scope).
@@ -5,6 +5,10 @@ import { KeyedFairMutex } from './mutex.js';
5
5
  import { HandoffStore } from './store.js';
6
6
  import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
7
7
  import { recordCausationSpan } from '../causation/audit.js';
8
+ import { logger } from '../../utils/logger.js';
9
+ const HANDOFF_STALE_WARN_MS = 72 * 60 * 60 * 1000;
10
+ const HANDOFF_LARGE_AGGREGATE_COUNT = 10;
11
+ const HANDOFF_LARGE_AGGREGATE_CHARS = 50_000;
8
12
  export class HandoffTargetBlockedError extends Error {
9
13
  blockingHandoffId;
10
14
  attentionReason;
@@ -25,6 +29,20 @@ export class HandoffDeliveryFailedError extends Error {
25
29
  this.name = 'HandoffDeliveryFailedError';
26
30
  }
27
31
  }
32
+ export class HandoffReturnPolicyRequiredError extends Error {
33
+ code = 'HANDOFF_RETURN_POLICY_REQUIRED';
34
+ constructor() {
35
+ super('跨会话 ec msg/group send 必须显式指定 --return none|required');
36
+ this.name = 'HandoffReturnPolicyRequiredError';
37
+ }
38
+ }
39
+ export class TriggerHandoffRequiredForbiddenError extends Error {
40
+ code = 'TRIGGER_HANDOFF_REQUIRED_FORBIDDEN';
41
+ constructor() {
42
+ super('Trigger 的跨会话 ec msg/group send 只允许 --return none,不能使用 required');
43
+ this.name = 'TriggerHandoffRequiredForbiddenError';
44
+ }
45
+ }
28
46
  export class HandoffRuntime {
29
47
  sessionManager;
30
48
  messageQueue;
@@ -32,6 +50,8 @@ export class HandoffRuntime {
32
50
  mutexes = new KeyedFairMutex();
33
51
  dispatcher;
34
52
  now;
53
+ staleWarned = new Set();
54
+ largeAggregateWarned = new Set();
35
55
  constructor(sessionManager, messageQueue, sender, store = new HandoffStore(), options = {}) {
36
56
  this.sessionManager = sessionManager;
37
57
  this.messageQueue = messageQueue;
@@ -56,6 +76,11 @@ export class HandoffRuntime {
56
76
  if (target.id === origin.id) {
57
77
  return { crossSession: false, targetSession: target };
58
78
  }
79
+ if (!input.explicitReturnPolicy)
80
+ throw new HandoffReturnPolicyRequiredError();
81
+ if (input.explicitReturnPolicy === 'required' && input.causation?.trigger?.path.length) {
82
+ throw new TriggerHandoffRequiredForbiddenError();
83
+ }
59
84
  await this.dispatcher.expireTargetQueued(input.selfAid, target.id);
60
85
  throwIfAborted(input.signal);
61
86
  const head = this.store.listByTarget(input.selfAid, target.id, 'queued')[0];
@@ -91,7 +116,8 @@ export class HandoffRuntime {
91
116
  }
92
117
  hasOpenTarget(selfAid, targetSessionId) {
93
118
  return this.store.listByTarget(selfAid, targetSessionId)
94
- .some(instance => instance.state === 'target_sent' || instance.state === 'return_pending');
119
+ .some(instance => instance.state === 'target_sent'
120
+ || (instance.state === 'return_pending' && instance.return_policy === 'required'));
95
121
  }
96
122
  async bindReply(input) {
97
123
  const key = `${input.selfAid}\u0000${input.targetSessionId}`;
@@ -105,6 +131,8 @@ export class HandoffRuntime {
105
131
  responseMessageId: input.responseMessageId,
106
132
  refMessageId: input.refMessageId,
107
133
  });
134
+ for (const instance of attached)
135
+ this.warnIfStale(instance);
108
136
  const handoffIds = attached.map(instance => instance.handoff_id);
109
137
  return {
110
138
  candidate: handoffIds.length > 0,
@@ -157,6 +185,7 @@ export class HandoffRuntime {
157
185
  messageId,
158
186
  timestamp: Date.now(),
159
187
  source: 'handoff',
188
+ boundSessionId: origin.id,
160
189
  causation: instance.return_causation
161
190
  ? deriveCausation(instance.return_causation)
162
191
  : instance.causation
@@ -186,55 +215,120 @@ export class HandoffRuntime {
186
215
  }
187
216
  }
188
217
  buildPromptItem(message) {
189
- const delivery = message.handoffDelivery;
190
- const selfAid = message.selfAID;
191
- if (!delivery || !selfAid)
192
- return null;
193
- const instance = this.store.get(selfAid, delivery.handoffId);
194
- if (!instance)
195
- return null;
196
- if (delivery.direction === 'target' && instance.state !== 'return_pending')
197
- return null;
198
- if (delivery.direction === 'origin' && instance.state !== 'origin_delivered')
199
- return null;
200
- return {
201
- kind: 'handoff',
202
- peerId: message.peerId,
203
- peerName: message.peerName,
204
- peerType: message.peerType,
205
- content: message.content,
206
- timestamp: message.timestamp,
207
- causation: message.causation,
208
- handoff: {
209
- kind: delivery.direction === 'target' ? 'request_to_target' : 'response_to_origin',
210
- handoffId: instance.handoff_id,
211
- previousContent: this.requestSummary(instance),
212
- previousMessageId: instance.target_message_id,
213
- returnPolicy: instance.return_policy,
214
- },
215
- };
218
+ return this.buildPromptItems(message)[0] ?? null;
216
219
  }
217
220
  buildPromptItems(message) {
218
221
  const delivery = message.handoffDelivery;
219
- if (!delivery)
222
+ const selfAid = message.selfAID;
223
+ if (!delivery || !selfAid)
220
224
  return [];
221
225
  const ids = delivery.handoffIds?.length ? delivery.handoffIds : [delivery.handoffId];
222
- const items = [];
226
+ const instances = [];
223
227
  for (const handoffId of ids) {
224
- const item = this.buildPromptItem({
225
- ...message,
226
- handoffDelivery: { direction: delivery.direction, handoffId },
227
- });
228
- if (item)
229
- items.push(item);
228
+ const instance = this.store.get(selfAid, handoffId);
229
+ if (!instance || instance.attention_required)
230
+ continue;
231
+ if (delivery.direction === 'target') {
232
+ const expectedState = instance.return_policy === 'none'
233
+ ? 'target_context_pending'
234
+ : 'return_pending';
235
+ if (instance.state !== expectedState)
236
+ continue;
237
+ }
238
+ else if (instance.state !== 'origin_delivered') {
239
+ continue;
240
+ }
241
+ instances.push(instance);
242
+ }
243
+ if (instances.length === 0)
244
+ return [];
245
+ if (delivery.direction === 'origin') {
246
+ this.warnIfLargeAggregate(message, instances, 'origin');
247
+ const first = instances[0];
248
+ return [{
249
+ kind: 'handoff',
250
+ peerId: message.peerId,
251
+ peerName: message.peerName,
252
+ peerType: message.peerType,
253
+ content: message.content,
254
+ timestamp: message.timestamp,
255
+ causation: message.causation,
256
+ handoff: {
257
+ kind: 'response_to_origin',
258
+ handoffId: first.handoff_id,
259
+ handoffIds: instances.map(instance => instance.handoff_id),
260
+ originEntries: instances.map(instance => ({
261
+ previousContent: this.requestSummary(instance),
262
+ responseContent: instance.return_content ?? '',
263
+ previousMessageId: instance.target_message_id,
264
+ })),
265
+ previousContent: this.requestSummary(first),
266
+ previousMessageId: first.target_message_id,
267
+ returnPolicy: first.return_policy,
268
+ },
269
+ }];
230
270
  }
231
- return items;
271
+ const requiredEntries = instances
272
+ .filter(instance => instance.return_policy === 'required')
273
+ .map(instance => ({
274
+ handoffId: instance.handoff_id,
275
+ previousContent: this.requestSummary(instance),
276
+ previousMessageId: instance.target_message_id,
277
+ }));
278
+ const contextEntries = instances
279
+ .filter(instance => instance.return_policy === 'none')
280
+ .map(instance => ({
281
+ previousContent: this.requestSummary(instance),
282
+ previousMessageId: instance.target_message_id,
283
+ }));
284
+ this.warnIfLargeAggregate(message, instances, 'target');
285
+ const first = instances[0];
286
+ return [{
287
+ kind: 'handoff',
288
+ peerId: message.peerId,
289
+ peerName: message.peerName,
290
+ peerType: message.peerType,
291
+ content: message.content,
292
+ timestamp: message.timestamp,
293
+ causation: message.causation,
294
+ handoff: {
295
+ kind: 'request_to_target',
296
+ handoffId: first.handoff_id,
297
+ handoffIds: instances.map(instance => instance.handoff_id),
298
+ requiredEntries,
299
+ contextEntries,
300
+ previousContent: this.requestSummary(first),
301
+ previousMessageId: first.target_message_id,
302
+ returnPolicy: requiredEntries.length > 0 ? 'required' : 'none',
303
+ },
304
+ }];
232
305
  }
233
306
  completeOriginContext(selfAid, handoffId) {
234
307
  const instance = this.store.get(selfAid, handoffId);
235
308
  if (instance?.state === 'origin_delivered')
236
309
  this.store.completeOriginContext(selfAid, handoffId);
237
310
  }
311
+ completeTargetContext(selfAid, handoffId) {
312
+ const instance = this.store.get(selfAid, handoffId);
313
+ if (instance?.state === 'target_context_pending' && instance.return_policy === 'none') {
314
+ this.store.completeTargetContext(selfAid, handoffId);
315
+ }
316
+ }
317
+ validateTargetSessionBinding(selfAid, handoffIds, executionSessionId) {
318
+ const invalidHandoffIds = [];
319
+ for (const handoffId of handoffIds) {
320
+ const instance = this.store.get(selfAid, handoffId);
321
+ if (!instance
322
+ || instance.target_session_id !== executionSessionId
323
+ || instance.consumed_target_session_id !== executionSessionId) {
324
+ invalidHandoffIds.push(handoffId);
325
+ if (instance && !instance.attention_required) {
326
+ this.store.markAttention(selfAid, handoffId, 'HANDOFF_SESSION_BINDING_MISMATCH');
327
+ }
328
+ }
329
+ }
330
+ return { ok: invalidHandoffIds.length === 0, invalidHandoffIds };
331
+ }
238
332
  status(selfAid, handoffId) {
239
333
  return this.store.status(selfAid, handoffId);
240
334
  }
@@ -276,6 +370,10 @@ export class HandoffRuntime {
276
370
  async recover(selfAids) {
277
371
  const originEnqueues = [];
278
372
  for (const selfAid of selfAids) {
373
+ // v1 initially represented return=none as return_pending and required an
374
+ // empty manual return. It has already been shown to a target turn, so
375
+ // finish it before version reconciliation instead of prompting again.
376
+ this.store.migrateLegacyNonePending(selfAid, this.now?.());
279
377
  this.expireQueued([selfAid]);
280
378
  const events = this.store.events(selfAid);
281
379
  const lastSendEvent = new Map();
@@ -285,6 +383,7 @@ export class HandoffRuntime {
285
383
  }
286
384
  }
287
385
  for (const instance of this.store.list(selfAid)) {
386
+ this.warnIfStale(instance);
288
387
  const versionedEvents = events.filter(event => event.handoff_id === instance.handoff_id && event.to_version !== undefined);
289
388
  const latestEventVersion = versionedEvents.reduce((latest, event) => Math.max(latest, event.to_version ?? 0), 0);
290
389
  if (latestEventVersion !== instance.version) {
@@ -313,6 +412,32 @@ export class HandoffRuntime {
313
412
  }
314
413
  await Promise.all(originEnqueues);
315
414
  }
415
+ warnIfStale(instance) {
416
+ if (instance.state !== 'target_sent' && instance.state !== 'return_pending')
417
+ return;
418
+ const ageMs = (this.now?.() ?? Date.now()) - instance.created_at;
419
+ const key = instance.handoff_id;
420
+ if (ageMs < HANDOFF_STALE_WARN_MS || this.staleWarned.has(key))
421
+ return;
422
+ this.staleWarned.add(key);
423
+ logger.warn(`[Handoff] stale open handoff id=${instance.handoff_id}`
424
+ + ` state=${instance.state} returnPolicy=${instance.return_policy}`
425
+ + ` ageHours=${Math.floor(ageMs / 3_600_000)}`
426
+ + ` originSession=${instance.origin_session_id} targetSession=${instance.target_session_id}`);
427
+ }
428
+ warnIfLargeAggregate(message, instances, direction) {
429
+ const chars = message.content.length
430
+ + instances.reduce((total, instance) => total + this.requestSummary(instance).length, 0);
431
+ if (instances.length < HANDOFF_LARGE_AGGREGATE_COUNT && chars < HANDOFF_LARGE_AGGREGATE_CHARS)
432
+ return;
433
+ const key = `${message.messageId ?? '<none>'}:${instances.map(instance => instance.handoff_id).join(',')}`;
434
+ if (this.largeAggregateWarned.has(key))
435
+ return;
436
+ this.largeAggregateWarned.add(key);
437
+ logger.warn(`[Handoff] large prompt aggregate direction=${direction}`
438
+ + ` session=${direction === 'target' ? instances[0].target_session_id : instances[0].origin_session_id}`
439
+ + ` count=${instances.length} chars=${chars} responseMessage=${message.messageId ?? '<none>'}`);
440
+ }
316
441
  requestSummary(instance) {
317
442
  return classifyAunPayloadForLog(instance.request.payload).content || '[payload]';
318
443
  }
@@ -272,7 +272,8 @@ export class HandoffStore {
272
272
  }
273
273
  attachSessionContext(input) {
274
274
  const open = this.listByTarget(input.selfAid, input.targetSessionId)
275
- .filter(item => (item.state === 'target_sent' || item.state === 'return_pending') && !item.attention_required)
275
+ .filter(item => (item.state === 'target_sent'
276
+ || (item.state === 'return_pending' && item.return_policy === 'required')) && !item.attention_required)
276
277
  .sort((left, right) => {
277
278
  const leftRef = input.refMessageId && left.target_message_id === input.refMessageId ? 0 : 1;
278
279
  const rightRef = input.refMessageId && right.target_message_id === input.refMessageId ? 0 : 1;
@@ -283,7 +284,9 @@ export class HandoffStore {
283
284
  const previousVersion = instance.version;
284
285
  const firstAttachment = instance.state === 'target_sent';
285
286
  if (firstAttachment) {
286
- instance.state = 'return_pending';
287
+ instance.state = instance.return_policy === 'none'
288
+ ? 'target_context_pending'
289
+ : 'return_pending';
287
290
  instance.response_message_id = input.responseMessageId;
288
291
  instance.consumed_at = now;
289
292
  instance.consumed_target_session_id = input.targetSessionId;
@@ -308,6 +311,47 @@ export class HandoffStore {
308
311
  bindExactReply(input) {
309
312
  return this.attachSessionContext(input)[0] ?? null;
310
313
  }
314
+ completeTargetContext(selfAid, handoffId, now = Date.now()) {
315
+ const instance = this.requireState(selfAid, handoffId, 'target_context_pending');
316
+ if (instance.return_policy !== 'none') {
317
+ throw new Error('target context auto-completion requires return=none');
318
+ }
319
+ const previous = instance.version;
320
+ instance.state = 'completed';
321
+ instance.version++;
322
+ instance.updated_at = now;
323
+ instance.completed_at = now;
324
+ this.appendEvent(selfAid, 'target_context_consumed', handoffId, {
325
+ operationKey: instance.response_message_id ?? undefined,
326
+ fromVersion: previous,
327
+ toVersion: instance.version,
328
+ now,
329
+ });
330
+ this.write(selfAid, instance);
331
+ return instance;
332
+ }
333
+ migrateLegacyNonePending(selfAid, now = Date.now()) {
334
+ const migrated = [];
335
+ for (const instance of this.list(selfAid)) {
336
+ if (instance.return_policy !== 'none' || instance.state !== 'return_pending')
337
+ continue;
338
+ const previous = instance.version;
339
+ instance.state = 'completed';
340
+ instance.version++;
341
+ instance.updated_at = now;
342
+ instance.completed_at = now;
343
+ this.appendEvent(selfAid, 'target_context_consumed', instance.handoff_id, {
344
+ operationKey: instance.response_message_id ?? undefined,
345
+ fromVersion: previous,
346
+ toVersion: instance.version,
347
+ details: { migration: true, previous_state: 'return_pending' },
348
+ now,
349
+ });
350
+ this.write(selfAid, instance);
351
+ migrated.push(instance);
352
+ }
353
+ return migrated;
354
+ }
311
355
  returnHandoff(input) {
312
356
  const selected = this.selectReturnCandidate(input);
313
357
  if ('ok' in selected)
@@ -3,6 +3,7 @@ export const HANDOFF_STATES = [
3
3
  'queued',
4
4
  'failed',
5
5
  'target_sent',
6
+ 'target_context_pending',
6
7
  'return_pending',
7
8
  'origin_queued',
8
9
  'origin_delivered',
@@ -1,10 +1,9 @@
1
- import { createHash } from 'crypto';
2
- import { resolveAnthropicConfig, resolveOpenaiConfig, } from '../../agents/baseagent.js';
3
- const MODEL_LIST_TTL_MS = 5 * 60 * 1000;
4
- const MODEL_LIST_MAX_PAGES = 20;
5
- const anthropicModelListCache = new Map();
6
- function apiEndpoint(baseUrl, defaultBaseUrl, resource) {
7
- const base = (baseUrl || defaultBaseUrl).replace(/\/+$/, '');
1
+ import { normalizeBaseagent, resolveAnthropicDirectConfig, resolveOpenaiDirectConfig, } from '../../agents/baseagent.js';
2
+ function apiEndpoint(baseUrl, resource) {
3
+ const configured = baseUrl?.trim();
4
+ if (!configured)
5
+ throw new Error('Direct text inference baseUrl is not configured');
6
+ const base = configured.replace(/\/+$/, '');
8
7
  return base.endsWith('/v1') ? `${base}/${resource}` : `${base}/v1/${resource}`;
9
8
  }
10
9
  async function parseError(response) {
@@ -28,53 +27,8 @@ export class AnthropicTextInferenceProvider {
28
27
  constructor(config) {
29
28
  this.config = config;
30
29
  }
31
- async listModels(signal) {
32
- const cacheKey = this.modelListCacheKey();
33
- const cached = anthropicModelListCache.get(cacheKey);
34
- if (cached && Date.now() - cached.fetchedAt < MODEL_LIST_TTL_MS)
35
- return [...cached.models];
36
- const models = [];
37
- const seen = new Set();
38
- let afterId;
39
- for (let page = 0; page < MODEL_LIST_MAX_PAGES; page++) {
40
- const endpoint = new URL(apiEndpoint(this.config.baseUrl, 'https://api.anthropic.com', 'models'));
41
- endpoint.searchParams.set('limit', '100');
42
- if (afterId)
43
- endpoint.searchParams.set('after_id', afterId);
44
- const response = await fetch(endpoint, {
45
- method: 'GET',
46
- signal,
47
- headers: this.headers(),
48
- });
49
- if (!response.ok)
50
- throw await parseError(response);
51
- const body = await response.json();
52
- const entries = Array.isArray(body.data)
53
- ? body.data
54
- : (Array.isArray(body.models) ? body.models : []);
55
- for (const entry of entries) {
56
- const id = typeof entry === 'string'
57
- ? entry
58
- : entry?.id || entry?.name || entry?.model;
59
- if (typeof id !== 'string' || !id.trim() || seen.has(id))
60
- continue;
61
- seen.add(id);
62
- models.push(id);
63
- }
64
- if (body.has_more !== true)
65
- break;
66
- const next = typeof body.last_id === 'string' && body.last_id ? body.last_id : undefined;
67
- if (!next || next === afterId)
68
- throw new Error('Claude model list pagination returned no next cursor');
69
- afterId = next;
70
- }
71
- if (models.length === 0)
72
- throw new Error('Claude model list API returned no models');
73
- anthropicModelListCache.set(cacheKey, { models, fetchedAt: Date.now() });
74
- return [...models];
75
- }
76
30
  async completeText(request) {
77
- const response = await fetch(apiEndpoint(this.config.baseUrl, 'https://api.anthropic.com', 'messages'), {
31
+ const response = await fetch(apiEndpoint(this.config.baseUrl, 'messages'), {
78
32
  method: 'POST',
79
33
  signal: request.signal,
80
34
  headers: this.headers(),
@@ -83,6 +37,7 @@ export class AnthropicTextInferenceProvider {
83
37
  max_tokens: 256,
84
38
  system: request.system,
85
39
  messages: [{ role: 'user', content: request.input }],
40
+ ...(request.effort ? { output_config: { effort: request.effort } } : {}),
86
41
  }),
87
42
  });
88
43
  if (!response.ok)
@@ -106,14 +61,6 @@ export class AnthropicTextInferenceProvider {
106
61
  Authorization: `Bearer ${this.config.apiKey}`,
107
62
  };
108
63
  }
109
- modelListCacheKey() {
110
- const endpoint = apiEndpoint(this.config.baseUrl, 'https://api.anthropic.com', 'models');
111
- const credential = createHash('sha256').update(this.config.apiKey).digest('hex');
112
- return `${endpoint}\0${credential}`;
113
- }
114
- }
115
- export function _resetTextInferenceModelCacheForTests() {
116
- anthropicModelListCache.clear();
117
64
  }
118
65
  export class OpenAITextInferenceProvider {
119
66
  config;
@@ -122,7 +69,7 @@ export class OpenAITextInferenceProvider {
122
69
  this.config = config;
123
70
  }
124
71
  async completeText(request) {
125
- const response = await fetch(apiEndpoint(this.config.baseUrl, 'https://api.openai.com', 'responses'), {
72
+ const response = await fetch(apiEndpoint(this.config.baseUrl, 'responses'), {
126
73
  method: 'POST',
127
74
  signal: request.signal,
128
75
  headers: {
@@ -155,19 +102,14 @@ export class OpenAITextInferenceProvider {
155
102
  }
156
103
  }
157
104
  export function createTextInferenceProvider(baseagent, config) {
158
- if (baseagent === 'claude') {
159
- const override = config.baseagents?.claude;
160
- if (!override)
161
- return undefined;
162
- const synthetic = { agents: { claude: override } };
163
- return new AnthropicTextInferenceProvider(resolveAnthropicConfig(synthetic, override));
105
+ const canonical = normalizeBaseagent(baseagent).canonical;
106
+ if (canonical === 'claude') {
107
+ const resolved = resolveAnthropicDirectConfig(config.baseagents?.claude);
108
+ return resolved ? new AnthropicTextInferenceProvider(resolved) : undefined;
164
109
  }
165
- if (baseagent === 'codex') {
166
- const override = config.baseagents?.codex;
167
- if (!override)
168
- return undefined;
169
- const synthetic = { agents: { codex: override } };
170
- return new OpenAITextInferenceProvider(resolveOpenaiConfig(synthetic, override));
110
+ if (canonical === 'codex') {
111
+ const resolved = resolveOpenaiDirectConfig(config.baseagents?.codex);
112
+ return resolved ? new OpenAITextInferenceProvider(resolved) : undefined;
171
113
  }
172
114
  return undefined;
173
115
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A file marker may contain surrounding whitespace inside its brackets so
3
+ * formatted responses such as `[ SEND_FILE:./report.md ]` remain actionable.
4
+ */
5
+ export function createSendFileMarkerPattern() {
6
+ return /\[\s*SEND_FILE:(?:(\w+):)?([^\]]*?\S[^\]]*?)\s*\]/g;
7
+ }