-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdream.ts
More file actions
225 lines (196 loc) · 7.31 KB
/
Copy pathdream.ts
File metadata and controls
225 lines (196 loc) · 7.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
/**
* Auto-dream: gated automatic memory consolidation for the Mem0 OpenCode plugin.
*
* Ported from the (stable) pi-agent plugin's dream module and adapted to
* OpenCode's hook model. When the cheap gates (time since last consolidation +
* sessions since) and the memory-count gate all pass, the plugin injects the
* DREAM_PROTOCOL into the agent's context so it consolidates memories (merge
* duplicates, drop stale/sensitive entries, rewrite vague ones) before
* answering. A filesystem lock prevents concurrent sessions from dreaming at
* once, and completion is recorded so it won't re-trigger until the next cycle.
*
* State + lock live in ~/.mem0/ alongside settings.json. Opt out with
* MEM0_DREAM=false, or tune via the `dream` block in ~/.mem0/settings.json.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
import { join } from "node:path";
export interface DreamConfig {
enabled: boolean;
auto: boolean;
minHours: number;
minSessions: number;
minMemories: number;
}
interface DreamState {
lastConsolidatedAt: number;
sessionsSince: number;
lastSessionId: string | null;
}
interface DreamLock {
pid: number;
startedAt: number;
}
const LOCK_STALE_MS = 60 * 60 * 1000;
export const DREAM_DEFAULTS: DreamConfig = {
enabled: true,
auto: true,
minHours: 24,
minSessions: 5,
minMemories: 20,
};
function statePath(stateDir: string): string {
return join(stateDir, "mem0-dream-state.json");
}
function lockPath(stateDir: string): string {
return join(stateDir, "mem0-dream.lock");
}
function ensureDir(dir: string): void {
try {
mkdirSync(dir, { recursive: true });
} catch {
/* exists */
}
}
function readState(stateDir: string): DreamState {
try {
return JSON.parse(readFileSync(statePath(stateDir), "utf-8")) as DreamState;
} catch {
return { lastConsolidatedAt: 0, sessionsSince: 0, lastSessionId: null };
}
}
function writeState(stateDir: string, state: DreamState): void {
ensureDir(stateDir);
writeFileSync(statePath(stateDir), JSON.stringify(state, null, 2));
}
/**
* Load dream config from ~/.mem0/settings.json (`dream` block), applying
* defaults. MEM0_DREAM=false (or 0/no/off) force-disables regardless.
*/
export function loadDreamConfig(settingsDir: string): DreamConfig {
let envEnabled: boolean | undefined;
const env = process.env.MEM0_DREAM;
if (env !== undefined) {
const s = env.toLowerCase();
envEnabled = s !== "false" && s !== "0" && s !== "no" && s !== "off";
}
let cfg: DreamConfig = { ...DREAM_DEFAULTS };
try {
const sp = join(settingsDir, "settings.json");
if (existsSync(sp)) {
const settings = JSON.parse(readFileSync(sp, "utf-8"));
const d = settings?.dream;
if (d && typeof d === "object") {
cfg = {
enabled: typeof d.enabled === "boolean" ? d.enabled : cfg.enabled,
auto: typeof d.auto === "boolean" ? d.auto : cfg.auto,
minHours: typeof d.minHours === "number" ? d.minHours : cfg.minHours,
minSessions: typeof d.minSessions === "number" ? d.minSessions : cfg.minSessions,
minMemories: typeof d.minMemories === "number" ? d.minMemories : cfg.minMemories,
};
}
}
} catch {
/* defaults */
}
if (envEnabled !== undefined) cfg.enabled = envEnabled;
return cfg;
}
/** Count a new session toward the dream gate (once per distinct sessionId). */
export function incrementSessionCount(stateDir: string, sessionId: string): void {
const state = readState(stateDir);
if (state.lastSessionId !== sessionId) {
state.sessionsSince++;
state.lastSessionId = sessionId;
writeState(stateDir, state);
}
}
/** Cheap gates that don't need an API call: time since last + sessions since. */
export function checkCheapGates(
stateDir: string,
config: Partial<DreamConfig>,
): { proceed: boolean; reason?: string } {
const minHours = config.minHours ?? DREAM_DEFAULTS.minHours;
const minSessions = config.minSessions ?? DREAM_DEFAULTS.minSessions;
const state = readState(stateDir);
const hoursSince = (Date.now() - state.lastConsolidatedAt) / 3_600_000;
if (hoursSince < minHours) {
return { proceed: false, reason: `time: ${hoursSince.toFixed(1)}h < ${minHours}h` };
}
if (state.sessionsSince < minSessions) {
return { proceed: false, reason: `sessions: ${state.sessionsSince} < ${minSessions}` };
}
return { proceed: true };
}
/** Memory-count gate (uses the count already fetched at session init). */
export function checkMemoryGate(
memoryCount: number,
config: Partial<DreamConfig>,
): { pass: boolean; reason?: string } {
const minMemories = config.minMemories ?? DREAM_DEFAULTS.minMemories;
if (memoryCount < minMemories) {
return { pass: false, reason: `memories: ${memoryCount} < ${minMemories}` };
}
return { pass: true };
}
/** Acquire an exclusive dream lock (stale locks > 1h are reclaimed). */
export function acquireDreamLock(stateDir: string): boolean {
ensureDir(stateDir);
const lp = lockPath(stateDir);
try {
const lock = JSON.parse(readFileSync(lp, "utf-8")) as DreamLock;
if (Date.now() - lock.startedAt < LOCK_STALE_MS) {
return false;
}
try {
unlinkSync(lp);
} catch {
/* race ok */
}
} catch {
/* no lock file */
}
const lock: DreamLock = { pid: process.pid, startedAt: Date.now() };
try {
writeFileSync(lp, JSON.stringify(lock), { flag: "wx" });
return true;
} catch {
return false;
}
}
export function releaseDreamLock(stateDir: string): void {
try {
unlinkSync(lockPath(stateDir));
} catch {
/* already gone */
}
}
/** Reset the gates after a successful consolidation. */
export function recordDreamCompletion(stateDir: string): void {
const state = readState(stateDir);
state.lastConsolidatedAt = Date.now();
state.sessionsSince = 0;
state.lastSessionId = null;
writeState(stateDir, state);
}
/**
* Consolidation protocol injected into the agent context when a dream is
* triggered. Uses the plugin's native OpenCode memory tools (get_memories /
* add_memory / delete_memory) rather than an MCP tool.
*/
export const DREAM_PROTOCOL = `<mem0-dream>
You are running memory consolidation. Complete these steps using the mem0 memory tools (get_memories, add_memory, delete_memory):
1. ORIENT — Call get_memories to list all memories. Count by category. Note oldest/newest.
2. GATHER TARGETS — Review each memory. Classify as:
- DELETE: sensitive information (API keys, passwords, tokens), expired/stale entries, noise, redundant operational details
- MERGE: near-duplicates (same fact stated differently). Keep the better-worded one, delete the other.
- REWRITE: vague, first-person, or poorly-categorized entries. add_memory with improved text, then delete_memory the old one.
- KEEP: everything else.
Skip any memory starting with "[PINNED]".
3. CONSOLIDATE — Execute the changes:
- Delete stale/duplicate entries with delete_memory
- For merges: add_memory the merged text, delete_memory both originals
- For rewrites: add_memory the improved version, delete_memory the original
4. REPORT — Summarize: how many reviewed, deleted, merged, rewritten, final count.
Quality targets: zero sensitive data stored, zero duplicates, all entries are atomic (one fact each), 15-50 words each.
After consolidation, respond to the user's message normally.
</mem0-dream>`;