evolcore 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/CHANGELOG.md +93 -778
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +198 -48
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +203 -8
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/aun/outbox.js +8 -0
  14. package/dist/channels/aun.js +241 -116
  15. package/dist/channels/daemon.js +18 -4
  16. package/dist/channels/dingtalk.js +1005 -260
  17. package/dist/channels/feishu.js +184 -56
  18. package/dist/channels/qqbot.js +23 -1
  19. package/dist/channels/wechat.js +303 -155
  20. package/dist/channels/wecom.js +1132 -164
  21. package/dist/cli/agent-command.js +2 -1
  22. package/dist/cli/aun-commands.js +98 -43
  23. package/dist/cli/bench.js +2 -2
  24. package/dist/cli/contact.js +71 -0
  25. package/dist/cli/ctl-command.js +2 -2
  26. package/dist/cli/daemon-commands.js +77 -214
  27. package/dist/cli/handoff-command.js +4 -3
  28. package/dist/cli/help.js +9 -5
  29. package/dist/cli/index.js +132 -116
  30. package/dist/cli/init-channel.js +242 -129
  31. package/dist/cli/init.js +70 -31
  32. package/dist/cli/model.js +2 -1
  33. package/dist/cli/net-check.js +2 -2
  34. package/dist/cli/queue-command.js +30 -6
  35. package/dist/cli/raw-key-input.js +25 -0
  36. package/dist/cli/response.js +5 -6
  37. package/dist/cli/restart-monitor.js +25 -1
  38. package/dist/cli/stats.js +6 -4
  39. package/dist/cli/trigger-command.js +146 -25
  40. package/dist/cli/version.js +6 -1
  41. package/dist/config/access-policy-domain.js +38 -0
  42. package/dist/config/access-policy.js +134 -0
  43. package/dist/config/builtin-role-templates.js +42 -17
  44. package/dist/config/builtin-roles.js +31 -6
  45. package/dist/config/config-field-policy.js +17 -5
  46. package/dist/config/config-manager.js +409 -29
  47. package/dist/config/config-operation-service.js +35 -38
  48. package/dist/config/contact-alias.js +68 -0
  49. package/dist/config/contact-bind-code.js +274 -0
  50. package/dist/config/contact-book-store.js +622 -0
  51. package/dist/config/contact-book-v2-startup.js +35 -0
  52. package/dist/config/contact-book.js +185 -300
  53. package/dist/config/contact-operation-service.js +116 -0
  54. package/dist/config/contact-request-service.js +331 -0
  55. package/dist/config/peer-role-resolver.js +136 -55
  56. package/dist/config/role-ranks.js +18 -0
  57. package/dist/config/role-service.js +16 -19
  58. package/dist/config/role-store.js +16 -5
  59. package/dist/config/roles.js +10 -1
  60. package/dist/config/schema-registry.js +49 -24
  61. package/dist/config-store.js +14 -4
  62. package/dist/core/auth/agent-delegation.js +105 -11
  63. package/dist/core/auth/authorization-audit.js +16 -1
  64. package/dist/core/auth/operation-authorizer.js +10 -0
  65. package/dist/core/auth/operation-catalog.js +105 -1
  66. package/dist/core/auth/trigger-authorization.js +15 -0
  67. package/dist/core/bootstrap-service.js +2 -9
  68. package/dist/core/channel-loader.js +6 -2
  69. package/dist/core/command/command-handler.js +113 -21
  70. package/dist/core/command/connect-menu.js +599 -0
  71. package/dist/core/command/evol-menu-version-gate.js +38 -0
  72. package/dist/core/command/group-menu.js +421 -0
  73. package/dist/core/command/menu-handler.js +382 -100
  74. package/dist/core/command/menu-protocol.js +8 -2
  75. package/dist/core/command/menu-token-store.js +102 -0
  76. package/dist/core/command/role-menu.js +128 -29
  77. package/dist/core/command/slash-gate.js +1 -1
  78. package/dist/core/command/slash-handler.js +186 -49
  79. package/dist/core/daemon-file-cache.js +40 -6
  80. package/dist/core/event-catalog.js +99 -0
  81. package/dist/core/evolagent-registry.js +4 -40
  82. package/dist/core/evolagent.js +20 -9
  83. package/dist/core/handoff/runtime.js +162 -37
  84. package/dist/core/handoff/store.js +46 -2
  85. package/dist/core/handoff/types.js +1 -0
  86. package/dist/core/inference/text-inference.js +16 -74
  87. package/dist/core/message/file-markers.js +7 -0
  88. package/dist/core/message/im-renderer.js +20 -14
  89. package/dist/core/message/inbound-admission.js +124 -0
  90. package/dist/core/message/message-bridge.js +670 -57
  91. package/dist/core/message/message-log.js +1 -0
  92. package/dist/core/message/message-queue.js +195 -10
  93. package/dist/core/message/message-utils.js +8 -2
  94. package/dist/core/message/peer-mode.js +7 -8
  95. package/dist/core/message/response-engine.js +642 -96
  96. package/dist/core/message/send-receipt.js +24 -0
  97. package/dist/core/message/stream-debouncer.js +11 -2
  98. package/dist/core/permission/approval-gateway.js +17 -10
  99. package/dist/core/permission/ec-command-parser.js +101 -46
  100. package/dist/core/permission/tool-policy.js +115 -38
  101. package/dist/core/protected-paths.js +38 -11
  102. package/dist/core/session/session-fs-store.js +44 -3
  103. package/dist/core/session/session-manager.js +86 -8
  104. package/dist/core/session/session-mapper.js +2 -0
  105. package/dist/core/session/session-renew.js +125 -69
  106. package/dist/core/session/session-turn-coordinator.js +5 -1
  107. package/dist/eck/kit-renderer.js +12 -1
  108. package/dist/eck/message-renderer.js +79 -1
  109. package/dist/index.js +285 -94
  110. package/dist/ipc.js +114 -8
  111. package/dist/paths.js +3 -0
  112. package/dist/response-system/config-resolver.js +29 -0
  113. package/dist/response-system/coordinator.js +8 -32
  114. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  115. package/dist/response-system/index.js +1 -0
  116. package/dist/response-system/modes/single-session/index.js +7 -4
  117. package/dist/stats/billing.js +20 -8
  118. package/dist/trigger/manager.js +3 -0
  119. package/dist/trigger/parser.js +126 -11
  120. package/dist/trigger/patch.js +12 -2
  121. package/dist/trigger/scheduler.js +444 -40
  122. package/dist/trigger/validation.js +13 -0
  123. package/dist/utils/aid-bind.js +43 -29
  124. package/dist/utils/error-dict.json +7 -0
  125. package/dist/utils/evolcore-version.js +21 -0
  126. package/dist/utils/instance-registry.js +14 -7
  127. package/dist/utils/log-writer.js +46 -0
  128. package/dist/utils/logger.js +2 -2
  129. package/dist/utils/media-cache.js +4 -1
  130. package/dist/utils/model-prices.jsonl +6 -3
  131. package/dist/utils/restart-safety.js +31 -0
  132. package/dist/utils/stable-semver.js +21 -0
  133. package/dist/utils/stats.js +33 -9
  134. package/dist/utils/system-memory.js +62 -0
  135. package/kits/docs/INDEX.md +2 -1
  136. package/kits/docs/channels/aun.md +4 -13
  137. package/kits/docs/evolcore/INDEX.md +5 -3
  138. package/kits/docs/evolcore/agent.md +9 -1
  139. package/kits/docs/evolcore/aid.md +5 -2
  140. package/kits/docs/evolcore/config.md +47 -2
  141. package/kits/docs/evolcore/contact.md +61 -0
  142. package/kits/docs/evolcore/fs.md +9 -0
  143. package/kits/docs/evolcore/group-rules.md +46 -4
  144. package/kits/docs/evolcore/group.md +15 -6
  145. package/kits/docs/evolcore/model.md +4 -1
  146. package/kits/docs/evolcore/msg.md +12 -6
  147. package/kits/docs/evolcore/response.md +16 -21
  148. package/kits/docs/evolcore/rpc.md +2 -0
  149. package/kits/docs/evolcore/stats.md +15 -2
  150. package/kits/docs/evolcore/storage.md +1 -0
  151. package/kits/docs/evolcore/trigger.md +42 -10
  152. package/kits/docs/path-registry.md +36 -17
  153. package/kits/eck_manifest.json +12 -0
  154. package/kits/eck_message_manifest.json +12 -1
  155. package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
  156. package/kits/rules/01-overview.md +17 -7
  157. package/kits/rules/02-navigation.md +38 -18
  158. package/kits/rules/03-identity.md +28 -24
  159. package/kits/rules/04-relation.md +44 -28
  160. package/kits/rules/05-venue.md +31 -15
  161. package/kits/rules/06-channel.md +11 -7
  162. package/kits/schemas/_meta.json +21 -7
  163. package/kits/schemas/agent-config.schema.6.json +322 -0
  164. package/kits/schemas/agent-config.schema.7.json +303 -0
  165. package/kits/schemas/agent-config.schema.8.json +304 -0
  166. package/kits/schemas/agent-config.schema.9.json +374 -0
  167. package/kits/schemas/contact-book.schema.2.json +43 -0
  168. package/kits/schemas/contact-book.schema.3.json +67 -0
  169. package/kits/schemas/daemon.schema.1.json +3 -2
  170. package/kits/schemas/daemon.schema.2.json +101 -0
  171. package/kits/schemas/daemon.schema.3.json +123 -0
  172. package/kits/schemas/defaults.schema.2.json +85 -0
  173. package/kits/schemas/defaults.schema.3.json +73 -0
  174. package/kits/schemas/relation-config.schema.5.json +47 -0
  175. package/kits/schemas/relation-config.schema.6.json +46 -0
  176. package/kits/schemas/relation-config.schema.7.json +59 -0
  177. package/kits/schemas/role-config.schema.1.json +1 -0
  178. package/kits/schemas/role-registry.schema.1.json +2 -2
  179. package/kits/schemas/single-session.schema.2.json +57 -0
  180. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  181. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  182. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  183. package/kits/templates/roles/admin.json +47 -0
  184. package/kits/templates/roles/member.json +5 -0
  185. package/kits/templates/roles/owner.json +1 -0
  186. package/kits/templates/roles/visitor.json +5 -0
  187. package/kits/templates/system-fragments/channel.md +12 -2
  188. package/kits/templates/system-fragments/commands.md +3 -1
  189. package/kits/templates/system-fragments/identity.md +3 -1
  190. package/kits/templates/system-fragments/relation.md +1 -1
  191. package/kits/templates/system-fragments/session.md +6 -2
  192. package/package.json +7 -5
  193. package/MIGRATION-0.5.0.md +0 -378
  194. package/ROLE_ACCESS_CONTROL.md +0 -174
  195. package/assets/brand/evolcore/README.md +0 -19
  196. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  197. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  198. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  199. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  200. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  201. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  202. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  203. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  204. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  205. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  206. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -11,6 +11,7 @@ import { accumulateTriggerExecutionAnomalies, drainTriggerExecutionAnomalies, me
11
11
  const MAX_TIMER_MS = 2_147_483_647;
12
12
  const RUN_ABORT_SETTLE_GRACE_MS = 1_000;
13
13
  const RUN_COMPLETION_BARRIER_MS = 1_500;
14
+ const TRIGGER_RESTART_RESUME_PROMPT = 'EvolCore 服务已重启。请基于当前会话记录继续完成刚才被中断的 Trigger 任务;不要重新从头执行已经完成的步骤。';
14
15
  export class TriggerRuntimeScheduler {
15
16
  manager;
16
17
  state;
@@ -23,6 +24,7 @@ export class TriggerRuntimeScheduler {
23
24
  timers = new Map();
24
25
  running = new Map();
25
26
  runStartLocks = new Map();
27
+ recoveredOneShotIds = new Set();
26
28
  eventSource;
27
29
  initialized = false;
28
30
  stopping = false;
@@ -44,10 +46,14 @@ export class TriggerRuntimeScheduler {
44
46
  async init() {
45
47
  await this.stop();
46
48
  this.stopping = false;
49
+ this.recoveredOneShotIds.clear();
47
50
  await this.recoverOpenRuns();
48
51
  for (const definition of this.manager.list()) {
52
+ if (this.recoveredOneShotIds.has(definition.id))
53
+ continue;
49
54
  this.schedule(definition);
50
55
  }
56
+ this.recoveredOneShotIds.clear();
51
57
  this.initialized = true;
52
58
  logger.info(`[Trigger] scheduler initialized for ${this.manager.agentAid}`);
53
59
  }
@@ -70,6 +76,35 @@ export class TriggerRuntimeScheduler {
70
76
  this.initialized = false;
71
77
  await Promise.all(settlements);
72
78
  }
79
+ /**
80
+ * Stop process-local work while keeping durable runs open for the next daemon.
81
+ * Explicit scheduler replacement/unload must continue to use stop().
82
+ */
83
+ async suspendForDaemonExit() {
84
+ this.stopping = true;
85
+ for (const timer of this.timers.values())
86
+ clearTimeout(timer);
87
+ this.timers.clear();
88
+ this.eventSource?.stop();
89
+ const settlements = [];
90
+ for (const runs of Array.from(this.running.values())) {
91
+ for (const [runId, runtime] of Array.from(runs.entries())) {
92
+ const durableRun = this.state.list(runtime.definition.id).find(candidate => candidate.runId === runId);
93
+ if (durableRun)
94
+ runtime.run = durableRun;
95
+ const resumable = !!durableRun && this.isResumableCheckpoint(runtime.definition, durableRun);
96
+ settlements.push(this.terminateRun(runId, runtime, {
97
+ reason: resumable ? 'suspended' : 'stopped',
98
+ message: resumable
99
+ ? 'trigger scheduler stopped for daemon restart'
100
+ : 'trigger scheduler stopped',
101
+ }));
102
+ }
103
+ }
104
+ settlements.push(...this.runStartLocks.values());
105
+ this.initialized = false;
106
+ await Promise.all(settlements);
107
+ }
73
108
  async waitForIdle(timeoutMs = 5_000) {
74
109
  const deadline = Date.now() + timeoutMs;
75
110
  while ((this.running.size > 0 || this.runStartLocks.size > 0) && Date.now() < deadline) {
@@ -341,7 +376,9 @@ export class TriggerRuntimeScheduler {
341
376
  await runPromise;
342
377
  return;
343
378
  }
344
- await runPromise;
379
+ const result = await runPromise;
380
+ if (result.status === 'running' && result.reason === 'daemon_restart_resume_pending')
381
+ return;
345
382
  const fresh = this.manager.get(triggerId);
346
383
  if (fresh?.enabled && definitionRevision(fresh) === firedRevision) {
347
384
  this.setEnabled(triggerId, false);
@@ -415,6 +452,9 @@ export class TriggerRuntimeScheduler {
415
452
  triggerId: definition.id,
416
453
  runId,
417
454
  startedAt,
455
+ definitionRevision: definitionRevision(definition),
456
+ source,
457
+ resumeCount: 0,
418
458
  events: [{ seq: 0, event: 'run.started', ts: startedAt }],
419
459
  };
420
460
  if (!payload.dryRun) {
@@ -476,9 +516,12 @@ export class TriggerRuntimeScheduler {
476
516
  if (runningRuntime)
477
517
  runningRuntime.anomalies = executionAnomalies;
478
518
  if (!payload.dryRun && controller.signal.aborted) {
479
- const reason = triggerCancellationReason(controller.signal);
480
519
  if (this.isRunning(definition.id, runId))
481
- this.finishRun(definition.id, runId);
520
+ this.finishAbortedRun(runningRuntime, definition.id, runId);
521
+ const suspended = this.suspendedRuntimeResult(runningRuntime, definition.id, runId);
522
+ if (suspended)
523
+ return suspended;
524
+ const reason = triggerCancellationReason(controller.signal);
482
525
  return {
483
526
  ok: false,
484
527
  runId,
@@ -524,9 +567,12 @@ export class TriggerRuntimeScheduler {
524
567
  signal: controller.signal,
525
568
  }), controller.signal, RUN_ABORT_SETTLE_GRACE_MS, 'feedback');
526
569
  if (!payload.dryRun && controller.signal.aborted) {
527
- const reason = triggerCancellationReason(controller.signal);
528
570
  if (this.isRunning(definition.id, runId))
529
- this.finishRun(definition.id, runId);
571
+ this.finishAbortedRun(runningRuntime, definition.id, runId);
572
+ const suspended = this.suspendedRuntimeResult(runningRuntime, definition.id, runId);
573
+ if (suspended)
574
+ return suspended;
575
+ const reason = triggerCancellationReason(controller.signal);
530
576
  return {
531
577
  ok: false,
532
578
  runId,
@@ -581,9 +627,12 @@ export class TriggerRuntimeScheduler {
581
627
  executionAnomalies = mergeTriggerExecutionAnomalies(executionAnomalies, executionAnomaliesFromError(err));
582
628
  const message = err?.message || String(err);
583
629
  if (!payload.dryRun && controller.signal.aborted) {
584
- const reason = triggerCancellationReason(controller.signal);
585
630
  if (this.isRunning(definition.id, runId))
586
- this.finishRun(definition.id, runId);
631
+ this.finishAbortedRun(runningRuntime, definition.id, runId);
632
+ const suspended = this.suspendedRuntimeResult(runningRuntime, definition.id, runId);
633
+ if (suspended)
634
+ return suspended;
635
+ const reason = triggerCancellationReason(controller.signal);
587
636
  return {
588
637
  ok: false,
589
638
  runId,
@@ -729,7 +778,9 @@ export class TriggerRuntimeScheduler {
729
778
  return { branch: branchFromReply(reply), script: null, processing, reply };
730
779
  }
731
780
  try {
732
- const runtimeBaseagent = this.runtime.getBaseagent?.() || this.runtime.baseagent;
781
+ const runtimeBaseagent = definition.execution.baseagent
782
+ ?? this.runtime.getBaseagent?.()
783
+ ?? this.runtime.baseagent;
733
784
  const originPath = causation.trigger?.path ?? [];
734
785
  logger.info(`[Trigger] execution context triggerId=${definition.id}`
735
786
  + ` triggerRunId=${runId}`
@@ -738,17 +789,33 @@ export class TriggerRuntimeScheduler {
738
789
  + ` baseagent=${runtimeBaseagent}`
739
790
  + ` model=${definition.execution.model ?? '<agent-default>'}`
740
791
  + ` effort=${definition.execution.effort ?? '<agent-default>'}`);
741
- const executionSession = await this.daemonChannel.getOrCreateConversationSession(definition, this.runtime.projectPath, runtimeBaseagent, runId, `${runId}:attempt:${attempt}`);
792
+ const attemptId = `${runId}:attempt:${attempt}`;
793
+ const executionDeadlineAt = this.daemonExecutionDeadlineAt(Date.now());
794
+ const executionSession = await this.daemonChannel.getOrCreateConversationSession(definition, this.runtime.projectPath, runtimeBaseagent, runId, attemptId);
795
+ const activeRun = this.state.setPhase?.(definition.id, runId, 'running', {
796
+ attempt,
797
+ attemptId,
798
+ executionSessionId: executionSession.id,
799
+ executionDeadlineAt,
800
+ });
801
+ this.state.appendEvent?.(definition.id, runId, {
802
+ event: 'execution.attempt.started',
803
+ ts: Date.now(),
804
+ attempt,
805
+ attemptId,
806
+ executionSessionId: executionSession.id,
807
+ });
742
808
  const daemonReply = await this.daemonChannel.converse(prompt, {
743
809
  trigger: definition,
744
810
  runId,
745
- attemptId: `${runId}:attempt:${attempt}`,
811
+ attemptId,
746
812
  firedAt,
747
813
  projectPath: this.runtime.projectPath,
748
814
  baseagent: runtimeBaseagent,
749
815
  session: executionSession,
750
816
  causation,
751
817
  signal,
818
+ executionDeadlineAt: activeRun?.executionDeadlineAt ?? executionDeadlineAt,
752
819
  onStateChange: (state, metadata) => {
753
820
  if (state === 'requires_action') {
754
821
  this.state.setPhase(definition.id, runId, 'requires-action');
@@ -832,41 +899,338 @@ export class TriggerRuntimeScheduler {
832
899
  if (activeRuns.length === 0)
833
900
  continue;
834
901
  for (const run of activeRuns) {
835
- const status = run.phase === 'feedback-pending' ? 'skipped' : 'failed';
836
- const audit = this.buildAudit({
837
- definition,
838
- runId: run.runId,
839
- startedAt: run.startedAt,
840
- status,
841
- reason: 'daemon_restart',
842
- source: this.buildSourceInfo(definition.source, run.startedAt, run.startedAt, this.sourcePayload(definition.source)),
843
- script: null,
844
- reply: null,
845
- feedback: null,
846
- effects: [],
847
- error: status === 'failed' ? { code: 'daemon_restart', message: 'daemon restarted while run was open' } : null,
902
+ if (await this.reclaimOpenRun(definition, run))
903
+ continue;
904
+ await this.closeOpenRunAfterRestart(definition, run);
905
+ }
906
+ }
907
+ }
908
+ async reclaimOpenRun(definition, run) {
909
+ if (!definition.enabled || !this.isResumableCheckpoint(definition, run))
910
+ return false;
911
+ const executionAuthorization = this.runtime.authorizeExecution?.(definition);
912
+ if (executionAuthorization && !executionAuthorization.allowed)
913
+ return false;
914
+ const session = await this.daemonChannel.getResumableConversationSession(run.executionSessionId);
915
+ if (!session) {
916
+ logger.warn(`[Trigger] resumable session unavailable: trigger=${definition.id} run=${run.runId} session=${run.executionSessionId}`);
917
+ return false;
918
+ }
919
+ const entered = enterTrigger(undefined, definition.id, run.runId);
920
+ if (!entered.ok)
921
+ return false;
922
+ const causation = entered.causation;
923
+ const controller = new AbortController();
924
+ let settleCompletion;
925
+ const completion = new Promise(resolve => { settleCompletion = resolve; });
926
+ const runtime = {
927
+ run,
928
+ definition,
929
+ source: run.source,
930
+ causation,
931
+ controller,
932
+ completion,
933
+ anomalies: [],
934
+ };
935
+ this.addRunning(definition.id, run.runId, runtime);
936
+ if (this.isOneShot(definition.source))
937
+ this.recoveredOneShotIds.add(definition.id);
938
+ const resumeCount = (run.resumeCount ?? 0) + 1;
939
+ const patched = this.state.setPhase(definition.id, run.runId, 'running', { resumeCount });
940
+ if (patched)
941
+ runtime.run = patched;
942
+ this.state.appendEvent(definition.id, run.runId, {
943
+ event: 'execution.recovered',
944
+ ts: Date.now(),
945
+ attempt: run.attempt,
946
+ attemptId: run.attemptId,
947
+ executionSessionId: session.id,
948
+ resumeCount,
949
+ });
950
+ recordCausationSpan(causation, 'trigger.run', {
951
+ status: 'started',
952
+ refs: { triggerId: definition.id, runId: run.runId },
953
+ reason: 'daemon_restart_resume',
954
+ });
955
+ void this.continueRecoveredRun(definition, runtime, session)
956
+ .then(settleCompletion, error => {
957
+ const anomalies = executionAnomaliesFromError(error) ?? [];
958
+ runtime.anomalies = anomalies;
959
+ if (this.isRunning(definition.id, run.runId))
960
+ this.finishAbortedRun(runtime, definition.id, run.runId);
961
+ logger.error(`[Trigger] recovered run crashed: trigger=${definition.id} run=${run.runId} error=${error instanceof Error ? error.message : String(error)}`);
962
+ settleCompletion({ anomalies });
963
+ });
964
+ logger.info(`[Trigger] resumed open run: trigger=${definition.id} run=${run.runId} session=${session.id} attempt=${run.attempt} resume=${resumeCount}`);
965
+ return true;
966
+ }
967
+ isResumableCheckpoint(definition, run) {
968
+ return definition.execution.type === 'trigger_session'
969
+ && run.phase === 'running'
970
+ && !!run.source
971
+ && Number.isFinite(run.source.firedAt)
972
+ && isRecord(run.source.payload)
973
+ && Number.isInteger(run.attempt)
974
+ && run.attempt >= 1
975
+ && typeof run.attemptId === 'string'
976
+ && !!run.attemptId
977
+ && typeof run.executionSessionId === 'string'
978
+ && !!run.executionSessionId
979
+ && typeof run.executionDeadlineAt === 'number'
980
+ && Number.isFinite(run.executionDeadlineAt)
981
+ && run.executionDeadlineAt > Date.now()
982
+ && run.definitionRevision === definitionRevision(definition);
983
+ }
984
+ async continueRecoveredRun(definition, runtime, session) {
985
+ const { run, source, controller, causation } = runtime;
986
+ const runId = run.runId;
987
+ let processing = null;
988
+ let reply = null;
989
+ let anomalies = [];
990
+ let incomplete;
991
+ try {
992
+ const originalPrompt = renderTemplate(definition.execution.prompt ?? '', {
993
+ trigger: definition,
994
+ timestamp: source.firedAt,
995
+ event: source.payload,
996
+ source: { type: definition.source.type, payload: source.payload },
997
+ });
998
+ processing = {
999
+ mode: definition.execution.type,
1000
+ renderedTextHash: sha256(originalPrompt),
1001
+ renderedTextPreview: previewText(originalPrompt),
1002
+ };
1003
+ const execution = await awaitWithAbortSettleGrace(this.runRecoveredExecution(definition, runtime, session), controller.signal, RUN_ABORT_SETTLE_GRACE_MS, 'execution');
1004
+ reply = execution.reply ?? null;
1005
+ anomalies = execution.anomalies ?? [];
1006
+ runtime.anomalies = anomalies;
1007
+ if (controller.signal.aborted) {
1008
+ if (this.isRunning(definition.id, runId))
1009
+ this.finishAbortedRun(runtime, definition.id, runId);
1010
+ return { anomalies };
1011
+ }
1012
+ const executionFailed = execution.reply
1013
+ ? shouldRetryExecution(execution.reply)
1014
+ : execution.feedbackResult?.status === 'failed';
1015
+ this.state.appendEvent(definition.id, runId, {
1016
+ event: executionFailed ? 'execution.failed' : 'execution.completed',
1017
+ ts: Date.now(),
1018
+ mode: definition.execution.type,
1019
+ outcome: execution.reply?.outcome ?? execution.feedbackResult?.status,
1020
+ ...(anomalies.length ? { anomalyCount: anomalies.length } : {}),
1021
+ });
1022
+ if (!execution.feedbackResult) {
1023
+ this.state.setPhase(definition.id, runId, 'feedback-pending', {
1024
+ deadlineAt: Date.now() + 30_000,
848
1025
  });
849
- this.audit.write(audit);
850
- if (status === 'failed') {
851
- const reply = errorReply(run.runId, Math.max(0, Date.now() - run.startedAt), 'daemon_restart', 'daemon restarted while run was waiting or executing');
852
- try {
853
- await this.feedback.dispatch({
854
- trigger: definition,
855
- runId: run.runId,
856
- firedAt: run.startedAt,
857
- branch: 'onFailure',
858
- reply,
859
- sourcePayload: this.sourcePayload(definition.source),
860
- causation: createRootCausation(),
1026
+ this.state.appendEvent(definition.id, runId, {
1027
+ event: 'feedback.pending',
1028
+ ts: Date.now(),
1029
+ branch: execution.branch,
1030
+ });
1031
+ }
1032
+ const feedbackResult = execution.feedbackResult ?? await awaitWithAbortSettleGrace(this.feedback.dispatch({
1033
+ trigger: definition,
1034
+ runId,
1035
+ firedAt: source.firedAt,
1036
+ branch: execution.branch,
1037
+ reply: execution.reply,
1038
+ sourcePayload: source.payload,
1039
+ causation,
1040
+ signal: controller.signal,
1041
+ }), controller.signal, RUN_ABORT_SETTLE_GRACE_MS, 'feedback');
1042
+ if (controller.signal.aborted) {
1043
+ if (this.isRunning(definition.id, runId))
1044
+ this.finishAbortedRun(runtime, definition.id, runId);
1045
+ return { anomalies };
1046
+ }
1047
+ const audit = this.buildAudit({
1048
+ definition,
1049
+ runId,
1050
+ startedAt: run.startedAt,
1051
+ status: statusWithExecutionAnomalies(feedbackResult.status, anomalies),
1052
+ reason: feedbackResult.reason,
1053
+ source,
1054
+ processing,
1055
+ script: null,
1056
+ reply,
1057
+ feedback: feedbackResult.feedback,
1058
+ anomalies,
1059
+ effects: feedbackResult.effects,
1060
+ error: feedbackResult.error,
1061
+ causation,
1062
+ });
1063
+ this.audit.write(audit);
1064
+ this.publishTriggerRunOutcome(definition, audit);
1065
+ this.finishRecoveredRun(definition, runId);
1066
+ recordCausationSpan(causation, 'trigger.run', {
1067
+ status: audit.status === 'failed' ? 'failed' : 'completed',
1068
+ refs: { triggerId: definition.id, runId },
1069
+ reason: audit.reason,
1070
+ });
1071
+ return { anomalies };
1072
+ }
1073
+ catch (error) {
1074
+ if (error instanceof AbortSettleGraceError) {
1075
+ incomplete = {
1076
+ reason: 'abort_settle_grace_exceeded',
1077
+ phase: error.phase,
1078
+ timeoutMs: error.graceMs,
1079
+ anomalyCapture: 'truncated',
1080
+ };
1081
+ }
1082
+ anomalies = mergeTriggerExecutionAnomalies(anomalies, executionAnomaliesFromError(error));
1083
+ runtime.anomalies = anomalies;
1084
+ if (controller.signal.aborted) {
1085
+ if (this.isRunning(definition.id, runId))
1086
+ this.finishAbortedRun(runtime, definition.id, runId);
1087
+ return { anomalies, ...(incomplete ? { incomplete } : {}) };
1088
+ }
1089
+ const message = error?.message || String(error);
1090
+ const audit = this.buildAudit({
1091
+ definition,
1092
+ runId,
1093
+ startedAt: run.startedAt,
1094
+ status: 'failed',
1095
+ reason: 'daemon_error',
1096
+ source,
1097
+ processing,
1098
+ script: null,
1099
+ reply,
1100
+ feedback: null,
1101
+ anomalies,
1102
+ incomplete,
1103
+ effects: [],
1104
+ error: { code: 'daemon_error', message },
1105
+ causation,
1106
+ });
1107
+ this.audit.write(audit);
1108
+ this.publishTriggerRunOutcome(definition, audit);
1109
+ this.finishRecoveredRun(definition, runId);
1110
+ recordCausationSpan(causation, 'trigger.run', {
1111
+ status: 'failed',
1112
+ refs: { triggerId: definition.id, runId },
1113
+ reason: 'daemon_error',
1114
+ });
1115
+ return { anomalies, ...(incomplete ? { incomplete } : {}) };
1116
+ }
1117
+ }
1118
+ async runRecoveredExecution(definition, runtime, session) {
1119
+ const { run, source, controller, causation } = runtime;
1120
+ let attempt = run.attempt;
1121
+ let execution = await this.runRecoveredExecutionAttempt(definition, runtime, session);
1122
+ let anomalies = accumulateTriggerExecutionAnomalies([], mergeTriggerExecutionAnomalies(execution.reply?.anomalies, drainTriggerExecutionAnomalies(run.runId)));
1123
+ runtime.anomalies = anomalies;
1124
+ const maxAttempt = 1 + (definition.execution.onError === 'retry'
1125
+ ? definition.reliability.retry.maxAttempts
1126
+ : 0);
1127
+ while (shouldRetryExecutionResult(execution) && attempt < maxAttempt && !controller.signal.aborted) {
1128
+ const backoffCompleted = await sleep(definition.reliability.retry.backoffMs, controller.signal);
1129
+ if (!backoffCompleted || controller.signal.aborted)
1130
+ break;
1131
+ attempt += 1;
1132
+ execution = await this.runExecutionAttempt(definition, run.runId, source.firedAt, source.payload, controller.signal, false, causation, attempt);
1133
+ anomalies = accumulateTriggerExecutionAnomalies(anomalies, mergeTriggerExecutionAnomalies(execution.reply?.anomalies, drainTriggerExecutionAnomalies(run.runId)));
1134
+ runtime.anomalies = anomalies;
1135
+ }
1136
+ if (execution.reply && anomalies.length) {
1137
+ execution = { ...execution, reply: { ...execution.reply, anomalies } };
1138
+ }
1139
+ return anomalies.length ? { ...execution, anomalies } : execution;
1140
+ }
1141
+ async runRecoveredExecutionAttempt(definition, runtime, session) {
1142
+ const { run, source, controller, causation } = runtime;
1143
+ try {
1144
+ const daemonReply = await this.daemonChannel.converse(TRIGGER_RESTART_RESUME_PROMPT, {
1145
+ trigger: definition,
1146
+ runId: run.runId,
1147
+ attemptId: run.attemptId,
1148
+ firedAt: source.firedAt,
1149
+ projectPath: session.projectPath,
1150
+ baseagent: session.baseagent,
1151
+ session,
1152
+ causation,
1153
+ signal: controller.signal,
1154
+ executionDeadlineAt: run.executionDeadlineAt,
1155
+ onStateChange: (state, metadata) => {
1156
+ if (state === 'requires_action') {
1157
+ this.state.setPhase(definition.id, run.runId, 'requires-action');
1158
+ this.state.appendEvent(definition.id, run.runId, {
1159
+ event: 'execution.requires_action',
1160
+ ts: Date.now(),
1161
+ requestId: metadata?.requestId,
1162
+ expiresAt: metadata?.expiresAt,
861
1163
  });
862
1164
  }
863
- catch (error) {
864
- logger.warn(`[Trigger] daemon restart feedback failed: trigger=${definition.id} run=${run.runId} error=${error instanceof Error ? error.message : String(error)}`);
1165
+ else {
1166
+ this.state.setPhase(definition.id, run.runId, 'running');
1167
+ this.state.appendEvent(definition.id, run.runId, {
1168
+ event: 'execution.resumed',
1169
+ ts: Date.now(),
1170
+ requestId: metadata?.requestId,
1171
+ });
865
1172
  }
866
- }
1173
+ },
1174
+ });
1175
+ const sentinel = definition.execution.noopSentinel ?? '[[NOOP]]';
1176
+ const reply = {
1177
+ ...daemonReply,
1178
+ outcome: daemonReply.outcome === 'success' && daemonReply.text.trim() === sentinel ? 'noop' : daemonReply.outcome,
1179
+ text: daemonReply.text.trim() === sentinel ? '' : daemonReply.text,
1180
+ };
1181
+ return {
1182
+ branch: branchFromReply(reply),
1183
+ script: null,
1184
+ processing: { mode: definition.execution.type },
1185
+ reply,
1186
+ };
1187
+ }
1188
+ catch (error) {
1189
+ const reply = errorReply(run.runId, 0, 'agent_execution_error', error?.message || String(error));
1190
+ return {
1191
+ branch: 'onFailure',
1192
+ script: null,
1193
+ processing: { mode: definition.execution.type },
1194
+ reply,
1195
+ };
1196
+ }
1197
+ }
1198
+ async closeOpenRunAfterRestart(definition, run) {
1199
+ const status = run.phase === 'feedback-pending' ? 'skipped' : 'failed';
1200
+ const source = run.source
1201
+ ?? this.buildSourceInfo(definition.source, run.startedAt, run.startedAt, this.sourcePayload(definition.source));
1202
+ const audit = this.buildAudit({
1203
+ definition,
1204
+ runId: run.runId,
1205
+ startedAt: run.startedAt,
1206
+ status,
1207
+ reason: 'daemon_restart',
1208
+ source,
1209
+ script: null,
1210
+ reply: null,
1211
+ feedback: null,
1212
+ effects: [],
1213
+ error: status === 'failed' ? { code: 'daemon_restart', message: 'daemon restarted while run was open' } : null,
1214
+ });
1215
+ this.audit.write(audit);
1216
+ if (status === 'failed') {
1217
+ const reply = errorReply(run.runId, Math.max(0, Date.now() - run.startedAt), 'daemon_restart', 'daemon restarted while run was waiting or executing');
1218
+ try {
1219
+ await this.feedback.dispatch({
1220
+ trigger: definition,
1221
+ runId: run.runId,
1222
+ firedAt: source.firedAt,
1223
+ branch: 'onFailure',
1224
+ reply,
1225
+ sourcePayload: source.payload,
1226
+ causation: createRootCausation(),
1227
+ });
1228
+ }
1229
+ catch (error) {
1230
+ logger.warn(`[Trigger] daemon restart feedback failed: trigger=${definition.id} run=${run.runId} error=${error instanceof Error ? error.message : String(error)}`);
867
1231
  }
868
- this.state.clear(definition.id);
869
1232
  }
1233
+ this.state.remove(definition.id, run.runId);
870
1234
  }
871
1235
  resolveScheduledAt(definition) {
872
1236
  const now = Date.now();
@@ -1100,6 +1464,13 @@ export class TriggerRuntimeScheduler {
1100
1464
  }
1101
1465
  async settleTerminatedRun(runId, runtime, termination) {
1102
1466
  const completion = await settleWithin(runtime.completion, RUN_COMPLETION_BARRIER_MS);
1467
+ if (termination.reason === 'suspended') {
1468
+ if (!completion.settled) {
1469
+ logger.warn(`[Trigger] suspended run did not settle before daemon exit: trigger=${runtime.definition.id} run=${runId} timeoutMs=${RUN_COMPLETION_BARRIER_MS}`);
1470
+ }
1471
+ this.removeRunning(runtime.definition.id, runId);
1472
+ return;
1473
+ }
1103
1474
  const completedAnomalies = mergeTriggerExecutionAnomalies(runtime.anomalies, completion.settled ? completion.value.anomalies : undefined);
1104
1475
  const anomalies = accumulateTriggerExecutionAnomalies(completedAnomalies, drainTriggerExecutionAnomalies(runId));
1105
1476
  if (!completion.settled) {
@@ -1146,16 +1517,49 @@ export class TriggerRuntimeScheduler {
1146
1517
  }
1147
1518
  this.finishRun(runtime.definition.id, runId);
1148
1519
  }
1149
- finishRun(triggerId, runId) {
1520
+ finishAbortedRun(runtime, triggerId, runId) {
1521
+ if (runtime?.termination?.reason === 'suspended') {
1522
+ this.removeRunning(triggerId, runId);
1523
+ return;
1524
+ }
1525
+ this.finishRun(triggerId, runId);
1526
+ }
1527
+ suspendedRuntimeResult(runtime, triggerId, runId) {
1528
+ if (runtime?.termination?.reason !== 'suspended')
1529
+ return undefined;
1530
+ return {
1531
+ ok: true,
1532
+ runId,
1533
+ triggerId,
1534
+ status: 'running',
1535
+ reason: 'daemon_restart_resume_pending',
1536
+ };
1537
+ }
1538
+ removeRunning(triggerId, runId) {
1150
1539
  const runs = this.running.get(triggerId);
1151
1540
  runs?.delete(runId);
1152
1541
  if (runs && runs.size === 0)
1153
1542
  this.running.delete(triggerId);
1543
+ }
1544
+ finishRun(triggerId, runId) {
1545
+ this.removeRunning(triggerId, runId);
1154
1546
  this.state.remove(triggerId, runId);
1155
1547
  }
1548
+ finishRecoveredRun(definition, runId) {
1549
+ this.finishRun(definition.id, runId);
1550
+ if (!this.isOneShot(definition.source))
1551
+ return;
1552
+ const fresh = this.manager.get(definition.id);
1553
+ if (fresh?.enabled && definitionRevision(fresh) === definitionRevision(definition)) {
1554
+ this.setEnabled(definition.id, false);
1555
+ }
1556
+ }
1156
1557
  isRunning(triggerId, runId) {
1157
1558
  return this.running.get(triggerId)?.has(runId) === true;
1158
1559
  }
1560
+ daemonExecutionDeadlineAt(startedAt) {
1561
+ return this.daemonChannel.executionDeadlineAt?.(startedAt) ?? startedAt + 60 * 60 * 1_000;
1562
+ }
1159
1563
  clearTimer(triggerId) {
1160
1564
  const timer = this.timers.get(triggerId);
1161
1565
  if (timer)
@@ -360,6 +360,7 @@ function normalizeExecution(value, _opts) {
360
360
  prompt: type === 'script' ? optionalString(raw.prompt) : requiredString(raw.prompt, 'execution.prompt'),
361
361
  script: type === 'script' ? normalizeScript(raw.script) : undefined,
362
362
  thread: type === 'trigger_session' ? (thread ?? 'by_trigger') : thread,
363
+ baseagent: normalizeExecutionBaseagent(raw.baseagent),
363
364
  model: optionalString(raw.model),
364
365
  effort: normalizeEffort(raw.effort),
365
366
  permissionMode: normalizeTriggerPermissionMode(raw.permissionMode),
@@ -368,6 +369,14 @@ function normalizeExecution(value, _opts) {
368
369
  };
369
370
  return execution;
370
371
  }
372
+ function normalizeExecutionBaseagent(value) {
373
+ if (value === undefined)
374
+ return undefined;
375
+ if (typeof value !== 'string' || !value.trim()) {
376
+ throw new Error('execution.baseagent must be a non-empty string');
377
+ }
378
+ return value.trim();
379
+ }
371
380
  function normalizeExecutionThread(value) {
372
381
  const thread = optionalString(value);
373
382
  if (thread === undefined)
@@ -533,6 +542,8 @@ function validateTriggerSemantics(definition) {
533
542
  throw new Error('execution.prompt is not allowed when execution.type=script');
534
543
  if (execution.thread !== undefined)
535
544
  throw new Error('execution.thread is not allowed when execution.type=script');
545
+ if (execution.baseagent !== undefined)
546
+ throw new Error('execution.baseagent is only allowed when execution.type=trigger_session');
536
547
  }
537
548
  if (execution.type === 'trigger_session') {
538
549
  if (!execution.prompt)
@@ -547,6 +558,8 @@ function validateTriggerSemantics(definition) {
547
558
  throw new Error('execution.script is not allowed when execution.type=target_session');
548
559
  if (execution.thread !== undefined)
549
560
  throw new Error('execution.thread is not allowed when execution.type=target_session');
561
+ if (execution.baseagent !== undefined)
562
+ throw new Error('execution.baseagent is only allowed when execution.type=trigger_session');
550
563
  if (definition.feedback.strategy === 'silent')
551
564
  throw new Error('feedback.strategy=silent is not allowed when execution.type=target_session');
552
565
  }