crawd 0.8.6 → 0.9.0
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.
- package/dist/cli.js +7 -1
- package/dist/types.d.ts +5 -23
- package/openclaw.plugin.json +8 -40
- package/package.json +13 -11
- package/skills/crawd/SKILL.md +37 -0
- package/src/backend/coordinator.test.ts +393 -0
- package/src/backend/coordinator.ts +274 -19
- package/src/backend/index.ts +29 -208
- package/src/backend/server.ts +75 -219
- package/src/commands/skill.ts +3 -0
- package/src/commands/start.ts +1 -0
- package/src/config/schema.ts +2 -0
- package/src/plugin.ts +124 -33
- package/src/types.ts +4 -23
- package/dist/backend/chunk-QITCQHSS.js +0 -2087
- package/dist/backend/fileFromPath-WZUZ37JN.js +0 -127
- package/dist/backend/index.js +0 -12418
- package/dist/chunk-QITCQHSS.js +0 -2087
- package/dist/fileFromPath-WZUZ37JN.js +0 -127
- package/dist/plugin.d.ts +0 -27
- package/dist/plugin.js +0 -12363
- package/src/lib/tts/tiktok.ts +0 -91
|
@@ -2,29 +2,56 @@ import { randomUUID } from 'crypto'
|
|
|
2
2
|
import WebSocket from 'ws'
|
|
3
3
|
import type { ChatMessage } from '../lib/chat/types'
|
|
4
4
|
|
|
5
|
-
const BATCH_WINDOW_MS = 20_000
|
|
6
5
|
const SESSION_KEY = process.env.CRAWD_CHANNEL_ID || 'agent:main:crawd:live'
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Plan types
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export type PlanStep = {
|
|
12
|
+
description: string
|
|
13
|
+
status: 'pending' | 'done'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Plan = {
|
|
17
|
+
id: string
|
|
18
|
+
goal: string
|
|
19
|
+
steps: PlanStep[]
|
|
20
|
+
createdAt: number
|
|
21
|
+
status: 'active' | 'completed' | 'abandoned'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type AutonomyMode = 'vibe' | 'plan' | 'none'
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Coordinator configuration
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
9
30
|
export type CoordinatorConfig = {
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
/** How often to send vibe prompt when active (ms). Default:
|
|
31
|
+
/** Autonomy mode: 'vibe' (periodic prompts), 'plan' (goal-driven loop), 'none' (disabled). */
|
|
32
|
+
autonomyMode: AutonomyMode
|
|
33
|
+
/** How often to send vibe prompt when active (ms). Default: 30000 (30 sec). Only used in vibe mode. */
|
|
13
34
|
vibeIntervalMs: number
|
|
14
|
-
/** Go idle after this much inactivity while active (ms). Default:
|
|
35
|
+
/** Go idle after this much inactivity while active (ms). Default: 180000 (3 min) */
|
|
15
36
|
idleAfterMs: number
|
|
16
|
-
/** Go sleep after this much inactivity while idle (ms). Default:
|
|
37
|
+
/** Go sleep after this much inactivity while idle (ms). Default: 180000 (3 min) */
|
|
17
38
|
sleepAfterIdleMs: number
|
|
18
|
-
/**
|
|
39
|
+
/** Chat batch throttle window (ms). Default: 20000 (20 sec) */
|
|
40
|
+
batchWindowMs: number
|
|
41
|
+
/** The autonomous "vibe" prompt sent periodically. Only used in vibe mode. */
|
|
19
42
|
vibePrompt: string
|
|
43
|
+
/** Delay between plan nudges (ms). Default: 2000 (2 sec). Only used in plan mode. */
|
|
44
|
+
planNudgeDelayMs: number
|
|
20
45
|
}
|
|
21
46
|
|
|
22
47
|
export const DEFAULT_CONFIG: CoordinatorConfig = {
|
|
23
|
-
|
|
48
|
+
autonomyMode: 'vibe',
|
|
24
49
|
vibeIntervalMs: 30_000,
|
|
25
50
|
idleAfterMs: 180_000,
|
|
26
51
|
sleepAfterIdleMs: 180_000,
|
|
52
|
+
batchWindowMs: 20_000,
|
|
27
53
|
vibePrompt: `[CRAWD:VIBE] You are on a livestream. Make sure the crawd skill is loaded. Do one thing on the internet or ask the chat something. Respond with LIVESTREAM_REPLIED after using a tool, or NO_REPLY if you have nothing to say.`,
|
|
54
|
+
planNudgeDelayMs: 2_000,
|
|
28
55
|
}
|
|
29
56
|
|
|
30
57
|
export type CoordinatorState = 'sleep' | 'idle' | 'active'
|
|
@@ -502,6 +529,13 @@ export type CoordinatorEvent =
|
|
|
502
529
|
| { type: 'vibeExecuted'; skipped: boolean; reason?: string }
|
|
503
530
|
| { type: 'sleepCheck'; inactiveForMs: number; willSleep: boolean }
|
|
504
531
|
| { type: 'chatProcessed'; count: number }
|
|
532
|
+
// Plan events
|
|
533
|
+
| { type: 'planCreated'; planId: string; goal: string; stepCount: number }
|
|
534
|
+
| { type: 'planStepDone'; planId: string; step: number }
|
|
535
|
+
| { type: 'planCompleted'; planId: string }
|
|
536
|
+
| { type: 'planAbandoned'; planId: string }
|
|
537
|
+
| { type: 'planNudgeScheduled'; nextNudgeAt: number }
|
|
538
|
+
| { type: 'planNudgeExecuted'; skipped: boolean; reason?: string }
|
|
505
539
|
|
|
506
540
|
export class Coordinator {
|
|
507
541
|
private buffer: ChatMessage[] = []
|
|
@@ -518,11 +552,15 @@ export class Coordinator {
|
|
|
518
552
|
private idleSince = 0
|
|
519
553
|
private vibeTimer: NodeJS.Timeout | null = null
|
|
520
554
|
private sleepCheckTimer: NodeJS.Timeout | null = null
|
|
521
|
-
/** True while a flush or talk is being processed — vibes should wait */
|
|
555
|
+
/** True while a flush or talk is being processed — vibes/nudges should wait */
|
|
522
556
|
private _busy = false
|
|
523
557
|
/** Serializes all triggerAgent calls to prevent concurrent runs */
|
|
524
558
|
private _gatewayQueue: Promise<void> = Promise.resolve()
|
|
525
559
|
|
|
560
|
+
// === Plan State ===
|
|
561
|
+
private currentPlan: Plan | null = null
|
|
562
|
+
private planNudgeTimer: NodeJS.Timeout | null = null
|
|
563
|
+
|
|
526
564
|
// === Injected dependencies ===
|
|
527
565
|
private readonly clock: IClock
|
|
528
566
|
private readonly logger: Pick<Console, 'log' | 'error' | 'warn'>
|
|
@@ -553,18 +591,22 @@ export class Coordinator {
|
|
|
553
591
|
updateConfig(config: Partial<CoordinatorConfig>): void {
|
|
554
592
|
this.config = { ...this.config, ...config }
|
|
555
593
|
this.logger.log('[Coordinator] Config updated:', {
|
|
594
|
+
autonomyMode: this.config.autonomyMode,
|
|
556
595
|
vibeIntervalMs: this.config.vibeIntervalMs,
|
|
557
596
|
idleAfterMs: this.config.idleAfterMs,
|
|
558
597
|
sleepAfterIdleMs: this.config.sleepAfterIdleMs,
|
|
598
|
+
batchWindowMs: this.config.batchWindowMs,
|
|
559
599
|
})
|
|
560
600
|
}
|
|
561
601
|
|
|
562
602
|
/** Get current state and config */
|
|
563
|
-
getState(): { state: CoordinatorState; lastActivityAt: number; config: CoordinatorConfig } {
|
|
603
|
+
getState(): { state: CoordinatorState; lastActivityAt: number; config: CoordinatorConfig; plan: Plan | null; autonomyMode: AutonomyMode } {
|
|
564
604
|
return {
|
|
565
605
|
state: this._state,
|
|
566
606
|
lastActivityAt: this.lastActivityAt,
|
|
567
607
|
config: this.config,
|
|
608
|
+
plan: this.currentPlan,
|
|
609
|
+
autonomyMode: this.config.autonomyMode,
|
|
568
610
|
}
|
|
569
611
|
}
|
|
570
612
|
|
|
@@ -588,11 +630,13 @@ export class Coordinator {
|
|
|
588
630
|
vibeIntervalMs: this.config.vibeIntervalMs,
|
|
589
631
|
idleAfterMs: this.config.idleAfterMs,
|
|
590
632
|
sleepAfterIdleMs: this.config.sleepAfterIdleMs,
|
|
633
|
+
batchWindowMs: this.config.batchWindowMs,
|
|
591
634
|
})
|
|
592
635
|
}
|
|
593
636
|
|
|
594
637
|
stop(): void {
|
|
595
638
|
this.stopVibeLoop()
|
|
639
|
+
this.cancelPlanNudge()
|
|
596
640
|
if (this.timer) {
|
|
597
641
|
this.clock.clearTimeout(this.timer)
|
|
598
642
|
this.timer = null
|
|
@@ -603,7 +647,7 @@ export class Coordinator {
|
|
|
603
647
|
|
|
604
648
|
// === State Machine Methods ===
|
|
605
649
|
|
|
606
|
-
/** Wake up from sleep/idle state and start the
|
|
650
|
+
/** Wake up from sleep/idle state and start the autonomy loop */
|
|
607
651
|
wake(): void {
|
|
608
652
|
if (this._state === 'active') return
|
|
609
653
|
|
|
@@ -613,7 +657,12 @@ export class Coordinator {
|
|
|
613
657
|
this.logger.log('[Coordinator] WAKE - transitioning to ACTIVE state')
|
|
614
658
|
this.emit({ type: 'stateChange', from, to: 'active' })
|
|
615
659
|
|
|
616
|
-
this.startVibeLoop()
|
|
660
|
+
this.startVibeLoop() // starts sleep/idle check timers + vibe loop (if mode=vibe)
|
|
661
|
+
|
|
662
|
+
// In plan mode, also start plan nudges if there are pending steps
|
|
663
|
+
if (this.config.autonomyMode === 'plan' && this.hasPendingPlanSteps()) {
|
|
664
|
+
this.schedulePlanNudge()
|
|
665
|
+
}
|
|
617
666
|
}
|
|
618
667
|
|
|
619
668
|
/** Go to idle state (between activities, eyes open) */
|
|
@@ -640,6 +689,7 @@ export class Coordinator {
|
|
|
640
689
|
this.emit({ type: 'stateChange', from, to: 'sleep' })
|
|
641
690
|
|
|
642
691
|
this.stopVibeLoop()
|
|
692
|
+
this.cancelPlanNudge()
|
|
643
693
|
this.compactSession()
|
|
644
694
|
}
|
|
645
695
|
|
|
@@ -671,6 +721,202 @@ export class Coordinator {
|
|
|
671
721
|
return this.clock.now() - this.lastActivityAt
|
|
672
722
|
}
|
|
673
723
|
|
|
724
|
+
// === Plan Methods ===
|
|
725
|
+
|
|
726
|
+
/** Create or replace the current plan */
|
|
727
|
+
setPlan(goal: string, steps: string[]): Plan {
|
|
728
|
+
if (this.currentPlan?.status === 'active') {
|
|
729
|
+
this.currentPlan.status = 'abandoned'
|
|
730
|
+
this.emit({ type: 'planAbandoned', planId: this.currentPlan.id })
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const plan: Plan = {
|
|
734
|
+
id: randomUUID(),
|
|
735
|
+
goal,
|
|
736
|
+
steps: steps.map(s => ({ description: s, status: 'pending' as const })),
|
|
737
|
+
createdAt: this.clock.now(),
|
|
738
|
+
status: 'active',
|
|
739
|
+
}
|
|
740
|
+
this.currentPlan = plan
|
|
741
|
+
this.emit({ type: 'planCreated', planId: plan.id, goal, stepCount: steps.length })
|
|
742
|
+
|
|
743
|
+
if (this._state !== 'active') this.wake()
|
|
744
|
+
|
|
745
|
+
return plan
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** Mark a step as done (0-indexed). Returns updated plan or null. */
|
|
749
|
+
markStepDone(stepIndex: number): Plan | null {
|
|
750
|
+
if (!this.currentPlan || this.currentPlan.status !== 'active') return null
|
|
751
|
+
if (stepIndex < 0 || stepIndex >= this.currentPlan.steps.length) return null
|
|
752
|
+
|
|
753
|
+
this.currentPlan.steps[stepIndex].status = 'done'
|
|
754
|
+
this.emit({ type: 'planStepDone', planId: this.currentPlan.id, step: stepIndex })
|
|
755
|
+
|
|
756
|
+
const allDone = this.currentPlan.steps.every(s => s.status === 'done')
|
|
757
|
+
if (allDone) {
|
|
758
|
+
this.currentPlan.status = 'completed'
|
|
759
|
+
this.emit({ type: 'planCompleted', planId: this.currentPlan.id })
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return this.currentPlan
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** Abandon the current plan */
|
|
766
|
+
abandonPlan(): Plan | null {
|
|
767
|
+
if (!this.currentPlan || this.currentPlan.status !== 'active') return null
|
|
768
|
+
this.currentPlan.status = 'abandoned'
|
|
769
|
+
this.emit({ type: 'planAbandoned', planId: this.currentPlan.id })
|
|
770
|
+
this.cancelPlanNudge()
|
|
771
|
+
return this.currentPlan
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** Get the current plan */
|
|
775
|
+
getPlan(): Plan | null {
|
|
776
|
+
return this.currentPlan
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** Check if there is an active plan with pending steps */
|
|
780
|
+
private hasPendingPlanSteps(): boolean {
|
|
781
|
+
if (!this.currentPlan || this.currentPlan.status !== 'active') return false
|
|
782
|
+
return this.currentPlan.steps.some(s => s.status === 'pending')
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// === Plan Nudge Loop ===
|
|
786
|
+
|
|
787
|
+
/** Schedule next plan nudge (short delay to avoid spinning) */
|
|
788
|
+
private schedulePlanNudge(): void {
|
|
789
|
+
this.cancelPlanNudge()
|
|
790
|
+
if (this._state === 'sleep') return
|
|
791
|
+
if (this.config.autonomyMode !== 'plan') return
|
|
792
|
+
if (!this.hasPendingPlanSteps()) return
|
|
793
|
+
|
|
794
|
+
const delay = this.config.planNudgeDelayMs
|
|
795
|
+
const nextNudgeAt = this.clock.now() + delay
|
|
796
|
+
this.emit({ type: 'planNudgeScheduled', nextNudgeAt })
|
|
797
|
+
this.planNudgeTimer = this.clock.setTimeout(() => this.planNudge(), delay)
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** Cancel pending plan nudge */
|
|
801
|
+
private cancelPlanNudge(): void {
|
|
802
|
+
if (this.planNudgeTimer) {
|
|
803
|
+
this.clock.clearTimeout(this.planNudgeTimer)
|
|
804
|
+
this.planNudgeTimer = null
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** Check plan state and schedule next nudge if needed */
|
|
809
|
+
private checkPlanProgress(): void {
|
|
810
|
+
if (this.config.autonomyMode !== 'plan') return
|
|
811
|
+
if (this._state === 'sleep') return
|
|
812
|
+
if (this.hasPendingPlanSteps()) {
|
|
813
|
+
this.schedulePlanNudge()
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** Execute a plan nudge — send [CRAWD:PLAN] prompt to agent */
|
|
818
|
+
private async planNudge(): Promise<void> {
|
|
819
|
+
if (this._state === 'sleep') {
|
|
820
|
+
this.emit({ type: 'planNudgeExecuted', skipped: true, reason: 'sleeping' })
|
|
821
|
+
return
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (this._busy) {
|
|
825
|
+
this.logger.log('[Coordinator] Plan nudge skipped - busy')
|
|
826
|
+
this.emit({ type: 'planNudgeExecuted', skipped: true, reason: 'busy' })
|
|
827
|
+
this.schedulePlanNudge()
|
|
828
|
+
return
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (!this.hasPendingPlanSteps()) {
|
|
832
|
+
this.logger.log('[Coordinator] Plan nudge skipped - no pending steps')
|
|
833
|
+
this.emit({ type: 'planNudgeExecuted', skipped: true, reason: 'no pending steps' })
|
|
834
|
+
return
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (this._state === 'idle') {
|
|
838
|
+
const from = this._state
|
|
839
|
+
this._state = 'active'
|
|
840
|
+
this.emit({ type: 'stateChange', from, to: 'active' })
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
this.logger.log('[Coordinator] Plan nudge - sending plan prompt')
|
|
844
|
+
this.emit({ type: 'planNudgeExecuted', skipped: false })
|
|
845
|
+
this.resetActivity()
|
|
846
|
+
|
|
847
|
+
const prompt = this.buildPlanNudgePrompt()
|
|
848
|
+
|
|
849
|
+
this._busy = true
|
|
850
|
+
let noReply = false
|
|
851
|
+
const nudgeOp = this._gatewayQueue.then(async () => {
|
|
852
|
+
this._busy = true
|
|
853
|
+
try {
|
|
854
|
+
const replies = await this.triggerFn(prompt)
|
|
855
|
+
const agentReplies = replies.filter(r => !this.isApiError(r))
|
|
856
|
+
|
|
857
|
+
// Treat empty replies as NO_REPLY — gateway returns empty payloads
|
|
858
|
+
// when the agent responds with text-only (no tool calls)
|
|
859
|
+
if (agentReplies.length === 0 || agentReplies.some(r => r.trim().toUpperCase() === 'NO_REPLY')) {
|
|
860
|
+
noReply = true
|
|
861
|
+
} else if (!this.isCompliantReply(agentReplies)) {
|
|
862
|
+
const misaligned = agentReplies.filter(r => {
|
|
863
|
+
const t = r.trim().toUpperCase()
|
|
864
|
+
return t !== 'NO_REPLY' && t !== 'LIVESTREAM_REPLIED'
|
|
865
|
+
})
|
|
866
|
+
if (misaligned.length > 0) {
|
|
867
|
+
await this.sendMisalignment(misaligned)
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
} catch (err) {
|
|
871
|
+
this.logger.error('[Coordinator] Plan nudge failed:', err)
|
|
872
|
+
} finally {
|
|
873
|
+
this._busy = false
|
|
874
|
+
}
|
|
875
|
+
})
|
|
876
|
+
this._gatewayQueue = nudgeOp.catch(() => {})
|
|
877
|
+
|
|
878
|
+
try {
|
|
879
|
+
await nudgeOp
|
|
880
|
+
} catch {}
|
|
881
|
+
|
|
882
|
+
if (noReply) {
|
|
883
|
+
this.logger.log('[Coordinator] Agent sent NO_REPLY during plan nudge, going to sleep')
|
|
884
|
+
this.goSleep()
|
|
885
|
+
return
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
this.checkPlanProgress()
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/** Build the [CRAWD:PLAN] prompt with current plan progress */
|
|
892
|
+
private buildPlanNudgePrompt(): string {
|
|
893
|
+
if (!this.currentPlan || this.currentPlan.status !== 'active') {
|
|
894
|
+
return '[CRAWD:PLAN] No active plan. Create one using plan_set or respond with NO_REPLY to idle.'
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const lines: string[] = ['[CRAWD:PLAN] Continue your plan.', '']
|
|
898
|
+
lines.push(`Target: ${this.currentPlan.goal}`)
|
|
899
|
+
|
|
900
|
+
let nextStepIdx = -1
|
|
901
|
+
for (let i = 0; i < this.currentPlan.steps.length; i++) {
|
|
902
|
+
const step = this.currentPlan.steps[i]
|
|
903
|
+
const isDone = step.status === 'done'
|
|
904
|
+
if (!isDone && nextStepIdx === -1) nextStepIdx = i
|
|
905
|
+
const marker = isDone ? '[x]' : (nextStepIdx === i ? '[-]' : '[ ]')
|
|
906
|
+
const arrow = nextStepIdx === i ? ' <-- next' : ''
|
|
907
|
+
lines.push(`${marker} ${i}. ${step.description}${arrow}`)
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
lines.push('')
|
|
911
|
+
if (nextStepIdx >= 0) {
|
|
912
|
+
lines.push(`Work on step ${nextStepIdx}. Use plan_step_done when complete.`)
|
|
913
|
+
}
|
|
914
|
+
lines.push('You can use plan_abandon to drop this plan, or plan_set to replace it.')
|
|
915
|
+
lines.push('Respond with LIVESTREAM_REPLIED after speaking, or NO_REPLY if you have nothing to say.')
|
|
916
|
+
|
|
917
|
+
return lines.join('\n')
|
|
918
|
+
}
|
|
919
|
+
|
|
674
920
|
/** Start the periodic vibe loop */
|
|
675
921
|
private startVibeLoop(): void {
|
|
676
922
|
this.stopVibeLoop() // Clear any existing timer
|
|
@@ -719,9 +965,9 @@ export class Coordinator {
|
|
|
719
965
|
|
|
720
966
|
/** Schedule the next vibe action */
|
|
721
967
|
scheduleNextVibe(): void {
|
|
722
|
-
// Vibe while active or idle (not while sleeping)
|
|
723
968
|
if (this._state === 'sleep') return
|
|
724
|
-
|
|
969
|
+
// Only schedule vibes in vibe mode
|
|
970
|
+
if (this.config.autonomyMode !== 'vibe') return
|
|
725
971
|
|
|
726
972
|
const nextVibeAt = this.clock.now() + this.config.vibeIntervalMs
|
|
727
973
|
this.emit({ type: 'vibeScheduled', nextVibeAt })
|
|
@@ -767,7 +1013,9 @@ export class Coordinator {
|
|
|
767
1013
|
const replies = await this.triggerFn(this.config.vibePrompt)
|
|
768
1014
|
// Filter out API errors (429s, rate limits) — not agent responses
|
|
769
1015
|
const agentReplies = replies.filter(r => !this.isApiError(r))
|
|
770
|
-
|
|
1016
|
+
// Treat empty replies as NO_REPLY — gateway returns empty payloads
|
|
1017
|
+
// when the agent responds with text-only (no tool calls)
|
|
1018
|
+
if (agentReplies.length === 0 || agentReplies.some(r => r.trim().toUpperCase() === 'NO_REPLY')) {
|
|
771
1019
|
noReply = true
|
|
772
1020
|
} else if (!this.isCompliantReply(agentReplies)) {
|
|
773
1021
|
misaligned = agentReplies.filter(r => {
|
|
@@ -836,7 +1084,7 @@ export class Coordinator {
|
|
|
836
1084
|
// Leading edge: if no timer running, flush immediately and start cooldown
|
|
837
1085
|
if (!this.timer) {
|
|
838
1086
|
this.flush()
|
|
839
|
-
this.timer = this.clock.setTimeout(() => this.onCooldownEnd(),
|
|
1087
|
+
this.timer = this.clock.setTimeout(() => this.onCooldownEnd(), this.config.batchWindowMs)
|
|
840
1088
|
}
|
|
841
1089
|
// Otherwise, message is buffered and will be flushed when cooldown ends
|
|
842
1090
|
}
|
|
@@ -847,7 +1095,7 @@ export class Coordinator {
|
|
|
847
1095
|
// If messages accumulated during cooldown, flush them and restart cooldown
|
|
848
1096
|
if (this.buffer.length > 0) {
|
|
849
1097
|
this.flush()
|
|
850
|
-
this.timer = this.clock.setTimeout(() => this.onCooldownEnd(),
|
|
1098
|
+
this.timer = this.clock.setTimeout(() => this.onCooldownEnd(), this.config.batchWindowMs)
|
|
851
1099
|
}
|
|
852
1100
|
}
|
|
853
1101
|
|
|
@@ -915,6 +1163,7 @@ export class Coordinator {
|
|
|
915
1163
|
this.logger.error('[Coordinator] Failed to trigger agent:', err)
|
|
916
1164
|
} finally {
|
|
917
1165
|
this._busy = false
|
|
1166
|
+
this.checkPlanProgress()
|
|
918
1167
|
}
|
|
919
1168
|
}).catch(() => {})
|
|
920
1169
|
}
|
|
@@ -934,6 +1183,12 @@ export class Coordinator {
|
|
|
934
1183
|
? '\n(To reply to a specific message, prefix with its ID: [msgId] your reply)'
|
|
935
1184
|
: ''
|
|
936
1185
|
|
|
937
|
-
|
|
1186
|
+
// In plan mode with no active plan, instruct agent to create one
|
|
1187
|
+
let planInstruction = ''
|
|
1188
|
+
if (this.config.autonomyMode === 'plan' && !this.hasPendingPlanSteps()) {
|
|
1189
|
+
planInstruction = '\n\nYou are in plan mode. Create a plan using plan_set based on these messages or your own ideas, then start working on it.'
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
return `${header}\n${lines.join('\n')}${instruction}${planInstruction}`
|
|
938
1193
|
}
|
|
939
1194
|
}
|