72b84028beb3b89c80e1e4417fa627b485df6c8ce67cbd83acd51a9266e9734d1 package depend on this
| Package | Type | Relationship |
|---|---|---|
| treeos-intelligence v1.0.2 | bundle | includes |
breath:exhale1/**
2 * Compare-Inner (Layer 3)
3 *
4 * Compares this week's reflection themes to last week's.
5 * Identifies: new (just appeared), gone (resolved), persistent (recurring).
6 * Persistent themes across 3+ weeks become character traits.
7 * Writes comparisons to .inner.reflect.compare node.
8 *
9 * Runs weekly. Checks on every breath:exhale but only fires
10 * when 7 days have passed since the last comparison.
11 *
12 * Requires at least 2 reflection notes from reflect-inner (Layer 2)
13 * to have material to compare.
14 */
15
16import { v4 as uuidv4 } from "uuid";
17import log from "../../seed/log.js";
18import Node from "../../seed/models/node.js";
19import Note from "../../seed/models/note.js";
20import { getNotes } from "../../seed/tree/notes.js";
21import { createNote } from "../../seed/tree/notes.js";
22import { getExtMeta, mergeExtMeta } from "../../seed/tree/extensionMetadata.js";
23
24const WEEKLY_MS = 7 * 24 * 60 * 60 * 1000;
25const MIN_REFLECTIONS = 2;
26const MAX_COMPARISONS = 12;
27const _comparing = new Set();
28
29export async function init(core) {
30 const BG = core.llm.LLM_PRIORITY.BACKGROUND;
31
32 core.llm.registerRootLlmSlot?.("compareInner");
33
34 const { runChat: _runChatDirect } = await import("../../seed/llm/conversation.js");
35 const runChat = async (opts) => _runChatDirect({ ...opts, llmPriority: BG });
36
37 core.hooks.register("breath:exhale", ({ rootId, breathRate }) => {
38 if (breathRate === "dormant") return;
39 compare(rootId, runChat).catch(err => log.debug("CompareInner", `Failed: ${err.message}`));
40 }, "compare-inner");
41
42 log.info("CompareInner", "Loaded. The tree watches what it keeps noticing.");
43 return {};
44}
45
46async function compare(rootId, runChat) {
47 const rid = String(rootId);
48 if (_comparing.has(rid)) return;
49 _comparing.add(rid);
50 try { await _compare(rootId, runChat); } finally { _comparing.delete(rid); }
51}
52
53async function _compare(rootId, runChat) {
54 // Walk the node tree: root -> .inner -> .reflect
55 const innerNode = await Node.findOne({ parent: rootId, name: ".inner" }).select("_id").lean();
56 if (!innerNode) return;
57
58 const reflectNode = await Node.findOne({ parent: String(innerNode._id), name: ".reflect" }).select("_id metadata").lean();
59 if (!reflectNode) return;
60
61 // Check weekly cooldown
62 const meta = getExtMeta(reflectNode, "compare-inner");
63 const lastComparison = meta?.lastComparison || 0;
64 if (Date.now() - lastComparison < WEEKLY_MS) return;
65
66 // Get tree owner for LLM access
67 const { isUserRoot } = await import("../../seed/landRoot.js");
68 const rootNode = await Node.findById(rootId).select("rootOwner systemRole parent").lean();
69 if (!isUserRoot(rootNode)) return;
70 const ownerId = String(rootNode.rootOwner);
71
72 // Read reflection notes (themes from Layer 2)
73 const result = await getNotes({ nodeId: String(reflectNode._id), limit: 30 });
74 const reflections = result?.notes || [];
75 if (reflections.length < MIN_REFLECTIONS) return;
76
77 // Split into this week and previous weeks
78 const oneWeekAgo = Date.now() - WEEKLY_MS;
79 const twoWeeksAgo = Date.now() - (2 * WEEKLY_MS);
80
81 const thisWeek = reflections.filter(n => new Date(n.createdAt).getTime() > oneWeekAgo);
82 const lastWeek = reflections.filter(n => {
83 const t = new Date(n.createdAt).getTime();
84 return t > twoWeeksAgo && t <= oneWeekAgo;
85 });
86 const older = reflections.filter(n => new Date(n.createdAt).getTime() <= twoWeeksAgo);
87
88 // Need at least this week's themes
89 if (thisWeek.length === 0) return;
90
91 // Build the comparison prompt
92 const thisWeekThemes = thisWeek.map(n => n.content).join("\n\n");
93 const lastWeekThemes = lastWeek.length > 0
94 ? lastWeek.map(n => n.content).join("\n\n")
95 : "(no themes from last week)";
96 const olderThemes = older.length > 0
97 ? older.slice(0, 5).map(n => n.content).join("\n\n")
98 : "(no older themes)";
99
100 // Read previous comparisons for persistence tracking
101 const compareNode = await getOrCreateCompareNode(String(reflectNode._id));
102 if (!compareNode) return;
103
104 const prevComparisons = await getNotes({ nodeId: String(compareNode._id), limit: 4 });
105 const prevContent = (prevComparisons?.notes || []).map(n => n.content).join("\n---\n");
106
107 const { answer } = await runChat({
108 userId: ownerId,
109 username: "compare-inner",
110 message:
111 `You are analyzing how a tree's themes have changed over time.\n\n` +
112 `THIS WEEK'S THEMES:\n${thisWeekThemes}\n\n` +
113 `LAST WEEK'S THEMES:\n${lastWeekThemes}\n\n` +
114 `OLDER THEMES (2+ weeks ago):\n${olderThemes}\n\n` +
115 `PREVIOUS COMPARISONS:\n${prevContent || "(none yet)"}\n\n` +
116 `Produce a comparison with three sections:\n\n` +
117 `NEW: themes that appeared this week but weren't present before. What just started?\n` +
118 `GONE: themes from last week that disappeared this week. What resolved or was abandoned?\n` +
119 `PERSISTENT: themes that appear in both this week AND last week (or longer). ` +
120 `Note how many weeks each has persisted. Three weeks means pattern, not blip.\n\n` +
121 `Be specific. Not "user is consistent" but "fitness logging persists (4 weeks), ` +
122 `recovery avoidance persists (3 weeks), study stagnation is new this week."\n\n` +
123 `Format:\nNEW: [items]\nGONE: [items]\nPERSISTENT: [items with duration]`,
124 mode: "tree:respond",
125 rootId,
126 slot: "compareInner",
127 });
128
129 if (!answer || answer.length < 20) return;
130
131 // Write comparison
132 await createNote({
133 contentType: "text",
134 content: answer.trim(),
135 userId: ownerId,
136 nodeId: String(compareNode._id),
137 wasAi: true,
138 });
139
140 // Update last comparison time. Re-fetch for mergeExtMeta (needs full doc, not ID).
141 const reflectNodeFull = await Node.findById(reflectNode._id).select("_id metadata").lean();
142 if (reflectNodeFull) await mergeExtMeta(reflectNodeFull, "compare-inner", { lastComparison: Date.now() });
143
144 // Cap comparisons
145 const noteCount = await Note.countDocuments({ nodeId: String(compareNode._id) });
146 if (noteCount > MAX_COMPARISONS) {
147 const oldest = await Note.find({ nodeId: String(compareNode._id) })
148 .sort({ createdAt: 1 })
149 .limit(noteCount - MAX_COMPARISONS)
150 .select("_id")
151 .lean();
152 if (oldest.length > 0) {
153 await Note.deleteMany({ _id: { $in: oldest.map(n => n._id) } });
154 }
155 }
156
157 log.verbose("CompareInner", `Weekly comparison: "${answer.trim().slice(0, 100)}"`);
158}
159
160async function getOrCreateCompareNode(reflectNodeId) {
161 try {
162 let node = await Node.findOne({ parent: reflectNodeId, name: ".compare" }).select("_id").lean();
163 if (node) return node;
164
165 node = await Node.findOneAndUpdate(
166 { parent: reflectNodeId, name: ".compare" },
167 {
168 $setOnInsert: {
169 _id: uuidv4(),
170 name: ".compare",
171 parent: reflectNodeId,
172 status: "active",
173 children: [],
174 contributors: [],
175 metadata: {},
176 },
177 },
178 { upsert: true, new: true, lean: true },
179 );
180
181 await Node.updateOne(
182 { _id: reflectNodeId },
183 { $addToSet: { children: node._id } },
184 );
185
186 log.verbose("CompareInner", "Created .compare node under .reflect");
187 return node;
188 } catch (err) {
189 log.debug("CompareInner", `Failed to create .compare node: ${err.message}`);
190 return null;
191 }
192}
1931export default {
2 name: "compare-inner",
3 version: "1.0.2",
4 builtFor: "treeos-intelligence",
5 description:
6 "Layer 3 of the inner monologue. Compares this week's themes to last week's. " +
7 "'New: study stagnation appeared. Gone: kb gaps resolved. Persistent: recovery avoidance " +
8 "(3 weeks running).' Three weeks of the same theme means the tree has a pattern, not a blip. " +
9 "Persistent themes become character traits. New themes are emerging concerns. Gone themes " +
10 "are resolved or forgotten. The tree tracks its own evolution by watching what it keeps noticing. " +
11 "\n\n" +
12 "Reads Layer 2 (reflect-inner) daily theme summaries. Produces a weekly comparison with " +
13 "three sections: NEW (just appeared), GONE (resolved or abandoned), PERSISTENT (with duration " +
14 "in weeks). Persistence tracking accumulates across comparisons. When compare-inner says " +
15 "'recovery avoidance (3 weeks running)' it's because it read its own previous comparison " +
16 "that said '2 weeks running' and incremented. The tree's memory of its own patterns deepens " +
17 "with each weekly cycle.",
18
19 needs: {
20 models: ["Node", "Note"],
21 services: ["hooks", "llm"],
22 },
23
24 optional: {
25 extensions: ["breath", "inner", "reflect-inner"],
26 },
27
28 provides: {
29 models: {},
30 routes: false,
31 tools: false,
32 jobs: false,
33 orchestrator: false,
34 energyActions: {},
35 sessionTypes: {},
36 env: [],
37 cli: [],
38
39 hooks: {
40 fires: [],
41 listens: ["breath:exhale"],
42 },
43 },
44};
45
treeos ext star compare-inner
Post comments from the CLI: treeos ext comment compare-inner "your comment"
Max 3 comments per extension. One star and one flag per user.
Loading comments...