EXTENSION for treeos-intelligence
contradiction
The tree's immune system. Notes on different nodes might contradict each other. Both valid in isolation. Together a conflict the user has not noticed. Listens to afterNote. On every note write, reads the current node enrichContext snapshot including codebook dictionaries, perspective tags, and parent summaries. Sends the new note plus contextual summary to the AI: does this note contradict anything in the existing context? When a contradiction is found, writes to metadata.contradictions on both nodes. The entry contains what conflicts, which nodes, when detected, severity (factual vs intentional vs temporal). Factual is wrong data. Intentional is a deliberate change not propagated. Temporal is something that was true before but is not now. enrichContext injects active contradictions at every position. The AI sees them and can surface them. The cascade integration is what makes it architectural. When a contradiction is detected locally, the extension fires a cascade signal with the contradiction payload. Propagation carries it to related nodes. Perspective filters determine which nodes care. The tree becomes aware of its own inconsistencies across branches. The AI does not resolve contradictions. It surfaces them. The user decides. But the tree cannot hold conflicting truths silently anymore. The immune system detects infection. The operator treats it.
v1.0.1 by TreeOS Site 0 downloads 5 files 624 lines 23.3 KB published 38d ago
treeos ext install contradiction
View changelog

Manifest

Provides

  • routes
  • tools
  • 1 CLI commands

Requires

  • services: llm
  • models: Node

Optional

  • extensions: propagation, perspective-filter, codebook
SHA256: e3a9ba12f5c2e70e27fb2790922866ab91504f42bdaac716fdd757ad78170fe6

Dependents

1 package depend on this

PackageTypeRelationship
treeos-intelligence v1.0.2bundleincludes

CLI Commands

CommandMethodDescription
contradictionsGETActive conflicts at this position. Actions: resolve, scan.
contradictions resolvePOSTMark as intentionally resolved
contradictions scanPOSTFull tree scan

Hooks

Listens To

  • afterNote
  • enrichContext

Source Code

1/**
2 * Contradiction Core
3 *
4 * Detects conflicting truths across tree branches.
5 * Writes contradiction records to both involved nodes.
6 * Fires cascade signals so the tree propagates awareness.
7 */
8
9import log from "../../seed/log.js";
10import Node from "../../seed/models/node.js";
11import Note from "../../seed/models/note.js";
12import { SYSTEM_ROLE, CONTENT_TYPE } from "../../seed/protocol.js";
13import { getContextForAi } from "../../seed/tree/treeFetch.js";
14import { parseJsonSafe } from "../../seed/orchestrators/helpers.js";
15import { v4 as uuidv4 } from "uuid";
16
17let _runChat = null;
18let _checkCascade = null;
19let _metadata = null;
20export function setServices(services) {
21  _runChat = services.runChat;
22  _checkCascade = services.checkCascade;
23  _metadata = services.metadata;
24}
25
26// ─────────────────────────────────────────────────────────────────────────
27// CONFIG
28// ─────────────────────────────────────────────────────────────────────────
29
30const DEFAULTS = {
31  maxContextChars: 8000,
32  maxContradictionsPerNode: 50,
33  cascadeOnDetection: true,
34  minNotesBetweenScans: 5,
35};
36
37export async function getContradictionConfig() {
38  const configNode = await Node.findOne({ systemRole: SYSTEM_ROLE.CONFIG }).select("metadata").lean();
39  if (!configNode) return { ...DEFAULTS };
40  const meta = configNode.metadata instanceof Map
41    ? configNode.metadata.get("contradiction") || {}
42    : configNode.metadata?.contradiction || {};
43  return { ...DEFAULTS, ...meta };
44}
45
46// ─────────────────────────────────────────────────────────────────────────
47// THROTTLE
48// ─────────────────────────────────────────────────────────────────────────
49
50/**
51 * Increment the note counter for a node. Returns the new count.
52 * Same pattern as codebook's compressionThreshold.
53 */
54export async function incrementNoteCount(nodeId) {
55  await _metadata.incExtMeta(nodeId, "contradiction", "notesSinceLastScan", 1);
56  const node = await Node.findById(nodeId).select("metadata").lean();
57  if (!node) return 0;
58  const meta = node.metadata instanceof Map
59    ? node.metadata.get("contradiction") || {}
60    : node.metadata?.contradiction || {};
61  return meta.notesSinceLastScan || 0;
62}
63
64/**
65 * Reset the note counter after a scan.
66 */
67export async function resetNoteCount(nodeId) {
68  await _metadata.batchSetExtMeta(nodeId, "contradiction", { notesSinceLastScan: 0 });
69}
70
71// ─────────────────────────────────────────────────────────────────────────
72// DETECTION
73// ─────────────────────────────────────────────────────────────────────────
74
75/**
76 * Check a new note against its node's context for contradictions.
77 * Returns array of contradiction objects or empty array.
78 */
79export async function detectContradictions(nodeId, noteContent, userId, username) {
80  if (!_runChat || !noteContent || noteContent.trim().length === 0) return [];
81
82  const config = await getContradictionConfig();
83
84  // Build context snapshot for this node
85  let contextSummary;
86  try {
87    const ctx = await getContextForAi(nodeId, {
88      includeNotes: true,
89      includeChildren: true,
90      includeParentChain: true,
91      userId,
92    });
93    contextSummary = JSON.stringify(ctx, null, 0);
94    if (contextSummary.length > config.maxContextChars) {
95      contextSummary = contextSummary.slice(0, config.maxContextChars);
96    }
97  } catch (err) {
98    log.debug("Contradiction", `Failed to build context for ${nodeId}: ${err.message}`);
99    return [];
100  }
101
102  // Find the root for runChat
103  let rootId = null;
104  try {
105    const { resolveRootNode } = await import("../../seed/tree/treeFetch.js");
106    const root = await resolveRootNode(nodeId);
107    rootId = root?._id;
108  } catch (err) {
109    log.debug("Contradiction", "resolveRootNode failed:", err.message);
110  }
111
112  const prompt =
113    `You are a contradiction detector for a knowledge tree.\n\n` +
114    `EXISTING CONTEXT at this position:\n${contextSummary}\n\n` +
115    `NEW NOTE just written:\n${noteContent}\n\n` +
116    `Does this new note contradict anything in the existing context?\n` +
117    `Only report confirmed contradictions, not differences in scope or perspective.\n\n` +
118    `If contradictions found, return JSON array:\n` +
119    `[\n` +
120    `  {\n` +
121    `    "claim": "what the new note says",\n` +
122    `    "conflictsWith": "what it contradicts in the context",\n` +
123    `    "sourceNodeName": "name of the node containing the conflicting info (if identifiable)",\n` +
124    `    "severity": "factual" | "intentional" | "temporal",\n` +
125    `    "explanation": "brief explanation of the conflict"\n` +
126    `  }\n` +
127    `]\n\n` +
128    `Severity types:\n` +
129    `- factual: wrong data, cannot both be true\n` +
130    `- intentional: a deliberate change that has not been propagated to related nodes\n` +
131    `- temporal: something that was true before but is not now\n\n` +
132    `If no contradictions found, return an empty array: []`;
133
134  try {
135    const { answer } = await _runChat({
136      userId,
137      username: username || "system",
138      message: prompt,
139      mode: "tree:respond",
140      rootId,
141      nodeId,
142      slot: "contradiction",
143    });
144
145    if (!answer) return [];
146
147    const parsed = parseJsonSafe(answer);
148    if (!Array.isArray(parsed)) return [];
149
150    // Validate and clean
151    return parsed
152      .filter((c) => c && typeof c.claim === "string" && typeof c.conflictsWith === "string")
153      .map((c) => ({
154        id: uuidv4(),
155        claim: c.claim,
156        conflictsWith: c.conflictsWith,
157        sourceNodeName: c.sourceNodeName || null,
158        severity: ["factual", "intentional", "temporal"].includes(c.severity) ? c.severity : "factual",
159        explanation: c.explanation || null,
160        detectedAt: new Date().toISOString(),
161        nodeId,
162        status: "active",
163      }));
164  } catch (err) {
165    log.warn("Contradiction", `Detection failed at ${nodeId}: ${err.message}`);
166    return [];
167  }
168}
169
170// ─────────────────────────────────────────────────────────────────────────
171// RECORD WRITING
172// ─────────────────────────────────────────────────────────────────────────
173
174/**
175 * Write contradiction records to a node's metadata.contradictions.
176 * Caps at maxContradictionsPerNode, dropping oldest resolved first.
177 */
178export async function writeContradictions(nodeId, contradictions) {
179  if (!contradictions || contradictions.length === 0) return;
180
181  const config = await getContradictionConfig();
182  const node = await Node.findById(nodeId).select("metadata").lean();
183  if (!node) return;
184
185  const meta = node.metadata instanceof Map
186    ? Object.fromEntries(node.metadata)
187    : (node.metadata || {});
188
189  const existing = Array.isArray(meta.contradictions) ? meta.contradictions : [];
190
191  // Merge new contradictions
192  const all = [...existing, ...contradictions];
193
194  // Cap: drop oldest resolved first, then oldest active
195  if (all.length > config.maxContradictionsPerNode) {
196    const resolved = all.filter((c) => c.status === "resolved");
197    const active = all.filter((c) => c.status !== "resolved");
198    const trimmed = [
199      ...active,
200      ...resolved.slice(-(config.maxContradictionsPerNode - active.length)),
201    ].slice(-config.maxContradictionsPerNode);
202    await _metadata.setExtMeta(await Node.findById(nodeId), "contradictions", trimmed);
203  } else {
204    await _metadata.setExtMeta(await Node.findById(nodeId), "contradictions", all);
205  }
206}
207
208/**
209 * Fire cascade signal for detected contradictions so related nodes become aware.
210 */
211export async function cascadeContradictions(nodeId, contradictions) {
212  if (!_checkCascade || contradictions.length === 0) return;
213
214  const config = await getContradictionConfig();
215  if (!config.cascadeOnDetection) return;
216
217  try {
218    await _checkCascade(nodeId, {
219      action: "contradiction:detected",
220      tags: ["contradiction"],
221      contradictions: contradictions.map((c) => ({
222        claim: c.claim,
223        conflictsWith: c.conflictsWith,
224        severity: c.severity,
225        sourceNodeName: c.sourceNodeName,
226      })),
227    });
228  } catch (err) {
229    log.debug("Contradiction", `Cascade failed for contradictions at ${nodeId}: ${err.message}`);
230  }
231}
232
233// ─────────────────────────────────────────────────────────────────────────
234// RESOLUTION
235// ─────────────────────────────────────────────────────────────────────────
236
237/**
238 * Resolve a contradiction by ID. Marks it as resolved with a timestamp.
239 */
240export async function resolveContradiction(nodeId, contradictionId) {
241  const node = await Node.findById(nodeId).select("metadata").lean();
242  if (!node) throw new Error("Node not found");
243
244  const meta = node.metadata instanceof Map
245    ? Object.fromEntries(node.metadata)
246    : (node.metadata || {});
247
248  const contradictions = Array.isArray(meta.contradictions) ? meta.contradictions : [];
249  const entry = contradictions.find((c) => c.id === contradictionId);
250  if (!entry) throw new Error("Contradiction not found");
251
252  entry.status = "resolved";
253  entry.resolvedAt = new Date().toISOString();
254
255  await _metadata.setExtMeta(await Node.findById(nodeId), "contradictions", contradictions);
256
257  return entry;
258}
259
260// ─────────────────────────────────────────────────────────────────────────
261// READING
262// ─────────────────────────────────────────────────────────────────────────
263
264/**
265 * Get contradictions for a node.
266 */
267export async function getContradictions(nodeId) {
268  const node = await Node.findById(nodeId).select("metadata").lean();
269  if (!node) return [];
270  const meta = node.metadata instanceof Map
271    ? Object.fromEntries(node.metadata)
272    : (node.metadata || {});
273  return Array.isArray(meta.contradictions) ? meta.contradictions : [];
274}
275
276// ─────────────────────────────────────────────────────────────────────────
277// FULL SCAN
278// ─────────────────────────────────────────────────────────────────────────
279
280/**
281 * Scan all notes in a tree for contradictions.
282 * Processes each node with notes, checking each note against context.
283 * Returns total contradictions found.
284 */
285export async function scanTree(rootId, userId, username) {
286  const { getDescendantIds } = await import("../../seed/tree/treeFetch.js");
287  const nodeIds = await getDescendantIds(rootId);
288  let totalFound = 0;
289
290  for (const nodeId of nodeIds) {
291    const node = await Node.findById(nodeId).select("systemRole status").lean();
292    if (!node || node.systemRole) continue;
293    if (node.status === "trimmed") continue;
294
295    const notes = await Note.find({ nodeId, contentType: CONTENT_TYPE.TEXT })
296      .sort({ createdAt: -1 })
297      .limit(5)
298      .select("content")
299      .lean();
300
301    if (notes.length === 0) continue;
302
303    // Check the most recent note against context
304    const contradictions = await detectContradictions(nodeId, notes[0].content, userId, username);
305    if (contradictions.length > 0) {
306      await writeContradictions(nodeId, contradictions);
307      await cascadeContradictions(nodeId, contradictions);
308      totalFound += contradictions.length;
309    }
310  }
311
312  return { nodesScanned: nodeIds.length, contradictionsFound: totalFound };
313}
314
1import log from "../../seed/log.js";
2import tools from "./tools.js";
3import { setServices, detectContradictions, writeContradictions, cascadeContradictions, incrementNoteCount, resetNoteCount, getContradictionConfig } from "./core.js";
4
5export async function init(core) {
6  const { checkCascade } = await import("../../seed/tree/cascade.js");
7  const BG = core.llm.LLM_PRIORITY.BACKGROUND;
8
9  core.llm.registerRootLlmSlot("contradiction");
10
11  setServices({
12    runChat: async (opts) => {
13      if (opts.userId && opts.userId !== "SYSTEM" && !await core.llm.userHasLlm(opts.userId)) return { answer: null };
14      return core.llm.runChat({ ...opts, llmPriority: BG });
15    },
16    checkCascade,
17    metadata: core.metadata,
18  });
19
20  // ── afterNote: throttled contradiction scanning ─────────────────────
21  // Increments a counter on every text note. Only fires the AI detection
22  // when the count hits minNotesBetweenScans. Resets on scan. Ten notes
23  // in a minute don't trigger ten LLM calls. They trigger two (at default 5).
24  const config = await getContradictionConfig();
25
26  core.hooks.register("afterNote", async ({ note, nodeId, userId, contentType, action }) => {
27    if (contentType !== "text") return;
28    if (action !== "create" && action !== "edit") return;
29    if (!userId || userId === "SYSTEM") return;
30
31    // Skip system nodes
32    try {
33      const Node = core.models.Node;
34      const node = await Node.findById(nodeId).select("systemRole").lean();
35      if (node?.systemRole) return;
36    } catch (err) {
37      log.debug("Contradiction", "systemRole check failed:", err.message);
38      return;
39    }
40
41    // Throttle: increment counter, only scan when threshold reached
42    const count = await incrementNoteCount(nodeId);
43    if (count < config.minNotesBetweenScans) return;
44
45    // Reset counter before scanning (not after, so concurrent notes don't double-fire)
46    await resetNoteCount(nodeId);
47
48    // Look up username
49    let username = null;
50    try {
51      const user = await core.models.User.findById(userId).select("username").lean();
52      username = user?.username;
53    } catch (err) {
54      log.debug("Contradiction", "username lookup failed:", err.message);
55    }
56
57    // Detect in background so we don't block the note write response
58    detectContradictions(nodeId, note.content || "", userId, username)
59      .then(async (contradictions) => {
60        if (contradictions.length === 0) return;
61
62        log.verbose("Contradiction",
63          `Detected ${contradictions.length} contradiction(s) at node ${nodeId}`,
64        );
65
66        await writeContradictions(nodeId, contradictions);
67        await cascadeContradictions(nodeId, contradictions);
68      })
69      .catch((err) => {
70        log.debug("Contradiction", `Background detection failed: ${err.message}`);
71      });
72  }, "contradiction");
73
74  // ── enrichContext: surface active contradictions to the AI ──────────
75  core.hooks.register("enrichContext", async ({ context, node, meta }) => {
76    const contradictions = meta.contradictions;
77    if (!Array.isArray(contradictions)) return;
78
79    const active = contradictions.filter((c) => c.status === "active");
80    if (active.length === 0) return;
81
82    context.contradictions = active.map((c) => ({
83      id: c.id,
84      claim: c.claim,
85      conflictsWith: c.conflictsWith,
86      severity: c.severity,
87      sourceNodeName: c.sourceNodeName,
88      detectedAt: c.detectedAt,
89    }));
90  }, "contradiction");
91
92  const { default: router } = await import("./routes.js");
93
94  return {
95    router,
96    tools,
97    exports: {
98      detectContradictions,
99      writeContradictions,
100    },
101  };
102}
103
1export default {
2  name: "contradiction",
3  version: "1.0.1",
4  builtFor: "treeos-intelligence",
5  description:
6    "The tree's immune system. Notes on different nodes might contradict each other. Both valid " +
7    "in isolation. Together a conflict the user has not noticed. Listens to afterNote. On every " +
8    "note write, reads the current node enrichContext snapshot including codebook dictionaries, " +
9    "perspective tags, and parent summaries. Sends the new note plus contextual summary to the AI: " +
10    "does this note contradict anything in the existing context? When a contradiction is found, " +
11    "writes to metadata.contradictions on both nodes. The entry contains what conflicts, which " +
12    "nodes, when detected, severity (factual vs intentional vs temporal). Factual is wrong data. " +
13    "Intentional is a deliberate change not propagated. Temporal is something that was true before " +
14    "but is not now. enrichContext injects active contradictions at every position. The AI sees " +
15    "them and can surface them. The cascade integration is what makes it architectural. When a " +
16    "contradiction is detected locally, the extension fires a cascade signal with the contradiction " +
17    "payload. Propagation carries it to related nodes. Perspective filters determine which nodes " +
18    "care. The tree becomes aware of its own inconsistencies across branches. The AI does not " +
19    "resolve contradictions. It surfaces them. The user decides. But the tree cannot hold " +
20    "conflicting truths silently anymore. The immune system detects infection. The operator treats it.",
21
22  needs: {
23    services: ["llm"],
24    models: ["Node"],
25  },
26
27  optional: {
28    extensions: ["propagation", "perspective-filter", "codebook"],
29  },
30
31  provides: {
32    models: {},
33    routes: "./routes.js",
34    tools: true,
35    jobs: false,
36    orchestrator: false,
37    energyActions: {},
38    sessionTypes: {},
39    env: [],
40
41    cli: [
42      {
43        command: "contradictions [action] [args...]", scope: ["tree"],
44        description: "Active conflicts at this position. Actions: resolve, scan.",
45        method: "GET",
46        endpoint: "/node/:nodeId/contradictions",
47        subcommands: {
48          "resolve": { method: "POST", endpoint: "/node/:nodeId/contradictions/resolve", args: ["id"], description: "Mark as intentionally resolved" },
49          "scan": { method: "POST", endpoint: "/root/:rootId/contradictions/scan", description: "Full tree scan" },
50        },
51      },
52    ],
53
54    hooks: {
55      fires: [],
56      listens: ["afterNote", "enrichContext"],
57    },
58  },
59};
60
1import express from "express";
2import authenticate from "../../seed/middleware/authenticate.js";
3import { sendOk, sendError, ERR } from "../../seed/protocol.js";
4import User from "../../seed/models/user.js";
5import { getContradictions, resolveContradiction, scanTree } from "./core.js";
6
7const router = express.Router();
8
9// GET /node/:nodeId/contradictions
10router.get("/node/:nodeId/contradictions", authenticate, async (req, res) => {
11  try {
12    const all = await getContradictions(req.params.nodeId);
13    const active = all.filter((c) => c.status === "active");
14    const resolved = all.filter((c) => c.status === "resolved");
15    sendOk(res, { active: active.length, resolved: resolved.length, contradictions: active });
16  } catch (err) {
17    sendError(res, 500, ERR.INTERNAL, err.message);
18  }
19});
20
21// POST /node/:nodeId/contradictions/resolve
22router.post("/node/:nodeId/contradictions/resolve", authenticate, async (req, res) => {
23  try {
24    const { contradictionId } = req.body;
25    if (!contradictionId) return sendError(res, 400, ERR.INVALID_INPUT, "contradictionId required");
26    const entry = await resolveContradiction(req.params.nodeId, contradictionId);
27    sendOk(res, entry);
28  } catch (err) {
29    sendError(res, 500, ERR.INTERNAL, err.message);
30  }
31});
32
33// POST /root/:rootId/contradictions/scan
34router.post("/root/:rootId/contradictions/scan", authenticate, async (req, res) => {
35  try {
36    const user = await User.findById(req.userId).select("username").lean();
37    const result = await scanTree(req.params.rootId, req.userId, user?.username);
38    sendOk(res, result);
39  } catch (err) {
40    sendError(res, 500, ERR.INTERNAL, err.message);
41  }
42});
43
44export default router;
45
1import { z } from "zod";
2import log from "../../seed/log.js";
3import { getContradictions, resolveContradiction, scanTree, detectContradictions, writeContradictions, cascadeContradictions } from "./core.js";
4
5export default [
6  {
7    name: "node-contradictions",
8    description: "Show active contradictions at a node. Surfaces conflicts between this node's content and the broader tree.",
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: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
16    handler: async ({ nodeId }) => {
17      try {
18        const all = await getContradictions(nodeId);
19        const active = all.filter((c) => c.status === "active");
20        if (active.length === 0) {
21          return { content: [{ type: "text", text: "No active contradictions at this node." }] };
22        }
23        return { content: [{ type: "text", text: JSON.stringify({ active: active.length, contradictions: active }, null, 2) }] };
24      } catch (err) {
25        return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
26      }
27    },
28  },
29  {
30    name: "resolve-contradiction",
31    description: "Mark a contradiction as intentionally resolved. The user has decided this conflict is acceptable or has been addressed.",
32    schema: {
33      nodeId: z.string().describe("The node with the contradiction."),
34      contradictionId: z.string().describe("The contradiction ID to resolve."),
35      userId: z.string().describe("Injected by server. Ignore."),
36      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
37      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
38    },
39    annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
40    handler: async ({ nodeId, contradictionId }) => {
41      try {
42        const entry = await resolveContradiction(nodeId, contradictionId);
43        return { content: [{ type: "text", text: `Contradiction resolved: "${entry.claim}" vs "${entry.conflictsWith}"` }] };
44      } catch (err) {
45        return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
46      }
47    },
48  },
49  {
50    name: "scan-contradictions",
51    description: "Force a full-tree contradiction scan. Checks the most recent note on every active node against its context. Token-intensive for large trees. Returns a cost estimate before scanning.",
52    schema: {
53      rootId: z.string().describe("The tree root to scan."),
54      confirm: z.boolean().optional().default(false).describe("Set to true to actually run the scan. Without it, returns only the cost estimate."),
55      userId: z.string().describe("Injected by server. Ignore."),
56      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
57      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
58    },
59    annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
60    handler: async ({ rootId, confirm, userId }) => {
61      try {
62        // Estimate cost before scanning
63        const { getDescendantIds } = await import("../../seed/tree/treeFetch.js");
64        const nodeIds = await getDescendantIds(rootId);
65        const { getContradictionConfig } = await import("./core.js");
66        const config = await getContradictionConfig();
67        const estimatedCalls = nodeIds.length;
68        const estimatedTokens = estimatedCalls * Math.round(config.maxContextChars * 0.3); // rough: ~0.3 tokens per char
69
70        if (!confirm) {
71          return {
72            content: [{
73              type: "text",
74              text: JSON.stringify({
75                warning: "Full tree scan is token-intensive. Review the estimate and call again with confirm: true to proceed.",
76                nodes: nodeIds.length,
77                estimatedLLMCalls: estimatedCalls,
78                estimatedTokens,
79                estimatedContextPerCall: `~${config.maxContextChars} chars`,
80              }, null, 2),
81            }],
82          };
83        }
84
85        let username = null;
86        try {
87          const User = (await import("../../seed/models/user.js")).default;
88          const user = await User.findById(userId).select("username").lean();
89          username = user?.username;
90        } catch (err) {
91          // Non-critical: scan proceeds without username
92          log.debug("Contradiction", "username lookup failed:", err.message);
93        }
94        const result = await scanTree(rootId, userId, username);
95        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
96      } catch (err) {
97        return { content: [{ type: "text", text: `Scan failed: ${err.message}` }] };
98      }
99    },
100  },
101];
102

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 contradiction

Comments

Loading comments...

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