1import log from "../../seed/log.js";
2import { getExtension } from "../loader.js";
3
4// Services wired from init() via setServices()
5let Node = null;
6let logContribution = async () => {};
7let useEnergy = async () => ({ energyUsed: 0 });
8let _metadata = null;
9
10export function setServices({ models, contributions, metadata }) {
11 Node = models.Node;
12 logContribution = contributions.logContribution;
13 if (metadata) _metadata = metadata;
14}
15export function setEnergyService(energy) { useEnergy = energy.useEnergy; }
16
17/**
18 * Resolve "latest" to the current prestige level for a node.
19 * Without prestige data, returns 0.
20 */
21export async function resolveVersion(nodeId, version) {
22 if (version === "latest") {
23 const node = await Node.findById(nodeId).select("metadata").lean();
24 if (!node) throw new Error("Node not found");
25 const prestige = _metadata.getExtMeta(node, "prestige");
26 return prestige?.current || 0;
27 }
28 return Number(version);
29}
30
31function calculateNextSchedule(scheduleData) {
32 if (scheduleData.schedule === null) return null;
33 const current = new Date(scheduleData.schedule);
34 return new Date(current.getTime() + scheduleData.reeffectTime * 60 * 60 * 1000).toISOString();
35}
36
37async function addPrestige({
38 nodeId,
39 userId,
40 wasAi,
41 chatId = null,
42 sessionId = null,
43}) {
44 const node = await Node.findById(nodeId).populate("children");
45 if (!node) throw new Error("Node not found");
46 if (node.systemRole) throw new Error("Cannot modify system nodes");
47
48 const { energyUsed } = await useEnergy({
49 userId,
50 action: "prestige",
51 });
52
53 const prestigeData = _metadata.getExtMeta(node, "prestige");
54 const currentLevel = prestigeData.current || 0;
55
56 await addPrestigeToNode(node);
57
58 await logContribution({
59 userId,
60 nodeId,
61 wasAi,
62 chatId,
63 sessionId,
64 action: "prestige",
65 nodeVersion: currentLevel,
66 energyUsed,
67 });
68
69 return { message: "Prestige added successfully." };
70}
71
72async function addPrestigeToNode(node) {
73 const meta = node.metadata instanceof Map ? Object.fromEntries(node.metadata) : (node.metadata || {});
74 const prestigeData = meta.prestige || { current: 0, history: [] };
75 const currentLevel = prestigeData.current || 0;
76
77 const snapshot = {
78 version: currentLevel,
79 status: node.status,
80 values: { ...(meta.values || {}) },
81 goals: { ...(meta.goals || {}) },
82 schedule: meta.schedules?.date || null,
83 reeffectTime: meta.schedules?.reeffectTime || 0,
84 archivedAt: new Date().toISOString(),
85 };
86
87 if (!prestigeData.history) prestigeData.history = [];
88 prestigeData.history.push(snapshot);
89
90 const values = meta.values || {};
91 const newValues = {};
92 for (const key of Object.keys(values)) {
93 newValues[key] = 0;
94 }
95
96 prestigeData.current = currentLevel + 1;
97
98 // Write prestige metadata (own namespace only)
99 await _metadata.setExtMeta(node, "prestige", prestigeData);
100
101 // Reset values via the values extension's export (not direct namespace write)
102 const valuesExt = getExtension("values");
103 if (valuesExt?.exports?.setValueForNode) {
104 for (const key of Object.keys(newValues)) {
105 try {
106 await valuesExt.exports.setValueForNode({
107 nodeId: node._id.toString(), key, value: 0,
108 userId: node.rootOwner?.toString() || "system",
109 });
110 } catch (err) {
111 log.debug("Prestige", "Value reset failed for key " + key + ":", err.message);
112 }
113 }
114 }
115
116 // Advance schedule via the schedules extension's export (not direct namespace write)
117 const schedulesExt = getExtension("schedules");
118 if (schedulesExt?.exports?.updateSchedule && meta.schedules?.date) {
119 const scheduleData = { schedule: new Date(meta.schedules.date), reeffectTime: meta.schedules?.reeffectTime || 0 };
120 const newSchedule = calculateNextSchedule(scheduleData);
121 if (newSchedule) {
122 try {
123 await schedulesExt.exports.updateSchedule(node._id.toString(), { schedule: newSchedule });
124 } catch (err) {
125 log.debug("Prestige", "Schedule advance failed:", err.message);
126 }
127 }
128 }
129
130 // Reset status to active via direct DB update.
131 // Note: this bypasses beforeStatusChange/afterStatusChange hooks intentionally.
132 // Prestige is a kernel-level reset, not a user status change.
133 await Node.updateOne({ _id: node._id }, { $set: { status: "active" } });
134}
135
136export { addPrestige, addPrestigeToNode };
137
1import tools from "./tools.js";
2import { setServices, setEnergyService, addPrestige, resolveVersion } from "./core.js";
3
4export async function init(core) {
5 setServices({ models: core.models, contributions: core.contributions, metadata: core.metadata });
6 if (core.energy) setEnergyService(core.energy);
7
8 const { default: router, setNodeModel } = await import("./routes.js");
9 setNodeModel(core.models.Node);
10
11 const Node = core.models.Node;
12
13 core.hooks.register("beforeNote", async (data) => {
14 const node = await Node.findById(data.nodeId).select("metadata").lean();
15 if (!node) return;
16 const prestige = core.metadata.getExtMeta(node, "prestige");
17 if (!data.metadata) data.metadata = {};
18 data.metadata.version = prestige?.current || 0;
19 }, "prestige");
20
21 core.hooks.register("beforeContribution", async (data) => {
22 const node = await Node.findById(data.nodeId).select("metadata").lean();
23 if (!node) return;
24 const prestige = core.metadata.getExtMeta(node, "prestige");
25 if (prestige?.current) {
26 data.nodeVersion = String(prestige.current);
27 }
28 }, "prestige");
29
30 core.hooks.register("enrichContext", async ({ context, node, meta }) => {
31 const prestige = meta.prestige;
32 if (prestige?.current) {
33 context.prestige = prestige.current;
34 context.totalVersions = (prestige.history?.length || 0) + 1;
35 }
36 }, "prestige");
37
38 // Register navigation for prestige tool (if treeos-base installed)
39 try {
40 const { getExtension } = await import("../loader.js");
41 const base = getExtension("treeos-base");
42 if (base?.exports?.registerToolNavigation) {
43 base.exports.registerToolNavigation("add-node-prestige", ({ args, withToken: t }) =>
44 t(`/api/v1/node/${args.nodeId}/${args.prestige || 0}?html`));
45 }
46 } catch {}
47
48 // Register UI slots
49 try {
50 const { getExtension } = await import("../loader.js");
51 const treeos = getExtension("treeos-base");
52 if (treeos?.exports?.registerSlot) {
53 // Versions list on node detail page
54 treeos.exports.registerSlot("node-detail-sections", "prestige", ({ node, nodeId, qs, isPublicAccess }) => {
55 const meta = node.metadata instanceof Map ? Object.fromEntries(node.metadata) : (node.metadata || {});
56 const prestige = meta.prestige || { current: 0, history: [] };
57 return `<div class="versions-section">
58 <h2>Versions</h2>
59 <ul class="versions-list">
60 ${[...Array(prestige.current + 1)].map((_, i) =>
61 `<li><a href="/api/v1/node/${nodeId}/${i}${qs}">Version ${i}${i === prestige.current ? " (current)" : ""}</a></li>`
62 ).reverse().join("")}
63 </ul>
64 ${!isPublicAccess ? `<form method="POST" action="/api/v1/node/${nodeId}/prestige${qs}"
65 onsubmit="return confirm('Complete current version and create new prestige level?')" style="margin-top:16px;">
66 <button type="submit" class="primary-button">Add New Version</button>
67 </form>` : ""}
68 </div>`;
69 }, { priority: 10 });
70
71 // Version badge on version detail page
72 treeos.exports.registerSlot("version-badge", "prestige", ({ version, data }) => {
73 return `<span class="version-badge version-status-${data?.status || "active"}">Version ${version}</span>`;
74 }, { priority: 10 });
75
76 // Version control on version detail page
77 treeos.exports.registerSlot("version-detail-sections", "prestige", ({ nodeId, version, qs, showPrestige }) => {
78 if (!showPrestige) return "";
79 return `<div class="actions-section">
80 <h3>Version Control</h3>
81 <form method="POST" action="/api/v1/node/${nodeId}/${version}/prestige${qs}"
82 onsubmit="return confirm('Complete current version and create new prestige level?')" class="action-form">
83 <button type="submit" class="primary-button">Add New Version</button>
84 </form>
85 </div>`;
86 }, { priority: 10 });
87 }
88 } catch {}
89
90 return {
91 router,
92 tools,
93 modeTools: [
94 { modeKey: "tree:edit", toolNames: ["add-node-prestige"] },
95 ],
96 exports: { addPrestige, resolveVersion },
97 };
98}
99
1export default {
2 name: "prestige",
3 version: "1.0.1",
4 builtFor: "TreeOS",
5 description:
6 "Nodes do not just accumulate forever. At some point the current phase of work is done. The " +
7 "budget was met. The sprint shipped. The chapter was written. Prestige closes the book on the " +
8 "current version and opens a new one. When you prestige a node, the extension snapshots " +
9 "everything: status, values, goals, schedule, reeffect time. That snapshot goes into the " +
10 "prestige history array. Then it resets. Values go to zero. Status returns to active. The " +
11 "schedule advances by the reeffect interval. The version counter increments. The node starts " +
12 "fresh with a clean slate, but the full record of every previous version is preserved in " +
13 "metadata.prestige.history." +
14 "\n\n" +
15 "Every note written to a prestiged node is tagged with the version number at time of writing " +
16 "via the beforeNote hook. Every contribution is stamped with the current version via " +
17 "beforeContribution. This means you can query the full history of a node and see exactly " +
18 "which version each piece of content and each action belonged to. Notes from version 2 are " +
19 "distinguishable from notes in version 5." +
20 "\n\n" +
21 "Cross-extension coordination happens through the extension loader. When prestige resets " +
22 "values, it calls the values extension's setValueForNode export, not a direct metadata " +
23 "write. When it advances the schedule, it calls the schedules extension's updateSchedule " +
24 "export. If those extensions are not installed, those resets simply do not happen. The " +
25 "prestige itself still works. enrichContext injects the current version number and total " +
26 "version count so the AI always knows what generation it is working in.",
27
28 needs: {
29 services: ["contributions", "hooks"],
30 models: ["Node"],
31 },
32
33 optional: {
34 services: ["energy"],
35 extensions: ["values", "schedules", "treeos-base"],
36 },
37
38 provides: {
39 models: {},
40 routes: "./routes.js",
41 tools: true,
42 jobs: false,
43 orchestrator: false,
44 energyActions: {
45 prestige: { cost: 1 },
46 },
47 sessionTypes: {},
48 cli: [
49 { command: "prestige", scope: ["tree"], description: "Add new version to current node", method: "POST", endpoint: "/node/:nodeId/prestige" },
50 ],
51 hooks: {
52 fires: [],
53 listens: ["beforeNote", "beforeContribution", "enrichContext"],
54 },
55 },
56};
57
1import log from "../../seed/log.js";
2import express from "express";
3import { sendOk, sendError, ERR } from "../../seed/protocol.js";
4import authenticate from "../../seed/middleware/authenticate.js";
5import { addPrestige } from "./core.js";
6
7let _Node = null;
8export function setNodeModel(Node) { _Node = Node; }
9
10const router = express.Router();
11
12async function useLatest(req, res, next) {
13 try {
14 const node = await _Node.findById(req.params.nodeId).select("metadata").lean();
15 if (!node) return sendError(res, 404, ERR.NODE_NOT_FOUND, "Node not found");
16 const meta = node.metadata instanceof Map ? node.metadata.get("prestige") : node.metadata?.prestige;
17 req.params.version = String(meta?.current || 0);
18 next();
19 } catch (err) {
20 sendError(res, 500, ERR.INTERNAL, err.message);
21 }
22}
23
24const prestigeHandler = async (req, res) => {
25 try {
26 const { nodeId, version } = req.params;
27 const userId = req.userId;
28
29 const nextVersion = Number(version) + 1;
30
31 if (Number.isNaN(nextVersion)) {
32 return sendError(res, 400, ERR.INVALID_INPUT, "Invalid version");
33 }
34
35 const result = await addPrestige({
36 nodeId,
37 userId,
38 });
39
40 if ("html" in req.query) {
41 return res.redirect(
42 `/api/v1/node/${nodeId}/${nextVersion}?token=${req.query.token ?? ""}&html`,
43 );
44 }
45
46 sendOk(res, result);
47 } catch (err) {
48 log.error("Prestige", "prestige error:", err);
49 sendError(res, 400, ERR.INVALID_INPUT, err.message);
50 }
51};
52
53router.post("/node/:nodeId/prestige", authenticate, useLatest, prestigeHandler);
54router.post("/node/:nodeId/:version/prestige", authenticate, prestigeHandler);
55
56export default router;
57
1import { z } from "zod";
2import { addPrestige, resolveVersion } from "./core.js";
3import { createNote } from "../../seed/tree/notes.js";
4
5export default [
6 {
7 name: "create-node-version-note",
8 description:
9 "Create a text note tagged with the node's current prestige version. Use this instead of create-node-note when version tracking matters.",
10 schema: {
11 content: z.string().describe("The text content of the note."),
12 nodeId: z.string().describe("The ID of the node the note belongs to."),
13 userId: z.string().describe("Injected by server. Ignore."),
14 chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
15 sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
16 },
17 annotations: {
18 readOnlyHint: false,
19 destructiveHint: false,
20 idempotentHint: false,
21 },
22 handler: async ({ content, nodeId, userId, chatId, sessionId }) => {
23 try {
24 const version = await resolveVersion(nodeId, "latest");
25 const result = await createNote({
26 contentType: "text",
27 content,
28 userId,
29 nodeId,
30 wasAi: true,
31 chatId,
32 sessionId,
33 metadata: {
34 treeos: { isReflection: true },
35 prestige: { version: version || 0 },
36 },
37 });
38 return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
39 } catch (err) {
40 return { content: [{ type: "text", text: `Failed to create versioned note: ${err.message}` }] };
41 }
42 },
43 },
44 {
45 name: "add-node-prestige",
46 description:
47 "Calls addPrestige() to increment a node's prestige level and create a new version.",
48 schema: {
49 nodeId: z
50 .string()
51 .describe("The unique ID of the node to add prestige to."),
52 userId: z.string().describe("Injected by server. Ignore."),
53 chatId: z
54 .string()
55 .nullable()
56 .optional()
57 .describe("Injected by server. Ignore."),
58 sessionId: z
59 .string()
60 .nullable()
61 .optional()
62 .describe("Injected by server. Ignore."),
63 },
64 annotations: {
65 readOnlyHint: false,
66 destructiveHint: false,
67 idempotentHint: false,
68 openWorldHint: false,
69 },
70 handler: async ({ nodeId, userId, chatId, sessionId }) => {
71 try {
72 const result = await addPrestige({
73 nodeId,
74 userId,
75 wasAi: true,
76 chatId,
77 sessionId,
78 });
79
80 return {
81 content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
82 };
83 } catch (err) {
84 return {
85 content: [
86 { type: "text", text: `Failed to add prestige: ${err.message}` },
87 ],
88 };
89 }
90 },
91 },
92];
93
Loading comments...