evolcore 0.0.26 → 0.0.28

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 (177) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/baseagent.js +10 -5
  5. package/dist/agents/claude-runner.js +123 -22
  6. package/dist/agents/codex/app-server-client.js +70 -26
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex-runner.js +363 -421
  9. package/dist/agents/ecagent-runner.js +170 -25
  10. package/dist/agents/gemini-runner.js +6 -0
  11. package/dist/agents/runner-types.js +50 -2
  12. package/dist/aun/aid/agentmd.js +73 -31
  13. package/dist/aun/aid/client.js +1 -1
  14. package/dist/aun/aid/control-aid.js +180 -1
  15. package/dist/aun/aid/identity.js +3 -3
  16. package/dist/aun/group/history.js +303 -0
  17. package/dist/aun/group/identity.js +43 -0
  18. package/dist/aun/group/index.js +5 -0
  19. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  20. package/dist/aun/{menu-client.js → menu/client.js} +13 -7
  21. package/dist/aun/menu/index.js +2 -0
  22. package/dist/aun/menu/transport.js +49 -0
  23. package/dist/aun/msg/index.js +2 -1
  24. package/dist/aun/msg/managed-operation.js +84 -18
  25. package/dist/aun/msg/mention-schema.js +51 -0
  26. package/dist/aun/msg/p2p.js +13 -8
  27. package/dist/aun/msg/upload.js +43 -88
  28. package/dist/aun/outbox.js +29 -7
  29. package/dist/aun/rpc/client.js +1 -0
  30. package/dist/aun/storage/client.js +39 -50
  31. package/dist/channels/aun.js +544 -239
  32. package/dist/channels/daemon.js +10 -6
  33. package/dist/cli/agent-command.js +108 -6
  34. package/dist/cli/agent-create.js +515 -2
  35. package/dist/cli/agent.js +109 -8
  36. package/dist/cli/aun-commands.js +191 -42
  37. package/dist/cli/config.js +8 -5
  38. package/dist/cli/contact.js +2 -2
  39. package/dist/cli/ctl-command.js +2 -2
  40. package/dist/cli/daemon-commands.js +111 -162
  41. package/dist/cli/diag.js +3 -3
  42. package/dist/cli/fs-command.js +3 -3
  43. package/dist/cli/handoff-command.js +6 -6
  44. package/dist/cli/index.js +8 -2
  45. package/dist/cli/init.js +34 -1
  46. package/dist/cli/managed-command-guard.js +2 -1
  47. package/dist/cli/menu-client.js +13 -5
  48. package/dist/cli/model.js +3 -3
  49. package/dist/cli/queue-command.js +2 -2
  50. package/dist/cli/response.js +2 -2
  51. package/dist/cli/restart-monitor.js +81 -19
  52. package/dist/cli/task-command.js +59 -0
  53. package/dist/cli/task-context.js +97 -49
  54. package/dist/cli/trigger-command.js +20 -10
  55. package/dist/config/builtin-roles.js +10 -0
  56. package/dist/config/config-field-policy.js +11 -0
  57. package/dist/config/config-manager.js +32 -4
  58. package/dist/config/config-operation-service.js +20 -5
  59. package/dist/config/contact-book-store.js +1 -1
  60. package/dist/config/contact-book.js +1 -1
  61. package/dist/config/contact-operation-service.js +1 -1
  62. package/dist/config/peer-role-resolver.js +1 -1
  63. package/dist/config/resolved-config-op.js +3 -0
  64. package/dist/config-store.js +1 -1
  65. package/dist/core/agent-application-service.js +5 -0
  66. package/dist/core/audit/event-key.js +17 -2
  67. package/dist/core/audit/log-integrity.js +34 -8
  68. package/dist/core/auth/agent-delegation.js +48 -14
  69. package/dist/core/auth/auth-gateway.js +2 -0
  70. package/dist/core/auth/authorization-audit.js +34 -38
  71. package/dist/core/auth/operation-authorizer.js +67 -5
  72. package/dist/core/auth/operation-catalog.js +110 -0
  73. package/dist/core/auth/remote-access-store.js +115 -9
  74. package/dist/core/bootstrap-messages.js +66 -7
  75. package/dist/core/bootstrap-service.js +110 -19
  76. package/dist/core/command/agent-control.js +88 -7
  77. package/dist/core/command/agent-storage-menu.js +443 -0
  78. package/dist/core/command/cli-intent-parser.js +19 -0
  79. package/dist/core/command/command-handler.js +105 -40
  80. package/dist/core/command/data-root-resolver.js +273 -0
  81. package/dist/core/command/diag-service.js +292 -27
  82. package/dist/core/command/group-menu.js +7 -2
  83. package/dist/core/command/menu-catalog.js +34 -4
  84. package/dist/core/command/menu-handler.js +364 -96
  85. package/dist/core/command/menu-protocol.js +43 -6
  86. package/dist/core/command/role-menu.js +2 -2
  87. package/dist/core/command/slash-handler.js +61 -37
  88. package/dist/core/event-catalog.js +4 -0
  89. package/dist/core/evolagent-registry.js +28 -2
  90. package/dist/core/group/invite-service.js +924 -0
  91. package/dist/core/interaction-router.js +22 -8
  92. package/dist/core/message/im-renderer.js +94 -16
  93. package/dist/core/message/message-bridge.js +140 -13
  94. package/dist/core/message/message-log.js +5 -3
  95. package/dist/core/message/message-queue.js +73 -41
  96. package/dist/core/message/response-engine.js +729 -118
  97. package/dist/core/permission/approval-gateway.js +36 -3
  98. package/dist/core/permission/ec-command-parser.js +76 -36
  99. package/dist/core/permission/protected-paths.js +134 -16
  100. package/dist/core/permission/sandbox-runtime.js +72 -3
  101. package/dist/core/permission/tool-error-code.js +15 -0
  102. package/dist/core/permission/tool-policy.js +8 -8
  103. package/dist/core/relation/peer-identity.js +57 -6
  104. package/dist/core/relation/shared-data.js +106 -0
  105. package/dist/core/role/runtime-policy.js +64 -0
  106. package/dist/core/runtime-lock.js +10 -26
  107. package/dist/core/session/runner-task-history.js +170 -0
  108. package/dist/core/session/session-manager.js +38 -6
  109. package/dist/core/session/session-mapper.js +41 -0
  110. package/dist/core/session/session-turn-coordinator.js +37 -7
  111. package/dist/core/session/session-turns.js +48 -7
  112. package/dist/core/task/task-recovery-service.js +176 -0
  113. package/dist/core/task/task-resume-operation.js +32 -0
  114. package/dist/eck/group-rules-sync.js +5 -3
  115. package/dist/eck/kit-renderer.js +1 -0
  116. package/dist/index.js +322 -207
  117. package/dist/ipc.js +141 -15
  118. package/dist/paths.js +17 -4
  119. package/dist/product.js +3 -0
  120. package/dist/trigger/event-source.js +19 -0
  121. package/dist/trigger/feedback.js +233 -40
  122. package/dist/trigger/parser.js +55 -5
  123. package/dist/trigger/patch.js +9 -0
  124. package/dist/trigger/scheduler.js +30 -4
  125. package/dist/trigger/validation.js +20 -3
  126. package/dist/utils/atomic-write.js +98 -0
  127. package/dist/utils/cross-platform.js +87 -16
  128. package/dist/utils/ecweb-utils.js +1 -0
  129. package/dist/utils/error-utils.js +40 -2
  130. package/dist/utils/logger.js +23 -13
  131. package/dist/utils/npm-ops.js +119 -4
  132. package/dist/utils/package-version-info.js +244 -0
  133. package/dist/utils/tool-summary.js +59 -3
  134. package/dist/utils/welcome.js +174 -1
  135. package/dist/utils/windows-shell-trust.js +37 -0
  136. package/ecagent/dist/harness/agent-harness.js +3 -0
  137. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  138. package/kits/docs/INDEX.md +0 -6
  139. package/kits/docs/channels/aun.md +23 -2
  140. package/kits/docs/channels/feishu.md +1 -1
  141. package/kits/docs/context-assembly.md +29 -17
  142. package/kits/docs/evolcore/INDEX.md +8 -3
  143. package/kits/docs/evolcore/agent-create.md +39 -7
  144. package/kits/docs/evolcore/agent.md +33 -1
  145. package/kits/docs/evolcore/aid.md +9 -1
  146. package/kits/docs/evolcore/config.md +40 -1
  147. package/kits/docs/evolcore/group-collaboration.md +1 -1
  148. package/kits/docs/evolcore/group.md +117 -2
  149. package/kits/docs/evolcore/menu-client.md +16 -0
  150. package/kits/docs/evolcore/msg.md +3 -0
  151. package/kits/docs/evolcore/storage.md +3 -0
  152. package/kits/docs/evolcore/task.md +14 -0
  153. package/kits/docs/evolcore/trigger.md +51 -3
  154. package/kits/docs/venues/group.md +5 -0
  155. package/kits/eck_manifest.json +27 -0
  156. package/kits/rules/01-overview.md +11 -7
  157. package/kits/rules/04-relation.md +2 -1
  158. package/kits/rules/05-venue.md +1 -1
  159. package/kits/rules/06-channel.md +6 -4
  160. package/kits/schemas/agent-config.schema.12.json +25 -1
  161. package/kits/schemas/daemon.schema.6.json +2 -2
  162. package/kits/schemas/defaults.schema.5.json +1 -1
  163. package/kits/schemas/relation-config.schema.1.json +1 -1
  164. package/kits/schemas/relation-config.schema.5.json +1 -1
  165. package/kits/schemas/role-config.schema.1.json +14 -0
  166. package/kits/templates/roles/member.json +5 -0
  167. package/kits/templates/roles/visitor.json +5 -0
  168. package/kits/templates/system-fragments/baseagent.md +1 -1
  169. package/kits/templates/system-fragments/bootstrap.md +26 -9
  170. package/kits/templates/system-fragments/channel.md +8 -44
  171. package/kits/templates/system-fragments/commands.md +10 -2
  172. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  173. package/kits/templates/system-fragments/reply.md +56 -0
  174. package/package.json +7 -3
  175. package/dist/aun/group-identity.js +0 -9
  176. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  177. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -1,8 +1,13 @@
1
1
  import { buildEnvelope } from '../core/message/message-utils.js';
2
- import { renderTemplate, previewText, sha256 } from './validation.js';
2
+ import { executionTimeoutMs, renderTemplate, previewText, sha256 } from './validation.js';
3
+ import { isDaemonEventSessionTrigger } from './event-source.js';
4
+ import { TASK_RECOVERY_PROMPT } from '../core/task/task-recovery-service.js';
5
+ import { ExecutionTimeoutError, OrphanedTargetSessionError } from '../utils/error-utils.js';
3
6
  const FEEDBACK_DEADLINE_MS = 30_000;
4
- const TARGET_SESSION_DEADLINE_MS = 30 * 60_000;
5
7
  const TARGET_SESSION_CANCEL_GRACE_MS = 1_000;
8
+ const TARGET_SESSION_OUTCOME_GRACE_MS = 5_000;
9
+ const TARGET_SESSION_PROBE_MS = 30_000;
10
+ const TARGET_SESSION_ORPHAN_GRACE_MS = 120_000;
6
11
  export class TriggerFeedbackDispatcher {
7
12
  deps;
8
13
  constructor(deps) {
@@ -50,20 +55,72 @@ export class TriggerFeedbackDispatcher {
50
55
  }
51
56
  async dispatchTargetSession(input) {
52
57
  const feedback = this.auditFeedback(input.trigger);
53
- const unresolvedTarget = this.resolveDeliveryTarget(input.trigger);
58
+ let eventSession;
59
+ let eventAgentAid;
60
+ let unresolvedTarget = this.resolveDeliveryTarget(input.trigger);
61
+ if (input.trigger.feedback.strategy === 'event_session') {
62
+ const payload = input.sourcePayload;
63
+ eventAgentAid = typeof payload?.agentAid === 'string' && payload.agentAid.trim()
64
+ ? payload.agentAid.trim()
65
+ : undefined;
66
+ if (input.trigger.source.type !== 'event' || !payload || !eventAgentAid
67
+ || (!isDaemonEventSessionTrigger(input.trigger) && eventAgentAid !== input.trigger.agentAid)
68
+ || typeof payload.sessionId !== 'string' || !/^[A-Za-z0-9_-]+$/.test(payload.sessionId)
69
+ || typeof payload.peerId !== 'string' || !payload.peerId.trim()) {
70
+ return this.failed(feedback, [], 'event_session_invalid', 'event must identify the owning agent, session, and original actor');
71
+ }
72
+ try {
73
+ eventSession = await targetSessionLookup(this.deps.sessionManager.getSessionById(payload.sessionId));
74
+ }
75
+ catch (error) {
76
+ return this.failed(feedback, [], 'target_session_setup_failed', String(error));
77
+ }
78
+ if (!eventSession || eventSession.deletedAt) {
79
+ return this.failed(feedback, [], 'event_session_missing', 'event source session no longer exists');
80
+ }
81
+ if (eventSession.selfAID !== eventAgentAid
82
+ || (!isDaemonEventSessionTrigger(input.trigger) && eventSession.selfAID !== input.trigger.agentAid)) {
83
+ return this.failed(feedback, [], 'event_session_agent_mismatch', 'event source session belongs to another agent');
84
+ }
85
+ const channelKey = eventSession.metadata?.channelKey ?? eventSession.channel;
86
+ if (!['private', 'group'].includes(eventSession.chatType) || channelTypeFromKey(channelKey) === 'daemon') {
87
+ return this.failed(feedback, [], 'event_session_route_invalid', 'event source is not a channel conversation');
88
+ }
89
+ if (channelTypeFromKey(channelKey) === 'aun' && typeof payload.encrypted !== 'boolean') {
90
+ return this.failed(feedback, [], 'event_session_encryption_missing', 'AUN event source must preserve the original encryption mode');
91
+ }
92
+ unresolvedTarget = {
93
+ channelKey,
94
+ channelId: eventSession.channelId,
95
+ chatType: eventSession.chatType,
96
+ session: eventSession.threadId ? 'thread' : 'main',
97
+ ...(eventSession.threadId ? { threadId: eventSession.threadId } : {}),
98
+ };
99
+ }
54
100
  if (!unresolvedTarget)
55
101
  return this.failed(feedback, [], 'target_missing', 'target_session feedback target is missing');
56
102
  const target = unresolvedTarget;
57
103
  if (channelTypeFromKey(target.channelKey) === 'aun' && !targetChatType(target)) {
58
104
  return this.failed(feedback, [], 'route_missing', 'AUN target_session has no explicit delivery route');
59
105
  }
106
+ const isTaskErrorRecovery = !!eventSession && input.sourcePayload?.type === 'task:error';
107
+ if (isTaskErrorRecovery && (typeof input.sourcePayload?.taskId !== 'string' || !input.sourcePayload.taskId
108
+ || !Number.isSafeInteger(input.sourcePayload.generation))) {
109
+ return this.failed(feedback, [], 'recovery_event_invalid', 'task:error must identify the original task and generation');
110
+ }
111
+ const prompt = isTaskErrorRecovery ? TASK_RECOVERY_PROMPT : input.prompt;
112
+ const targetAgentAid = eventSession?.selfAID ?? input.trigger.agentAid;
60
113
  feedback.target = target;
61
- feedback.renderedTextHash = sha256(input.prompt);
62
- feedback.renderedTextPreview = previewText(input.prompt);
114
+ feedback.renderedTextHash = sha256(prompt);
115
+ feedback.renderedTextPreview = previewText(prompt);
63
116
  if (input.dryRun) {
64
117
  return { status: 'completed', feedback, effects: [], error: null };
65
118
  }
119
+ if (isTaskErrorRecovery && (!this.deps.recoverTask || !this.deps.eventBus)) {
120
+ return this.failed(feedback, [], 'recovery_unavailable', 'task recovery and outcome tracking must be configured');
121
+ }
66
122
  const delivery = new AbortController();
123
+ const timeoutMs = executionTimeoutMs(input.trigger.execution);
67
124
  let abortKind;
68
125
  const abortDelivery = (kind, reason) => {
69
126
  if (delivery.signal.aborted)
@@ -77,33 +134,36 @@ export class TriggerFeedbackDispatcher {
77
134
  input.signal?.addEventListener('abort', onInputAbort, { once: true });
78
135
  if (input.signal?.aborted)
79
136
  onInputAbort();
80
- const deadlineTimer = setTimeout(() => abortDelivery('timeout', new Error(`target session timed out after ${TARGET_SESSION_DEADLINE_MS}ms`)), TARGET_SESSION_DEADLINE_MS);
81
- deadlineTimer.unref?.();
137
+ const deadlineTimer = timeoutMs === undefined ? undefined : setTimeout(() => abortDelivery('timeout', new ExecutionTimeoutError('target_session_timeout', timeoutMs)), timeoutMs);
138
+ deadlineTimer?.unref?.();
82
139
  let outcome;
140
+ let probeTimer;
141
+ let probing = false;
142
+ let disposed = false;
83
143
  let releaseSessionLock;
84
144
  try {
85
145
  if (delivery.signal.aborted) {
86
146
  const aborted = targetSessionCancellationError(abortKind, delivery.signal.reason, false);
87
147
  return this.failed(feedback, [], aborted.code, aborted.message);
88
148
  }
89
- const binding = this.deps.getChannel(input.trigger.agentAid, target.channelKey);
90
- if (!binding) {
91
- return this.failed(feedback, [], 'channel_not_configured', `agent ${input.trigger.agentAid} has no channel ${target.channelKey}`);
149
+ const binding = this.deps.getChannel(targetAgentAid, target.channelKey);
150
+ if (!binding || binding.agentAid !== targetAgentAid) {
151
+ return this.failed(feedback, [], 'channel_not_configured', `agent ${targetAgentAid} has no channel ${target.channelKey}`);
92
152
  }
93
153
  const startedAt = Date.now();
94
154
  const channelType = channelTypeFromKey(target.channelKey);
95
155
  let session;
96
156
  try {
97
- const existingLookup = target.session === 'thread'
157
+ const existingLookup = eventSession ?? (target.session === 'thread'
98
158
  ? target.threadId && this.deps.sessionManager.getThreadSession
99
159
  ? this.deps.sessionManager.getThreadSession(target.channelKey, target.channelId, target.threadId)
100
160
  : undefined
101
161
  : this.deps.sessionManager.getActiveSession
102
162
  ? this.deps.sessionManager.getActiveSession(target.channelKey, target.channelId)
103
- : undefined;
163
+ : undefined);
104
164
  const existing = existingLookup === undefined
105
165
  ? undefined
106
- : await raceWithAbort(Promise.resolve(existingLookup), delivery.signal);
166
+ : await raceWithAbort(targetSessionLookup(Promise.resolve(existingLookup)), delivery.signal);
107
167
  if (existing) {
108
168
  session = existing;
109
169
  }
@@ -114,7 +174,7 @@ export class TriggerFeedbackDispatcher {
114
174
  const mainLookup = this.deps.sessionManager.getActiveSession?.(target.channelKey, target.channelId);
115
175
  const main = mainLookup === undefined
116
176
  ? undefined
117
- : await raceWithAbort(Promise.resolve(mainLookup), delivery.signal);
177
+ : await raceWithAbort(targetSessionLookup(Promise.resolve(mainLookup)), delivery.signal);
118
178
  const mainMetadata = main?.metadata;
119
179
  const targetDelivery = targetDeliveryFor(target);
120
180
  const chatType = targetDelivery?.chatType
@@ -126,7 +186,7 @@ export class TriggerFeedbackDispatcher {
126
186
  : mainMetadata?.groupId;
127
187
  const peerId = mainMetadata?.peerId ?? `trigger:${input.trigger.id}`;
128
188
  const peerType = mainMetadata?.peerType ?? 'system';
129
- session = await raceWithAbort(this.deps.sessionManager.getOrCreateSession(target.channelKey, target.channelId, main?.projectPath ?? binding.projectPath, target.threadId, {
189
+ session = await raceWithAbort(targetSessionLookup(this.deps.sessionManager.getOrCreateSession(target.channelKey, target.channelId, main?.projectPath ?? binding.projectPath, target.threadId, {
130
190
  channelKey: mainMetadata?.channelKey ?? target.channelKey,
131
191
  peerId,
132
192
  peerName: mainMetadata?.peerName ?? input.trigger.name,
@@ -134,17 +194,17 @@ export class TriggerFeedbackDispatcher {
134
194
  ...(groupId ? { groupId } : {}),
135
195
  ...(mainMetadata?.groupName ? { groupName: mainMetadata.groupName } : {}),
136
196
  replyContext: replyContextFromTarget(target),
137
- }, input.trigger.name, peerId, chatType, main?.baseagent ?? binding.baseagent, main?.selfAID ?? input.trigger.agentAid, main?.channelType ?? channelType, peerType), delivery.signal);
197
+ }, input.trigger.name, peerId, chatType, main?.baseagent ?? binding.baseagent, main?.selfAID ?? targetAgentAid, main?.channelType ?? channelType, peerType)), delivery.signal);
138
198
  }
139
199
  else {
140
200
  const targetDelivery = targetDeliveryFor(target);
141
- session = await raceWithAbort(this.deps.sessionManager.getOrCreateSession(target.channelKey, target.channelId, binding.projectPath, undefined, {
201
+ session = await raceWithAbort(targetSessionLookup(this.deps.sessionManager.getOrCreateSession(target.channelKey, target.channelId, binding.projectPath, undefined, {
142
202
  channelKey: target.channelKey,
143
203
  peerId: `trigger:${input.trigger.id}`,
144
204
  peerName: input.trigger.name,
145
205
  ...(targetDelivery?.chatType === 'group' ? { groupId: targetDelivery.groupId } : {}),
146
206
  replyContext: replyContextFromTarget(target),
147
- }, input.trigger.name, undefined, targetDelivery?.chatType === 'group' ? 'group' : 'private', binding.baseagent, input.trigger.agentAid, channelType, 'system'), delivery.signal);
207
+ }, input.trigger.name, undefined, targetDelivery?.chatType === 'group' ? 'group' : 'private', binding.baseagent, targetAgentAid, channelType, 'system')), delivery.signal);
148
208
  }
149
209
  }
150
210
  catch (err) {
@@ -178,17 +238,23 @@ export class TriggerFeedbackDispatcher {
178
238
  channel: target.channelKey,
179
239
  channelType,
180
240
  channelId: target.channelId,
181
- selfAID: input.trigger.agentAid,
241
+ selfAID: targetAgentAid,
182
242
  baseagent: session.baseagent || binding.baseagent,
183
243
  threadId: session.threadId || '',
184
244
  chatType: messageChatType,
185
245
  peerId: `trigger:${input.trigger.id}`,
186
246
  peerName: input.trigger.name,
187
247
  peerType: 'system',
188
- content: input.prompt,
248
+ content: prompt,
189
249
  messageId: input.attemptId,
190
250
  timestamp: Date.now(),
191
251
  source: 'trigger',
252
+ ...(eventSession ? {
253
+ // Bind the exact session; never fall back to a newly activated main
254
+ // session or create a replacement for a deleted source thread.
255
+ boundSessionId: eventSession.id,
256
+ encrypted: input.sourcePayload?.encrypted,
257
+ } : {}),
192
258
  causation: input.causation,
193
259
  groupId: targetChatType === 'group' ? targetDelivery.groupId : undefined,
194
260
  replyContext: replyContextFromTarget(target),
@@ -202,30 +268,128 @@ export class TriggerFeedbackDispatcher {
202
268
  modelOverride: input.trigger.execution.model,
203
269
  effortOverride: input.trigger.execution.effort,
204
270
  permissionModeOverride: input.trigger.execution.permissionMode,
205
- originPeerId: input.trigger.origin?.peerId,
206
- originChannelKey: input.trigger.origin?.channelKey,
207
- originChannelType: input.trigger.origin?.channelType
271
+ originPeerId: eventSession ? input.sourcePayload.peerId : input.trigger.origin?.peerId,
272
+ originChannelKey: eventSession ? target.channelKey : input.trigger.origin?.channelKey,
273
+ originChannelType: eventSession ? channelType : input.trigger.origin?.channelType
208
274
  ?? (input.trigger.origin?.channelKey ? channelTypeFromKey(input.trigger.origin.channelKey) : undefined),
209
- originControl: input.trigger.origin?.channelKey === 'daemon',
275
+ ...(eventSession ? { originChatType: messageChatType, originConversationId: target.channelId } : {}),
276
+ originControl: !eventSession && input.trigger.origin?.channelKey === 'daemon',
210
277
  authorizedBy: input.trigger.authorizedBy,
211
278
  },
212
279
  };
213
280
  outcome = this.waitForTriggerOutcome(input.runId, input.attemptId, delivery.signal);
281
+ let missingSince;
282
+ let observedTaskId;
283
+ let orphanNotificationOwned = false;
284
+ const notifyOrphan = async () => {
285
+ if (orphanNotificationOwned || disposed || delivery.signal.aborted)
286
+ return;
287
+ orphanNotificationOwned = true;
288
+ const text = '⚠️ 任务执行状态丢失,已结束本次执行。';
289
+ const envelope = buildEnvelope({
290
+ taskId: observedTaskId ?? input.attemptId, sessionId: session.id,
291
+ channel: target.channelKey, channelId: target.channelId,
292
+ agentName: binding.agentName, chatmode: 'interactive', causation: input.causation,
293
+ replyContext: { ...replyContextFromTarget(target), sessionId: session.id,
294
+ metadata: { ...session.metadata?.replyContext?.metadata,
295
+ ...(message.encrypted !== undefined ? { encrypted: message.encrypted } : {}) } },
296
+ });
297
+ await binding.adapter.send(envelope, { kind: 'status.error',
298
+ metadata: { errorType: 'orphaned_target_session', message: text } });
299
+ await binding.adapter.send(envelope, { kind: 'result.error', reason: 'orphaned_target_session', text });
300
+ };
301
+ const inspect = async () => {
302
+ if (!this.deps.inspectTargetExecution || disposed || delivery.signal.aborted)
303
+ return undefined;
304
+ const observed = await targetSessionLookup(this.deps.inspectTargetExecution(session.id, input.attemptId, targetAgentAid));
305
+ if (disposed || delivery.signal.aborted)
306
+ return undefined;
307
+ observedTaskId = observed.taskId ?? observedTaskId;
308
+ if (observed.state === 'terminal') {
309
+ const base = { triggerId: input.trigger.id, name: input.trigger.name, runId: input.runId,
310
+ attemptId: input.attemptId, originTriggerId: input.trigger.id, messageId: input.attemptId,
311
+ targetChannel: target.channelKey, targetChannelId: target.channelId, fireTime: input.firedAt };
312
+ const event = observed.outcome === 'completed'
313
+ ? { ...base, type: 'trigger:completed', durationMs: Date.now() - startedAt }
314
+ : observed.outcome === 'failed'
315
+ ? { ...base, type: 'trigger:failed', error: observed.reason ?? 'target_session_failed', phase: 'execute' }
316
+ : { ...base, type: 'trigger:skipped', reason: observed.reason ?? 'stale_generation',
317
+ decisionSource: 'infrastructure', executionState: 'interrupted' };
318
+ outcome?.settle(event);
319
+ return event;
320
+ }
321
+ if (observed.state !== 'missing') {
322
+ missingSince = undefined;
323
+ return undefined;
324
+ }
325
+ missingSince ??= Date.now();
326
+ if (Date.now() - missingSince >= TARGET_SESSION_ORPHAN_GRACE_MS) {
327
+ if (!observed.hasTerminalOwner
328
+ || this.deps.messageQueue.getMessageState(session.id, input.attemptId, targetAgentAid).state !== 'active') {
329
+ await notifyOrphan();
330
+ }
331
+ abortDelivery('cancelled', new OrphanedTargetSessionError());
332
+ }
333
+ return undefined;
334
+ };
335
+ if (this.deps.inspectTargetExecution) {
336
+ probeTimer = setInterval(() => {
337
+ if (probing || disposed)
338
+ return;
339
+ probing = true;
340
+ void inspect().catch(() => { missingSince = undefined; }).finally(() => { probing = false; });
341
+ }, TARGET_SESSION_PROBE_MS);
342
+ probeTimer.unref?.();
343
+ }
214
344
  let enqueuePromise;
215
345
  let event;
216
346
  try {
217
- enqueuePromise = this.deps.messageQueue.enqueue(session.id, message, session.projectPath, {
218
- interruptible: false,
219
- agentName: binding.agentName,
220
- sessionKeyField: session.sessionKey,
221
- selfAID: input.trigger.agentAid,
222
- signal: delivery.signal,
223
- });
347
+ if (isTaskErrorRecovery) {
348
+ const recovery = await this.deps.recoverTask({
349
+ sessionId: session.id, agentAid: targetAgentAid, automatic: true,
350
+ expectedTaskId: input.sourcePayload.taskId,
351
+ expectedGeneration: input.sourcePayload.generation,
352
+ peerId: input.sourcePayload.peerId,
353
+ encrypted: input.sourcePayload.encrypted,
354
+ message, signal: delivery.signal,
355
+ });
356
+ if (recovery.status !== 'queued_recovery') {
357
+ if (delivery.signal.aborted)
358
+ throw delivery.signal.reason;
359
+ return { status: 'skipped', reason: `recovery_${recovery.status}`, feedback, effects: [], error: null };
360
+ }
361
+ if (!recovery.completion)
362
+ throw new Error('task recovery completion tracking is unavailable');
363
+ enqueuePromise = recovery.completion;
364
+ }
365
+ else {
366
+ enqueuePromise = this.deps.messageQueue.enqueue(session.id, message, session.projectPath, {
367
+ interruptible: false,
368
+ agentName: binding.agentName,
369
+ sessionKeyField: session.sessionKey,
370
+ selfAID: targetAgentAid,
371
+ signal: delivery.signal,
372
+ });
373
+ }
224
374
  if (outcome) {
225
375
  const enqueueFailure = new Promise((_, reject) => {
226
376
  void enqueuePromise.catch(reject);
227
377
  });
228
- event = await Promise.race([outcome.promise, enqueueFailure]);
378
+ const queueCompletion = enqueuePromise.then(async () => {
379
+ const terminal = await settleTriggerOutcome(outcome.promise, TARGET_SESSION_OUTCOME_GRACE_MS);
380
+ if (terminal)
381
+ return terminal;
382
+ if (disposed || delivery.signal.aborted)
383
+ return undefined;
384
+ const reconciled = await inspect();
385
+ if (reconciled)
386
+ return reconciled;
387
+ if (this.deps.inspectTargetExecution)
388
+ return outcome.promise;
389
+ await notifyOrphan();
390
+ throw new OrphanedTargetSessionError();
391
+ });
392
+ event = await Promise.race([outcome.promise, queueCompletion, enqueueFailure]);
229
393
  }
230
394
  else {
231
395
  await raceWithAbort(enqueuePromise, delivery.signal);
@@ -261,12 +425,12 @@ export class TriggerFeedbackDispatcher {
261
425
  }
262
426
  : undefined;
263
427
  return {
264
- status: interruption ? 'skipped' : 'failed',
428
+ status: interruption || event.reason === 'recovery_stale' ? 'skipped' : 'failed',
265
429
  reason: event.reason,
266
430
  ...(interruption ? { interruption } : {}),
267
431
  feedback,
268
432
  effects: [this.inboundEffect('success', target, session.id, startedAt)],
269
- error: interruption
433
+ error: interruption || event.reason === 'recovery_stale'
270
434
  ? null
271
435
  : { code: event.reason || 'target_session_skipped', message: event.reason || 'target session skipped' },
272
436
  };
@@ -294,14 +458,17 @@ export class TriggerFeedbackDispatcher {
294
458
  }
295
459
  return {
296
460
  status: 'failed',
297
- reason: 'enqueue_error',
461
+ reason: err instanceof OrphanedTargetSessionError ? err.reason : 'enqueue_error',
298
462
  feedback,
299
463
  effects: [this.inboundEffect('failed', target, session.id, startedAt, undefined, err?.message || String(err))],
300
- error: { code: 'enqueue_error', message: err?.message || String(err) },
464
+ error: { code: err instanceof OrphanedTargetSessionError ? err.reason : 'enqueue_error', message: err?.message || String(err) },
301
465
  };
302
466
  }
303
467
  }
304
468
  finally {
469
+ disposed = true;
470
+ if (probeTimer)
471
+ clearInterval(probeTimer);
305
472
  releaseSessionLock?.();
306
473
  clearTimeout(deadlineTimer);
307
474
  input.signal?.removeEventListener('abort', onInputAbort);
@@ -334,7 +501,7 @@ export class TriggerFeedbackDispatcher {
334
501
  };
335
502
  }
336
503
  resolveDeliveryTarget(trigger) {
337
- if (trigger.feedback.strategy === 'silent')
504
+ if (trigger.feedback.strategy === 'silent' || trigger.feedback.strategy === 'event_session')
338
505
  return undefined;
339
506
  if (trigger.feedback.strategy === 'target')
340
507
  return trigger.feedback.target;
@@ -526,6 +693,7 @@ export class TriggerFeedbackDispatcher {
526
693
  if (!this.deps.eventBus)
527
694
  return undefined;
528
695
  let cancel = () => { };
696
+ let settle = (_event) => { };
529
697
  const promise = new Promise(resolve => {
530
698
  let settled = false;
531
699
  const finish = (event) => {
@@ -570,8 +738,9 @@ export class TriggerFeedbackDispatcher {
570
738
  if (signal?.aborted)
571
739
  onAbort();
572
740
  cancel = () => finish();
741
+ settle = event => finish(event);
573
742
  });
574
- return { promise, cancel: () => cancel() };
743
+ return { promise, cancel: () => cancel(), settle: event => settle(event) };
575
744
  }
576
745
  failed(feedback, effects, code, message) {
577
746
  return {
@@ -751,9 +920,10 @@ function feedbackCancellation(parent, timeoutMs) {
751
920
  };
752
921
  }
753
922
  function targetSessionCancellationError(kind, reason, barrierTimedOut) {
754
- const code = kind === 'timeout' ? 'target_session_timeout' : 'cancelled';
923
+ const code = reason instanceof OrphanedTargetSessionError ? reason.reason
924
+ : kind === 'timeout' ? 'target_session_timeout' : 'cancelled';
755
925
  const base = kind === 'timeout'
756
- ? `target session timed out after ${TARGET_SESSION_DEADLINE_MS}ms`
926
+ ? `target session timed out after ${reason instanceof ExecutionTimeoutError ? reason.timeoutMs : 'unknown'}ms`
757
927
  : reason instanceof Error
758
928
  ? reason.message
759
929
  : 'target session delivery cancelled';
@@ -796,6 +966,21 @@ function settleWithin(promise, timeoutMs) {
796
966
  promise.then(() => finish(true), () => finish(true));
797
967
  });
798
968
  }
969
+ function settleTriggerOutcome(promise, timeoutMs) {
970
+ return new Promise(resolve => {
971
+ let settled = false;
972
+ const finish = (value) => {
973
+ if (settled)
974
+ return;
975
+ settled = true;
976
+ clearTimeout(timer);
977
+ resolve(value);
978
+ };
979
+ const timer = setTimeout(() => finish(undefined), timeoutMs);
980
+ timer.unref?.();
981
+ promise.then(finish, () => finish(undefined));
982
+ });
983
+ }
799
984
  function sleep(ms, signal) {
800
985
  if (signal?.aborted)
801
986
  return Promise.resolve(false);
@@ -815,3 +1000,11 @@ function sleep(ms, signal) {
815
1000
  signal?.addEventListener('abort', onAbort, { once: true });
816
1001
  });
817
1002
  }
1003
+ /** Bound local session lookup/setup without limiting an admitted task's execution. */
1004
+ function targetSessionLookup(promise) {
1005
+ return new Promise((resolve, reject) => {
1006
+ const timer = setTimeout(() => reject(new Error('target_session_setup_timeout')), 30_000);
1007
+ timer.unref?.();
1008
+ promise.then(resolve, reject).finally(() => clearTimeout(timer));
1009
+ });
1010
+ }
@@ -1,8 +1,8 @@
1
1
  import { CronExpressionParser } from 'cron-parser';
2
2
  const TRIGGER_UPDATE_FLAGS = new Set([
3
- 'once', 'delay', 'at', 'cron', 'every', 'event', 'tz',
3
+ 'once', 'delay', 'at', 'cron', 'every', 'event', 'event-filter', 'tz',
4
4
  'prompt', 'name', 'description',
5
- 'script-file', 'script-runtime', 'script-args', 'script-timeout',
5
+ 'script-file', 'script-runtime', 'script-args', 'script-timeout', 'execution-timeout',
6
6
  'feedback', 'target-channel', 'target-channel-id', 'target-chat-type', 'target-session', 'target-thread-id',
7
7
  'trigger-thread', 'baseagent',
8
8
  'on-error', 'noop-sentinel', 'retry-attempts', 'retry-backoff',
@@ -10,9 +10,9 @@ const TRIGGER_UPDATE_FLAGS = new Set([
10
10
  'max-runs', 'max-duration', 'concurrency', 'missed-policy',
11
11
  ]);
12
12
  const TRIGGER_CREATE_FLAGS = new Set([
13
- 'once', 'delay', 'at', 'cron', 'every', 'event', 'tz',
13
+ 'once', 'delay', 'at', 'cron', 'every', 'event', 'event-filter', 'tz',
14
14
  'exec', 'prompt', 'name', 'agent',
15
- 'script-path', 'script-runtime', 'script-args', 'script-timeout',
15
+ 'script-path', 'script-runtime', 'script-args', 'script-timeout', 'execution-timeout',
16
16
  'feedback', 'target-channel', 'target-channel-id', 'target-chat-type', 'target-session', 'target-thread-id',
17
17
  'trigger-thread', 'baseagent',
18
18
  'model', 'effort', 'permission', 'max-runs', 'max-duration',
@@ -205,7 +205,9 @@ function parseFeedbackStrategy(flags) {
205
205
  return { ok: false, error: '--feedback 不能为空' };
206
206
  if (raw === 'origin' || raw === 'target' || raw === 'silent')
207
207
  return { ok: true, value: raw };
208
- return { ok: false, error: '--feedback 只接受 origin、target 或 silent' };
208
+ if (raw === 'event-session' || raw === 'event_session')
209
+ return { ok: true, value: 'event_session' };
210
+ return { ok: false, error: '--feedback 只接受 origin、target、event-session 或 silent' };
209
211
  }
210
212
  function parseTriggerThread(flags) {
211
213
  if (!flags.has('trigger-thread'))
@@ -298,6 +300,24 @@ function parseSourceFlags(flags, opts = {}) {
298
300
  return { ok: false, error: '--event 不能为空' };
299
301
  return { ok: true, scheduleType: 'event', scheduleValue: raw };
300
302
  }
303
+ function parseEventFilterFlag(flags) {
304
+ if (!flags.has('event-filter'))
305
+ return { ok: true };
306
+ const raw = flags.get('event-filter');
307
+ if (!raw || raw === true)
308
+ return { ok: false, error: '--event-filter 不能为空' };
309
+ let value;
310
+ try {
311
+ value = JSON.parse(raw);
312
+ }
313
+ catch {
314
+ return { ok: false, error: '--event-filter 必须是合法的 JSON 对象' };
315
+ }
316
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
317
+ return { ok: false, error: '--event-filter 必须是合法的 JSON 对象' };
318
+ }
319
+ return { ok: true, value: value };
320
+ }
301
321
  function commonParsed(flags, opts = {}) {
302
322
  const deprecated = rejectDeprecatedFlags(flags);
303
323
  if (deprecated)
@@ -311,6 +331,12 @@ function commonParsed(flags, opts = {}) {
311
331
  const source = parseSourceFlags(flags, opts);
312
332
  if (!source.ok)
313
333
  return source;
334
+ const eventFilter = parseEventFilterFlag(flags);
335
+ if (!eventFilter.ok)
336
+ return eventFilter;
337
+ if (flags.has('event-filter') && source.scheduleType !== 'event' && (!opts.update || source.scheduleType !== undefined)) {
338
+ return { ok: false, error: '--event-filter 只能和 --event 一起使用' };
339
+ }
314
340
  if (flags.has('tz') && source.scheduleType !== 'cron' && (!opts.update || source.scheduleType !== undefined)) {
315
341
  return { ok: false, error: '--tz 只能和 --cron 一起使用' };
316
342
  }
@@ -329,6 +355,11 @@ function commonParsed(flags, opts = {}) {
329
355
  const scriptTimeout = parseDurationFlag(flags, 'script-timeout', '--script-timeout');
330
356
  if (!scriptTimeout.ok)
331
357
  return scriptTimeout;
358
+ const executionTimeout = opts.update && flags.get('execution-timeout') === 'default'
359
+ ? { ok: true, value: undefined }
360
+ : parseDurationFlag(flags, 'execution-timeout', '--execution-timeout');
361
+ if (!executionTimeout.ok)
362
+ return executionTimeout;
332
363
  const maxRuns = parsePositiveIntegerFlag(flags, 'max-runs', '--max-runs');
333
364
  if (!maxRuns.ok)
334
365
  return maxRuns;
@@ -418,10 +449,18 @@ function commonParsed(flags, opts = {}) {
418
449
  }
419
450
  const targetSessionValue = targetSession.value ?? (targetThreadId ? 'thread' : 'main');
420
451
  const feedbackStrategy = feedback.value ?? (hasTargetField ? 'target' : 'origin');
452
+ if (feedbackStrategy === 'event_session') {
453
+ if (hasTargetField)
454
+ return { ok: false, error: '--feedback event-session 不能指定固定 target 字段' };
455
+ if (!opts.update && (source.scheduleType !== 'event' || inferredExecution !== 'target_session')) {
456
+ return { ok: false, error: '--feedback event-session 需要 --event 和 --exec target-session' };
457
+ }
458
+ }
421
459
  return {
422
460
  ok: true,
423
461
  value: {
424
462
  ...source,
463
+ eventFilter: eventFilter.value,
425
464
  executionType: execution.value,
426
465
  inferredExecution,
427
466
  feedbackStrategy,
@@ -437,6 +476,7 @@ function commonParsed(flags, opts = {}) {
437
476
  scriptRuntime: scriptRuntime,
438
477
  scriptArgs,
439
478
  scriptTimeoutMs: scriptTimeout.value,
479
+ executionTimeoutMs: executionTimeout.value,
440
480
  triggerThread: triggerThread.value,
441
481
  baseagent: baseagent.value,
442
482
  maxRuns: maxRuns.value,
@@ -462,6 +502,7 @@ export function parseTriggerSet(args) {
462
502
  scheduleType: value.scheduleType,
463
503
  scheduleValue: value.scheduleValue ?? '',
464
504
  timezone: value.timezone,
505
+ eventFilter: value.eventFilter,
465
506
  executionType: value.inferredExecution,
466
507
  feedbackStrategy: value.feedbackStrategy,
467
508
  targetChannel: value.targetChannel,
@@ -476,6 +517,7 @@ export function parseTriggerSet(args) {
476
517
  scriptRuntime: value.scriptRuntime,
477
518
  scriptArgs: value.scriptArgs,
478
519
  scriptTimeoutMs: value.scriptTimeoutMs,
520
+ executionTimeoutMs: value.executionTimeoutMs,
479
521
  triggerThread: value.triggerThread,
480
522
  baseagent: value.baseagent,
481
523
  maxRuns: value.maxRuns,
@@ -502,6 +544,7 @@ export function parseTriggerSetArgv(args, opts = {}) {
502
544
  scheduleType: value.scheduleType,
503
545
  scheduleValue: value.scheduleValue ?? '',
504
546
  timezone: value.timezone,
547
+ eventFilter: value.eventFilter,
505
548
  executionType: value.inferredExecution,
506
549
  feedbackStrategy: value.feedbackStrategy,
507
550
  targetChannel: value.targetChannel,
@@ -516,6 +559,7 @@ export function parseTriggerSetArgv(args, opts = {}) {
516
559
  scriptRuntime: value.scriptRuntime,
517
560
  scriptArgs: value.scriptArgs,
518
561
  scriptTimeoutMs: value.scriptTimeoutMs,
562
+ executionTimeoutMs: value.executionTimeoutMs,
519
563
  triggerThread: value.triggerThread,
520
564
  baseagent: value.baseagent,
521
565
  maxRuns: value.maxRuns,
@@ -625,6 +669,10 @@ function parseTriggerUpdateFlags(nameOrId, flags, opts = {}) {
625
669
  update.scriptFile = scriptFile.value;
626
670
  if (value.model !== undefined)
627
671
  update.model = value.model;
672
+ if (flags.get('execution-timeout') === 'default')
673
+ update.executionTimeoutMs = null;
674
+ else if (value.executionTimeoutMs !== undefined)
675
+ update.executionTimeoutMs = value.executionTimeoutMs;
628
676
  if (value.effort !== undefined)
629
677
  update.effort = value.effort;
630
678
  if (value.permissionMode !== undefined)
@@ -659,6 +707,8 @@ function parseTriggerUpdateFlags(nameOrId, flags, opts = {}) {
659
707
  update.description = description.value || null;
660
708
  if (flags.has('feedback') && value.feedbackStrategy !== undefined)
661
709
  update.feedback = value.feedbackStrategy;
710
+ if (flags.has('event-filter'))
711
+ update.eventFilter = value.eventFilter;
662
712
  if (value.targetChannel !== undefined)
663
713
  update.targetChannel = value.targetChannel;
664
714
  if (value.targetChannelId !== undefined)
@@ -5,8 +5,10 @@ const PATCH_KEYS = new Set([
5
5
  'scheduleType',
6
6
  'scheduleValue',
7
7
  'timezone',
8
+ 'eventFilter',
8
9
  'prompt',
9
10
  'model',
11
+ 'executionTimeoutMs',
10
12
  'effort',
11
13
  'permissionMode',
12
14
  'maxRuns',
@@ -56,6 +58,8 @@ export function applyTriggerPatch(definition, input, opts = {}) {
56
58
  }
57
59
  if (patch.model !== undefined)
58
60
  setOptional(updated.execution, 'model', patch.model);
61
+ if (patch.executionTimeoutMs !== undefined)
62
+ setOptional(updated.execution, 'timeoutMs', patch.executionTimeoutMs);
59
63
  if (patch.effort !== undefined)
60
64
  setOptional(updated.execution, 'effort', patch.effort);
61
65
  if (patch.permissionMode !== undefined)
@@ -226,6 +230,11 @@ function applySourcePatch(updated, patch) {
226
230
  else
227
231
  updated.source.timezone = String(patch.timezone);
228
232
  }
233
+ if (patch.eventFilter !== undefined) {
234
+ if (updated.source.type !== 'event')
235
+ throw new Error('eventFilter is only valid for an event trigger');
236
+ updated.source.filter = patch.eventFilter;
237
+ }
229
238
  }
230
239
  function sourceFromSchedule(type, value) {
231
240
  switch (type) {