Build Session Context
graph_build_contextConsolidate graph health, recent entities, knowledge hubs, and contradictions into a single context bundle. Reduces multiple API calls to one for efficient session startup.
Instructions
Single tool call that bundles a session's worth of context: graph health, pending work, last dream run summary, recent additions, top knowledge hubs, unresolved contradictions, and (optionally) a topic neighbourhood. Use this at session start instead of running graph_stats / graph_query / graph_contradictions separately. Cuts 4-5 round trips to one.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| topic | No | Optional topic to fetch a neighbourhood for (uses graph_query under the hood). | |
| project_cwd | No | Optional project directory for affinity scoring on the topic neighbourhood. | |
| recent_days | No | Window in days for 'recently added' entities (default 7). | |
| hub_count | No | Number of top knowledge hubs to include (default 5). | |
| include_contradictions | No | Include unresolved contradictions (default true). | |
| max_recent | No | Max recent entities to list (default 15). |
Implementation Reference
- src/mcp-server/index.ts:1165-1242 (handler)The async handler function that executes the graph_build_context tool logic. It calls client.getStats, client.getRecentAdditions, client.getTopHubs, client.findContradictions, and client.query in parallel, then reads file-based context (pending work counts, last dream run from audit log or manifest) and assembles a comprehensive session context response.
}, async (args) => { try { const tenantId = currentTenant(); const recentDays = args.recent_days ?? 7; const hubCount = args.hub_count ?? 5; const includeContradictions = args.include_contradictions ?? true; const maxRecent = args.max_recent ?? 15; // Run the graph queries in parallel — independent const [statsResult, recent, hubs, contradictions, topicResult] = await Promise.all([ client.getStats(tenantId), client.getRecentAdditions(tenantId, recentDays, maxRecent), client.getTopHubs(tenantId, hubCount), includeContradictions ? client.findContradictions(tenantId, false) : Promise.resolve({ contradictions: [] as Array<Record<string, unknown>> }), args.topic ? client.query(tenantId, [args.topic], { max_hops: 2, min_weight: 0.3, limit: 15, project_context: args.project_cwd, current_only: true, }) : Promise.resolve(null), ]); // File-based context (non-graph) const pendingWork = countPendingWork(); const lastDream = readLastDreamFromAudit() ?? lastDreamFromManifest(); const hoursSinceLastDream = pendingWork.last_dream_run ? Math.round(((Date.now() - new Date(pendingWork.last_dream_run).getTime()) / (1000 * 60 * 60)) * 10) / 10 : null; return toolResult({ generated_at: new Date().toISOString(), graph_health: { nodes: statsResult.nodes.total, edges: statsResult.edges.total, by_node_type: statsResult.nodes.by_type, avg_weight: statsResult.health.avg_weight, orphaned: statsResult.health.orphaned_nodes, stale: statsResult.health.stale_nodes, unresolved_contradictions: statsResult.health.unresolved_contradictions, }, pending_work: { unprocessed_transcripts: pendingWork.unprocessed_transcripts, pending_ingests: pendingWork.pending_ingests, last_dream_run: pendingWork.last_dream_run, hours_since_last_dream: hoursSinceLastDream, }, last_dream: lastDream, recent_additions: { days: recentDays, entity_count: recent.nodes.length, edge_count: recent.edge_count, entities: recent.nodes, }, top_hubs: hubs, contradictions: includeContradictions ? (contradictions as { contradictions: Array<Record<string, unknown>> }).contradictions : null, topic_neighbourhood: topicResult ? { topic: args.topic ?? "", node_count: topicResult.nodes.length, edge_count: topicResult.edges.length, nodes: topicResult.nodes.slice(0, 15), edges: topicResult.edges.slice(0, 25), } : null, }); } catch (err) { const e = err instanceof Error ? err : new Error(String(err)); return toolError(`graph_build_context failed: ${e.message}`); } }); - src/mcp-server/index.ts:1125-1163 (schema)Input schema for graph_build_context defining optional parameters: topic, project_cwd, recent_days (default 7), hub_count (default 5), include_contradictions (default true), and max_recent (default 15).
inputSchema: { topic: z .string() .optional() .describe("Optional topic to fetch a neighbourhood for (uses graph_query under the hood)."), project_cwd: z .string() .optional() .describe("Optional project directory for affinity scoring on the topic neighbourhood."), recent_days: z .number() .int() .min(1) .max(90) .optional() .default(7) .describe("Window in days for 'recently added' entities (default 7)."), hub_count: z .number() .int() .min(1) .max(20) .optional() .default(5) .describe("Number of top knowledge hubs to include (default 5)."), include_contradictions: z .boolean() .optional() .default(true) .describe("Include unresolved contradictions (default true)."), max_recent: z .number() .int() .min(1) .max(50) .optional() .default(15) .describe("Max recent entities to list (default 15)."), }, - src/mcp-server/index.ts:1118-1242 (registration)Registration of the tool via server.registerTool('graph_build_context', ...) with title 'Build Session Context', description, inputSchema, and the handler function.
server.registerTool("graph_build_context", { title: "Build Session Context", description: "Single tool call that bundles a session's worth of context: graph health, pending work, last dream " + "run summary, recent additions, top knowledge hubs, unresolved contradictions, and (optionally) a " + "topic neighbourhood. Use this at session start instead of running graph_stats / graph_query / " + "graph_contradictions separately. Cuts 4-5 round trips to one.", inputSchema: { topic: z .string() .optional() .describe("Optional topic to fetch a neighbourhood for (uses graph_query under the hood)."), project_cwd: z .string() .optional() .describe("Optional project directory for affinity scoring on the topic neighbourhood."), recent_days: z .number() .int() .min(1) .max(90) .optional() .default(7) .describe("Window in days for 'recently added' entities (default 7)."), hub_count: z .number() .int() .min(1) .max(20) .optional() .default(5) .describe("Number of top knowledge hubs to include (default 5)."), include_contradictions: z .boolean() .optional() .default(true) .describe("Include unresolved contradictions (default true)."), max_recent: z .number() .int() .min(1) .max(50) .optional() .default(15) .describe("Max recent entities to list (default 15)."), }, annotations: { readOnlyHint: true }, }, async (args) => { try { const tenantId = currentTenant(); const recentDays = args.recent_days ?? 7; const hubCount = args.hub_count ?? 5; const includeContradictions = args.include_contradictions ?? true; const maxRecent = args.max_recent ?? 15; // Run the graph queries in parallel — independent const [statsResult, recent, hubs, contradictions, topicResult] = await Promise.all([ client.getStats(tenantId), client.getRecentAdditions(tenantId, recentDays, maxRecent), client.getTopHubs(tenantId, hubCount), includeContradictions ? client.findContradictions(tenantId, false) : Promise.resolve({ contradictions: [] as Array<Record<string, unknown>> }), args.topic ? client.query(tenantId, [args.topic], { max_hops: 2, min_weight: 0.3, limit: 15, project_context: args.project_cwd, current_only: true, }) : Promise.resolve(null), ]); // File-based context (non-graph) const pendingWork = countPendingWork(); const lastDream = readLastDreamFromAudit() ?? lastDreamFromManifest(); const hoursSinceLastDream = pendingWork.last_dream_run ? Math.round(((Date.now() - new Date(pendingWork.last_dream_run).getTime()) / (1000 * 60 * 60)) * 10) / 10 : null; return toolResult({ generated_at: new Date().toISOString(), graph_health: { nodes: statsResult.nodes.total, edges: statsResult.edges.total, by_node_type: statsResult.nodes.by_type, avg_weight: statsResult.health.avg_weight, orphaned: statsResult.health.orphaned_nodes, stale: statsResult.health.stale_nodes, unresolved_contradictions: statsResult.health.unresolved_contradictions, }, pending_work: { unprocessed_transcripts: pendingWork.unprocessed_transcripts, pending_ingests: pendingWork.pending_ingests, last_dream_run: pendingWork.last_dream_run, hours_since_last_dream: hoursSinceLastDream, }, last_dream: lastDream, recent_additions: { days: recentDays, entity_count: recent.nodes.length, edge_count: recent.edge_count, entities: recent.nodes, }, top_hubs: hubs, contradictions: includeContradictions ? (contradictions as { contradictions: Array<Record<string, unknown>> }).contradictions : null, topic_neighbourhood: topicResult ? { topic: args.topic ?? "", node_count: topicResult.nodes.length, edge_count: topicResult.edges.length, nodes: topicResult.nodes.slice(0, 15), edges: topicResult.edges.slice(0, 25), } : null, }); } catch (err) { const e = err instanceof Error ? err : new Error(String(err)); return toolError(`graph_build_context failed: ${e.message}`); } }); - src/mcp-server/index.ts:963-1025 (helper)Helper function readLastDreamFromAudit() — reads the last ~32KB of dream-audit.jsonl to extract the most recent run_start/run_end pair.
function readLastDreamFromAudit(): { started_at: string; ended_at: string | null; duration_ms: number | null; source: string | null; transcripts_processed: number | null; ingest_processed: number | null; entities_created: number | null; edges_created: number | null; errors: number | null; } | null { const auditPath = join(GRAPH_MEMORY_HOME, "logs", "dream-audit.jsonl"); let raw: string; let didTruncate = false; try { const stats = statSync(auditPath); const tailBytes = Math.min(stats.size, 32768); didTruncate = stats.size > tailBytes; const fd = openSync(auditPath, "r"); try { const buf = Buffer.alloc(tailBytes); readSync(fd, buf, 0, tailBytes, stats.size - tailBytes); raw = buf.toString("utf-8"); } finally { closeSync(fd); } } catch { return null; } // Parse the lines we got (drop the first one if we may have started mid-line) const lines = raw.split("\n").filter((l) => l.trim().length > 0); if (lines.length === 0) return null; if (didTruncate) lines.shift(); let lastStart: Record<string, unknown> | null = null; let lastEnd: Record<string, unknown> | null = null; for (const line of lines) { try { const evt = JSON.parse(line) as Record<string, unknown>; if (evt["event"] === "run_start") { lastStart = evt; lastEnd = null; } else if (evt["event"] === "run_end") { lastEnd = evt; } } catch { /* skip bad lines */ } } if (!lastStart) return null; return { started_at: String(lastStart["timestamp"] ?? ""), ended_at: lastEnd ? String(lastEnd["timestamp"] ?? "") : null, duration_ms: lastEnd ? Number(lastEnd["duration_ms"] ?? 0) : null, source: String(lastStart["source"] ?? "") || null, transcripts_processed: lastEnd ? Number(lastEnd["transcripts_processed"] ?? 0) : null, ingest_processed: lastEnd ? Number(lastEnd["ingest_processed"] ?? 0) : null, entities_created: lastEnd ? Number(lastEnd["entities_created"] ?? 0) : null, edges_created: lastEnd ? Number(lastEnd["edges_created"] ?? 0) : null, errors: lastEnd ? Number(lastEnd["errors"] ?? 0) : null, }; } - src/mcp-server/index.ts:1027-1075 (helper)Helper function lastDreamFromManifest() — fallback to read last dream info from manifest.json when audit log is missing.
/** Read the manifest and synthesize a minimal last_dream record from it. Used when dream-audit.jsonl is missing or pre-dates the audit log feature. */ function lastDreamFromManifest(): { started_at: string; ended_at: string | null; duration_ms: number | null; source: string | null; transcripts_processed: number | null; ingest_processed: number | null; entities_created: number | null; edges_created: number | null; errors: number | null; } | null { try { const manifestPath = join(GRAPH_MEMORY_HOME, "processed", "manifest.json"); const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { last_dream_run?: string | null; processed?: Record<string, { processed_at?: string; entities_extracted?: number; edges_created?: number }>; }; if (!manifest.last_dream_run) return null; // Sum stats for transcripts whose processed_at is within 1 hour of last_dream_run const lastRun = new Date(manifest.last_dream_run).getTime(); let entitiesCreated = 0; let edgesCreated = 0; let transcriptsProcessed = 0; for (const entry of Object.values(manifest.processed ?? {})) { if (!entry.processed_at) continue; const t = new Date(entry.processed_at).getTime(); if (Math.abs(t - lastRun) > 1000 * 60 * 60) continue; transcriptsProcessed++; entitiesCreated += entry.entities_extracted ?? 0; edgesCreated += entry.edges_created ?? 0; } return { started_at: manifest.last_dream_run, ended_at: null, duration_ms: null, source: "manifest", transcripts_processed: transcriptsProcessed, ingest_processed: null, entities_created: entitiesCreated, edges_created: edgesCreated, errors: null, }; } catch { return null; } }