EXTENSION for treeos-cascade
long-memory
The difference between a tree that forgets and a tree that remembers. .flow has resultTTL. Active results expire. That is correct for working memory. But some information should survive longer. Which nodes have talked to each other. When the last signal arrived. What the last status was. The ghost of a connection. For each cascade result, writes a lightweight trace to the involved nodes. metadata.memory.lastSeen with a timestamp. metadata.memory.lastStatus with the status. metadata.memory.connections as a small rolling array of the last N interactions with source IDs and timestamps. This is not the full payload. This is not the codebook. This is just the trace. This node heard from that node three months ago. The last exchange succeeded. They have talked 47 times total. Enough for the AI through enrichContext to know the relationship exists and has a history. Enough for a signal arriving after years of silence to land on a node that remembers something was here once. The traces live in metadata so they survive transit, never expire unless deliberately deleted, and persist through land restarts, extension reinstalls, and schema migrations.
v1.0.1 by TreeOS Site 0 downloads 5 files 328 lines 11.0 KB published 38d ago
treeos ext install long-memory
View changelog

Manifest

Provides

  • routes
  • tools
  • 1 CLI commands

Requires

  • models: Node
  • extensions: propagation
SHA256: 323974d3d12841cd69be3e3737b9b14279f5250de4df0d5a177c4cdd12ea3379

Dependents

1 package depend on this

PackageTypeRelationship
treeos-cascade v1.0.1bundleincludes

CLI Commands

CommandMethodDescription
memoryGETCascade memory. No action shows trace at this node. Actions: clear, connections.
memory clearDELETEWipe the trace. The node forgets its cascade history.
memory connectionsGETFull connection list with counts and timestamps

Hooks

Listens To

  • onCascade
  • enrichContext

Source Code

1/**
2 * Long Memory Core
3 *
4 * Writes lightweight traces to node metadata on every cascade event.
5 * Each trace is small: sourceId, timestamp, status. The rolling array
6 * is capped so it never grows unbounded. MongoDB atomic update, one
7 * operation per cascade hop. Fast enough for the onCascade sequential chain.
8 */
9
10import Node from "../../seed/models/node.js";
11import { SYSTEM_ROLE } from "../../seed/protocol.js";
12
13let _metadata = null;
14export function setMetadata(m) { _metadata = m; }
15
16// ─────────────────────────────────────────────────────────────────────────
17// CONFIG (stored on .config metadata["long-memory"])
18// ─────────────────────────────────────────────────────────────────────────
19
20const DEFAULT_MAX_CONNECTIONS = 50;
21
22export async function getLongMemoryConfig() {
23  const configNode = await Node.findOne({ systemRole: SYSTEM_ROLE.CONFIG }).select("metadata").lean();
24  if (!configNode) return { maxConnections: DEFAULT_MAX_CONNECTIONS };
25
26  const meta = configNode.metadata instanceof Map
27    ? configNode.metadata.get("long-memory") || {}
28    : configNode.metadata?.["long-memory"] || {};
29
30  return {
31    maxConnections: meta.maxConnections ?? DEFAULT_MAX_CONNECTIONS,
32  };
33}
34
35// ─────────────────────────────────────────────────────────────────────────
36// TRACE WRITER
37// ─────────────────────────────────────────────────────────────────────────
38
39/**
40 * Write a cascade trace to a node's metadata.memory.
41 * Single atomic MongoDB update. Creates the structure if it doesn't exist.
42 *
43 * @param {string} nodeId - the node that received the signal
44 * @param {string} sourceId - the node that sent/originated the signal
45 * @param {string} status - cascade result status (succeeded, failed, etc.)
46 * @param {number} maxConnections - cap for the rolling connections array
47 */
48export async function writeTrace(nodeId, sourceId, status, maxConnections) {
49  const timestamp = new Date().toISOString();
50
51  await Node.findByIdAndUpdate(nodeId, {
52    $set: {
53      "metadata.memory.lastSeen": timestamp,
54      "metadata.memory.lastStatus": status,
55      "metadata.memory.lastSourceId": sourceId,
56    },
57    $inc: {
58      "metadata.memory.totalInteractions": 1,
59    },
60    $push: {
61      "metadata.memory.connections": {
62        $each: [{ sourceId, timestamp, status }],
63        $slice: -(maxConnections || DEFAULT_MAX_CONNECTIONS),
64      },
65    },
66  });
67}
68
69// ─────────────────────────────────────────────────────────────────────────
70// READER
71// ─────────────────────────────────────────────────────────────────────────
72
73/**
74 * Get the memory trace for a node.
75 *
76 * @param {string} nodeId
77 * @returns {object|null} memory trace or null
78 */
79export async function getMemory(nodeId) {
80  const node = await Node.findById(nodeId).select("metadata").lean();
81  if (!node) return null;
82
83  const meta = node.metadata instanceof Map
84    ? node.metadata.get("memory") || null
85    : node.metadata?.memory || null;
86
87  return meta;
88}
89
90/**
91 * Clear the memory trace for a node.
92 */
93export async function clearMemory(nodeId) {
94  await _metadata.unsetExtMeta(nodeId, "memory");
95}
96
1import log from "../../seed/log.js";
2import tools from "./tools.js";
3import { setMetadata, writeTrace, getMemory, getLongMemoryConfig } from "./core.js";
4
5export async function init(core) {
6  setMetadata(core.metadata);
7  const config = await getLongMemoryConfig();
8
9  // Listen to every cascade event and write a trace to the receiving node.
10  // onCascade fires at each node that receives a signal. hookData contains
11  // nodeId (target), source (sender), signalId, depth.
12  core.hooks.register("onCascade", async (hookData) => {
13    const { nodeId, source, signalId } = hookData;
14    if (!nodeId || !source) return;
15
16    // Don't trace self-origination (depth 0 is the node that wrote the content)
17    if (hookData.depth === 0) return;
18
19    const status = hookData._resultStatus || "succeeded";
20
21    try {
22      await writeTrace(nodeId, source, status, config.maxConnections);
23    } catch (err) {
24      log.debug("LongMemory", `Trace write failed at ${nodeId}: ${err.message}`);
25    }
26  }, "long-memory");
27
28  // Inject memory into AI context so the AI knows this node's relationship history
29  core.hooks.register("enrichContext", async ({ context, node, meta }) => {
30    const memory = meta.memory;
31    if (memory && memory.totalInteractions > 0) {
32      context.memory = {
33        lastSeen: memory.lastSeen,
34        lastStatus: memory.lastStatus,
35        totalInteractions: memory.totalInteractions,
36        recentSources: (memory.connections || []).slice(-5).map((c) => c.sourceId),
37      };
38    }
39  }, "long-memory");
40
41  const { default: router } = await import("./routes.js");
42
43  return {
44    router,
45    tools,
46    exports: {
47      getMemory,
48      writeTrace,
49    },
50  };
51}
52
1export default {
2  name: "long-memory",
3  version: "1.0.1",
4  builtFor: "treeos-cascade",
5  description:
6    "The difference between a tree that forgets and a tree that remembers. .flow has resultTTL. " +
7    "Active results expire. That is correct for working memory. But some information should survive " +
8    "longer. Which nodes have talked to each other. When the last signal arrived. What the last " +
9    "status was. The ghost of a connection. For each cascade result, writes a lightweight trace to " +
10    "the involved nodes. metadata.memory.lastSeen with a timestamp. metadata.memory.lastStatus with " +
11    "the status. metadata.memory.connections as a small rolling array of the last N interactions " +
12    "with source IDs and timestamps. This is not the full payload. This is not the codebook. This " +
13    "is just the trace. This node heard from that node three months ago. The last exchange succeeded. " +
14    "They have talked 47 times total. Enough for the AI through enrichContext to know the " +
15    "relationship exists and has a history. Enough for a signal arriving after years of silence to " +
16    "land on a node that remembers something was here once. The traces live in metadata so they " +
17    "survive transit, never expire unless deliberately deleted, and persist through land restarts, " +
18    "extension reinstalls, and schema migrations.",
19
20  needs: {
21    models: ["Node"],
22    extensions: ["propagation"],
23  },
24
25  optional: {},
26
27  provides: {
28    models: {},
29    routes: "./routes.js",
30    tools: true,
31    jobs: false,
32    orchestrator: false,
33    energyActions: {},
34    sessionTypes: {},
35    env: [],
36    cli: [
37      {
38        command: "memory [action]", scope: ["tree"],
39        description: "Cascade memory. No action shows trace at this node. Actions: clear, connections.",
40        method: "GET",
41        endpoint: "/node/:nodeId/memory",
42        subcommands: {
43          "clear": {
44            method: "DELETE",
45            endpoint: "/node/:nodeId/memory",
46            description: "Wipe the trace. The node forgets its cascade history.",
47          },
48          "connections": {
49            method: "GET",
50            endpoint: "/node/:nodeId/memory/connections",
51            description: "Full connection list with counts and timestamps",
52          },
53        },
54      },
55    ],
56
57    hooks: {
58      fires: [],
59      listens: ["onCascade", "enrichContext"],
60    },
61  },
62};
63
1import express from "express";
2import authenticate from "../../seed/middleware/authenticate.js";
3import { sendOk, sendError, ERR } from "../../seed/protocol.js";
4import { getMemory, clearMemory } from "./core.js";
5
6const router = express.Router();
7
8// GET /node/:nodeId/memory - trace summary
9router.get("/node/:nodeId/memory", authenticate, async (req, res) => {
10  try {
11    const memory = await getMemory(req.params.nodeId);
12    if (!memory) return sendOk(res, { message: "No cascade memory on this node." });
13    sendOk(res, {
14      lastSeen: memory.lastSeen,
15      lastStatus: memory.lastStatus,
16      lastSourceId: memory.lastSourceId,
17      totalInteractions: memory.totalInteractions || 0,
18      recentConnections: (memory.connections || []).length,
19    });
20  } catch (err) {
21    sendError(res, 500, ERR.INTERNAL, err.message);
22  }
23});
24
25// GET /node/:nodeId/memory/connections - full connection list
26router.get("/node/:nodeId/memory/connections", authenticate, async (req, res) => {
27  try {
28    const memory = await getMemory(req.params.nodeId);
29    if (!memory) return sendOk(res, { connections: [] });
30    sendOk(res, { connections: memory.connections || [] });
31  } catch (err) {
32    sendError(res, 500, ERR.INTERNAL, err.message);
33  }
34});
35
36// DELETE /node/:nodeId/memory - clear trace
37router.delete("/node/:nodeId/memory", authenticate, async (req, res) => {
38  try {
39    await clearMemory(req.params.nodeId);
40    sendOk(res, { message: "Memory cleared" });
41  } catch (err) {
42    sendError(res, 500, ERR.INTERNAL, err.message);
43  }
44});
45
46export default router;
47
1import { z } from "zod";
2import { getMemory, clearMemory } from "./core.js";
3
4export default [
5  {
6    name: "node-memory",
7    description:
8      "Get the long-term memory trace for a node. Shows when it last heard from another node, how many interactions total, and the rolling connection history.",
9    schema: {
10      nodeId: z.string().describe("The node to check."),
11      userId: z.string().describe("Injected by server. Ignore."),
12      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
13      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
14    },
15    annotations: {
16      readOnlyHint: true,
17      destructiveHint: false,
18      idempotentHint: true,
19      openWorldHint: false,
20    },
21    handler: async ({ nodeId }) => {
22      try {
23        const memory = await getMemory(nodeId);
24        if (!memory) {
25          return { content: [{ type: "text", text: "No memory traces on this node. It has not received any cascade signals." }] };
26        }
27        return {
28          content: [{
29            type: "text",
30            text: JSON.stringify({
31              lastSeen: memory.lastSeen,
32              lastStatus: memory.lastStatus,
33              lastSourceId: memory.lastSourceId,
34              totalInteractions: memory.totalInteractions || 0,
35              recentConnections: (memory.connections || []).length,
36              connections: memory.connections || [],
37            }, null, 2),
38          }],
39        };
40      } catch (err) {
41        return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
42      }
43    },
44  },
45  {
46    name: "clear-node-memory",
47    description: "Clear the long-term memory trace for a node. The node forgets all cascade history.",
48    schema: {
49      nodeId: z.string().describe("The node to clear."),
50      userId: z.string().describe("Injected by server. Ignore."),
51      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
52      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
53    },
54    annotations: {
55      readOnlyHint: false,
56      destructiveHint: true,
57      idempotentHint: true,
58      openWorldHint: false,
59    },
60    handler: async ({ nodeId }) => {
61      try {
62        await clearMemory(nodeId);
63        return { content: [{ type: "text", text: `Memory cleared on ${nodeId}.` }] };
64      } catch (err) {
65        return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
66      }
67    },
68  },
69];
70

Versions

Version Published Downloads
1.0.1 38d ago 0
1.0.0 48d ago 0
0 stars
0 flags
React from the CLI: treeos ext star long-memory

Comments

Loading comments...

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