evolclaw 3.1.6 → 3.1.7

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.
@@ -164,15 +164,16 @@ export class TriggerScheduler {
164
164
  onFire() {
165
165
  this.timer = null;
166
166
  const now = Date.now();
167
- // Fire all triggers that are due
168
- while (this.heap.peek() && this.heap.peek().nextFireAt <= now + 50) {
167
+ // Loop guard reads a LIVE clock (not the frozen `now`): if rescheduling ever regresses
168
+ // and pushes an occurrence back inside the window, re-reading the clock each iteration
169
+ // still lets time advance past it so the loop drains and terminates.
170
+ while (this.heap.peek() && this.heap.peek().nextFireAt <= Date.now() + 50) {
169
171
  const trigger = this.heap.pop();
170
172
  if (trigger.scheduleType === 'cron' && this.inflightCron.has(trigger.id)) {
171
- // Previous run still in flight — skip
173
+ // Previous run still in flight — skip this occurrence, reschedule the next one.
172
174
  logger.warn(`[${this.aid}] Cron trigger ${trigger.name} still running, skipping`);
173
175
  this.eventBus.publish({ type: 'trigger:skipped', triggerId: trigger.id, reason: 'overlap' });
174
- // Re-schedule next cron occurrence
175
- const next = calcNextFireAt('cron', trigger.scheduleValue, now);
176
+ const next = this.nextCronFireAt(trigger.scheduleValue, Date.now());
176
177
  this.manager.updateNextFireAt(trigger.id, next);
177
178
  this.heap.push({ ...trigger, nextFireAt: next });
178
179
  continue;
@@ -181,6 +182,21 @@ export class TriggerScheduler {
181
182
  }
182
183
  this.resetTimer();
183
184
  }
185
+ /**
186
+ * Next cron occurrence strictly outside the firing window.
187
+ *
188
+ * cron-parser returns the next match at-or-after the reference instant. When the timer
189
+ * wakes a hair early — e.g. 08:59:59.999 for a `0 9 * * *` trigger — the "next" occurrence
190
+ * computes to 09:00:00.000, only ~1ms ahead and inside onFire's `<= now + 50` window. That
191
+ * occurrence gets popped and re-fired in the same pass, producing a tight loop. Recomputing
192
+ * from past the window forces the genuine next occurrence (e.g. tomorrow 09:00).
193
+ */
194
+ nextCronFireAt(scheduleValue, ref) {
195
+ let next = calcNextFireAt('cron', scheduleValue, ref);
196
+ if (next <= ref + 50)
197
+ next = calcNextFireAt('cron', scheduleValue, ref + 51);
198
+ return next;
199
+ }
184
200
  fireTrigger(trigger, now) {
185
201
  const messageId = `trigger:${trigger.id}:${now}`;
186
202
  const msg = this.buildSyntheticMessage(trigger, messageId);
@@ -189,8 +205,8 @@ export class TriggerScheduler {
189
205
  this.manager.updateFireStats(trigger.id, now);
190
206
  if (trigger.scheduleType === 'cron') {
191
207
  this.inflightCron.add(trigger.id);
192
- // Re-schedule next occurrence
193
- const next = calcNextFireAt('cron', trigger.scheduleValue, now);
208
+ // Re-schedule next occurrence (outside the firing window — see nextCronFireAt)
209
+ const next = this.nextCronFireAt(trigger.scheduleValue, now);
194
210
  this.manager.updateNextFireAt(trigger.id, next);
195
211
  this.heap.push({ ...trigger, nextFireAt: next });
196
212
  }
package/dist/index.js CHANGED
@@ -522,54 +522,6 @@ async function main() {
522
522
  logger.error(`[Trigger] Scheduler init failed for ${agent.aid}: ${err}`);
523
523
  }
524
524
  }
525
- // Inject primary agent's trigger scheduler as fallback (used when owning agent has no scheduler)
526
- const primaryAgentForTrigger = agentRegistry.runnableAgents()[0];
527
- if (primaryAgentForTrigger?.triggerScheduler && primaryAgentForTrigger?.triggerManager) {
528
- cmdHandler.setTriggerScheduler(primaryAgentForTrigger.triggerScheduler, primaryAgentForTrigger.triggerManager);
529
- }
530
- // Seed default __upgrade-check trigger (daily at random time 3:00~3:59)
531
- // 用户可通过 /trigger cancel __upgrade-check 永久禁用(不会再自动重建)
532
- if (!isLinkedInstall() && primaryAgentForTrigger?.triggerManager && primaryAgentForTrigger?.triggerScheduler) {
533
- const mgr = primaryAgentForTrigger.triggerManager;
534
- const sched = primaryAgentForTrigger.triggerScheduler;
535
- const UPGRADE_TRIGGER_NAME = '__upgrade-check';
536
- if (!mgr.getByName(UPGRADE_TRIGGER_NAME)) {
537
- // Check history: if user cancelled it before, respect that decision
538
- const { history } = mgr.listAll();
539
- const wasCancelled = history.some(h => h.name === UPGRADE_TRIGGER_NAME && h.doneReason === 'cancelled');
540
- if (!wasCancelled) {
541
- // Random minute in 3:00~3:59 to avoid all instances hitting registry simultaneously
542
- const randomMinute = Math.floor(Math.random() * 60);
543
- const cronExpr = `${randomMinute} 3 * * *`;
544
- // Use first channel instance as target (command doesn't need real channelId)
545
- const firstChannel = channelInstances[0]?.adapter?.channelName || 'system';
546
- const trigger = {
547
- id: crypto.randomUUID(),
548
- name: UPGRADE_TRIGGER_NAME,
549
- scheduleType: 'cron',
550
- scheduleValue: cronExpr,
551
- nextFireAt: calcNextFireAt('cron', cronExpr),
552
- targetChannel: firstChannel,
553
- targetChannelId: '__system__',
554
- targetSessionStrategy: 'latest',
555
- prompt: '检查 evolclaw 是否有新版本可用。执行 `npm view evolclaw version` 获取最新版本,与当前版本(执行 `evolclaw --version`)对比。如果有新版本,执行 /restart 进行升级。如果已是最新版本,无需任何操作。',
556
- createdByPeerId: '__system__',
557
- createdByChannel: '__system__',
558
- fireCount: 0,
559
- createdAt: Date.now(),
560
- updatedAt: Date.now(),
561
- };
562
- try {
563
- mgr.register(trigger);
564
- sched.register(trigger);
565
- logger.info(`[Trigger] Seeded default trigger: ${UPGRADE_TRIGGER_NAME} (cron ${cronExpr})`);
566
- }
567
- catch (e) {
568
- logger.warn(`[Trigger] Failed to seed ${UPGRADE_TRIGGER_NAME}: ${e}`);
569
- }
570
- }
571
- }
572
- }
573
525
  // 默认策略
574
526
  const defaultPolicy = {
575
527
  canSwitchProject: (chatType, role) => chatType === 'private' ? (role === 'owner' || role === 'admin') : role === 'owner',
@@ -641,6 +593,54 @@ async function main() {
641
593
  for (const inst of channelInstances) {
642
594
  registerChannelInstance(inst);
643
595
  }
596
+ // Inject primary agent's trigger scheduler after all channels are registered so
597
+ // channelTypeMap is fully populated when setTriggerScheduler backfills old triggers.
598
+ // Seed __upgrade-check here too — needs channelInstances[0].channelType to be resolved.
599
+ const primaryAgentForTrigger = agentRegistry.runnableAgents()[0];
600
+ if (primaryAgentForTrigger?.triggerScheduler && primaryAgentForTrigger?.triggerManager) {
601
+ cmdHandler.setTriggerScheduler(primaryAgentForTrigger.triggerScheduler, primaryAgentForTrigger.triggerManager);
602
+ }
603
+ // Seed default __upgrade-check trigger (daily at random time 3:00~3:59)
604
+ // 用户可通过 /trigger cancel __upgrade-check 永久禁用(不会再自动重建)
605
+ if (!isLinkedInstall() && primaryAgentForTrigger?.triggerManager && primaryAgentForTrigger?.triggerScheduler) {
606
+ const mgr = primaryAgentForTrigger.triggerManager;
607
+ const sched = primaryAgentForTrigger.triggerScheduler;
608
+ const UPGRADE_TRIGGER_NAME = '__upgrade-check';
609
+ if (!mgr.getByName(UPGRADE_TRIGGER_NAME)) {
610
+ const { history } = mgr.listAll();
611
+ const wasCancelled = history.some(h => h.name === UPGRADE_TRIGGER_NAME && h.doneReason === 'cancelled');
612
+ if (!wasCancelled) {
613
+ const randomMinute = Math.floor(Math.random() * 60);
614
+ const cronExpr = `${randomMinute} 3 * * *`;
615
+ const firstChannelInst = channelInstances[0];
616
+ const firstChannel = firstChannelInst?.adapter?.channelName || 'system';
617
+ const trigger = {
618
+ id: crypto.randomUUID(),
619
+ name: UPGRADE_TRIGGER_NAME,
620
+ scheduleType: 'cron',
621
+ scheduleValue: cronExpr,
622
+ nextFireAt: calcNextFireAt('cron', cronExpr),
623
+ targetChannel: firstChannel,
624
+ targetChannelId: '__system__',
625
+ targetSessionStrategy: 'latest',
626
+ prompt: '检查 evolclaw 是否有新版本可用。执行 `npm view evolclaw version` 获取最新版本,与当前版本(执行 `evolclaw --version`)对比。如果有新版本,执行 /restart 进行升级。如果已是最新版本,无需任何操作。',
627
+ createdByPeerId: '__system__',
628
+ createdByChannel: '__system__',
629
+ fireCount: 0,
630
+ createdAt: Date.now(),
631
+ updatedAt: Date.now(),
632
+ };
633
+ try {
634
+ mgr.register(trigger);
635
+ sched.register(trigger);
636
+ logger.info(`[Trigger] Seeded default trigger: ${UPGRADE_TRIGGER_NAME} (cron ${cronExpr})`);
637
+ }
638
+ catch (e) {
639
+ logger.warn(`[Trigger] Failed to seed ${UPGRADE_TRIGGER_NAME}: ${e}`);
640
+ }
641
+ }
642
+ }
643
+ }
644
644
  // Bind adapters to their owning agents and mark running
645
645
  for (const inst of channelInstances) {
646
646
  const agent = agentRegistry.resolveByChannel(inst.adapter.channelKey);
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema_version": 1,
3
+ "sections": [
4
+ {
5
+ "id": "msg-item",
6
+ "type": "file",
7
+ "file": "$KITS_MESSAGE_FRAGMENTS/item.md",
8
+ "order": 10,
9
+ "needsInjection": true,
10
+ "when": "always",
11
+ "description": "单条消息渲染(逐条)"
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,2 @@
1
+ ‹{{now}}{{?chatType=group}} · {{peerName}}{{/}}›
2
+ {{content}}
@@ -9,6 +9,9 @@ sessionKey: {{sessionKey}} # 会话路由键(channelType#urlEncode(channelId)#
9
9
  sessionName: {{sessionName}}
10
10
  {{/}}
11
11
  sessionCreatedAt: {{sessionCreatedAt}}
12
+ {{?localDate}}
13
+ localDate: {{localDate}} {{weekday}} # 当前日期与星期(每条消息的精确时刻见消息正文前缀 ‹…›)
14
+ {{/}}
12
15
  {{?timezone}}
13
16
  timezone: {{timezone}} # 时区 IANA 名:把消息/记忆里的 ISO 时间戳转成本地时间字符串时按此换算
14
17
  {{/}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "3.1.6",
3
+ "version": "3.1.7",
4
4
  "description": "Lightweight AI Agent gateway connecting Claude Agent SDK to messaging channels (Feishu, ACP) with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",