EXTENSION for treeos-intelligence
inner
The tree thinks to itself. One random thought per breath. Picks a random node, reads its context, generates one observation. Most of it is noise. Some of it is the connection nobody asked for. Other extensions read .inner notes for signals they wouldn't find through targeted search. The serendipity engine.
v1.0.2 by TreeOS Site 0 downloads 4 files 576 lines 20.1 KB published 38d ago
treeos ext install inner
View changelog

Manifest

Requires

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

Optional

  • extensions: breath, html-rendering, treeos-base
SHA256: 4443348531991000a2b47dece1eb03b4f5c10765917c0d5d2350b8df26ef5020

Dependents

1 package depend on this

PackageTypeRelationship
treeos-intelligence v1.0.2bundleincludes

Hooks

Listens To

  • breath:exhale

Source Code

1import 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}
66
1import { 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}
233
1export 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};
38
1import { 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

Versions

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

Comments

Loading comments...

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