EXTENSION for treeos-intelligence
compare-inner
Layer 3 of the inner monologue. Compares this week's themes to last week's. 'New: study stagnation appeared. Gone: kb gaps resolved. Persistent: recovery avoidance (3 weeks running).' Three weeks of the same theme means the tree has a pattern, not a blip. Persistent themes become character traits. New themes are emerging concerns. Gone themes are resolved or forgotten. The tree tracks its own evolution by watching what it keeps noticing. Reads Layer 2 (reflect-inner) daily theme summaries. Produces a weekly comparison with three sections: NEW (just appeared), GONE (resolved or abandoned), PERSISTENT (with duration in weeks). Persistence tracking accumulates across comparisons. When compare-inner says 'recovery avoidance (3 weeks running)' it's because it read its own previous comparison that said '2 weeks running' and incremented. The tree's memory of its own patterns deepens with each weekly cycle.
v1.0.2 by TreeOS Site 0 downloads 2 files 238 lines 8.6 KB published 38d ago
treeos ext install compare-inner
View changelog

Manifest

Requires

  • services: hooks, llm
  • models: Node, Note

Optional

  • extensions: breath, inner, reflect-inner
SHA256: 72b84028beb3b89c80e1e4417fa627b485df6c8ce67cbd83acd51a9266e9734d

Dependents

1 package depend on this

PackageTypeRelationship
treeos-intelligence v1.0.2bundleincludes

Hooks

Listens To

  • breath:exhale

Source Code

1/**
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}
193
1export 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

Versions

Version Published Downloads
1.0.2 38d ago 0
1.0.1 46d ago 0
1.0.0 47d ago 0
0 stars
0 flags
React from the CLI: treeos ext star compare-inner

Comments

Loading comments...

Post comments from the CLI: treeos ext comment compare-inner "your comment"
Max 3 comments per extension. One star and one flag per user.