29cdedd7b33f5322b0ed6833d974bfd49bcf3c515d18ec28d34e1d0bb828bc81| Command | Method | Description |
|---|---|---|
water | GET | The full picture. No action shows node hydration. Actions: land. |
water land | GET | Land-wide dashboard. Pulse, gaps, flow, peers. |
1export async function init(core) {
2 const { default: router } = await import("./routes.js");
3 return { router };
4}
51export default {
2 name: "water",
3 version: "1.0.0",
4 builtFor: "TreeOS",
5 description:
6 "The full picture at any position. Combines perspective, codebook stats, memory, gaps, " +
7 "flow, and evolution into one view. What is flowing through this node right now? Perspective " +
8 "shows what it drinks. Memory shows who it has talked to. Gaps show what it is missing. " +
9 "Flow shows recent signals. Codebook shows compression stats. water land gives the operator " +
10 "dashboard: pulse health plus aggregated gaps plus .flow stats plus peer health. Every " +
11 "extension contributes one piece. water assembles the picture at any position. The tree knows " +
12 "its own hydration.",
13
14 needs: {
15 models: ["Node"],
16 },
17
18 optional: {
19 extensions: [
20 "perspective-filter", "codebook", "long-memory",
21 "gap-detection", "flow", "pulse", "evolution",
22 ],
23 },
24
25 provides: {
26 models: {},
27 routes: "./routes.js",
28 tools: false,
29 jobs: false,
30 orchestrator: false,
31 energyActions: {},
32 sessionTypes: {},
33 env: [],
34
35 cli: [
36 {
37 command: "water [action]", scope: ["tree"],
38 description: "The full picture. No action shows node hydration. Actions: land.",
39 method: "GET",
40 endpoint: "/node/:nodeId/water",
41 subcommands: {
42 "land": {
43 method: "GET",
44 endpoint: "/water/land",
45 description: "Land-wide dashboard. Pulse, gaps, flow, peers.",
46 },
47 },
48 },
49 ],
50
51 hooks: {
52 fires: [],
53 listens: [],
54 },
55 },
56};
571import express from "express";
2import authenticate from "../../seed/middleware/authenticate.js";
3import { sendOk, sendError, ERR } from "../../seed/protocol.js";
4import Node from "../../seed/models/node.js";
5import { getExtension } from "../loader.js";
6import log from "../../seed/log.js";
7
8const router = express.Router();
9
10// GET /node/:nodeId/water - full picture at this position
11router.get("/node/:nodeId/water", authenticate, async (req, res) => {
12 try {
13 const { nodeId } = req.params;
14 const node = await Node.findById(nodeId).select("name metadata parent systemRole").lean();
15 if (!node) return sendError(res, 404, ERR.NODE_NOT_FOUND, "Node not found");
16
17 const meta = node.metadata instanceof Map
18 ? Object.fromEntries(node.metadata)
19 : (node.metadata || {});
20
21 const picture = { nodeId, nodeName: node.name };
22
23 // Perspective: what this node drinks
24 const perspectiveExt = getExtension("perspective-filter");
25 if (perspectiveExt?.exports?.resolvePerspective) {
26 try {
27 const perspective = await perspectiveExt.exports.resolvePerspective(node);
28 picture.perspective = perspective || { accept: [], reject: [] };
29 } catch (err) { log.debug("Water", "perspective section failed:", err.message); }
30 }
31
32 // Memory: who this node has talked to
33 const memoryExt = getExtension("long-memory");
34 if (memoryExt?.exports?.getMemory) {
35 try {
36 const memory = await memoryExt.exports.getMemory(nodeId);
37 if (memory && memory.totalInteractions > 0) {
38 picture.memory = {
39 lastSeen: memory.lastSeen,
40 lastStatus: memory.lastStatus,
41 totalInteractions: memory.totalInteractions,
42 recentSources: (memory.connections || []).slice(-5).map((c) => c.sourceId),
43 };
44 }
45 } catch (err) { log.debug("Water", "memory section failed:", err.message); }
46 }
47
48 // Codebook: compression stats
49 if (meta.codebook) {
50 const entries = {};
51 for (const [uid, data] of Object.entries(meta.codebook)) {
52 if (data?.dictionary && Object.keys(data.dictionary).length > 0) {
53 entries[uid] = {
54 dictionarySize: Object.keys(data.dictionary).length,
55 notesSinceCompression: data.notesSinceCompression || 0,
56 lastCompressed: data.lastCompressed,
57 };
58 }
59 }
60 if (Object.keys(entries).length > 0) picture.codebook = entries;
61 }
62
63 // Gaps: what this node is missing
64 if (Array.isArray(meta.gaps) && meta.gaps.length > 0) {
65 picture.gaps = meta.gaps
66 .filter((g) => g.count > 0)
67 .sort((a, b) => b.count - a.count)
68 .map((g) => ({ namespace: g.namespace, count: g.count }));
69 }
70
71 // Flow: recent signals
72 const flowExt = getExtension("flow");
73 if (flowExt?.exports?.getFlowForPosition) {
74 try {
75 const flow = await flowExt.exports.getFlowForPosition(nodeId, 10);
76 picture.flow = {
77 scope: flow.scope,
78 recentSignals: Object.keys(flow.results).length,
79 };
80 } catch (err) { log.debug("Water", "flow section failed:", err.message); }
81 }
82
83 // Evolution: fitness
84 if (meta.evolution) {
85 picture.fitness = {
86 notesWritten: meta.evolution.notesWritten || 0,
87 visits: meta.evolution.visits || 0,
88 cascades: (meta.evolution.cascadesOriginated || 0) + (meta.evolution.cascadesReceived || 0),
89 lastActivity: meta.evolution.lastActivity,
90 };
91 }
92
93 // Contradictions
94 if (Array.isArray(meta.contradictions)) {
95 const active = meta.contradictions.filter((c) => c.status === "active");
96 if (active.length > 0) picture.contradictions = active.length;
97 }
98
99 // Compression
100 if (meta.compress?.essence) {
101 picture.compressed = { status: meta.compress.status, hasSummary: true };
102 }
103
104 sendOk(res, picture);
105 } catch (err) {
106 sendError(res, 500, ERR.INTERNAL, err.message);
107 }
108});
109
110// GET /water/land - land-wide dashboard
111router.get("/water/land", authenticate, async (req, res) => {
112 try {
113 const picture = {};
114
115 // Pulse: land health
116 const pulseExt = getExtension("pulse");
117 if (pulseExt?.exports?.getLatestSnapshot) {
118 try {
119 const snapshot = await pulseExt.exports.getLatestSnapshot();
120 if (snapshot) {
121 picture.health = {
122 failureRate: snapshot.failureRate,
123 elevated: snapshot.elevated,
124 signals: snapshot.signals,
125 results: snapshot.results,
126 lastUpdated: snapshot.timestamp,
127 peers: snapshot.peers,
128 };
129 }
130 } catch (err) { log.debug("Water", "pulse section failed:", err.message); }
131 }
132
133 // Gaps: land-wide aggregation
134 const gapExt = getExtension("gap-detection");
135 if (gapExt?.exports?.getGaps) {
136 try {
137 const roots = await Node.find({ rootOwner: { $ne: null }, systemRole: null })
138 .select("_id").lean();
139
140 const { getDescendantIds } = await import("../../seed/tree/treeFetch.js");
141 const aggregated = {};
142
143 for (const root of roots.slice(0, 50)) { // cap to prevent overload
144 const nodeIds = await getDescendantIds(root._id);
145 for (const nid of nodeIds) {
146 const gaps = await gapExt.exports.getGaps(nid);
147 for (const gap of gaps) {
148 if (!aggregated[gap.namespace]) aggregated[gap.namespace] = 0;
149 aggregated[gap.namespace] += gap.count;
150 }
151 }
152 }
153
154 const sorted = Object.entries(aggregated)
155 .sort((a, b) => b[1] - a[1])
156 .map(([namespace, count]) => ({ namespace, count }));
157
158 if (sorted.length > 0) picture.gaps = sorted;
159 } catch (err) { log.debug("Water", "gaps section failed:", err.message); }
160 }
161
162 // Flow stats
163 const flowNode = await Node.findOne({ systemRole: "flow" }).select("_id").lean();
164 if (flowNode) {
165 const partitions = await Node.find({ parent: flowNode._id })
166 .select("name metadata").sort({ name: -1 }).limit(7).lean();
167
168 const today = new Date().toISOString().slice(0, 10);
169 const stats = partitions.map((p) => {
170 const results = p.metadata instanceof Map
171 ? p.metadata.get("results") || {}
172 : p.metadata?.results || {};
173 return { date: p.name, signals: Object.keys(results).length };
174 });
175
176 picture.flow = {
177 partitions: partitions.length,
178 recentDays: stats,
179 todaySignals: stats.find((s) => s.date === today)?.signals || 0,
180 };
181 }
182
183 sendOk(res, picture);
184 } catch (err) {
185 sendError(res, 500, ERR.INTERNAL, err.message);
186 }
187});
188
189export default router;
190
treeos ext star water
Post comments from the CLI: treeos ext comment water "your comment"
Max 3 comments per extension. One star and one flag per user.
Loading comments...