evolcore 0.0.12 → 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 (104) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/ec-safe-output.js +26 -7
  3. package/dist/agents/claude-runner.js +53 -7
  4. package/dist/agents/codex-app-server-client.js +53 -5
  5. package/dist/agents/codex-runner.js +104 -59
  6. package/dist/agents/ecagent-runner.js +4 -1
  7. package/dist/aun/aid/control-aid.js +36 -13
  8. package/dist/aun/aid/store.js +13 -7
  9. package/dist/aun/group-fs-download.js +56 -0
  10. package/dist/aun/group-identity.js +1 -2
  11. package/dist/aun/msg/group.js +11 -33
  12. package/dist/aun/msg/index.js +1 -2
  13. package/dist/aun/msg/managed-operation.js +116 -55
  14. package/dist/aun/msg/upload.js +16 -1
  15. package/dist/aun/outbox.js +31 -2
  16. package/dist/aun/rpc/client.js +66 -0
  17. package/dist/aun/rpc/index.js +1 -2
  18. package/dist/aun/storage/{upload.js → client.js} +11 -2
  19. package/dist/aun/storage/index.js +1 -2
  20. package/dist/channels/aun.js +371 -136
  21. package/dist/cli/agent.js +9 -96
  22. package/dist/cli/cli-argv.js +10 -0
  23. package/dist/cli/config.js +9 -17
  24. package/dist/cli/ctl-command.js +1 -1
  25. package/dist/cli/daemon-commands.js +39 -24
  26. package/dist/cli/fs-command.js +19 -10
  27. package/dist/cli/init.js +77 -8
  28. package/dist/cli/queue-command.js +42 -0
  29. package/dist/cli/restart-monitor.js +3 -14
  30. package/dist/cli/trigger-command.js +7 -0
  31. package/dist/config/access-policy.js +1 -2
  32. package/dist/config/builtin-roles.js +5 -0
  33. package/dist/config/config-manager.js +21 -1
  34. package/dist/config/peer-role-resolver.js +6 -2
  35. package/dist/config/role-migration-startup.js +53 -0
  36. package/dist/config/role-store.js +1 -1
  37. package/dist/core/auth/agent-delegation.js +1 -1
  38. package/dist/core/auth/auth-gateway.js +29 -1
  39. package/dist/core/auth/authorization-audit.js +15 -0
  40. package/dist/core/auth/operation-authorizer.js +1 -0
  41. package/dist/core/auth/operation-catalog.js +9 -0
  42. package/dist/core/bootstrap-messages.js +9 -2
  43. package/dist/core/bootstrap-service.js +1 -0
  44. package/dist/core/command/command-handler.js +198 -51
  45. package/dist/core/command/menu-catalog.js +4 -0
  46. package/dist/core/command/menu-handler.js +85 -18
  47. package/dist/core/command/menu-protocol.js +23 -2
  48. package/dist/core/command/slash-handler.js +58 -10
  49. package/dist/core/event-catalog.js +6 -0
  50. package/dist/core/handoff/runtime.js +12 -1
  51. package/dist/core/handoff/store.js +5 -1
  52. package/dist/core/interaction-router.js +10 -0
  53. package/dist/core/message/im-renderer.js +19 -0
  54. package/dist/core/message/message-bridge.js +63 -11
  55. package/dist/core/message/message-log.js +2 -0
  56. package/dist/core/message/message-queue.js +267 -39
  57. package/dist/core/message/message-utils.js +81 -4
  58. package/dist/core/message/response-engine.js +107 -41
  59. package/dist/core/message/stream-debouncer.js +10 -2
  60. package/dist/core/permission/approval-gateway.js +1 -0
  61. package/dist/core/permission/ec-command-parser.js +93 -14
  62. package/dist/core/permission/index.js +1 -1
  63. package/dist/core/permission/mode.js +15 -0
  64. package/dist/core/permission/readonly-shell-query.js +148 -8
  65. package/dist/core/permission/sandbox-runtime.js +21 -0
  66. package/dist/core/permission/tool-policy.js +249 -27
  67. package/dist/core/protected-paths.js +107 -2
  68. package/dist/core/session/session-manager.js +21 -2
  69. package/dist/core/session/session-mapper.js +20 -1
  70. package/dist/eck/detect.js +18 -1
  71. package/dist/eck/group-rules-sync.js +15 -44
  72. package/dist/index.js +161 -86
  73. package/dist/ipc.js +16 -3
  74. package/dist/paths.js +3 -0
  75. package/dist/trigger/feedback.js +64 -11
  76. package/dist/trigger/manager.js +21 -1
  77. package/dist/trigger/parser.js +14 -2
  78. package/dist/trigger/scheduler.js +1 -0
  79. package/dist/trigger/validation.js +67 -6
  80. package/dist/utils/ecweb-supervisor.js +332 -0
  81. package/dist/utils/error-utils.js +10 -0
  82. package/kits/docs/evolcore/trigger.md +17 -1
  83. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  84. package/kits/schemas/role-config.schema.1.json +1 -0
  85. package/kits/templates/roles/admin.json +5 -0
  86. package/kits/templates/roles/member.json +7 -1
  87. package/kits/templates/roles/visitor.json +2 -0
  88. package/package.json +1 -1
  89. package/dist/aun/msg/payload-type.js +0 -27
  90. package/dist/aun/rpc/caller.js +0 -42
  91. package/dist/aun/rpc/connection.js +0 -25
  92. package/dist/aun/storage/manage.js +0 -10
  93. package/dist/config/access-policy-domain.js +0 -18
  94. package/dist/config/config-batch-get.js +0 -11
  95. package/dist/config/owner-policy.js +0 -4
  96. package/dist/config/role-config-v4-startup.js +0 -32
  97. package/dist/config/role-config-v5-startup.js +0 -27
  98. package/dist/core/auth/trigger-authorization.js +0 -15
  99. package/dist/core/interaction-registration.js +0 -10
  100. package/dist/core/permission/execution-sandbox.js +0 -16
  101. package/dist/core/relation/peer-key.js +0 -1
  102. package/dist/core/session/session-key.js +0 -24
  103. package/dist/eck/baseagent-caps.js +0 -18
  104. package/dist/eck/rules-loader.js +0 -28
package/dist/paths.js CHANGED
@@ -149,6 +149,9 @@ export function daemonControlDir() {
149
149
  export function daemonQueuePath() {
150
150
  return path.join(daemonDataDir(), 'message-queue.json');
151
151
  }
152
+ export function daemonMessageDedupPath() {
153
+ return path.join(daemonDataDir(), 'message-dedup.json');
154
+ }
152
155
  export function channelStateDir(channelKey) {
153
156
  if (!channelKey || channelKey === '.' || channelKey === '..') {
154
157
  throw new Error('channelKey is required');
@@ -23,9 +23,13 @@ export class TriggerFeedbackDispatcher {
23
23
  dryRunText: renderedText,
24
24
  };
25
25
  }
26
- const target = this.resolveDeliveryTarget(input.trigger);
27
- if (!target)
26
+ const unresolvedTarget = this.resolveDeliveryTarget(input.trigger);
27
+ if (!unresolvedTarget)
28
28
  return this.failed(feedback, [], 'target_missing', 'trigger feedback target is missing');
29
+ const target = unresolvedTarget;
30
+ if (channelTypeFromKey(target.channelKey) === 'aun' && !targetChatType(target)) {
31
+ return this.failed(feedback, [], 'route_missing', 'AUN trigger feedback has no explicit delivery route');
32
+ }
29
33
  feedback.target = target;
30
34
  if (input.dryRun) {
31
35
  return { status: 'dry-run', feedback, effects: [], error: null, dryRunText: renderedText };
@@ -43,9 +47,13 @@ export class TriggerFeedbackDispatcher {
43
47
  }
44
48
  async dispatchTargetSession(input) {
45
49
  const feedback = this.auditFeedback(input.trigger);
46
- const target = this.resolveDeliveryTarget(input.trigger);
47
- if (!target)
50
+ const unresolvedTarget = this.resolveDeliveryTarget(input.trigger);
51
+ if (!unresolvedTarget)
48
52
  return this.failed(feedback, [], 'target_missing', 'target_session feedback target is missing');
53
+ const target = unresolvedTarget;
54
+ if (channelTypeFromKey(target.channelKey) === 'aun' && !targetChatType(target)) {
55
+ return this.failed(feedback, [], 'route_missing', 'AUN target_session has no explicit delivery route');
56
+ }
49
57
  feedback.target = target;
50
58
  feedback.renderedTextHash = sha256(input.prompt);
51
59
  feedback.renderedTextPreview = previewText(input.prompt);
@@ -105,7 +113,14 @@ export class TriggerFeedbackDispatcher {
105
113
  ? undefined
106
114
  : await raceWithAbort(Promise.resolve(mainLookup), delivery.signal);
107
115
  const mainMetadata = main?.metadata;
108
- const chatType = main?.chatType === 'group' ? 'group' : 'private';
116
+ const targetDelivery = targetDeliveryFor(target);
117
+ const chatType = targetDelivery?.chatType
118
+ ?? (main?.chatType === 'group' ? 'group' : 'private');
119
+ const groupId = targetDelivery?.chatType === 'group'
120
+ ? targetDelivery.groupId
121
+ : targetDelivery?.chatType === 'private'
122
+ ? undefined
123
+ : mainMetadata?.groupId;
109
124
  const peerId = mainMetadata?.peerId ?? `trigger:${input.trigger.id}`;
110
125
  const peerType = mainMetadata?.peerType ?? 'system';
111
126
  session = await raceWithAbort(this.deps.sessionManager.getOrCreateSession(target.channelKey, target.channelId, main?.projectPath ?? binding.projectPath, target.threadId, {
@@ -113,13 +128,20 @@ export class TriggerFeedbackDispatcher {
113
128
  peerId,
114
129
  peerName: mainMetadata?.peerName ?? input.trigger.name,
115
130
  peerType,
116
- ...(mainMetadata?.groupId ? { groupId: mainMetadata.groupId } : {}),
131
+ ...(groupId ? { groupId } : {}),
117
132
  ...(mainMetadata?.groupName ? { groupName: mainMetadata.groupName } : {}),
118
133
  replyContext: replyContextFromTarget(target),
119
134
  }, input.trigger.name, peerId, chatType, main?.baseagent ?? binding.baseagent, main?.selfAID ?? input.trigger.agentAid, main?.channelType ?? channelType, peerType), delivery.signal);
120
135
  }
121
136
  else {
122
- session = await raceWithAbort(this.deps.sessionManager.getOrCreateSession(target.channelKey, target.channelId, binding.projectPath, undefined, { channelKey: target.channelKey, peerId: `trigger:${input.trigger.id}`, peerName: input.trigger.name }, input.trigger.name, undefined, 'private', binding.baseagent, input.trigger.agentAid, channelType, 'system'), delivery.signal);
137
+ const targetDelivery = targetDeliveryFor(target);
138
+ session = await raceWithAbort(this.deps.sessionManager.getOrCreateSession(target.channelKey, target.channelId, binding.projectPath, undefined, {
139
+ channelKey: target.channelKey,
140
+ peerId: `trigger:${input.trigger.id}`,
141
+ peerName: input.trigger.name,
142
+ ...(targetDelivery?.chatType === 'group' ? { groupId: targetDelivery.groupId } : {}),
143
+ replyContext: replyContextFromTarget(target),
144
+ }, input.trigger.name, undefined, targetDelivery?.chatType === 'group' ? 'group' : 'private', binding.baseagent, input.trigger.agentAid, channelType, 'system'), delivery.signal);
123
145
  }
124
146
  }
125
147
  catch (err) {
@@ -143,6 +165,12 @@ export class TriggerFeedbackDispatcher {
143
165
  }
144
166
  releaseSessionLock = access.release;
145
167
  }
168
+ // The persisted session can predate an explicit target route, and the
169
+ // same channelId may legally be used by both private and group sends.
170
+ // Keep the per-trigger route authoritative for this queued message.
171
+ const targetDelivery = targetDeliveryFor(target);
172
+ const targetChatType = targetDelivery?.chatType;
173
+ const messageChatType = targetChatType ?? (session.chatType === 'group' ? 'group' : 'private');
146
174
  const message = {
147
175
  channel: target.channelKey,
148
176
  channelType,
@@ -150,7 +178,7 @@ export class TriggerFeedbackDispatcher {
150
178
  selfAID: input.trigger.agentAid,
151
179
  baseagent: session.baseagent || binding.baseagent,
152
180
  threadId: session.threadId || '',
153
- chatType: session.chatType === 'group' ? 'group' : 'private',
181
+ chatType: messageChatType,
154
182
  peerId: `trigger:${input.trigger.id}`,
155
183
  peerName: input.trigger.name,
156
184
  peerType: 'system',
@@ -159,6 +187,8 @@ export class TriggerFeedbackDispatcher {
159
187
  timestamp: Date.now(),
160
188
  source: 'trigger',
161
189
  causation: input.causation,
190
+ groupId: targetChatType === 'group' ? targetDelivery.groupId : undefined,
191
+ replyContext: replyContextFromTarget(target),
162
192
  triggerMeta: {
163
193
  triggerId: input.trigger.id,
164
194
  runId: input.runId,
@@ -346,6 +376,9 @@ export class TriggerFeedbackDispatcher {
346
376
  const startedAt = Date.now();
347
377
  try {
348
378
  const isAunTarget = channelTypeFromKey(target.channelKey) === 'aun';
379
+ if (isAunTarget && !targetChatType(target)) {
380
+ throw Object.assign(new Error('AUN trigger feedback requires an explicit delivery route'), { code: 'AUN_OUTBOUND_ROUTE_REQUIRED' });
381
+ }
349
382
  await raceWithAbort(binding.adapter.send(buildEnvelope({
350
383
  taskId: `trigger:${input.runId}`,
351
384
  channel: binding.adapter.channelKey,
@@ -579,12 +612,32 @@ function originAsTarget(origin) {
579
612
  channelId: origin.channelId,
580
613
  session: origin.session,
581
614
  threadId: origin.threadId,
615
+ chatType: origin.delivery?.chatType,
582
616
  };
583
617
  }
584
618
  function replyContextFromTarget(target) {
585
- if (target.session !== 'thread' || !target.threadId)
586
- return undefined;
587
- return { threadId: target.threadId };
619
+ const ctx = {};
620
+ const delivery = targetDeliveryFor(target);
621
+ if (delivery)
622
+ ctx.delivery = delivery;
623
+ if (target.session === 'thread' && target.threadId)
624
+ ctx.threadId = target.threadId;
625
+ return Object.keys(ctx).length > 0 ? ctx : undefined;
626
+ }
627
+ function targetChatType(target) {
628
+ return target.chatType;
629
+ }
630
+ function targetDeliveryFor(target) {
631
+ const chatType = targetChatType(target);
632
+ if (chatType === 'private')
633
+ return { chatType: 'private' };
634
+ if (chatType === 'group') {
635
+ return {
636
+ chatType: 'group',
637
+ groupId: target.channelId,
638
+ };
639
+ }
640
+ return undefined;
588
641
  }
589
642
  function replyContextWithCausation(target, causation) {
590
643
  const base = replyContextFromTarget(target);
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { agentTriggersDir } from '../paths.js';
4
- import { definitionRevision, normalizeTriggerDefinition, resolveScriptPath, safeRelativePath, } from './validation.js';
4
+ import { assertExplicitAunDeliveryRoutes, definitionRevision, normalizeTriggerDefinition, resolveScriptPath, safeRelativePath, } from './validation.js';
5
5
  import { atomicWriteJson } from '../core/session/session-fs-store.js';
6
6
  import { TriggerHistoryStore } from './history.js';
7
7
  import { rewriteLegacyTriggerSessionPaths } from './legacy-session-history.js';
@@ -36,6 +36,12 @@ export class TriggerDefinitionManager {
36
36
  return undefined;
37
37
  const raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
38
38
  const definition = normalizeTriggerDefinition(raw);
39
+ if (legacyFeedbackTargetNeedsMigration(raw, definition)) {
40
+ definition.feedback.target.chatType ??= 'private';
41
+ definition.updatedAt = Date.now();
42
+ this.writeDefinition(definition);
43
+ this.history.recordDefinition('trigger.updated', definition, { reason: 'legacy-aun-target-chat-type' });
44
+ }
39
45
  if (typeof definition.execution.prompt === 'string') {
40
46
  const rewritten = rewriteLegacyTriggerSessionPaths(definition.execution.prompt, definition.agentAid);
41
47
  if (rewritten !== definition.execution.prompt) {
@@ -56,6 +62,7 @@ export class TriggerDefinitionManager {
56
62
  create(input, files = [], opts = {}) {
57
63
  const now = Date.now();
58
64
  const definition = normalizeTriggerDefinition(input, { now });
65
+ assertExplicitAunDeliveryRoutes(definition);
59
66
  this.assertAgent(definition);
60
67
  if (opts.enable !== undefined)
61
68
  definition.enabled = opts.enable;
@@ -82,6 +89,7 @@ export class TriggerDefinitionManager {
82
89
  id: triggerId,
83
90
  ...(origin ? { origin } : {}),
84
91
  }, { now: Date.now() });
92
+ assertExplicitAunDeliveryRoutes(updated);
85
93
  this.assertAgent(updated);
86
94
  if (existing.execution.baseagent !== updated.execution.baseagent) {
87
95
  throw new Error('execution.baseagent is immutable; create a new Trigger to use another baseagent');
@@ -101,6 +109,8 @@ export class TriggerDefinitionManager {
101
109
  }
102
110
  setEnabled(triggerId, enabled) {
103
111
  const definition = this.require(triggerId);
112
+ if (enabled)
113
+ assertExplicitAunDeliveryRoutes(definition);
104
114
  definition.enabled = enabled;
105
115
  definition.updatedAt = Date.now();
106
116
  this.writeDefinition(definition);
@@ -270,6 +280,16 @@ export class TriggerDefinitionManager {
270
280
  function optionalString(value) {
271
281
  return typeof value === 'string' && value.length > 0 ? value : undefined;
272
282
  }
283
+ function legacyFeedbackTargetNeedsMigration(raw, definition) {
284
+ if (!isRecord(raw) || !definition.feedback.target)
285
+ return false;
286
+ const feedback = isRecord(raw.feedback) ? raw.feedback : undefined;
287
+ const target = feedback && isRecord(feedback.target) ? feedback.target : undefined;
288
+ if (!target || target.chatType !== undefined)
289
+ return false;
290
+ const channelKey = optionalString(target.channelKey);
291
+ return !!channelKey && (channelKey === 'aun' || channelKey.startsWith('aun#'));
292
+ }
273
293
  function mergeOrigin(existing, incoming) {
274
294
  if (existing?.channelKey && existing.channelId)
275
295
  return existing;
@@ -9,7 +9,7 @@ const TRIGGER_CREATE_FLAGS = new Set([
9
9
  'once', 'delay', 'at', 'cron', 'every', 'event', 'tz',
10
10
  'exec', 'prompt', 'name', 'agent',
11
11
  'script-path', 'script-runtime', 'script-args', 'script-timeout',
12
- 'feedback', 'target-channel', 'target-channel-id', 'target-session', 'target-thread-id',
12
+ 'feedback', 'target-channel', 'target-channel-id', 'target-chat-type', 'target-session', 'target-thread-id',
13
13
  'trigger-thread', 'baseagent',
14
14
  'model', 'effort', 'permission', 'max-runs', 'max-duration',
15
15
  ]);
@@ -375,13 +375,22 @@ function commonParsed(flags, opts = {}) {
375
375
  const targetChannelId = flags.get('target-channel-id');
376
376
  if (targetChannelId === true)
377
377
  return { ok: false, error: '--target-channel-id 不能为空' };
378
+ const targetChatType = flags.get('target-chat-type');
379
+ if (targetChatType === true)
380
+ return { ok: false, error: '--target-chat-type 不能为空' };
381
+ if (targetChatType !== undefined && targetChatType !== 'private' && targetChatType !== 'group') {
382
+ return { ok: false, error: '--target-chat-type 只接受 private 或 group' };
383
+ }
378
384
  const targetThreadId = flags.get('target-thread-id');
379
385
  if (targetThreadId === true)
380
386
  return { ok: false, error: '--target-thread-id 不能为空' };
381
- const hasTargetField = targetChannel !== undefined || targetChannelId !== undefined || targetSession.value !== undefined || targetThreadId !== undefined;
387
+ const hasTargetField = targetChannel !== undefined || targetChannelId !== undefined || targetChatType !== undefined || targetSession.value !== undefined || targetThreadId !== undefined;
382
388
  if ((targetChannel === undefined) !== (targetChannelId === undefined)) {
383
389
  return { ok: false, error: '--target-channel 与 --target-channel-id 必须同时指定或同时省略' };
384
390
  }
391
+ if (targetChatType !== undefined && targetChannelId === undefined) {
392
+ return { ok: false, error: '--target-chat-type 必须与 --target-channel/--target-channel-id 一起使用' };
393
+ }
385
394
  if (!opts.update && !execution.value) {
386
395
  return { ok: false, error: '创建 trigger 必须显式指定 --exec script|trigger-session|target-session' };
387
396
  }
@@ -409,6 +418,7 @@ function commonParsed(flags, opts = {}) {
409
418
  feedbackStrategy,
410
419
  targetChannel: targetChannel,
411
420
  targetChannelId: targetChannelId,
421
+ targetChatType: targetChatType,
412
422
  targetSession: targetSessionValue,
413
423
  targetThreadId: targetThreadId,
414
424
  agentId: flags.has('agent') ? flags.get('agent') : undefined,
@@ -447,6 +457,7 @@ export function parseTriggerSet(args) {
447
457
  feedbackStrategy: value.feedbackStrategy,
448
458
  targetChannel: value.targetChannel,
449
459
  targetChannelId: value.targetChannelId,
460
+ targetChatType: value.targetChatType,
450
461
  targetSession: value.targetSession,
451
462
  targetThreadId: value.targetThreadId,
452
463
  agentId: value.agentId,
@@ -486,6 +497,7 @@ export function parseTriggerSetArgv(args, opts = {}) {
486
497
  feedbackStrategy: value.feedbackStrategy,
487
498
  targetChannel: value.targetChannel,
488
499
  targetChannelId: value.targetChannelId,
500
+ targetChatType: value.targetChatType,
489
501
  targetSession: value.targetSession,
490
502
  targetThreadId: value.targetThreadId,
491
503
  agentId: value.agentId,
@@ -2016,6 +2016,7 @@ function primaryTarget(definition) {
2016
2016
  channelId: definition.origin.channelId,
2017
2017
  session: definition.origin.session,
2018
2018
  threadId: definition.origin.threadId,
2019
+ chatType: definition.origin.delivery?.chatType,
2019
2020
  };
2020
2021
  }
2021
2022
  function eventTestMatchError(reason, eventPattern) {
@@ -26,6 +26,34 @@ export function normalizeTriggerDefinition(input, opts = {}) {
26
26
  }
27
27
  return normalizeV3Phase2(raw, opts);
28
28
  }
29
+ /**
30
+ * Write-time route contract for trigger definitions.
31
+ *
32
+ * Normalization keeps old AUN definitions readable so operators can inspect
33
+ * and remove them, but new writes must never persist a target that would need
34
+ * channelId shape inference at runtime.
35
+ */
36
+ export function assertExplicitAunDeliveryRoutes(definition) {
37
+ if (definition.feedback.strategy === 'silent')
38
+ return;
39
+ const target = definition.feedback.strategy === 'target'
40
+ ? definition.feedback.target
41
+ : definition.origin;
42
+ if (!target)
43
+ return;
44
+ const channelType = ('channelType' in target ? target.channelType : undefined)
45
+ ?? target.channelKey.split('#', 1)[0];
46
+ if (channelType !== 'aun')
47
+ return;
48
+ const chatType = definition.feedback.strategy === 'target'
49
+ ? definition.feedback.target?.chatType
50
+ : definition.origin?.delivery?.chatType;
51
+ if (!chatType) {
52
+ throw Object.assign(new Error('AUN trigger feedback target requires an explicit delivery route'), {
53
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
54
+ });
55
+ }
56
+ }
29
57
  export function validateTriggerDefinition(definition) {
30
58
  normalizeTriggerDefinition(definition);
31
59
  }
@@ -232,16 +260,30 @@ function normalizeOrigin(value) {
232
260
  throw new Error('origin.threadId is required when origin.session=thread');
233
261
  const rawChannelId = requiredString(raw.channelId, 'origin.channelId');
234
262
  const peerId = optionalString(raw.peerId);
235
- const legacyGroupOrigin = !!peerId && rawChannelId !== peerId;
263
+ const channelKey = requiredString(raw.channelKey, 'origin.channelKey');
264
+ const channelType = optionalString(raw.channelType);
265
+ const isAunOrigin = channelType === 'aun' || channelKey === 'aun' || channelKey.startsWith('aun#');
266
+ // Legacy non-AUN definitions used channelId/peerId shape to represent a
267
+ // group-created trigger. AUN routes deliberately do not get that recovery:
268
+ // the transport type must have been persisted explicitly.
269
+ const legacyGroupOrigin = !isAunOrigin && raw.delivery === undefined && !!peerId && rawChannelId !== peerId;
270
+ const legacyPrivateOrigin = !isAunOrigin && raw.delivery === undefined && !!peerId && rawChannelId === peerId;
236
271
  const session = legacyGroupOrigin ? 'main' : rawSession;
272
+ const channelId = legacyGroupOrigin ? peerId : rawChannelId;
273
+ const delivery = raw.delivery === undefined
274
+ ? legacyGroupOrigin || legacyPrivateOrigin || !isAunOrigin
275
+ ? { chatType: 'private' }
276
+ : undefined
277
+ : normalizeDeliveryTarget(raw.delivery, 'origin.delivery', channelId);
237
278
  return {
238
- channelKey: requiredString(raw.channelKey, 'origin.channelKey'),
239
- channelType: optionalString(raw.channelType),
240
- channelId: peerId ?? rawChannelId,
279
+ channelKey,
280
+ channelType,
281
+ channelId,
241
282
  session,
242
283
  threadId: session === 'thread' ? threadId : undefined,
243
284
  peerId,
244
285
  sessionKey: legacyGroupOrigin ? undefined : optionalString(raw.sessionKey),
286
+ delivery,
245
287
  };
246
288
  }
247
289
  function normalizeSource(value) {
@@ -486,14 +528,33 @@ function normalizeFeedbackTarget(value, label, agentAid) {
486
528
  if (session === 'thread' && !threadId)
487
529
  throw new Error(`${label}.threadId is required when ${label}.session=thread`);
488
530
  const channelKey = requiredString(raw.channelKey, `${label}.channelKey`);
489
- return {
531
+ const channelId = requiredString(raw.channelId, `${label}.channelId`);
532
+ const rawChatType = raw.chatType;
533
+ if (rawChatType !== undefined && rawChatType !== 'private' && rawChatType !== 'group') {
534
+ throw new Error(`${label}.chatType must be private or group`);
535
+ }
536
+ const target = {
490
537
  channelKey: channelKey === 'aun'
491
538
  ? formatChannelKey({ type: 'aun', selfAID: agentAid, name: 'main' })
492
539
  : channelKey,
493
- channelId: requiredString(raw.channelId, `${label}.channelId`),
540
+ channelId,
494
541
  session,
495
542
  threadId,
543
+ chatType: rawChatType,
496
544
  };
545
+ return target;
546
+ }
547
+ function normalizeDeliveryTarget(value, label, channelId) {
548
+ if (!isObject(value))
549
+ throw new Error(`${label} must be an object`);
550
+ if (value.chatType === 'private')
551
+ return { chatType: 'private' };
552
+ if (value.chatType !== 'group')
553
+ throw new Error(`${label}.chatType must be private or group`);
554
+ const groupId = requiredString(value.groupId, `${label}.groupId`);
555
+ if (groupId !== channelId)
556
+ throw new Error(`${label}.groupId must match channelId`);
557
+ return { chatType: 'group', groupId };
497
558
  }
498
559
  function normalizeFeedbackTemplate(value, label) {
499
560
  if (value === undefined)