evolcore 0.0.11 → 0.0.13

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 (164) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bin/codex-managed-hook.mjs +80 -0
  3. package/bin/ec-safe-output.js +26 -7
  4. package/dist/agents/claude-runner.js +86 -7
  5. package/dist/agents/codex-app-server-client.js +174 -20
  6. package/dist/agents/codex-runner.js +375 -92
  7. package/dist/agents/ecagent-runner.js +39 -4
  8. package/dist/agents/gemini-runner.js +1 -0
  9. package/dist/aun/aid/client.js +11 -21
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/managed-operation.js +107 -0
  12. package/dist/aun/aid/store.js +13 -7
  13. package/dist/aun/group-fs-download.js +56 -0
  14. package/dist/aun/group-identity.js +1 -2
  15. package/dist/aun/msg/group.js +91 -44
  16. package/dist/aun/msg/index.js +1 -2
  17. package/dist/aun/msg/managed-operation.js +865 -0
  18. package/dist/aun/msg/mention-schema.js +20 -0
  19. package/dist/aun/msg/p2p.js +7 -0
  20. package/dist/aun/msg/upload.js +16 -1
  21. package/dist/aun/outbox.js +31 -2
  22. package/dist/aun/rpc/client.js +66 -0
  23. package/dist/aun/rpc/index.js +1 -2
  24. package/dist/aun/storage/{upload.js → client.js} +11 -2
  25. package/dist/aun/storage/index.js +1 -2
  26. package/dist/channels/aun.js +655 -240
  27. package/dist/channels/daemon.js +13 -0
  28. package/dist/cli/agent-command.js +56 -8
  29. package/dist/cli/agent.js +108 -31
  30. package/dist/cli/aun-commands.js +270 -69
  31. package/dist/cli/bench.js +12 -3
  32. package/dist/cli/cli-argv.js +10 -0
  33. package/dist/cli/config.js +9 -17
  34. package/dist/cli/ctl-command.js +1 -1
  35. package/dist/cli/daemon-commands.js +70 -55
  36. package/dist/cli/fs-command.js +79 -10
  37. package/dist/cli/handoff-command.js +3 -0
  38. package/dist/cli/index.js +30 -2
  39. package/dist/cli/init.js +77 -8
  40. package/dist/cli/managed-command-guard.js +41 -0
  41. package/dist/cli/model.js +11 -4
  42. package/dist/cli/queue-command.js +53 -1
  43. package/dist/cli/response.js +71 -0
  44. package/dist/cli/restart-monitor.js +7 -32
  45. package/dist/cli/trigger-command.js +11 -0
  46. package/dist/config/access-policy.js +1 -2
  47. package/dist/config/builtin-roles.js +9 -0
  48. package/dist/config/config-manager.js +21 -1
  49. package/dist/config/contact-book-store.js +11 -3
  50. package/dist/config/contact-request-service.js +161 -15
  51. package/dist/config/mention-mode.js +8 -24
  52. package/dist/config/peer-role-resolver.js +6 -2
  53. package/dist/config/role-migration-startup.js +53 -0
  54. package/dist/config/role-store.js +1 -1
  55. package/dist/core/auth/agent-delegation.js +1 -2
  56. package/dist/core/auth/auth-gateway.js +29 -1
  57. package/dist/core/auth/authorization-audit.js +15 -0
  58. package/dist/core/auth/operation-authorizer.js +31 -7
  59. package/dist/core/auth/operation-catalog.js +196 -25
  60. package/dist/core/bootstrap-messages.js +25 -0
  61. package/dist/core/bootstrap-service.js +64 -4
  62. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  63. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  64. package/dist/core/capability/skill-discovery.js +55 -0
  65. package/dist/core/channel-loader.js +1 -1
  66. package/dist/core/command/cli-intent-parser.js +9 -0
  67. package/dist/core/command/command-handler.js +650 -42
  68. package/dist/core/command/connect-menu.js +15 -0
  69. package/dist/core/command/group-menu.js +4 -4
  70. package/dist/core/command/menu-catalog.js +370 -0
  71. package/dist/core/command/menu-handler.js +230 -51
  72. package/dist/core/command/menu-protocol.js +94 -2
  73. package/dist/core/command/slash-gate.js +5 -2
  74. package/dist/core/command/slash-handler.js +99 -31
  75. package/dist/core/daemon-file-cache.js +2 -1
  76. package/dist/core/data-migration.js +9 -2
  77. package/dist/core/event-catalog.js +6 -0
  78. package/dist/core/evolagent.js +4 -2
  79. package/dist/core/handoff/dispatcher.js +5 -2
  80. package/dist/core/handoff/runtime.js +157 -22
  81. package/dist/core/handoff/store.js +29 -1
  82. package/dist/core/handoff/types.js +1 -0
  83. package/dist/core/interaction-router.js +10 -0
  84. package/dist/core/message/im-renderer.js +19 -0
  85. package/dist/core/message/logical-queue-bridge.js +1 -0
  86. package/dist/core/message/mention-schema.js +126 -0
  87. package/dist/core/message/message-bridge.js +298 -88
  88. package/dist/core/message/message-log.js +3 -0
  89. package/dist/core/message/message-queue.js +268 -39
  90. package/dist/core/message/message-utils.js +81 -4
  91. package/dist/core/message/response-engine.js +161 -77
  92. package/dist/core/message/stream-debouncer.js +10 -2
  93. package/dist/core/permission/approval-gateway.js +1 -0
  94. package/dist/core/permission/ec-command-parser.js +243 -18
  95. package/dist/core/permission/index.js +1 -1
  96. package/dist/core/permission/mode.js +15 -0
  97. package/dist/core/permission/readonly-shell-query.js +148 -8
  98. package/dist/core/permission/sandbox-runtime.js +43 -1
  99. package/dist/core/permission/tool-policy.js +322 -57
  100. package/dist/core/protected-paths.js +114 -4
  101. package/dist/core/session/session-manager.js +42 -4
  102. package/dist/core/session/session-mapper.js +20 -1
  103. package/dist/eck/detect.js +18 -1
  104. package/dist/eck/group-rules-sync.js +15 -44
  105. package/dist/eck/manifest-engine.js +39 -13
  106. package/dist/index.js +541 -180
  107. package/dist/ipc.js +232 -10
  108. package/dist/paths.js +3 -0
  109. package/dist/response-system/coordinator.js +6 -4
  110. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  111. package/dist/response-system/modes/single-session/index.js +14 -1
  112. package/dist/response-system/selector.js +6 -5
  113. package/dist/trigger/anomaly-store.js +4 -0
  114. package/dist/trigger/feedback.js +64 -11
  115. package/dist/trigger/manager.js +21 -1
  116. package/dist/trigger/parser.js +14 -2
  117. package/dist/trigger/scheduler.js +1 -0
  118. package/dist/trigger/script-executor.js +1 -0
  119. package/dist/trigger/validation.js +67 -6
  120. package/dist/utils/codex-app-server-registry.js +90 -0
  121. package/dist/utils/codex-cli.js +5 -1
  122. package/dist/utils/cross-platform.js +15 -0
  123. package/dist/utils/ecweb-supervisor.js +332 -0
  124. package/dist/utils/error-utils.js +10 -0
  125. package/dist/utils/npm-ops.js +5 -12
  126. package/dist/utils/tool-summary.js +11 -0
  127. package/kits/docs/channels/aun.md +1 -1
  128. package/kits/docs/context-assembly.md +2 -2
  129. package/kits/docs/evolcore/agent.md +11 -6
  130. package/kits/docs/evolcore/aid.md +14 -0
  131. package/kits/docs/evolcore/trigger.md +17 -1
  132. package/kits/docs/prompt-loading-architecture.md +1 -2
  133. package/kits/docs/venues/group.md +1 -1
  134. package/kits/eck_manifest.json +0 -12
  135. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  136. package/kits/rules/04-relation.md +1 -1
  137. package/kits/rules/05-venue.md +2 -2
  138. package/kits/schemas/role-config.schema.1.json +1 -0
  139. package/kits/schemas/single-session.schema.1.json +3 -3
  140. package/kits/schemas/single-session.schema.2.json +3 -3
  141. package/kits/templates/roles/admin.json +27 -0
  142. package/kits/templates/roles/member.json +34 -1
  143. package/kits/templates/roles/visitor.json +35 -3
  144. package/kits/templates/system-fragments/bootstrap.md +1 -1
  145. package/kits/templates/system-fragments/channel.md +1 -1
  146. package/kits/templates/system-fragments/session.md +1 -1
  147. package/package.json +2 -2
  148. package/dist/aun/msg/payload-type.js +0 -27
  149. package/dist/aun/rpc/caller.js +0 -42
  150. package/dist/aun/rpc/connection.js +0 -25
  151. package/dist/aun/storage/manage.js +0 -10
  152. package/dist/config/access-policy-domain.js +0 -18
  153. package/dist/config/config-batch-get.js +0 -11
  154. package/dist/config/owner-policy.js +0 -4
  155. package/dist/config/role-config-v4-startup.js +0 -32
  156. package/dist/config/role-config-v5-startup.js +0 -27
  157. package/dist/core/auth/trigger-authorization.js +0 -15
  158. package/dist/core/command/evol-menu-version-gate.js +0 -39
  159. package/dist/core/interaction-registration.js +0 -10
  160. package/dist/core/permission/execution-sandbox.js +0 -16
  161. package/dist/core/relation/peer-key.js +0 -1
  162. package/dist/core/session/session-key.js +0 -24
  163. package/dist/eck/baseagent-caps.js +0 -18
  164. package/dist/eck/rules-loader.js +0 -28
@@ -6,7 +6,9 @@ import { HandoffStore } from './store.js';
6
6
  import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
7
7
  import { recordCausationSpan } from '../causation/audit.js';
8
8
  import { logger } from '../../utils/logger.js';
9
+ import { replyContextFromSession } from '../message/message-utils.js';
9
10
  const HANDOFF_STALE_WARN_MS = 72 * 60 * 60 * 1000;
11
+ const HANDOFF_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
10
12
  const HANDOFF_LARGE_AGGREGATE_COUNT = 10;
11
13
  const HANDOFF_LARGE_AGGREGATE_CHARS = 50_000;
12
14
  export class HandoffTargetBlockedError extends Error {
@@ -43,6 +45,15 @@ export class TriggerHandoffRequiredForbiddenError extends Error {
43
45
  this.name = 'TriggerHandoffRequiredForbiddenError';
44
46
  }
45
47
  }
48
+ export class HandoffSessionReplacedError extends Error {
49
+ sessionId;
50
+ code = 'HANDOFF_SESSION_REPLACED';
51
+ constructor(sessionId) {
52
+ super(`handoff session was replaced during creation: ${sessionId}`);
53
+ this.sessionId = sessionId;
54
+ this.name = 'HandoffSessionReplacedError';
55
+ }
56
+ }
46
57
  export class HandoffRuntime {
47
58
  sessionManager;
48
59
  messageQueue;
@@ -52,6 +63,8 @@ export class HandoffRuntime {
52
63
  now;
53
64
  staleWarned = new Set();
54
65
  largeAggregateWarned = new Set();
66
+ lifecycleSequence = 0;
67
+ sessionDiscardSequence = new Map();
55
68
  constructor(sessionManager, messageQueue, sender, store = new HandoffStore(), options = {}) {
56
69
  this.sessionManager = sessionManager;
57
70
  this.messageQueue = messageQueue;
@@ -60,13 +73,19 @@ export class HandoffRuntime {
60
73
  this.dispatcher = new HandoffDispatcher(store, this.mutexes, sender, options);
61
74
  }
62
75
  async createOutbound(input) {
76
+ const operationSequence = ++this.lifecycleSequence;
63
77
  throwIfAborted(input.signal);
64
78
  const origin = await this.sessionManager.getSessionById(input.originSessionId);
65
79
  throwIfAborted(input.signal);
66
80
  if (!origin || origin.selfAID !== input.selfAid)
67
81
  throw new Error('origin session not found');
68
82
  const targetChannelKey = formatChannelKey({ type: 'aun', selfAID: input.selfAid, name: 'main' });
69
- const targetChatType = input.targetChatType ?? 'private';
83
+ if (!input.targetChatType) {
84
+ throw Object.assign(new Error('AUN handoff requires an explicit target chat type'), {
85
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
86
+ });
87
+ }
88
+ const targetChatType = input.targetChatType;
70
89
  const target = await this.sessionManager.getOrCreateSession(targetChannelKey, input.to, origin.projectPath, input.thread, {
71
90
  channelKey: targetChannelKey,
72
91
  peerId: input.to,
@@ -81,24 +100,32 @@ export class HandoffRuntime {
81
100
  if (input.explicitReturnPolicy === 'required' && input.causation?.trigger?.path.length) {
82
101
  throw new TriggerHandoffRequiredForbiddenError();
83
102
  }
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
103
  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?.(),
104
+ const handoff = await this.mutexes.forKey(this.targetKey(input.selfAid, target.id)).runExclusive(async () => {
105
+ const replacedSessionId = [origin.id, target.id].find(sessionId => ((this.sessionDiscardSequence.get(this.sessionKey(input.selfAid, sessionId)) ?? 0) > operationSequence));
106
+ if (replacedSessionId)
107
+ throw new HandoffSessionReplacedError(replacedSessionId);
108
+ this.dispatcher.expireQueued(input.selfAid, target.id);
109
+ throwIfAborted(input.signal);
110
+ const head = this.store.listByTarget(input.selfAid, target.id, 'queued')[0];
111
+ if (head?.attention_required) {
112
+ throw new HandoffTargetBlockedError(head.handoff_id, head.attention_reason ?? 'UNKNOWN_ATTENTION');
113
+ }
114
+ return this.store.create({
115
+ selfAid: input.selfAid,
116
+ originSessionId: origin.id,
117
+ originMessageId: input.originMessageId,
118
+ targetSessionId: target.id,
119
+ payload: input.payload,
120
+ encrypt: input.encrypt,
121
+ delivery: targetChatType === 'group'
122
+ ? { chatType: 'group', groupId: input.to }
123
+ : { chatType: 'private' },
124
+ returnPolicy: input.explicitReturnPolicy ?? 'required',
125
+ originAuthorization: input.originAuthorization,
126
+ causation: handoffCausation,
127
+ now: this.now?.(),
128
+ });
102
129
  });
103
130
  recordCausationSpan(handoffCausation, 'handoff.request', {
104
131
  status: 'started',
@@ -106,6 +133,10 @@ export class HandoffRuntime {
106
133
  });
107
134
  await this.dispatcher.drain(input.selfAid, target.id);
108
135
  const dispatched = this.store.get(input.selfAid, handoff.handoff_id) ?? handoff;
136
+ const replacedSessionId = [origin.id, target.id].find(sessionId => ((this.sessionDiscardSequence.get(this.sessionKey(input.selfAid, sessionId)) ?? 0) > operationSequence));
137
+ if (dispatched.state === 'discarded' || replacedSessionId) {
138
+ throw new HandoffSessionReplacedError(replacedSessionId ?? target.id);
139
+ }
109
140
  if (dispatched.state === 'failed') {
110
141
  throw new HandoffDeliveryFailedError(dispatched.handoff_id);
111
142
  }
@@ -142,7 +173,9 @@ export class HandoffRuntime {
142
173
  }
143
174
  markBindingIncomplete(selfAid, handoffId) {
144
175
  const instance = this.store.get(selfAid, handoffId);
145
- if (instance && !instance.attention_required) {
176
+ if (instance
177
+ && (instance.state === 'target_context_pending' || instance.state === 'return_pending')
178
+ && !instance.attention_required) {
146
179
  this.store.markAttention(selfAid, handoffId, 'REPLY_BINDING_INCOMPLETE');
147
180
  }
148
181
  }
@@ -185,6 +218,8 @@ export class HandoffRuntime {
185
218
  messageId,
186
219
  timestamp: Date.now(),
187
220
  source: 'handoff',
221
+ groupId: origin.chatType === 'group' ? (origin.metadata?.groupId || origin.channelId) : undefined,
222
+ replyContext: replyContextFromSession(origin),
188
223
  boundSessionId: origin.id,
189
224
  causation: instance.return_causation
190
225
  ? deriveCausation(instance.return_causation)
@@ -207,16 +242,31 @@ export class HandoffRuntime {
207
242
  role: origin.chatType === 'group' ? (origin.identity?.role ?? 'none') : undefined,
208
243
  sessionKeyField: origin.sessionKey,
209
244
  selfAID: origin.selfAID,
210
- onPersisted: () => this.store.recordOriginEnqueued(selfAid, handoffId, messageId),
245
+ onPersisted: () => {
246
+ if (this.store.get(selfAid, handoffId)?.state !== 'origin_queued') {
247
+ throw new Error(`handoff is no longer deliverable: ${handoffId}`);
248
+ }
249
+ this.store.recordOriginEnqueued(selfAid, handoffId, messageId);
250
+ },
211
251
  });
212
252
  }
213
253
  catch (error) {
214
- this.store.recordOriginEnqueueFailed(selfAid, handoffId, error instanceof Error ? error.message : String(error));
254
+ if (this.store.get(selfAid, handoffId)?.state === 'origin_queued') {
255
+ this.store.recordOriginEnqueueFailed(selfAid, handoffId, error instanceof Error ? error.message : String(error));
256
+ }
215
257
  }
216
258
  }
217
259
  buildPromptItem(message) {
218
260
  return this.buildPromptItems(message)[0] ?? null;
219
261
  }
262
+ isDiscardedDelivery(message) {
263
+ const delivery = message.handoffDelivery;
264
+ const selfAid = message.selfAID;
265
+ if (!delivery || !selfAid)
266
+ return false;
267
+ const ids = delivery.handoffIds?.length ? delivery.handoffIds : [delivery.handoffId];
268
+ return ids.length > 0 && ids.every(handoffId => this.store.get(selfAid, handoffId)?.state === 'discarded');
269
+ }
220
270
  buildPromptItems(message) {
221
271
  const delivery = message.handoffDelivery;
222
272
  const selfAid = message.selfAID;
@@ -367,9 +417,54 @@ export class HandoffRuntime {
367
417
  expired += this.dispatcher.expireQueued(selfAid).length;
368
418
  return expired;
369
419
  }
370
- async recover(selfAids) {
420
+ async discardSession(selfAid, sessionId) {
421
+ this.sessionDiscardSequence.set(this.sessionKey(selfAid, sessionId), ++this.lifecycleSequence);
422
+ const candidates = this.store.list(selfAid)
423
+ .filter(instance => (instance.origin_session_id === sessionId || instance.target_session_id === sessionId) && instance.state !== 'failed' && instance.state !== 'completed' && instance.state !== 'discarded');
424
+ let discarded = 0;
425
+ for (const instance of candidates) {
426
+ const result = await this.discardWithTargetLock(selfAid, instance.handoff_id, 'session_replaced', {
427
+ session_id: sessionId,
428
+ });
429
+ if (result)
430
+ discarded++;
431
+ }
432
+ return discarded;
433
+ }
434
+ async discardExpired(selfAids, retentionMs = HANDOFF_RETENTION_MS) {
435
+ const now = this.now?.() ?? Date.now();
436
+ let discarded = 0;
437
+ for (const selfAid of selfAids) {
438
+ const latestEventVersions = new Map();
439
+ for (const event of this.store.events(selfAid)) {
440
+ if (!event.handoff_id || event.to_version === undefined)
441
+ continue;
442
+ latestEventVersions.set(event.handoff_id, Math.max(latestEventVersions.get(event.handoff_id) ?? 0, event.to_version));
443
+ }
444
+ for (const instance of this.store.list(selfAid)) {
445
+ if (instance.state === 'failed' || instance.state === 'completed' || instance.state === 'discarded')
446
+ continue;
447
+ if (now - instance.created_at <= retentionMs)
448
+ continue;
449
+ if ((latestEventVersions.get(instance.handoff_id) ?? 0) !== instance.version)
450
+ continue;
451
+ const result = await this.discardWithTargetLock(selfAid, instance.handoff_id, 'retention_expired', {
452
+ retention_ms: retentionMs,
453
+ age_ms: now - instance.created_at,
454
+ }, instance.version, now);
455
+ if (result) {
456
+ discarded++;
457
+ logger.info(`[Handoff] discarded expired handoff: ${instance.handoff_id}`);
458
+ }
459
+ }
460
+ }
461
+ return discarded;
462
+ }
463
+ async recover(selfAids, options = {}) {
371
464
  const originEnqueues = [];
372
465
  for (const selfAid of selfAids) {
466
+ if (options.discardExpired !== false)
467
+ await this.discardExpired([selfAid]);
373
468
  // v1 initially represented return=none as return_pending and required an
374
469
  // empty manual return. It has already been shown to a target turn, so
375
470
  // finish it before version reconciliation instead of prompting again.
@@ -412,6 +507,40 @@ export class HandoffRuntime {
412
507
  }
413
508
  await Promise.all(originEnqueues);
414
509
  }
510
+ async discardWithTargetLock(selfAid, handoffId, reason, details, expectedVersion, now = this.now?.()) {
511
+ const snapshot = this.store.get(selfAid, handoffId);
512
+ if (!snapshot)
513
+ return false;
514
+ const discarded = await this.mutexes.forKey(this.targetKey(selfAid, snapshot.target_session_id)).runExclusive(async () => {
515
+ const current = this.store.get(selfAid, handoffId);
516
+ if (!current || current.state === 'failed' || current.state === 'completed' || current.state === 'discarded') {
517
+ return null;
518
+ }
519
+ if (expectedVersion !== undefined && current.version !== expectedVersion)
520
+ return null;
521
+ this.store.discard(selfAid, handoffId, reason, details, now);
522
+ return current;
523
+ });
524
+ if (!discarded)
525
+ return false;
526
+ await this.cancelOriginDelivery(discarded);
527
+ return true;
528
+ }
529
+ async cancelOriginDelivery(instance) {
530
+ if (instance.state !== 'origin_queued' && instance.state !== 'origin_delivered')
531
+ return;
532
+ const messageId = instance.origin_delivery_message_id
533
+ ?? `origin-deliver:${instance.handoff_id}:${instance.origin_session_id}`;
534
+ const queue = this.messageQueue;
535
+ try {
536
+ const queued = queue.cancelMessageByIdInSession?.(instance.origin_session_id, messageId) ?? false;
537
+ if (!queued)
538
+ await queue.cancelActiveAndWait?.(messageId);
539
+ }
540
+ catch (error) {
541
+ logger.warn(`[Handoff] failed to cancel discarded origin delivery ${messageId}:`, error);
542
+ }
543
+ }
415
544
  warnIfStale(instance) {
416
545
  if (instance.state !== 'target_sent' && instance.state !== 'return_pending')
417
546
  return;
@@ -425,6 +554,12 @@ export class HandoffRuntime {
425
554
  + ` ageHours=${Math.floor(ageMs / 3_600_000)}`
426
555
  + ` originSession=${instance.origin_session_id} targetSession=${instance.target_session_id}`);
427
556
  }
557
+ targetKey(selfAid, targetSessionId) {
558
+ return `${selfAid}\u0000${targetSessionId}`;
559
+ }
560
+ sessionKey(selfAid, sessionId) {
561
+ return `${selfAid}\u0000${sessionId}`;
562
+ }
428
563
  warnIfLargeAggregate(message, instances, direction) {
429
564
  const chars = message.content.length
430
565
  + 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}`);
@@ -114,7 +115,11 @@ export class HandoffStore {
114
115
  origin_session_id: input.originSessionId,
115
116
  origin_message_id: input.originMessageId,
116
117
  target_session_id: input.targetSessionId,
117
- request: { payload: clonePayload(input.payload), encrypt: input.encrypt },
118
+ request: {
119
+ payload: clonePayload(input.payload),
120
+ encrypt: input.encrypt,
121
+ ...(input.delivery ? { delivery: input.delivery } : {}),
122
+ },
118
123
  ...(input.originAuthorization
119
124
  ? { origin_authorization: { ...input.originAuthorization } }
120
125
  : {}),
@@ -514,6 +519,29 @@ export class HandoffStore {
514
519
  this.write(selfAid, instance);
515
520
  return instance;
516
521
  }
522
+ discard(selfAid, handoffId, reason, details = {}, now = Date.now()) {
523
+ const instance = this.get(selfAid, handoffId);
524
+ if (!instance)
525
+ throw new Error('handoff not found');
526
+ if (TERMINAL_HANDOFF_STATES.has(instance.state))
527
+ return instance;
528
+ const previousState = instance.state;
529
+ const previousVersion = instance.version;
530
+ instance.state = 'discarded';
531
+ instance.attention_required = false;
532
+ instance.attention_reason = null;
533
+ instance.version++;
534
+ instance.updated_at = now;
535
+ instance.completed_at = now;
536
+ this.appendEvent(selfAid, 'handoff_discarded', handoffId, {
537
+ fromVersion: previousVersion,
538
+ toVersion: instance.version,
539
+ details: { ...details, reason, previous_state: previousState },
540
+ now,
541
+ });
542
+ this.write(selfAid, instance);
543
+ return instance;
544
+ }
517
545
  requireState(selfAid, handoffId, expected) {
518
546
  const instance = this.get(selfAid, handoffId);
519
547
  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;
@@ -228,6 +228,16 @@ export class InteractionRouter {
228
228
  return undefined;
229
229
  }
230
230
  }
231
+ /** Bind an adapter response to its runtime-owned channel identity domain. */
232
+ export function registerAdapterInteractions(adapter, router) {
233
+ const channelKey = adapter.channelKey;
234
+ adapter.onInteraction?.(async (response) => {
235
+ return router.handleAsync({ ...response, channelKey });
236
+ });
237
+ adapter.onInteractionInvalidated?.((interactionId, reason) => {
238
+ return router.cancelFromChannel(interactionId, reason);
239
+ });
240
+ }
231
241
  /** 把 CommandCard 渲染为文本提示,channel 不支持卡片时使用 */
232
242
  export function renderCommandCardAsText(card) {
233
243
  const lines = [card.title];
@@ -244,6 +244,25 @@ export class IMRenderer {
244
244
  durationMs: opts.durationMs,
245
245
  });
246
246
  }
247
+ /** 发送不进入 activity/thought 历史的非终态 warning status。 */
248
+ async sendWarningStatus(message, subtype, details) {
249
+ const payload = {
250
+ kind: 'status.progress',
251
+ metadata: {
252
+ activityType: 'progress',
253
+ message,
254
+ severity: 'warn',
255
+ ...(subtype !== undefined && { subtype }),
256
+ ...(details?.missingItems !== undefined && { missingItems: details.missingItems }),
257
+ },
258
+ };
259
+ try {
260
+ await this.opts.send(payload);
261
+ }
262
+ catch (error) {
263
+ logger.warn(`[IMRenderer] warning status send failed: ${error instanceof Error ? error.message : String(error)}`);
264
+ }
265
+ }
247
266
  /** 添加任务生命周期边界事件,复用 activity.batch 投影路径。 */
248
267
  addLifecycle(phase, metadata, text) {
249
268
  const item = {
@@ -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
+ }