@@ -31,6 +31,17 @@ export function parseScheduleWindow(
3131 return { startMinutes, endMinutes } ;
3232}
3333
34+ /**
35+ * Check if a dream timestamp falls within today's schedule window.
36+ * Uses a simple heuristic: if the dream ran less than 12 hours ago,
37+ * it's considered "from the current window" and the project should not be re-enqueued.
38+ * This prevents the dreamer's own memory updates from triggering re-enqueuing.
39+ */
40+ function isDreamFromCurrentWindow ( lastDreamAtMs : number , now : Date ) : boolean {
41+ const twelveHoursMs = 12 * 60 * 60 * 1000 ;
42+ return now . getTime ( ) - lastDreamAtMs < twelveHoursMs ;
43+ }
44+
3445/** Check if the current time is inside the schedule window. Handles overnight windows (e.g. 23:00-05:00). */
3546export function isInScheduleWindow ( schedule : string , now : Date = new Date ( ) ) : boolean {
3647 const window = parseScheduleWindow ( schedule ) ;
@@ -60,12 +71,20 @@ export function findProjectsNeedingDream(db: Database): string[] {
6071 . all ( ) ;
6172
6273 const projects : string [ ] = [ ] ;
74+ const now = new Date ( ) ;
6375 for ( const row of projectRows ) {
6476 const lastDreamAtStr = getDreamState ( db , `last_dream_at:${ row . project_path } ` ) ;
6577 // Fall back to global key for migration from old single-key format
6678 const fallbackStr = ! lastDreamAtStr ? getDreamState ( db , "last_dream_at" ) : null ;
6779 const lastDreamAt = Number ( lastDreamAtStr ?? fallbackStr ?? "0" ) || 0 ;
6880
81+ // Skip if a dream already ran in the current schedule window.
82+ // This prevents re-enqueuing because dreamer's own memory updates
83+ // (consolidate, verify, improve, archive) set updated_at > last_dream_at.
84+ if ( lastDreamAt > 0 && isDreamFromCurrentWindow ( lastDreamAt , now ) ) {
85+ continue ;
86+ }
87+
6988 const updatedMemories = db
7089 . query < { cnt : number } , [ string , number ] > (
7190 `SELECT COUNT(*) as cnt FROM memories
0 commit comments