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 +26 -0
- package/dist/src/cli.js +28 -0
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +8 -0
- package/dist/src/dreamer.d.ts +1 -1
- package/dist/src/dreamer.js +25 -7
- package/dist/src/entropy.d.ts +23 -0
- package/dist/src/entropy.js +188 -0
- package/dist/src/meta-loop.d.ts +23 -0
- package/dist/src/meta-loop.js +72 -0
- package/dist/src/reflection.js +2 -1
- package/dist/src/rhythm.d.ts +19 -0
- package/dist/src/rhythm.js +129 -0
- package/dist/src/types.d.ts +5 -0
- package/dist/src/waking.js +3 -0
- package/dist/test/entropy.test.d.ts +2 -0
- package/dist/test/entropy.test.js +128 -0
- package/dist/test/meta-loop.test.d.ts +2 -0
- package/dist/test/meta-loop.test.js +96 -0
- package/dist/test/rhythm.test.d.ts +2 -0
- package/dist/test/rhythm.test.js +102 -0
- package/openclaw.plugin.json +6 -1
- package/package.json +1 -1
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();
|
package/dist/src/config.d.ts
CHANGED
|
@@ -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() */
|
package/dist/src/config.js
CHANGED
|
@@ -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() */
|
package/dist/src/dreamer.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/src/dreamer.js
CHANGED
|
@@ -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:
|
|
61
|
-
?
|
|
62
|
-
:
|
|
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
|
package/dist/src/reflection.js
CHANGED
|
@@ -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
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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
|
package/dist/src/waking.js
CHANGED
|
@@ -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,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,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,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
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "openclawdreams",
|
|
3
3
|
"name": "openclawdreams",
|
|
4
4
|
"displayName": "ElectricSheep",
|
|
5
|
-
"version": "2.0
|
|
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
|
}
|