evolcore 0.0.3 → 0.0.5

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 (148) hide show
  1. package/CHANGELOG.md +83 -0
  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 +1 -31
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +157 -5
  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/channels/aun.js +220 -112
  14. package/dist/channels/daemon.js +18 -4
  15. package/dist/channels/dingtalk.js +52 -137
  16. package/dist/channels/feishu.js +56 -4
  17. package/dist/channels/qqbot.js +23 -1
  18. package/dist/channels/wechat.js +303 -155
  19. package/dist/channels/wecom.js +383 -7
  20. package/dist/cli/aun-commands.js +11 -11
  21. package/dist/cli/daemon-commands.js +46 -15
  22. package/dist/cli/handoff-command.js +2 -1
  23. package/dist/cli/init-channel.js +185 -67
  24. package/dist/cli/init.js +49 -27
  25. package/dist/cli/trigger-command.js +92 -11
  26. package/dist/config/access-policy-domain.js +18 -0
  27. package/dist/config/access-policy.js +110 -0
  28. package/dist/config/builtin-role-templates.js +27 -14
  29. package/dist/config/builtin-roles.js +25 -6
  30. package/dist/config/config-field-policy.js +17 -5
  31. package/dist/config/config-manager.js +198 -22
  32. package/dist/config/config-operation-service.js +35 -38
  33. package/dist/config/contact-bind-code.js +274 -0
  34. package/dist/config/contact-book-store.js +173 -5
  35. package/dist/config/contact-book.js +32 -0
  36. package/dist/config/contact-operation-service.js +9 -3
  37. package/dist/config/contact-request-service.js +331 -0
  38. package/dist/config/peer-role-resolver.js +3 -1
  39. package/dist/config/schema-registry.js +49 -24
  40. package/dist/config-store.js +14 -2
  41. package/dist/core/auth/agent-delegation.js +105 -11
  42. package/dist/core/auth/authorization-audit.js +6 -0
  43. package/dist/core/auth/operation-authorizer.js +8 -0
  44. package/dist/core/auth/operation-catalog.js +51 -3
  45. package/dist/core/auth/trigger-authorization.js +15 -0
  46. package/dist/core/bootstrap-service.js +2 -9
  47. package/dist/core/channel-loader.js +6 -2
  48. package/dist/core/command/command-handler.js +10 -13
  49. package/dist/core/command/connect-menu.js +229 -26
  50. package/dist/core/command/evol-menu-version-gate.js +38 -0
  51. package/dist/core/command/group-menu.js +421 -0
  52. package/dist/core/command/menu-handler.js +269 -85
  53. package/dist/core/command/menu-protocol.js +8 -2
  54. package/dist/core/command/menu-token-store.js +102 -0
  55. package/dist/core/command/role-menu.js +16 -0
  56. package/dist/core/command/slash-gate.js +1 -1
  57. package/dist/core/command/slash-handler.js +158 -31
  58. package/dist/core/daemon-file-cache.js +28 -0
  59. package/dist/core/event-catalog.js +29 -0
  60. package/dist/core/evolagent-registry.js +4 -39
  61. package/dist/core/handoff/runtime.js +162 -37
  62. package/dist/core/handoff/store.js +46 -2
  63. package/dist/core/handoff/types.js +1 -0
  64. package/dist/core/inference/text-inference.js +16 -74
  65. package/dist/core/message/file-markers.js +7 -0
  66. package/dist/core/message/im-renderer.js +18 -14
  67. package/dist/core/message/inbound-admission.js +134 -0
  68. package/dist/core/message/message-bridge.js +616 -74
  69. package/dist/core/message/message-log.js +1 -0
  70. package/dist/core/message/message-queue.js +185 -10
  71. package/dist/core/message/peer-mode.js +7 -8
  72. package/dist/core/message/response-engine.js +381 -79
  73. package/dist/core/permission/approval-gateway.js +17 -10
  74. package/dist/core/permission/ec-command-parser.js +101 -46
  75. package/dist/core/permission/tool-policy.js +69 -24
  76. package/dist/core/protected-paths.js +36 -11
  77. package/dist/core/session/session-fs-store.js +19 -4
  78. package/dist/core/session/session-manager.js +232 -4
  79. package/dist/core/session/session-mapper.js +2 -0
  80. package/dist/core/session/session-renew.js +125 -69
  81. package/dist/core/session/session-turn-coordinator.js +5 -1
  82. package/dist/eck/kit-renderer.js +12 -1
  83. package/dist/eck/message-renderer.js +79 -1
  84. package/dist/index.js +224 -89
  85. package/dist/ipc.js +81 -4
  86. package/dist/paths.js +3 -0
  87. package/dist/response-system/config-resolver.js +29 -0
  88. package/dist/response-system/coordinator.js +8 -32
  89. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  90. package/dist/response-system/index.js +1 -0
  91. package/dist/response-system/modes/single-session/index.js +7 -4
  92. package/dist/trigger/parser.js +55 -15
  93. package/dist/trigger/patch.js +4 -1
  94. package/dist/trigger/scheduler.js +441 -39
  95. package/dist/utils/cross-platform.js +8 -2
  96. package/dist/utils/error-dict.json +7 -0
  97. package/dist/utils/evolcore-version.js +21 -0
  98. package/dist/utils/logger.js +2 -2
  99. package/dist/utils/process-introspect.js +19 -3
  100. package/dist/utils/stable-semver.js +21 -0
  101. package/dist/utils/stats.js +33 -9
  102. package/kits/docs/channels/aun.md +4 -13
  103. package/kits/docs/evolcore/INDEX.md +1 -1
  104. package/kits/docs/evolcore/config.md +47 -2
  105. package/kits/docs/evolcore/contact.md +8 -3
  106. package/kits/docs/evolcore/group-rules.md +46 -4
  107. package/kits/docs/evolcore/group.md +4 -4
  108. package/kits/docs/evolcore/msg.md +5 -5
  109. package/kits/docs/evolcore/trigger.md +25 -8
  110. package/kits/docs/path-registry.md +36 -17
  111. package/kits/eck_message_manifest.json +12 -1
  112. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  113. package/kits/rules/01-overview.md +17 -7
  114. package/kits/rules/02-navigation.md +38 -18
  115. package/kits/rules/03-identity.md +28 -24
  116. package/kits/rules/04-relation.md +44 -28
  117. package/kits/rules/05-venue.md +31 -15
  118. package/kits/rules/06-channel.md +11 -7
  119. package/kits/schemas/_meta.json +18 -7
  120. package/kits/schemas/agent-config.schema.7.json +303 -0
  121. package/kits/schemas/agent-config.schema.8.json +304 -0
  122. package/kits/schemas/agent-config.schema.9.json +364 -0
  123. package/kits/schemas/contact-book.schema.3.json +67 -0
  124. package/kits/schemas/daemon.schema.1.json +3 -2
  125. package/kits/schemas/daemon.schema.2.json +101 -0
  126. package/kits/schemas/daemon.schema.3.json +123 -0
  127. package/kits/schemas/defaults.schema.2.json +85 -0
  128. package/kits/schemas/defaults.schema.3.json +73 -0
  129. package/kits/schemas/relation-config.schema.6.json +46 -0
  130. package/kits/schemas/relation-config.schema.7.json +59 -0
  131. package/kits/schemas/single-session.schema.2.json +57 -0
  132. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  133. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  134. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  135. package/kits/templates/roles/admin.json +46 -0
  136. package/kits/templates/roles/member.json +4 -0
  137. package/kits/templates/roles/visitor.json +4 -0
  138. package/kits/templates/system-fragments/channel.md +12 -2
  139. package/kits/templates/system-fragments/identity.md +3 -1
  140. package/kits/templates/system-fragments/relation.md +1 -1
  141. package/kits/templates/system-fragments/session.md +6 -2
  142. package/package.json +4 -2
  143. package/MIGRATION-0.5.0.md +0 -378
  144. package/ROLE_ACCESS_CONTROL.md +0 -174
  145. package/dist/channels/contact-bind-code.js +0 -134
  146. package/dist/channels/wecom-card.js +0 -101
  147. package/dist/channels/wecom-onboarding.js +0 -82
  148. package/dist/channels/wecom-state.js +0 -191
@@ -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,
@@ -740,17 +789,33 @@ export class TriggerRuntimeScheduler {
740
789
  + ` baseagent=${runtimeBaseagent}`
741
790
  + ` model=${definition.execution.model ?? '<agent-default>'}`
742
791
  + ` effort=${definition.execution.effort ?? '<agent-default>'}`);
743
- 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
+ });
744
808
  const daemonReply = await this.daemonChannel.converse(prompt, {
745
809
  trigger: definition,
746
810
  runId,
747
- attemptId: `${runId}:attempt:${attempt}`,
811
+ attemptId,
748
812
  firedAt,
749
813
  projectPath: this.runtime.projectPath,
750
814
  baseagent: runtimeBaseagent,
751
815
  session: executionSession,
752
816
  causation,
753
817
  signal,
818
+ executionDeadlineAt: activeRun?.executionDeadlineAt ?? executionDeadlineAt,
754
819
  onStateChange: (state, metadata) => {
755
820
  if (state === 'requires_action') {
756
821
  this.state.setPhase(definition.id, runId, 'requires-action');
@@ -834,41 +899,338 @@ export class TriggerRuntimeScheduler {
834
899
  if (activeRuns.length === 0)
835
900
  continue;
836
901
  for (const run of activeRuns) {
837
- const status = run.phase === 'feedback-pending' ? 'skipped' : 'failed';
838
- const audit = this.buildAudit({
839
- definition,
840
- runId: run.runId,
841
- startedAt: run.startedAt,
842
- status,
843
- reason: 'daemon_restart',
844
- source: this.buildSourceInfo(definition.source, run.startedAt, run.startedAt, this.sourcePayload(definition.source)),
845
- script: null,
846
- reply: null,
847
- feedback: null,
848
- effects: [],
849
- 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,
850
1025
  });
851
- this.audit.write(audit);
852
- if (status === 'failed') {
853
- const reply = errorReply(run.runId, Math.max(0, Date.now() - run.startedAt), 'daemon_restart', 'daemon restarted while run was waiting or executing');
854
- try {
855
- await this.feedback.dispatch({
856
- trigger: definition,
857
- runId: run.runId,
858
- firedAt: run.startedAt,
859
- branch: 'onFailure',
860
- reply,
861
- sourcePayload: this.sourcePayload(definition.source),
862
- 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,
863
1163
  });
864
1164
  }
865
- catch (error) {
866
- 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
+ });
867
1172
  }
868
- }
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)}`);
869
1231
  }
870
- this.state.clear(definition.id);
871
1232
  }
1233
+ this.state.remove(definition.id, run.runId);
872
1234
  }
873
1235
  resolveScheduledAt(definition) {
874
1236
  const now = Date.now();
@@ -1102,6 +1464,13 @@ export class TriggerRuntimeScheduler {
1102
1464
  }
1103
1465
  async settleTerminatedRun(runId, runtime, termination) {
1104
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
+ }
1105
1474
  const completedAnomalies = mergeTriggerExecutionAnomalies(runtime.anomalies, completion.settled ? completion.value.anomalies : undefined);
1106
1475
  const anomalies = accumulateTriggerExecutionAnomalies(completedAnomalies, drainTriggerExecutionAnomalies(runId));
1107
1476
  if (!completion.settled) {
@@ -1148,16 +1517,49 @@ export class TriggerRuntimeScheduler {
1148
1517
  }
1149
1518
  this.finishRun(runtime.definition.id, runId);
1150
1519
  }
1151
- 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) {
1152
1539
  const runs = this.running.get(triggerId);
1153
1540
  runs?.delete(runId);
1154
1541
  if (runs && runs.size === 0)
1155
1542
  this.running.delete(triggerId);
1543
+ }
1544
+ finishRun(triggerId, runId) {
1545
+ this.removeRunning(triggerId, runId);
1156
1546
  this.state.remove(triggerId, runId);
1157
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
+ }
1158
1557
  isRunning(triggerId, runId) {
1159
1558
  return this.running.get(triggerId)?.has(runId) === true;
1160
1559
  }
1560
+ daemonExecutionDeadlineAt(startedAt) {
1561
+ return this.daemonChannel.executionDeadlineAt?.(startedAt) ?? startedAt + 60 * 60 * 1_000;
1562
+ }
1161
1563
  clearTimer(triggerId) {
1162
1564
  const timer = this.timers.get(triggerId);
1163
1565
  if (timer)
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'url';
3
3
  import { execFileSync, execFile, spawn, spawnSync } from 'child_process';
4
4
  import { promisify } from 'util';
5
5
  import fs from 'fs';
6
+ import { getProcessStartTime } from './process-introspect.js';
6
7
  const execFileAsync = promisify(execFile);
7
8
  export const isWindows = process.platform === 'win32';
8
9
  const ENCODE_PATH_MAX = 200;
@@ -133,10 +134,15 @@ export function getProcessInfo(pid) {
133
134
  }
134
135
  }
135
136
  else {
136
- const etimes = execFileSync('ps', ['-p', String(pid), '-o', 'etimes='], { encoding: 'utf-8' }).trim();
137
+ // procps `etimes` can overflow for freshly spawned processes in some
138
+ // container/time-namespace combinations. /proc start time is reliable
139
+ // on Linux and is already used for PID reuse protection elsewhere.
140
+ const startedAt = process.platform === 'linux' ? getProcessStartTime(pid) : null;
141
+ const uptime = startedAt !== null
142
+ ? formatUptime(Math.max(0, Math.floor((Date.now() - startedAt) / 1000)))
143
+ : formatUptime(parseInt(execFileSync('ps', ['-p', String(pid), '-o', 'etimes='], { encoding: 'utf-8' }).trim(), 10));
137
144
  const cpu = execFileSync('ps', ['-p', String(pid), '-o', '%cpu='], { encoding: 'utf-8' }).trim();
138
145
  const mem = execFileSync('ps', ['-p', String(pid), '-o', 'rss='], { encoding: 'utf-8' }).trim();
139
- const uptime = formatUptime(parseInt(etimes, 10));
140
146
  return { uptime, cpu, memory: mem };
141
147
  }
142
148
  }
@@ -56,6 +56,13 @@
56
56
  "type": "api_error",
57
57
  "message": "⚠️ 当前算力池切换中,请稍后重试"
58
58
  },
59
+ {
60
+ "id": "retry-attempt-stalled",
61
+ "match": "retry_attempt_stalled",
62
+ "action": "retry",
63
+ "type": "api_error",
64
+ "message": "⚠️ API 重试调用长时间无响应"
65
+ },
59
66
  {
60
67
  "id": "selected-model-at-capacity",
61
68
  "match": "selected model is at capacity",
@@ -0,0 +1,21 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getPackageRoot } from '../paths.js';
4
+ let cachedVersion;
5
+ export function readInstalledEvolcoreVersion() {
6
+ if (cachedVersion !== undefined)
7
+ return cachedVersion;
8
+ let version;
9
+ try {
10
+ const pkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), 'package.json'), 'utf-8'));
11
+ version = typeof pkg.version === 'string' && pkg.version ? pkg.version : 'unknown';
12
+ }
13
+ catch {
14
+ version = 'unknown';
15
+ }
16
+ cachedVersion = version;
17
+ return version;
18
+ }
19
+ export function _resetInstalledEvolcoreVersionForTests() {
20
+ cachedVersion = undefined;
21
+ }
@@ -51,8 +51,8 @@ function log(level, ...args) {
51
51
  getWriters().main.write(msg);
52
52
  }
53
53
  /**
54
- * 设置日志级别(config 加载后调用,覆盖环境变量默认值)
55
- * 优先级:config.debug.logLevel → LOG_LEVEL 环境变量 → 'INFO'
54
+ * 设置日志级别(daemon.json 加载后调用,覆盖环境变量默认值)
55
+ * 优先级:daemon.json debug.logLevel → LOG_LEVEL 环境变量 → 'INFO'
56
56
  */
57
57
  export function setLogLevel(level) {
58
58
  const upper = level.toUpperCase();