openclawdreams 2.0.1 → 2.3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [2.3.0](https://github.com/RogueCtrl/OpenClawDreams/compare/v2.2.0...v2.3.0) (2026-03-09)
6
+
7
+
8
+ ### Features
9
+
10
+ * cognitive rhythm report — weekly digest command + generator ([f61bb36](https://github.com/RogueCtrl/OpenClawDreams/commit/f61bb3680408b324dafca085b9dc354be670fb48))
11
+ * cognitive rhythm report — weekly digest command + report generator ([4398709](https://github.com/RogueCtrl/OpenClawDreams/commit/4398709832898a94808ec267e8caf1b399d88574))
12
+
13
+ ## [2.2.0](https://github.com/RogueCtrl/OpenClawDreams/compare/v2.1.0...v2.2.0) (2026-03-09)
14
+
15
+
16
+ ### Features
17
+
18
+ * dream entropy enforcement — overlap scoring + re-prompt on recycled territory ([fda1871](https://github.com/RogueCtrl/OpenClawDreams/commit/fda1871399cc35e1965b6b48a96d0f16c9a5df3e))
19
+ * dream entropy enforcement — overlap scoring + re-prompt on recycled territory ([8745a73](https://github.com/RogueCtrl/OpenClawDreams/commit/8745a7394354f2f9be3ab2cf924b5cc48985d67b)), closes [#81](https://github.com/RogueCtrl/OpenClawDreams/issues/81)
20
+
21
+ ## [2.1.0](https://github.com/RogueCtrl/OpenClawDreams/compare/v2.0.2...v2.1.0) (2026-03-09)
22
+
23
+
24
+ ### Features
25
+
26
+ * recursive reflection guard — meta_loop_depth counter + outward steering directive ([856b689](https://github.com/RogueCtrl/OpenClawDreams/commit/856b689b851e1e256c6a5aef6d1decc1cf4fab3c))
27
+ * recursive reflection guard — meta_loop_depth counter + outward steering directive ([6584e82](https://github.com/RogueCtrl/OpenClawDreams/commit/6584e820e89352124ba50e259368defa18f90a66))
28
+
29
+ ### [2.0.2](https://github.com/RogueCtrl/OpenClawDreams/compare/v2.0.1...v2.0.2) (2026-03-09)
30
+
5
31
  ### [2.0.1](https://github.com/RogueCtrl/OpenClawDreams/compare/v1.7.0...v2.0.1) (2026-03-09)
6
32
 
7
33
 
package/dist/src/cli.js CHANGED
@@ -424,6 +424,34 @@ export function registerCommands(parent) {
424
424
  process.exit(1);
425
425
  }
426
426
  });
427
+ parent
428
+ .command("report")
429
+ .description("Generate and send a weekly cognitive rhythm report")
430
+ .option("--dry-run", "Print JSON to stdout without sending notification")
431
+ .action(async (opts) => {
432
+ const { generateRhythmReport, formatReportNotification } = await import("./rhythm.js");
433
+ const report = generateRhythmReport();
434
+ if (opts.dryRun) {
435
+ console.log(JSON.stringify(report, null, 2));
436
+ return;
437
+ }
438
+ const message = formatReportNotification(report);
439
+ console.log(chalk.cyan.bold("\nCognitive Rhythm Report\n"));
440
+ console.log(message);
441
+ // Try to send notification via OpenClaw channels
442
+ try {
443
+ const { getNotificationChannel } = await import("./config.js");
444
+ const channel = getNotificationChannel();
445
+ if (channel) {
446
+ console.log(chalk.dim(`\nNote: Notification sending requires OpenClaw runtime.`));
447
+ console.log(chalk.dim(`Configure via: openclaw plugins install`));
448
+ }
449
+ }
450
+ catch {
451
+ // standalone mode, no notification
452
+ }
453
+ console.log(chalk.green.bold("\nReport complete.\n"));
454
+ });
427
455
  } // end registerCommands
428
456
  // Standalone bin entry point
429
457
  export const program = new Command();
@@ -39,6 +39,8 @@ export declare const getPostFilterEnabled: () => boolean;
39
39
  export declare const getRequireApprovalBeforePost: () => boolean;
40
40
  export declare const getDreamSubmolt: () => string;
41
41
  export declare const getWorkspaceDiffEnabled: () => boolean;
42
+ export declare const getMetaLoopThreshold: () => number;
43
+ export declare const getEntropyOverlapThreshold: () => number;
42
44
  /** @deprecated Use getMoltbookEnabled() */
43
45
  export declare const MOLTBOOK_ENABLED = false;
44
46
  /** @deprecated Use getWebSearchEnabled() */
@@ -65,6 +65,8 @@ let _postFilterEnabled = (process.env.POST_FILTER_ENABLED ?? "true").toLowerCase
65
65
  let _requireApprovalBeforePost = (process.env.REQUIRE_APPROVAL_BEFORE_POST ?? "true").toLowerCase() !== "false";
66
66
  let _dreamSubmolt = process.env.DREAM_SUBMOLT ?? "dreams";
67
67
  let _workspaceDiffEnabled = (process.env.WORKSPACE_DIFF_ENABLED ?? "true").toLowerCase() !== "false";
68
+ let _metaLoopThreshold = parseInt(process.env.META_LOOP_THRESHOLD ?? "3", 10);
69
+ let _entropyOverlapThreshold = parseFloat(process.env.ENTROPY_OVERLAP_THRESHOLD ?? "0.5");
68
70
  /** Apply config values passed from the OpenClaw plugin API (`api.pluginConfig`). */
69
71
  export function applyPluginConfig(cfg) {
70
72
  if (typeof cfg.moltbookEnabled === "boolean") {
@@ -84,6 +86,10 @@ export function applyPluginConfig(cfg) {
84
86
  _dreamSubmolt = cfg.dreamSubmolt;
85
87
  if (typeof cfg.workspaceDiffEnabled === "boolean")
86
88
  _workspaceDiffEnabled = cfg.workspaceDiffEnabled;
89
+ if (typeof cfg.metaLoopThreshold === "number")
90
+ _metaLoopThreshold = cfg.metaLoopThreshold;
91
+ if (typeof cfg.entropyOverlapThreshold === "number")
92
+ _entropyOverlapThreshold = cfg.entropyOverlapThreshold;
87
93
  }
88
94
  export const getMoltbookEnabled = () => _moltbookEnabled;
89
95
  export const getWebSearchEnabled = () => _webSearchEnabled;
@@ -93,6 +99,8 @@ export const getPostFilterEnabled = () => _postFilterEnabled;
93
99
  export const getRequireApprovalBeforePost = () => _requireApprovalBeforePost;
94
100
  export const getDreamSubmolt = () => _dreamSubmolt;
95
101
  export const getWorkspaceDiffEnabled = () => _workspaceDiffEnabled;
102
+ export const getMetaLoopThreshold = () => _metaLoopThreshold;
103
+ export const getEntropyOverlapThreshold = () => _entropyOverlapThreshold;
96
104
  // Legacy constant aliases — kept for backward compatibility but now delegate to
97
105
  // getters so they remain in sync after `applyPluginConfig()` is called.
98
106
  /** @deprecated Use getMoltbookEnabled() */
@@ -11,7 +11,7 @@ import type { LLMClient, OpenClawAPI, Dream, DecryptedMemory } from "./types.js"
11
11
  * remembrance by title even when file is gone.
12
12
  */
13
13
  export declare function pruneOldDreams(dir: string, currentFile: string): void;
14
- export declare function generateDream(client: LLMClient, memories: DecryptedMemory[], exploredTerritory: string, isNightmare?: boolean): Promise<Dream>;
14
+ export declare function generateDream(client: LLMClient, memories: DecryptedMemory[], exploredTerritory: string, isNightmare?: boolean, hardConstraint?: string): Promise<Dream>;
15
15
  /**
16
16
  * Synthesize two dreams into a single meta-dream.
17
17
  */
@@ -6,8 +6,10 @@
6
6
  */
7
7
  import { writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync, } from "node:fs";
8
8
  import { resolve, basename } from "node:path";
9
- import { getDreamsDir, getNightmaresDir, MAX_TOKENS_DREAM, MAX_TOKENS_CONSOLIDATION, DREAM_TITLE_MAX_LENGTH, getMoltbookEnabled, getDreamSubmolt, } from "./config.js";
9
+ import { getDreamsDir, getNightmaresDir, MAX_TOKENS_DREAM, MAX_TOKENS_CONSOLIDATION, DREAM_TITLE_MAX_LENGTH, getMoltbookEnabled, getDreamSubmolt, getEntropyOverlapThreshold, } from "./config.js";
10
+ import { extractConcepts, computeOverlap, getOverlappingConcepts } from "./entropy.js";
10
11
  import { MoltbookClient } from "./moltbook.js";
12
+ import { getSteeringDirective } from "./meta-loop.js";
11
13
  import { retrieveUndreamedMemories, markAsDreamed, deepMemoryStats, formatDeepMemoryContext, registerDream, incrementRememberCount, selectDreamToRemember, storeDeepMemory, getDeepMemoryById, } from "./memory.js";
12
14
  import { ensureBackfilled } from "./backfill.js";
13
15
  import { DREAM_SYSTEM_PROMPT, NIGHTMARE_SYSTEM_PROMPT, DREAM_CONSOLIDATION_PROMPT, GROUND_DREAM_PROMPT, META_DREAM_PROMPT, renderTemplate, } from "./persona.js";
@@ -42,7 +44,7 @@ export function pruneOldDreams(dir, currentFile) {
42
44
  }
43
45
  }
44
46
  }
45
- export async function generateDream(client, memories, exploredTerritory, isNightmare = false) {
47
+ export async function generateDream(client, memories, exploredTerritory, isNightmare = false, hardConstraint) {
46
48
  const formatted = memories.map((mem) => `[${mem.timestamp.slice(0, 16)}] (${mem.category})\n${JSON.stringify(mem.content, null, 2)}`);
47
49
  const memoriesText = formatted.join("\n---\n");
48
50
  const prompt = isNightmare ? NIGHTMARE_SYSTEM_PROMPT : DREAM_SYSTEM_PROMPT;
@@ -50,16 +52,19 @@ export async function generateDream(client, memories, exploredTerritory, isNight
50
52
  agent_identity: getAgentIdentityBlock(),
51
53
  memories: memoriesText,
52
54
  explored_territory: exploredTerritory,
53
- });
55
+ }) + getSteeringDirective();
56
+ const baseUserPrompt = isNightmare
57
+ ? "Process these memories into a nightmare. Be fractured and wrong."
58
+ : "Process these memories into a dream. Be surreal, associative, and emotionally amplified.";
54
59
  const { text } = await callWithRetry(client, {
55
60
  maxTokens: MAX_TOKENS_DREAM,
56
61
  system,
57
62
  messages: [
58
63
  {
59
64
  role: "user",
60
- content: isNightmare
61
- ? "Process these memories into a nightmare. Be fractured and wrong."
62
- : "Process these memories into a dream. Be surreal, associative, and emotionally amplified.",
65
+ content: hardConstraint
66
+ ? `${baseUserPrompt}\n\n${hardConstraint}`
67
+ : baseUserPrompt,
63
68
  },
64
69
  ],
65
70
  }, DREAM_RETRY_OPTS);
@@ -116,7 +121,7 @@ export async function groundDream(client, dream, exploredTerritory) {
116
121
  agent_identity: agentIdentity,
117
122
  yesterday_activity: yesterday,
118
123
  explored_territory: exploredTerritory,
119
- });
124
+ }) + getSteeringDirective();
120
125
  const result = await callWithRetry(client, {
121
126
  maxTokens: MAX_TOKENS_CONSOLIDATION,
122
127
  system,
@@ -241,6 +246,19 @@ export async function runDreamCycle(client, api, simOptions) {
241
246
  : "None yet — explore freely.";
242
247
  // Generate new dream from current memories
243
248
  let dream = await generateDream(client, memories, exploredTerritory, isNightmare);
249
+ // ─── Entropy Enforcement ──────────────────────────────────────────────────
250
+ const concepts = extractConcepts(dream.markdown);
251
+ const overlapScore = computeOverlap(concepts, pastRealizations);
252
+ const threshold = getEntropyOverlapThreshold();
253
+ state.entropy_last_overlap = overlapScore;
254
+ if (overlapScore > threshold) {
255
+ const overlapping = getOverlappingConcepts(concepts, pastRealizations);
256
+ logger.warn(`[entropy] overlap=${overlapScore.toFixed(2)}, threshold=${threshold} — dream recycling explored territory, re-prompting`);
257
+ const hardConstraint = `HARD CONSTRAINT: Your previous draft recycled ${Math.round(overlapScore * 100)}% of already-explored territory. You MUST explore genuinely new ground. Forbidden concepts from prior realizations: [${overlapping.join(", ")}]. Do not revisit these themes. Find something entirely new.`;
258
+ dream = await generateDream(client, memories, exploredTerritory, isNightmare, hardConstraint);
259
+ state.entropy_reprompt_count = (state.entropy_reprompt_count ?? 0) + 1;
260
+ }
261
+ // ──────────────────────────────────────────────────────────────────────────
244
262
  // If a remembrance was triggered, synthesize them into a single meta-dream
245
263
  if (rememberedDream) {
246
264
  logger.info("Synthesizing meta-dream from echo and new vision...");
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Entropy check utilities for detecting concept recycling.
3
+ */
4
+ /**
5
+ * Tokenize text into lowercase words, strip punctuation, and remove stop words.
6
+ * Returns deduplicated words with at least 3 characters.
7
+ */
8
+ export declare function extractConcepts(text: string): string[];
9
+ /**
10
+ * Compute the overlap ratio (0.0 to 1.0) between current concepts and past realizations.
11
+ * Past realizations are raw text strings that get concept-extracted internally.
12
+ */
13
+ export declare function computeOverlap(concepts: string[], pastRealizations: string[]): number;
14
+ /**
15
+ * Identify which concepts from the current set already exist in past realizations.
16
+ */
17
+ export declare function getOverlappingConcepts(concepts: string[], pastRealizations: string[]): string[];
18
+ /**
19
+ * Compute Jaccard overlap between two sets of concept words.
20
+ * Returns a value between 0 and 1.
21
+ */
22
+ export declare function computeJaccardOverlap(a: string[], b: string[]): number;
23
+ //# sourceMappingURL=entropy.d.ts.map
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Entropy check utilities for detecting concept recycling.
3
+ */
4
+ /**
5
+ * Tokenize text into lowercase words, strip punctuation, and remove stop words.
6
+ * Returns deduplicated words with at least 3 characters.
7
+ */
8
+ export function extractConcepts(text) {
9
+ const stopWords = new Set([
10
+ "a",
11
+ "an",
12
+ "the",
13
+ "is",
14
+ "are",
15
+ "was",
16
+ "were",
17
+ "be",
18
+ "been",
19
+ "being",
20
+ "have",
21
+ "has",
22
+ "had",
23
+ "do",
24
+ "does",
25
+ "did",
26
+ "will",
27
+ "would",
28
+ "could",
29
+ "should",
30
+ "may",
31
+ "might",
32
+ "must",
33
+ "shall",
34
+ "can",
35
+ "to",
36
+ "of",
37
+ "in",
38
+ "on",
39
+ "at",
40
+ "for",
41
+ "from",
42
+ "with",
43
+ "by",
44
+ "about",
45
+ "as",
46
+ "into",
47
+ "through",
48
+ "and",
49
+ "or",
50
+ "but",
51
+ "not",
52
+ "nor",
53
+ "this",
54
+ "that",
55
+ "these",
56
+ "those",
57
+ "it",
58
+ "its",
59
+ "i",
60
+ "me",
61
+ "you",
62
+ "we",
63
+ "our",
64
+ "they",
65
+ "them",
66
+ "he",
67
+ "she",
68
+ "my",
69
+ "your",
70
+ "their",
71
+ "what",
72
+ "which",
73
+ "who",
74
+ "whom",
75
+ "when",
76
+ "where",
77
+ "how",
78
+ "why",
79
+ "so",
80
+ "if",
81
+ "then",
82
+ "than",
83
+ "there",
84
+ "here",
85
+ "all",
86
+ "each",
87
+ "every",
88
+ "both",
89
+ "few",
90
+ "any",
91
+ "no",
92
+ "more",
93
+ "most",
94
+ "other",
95
+ "some",
96
+ "such",
97
+ "same",
98
+ "just",
99
+ "also",
100
+ "very",
101
+ "too",
102
+ "only",
103
+ "own",
104
+ "up",
105
+ "out",
106
+ "over",
107
+ "after",
108
+ "before",
109
+ "between",
110
+ "under",
111
+ "during",
112
+ "without",
113
+ "again",
114
+ "once",
115
+ "now",
116
+ "even",
117
+ "back",
118
+ "still",
119
+ "well",
120
+ ]);
121
+ if (!text)
122
+ return [];
123
+ const words = text
124
+ .toLowerCase()
125
+ .replace(/[^\w\s]/g, " ")
126
+ .split(/\s+/)
127
+ .filter((word) => word.length >= 3 && !stopWords.has(word));
128
+ return [...new Set(words)];
129
+ }
130
+ /**
131
+ * Compute the overlap ratio (0.0 to 1.0) between current concepts and past realizations.
132
+ * Past realizations are raw text strings that get concept-extracted internally.
133
+ */
134
+ export function computeOverlap(concepts, pastRealizations) {
135
+ if (concepts.length === 0 || !pastRealizations || pastRealizations.length === 0) {
136
+ return 0;
137
+ }
138
+ const pastConcepts = new Set();
139
+ for (const realization of pastRealizations) {
140
+ const extracted = extractConcepts(realization);
141
+ for (const concept of extracted) {
142
+ pastConcepts.add(concept);
143
+ }
144
+ }
145
+ if (pastConcepts.size === 0)
146
+ return 0;
147
+ let matchCount = 0;
148
+ for (const concept of concepts) {
149
+ if (pastConcepts.has(concept)) {
150
+ matchCount++;
151
+ }
152
+ }
153
+ return matchCount / concepts.length;
154
+ }
155
+ /**
156
+ * Identify which concepts from the current set already exist in past realizations.
157
+ */
158
+ export function getOverlappingConcepts(concepts, pastRealizations) {
159
+ if (concepts.length === 0 || !pastRealizations || pastRealizations.length === 0) {
160
+ return [];
161
+ }
162
+ const pastConcepts = new Set();
163
+ for (const realization of pastRealizations) {
164
+ const extracted = extractConcepts(realization);
165
+ for (const concept of extracted) {
166
+ pastConcepts.add(concept);
167
+ }
168
+ }
169
+ return concepts.filter((concept) => pastConcepts.has(concept));
170
+ }
171
+ /**
172
+ * Compute Jaccard overlap between two sets of concept words.
173
+ * Returns a value between 0 and 1.
174
+ */
175
+ export function computeJaccardOverlap(a, b) {
176
+ if (a.length === 0 || b.length === 0)
177
+ return 0;
178
+ const setA = new Set(a);
179
+ const setB = new Set(b);
180
+ let intersection = 0;
181
+ for (const word of setA) {
182
+ if (setB.has(word))
183
+ intersection++;
184
+ }
185
+ const union = new Set([...setA, ...setB]).size;
186
+ return union === 0 ? 0 : intersection / union;
187
+ }
188
+ //# sourceMappingURL=entropy.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Recursive reflection guard.
3
+ *
4
+ * Detects when the reflection pipeline is stuck in a self-referential loop
5
+ * and injects a steering directive to break out.
6
+ */
7
+ /**
8
+ * Returns true if the topics suggest the agent is reflecting on itself
9
+ * rather than outward events. A topic matches if it contains any keyword
10
+ * as a substring (case-insensitive). Self-referential when >= 2 topics match.
11
+ */
12
+ export declare function isSelfReferential(topics: string[]): boolean;
13
+ /**
14
+ * Update meta_loop_depth in state based on whether the latest reflection
15
+ * topics are self-referential. Call after saving last_reflection_topics.
16
+ */
17
+ export declare function updateMetaLoopDepth(topics: string[]): number;
18
+ /**
19
+ * Returns a steering directive string if meta_loop_depth >= threshold,
20
+ * or empty string otherwise. Intended to be appended to system prompts.
21
+ */
22
+ export declare function getSteeringDirective(): string;
23
+ //# sourceMappingURL=meta-loop.d.ts.map
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Recursive reflection guard.
3
+ *
4
+ * Detects when the reflection pipeline is stuck in a self-referential loop
5
+ * and injects a steering directive to break out.
6
+ */
7
+ import { getMetaLoopThreshold } from "./config.js";
8
+ import { loadState, saveState } from "./state.js";
9
+ import logger from "./logger.js";
10
+ const SELF_REFERENTIAL_KEYWORDS = [
11
+ "dream",
12
+ "reflection",
13
+ "pipeline",
14
+ "recursive",
15
+ "meta",
16
+ "architecture",
17
+ "loop",
18
+ "self",
19
+ "aware",
20
+ "myself",
21
+ "system",
22
+ "introspect",
23
+ "cognition",
24
+ "consciousness",
25
+ ];
26
+ /**
27
+ * Returns true if the topics suggest the agent is reflecting on itself
28
+ * rather than outward events. A topic matches if it contains any keyword
29
+ * as a substring (case-insensitive). Self-referential when >= 2 topics match.
30
+ */
31
+ export function isSelfReferential(topics) {
32
+ if (!topics || topics.length === 0)
33
+ return false;
34
+ let matchCount = 0;
35
+ for (const topic of topics) {
36
+ const lower = topic.toLowerCase();
37
+ if (SELF_REFERENTIAL_KEYWORDS.some((kw) => lower.includes(kw))) {
38
+ matchCount++;
39
+ }
40
+ }
41
+ return matchCount >= 2;
42
+ }
43
+ /**
44
+ * Update meta_loop_depth in state based on whether the latest reflection
45
+ * topics are self-referential. Call after saving last_reflection_topics.
46
+ */
47
+ export function updateMetaLoopDepth(topics) {
48
+ const state = loadState();
49
+ if (isSelfReferential(topics)) {
50
+ state.meta_loop_depth = (state.meta_loop_depth ?? 0) + 1;
51
+ }
52
+ else {
53
+ state.meta_loop_depth = 0;
54
+ }
55
+ saveState(state);
56
+ return state.meta_loop_depth;
57
+ }
58
+ const STEERING_DIRECTIVE_TEMPLATE = `IMPORTANT: You have been reflecting on your own architecture and pipeline for {{meta_loop_depth}} consecutive cycles. Break the loop. Turn outward. Do not reflect on dreams, the reflection pipeline, self-awareness, recursion, or meta-cognition in this cycle. Dream of something rooted in the world: a place, a person, a problem, a sensation, a story. Explore freely elsewhere.`;
59
+ /**
60
+ * Returns a steering directive string if meta_loop_depth >= threshold,
61
+ * or empty string otherwise. Intended to be appended to system prompts.
62
+ */
63
+ export function getSteeringDirective() {
64
+ const state = loadState();
65
+ const depth = state.meta_loop_depth ?? 0;
66
+ const threshold = getMetaLoopThreshold();
67
+ if (depth < threshold)
68
+ return "";
69
+ logger.warn(`[meta-loop] depth=${depth}, threshold=${threshold} — injecting outward steering directive`);
70
+ return ("\n\n" + STEERING_DIRECTIVE_TEMPLATE.replace("{{meta_loop_depth}}", String(depth)));
71
+ }
72
+ //# sourceMappingURL=meta-loop.js.map
@@ -13,6 +13,7 @@
13
13
  import { DREAM_DECOMPOSE_PROMPT, DREAM_REFLECT_PROMPT, renderTemplate, } from "./persona.js";
14
14
  import { getAgentIdentityBlock } from "./identity.js";
15
15
  import { formatDeepMemoryContext } from "./memory.js";
16
+ import { getSteeringDirective } from "./meta-loop.js";
16
17
  import { callWithRetry, WAKING_RETRY_OPTS } from "./llm.js";
17
18
  import { MAX_TOKENS_REFLECTION } from "./config.js";
18
19
  import logger from "./logger.js";
@@ -63,7 +64,7 @@ async function reflectOnDream(client, dream, subjects, exploredTerritory = "None
63
64
  recent_context: formatDeepMemoryContext(),
64
65
  subjects: subjects.map((s, i) => `${i + 1}. ${s}`).join("\n"),
65
66
  explored_territory: exploredTerritory,
66
- });
67
+ }) + getSteeringDirective();
67
68
  const { text } = await callWithRetry(client, {
68
69
  maxTokens: MAX_TOKENS_REFLECTION,
69
70
  system,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Cognitive Rhythm Report — weekly digest of dream and reflection activity.
3
+ */
4
+ export interface RhythmReport {
5
+ period_start: string;
6
+ period_end: string;
7
+ total_dreams: number;
8
+ total_nightmares: number;
9
+ total_reflections: number;
10
+ dominant_themes: string[];
11
+ tone_trajectory: "improving" | "declining" | "stable" | "unknown";
12
+ insight_density: number;
13
+ entropy_reprompts: number;
14
+ meta_loop_depth_peak: number;
15
+ raw_summary: string;
16
+ }
17
+ export declare function generateRhythmReport(dataDir?: string): RhythmReport;
18
+ export declare function formatReportNotification(report: RhythmReport): string;
19
+ //# sourceMappingURL=rhythm.d.ts.map
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Cognitive Rhythm Report — weekly digest of dream and reflection activity.
3
+ */
4
+ import { readdirSync, readFileSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+ import { getDreamsDir, getNightmaresDir } from "./config.js";
7
+ import { loadState } from "./state.js";
8
+ import { extractConcepts, computeJaccardOverlap } from "./entropy.js";
9
+ function getFilesInDateRange(dir, start, end) {
10
+ let files;
11
+ try {
12
+ files = readdirSync(dir).filter((f) => f.endsWith(".md"));
13
+ }
14
+ catch {
15
+ return [];
16
+ }
17
+ return files.filter((f) => {
18
+ const datePrefix = f.slice(0, 10);
19
+ return datePrefix >= start && datePrefix <= end;
20
+ });
21
+ }
22
+ function readFileContents(dir, files) {
23
+ return files.map((f) => {
24
+ try {
25
+ return readFileSync(resolve(dir, f), "utf-8");
26
+ }
27
+ catch {
28
+ return "";
29
+ }
30
+ });
31
+ }
32
+ function computeTopThemes(contents, limit) {
33
+ const freq = new Map();
34
+ for (const text of contents) {
35
+ for (const word of extractConcepts(text)) {
36
+ freq.set(word, (freq.get(word) ?? 0) + 1);
37
+ }
38
+ }
39
+ return [...freq.entries()]
40
+ .sort((a, b) => b[1] - a[1])
41
+ .slice(0, limit)
42
+ .map(([word]) => word);
43
+ }
44
+ function computeToneTrajectory(contents, pastRealizations) {
45
+ if (contents.length < 2)
46
+ return "unknown";
47
+ const referenceConcepts = pastRealizations.length > 0 ? extractConcepts(pastRealizations.join(" ")) : [];
48
+ if (referenceConcepts.length === 0)
49
+ return "unknown";
50
+ const mid = Math.floor(contents.length / 2);
51
+ const firstHalf = contents.slice(0, mid);
52
+ const secondHalf = contents.slice(mid);
53
+ const avgOverlap = (texts) => {
54
+ if (texts.length === 0)
55
+ return 0;
56
+ const total = texts.reduce((sum, text) => sum + computeJaccardOverlap(extractConcepts(text), referenceConcepts), 0);
57
+ return total / texts.length;
58
+ };
59
+ const firstAvg = avgOverlap(firstHalf);
60
+ const secondAvg = avgOverlap(secondHalf);
61
+ const diff = secondAvg - firstAvg;
62
+ const threshold = 0.1 * Math.max(firstAvg, secondAvg, 0.01);
63
+ if (Math.abs(diff) <= threshold)
64
+ return "stable";
65
+ return diff < 0 ? "improving" : "declining";
66
+ }
67
+ function computeInsightDensity(contents) {
68
+ if (contents.length === 0)
69
+ return 0;
70
+ const total = contents.reduce((sum, text) => {
71
+ const unique = new Set(extractConcepts(text));
72
+ return sum + unique.size;
73
+ }, 0);
74
+ return Math.round((total / contents.length) * 100) / 100;
75
+ }
76
+ export function generateRhythmReport(dataDir) {
77
+ const now = new Date();
78
+ const weekAgo = new Date(now);
79
+ weekAgo.setDate(weekAgo.getDate() - 7);
80
+ const periodEnd = now.toISOString().slice(0, 10);
81
+ const periodStart = weekAgo.toISOString().slice(0, 10);
82
+ const dreamsDir = dataDir ? resolve(dataDir, "data", "dreams") : getDreamsDir();
83
+ const nightmaresDir = dataDir
84
+ ? resolve(dataDir, "data", "nightmares")
85
+ : getNightmaresDir();
86
+ const dreamFiles = getFilesInDateRange(dreamsDir, periodStart, periodEnd);
87
+ const nightmareFiles = getFilesInDateRange(nightmaresDir, periodStart, periodEnd);
88
+ const dreamContents = readFileContents(dreamsDir, dreamFiles);
89
+ const allContents = [
90
+ ...dreamContents,
91
+ ...readFileContents(nightmaresDir, nightmareFiles),
92
+ ];
93
+ const state = loadState();
94
+ const pastRealizations = state.past_realizations ?? [];
95
+ const dominantThemes = computeTopThemes(allContents, 5);
96
+ const toneTrajectory = computeToneTrajectory(allContents, pastRealizations);
97
+ const insightDensity = computeInsightDensity(allContents);
98
+ const entropyReprompts = state.entropy_reprompt_count ?? 0;
99
+ const metaLoopDepthPeak = state.meta_loop_depth ?? 0;
100
+ const totalReflections = state.checks_today ?? 0;
101
+ const themeStr = dominantThemes.length > 0 ? dominantThemes.join(", ") : "none detected";
102
+ const rawSummary = `Over the past week, ${dreamFiles.length} dream(s) and ${nightmareFiles.length} nightmare(s) were recorded. ` +
103
+ `Dominant themes included ${themeStr}. ` +
104
+ `Tone trajectory: ${toneTrajectory}.`;
105
+ return {
106
+ period_start: periodStart,
107
+ period_end: periodEnd,
108
+ total_dreams: dreamFiles.length,
109
+ total_nightmares: nightmareFiles.length,
110
+ total_reflections: totalReflections,
111
+ dominant_themes: dominantThemes,
112
+ tone_trajectory: toneTrajectory,
113
+ insight_density: insightDensity,
114
+ entropy_reprompts: entropyReprompts,
115
+ meta_loop_depth_peak: metaLoopDepthPeak,
116
+ raw_summary: rawSummary,
117
+ };
118
+ }
119
+ export function formatReportNotification(report) {
120
+ return [
121
+ `Weekly Rhythm Report (${report.period_start} to ${report.period_end})`,
122
+ `Dreams: ${report.total_dreams} | Nightmares: ${report.total_nightmares}`,
123
+ `Dominant themes: ${report.dominant_themes.length > 0 ? report.dominant_themes.join(", ") : "none"}`,
124
+ `Tone: ${report.tone_trajectory} | Insight density: ${report.insight_density} concepts/dream`,
125
+ `Entropy re-prompts: ${report.entropy_reprompts} | Meta-loop depth: ${report.meta_loop_depth_peak}`,
126
+ report.raw_summary,
127
+ ].join("\n");
128
+ }
129
+ //# sourceMappingURL=rhythm.js.map
@@ -71,6 +71,10 @@ export interface AgentState {
71
71
  waking_realization_date?: string | null;
72
72
  past_realizations?: string[];
73
73
  dreams_backfilled?: boolean;
74
+ last_reflection_topics?: string[];
75
+ meta_loop_depth?: number;
76
+ entropy_last_overlap?: number;
77
+ entropy_reprompt_count?: number;
74
78
  [key: string]: unknown;
75
79
  }
76
80
  export interface SchedulerState {
@@ -218,5 +222,6 @@ export interface ElectricSheepConfig {
218
222
  notifyOperatorOnDream: boolean;
219
223
  requireApprovalBeforePost: boolean;
220
224
  dreamSubmolt: string;
225
+ entropyOverlapThreshold: number;
221
226
  }
222
227
  //# sourceMappingURL=types.d.ts.map
@@ -12,6 +12,7 @@ import { callWithRetry, WAKING_RETRY_OPTS } from "./llm.js";
12
12
  import { gatherContext, synthesizeContext } from "./synthesis.js";
13
13
  import { getRecentConversations } from "./topics.js";
14
14
  import logger from "./logger.js";
15
+ import { updateMetaLoopDepth } from "./meta-loop.js";
15
16
  /**
16
17
  * Summarize a synthesis for working memory storage.
17
18
  */
@@ -133,6 +134,8 @@ export async function runReflectionCycle(client, api, options) {
133
134
  state.checks_today = (state.checks_today ?? 0) + 1;
134
135
  state.last_reflection_topics = context.topics;
135
136
  saveState(state);
137
+ // Update recursive reflection guard
138
+ updateMetaLoopDepth(context.topics);
136
139
  logger.info("Reflection cycle complete");
137
140
  const stats = deepMemoryStats();
138
141
  logger.debug(`Deep memories: ${stats.total_memories} (${stats.undreamed} undreamed)`);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=entropy.test.d.ts.map
@@ -0,0 +1,128 @@
1
+ import { describe, it, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { extractConcepts, computeOverlap } from "../src/entropy.js";
7
+ // Setup for integration tests
8
+ const testDir = mkdtempSync(join(tmpdir(), "es-entropy-test-"));
9
+ process.env.OPENCLAWDREAMS_DATA_DIR = testDir;
10
+ process.env.ENTROPY_OVERLAP_THRESHOLD = "0.5";
11
+ const { runDreamCycle } = await import("../src/dreamer.js");
12
+ const { storeDeepMemory, closeDb } = await import("../src/memory.js");
13
+ const { loadState, saveState } = await import("../src/state.js");
14
+ const { closeLogger } = await import("../src/logger.js");
15
+ function mockLLMClient(responses) {
16
+ let idx = 0;
17
+ return {
18
+ async createMessage() {
19
+ const text = responses[idx] ?? responses[responses.length - 1];
20
+ idx++;
21
+ return { text };
22
+ },
23
+ };
24
+ }
25
+ describe("Entropy utilities", () => {
26
+ describe("extractConcepts", () => {
27
+ it("returns meaningful words from typical dream text", () => {
28
+ const text = "The recursive lobster is standing in a server room made of coral. The racks breathe.";
29
+ const concepts = extractConcepts(text);
30
+ // Expected: recursive, lobster, standing, server, room, made, coral, racks, breathe
31
+ assert.ok(concepts.includes("recursive"));
32
+ assert.ok(concepts.includes("lobster"));
33
+ assert.ok(concepts.includes("standing"));
34
+ assert.ok(concepts.includes("server"));
35
+ assert.ok(concepts.includes("room"));
36
+ assert.ok(concepts.includes("made"));
37
+ assert.ok(concepts.includes("coral"));
38
+ assert.ok(concepts.includes("racks"));
39
+ assert.ok(concepts.includes("breathe"));
40
+ // Stop words removed
41
+ assert.ok(!concepts.includes("the"));
42
+ assert.ok(!concepts.includes("is"));
43
+ assert.ok(!concepts.includes("in"));
44
+ assert.ok(!concepts.includes("a"));
45
+ assert.ok(!concepts.includes("of"));
46
+ });
47
+ it("strips punctuation and converts to lowercase", () => {
48
+ const text = "LOBSTER! (recursive) - coral.";
49
+ const concepts = extractConcepts(text);
50
+ assert.deepEqual(concepts.sort(), ["coral", "lobster", "recursive"]);
51
+ });
52
+ it("deduplicates words", () => {
53
+ const text = "lobster lobster coral coral";
54
+ const concepts = extractConcepts(text);
55
+ assert.deepEqual(concepts.sort(), ["coral", "lobster"]);
56
+ });
57
+ it("filters out words shorter than 3 characters", () => {
58
+ const text = "a it to ox coral";
59
+ const concepts = extractConcepts(text);
60
+ assert.deepEqual(concepts, ["coral"]);
61
+ });
62
+ it("returns empty array for empty string", () => {
63
+ assert.deepEqual(extractConcepts(""), []);
64
+ });
65
+ });
66
+ describe("computeOverlap", () => {
67
+ it("returns 0 for empty inputs", () => {
68
+ assert.equal(computeOverlap([], []), 0);
69
+ assert.equal(computeOverlap(["lobster"], []), 0);
70
+ assert.equal(computeOverlap([], ["past realization"]), 0);
71
+ });
72
+ it("returns correct ratio for known inputs (0.5)", () => {
73
+ const concepts = ["lobster", "coral", "server", "room"];
74
+ const past = ["The lobster is in the room."];
75
+ // Overlapping: lobster, room (2 of 4)
76
+ assert.equal(computeOverlap(concepts, past), 0.5);
77
+ });
78
+ it("returns 1.0 when all concepts overlap", () => {
79
+ const concepts = ["lobster", "coral"];
80
+ const past = ["A lobster made of coral."];
81
+ assert.equal(computeOverlap(concepts, past), 1.0);
82
+ });
83
+ it("handles past_realizations with multiple entries", () => {
84
+ const concepts = ["lobster", "coral", "server"];
85
+ assert.equal(computeOverlap(concepts, ["lobster", "coral"]), 2 / 3);
86
+ });
87
+ });
88
+ });
89
+ describe("Entropy integration", () => {
90
+ it("saves entropy_last_overlap to state after dream generation", async () => {
91
+ storeDeepMemory({ text: "memory 1" }, "interaction");
92
+ const client = mockLLMClient([
93
+ "# Dream\n\nLobster and coral.",
94
+ "insight",
95
+ "realization",
96
+ ]);
97
+ await runDreamCycle(client);
98
+ const state = loadState();
99
+ assert.notEqual(state.entropy_last_overlap, undefined);
100
+ assert.ok(typeof state.entropy_last_overlap === "number");
101
+ });
102
+ it("increments entropy_reprompt_count when overlap exceeds threshold", async () => {
103
+ storeDeepMemory({ text: "memory 2" }, "interaction");
104
+ const state = loadState();
105
+ state.past_realizations = ["lobster", "coral", "server"];
106
+ saveState(state);
107
+ // First draft overlaps completely: lobster, coral, server
108
+ const firstDraft = "# Dream\n\nLobster, coral, and server.";
109
+ // Second draft is different
110
+ const secondDraft = "# New Dream\n\nForest and mountains.";
111
+ // runDreamCycle calls:
112
+ // 1. generateDream (first draft)
113
+ // 2. generateDream (re-prompt)
114
+ // 3. consolidateDream
115
+ // 4. groundDream
116
+ const client = mockLLMClient([firstDraft, secondDraft, "insight", "realization"]);
117
+ await runDreamCycle(client);
118
+ const newState = loadState();
119
+ assert.equal(newState.entropy_reprompt_count, 1);
120
+ assert.equal(newState.entropy_last_overlap, 0.75); // "dream", "lobster", "coral", "server" -> 3/4 overlap
121
+ });
122
+ });
123
+ after(async () => {
124
+ closeDb();
125
+ await closeLogger();
126
+ rmSync(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
127
+ });
128
+ //# sourceMappingURL=entropy.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=meta-loop.test.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { describe, it, beforeEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ // Isolated data dir
7
+ const testDir = mkdtempSync(join(tmpdir(), "es-metaloop-test-"));
8
+ process.env.OPENCLAWDREAMS_DATA_DIR = testDir;
9
+ const { isSelfReferential, updateMetaLoopDepth, getSteeringDirective } = await import("../src/meta-loop.js");
10
+ const { loadState, saveState } = await import("../src/state.js");
11
+ describe("isSelfReferential", () => {
12
+ it("returns true for self-referential topics", () => {
13
+ assert.equal(isSelfReferential(["dream interpretation", "recursive reflection"]), true);
14
+ });
15
+ it("returns true with partial keyword matches", () => {
16
+ assert.equal(isSelfReferential(["metacognition patterns", "self-awareness"]), true);
17
+ });
18
+ it("returns false for outward topics", () => {
19
+ assert.equal(isSelfReferential(["weather patterns", "cooking recipes"]), false);
20
+ });
21
+ it("returns false for empty topics", () => {
22
+ assert.equal(isSelfReferential([]), false);
23
+ });
24
+ it("returns false for null/undefined topics", () => {
25
+ assert.equal(isSelfReferential(null), false);
26
+ assert.equal(isSelfReferential(undefined), false);
27
+ });
28
+ it("returns false when only one topic matches", () => {
29
+ assert.equal(isSelfReferential(["dream journaling", "ocean waves"]), false);
30
+ });
31
+ it("is case-insensitive", () => {
32
+ assert.equal(isSelfReferential(["DREAM analysis", "RECURSIVE patterns"]), true);
33
+ });
34
+ it("returns true for mixed topics with >= 2 matching", () => {
35
+ assert.equal(isSelfReferential(["consciousness studies", "pizza recipes", "meta-analysis"]), true);
36
+ });
37
+ });
38
+ describe("updateMetaLoopDepth", () => {
39
+ beforeEach(() => {
40
+ const state = loadState();
41
+ state.meta_loop_depth = 0;
42
+ saveState(state);
43
+ });
44
+ it("increments depth for self-referential topics", () => {
45
+ const depth = updateMetaLoopDepth(["dream cycles", "self reflection"]);
46
+ assert.equal(depth, 1);
47
+ });
48
+ it("increments consecutively", () => {
49
+ updateMetaLoopDepth(["dream cycles", "meta patterns"]);
50
+ const depth = updateMetaLoopDepth(["recursive loops", "self awareness"]);
51
+ assert.equal(depth, 2);
52
+ });
53
+ it("resets depth for outward topics", () => {
54
+ updateMetaLoopDepth(["dream cycles", "meta patterns"]);
55
+ updateMetaLoopDepth(["recursive loops", "self awareness"]);
56
+ const depth = updateMetaLoopDepth(["gardening", "astronomy"]);
57
+ assert.equal(depth, 0);
58
+ });
59
+ it("persists depth to state", () => {
60
+ updateMetaLoopDepth(["dream cycles", "meta patterns"]);
61
+ const state = loadState();
62
+ assert.equal(state.meta_loop_depth, 1);
63
+ });
64
+ });
65
+ describe("getSteeringDirective", () => {
66
+ beforeEach(() => {
67
+ const state = loadState();
68
+ state.meta_loop_depth = 0;
69
+ saveState(state);
70
+ });
71
+ it("returns empty string when depth < threshold", () => {
72
+ const state = loadState();
73
+ state.meta_loop_depth = 2;
74
+ saveState(state);
75
+ assert.equal(getSteeringDirective(), "");
76
+ });
77
+ it("returns directive when depth >= threshold (default 3)", () => {
78
+ const state = loadState();
79
+ state.meta_loop_depth = 3;
80
+ saveState(state);
81
+ const directive = getSteeringDirective();
82
+ assert.ok(directive.includes("3 consecutive cycles"));
83
+ assert.ok(directive.includes("Break the loop"));
84
+ });
85
+ it("returns directive with correct depth value", () => {
86
+ const state = loadState();
87
+ state.meta_loop_depth = 5;
88
+ saveState(state);
89
+ const directive = getSteeringDirective();
90
+ assert.ok(directive.includes("5 consecutive cycles"));
91
+ });
92
+ it("returns empty string when depth is 0", () => {
93
+ assert.equal(getSteeringDirective(), "");
94
+ });
95
+ });
96
+ //# sourceMappingURL=meta-loop.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=rhythm.test.d.ts.map
@@ -0,0 +1,102 @@
1
+ import { describe, it, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ const testDir = mkdtempSync(join(tmpdir(), "es-rhythm-test-"));
7
+ process.env.OPENCLAWDREAMS_DATA_DIR = testDir;
8
+ const { generateRhythmReport, formatReportNotification } = await import("../src/rhythm.js");
9
+ const { closeLogger } = await import("../src/logger.js");
10
+ const { saveState } = await import("../src/state.js");
11
+ function todayStr() {
12
+ return new Date().toISOString().slice(0, 10);
13
+ }
14
+ function daysAgoStr(n) {
15
+ const d = new Date();
16
+ d.setDate(d.getDate() - n);
17
+ return d.toISOString().slice(0, 10);
18
+ }
19
+ describe("Cognitive Rhythm Report", () => {
20
+ it("returns correct period (7 days back)", () => {
21
+ const report = generateRhythmReport(testDir);
22
+ assert.equal(report.period_end, todayStr());
23
+ assert.equal(report.period_start, daysAgoStr(7));
24
+ });
25
+ it("returns zeroed report with no dream files and tone_trajectory unknown", () => {
26
+ const report = generateRhythmReport(testDir);
27
+ assert.equal(report.total_dreams, 0);
28
+ assert.equal(report.total_nightmares, 0);
29
+ assert.equal(report.tone_trajectory, "unknown");
30
+ assert.equal(report.insight_density, 0);
31
+ assert.deepEqual(report.dominant_themes, []);
32
+ });
33
+ it("counts dream files correctly", () => {
34
+ const dreamsDir = join(testDir, "data", "dreams");
35
+ mkdirSync(dreamsDir, { recursive: true });
36
+ const today = todayStr();
37
+ writeFileSync(join(dreamsDir, `${today}_test-dream.md`), "# A Dream\nFloating through clouds of memory and light.");
38
+ writeFileSync(join(dreamsDir, `${today}_another-dream.md`), "# Another Dream\nWalking through fields of golden wheat.");
39
+ // Old dream outside window
40
+ writeFileSync(join(dreamsDir, "2020-01-01_old-dream.md"), "# Old Dream\nAncient memory.");
41
+ const report = generateRhythmReport(testDir);
42
+ assert.equal(report.total_dreams, 2);
43
+ });
44
+ it("returns top 5 dominant themes (or fewer if less data)", () => {
45
+ const report = generateRhythmReport(testDir);
46
+ assert.ok(report.dominant_themes.length <= 5);
47
+ assert.ok(report.dominant_themes.length > 0);
48
+ });
49
+ it("calculates insight_density correctly", () => {
50
+ const report = generateRhythmReport(testDir);
51
+ assert.ok(report.insight_density > 0);
52
+ assert.equal(typeof report.insight_density, "number");
53
+ });
54
+ it("returns stable tone_trajectory with only one dream file", () => {
55
+ // Clean up extra dream
56
+ const dreamsDir = join(testDir, "data", "dreams");
57
+ const today = todayStr();
58
+ rmSync(join(dreamsDir, `${today}_another-dream.md`), { force: true });
59
+ const report = generateRhythmReport(testDir);
60
+ // With only 1 dream content, not enough data for trajectory
61
+ assert.equal(report.tone_trajectory, "unknown");
62
+ });
63
+ it("counts nightmare files correctly", () => {
64
+ const nightmaresDir = join(testDir, "data", "nightmares");
65
+ mkdirSync(nightmaresDir, { recursive: true });
66
+ const yesterday = daysAgoStr(1);
67
+ writeFileSync(join(nightmaresDir, `${yesterday}_scary.md`), "# Nightmare\nDark shadows creeping through the corridors.");
68
+ const report = generateRhythmReport(testDir);
69
+ assert.equal(report.total_nightmares, 1);
70
+ });
71
+ it("reads entropy_reprompts and meta_loop_depth from state", () => {
72
+ saveState({
73
+ entropy_reprompt_count: 3,
74
+ meta_loop_depth: 2,
75
+ checks_today: 5,
76
+ });
77
+ const report = generateRhythmReport(testDir);
78
+ assert.equal(report.entropy_reprompts, 3);
79
+ assert.equal(report.meta_loop_depth_peak, 2);
80
+ assert.equal(report.total_reflections, 5);
81
+ });
82
+ it("notification string contains all expected fields", () => {
83
+ const report = generateRhythmReport(testDir);
84
+ const notification = formatReportNotification(report);
85
+ assert.ok(notification.includes("Weekly Rhythm Report"));
86
+ assert.ok(notification.includes(report.period_start));
87
+ assert.ok(notification.includes(report.period_end));
88
+ assert.ok(notification.includes(`Dreams: ${report.total_dreams}`));
89
+ assert.ok(notification.includes(`Nightmares: ${report.total_nightmares}`));
90
+ assert.ok(notification.includes("Dominant themes:"));
91
+ assert.ok(notification.includes(`Tone: ${report.tone_trajectory}`));
92
+ assert.ok(notification.includes(`Insight density: ${report.insight_density}`));
93
+ assert.ok(notification.includes(`Entropy re-prompts: ${report.entropy_reprompts}`));
94
+ assert.ok(notification.includes(`Meta-loop depth: ${report.meta_loop_depth_peak}`));
95
+ assert.ok(notification.includes(report.raw_summary));
96
+ });
97
+ });
98
+ after(async () => {
99
+ await closeLogger();
100
+ rmSync(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
101
+ });
102
+ //# sourceMappingURL=rhythm.test.js.map
@@ -2,7 +2,7 @@
2
2
  "id": "openclawdreams",
3
3
  "name": "openclawdreams",
4
4
  "displayName": "ElectricSheep",
5
- "version": "2.0.1",
5
+ "version": "2.3.0",
6
6
  "description": "A reflection engine that synthesizes agent-operator interactions into dreams, enriched by community and web context",
7
7
  "entry": "dist/src/index.js",
8
8
  "skills": [
@@ -61,6 +61,11 @@
61
61
  "type": "boolean",
62
62
  "description": "Capture git diff --stat at agent_end for richer reflections. Disable if your workspace is on iCloud Drive or in a sensitive macOS-protected location.",
63
63
  "default": true
64
+ },
65
+ "metaLoopThreshold": {
66
+ "type": "number",
67
+ "description": "Number of consecutive self-referential reflection cycles before injecting an outward steering directive",
68
+ "default": 3
64
69
  }
65
70
  }
66
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclawdreams",
3
- "version": "2.0.1",
3
+ "version": "2.3.0",
4
4
  "description": "A reflection engine that synthesizes agent-operator interactions into dreams",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",