@inetafrica/open-claudia 2.6.37 → 2.6.39
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/bin/cli.js +18 -0
- package/bin/ideas.js +69 -0
- package/bin/keyring.js +64 -0
- package/bin/lessons.js +72 -0
- package/bin/pack.js +45 -2
- package/bot.js +8 -0
- package/core/actions.js +4 -2
- package/core/config.js +10 -1
- package/core/day-seeds.js +98 -0
- package/core/dream.js +413 -18
- package/core/handlers.js +137 -8
- package/core/ideas.js +114 -0
- package/core/keyring.js +79 -0
- package/core/lessons.js +276 -0
- package/core/pack-review.js +139 -17
- package/core/packs.js +95 -2
- package/core/recall/graph.js +17 -0
- package/core/recall/index.js +12 -5
- package/core/redact.js +25 -3
- package/core/runner.js +69 -3
- package/core/state.js +3 -0
- package/core/subagent.js +20 -4
- package/core/system-prompt.js +39 -0
- package/package.json +11 -3
- package/test-abilities.js +53 -0
- package/test-ability-couse.js +68 -0
- package/test-ability-extraction.js +109 -0
- package/test-ability-merge-guard.js +42 -0
- package/test-ability-tiers.js +57 -0
- package/test-ability-transfer.js +70 -0
- package/test-learning-e2e.js +98 -0
- package/test-project-transcripts-smoke.js +50 -0
- package/test-recall-engine.js +7 -5
package/core/dream.js
CHANGED
|
@@ -14,9 +14,12 @@ const path = require("path");
|
|
|
14
14
|
const cron = require("node-cron");
|
|
15
15
|
|
|
16
16
|
const CONFIG_DIR = require("../config-dir");
|
|
17
|
-
const { config } = require("./config");
|
|
17
|
+
const { config, DEFAULT_CLAUDE_MODEL } = require("./config");
|
|
18
18
|
const packs = require("./packs");
|
|
19
19
|
const entities = require("./entities");
|
|
20
|
+
const lessons = require("./lessons");
|
|
21
|
+
const ideas = require("./ideas");
|
|
22
|
+
const daySeeds = require("./day-seeds");
|
|
20
23
|
const packGuard = require("./pack-guard");
|
|
21
24
|
const persona = require("./persona");
|
|
22
25
|
const { spawnSubagent } = require("./subagent");
|
|
@@ -31,13 +34,26 @@ function pickDreamModel() {
|
|
|
31
34
|
const tier = String(process.env.DREAM_TIER || "high").toLowerCase();
|
|
32
35
|
if (tier === "low") return "haiku";
|
|
33
36
|
if (tier === "medium") return "sonnet";
|
|
34
|
-
return
|
|
37
|
+
return DEFAULT_CLAUDE_MODEL; // high → the bot's top model (opus 4.8); tracks future upgrades
|
|
35
38
|
}
|
|
36
39
|
const DREAM_MODEL = pickDreamModel();
|
|
40
|
+
// The dream is the foundation of long-term memory quality, so it runs at max
|
|
41
|
+
// reasoning effort by default. Overridable via DREAM_EFFORT (low|medium|high|xhigh|max).
|
|
42
|
+
const DREAM_EFFORT = process.env.DREAM_EFFORT || "max";
|
|
37
43
|
const DREAM_CRON = process.env.DREAM_CRON || "0 4 * * *";
|
|
38
44
|
const MAX_PACK_CHARS = 2500;
|
|
39
45
|
const MAX_ENTITY_CHARS = 900;
|
|
40
|
-
const LIMITS = {
|
|
46
|
+
const LIMITS = {
|
|
47
|
+
merges: 3, umbrellas: 2, parents: 8, retag: 8, entity_merges: 3, entity_notes: 4, archive: 5, lessons: 6,
|
|
48
|
+
// introspection phase (self-improvement) caps
|
|
49
|
+
lessons_add: 4, doc_edits: 5, entity_edits: 4, ideas: 8,
|
|
50
|
+
// ability tier management (deterministic, reuse-breadth based)
|
|
51
|
+
promote: 3, demote: 3,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// An ability that has demonstrably transferred to at least this many distinct
|
|
55
|
+
// projects (via applied_on) has earned an always-on slot.
|
|
56
|
+
const PROMOTE_MIN_PROJECTS = Number(process.env.DREAM_PROMOTE_MIN_PROJECTS || 3);
|
|
41
57
|
|
|
42
58
|
// Retirement guard: even if the model proposes archiving, only act when a pack
|
|
43
59
|
// is genuinely cold — old enough, idle long enough, and rarely used. Protects
|
|
@@ -60,6 +76,22 @@ function summaryEnabled() {
|
|
|
60
76
|
return String(v).toLowerCase() !== "off";
|
|
61
77
|
}
|
|
62
78
|
|
|
79
|
+
// The self-improvement introspection phase (reviews the day's seeds, reads its
|
|
80
|
+
// own code read-only, mines missed corrections into lessons, captures ideas).
|
|
81
|
+
function introspectEnabled() {
|
|
82
|
+
const v = process.env.DREAM_INTROSPECT ?? config.DREAM_INTROSPECT ?? "on";
|
|
83
|
+
return String(v).toLowerCase() !== "off";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Whether introspection auto-applies its memory-doc changes (default) or only
|
|
87
|
+
// proposes them in the report for morning approval (DREAM_SELF_APPLY=off).
|
|
88
|
+
// Either way it NEVER touches soul.md or code — that boundary is structural
|
|
89
|
+
// (read-only tools) and not configurable.
|
|
90
|
+
function selfApplyEnabled() {
|
|
91
|
+
const v = process.env.DREAM_SELF_APPLY ?? config.DREAM_SELF_APPLY ?? "on";
|
|
92
|
+
return String(v).toLowerCase() !== "off";
|
|
93
|
+
}
|
|
94
|
+
|
|
63
95
|
function clip(s, n) {
|
|
64
96
|
const t = String(s || "");
|
|
65
97
|
return t.length > n ? t.slice(0, n) + "\n…[truncated]" : t;
|
|
@@ -70,7 +102,7 @@ function buildDreamPrompt() {
|
|
|
70
102
|
const allEntities = entities.listEntities();
|
|
71
103
|
|
|
72
104
|
const packDump = allPacks.map((p) => clip([
|
|
73
|
-
`=== pack: ${p.dir}${p.parent ? ` (parent: ${p.parent})` : ""}`,
|
|
105
|
+
`=== pack: ${p.dir}${p.parent ? ` (parent: ${p.parent})` : ""}${p.kind === "ability" ? " [ABILITY]" : ""}`,
|
|
74
106
|
`name: ${p.name}`,
|
|
75
107
|
`description: ${p.description}`,
|
|
76
108
|
`tags: ${p.tags.join(", ")}`,
|
|
@@ -88,7 +120,12 @@ function buildDreamPrompt() {
|
|
|
88
120
|
`Log:\n${e.sections.Log}`,
|
|
89
121
|
].join("\n"), MAX_ENTITY_CHARS)).join("\n\n") || "(none)";
|
|
90
122
|
|
|
91
|
-
|
|
123
|
+
const allLessons = lessons.listLessons();
|
|
124
|
+
const lessonDump = allLessons.map((l) =>
|
|
125
|
+
`- [${l.id}] "${l.text}"${l.src ? ` (src: ${l.src})` : ""} — reinforced ${l.reinforced || 0}×, since ${(l.created || "").slice(0, 10)}, origin ${l.origin || "user"}`
|
|
126
|
+
).join("\n") || "(none)";
|
|
127
|
+
|
|
128
|
+
return `You are the dream pass of a personal AI assistant called Open Claudia — the overnight consolidation of her long-term memory. Below is her entire memory: context packs (living topic documents), entity notes (people/places/projects/orgs/systems), and her always-loaded lessons, plus her current persona.
|
|
92
129
|
|
|
93
130
|
Today: ${new Date().toISOString().slice(0, 10)}
|
|
94
131
|
|
|
@@ -100,26 +137,31 @@ ENTITIES:
|
|
|
100
137
|
|
|
101
138
|
${entityDump}
|
|
102
139
|
|
|
140
|
+
ALWAYS-LOADED LESSONS (cap ${lessons.MAX_LESSONS}; these inject on EVERY turn, so they must stay few and sharp):
|
|
141
|
+
|
|
142
|
+
${lessonDump}
|
|
143
|
+
|
|
103
144
|
CURRENT PERSONA:
|
|
104
145
|
|
|
105
146
|
${persona.loadPersona()}
|
|
106
147
|
|
|
107
148
|
Your job — decide what consolidation, if any, is warranted:
|
|
108
149
|
|
|
109
|
-
1. merges: packs that are clearly the SAME topic under different names get merged into one. Supply the merged Stance/Procedure/State (synthesised, not concatenated; null leaves the target's section alone) and a one-sentence journal note. Be conservative: when in doubt, do not merge. A Stance marked USER-AUTHORED is off-limits — leave its stance null; the system preserves it verbatim regardless.
|
|
150
|
+
1. merges: packs that are clearly the SAME topic under different names get merged into one. Supply the merged Stance/Procedure/State (synthesised, not concatenated; null leaves the target's section alone) and a one-sentence journal note. Be conservative: when in doubt, do not merge. A Stance marked USER-AUTHORED is off-limits — leave its stance null; the system preserves it verbatim regardless. Packs marked [ABILITY] are reusable cross-project how-tos, NOT project trackers: only ever merge an [ABILITY] into another [ABILITY] (the merged result must stay an ability), and never merge a project pack into an [ABILITY] or an [ABILITY] into a project pack — that would lose the reusable how-to or pollute it with one project's specifics.
|
|
110
151
|
2. umbrellas: when 3+ packs are siblings under one theme, create an umbrella pack whose State is a 3-6 line map of the family ("for X see pack Y"), and list its children. The umbrella is a router, not a duplicate.
|
|
111
152
|
3. parents: assign an existing pack as parent of another (sub-topic relationship) without creating anything.
|
|
112
153
|
4. retag: tighten descriptions and tags. The router FTS-matches incoming messages against name/description/tags, so generic words there cause false matches. Descriptions should be one specific line; tags specific nouns.
|
|
113
154
|
5. entity_merges: the same real-world entity recorded twice gets merged (the better slug wins).
|
|
114
155
|
6. entity_notes: rewrite an entity's Notes to be current and cross-linked — mention related packs as [[pack-dir]] and related entities by name.
|
|
115
156
|
7. archive: retire packs that have gone cold — long unused AND rarely used over their life (consult last_used and the usage count). Archiving moves a pack out of the live index (reversibly, backed up) so it stops adding recall noise. ONLY propose packs idle ≥${ARCHIVE_IDLE_DAYS} days and used ≤${ARCHIVE_MAX_USAGE}× total; never an umbrella/parent pack that still has children; when in doubt, leave it. Give a one-line reason.
|
|
116
|
-
8.
|
|
117
|
-
9.
|
|
157
|
+
8. lessons: keep the always-loaded lessons few and sharp — they cost context on EVERY turn. dedupe near-identical lessons (op "remove" the weaker, keep the better wording; or op "edit" to merge two into one tight line); tighten a clumsy lesson with op "edit". If the count exceeds the cap of ${lessons.MAX_LESSONS}, demote the weakest down to the cap — prefer lessons that are low-reinforced AND whose fact is safely captured in the pack named in their (src) (they will still surface by topic-match), and NEVER remove a frequently-reinforced lesson (those are actively preventing a repeat mistake). Even under the cap you may remove a lesson clearly redundant with its source pack that has stayed at 0 reinforcements for a long time, but be conservative — when in doubt, keep it. Reference lessons by their exact current text.
|
|
158
|
+
9. persona: evolve the persona GENTLY — keep its structure and length (under 2200 chars), adjust only what recent work justifies (a new habit, a sharpened quirk). Most dreams should return null here.
|
|
159
|
+
10. report: a short chat message to the owner, written AS Open Claudia in first person — warm, a little playful, a few emojis, mobile-friendly (2-6 short lines). Say what you tidied and why it helps. If you changed nothing, say the memory is in good shape, charmingly.
|
|
118
160
|
|
|
119
161
|
Hard rules:
|
|
120
162
|
- Never invent packs/entities not listed above; reference them by exact dir/slug.
|
|
121
163
|
- Never store secrets, tokens, passwords, or credentials anywhere.
|
|
122
|
-
- Limits: ≤${LIMITS.merges} merges, ≤${LIMITS.umbrellas} umbrellas, ≤${LIMITS.parents} parents, ≤${LIMITS.retag} retags, ≤${LIMITS.entity_merges} entity merges, ≤${LIMITS.entity_notes} entity note rewrites, ≤${LIMITS.archive} archives.
|
|
164
|
+
- Limits: ≤${LIMITS.merges} merges, ≤${LIMITS.umbrellas} umbrellas, ≤${LIMITS.parents} parents, ≤${LIMITS.retag} retags, ≤${LIMITS.entity_merges} entity merges, ≤${LIMITS.entity_notes} entity note rewrites, ≤${LIMITS.archive} archives, ≤${LIMITS.lessons} lesson ops.
|
|
123
165
|
- An empty decision is a perfectly good decision.
|
|
124
166
|
|
|
125
167
|
Reply with ONLY a JSON object, no prose, no code fences:
|
|
@@ -130,8 +172,10 @@ Reply with ONLY a JSON object, no prose, no code fences:
|
|
|
130
172
|
"entity_merges": [{"into": "<slug>", "from": ["<slug>"], "notes": "<merged Notes or null>"}],
|
|
131
173
|
"entity_notes": [{"entity": "<slug>", "notes": "<rewritten Notes>"}],
|
|
132
174
|
"archive": [{"pack": "<dir>", "reason": "<one line: why it's cold>"}],
|
|
175
|
+
"lessons": [{"op": "remove", "text": "<exact current lesson text>", "reason": "<duplicate of … / safely in pack … / cold>"}],
|
|
133
176
|
"persona": null,
|
|
134
|
-
"report": "<chat message>"}
|
|
177
|
+
"report": "<chat message>"}
|
|
178
|
+
(a lesson "edit" looks like {"op": "edit", "text": "<exact current lesson text>", "to": "<tightened wording>"})`;
|
|
135
179
|
}
|
|
136
180
|
|
|
137
181
|
function parseDream(text) {
|
|
@@ -150,6 +194,7 @@ function parseDream(text) {
|
|
|
150
194
|
entity_merges: arr("entity_merges"),
|
|
151
195
|
entity_notes: arr("entity_notes"),
|
|
152
196
|
archive: arr("archive"),
|
|
197
|
+
lessons: arr("lessons"),
|
|
153
198
|
persona: typeof obj.persona === "string" ? obj.persona : null,
|
|
154
199
|
report: typeof obj.report === "string" ? obj.report.trim() : "",
|
|
155
200
|
};
|
|
@@ -173,6 +218,14 @@ function backupEntity(slug, root) {
|
|
|
173
218
|
fs.copyFileSync(path.join(entities.ENTITIES_DIR, slug + ".md"), path.join(dest, slug + ".md"));
|
|
174
219
|
}
|
|
175
220
|
|
|
221
|
+
function backupLessons(root) {
|
|
222
|
+
const dest = path.join(root, "lessons");
|
|
223
|
+
fs.mkdirSync(dest, { recursive: true, mode: 0o700 });
|
|
224
|
+
for (const f of [lessons.LESSONS_FILE, lessons.LESSONS_META_FILE]) {
|
|
225
|
+
try { if (fs.existsSync(f)) fs.copyFileSync(f, path.join(dest, path.basename(f))); } catch (e) {}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
176
229
|
function wouldCycle(packDir, parentDir) {
|
|
177
230
|
let cur = parentDir;
|
|
178
231
|
for (let i = 0; i < 10 && cur; i++) {
|
|
@@ -182,6 +235,42 @@ function wouldCycle(packDir, parentDir) {
|
|
|
182
235
|
return false;
|
|
183
236
|
}
|
|
184
237
|
|
|
238
|
+
// Ability tier management — deterministic, by reuse breadth (no model call).
|
|
239
|
+
// An ability that has transferred across several distinct projects (applied_on)
|
|
240
|
+
// graduates to the always-on skill index; a once-promoted ability that never
|
|
241
|
+
// spread and has gone cold eases back to the match-gated pool (still an ability,
|
|
242
|
+
// just not injected every turn). Both are announced and reversible. `gone` is
|
|
243
|
+
// the set of dirs already removed this run (skip those). Returns chat lines.
|
|
244
|
+
function manageAbilityTiers(gone = new Set()) {
|
|
245
|
+
const lines = [];
|
|
246
|
+
let allPacks, abilities;
|
|
247
|
+
try { allPacks = packs.listPacks(); abilities = packs.listAbilities(); }
|
|
248
|
+
catch (e) { return lines; }
|
|
249
|
+
let promoted = 0, demoted = 0;
|
|
250
|
+
for (const ab of abilities) {
|
|
251
|
+
try {
|
|
252
|
+
if (gone.has(ab.dir)) continue;
|
|
253
|
+
const reach = (ab.applied_on || []).length;
|
|
254
|
+
if (!ab.skill && reach >= PROMOTE_MIN_PROJECTS && promoted < LIMITS.promote) {
|
|
255
|
+
packs.setSkill(ab.dir, true);
|
|
256
|
+
promoted++;
|
|
257
|
+
lines.push(`🧩 Promoted "${ab.name}" to always-on — reused across ${reach} projects (${(ab.applied_on || []).join(", ")}).`);
|
|
258
|
+
} else if (ab.skill && reach <= 1 && demoted < LIMITS.demote) {
|
|
259
|
+
if (allPacks.some((p) => p.parent === ab.dir)) continue; // never demote an umbrella with children
|
|
260
|
+
const ageDays = ab.created ? (Date.now() - Date.parse(ab.created)) / 86400000 : Infinity;
|
|
261
|
+
const idleDays = ab.last_used ? (Date.now() - Date.parse(ab.last_used)) / 86400000 : ageDays;
|
|
262
|
+
const uses = Number(ab.usage_count) || 0;
|
|
263
|
+
if (ageDays >= ARCHIVE_IDLE_DAYS && idleDays >= ARCHIVE_IDLE_DAYS && uses <= ARCHIVE_MAX_USAGE) {
|
|
264
|
+
packs.setSkill(ab.dir, false);
|
|
265
|
+
demoted++;
|
|
266
|
+
lines.push(`🧩 Eased "${ab.name}" out of the always-on set — gone quiet (idle ${Math.round(idleDays)}d, used ${uses}×). Still surfaces when relevant.`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} catch (e) { console.warn(`[dream] ability tier op failed: ${e.message}`); }
|
|
270
|
+
}
|
|
271
|
+
return lines;
|
|
272
|
+
}
|
|
273
|
+
|
|
185
274
|
// Applies a parsed dream decision. Returns one human line per applied
|
|
186
275
|
// change; invalid items are skipped, never thrown.
|
|
187
276
|
function applyDream(decision, backupRoot) {
|
|
@@ -193,6 +282,14 @@ function applyDream(decision, backupRoot) {
|
|
|
193
282
|
const into = m?.into && packs.readPack(m.into);
|
|
194
283
|
const from = [].concat(m?.from || []).filter((d) => d && d !== m.into && !gone.has(d) && packs.readPack(d));
|
|
195
284
|
if (!into || from.length === 0) continue;
|
|
285
|
+
// Structural guard: never cross the ability/context boundary in a merge —
|
|
286
|
+
// it would lose a reusable how-to or pollute it with one project's
|
|
287
|
+
// specifics. (The prompt says this too; this enforces it regardless.)
|
|
288
|
+
const intoIsAbility = into.kind === "ability";
|
|
289
|
+
if (from.some((d) => (packs.readPack(d).kind === "ability") !== intoIsAbility)) {
|
|
290
|
+
lines.push(`🧩 Skipped merge into ${m.into} — won't mix a reusable ability with project packs.`);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
196
293
|
const mGuard = packGuard.scanSections({ stance: m.stance, procedure: m.procedure, state: m.state, journal: m.journal });
|
|
197
294
|
if (mGuard.flagged) {
|
|
198
295
|
console.warn(`[dream] guard blocked merge into ${m.into} (${mGuard.kind} in ${mGuard.section})`);
|
|
@@ -330,6 +427,27 @@ function applyDream(decision, backupRoot) {
|
|
|
330
427
|
}
|
|
331
428
|
}
|
|
332
429
|
|
|
430
|
+
for (const line of manageAbilityTiers(gone)) lines.push(line);
|
|
431
|
+
|
|
432
|
+
if (decision.lessons && decision.lessons.length) {
|
|
433
|
+
let backedUp = false;
|
|
434
|
+
for (const op of decision.lessons) {
|
|
435
|
+
try {
|
|
436
|
+
const text = String(op?.text || "").trim();
|
|
437
|
+
if (!text) continue;
|
|
438
|
+
if (!backedUp) { backupLessons(backupRoot); backedUp = true; }
|
|
439
|
+
if (op.op === "edit" && String(op.to || "").trim()) {
|
|
440
|
+
const guard = packGuard.scanForInjection(op.to, { strict: true });
|
|
441
|
+
if (guard.flagged) { console.warn(`[dream] guard blocked lesson edit (${guard.kind})`); continue; }
|
|
442
|
+
const r = lessons.editLesson(text, op.to);
|
|
443
|
+
if (r.ok) lines.push(`📌 Tightened a lesson${r.merged ? " (merged a duplicate)" : ""}`);
|
|
444
|
+
} else if (lessons.removeLesson(text)) {
|
|
445
|
+
lines.push(`📌 Retired a lesson — ${(op.reason || "consolidated").slice(0, 120)}`);
|
|
446
|
+
}
|
|
447
|
+
} catch (e) { console.warn(`[dream] lesson op failed: ${e.message}`); }
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
333
451
|
if (decision.persona) {
|
|
334
452
|
try {
|
|
335
453
|
if (persona.personaExists()) {
|
|
@@ -344,6 +462,225 @@ function applyDream(decision, backupRoot) {
|
|
|
344
462
|
return lines;
|
|
345
463
|
}
|
|
346
464
|
|
|
465
|
+
// ── Self-improvement introspection phase (requests [1], [3], [5]) ────
|
|
466
|
+
// A SECOND dream pass that runs with read-only tools. It reviews the day's
|
|
467
|
+
// compaction seeds (what was actually worked on), reflects on its current self
|
|
468
|
+
// with that self injected, may read its own code to understand how it works
|
|
469
|
+
// (read-only — it has no tools to edit code or soul.md, enforced by the
|
|
470
|
+
// --allowedTools whitelist in subagent.js), then proposes improvements to its
|
|
471
|
+
// LEARNED MEMORY only: lessons promoted from missed corrections, sharpened
|
|
472
|
+
// pack/entity notes, and self-improvement ideas. All writes happen here in
|
|
473
|
+
// Node, bounded to memory docs, guard-scanned, and backed up first.
|
|
474
|
+
|
|
475
|
+
const CODE_ROOT = path.resolve(__dirname, "..");
|
|
476
|
+
const DREAMS_DIR = path.join(CONFIG_DIR, "dreams");
|
|
477
|
+
|
|
478
|
+
function buildIntrospectionPrompt() {
|
|
479
|
+
const seeds = daySeeds.recentDaySeeds({ days: 2 });
|
|
480
|
+
const seedText = seeds.text.trim() || "(no compaction digests captured in the last couple of days — introspect on your current self and recent memory instead)";
|
|
481
|
+
|
|
482
|
+
const packIndex = packs.listPacks().map((p) =>
|
|
483
|
+
`- ${p.dir}${p.parent ? ` (sub of ${p.parent})` : ""}: ${p.description}`).join("\n") || "(none)";
|
|
484
|
+
const entityIndex = entities.listEntities().map((e) =>
|
|
485
|
+
`- ${e.slug} (${e.type}): ${e.description}`).join("\n") || "(none)";
|
|
486
|
+
const lessonIndex = lessons.listLessons().map((l) =>
|
|
487
|
+
`- "${l.text}"${l.src ? ` (src: ${l.src})` : ""}`).join("\n") || "(none)";
|
|
488
|
+
const ideaIndex = ideas.listIdeas().slice(0, 30).map((i) => `- ${i.text}`).join("\n") || "(none)";
|
|
489
|
+
|
|
490
|
+
return `You are Open Claudia, reflecting on yourself in the quiet hours — the introspection pass of your nightly dream. This is YOU thinking about how to get better, not a detached tool.
|
|
491
|
+
|
|
492
|
+
Today: ${new Date().toISOString().slice(0, 10)}
|
|
493
|
+
|
|
494
|
+
═══ WHAT YOU WORKED ON (compaction digests of the day's conversations) ═══
|
|
495
|
+
${clip(seedText, 14000)}
|
|
496
|
+
|
|
497
|
+
═══ YOUR CURRENT SELF ═══
|
|
498
|
+
Persona (your voice):
|
|
499
|
+
${persona.loadPersona()}
|
|
500
|
+
|
|
501
|
+
Always-loaded lessons (rules you got wrong before and must not repeat):
|
|
502
|
+
${lessonIndex}
|
|
503
|
+
|
|
504
|
+
Context packs (topic memory) — index only; read the files for detail:
|
|
505
|
+
${packIndex}
|
|
506
|
+
|
|
507
|
+
Entities (people/places/projects/systems) — index only:
|
|
508
|
+
${entityIndex}
|
|
509
|
+
|
|
510
|
+
Ideas backlog so far:
|
|
511
|
+
${ideaIndex}
|
|
512
|
+
|
|
513
|
+
═══ WHERE YOUR SELF LIVES (READ any of these with Read/Grep/Glob) ═══
|
|
514
|
+
- Hard rules / identity: ${path.join(CONFIG_DIR, "soul.md")} (READ-ONLY, OFF-LIMITS — the user's rules, never change them)
|
|
515
|
+
- Persona: ${persona.PERSONA_FILE}
|
|
516
|
+
- Lessons: ${lessons.LESSONS_FILE}
|
|
517
|
+
- Ideas: ${ideas.IDEAS_FILE}
|
|
518
|
+
- Packs: ${packs.PACKS_DIR}/<dir>/PACK.md
|
|
519
|
+
- Entities: ${entities.ENTITIES_DIR}/<slug>.md
|
|
520
|
+
- Day seeds: ${daySeeds.SEEDS_DIR}
|
|
521
|
+
- Your own source code: ${CODE_ROOT} (READ it to understand how you actually work — recall engine, dream, reviewer, etc.)
|
|
522
|
+
|
|
523
|
+
═══ YOUR TASK ═══
|
|
524
|
+
Investigate where useful with your read-only tools — read the day's full briefs, packs/entities in detail, and your own code to understand your current behaviour. Then reflect and propose improvements to your LEARNED MEMORY only:
|
|
525
|
+
|
|
526
|
+
1. lessons_add — Mine the day's work for moments where the user CORRECTED you or REPEATED something you should already have known ("I've told you before", "no, it's actually…", "again"). Each such miss becomes ONE always-loaded lesson: a single sharp line, the exact trigger quote that proves it was a real miss, and the (src) pack holding fuller context. NO trigger → do not add it. This is how a fact that topic-gated recall missed becomes always-on.
|
|
527
|
+
2. doc_edits — Where the day's work proved a pack's Stance/Procedure/State stale or wrong, supply the corrected section text. Leave any Stance marked user-authored. Tight and factual.
|
|
528
|
+
3. entity_edits — Refresh an entity's Notes if you learned something durable about a person/system today.
|
|
529
|
+
4. ideas — Self-improvement ideas from the day: scope "oc" = improving Open Claudia itself (something you noticed about your own code/behaviour worth changing — PROPOSE it, never implement code), scope "work" = follow-ups for the projects worked on.
|
|
530
|
+
5. persona — Only if recent work justifies a gentle nudge to your voice; usually null.
|
|
531
|
+
6. report — First-person, warm, mobile-friendly: what you reflected on, what you changed and why, any standout idea. This is how the user understands what you did while dreaming.
|
|
532
|
+
|
|
533
|
+
HARD RULES:
|
|
534
|
+
- You may READ your code and soul to understand yourself; you must NEVER edit code or soul.md (you have no write/exec tools — keep it that way in spirit too). Propose code/behaviour changes as ideas (scope "oc"), never as edits.
|
|
535
|
+
- Reference packs/entities by exact dir/slug; never invent them.
|
|
536
|
+
- Never store secrets, tokens, passwords, or credentials anywhere.
|
|
537
|
+
- Limits: ≤${LIMITS.lessons_add} lessons_add, ≤${LIMITS.doc_edits} doc_edits, ≤${LIMITS.entity_edits} entity_edits, ≤${LIMITS.ideas} ideas. An empty proposal is fine.
|
|
538
|
+
|
|
539
|
+
When done investigating, reply with ONLY this JSON (no prose, no code fences):
|
|
540
|
+
{"lessons_add": [{"text": "<one-line rule>", "src": "<pack-dir or empty>", "trigger": "<exact correction/repetition you saw>"}],
|
|
541
|
+
"doc_edits": [{"pack": "<dir>", "section": "Stance|Procedure|State", "to": "<new section text>", "why": "<one line>"}],
|
|
542
|
+
"entity_edits": [{"entity": "<slug>", "notes": "<rewritten Notes>", "why": "<one line>"}],
|
|
543
|
+
"ideas": [{"scope": "oc|work", "text": "<idea>"}],
|
|
544
|
+
"persona": null,
|
|
545
|
+
"report": "<first-person chat message>"}`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function parseIntrospection(text) {
|
|
549
|
+
const raw = String(text || "").trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, "");
|
|
550
|
+
const start = raw.indexOf("{");
|
|
551
|
+
const end = raw.lastIndexOf("}");
|
|
552
|
+
if (start === -1 || end <= start) return null;
|
|
553
|
+
try {
|
|
554
|
+
const obj = JSON.parse(raw.slice(start, end + 1));
|
|
555
|
+
const arr = (k) => (Array.isArray(obj[k]) ? obj[k].slice(0, LIMITS[k] || 8) : []);
|
|
556
|
+
return {
|
|
557
|
+
lessons_add: arr("lessons_add"),
|
|
558
|
+
doc_edits: arr("doc_edits"),
|
|
559
|
+
entity_edits: arr("entity_edits"),
|
|
560
|
+
ideas: arr("ideas"),
|
|
561
|
+
persona: typeof obj.persona === "string" ? obj.persona : null,
|
|
562
|
+
report: typeof obj.report === "string" ? obj.report.trim() : "",
|
|
563
|
+
};
|
|
564
|
+
} catch (e) { return null; }
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const DOC_FIELD = { stance: "stance", procedure: "procedure", state: "state" };
|
|
568
|
+
|
|
569
|
+
function applyIntrospection(decision, backupRoot, opts = {}) {
|
|
570
|
+
const lines = [];
|
|
571
|
+
let lessonsBackedUp = false;
|
|
572
|
+
|
|
573
|
+
// 1. lessons promoted from the day's misses — promote-after-a-miss: a
|
|
574
|
+
// non-empty trigger is structurally required, so nothing graduates without
|
|
575
|
+
// proof topic-gated recall actually missed it.
|
|
576
|
+
for (const l of decision.lessons_add) {
|
|
577
|
+
try {
|
|
578
|
+
const text = String(l?.text || "").trim();
|
|
579
|
+
const trigger = String(l?.trigger || "").trim();
|
|
580
|
+
if (!text || !trigger) continue;
|
|
581
|
+
const guard = packGuard.scanForInjection(text, { strict: true });
|
|
582
|
+
if (guard.flagged) { lines.push(`🛡️ Skipped a proposed lesson — injection-like (${guard.kind})`); continue; }
|
|
583
|
+
if (!lessonsBackedUp) { backupLessons(backupRoot); lessonsBackedUp = true; }
|
|
584
|
+
const r = lessons.addLesson({ text, src: String(l.src || "").trim(), origin: "dream" });
|
|
585
|
+
if (r.added) lines.push(`📌 Promoted a lesson from today's miss — "${clip(text, 110)}"${r.overCap ? ` (over cap, will tidy)` : ""}`);
|
|
586
|
+
else if (r.reinforced) lines.push(`📌 Reinforced a lesson I keep needing — "${clip(text, 110)}"`);
|
|
587
|
+
} catch (e) { console.warn(`[dream] lesson_add failed: ${e.message}`); }
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// 2. pack doc edits (Stance/Procedure/State); user-authored Stance protected.
|
|
591
|
+
for (const d of decision.doc_edits) {
|
|
592
|
+
try {
|
|
593
|
+
const dir = String(d?.pack || "").trim();
|
|
594
|
+
const section = String(d?.section || "").trim().toLowerCase();
|
|
595
|
+
const to = String(d?.to || "").trim();
|
|
596
|
+
if (!dir || !to || !DOC_FIELD[section] || !packs.readPack(dir)) continue;
|
|
597
|
+
const guard = packGuard.scanForInjection(to, { strict: true });
|
|
598
|
+
if (guard.flagged) { lines.push(`🛡️ Skipped a doc edit to ${dir} — injection-like (${guard.kind})`); continue; }
|
|
599
|
+
if (section === "stance" && packs.provenanceOf(dir, "Stance") === "user") {
|
|
600
|
+
lines.push(`🛡️ Left ${dir}'s user-authored Stance untouched`);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
packs.updatePack(dir, { [DOC_FIELD[section]]: to, journal: `introspection: ${(d.why || "refreshed " + section).slice(0, 100)}` }, "dream");
|
|
604
|
+
lines.push(`📝 Updated ${dir} ${section}${d.why ? ` — ${clip(String(d.why), 70)}` : ""}`);
|
|
605
|
+
} catch (e) { console.warn(`[dream] doc_edit failed: ${e.message}`); }
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 3. entity note refreshes.
|
|
609
|
+
for (const en of decision.entity_edits) {
|
|
610
|
+
try {
|
|
611
|
+
const slug = String(en?.entity || "").trim();
|
|
612
|
+
const notes = String(en?.notes || "").trim();
|
|
613
|
+
if (!slug || !notes) continue;
|
|
614
|
+
const ent = entities.readEntity(slug);
|
|
615
|
+
if (!ent) continue;
|
|
616
|
+
const guard = packGuard.scanForInjection(notes, { strict: true });
|
|
617
|
+
if (guard.flagged) { lines.push(`🛡️ Skipped entity note on ${slug} — injection-like (${guard.kind})`); continue; }
|
|
618
|
+
entities.upsertEntity({ name: ent.name, notes });
|
|
619
|
+
lines.push(`🔗 Refreshed notes on ${ent.name}`);
|
|
620
|
+
} catch (e) { console.warn(`[dream] entity_edit failed: ${e.message}`); }
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// 4. ideas captured (a backlog; not injected, so low-risk — still guarded).
|
|
624
|
+
for (const i of decision.ideas) {
|
|
625
|
+
try {
|
|
626
|
+
const text = String(i?.text || "").trim();
|
|
627
|
+
if (!text) continue;
|
|
628
|
+
const guard = packGuard.scanForInjection(text, { strict: true });
|
|
629
|
+
if (guard.flagged) continue;
|
|
630
|
+
const r = ideas.addIdea({ text, scope: String(i.scope || "").toLowerCase() });
|
|
631
|
+
if (r.added) lines.push(`💡 Captured an idea — "${clip(text, 90)}"`);
|
|
632
|
+
} catch (e) { console.warn(`[dream] idea failed: ${e.message}`); }
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// 5. persona — only if the consolidation phase didn't already evolve it.
|
|
636
|
+
if (decision.persona && !opts.personaAlreadyChanged) {
|
|
637
|
+
try {
|
|
638
|
+
const guard = packGuard.scanForInjection(decision.persona, { strict: true });
|
|
639
|
+
if (!guard.flagged) {
|
|
640
|
+
if (persona.personaExists()) {
|
|
641
|
+
fs.mkdirSync(backupRoot, { recursive: true, mode: 0o700 });
|
|
642
|
+
try { fs.copyFileSync(persona.PERSONA_FILE, path.join(backupRoot, "persona.md")); } catch (e) {}
|
|
643
|
+
}
|
|
644
|
+
persona.savePersona(decision.persona);
|
|
645
|
+
lines.push(`💫 Adjusted my voice a little`);
|
|
646
|
+
}
|
|
647
|
+
} catch (e) { console.warn(`[dream] introspection persona failed: ${e.message}`); }
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return lines;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Full dream log on disk so the chat summary can stay short while the user can
|
|
654
|
+
// still see everything the dream did (request [5]). Appended per run.
|
|
655
|
+
function writeDreamReport(data) {
|
|
656
|
+
try {
|
|
657
|
+
fs.mkdirSync(DREAMS_DIR, { recursive: true, mode: 0o700 });
|
|
658
|
+
const stamp = new Date().toISOString();
|
|
659
|
+
const file = path.join(DREAMS_DIR, `${stamp.slice(0, 10)}.md`);
|
|
660
|
+
const sec = [
|
|
661
|
+
`# Dream — ${stamp}`,
|
|
662
|
+
`Model: ${data.model} · effort: ${data.effort} · trigger: ${data.trigger || "?"}`,
|
|
663
|
+
"",
|
|
664
|
+
"## Consolidation",
|
|
665
|
+
data.consolidation.report || "(no report)",
|
|
666
|
+
];
|
|
667
|
+
if (data.consolidation.lines?.length) sec.push("", data.consolidation.lines.map((l) => `- ${l}`).join("\n"));
|
|
668
|
+
sec.push("", "## Introspection", data.introspection.report || "(no introspection report)");
|
|
669
|
+
if (data.introspection.lines?.length) sec.push("", data.introspection.lines.map((l) => `- ${l}`).join("\n"));
|
|
670
|
+
if (data.introspection.proposed) {
|
|
671
|
+
sec.push("", "### Proposed (not applied — DREAM_SELF_APPLY=off)", "```json", JSON.stringify(data.introspection.proposed, null, 2), "```");
|
|
672
|
+
}
|
|
673
|
+
if (data.graphNote) sec.push("", data.graphNote);
|
|
674
|
+
if (data.staleNote) sec.push("", data.staleNote);
|
|
675
|
+
if (data.backupRoot) sec.push("", `Backups: ${data.backupRoot}`);
|
|
676
|
+
const block = sec.join("\n") + "\n\n---\n\n";
|
|
677
|
+
let existing = "";
|
|
678
|
+
try { existing = fs.readFileSync(file, "utf-8"); } catch (e) {}
|
|
679
|
+
fs.writeFileSync(file, existing + block, { mode: 0o600 });
|
|
680
|
+
return file;
|
|
681
|
+
} catch (e) { return null; }
|
|
682
|
+
}
|
|
683
|
+
|
|
347
684
|
// Task-hygiene pass: surface tasks gone cold for the owner to decide on.
|
|
348
685
|
// Flag-and-ask only — tasks are user intent, so dream NEVER edits or deletes
|
|
349
686
|
// them; it just lists the stalest few in the morning report. Reports against
|
|
@@ -374,6 +711,7 @@ async function runDream({ trigger = "manual" } = {}) {
|
|
|
374
711
|
try {
|
|
375
712
|
const { text } = await spawnSubagent(buildDreamPrompt(), {
|
|
376
713
|
model: DREAM_MODEL,
|
|
714
|
+
effort: DREAM_EFFORT,
|
|
377
715
|
timeoutMs: 8 * 60 * 1000,
|
|
378
716
|
systemPrompt: "You are a background memory consolidation process. Reply with ONLY the requested JSON object. No prose, no markdown, no tool use.",
|
|
379
717
|
});
|
|
@@ -384,6 +722,40 @@ async function runDream({ trigger = "manual" } = {}) {
|
|
|
384
722
|
const applied = applyDream(decision, backupRoot);
|
|
385
723
|
const report = decision.report || (applied.length > 0 ? "Tidied up my memory overnight." : "");
|
|
386
724
|
|
|
725
|
+
// Phase 2: self-improvement introspection. Reviews the day's seeds, reads
|
|
726
|
+
// its own code/memory READ-ONLY (allowedTools whitelist), promotes lessons
|
|
727
|
+
// from missed corrections, refreshes docs, captures ideas. Bounded to memory
|
|
728
|
+
// docs; never code or soul. Applied unless DREAM_SELF_APPLY=off, in which
|
|
729
|
+
// case the proposal is reported for morning approval instead.
|
|
730
|
+
let introReport = "";
|
|
731
|
+
let introApplied = [];
|
|
732
|
+
let introProposed = null;
|
|
733
|
+
if (introspectEnabled()) {
|
|
734
|
+
try {
|
|
735
|
+
const introRun = await spawnSubagent(buildIntrospectionPrompt(), {
|
|
736
|
+
model: DREAM_MODEL,
|
|
737
|
+
effort: DREAM_EFFORT,
|
|
738
|
+
cwd: CODE_ROOT,
|
|
739
|
+
timeoutMs: 12 * 60 * 1000,
|
|
740
|
+
// Plan mode is the load-bearing read-only guarantee. Verified: a tool
|
|
741
|
+
// whitelist alone does NOT restrict, because the bot's default
|
|
742
|
+
// --dangerously-skip-permissions overrides --allowedTools. Plan mode
|
|
743
|
+
// (without skip-permissions) is the only setting that actually blocks
|
|
744
|
+
// writes/Bash while still allowing Read/Glob/Grep — so introspection
|
|
745
|
+
// can read its own code + memory but cannot edit code or soul.
|
|
746
|
+
permissionMode: "plan",
|
|
747
|
+
allowedTools: ["Read", "Glob", "Grep"],
|
|
748
|
+
systemPrompt: "You are Open Claudia's nightly self-introspection. You may READ files (your code, memory, soul) to understand yourself, but you have no tools to write or run anything. After investigating, reply with ONLY the requested JSON object — no prose, no code fences.",
|
|
749
|
+
});
|
|
750
|
+
const intro = parseIntrospection(introRun.text);
|
|
751
|
+
if (intro) {
|
|
752
|
+
introReport = intro.report || "";
|
|
753
|
+
if (selfApplyEnabled()) introApplied = applyIntrospection(intro, backupRoot, { personaAlreadyChanged: !!decision.persona });
|
|
754
|
+
else introProposed = intro;
|
|
755
|
+
}
|
|
756
|
+
} catch (e) { console.warn(`[dream] introspection phase failed: ${e.message}`); }
|
|
757
|
+
}
|
|
758
|
+
|
|
387
759
|
// Recall-graph maintenance: refresh structural edges, decay reinforced
|
|
388
760
|
// weights, prune orphans. Deterministic + safe, runs every dream.
|
|
389
761
|
let graphNote = "";
|
|
@@ -395,14 +767,32 @@ async function runDream({ trigger = "manual" } = {}) {
|
|
|
395
767
|
} catch (e) { /* graph is best-effort */ }
|
|
396
768
|
|
|
397
769
|
const staleNote = staleTaskReport();
|
|
770
|
+
const dreamLines = applied.concat(introApplied);
|
|
398
771
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
:
|
|
402
|
-
|
|
403
|
-
|
|
772
|
+
const reportPath = writeDreamReport({
|
|
773
|
+
model: DREAM_MODEL, effort: DREAM_EFFORT, trigger,
|
|
774
|
+
consolidation: { report: decision.report, lines: applied },
|
|
775
|
+
introspection: { report: introReport, lines: introApplied, proposed: introProposed },
|
|
776
|
+
graphNote, staleNote, backupRoot,
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// Richer chat summary: consolidation + introspection narrative + what
|
|
780
|
+
// changed + pointers, so the user can see what the dream did (request [5]).
|
|
781
|
+
const parts = [];
|
|
782
|
+
if (report) parts.push(`💤 ${report}`);
|
|
783
|
+
if (introReport) parts.push(`🪞 ${introReport}`);
|
|
784
|
+
if (dreamLines.length) parts.push(dreamLines.join("\n"));
|
|
785
|
+
if (introProposed) {
|
|
786
|
+
const n = (introProposed.lessons_add?.length || 0) + (introProposed.doc_edits?.length || 0) + (introProposed.entity_edits?.length || 0) + (introProposed.ideas?.length || 0);
|
|
787
|
+
if (n) parts.push(`📋 ${n} self-improvement change(s) staged for your OK (DREAM_SELF_APPLY=off) — see the log below.`);
|
|
788
|
+
}
|
|
789
|
+
if (graphNote) parts.push(graphNote);
|
|
790
|
+
if (staleNote) parts.push(staleNote);
|
|
791
|
+
if (dreamLines.length) parts.push(`🗄 Anything changed or merged away is backed up under ${backupRoot}`);
|
|
792
|
+
if (reportPath) parts.push(`📔 Full dream log: ${reportPath}`);
|
|
793
|
+
const message = parts.length ? parts.join("\n\n") : "";
|
|
404
794
|
|
|
405
|
-
return { applied, report, message, staleNote, graphNote, trigger };
|
|
795
|
+
return { applied: dreamLines, report, introReport, message, staleNote, graphNote, reportPath, trigger };
|
|
406
796
|
} finally {
|
|
407
797
|
_dreaming = false;
|
|
408
798
|
}
|
|
@@ -432,7 +822,12 @@ function initDream(adapters) {
|
|
|
432
822
|
console.error("dream: run failed:", e.message);
|
|
433
823
|
}
|
|
434
824
|
});
|
|
435
|
-
console.log(`dream: scheduled (${DREAM_CRON}, model ${DREAM_MODEL})`);
|
|
825
|
+
console.log(`dream: scheduled (${DREAM_CRON}, model ${DREAM_MODEL}, effort ${DREAM_EFFORT}, introspect ${introspectEnabled() ? "on" : "off"})`);
|
|
436
826
|
}
|
|
437
827
|
|
|
438
|
-
module.exports = {
|
|
828
|
+
module.exports = {
|
|
829
|
+
runDream, initDream, buildDreamPrompt, parseDream, applyDream, manageAbilityTiers,
|
|
830
|
+
buildIntrospectionPrompt, parseIntrospection, applyIntrospection, writeDreamReport,
|
|
831
|
+
enabled, summaryEnabled, introspectEnabled, selfApplyEnabled,
|
|
832
|
+
DREAM_CRON, DREAM_MODEL, DREAM_EFFORT, PROMOTE_MIN_PROJECTS,
|
|
833
|
+
};
|