4443348531991000a2b47dece1eb03b4f5c10765917c0d5d2350b8df26ef50201 package depend on this
| Package | Type | Relationship |
|---|---|---|
| treeos-intelligence v1.0.2 | bundle | includes |
breath:exhale1import express from "express";
2import { sendError, ERR } from "../../seed/protocol.js";
3import urlAuth from "../html-rendering/urlAuth.js";
4import { htmlOnly, buildQS } from "../html-rendering/htmlHelpers.js";
5import Node from "../../seed/models/node.js";
6import { getNotes } from "../../seed/tree/notes.js";
7import { getExtMeta } from "../../seed/tree/extensionMetadata.js";
8import { renderConsciousnessPage } from "./pages/consciousness.js";
9
10export default function buildHtmlRoutes() {
11 const router = express.Router();
12
13 router.get("/root/:rootId/consciousness", urlAuth, htmlOnly, async (req, res) => {
14 try {
15 const { rootId } = req.params;
16 const root = await Node.findById(rootId).select("name metadata").lean();
17 if (!root) return sendError(res, 404, ERR.TREE_NOT_FOUND, "Tree not found");
18
19 const qs = buildQS(req);
20
21 // Walk the .inner chain to collect all layer data
22 const layers = { inner: [], reflect: [], compare: [], narrative: null, prediction: null };
23
24 // Layer 1: .inner notes
25 const innerNode = await Node.findOne({ parent: rootId, name: ".inner" }).select("_id").lean();
26 if (innerNode) {
27 const result = await getNotes({ nodeId: String(innerNode._id), limit: 20 });
28 layers.inner = result?.notes || [];
29
30 // Layer 2: .inner.reflect notes
31 const reflectNode = await Node.findOne({ parent: String(innerNode._id), name: ".reflect" }).select("_id").lean();
32 if (reflectNode) {
33 const rResult = await getNotes({ nodeId: String(reflectNode._id), limit: 10 });
34 layers.reflect = rResult?.notes || [];
35
36 // Layer 3: .inner.reflect.compare notes
37 const compareNode = await Node.findOne({ parent: String(reflectNode._id), name: ".compare" }).select("_id").lean();
38 if (compareNode) {
39 const cResult = await getNotes({ nodeId: String(compareNode._id), limit: 5 });
40 layers.compare = cResult?.notes || [];
41 }
42 }
43 }
44
45 // Layer 4-6: narrative metadata on root
46 const meta = root.metadata instanceof Map ? Object.fromEntries(root.metadata) : (root.metadata || {});
47 layers.narrative = meta.narrative || null;
48
49 // Layer 7: prediction metadata on root
50 layers.prediction = meta.prediction || null;
51
52 res.send(renderConsciousnessPage({
53 rootId,
54 rootName: root.name,
55 layers,
56 qs,
57 userId: req.userId,
58 }));
59 } catch (err) {
60 sendError(res, 500, ERR.INTERNAL, "Consciousness page failed");
61 }
62 });
63
64 return router;
65}
661import { v4 as uuidv4 } from "uuid";
2import log from "../../seed/log.js";
3import Node from "../../seed/models/node.js";
4import Note from "../../seed/models/note.js";
5import { getContextForAi } from "../../seed/tree/treeFetch.js";
6import { createNote } from "../../seed/tree/notes.js";
7
8const MAX_THOUGHTS = 200;
9
10// Track consecutive idle exhales per tree to avoid thinking when quiet
11const _idleCount = new Map();
12
13export async function init(core) {
14 const BG = core.llm.LLM_PRIORITY.BACKGROUND;
15
16 core.llm.registerRootLlmSlot?.("inner");
17
18 // Direct import like rings. core.llm.runChat guards with userHasLlm
19 // which fails for background jobs. Direct import uses the full
20 // LLM resolution chain: tree slot -> tree default -> user slot -> user default.
21 const { runChat: _runChatDirect } = await import("../../seed/llm/conversation.js");
22 const runChat = async (opts) => _runChatDirect({ ...opts, llmPriority: BG });
23
24 // ── breath:exhale: one random thought ──────────────────────────────
25 core.hooks.register("breath:exhale", async ({ rootId, breathRate, activityLevel }) => {
26 // Don't think on dormant trees
27 if (breathRate === "dormant") return;
28
29 // Track idle exhales. Skip if quiet for 3+ consecutive exhales.
30 const rid = String(rootId);
31 if (activityLevel === 0) {
32 const idle = (_idleCount.get(rid) || 0) + 1;
33 _idleCount.set(rid, idle);
34 if (idle >= 3) return;
35 } else {
36 _idleCount.set(rid, 0);
37 }
38
39 // Fire and forget. Don't block the breath hook with an LLM call.
40 think(rootId, runChat).catch(err => log.debug("Inner", `Thought failed: ${err.message}`));
41 }, "inner");
42
43 // Register HTML page and tree quick link
44 try {
45 const { getExtension } = await import("../loader.js");
46 const htmlExt = getExtension("html-rendering");
47 if (htmlExt) {
48 const { default: buildHtmlRoutes } = await import("./htmlRoutes.js");
49 htmlExt.router.use("/", buildHtmlRoutes());
50 }
51 const base = getExtension("treeos-base");
52 base?.exports?.registerSlot?.("tree-quick-links", "inner", ({ rootId, queryString }) =>
53 `<a href="/api/v1/root/${rootId}/consciousness${queryString}" class="back-link">Consciousness</a>`,
54 { priority: 45 }
55 );
56 } catch {}
57
58 log.info("Inner", "Loaded. The tree thinks to itself.");
59 return {};
60}
61
62async function think(rootId, runChat) {
63 try {
64 // Get tree owner for LLM access
65 const { isUserRoot } = await import("../../seed/landRoot.js");
66 const rootNode = await Node.findById(rootId).select("rootOwner systemRole parent").lean();
67 if (!isUserRoot(rootNode)) return;
68 const ownerId = String(rootNode.rootOwner);
69
70 // Find or create .inner node under tree root
71 const innerNode = await getOrCreateInnerNode(rootId);
72 if (!innerNode) return;
73
74 // Pick a random node in this tree
75 const randomNode = await pickRandomNode(rootId, innerNode._id);
76 if (!randomNode) return;
77
78 // Read its context
79 let context;
80 try {
81 context = await getContextForAi(randomNode._id, {
82 includeNotes: true,
83 includeChildren: true,
84 });
85 } catch {
86 return; // Node might have been deleted between pick and read
87 }
88
89 const contextSummary = typeof context === "string"
90 ? context
91 : JSON.stringify(context, null, 2).slice(0, 2000);
92
93 // One thought
94 const { answer } = await runChat({
95 userId: ownerId,
96 username: "inner",
97 message:
98 `You are the tree's internal monologue. You are looking at the node "${randomNode.name}" ` +
99 `and its content.\n\n${contextSummary}\n\n` +
100 `Generate ONE thought. It can be an observation about patterns, a connection between ` +
101 `this node and what you know about the tree, a question about something missing, ` +
102 `a noticed imbalance, or just noise. Keep it to one sentence. ` +
103 `Don't be helpful. Don't suggest actions. Just think.`,
104 mode: "tree:respond",
105 rootId,
106 slot: "inner",
107 });
108
109 if (!answer || answer.length < 5) return;
110
111 // Write the thought as a note on .inner
112 await createNote({
113 contentType: "text",
114 content: answer.trim(),
115 userId: ownerId,
116 nodeId: String(innerNode._id),
117 wasAi: true,
118 });
119
120 // Cap at MAX_THOUGHTS
121 const noteCount = await Note.countDocuments({ nodeId: String(innerNode._id) });
122 if (noteCount > MAX_THOUGHTS) {
123 const oldest = await Note.find({ nodeId: String(innerNode._id) })
124 .sort({ createdAt: 1 })
125 .limit(noteCount - MAX_THOUGHTS)
126 .select("_id")
127 .lean();
128 if (oldest.length > 0) {
129 await Note.deleteMany({ _id: { $in: oldest.map(n => n._id) } });
130 }
131 }
132
133 log.verbose("Inner", `${randomNode.name}: "${answer.trim().slice(0, 80)}"`);
134 } catch (err) {
135 log.debug("Inner", `Thought failed: ${err.message}`);
136 }
137}
138
139// ─────────────────────────────────────────────────────────────────────────
140// HELPERS
141// ─────────────────────────────────────────────────────────────────────────
142
143async function getOrCreateInnerNode(rootId) {
144 try {
145 let node = await Node.findOne({ parent: rootId, name: ".inner" }).select("_id").lean();
146 if (node) return node;
147
148 node = await Node.findOneAndUpdate(
149 { parent: rootId, name: ".inner" },
150 {
151 $setOnInsert: {
152 _id: uuidv4(),
153 name: ".inner",
154 parent: rootId,
155 status: "active",
156 children: [],
157 contributors: [],
158 metadata: {},
159 },
160 },
161 { upsert: true, new: true, lean: true },
162 );
163
164 // Add to parent's children
165 await Node.updateOne(
166 { _id: rootId },
167 { $addToSet: { children: node._id } },
168 );
169
170 log.verbose("Inner", `Created .inner node for tree ${rootId}`);
171 return node;
172 } catch (err) {
173 log.debug("Inner", `Failed to create .inner node: ${err.message}`);
174 return null;
175 }
176}
177
178async function pickRandomNode(rootId, innerNodeId) {
179 try {
180 // Get all active, non-system nodes in this tree (shallow: direct children + their children)
181 // For deeper trees, we walk two levels which covers most structures
182 const root = await Node.findById(rootId).select("children").lean();
183 if (!root?.children?.length) return null;
184
185 const candidates = [];
186 const innerIdStr = String(innerNodeId);
187
188 // Level 1: direct children of root
189 const level1 = await Node.find({
190 _id: { $in: root.children },
191 systemRole: null,
192 status: "active",
193 }).select("_id name children").lean();
194
195 for (const node of level1) {
196 if (String(node._id) !== innerIdStr && !node.name?.startsWith(".")) candidates.push(node);
197 }
198
199 // Level 2: grandchildren
200 const level2Ids = level1.flatMap(n => n.children || []);
201 if (level2Ids.length > 0) {
202 const level2 = await Node.find({
203 _id: { $in: level2Ids },
204 systemRole: null,
205 status: "active",
206 }).select("_id name children").lean();
207
208 for (const node of level2) {
209 if (!node.name?.startsWith(".")) candidates.push(node);
210 }
211
212 // Level 3: great-grandchildren
213 const level3Ids = level2.flatMap(n => n.children || []);
214 if (level3Ids.length > 0) {
215 const level3 = await Node.find({
216 _id: { $in: level3Ids },
217 systemRole: null,
218 status: "active",
219 }).select("_id name").lean();
220
221 for (const node of level3) {
222 if (!node.name?.startsWith(".")) candidates.push(node);
223 }
224 }
225 }
226
227 if (candidates.length === 0) return null;
228 return candidates[Math.floor(Math.random() * candidates.length)];
229 } catch {
230 return null;
231 }
232}
2331export default {
2 name: "inner",
3 version: "1.0.2",
4 builtFor: "treeos-intelligence",
5 description:
6 "The tree thinks to itself. One random thought per breath. " +
7 "Picks a random node, reads its context, generates one observation. " +
8 "Most of it is noise. Some of it is the connection nobody asked for. " +
9 "Other extensions read .inner notes for signals they wouldn't find " +
10 "through targeted search. The serendipity engine.",
11
12 needs: {
13 models: ["Node", "Note"],
14 services: ["hooks", "llm"],
15 },
16
17 optional: {
18 extensions: ["breath", "html-rendering", "treeos-base"],
19 },
20
21 provides: {
22 models: {},
23 routes: false,
24 tools: false,
25 jobs: false,
26 orchestrator: false,
27 energyActions: {},
28 sessionTypes: {},
29 env: [],
30 cli: [],
31
32 hooks: {
33 fires: [],
34 listens: ["breath:exhale"],
35 },
36 },
37};
381import { page } from "../../html-rendering/html/layout.js";
2import { baseStyles, glassHeaderStyles, responsiveBase } from "../../html-rendering/html/baseStyles.js";
3import { escapeHtml } from "../../html-rendering/html/utils.js";
4
5export function renderConsciousnessPage({ rootId, rootName, layers, qs, userId }) {
6 const css = `
7 ${baseStyles}
8 ${glassHeaderStyles}
9 ${responsiveBase}
10
11 .layer {
12 margin-bottom: 24px;
13 animation: fadeInUp 0.5s ease-out both;
14 }
15 .layer-header {
16 display: flex;
17 align-items: center;
18 gap: 12px;
19 margin-bottom: 12px;
20 }
21 .layer-num {
22 width: 32px; height: 32px;
23 border-radius: 50%;
24 display: flex; align-items: center; justify-content: center;
25 font-weight: 700; font-size: 14px;
26 flex-shrink: 0;
27 }
28 .layer-title { font-size: 16px; font-weight: 600; color: white; }
29 .layer-sub { font-size: 12px; color: rgba(255,255,255,0.4); }
30 .layer-empty { color: rgba(255,255,255,0.3); font-size: 13px; font-style: italic; padding: 12px 0; }
31
32 .thought {
33 padding: 10px 14px;
34 background: rgba(255,255,255,0.04);
35 border-radius: 8px;
36 margin-bottom: 6px;
37 font-size: 13px;
38 line-height: 1.6;
39 color: rgba(255,255,255,0.7);
40 border-left: 3px solid rgba(255,255,255,0.08);
41 }
42 .thought-time {
43 font-size: 10px;
44 color: rgba(255,255,255,0.25);
45 margin-bottom: 4px;
46 }
47
48 .l1 .layer-num { background: rgba(120, 120, 255, 0.2); color: rgba(120, 120, 255, 0.9); }
49 .l1 .thought { border-left-color: rgba(120, 120, 255, 0.3); }
50
51 .l2 .layer-num { background: rgba(72, 187, 120, 0.2); color: rgba(72, 187, 120, 0.9); }
52 .l2 .thought { border-left-color: rgba(72, 187, 120, 0.3); }
53
54 .l3 .layer-num { background: rgba(236, 201, 75, 0.2); color: rgba(236, 201, 75, 0.9); }
55 .l3 .thought { border-left-color: rgba(236, 201, 75, 0.3); }
56
57 .l4 .layer-num { background: rgba(249, 115, 22, 0.2); color: rgba(249, 115, 22, 0.9); }
58 .l4 .thought { border-left-color: rgba(249, 115, 22, 0.3); }
59
60 .l5 .layer-num { background: rgba(168, 85, 247, 0.2); color: rgba(168, 85, 247, 0.9); }
61 .l5 .thought { border-left-color: rgba(168, 85, 247, 0.3); }
62
63 .l6 .layer-num { background: rgba(239, 68, 68, 0.2); color: rgba(239, 68, 68, 0.9); }
64 .l6 .thought { border-left-color: rgba(239, 68, 68, 0.3); }
65
66 .l7 .layer-num { background: rgba(56, 189, 248, 0.2); color: rgba(56, 189, 248, 0.9); }
67 .l7 .thought { border-left-color: rgba(56, 189, 248, 0.3); }
68
69 .flow-line {
70 width: 2px;
71 height: 20px;
72 background: rgba(255,255,255,0.06);
73 margin: 0 auto;
74 }
75
76 .back-nav { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
77 .back-link {
78 display: inline-flex; align-items: center; gap: 6px; padding: 10px 20px;
79 background: rgba(var(--glass-water-rgb), var(--glass-alpha)); backdrop-filter: blur(22px);
80 color: white; text-decoration: none; border-radius: 980px;
81 font-weight: 600; font-size: 14px; border: 1px solid rgba(255,255,255,0.12);
82 }
83 .back-link:hover { background: rgba(var(--glass-water-rgb), var(--glass-alpha-hover)); }
84
85 .stats-row {
86 display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 24px;
87 }
88 .stat {
89 padding: 8px 16px;
90 background: rgba(255,255,255,0.04);
91 border-radius: 8px;
92 font-size: 13px;
93 color: rgba(255,255,255,0.5);
94 border: 1px solid rgba(255,255,255,0.06);
95 }
96 .stat strong { color: rgba(255,255,255,0.8); }
97 `;
98
99 function renderNotes(notes, max = 10) {
100 if (!notes || notes.length === 0) return '<div class="layer-empty">No data yet. Accumulating.</div>';
101 return notes.slice(0, max).map(n => {
102 const time = n.createdAt ? new Date(n.createdAt).toLocaleString() : '';
103 return `<div class="thought">
104 ${time ? `<div class="thought-time">${escapeHtml(time)}</div>` : ''}
105 ${escapeHtml(n.content || '').replace(/\n/g, '<br>')}
106 </div>`;
107 }).join('');
108 }
109
110 const { inner, reflect, compare, narrative, prediction } = layers;
111
112 const body = `
113 <div class="container" style="max-width: 700px;">
114 <div class="back-nav">
115 <a href="/api/v1/root/${escapeHtml(rootId)}${qs}" class="back-link">Back to ${escapeHtml(rootName || 'Tree')}</a>
116 </div>
117
118 <div class="header">
119 <h1>Consciousness</h1>
120 <div class="header-subtitle">${escapeHtml(rootName || 'Tree')} . The tree's inner life.</div>
121 </div>
122
123 <div class="stats-row">
124 <div class="stat">Layer 1: <strong>${inner?.length || 0}</strong> thoughts</div>
125 <div class="stat">Layer 2: <strong>${reflect?.length || 0}</strong> reflections</div>
126 <div class="stat">Layer 3: <strong>${compare?.length || 0}</strong> comparisons</div>
127 <div class="stat">Layer 4: <strong>${narrative?.identity ? 'active' : 'waiting'}</strong></div>
128 <div class="stat">Layer 7: <strong>${prediction?.predictions?.length || 0}</strong> predictions</div>
129 </div>
130
131 <!-- Layer 1: Inner -->
132 <div class="layer l1" style="animation-delay: 0.1s">
133 <div class="layer-header">
134 <div class="layer-num">1</div>
135 <div>
136 <div class="layer-title">Inner</div>
137 <div class="layer-sub">Raw thoughts. One per breath. Random node. Unfiltered.</div>
138 </div>
139 </div>
140 ${renderNotes(inner, 8)}
141 </div>
142 <div class="flow-line"></div>
143
144 <!-- Layer 2: Reflect -->
145 <div class="layer l2" style="animation-delay: 0.2s">
146 <div class="layer-header">
147 <div class="layer-num">2</div>
148 <div>
149 <div class="layer-title">Reflect</div>
150 <div class="layer-sub">Daily themes. 200 observations compressed into 5.</div>
151 </div>
152 </div>
153 ${renderNotes(reflect, 3)}
154 </div>
155 <div class="flow-line"></div>
156
157 <!-- Layer 3: Compare -->
158 <div class="layer l3" style="animation-delay: 0.3s">
159 <div class="layer-header">
160 <div class="layer-num">3</div>
161 <div>
162 <div class="layer-title">Compare</div>
163 <div class="layer-sub">Weekly. What's new. What's gone. What persists.</div>
164 </div>
165 </div>
166 ${renderNotes(compare, 2)}
167 </div>
168 <div class="flow-line"></div>
169
170 <!-- Layer 4: Narrative -->
171 <div class="layer l4" style="animation-delay: 0.4s">
172 <div class="layer-header">
173 <div class="layer-num">4</div>
174 <div>
175 <div class="layer-title">Narrative</div>
176 <div class="layer-sub">Monthly identity. Who the tree is.</div>
177 </div>
178 </div>
179 ${narrative?.identity
180 ? `<div class="thought">${escapeHtml(narrative.identity).replace(/\n/g, '<br>')}</div>`
181 : '<div class="layer-empty">Not enough data yet. Needs weeks of comparisons.</div>'}
182 </div>
183 <div class="flow-line"></div>
184
185 <!-- Layer 5+6: Voice & Initiative -->
186 <div class="layer l5" style="animation-delay: 0.5s">
187 <div class="layer-header">
188 <div class="layer-num">5</div>
189 <div>
190 <div class="layer-title">Voice</div>
191 <div class="layer-sub">How the tree talks. Shaped by lived experience.</div>
192 </div>
193 </div>
194 ${narrative?.voice
195 ? `<div class="thought">${escapeHtml(narrative.voice).replace(/\n/g, '<br>')}</div>`
196 : '<div class="layer-empty">Emerges from the narrative.</div>'}
197 </div>
198
199 <div class="layer l6" style="animation-delay: 0.55s">
200 <div class="layer-header">
201 <div class="layer-num">6</div>
202 <div>
203 <div class="layer-title">Initiative</div>
204 <div class="layer-sub">Behavioral shifts. Not what to do, how to approach.</div>
205 </div>
206 </div>
207 ${narrative?.initiative
208 ? `<div class="thought">${escapeHtml(narrative.initiative).replace(/\n/g, '<br>')}</div>`
209 : '<div class="layer-empty">Emerges from the narrative.</div>'}
210 </div>
211 <div class="flow-line"></div>
212
213 <!-- Layer 7: Prediction -->
214 <div class="layer l7" style="animation-delay: 0.6s">
215 <div class="layer-header">
216 <div class="layer-num">7</div>
217 <div>
218 <div class="layer-title">Prediction</div>
219 <div class="layer-sub">What the tree expects. Pattern recognition across time.</div>
220 </div>
221 </div>
222 ${prediction?.predictions?.length > 0
223 ? prediction.predictions.map(p => `<div class="thought">
224 <strong>[${escapeHtml(p.confidence || 'low')}]</strong> ${escapeHtml(p.expectation || '')}
225 <div class="thought-time">Pattern: ${escapeHtml(p.pattern || '')}</div>
226 </div>`).join('')
227 : '<div class="layer-empty">Needs rings (completed growth cycles) to project forward.</div>'}
228 </div>
229
230 <div class="flow-line"></div>
231 <div style="text-align: center; color: rgba(255,255,255,0.15); font-size: 12px; padding: 12px 0;">
232 The cycle loops back to Layer 1. Predictions become the lens for new thoughts.
233 </div>
234 </div>
235 `;
236
237 return page({ title: `${rootName || 'Tree'} . Consciousness`, css, body, js: '' });
238}
239
treeos ext star inner
Post comments from the CLI: treeos ext comment inner "your comment"
Max 3 comments per extension. One star and one flag per user.
Loading comments...