twining_status
Retrieve detailed Twining state health check including blackboard entry count, decision counts, graph entity/relation counts, and actionable warnings.
Instructions
Overall health check of the Twining state. Shows blackboard entry count, decision counts, graph entity/relation counts, actionable warnings, and a human-readable summary. Note: twining_assemble now includes a status summary — use this only when you need the full detailed health check.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/tools/lifecycle-tools.ts:37-176 (handler)The main handler function for the twining_status tool. Gathers blackboard entries, decision counts, graph entity/relation counts, agent stats, checks for stale provisionals, archiving needs, orphan entities, and returns a structured health report with warnings and summary.
async () => { try { // Get project name from parent directory const projectRoot = path.dirname(twiningDir); const project = path.basename(projectRoot); // Count blackboard entries const { total_count: blackboard_entries } = await blackboardStore.read(); // Count decisions by status const index = await decisionStore.getIndex(); const active_decisions = index.filter( (e) => e.status === "active", ).length; const provisional_decisions = index.filter( (e) => e.status === "provisional", ).length; // Graph counts const entities = await graphStore.getEntities(); const relations = await graphStore.getRelations(); const graph_entities = entities.length; const graph_relations = relations.length; // Find last activity timestamp const recentEntries = await blackboardStore.recent(1); const lastBBActivity = recentEntries.length > 0 ? recentEntries[0]!.timestamp : null; const lastDecisionActivity = index.length > 0 ? index.reduce((latest, e) => e.timestamp > latest ? e.timestamp : latest, index[0]!.timestamp) : null; let last_activity = "none"; if (lastBBActivity && lastDecisionActivity) { last_activity = lastBBActivity > lastDecisionActivity ? lastBBActivity : lastDecisionActivity; } else if (lastBBActivity) { last_activity = lastBBActivity; } else if (lastDecisionActivity) { last_activity = lastDecisionActivity; } // Archiving threshold const archiveThreshold = config.archive.max_blackboard_entries_before_archive; const needs_archiving = blackboard_entries >= archiveThreshold; // Actionable warnings const warnings: string[] = []; // Stale provisionals: older than 7 days const sevenDaysAgo = new Date( Date.now() - 7 * 24 * 60 * 60 * 1000, ).toISOString(); const staleProvisionals = index.filter( (e) => e.status === "provisional" && e.timestamp < sevenDaysAgo, ); if (staleProvisionals.length > 0) { warnings.push( `${staleProvisionals.length} provisional decisions older than 7 days need resolution`, ); } // Archive needed if (needs_archiving) { warnings.push( `Blackboard has ${blackboard_entries} entries, archive recommended (threshold: ${archiveThreshold})`, ); } // Orphan entities: entities with zero relations if (graph_entities > 0) { const entityIds = new Set(entities.map((e) => e.id)); const connectedIds = new Set<string>(); for (const r of relations) { connectedIds.add(r.source); connectedIds.add(r.target); } const orphanCount = [...entityIds].filter( (id) => !connectedIds.has(id), ).length; if (orphanCount > 0) { warnings.push( `${orphanCount} graph entities have no relations`, ); } } // Agent counts let registered_agents = 0; let active_agents = 0; if (agentStore) { const agents = await agentStore.getAll(); registered_agents = agents.length; const thresholds = config.agents?.liveness ?? DEFAULT_LIVENESS_THRESHOLDS; const now = new Date(); active_agents = agents.filter( (a) => computeLiveness(a.last_active, now, thresholds) === "active", ).length; } // Build summary string const healthStatus = warnings.length === 0 ? "Healthy" : "Needs attention"; const warningsSummary = warnings.length > 0 ? ` ${warnings.join(". ")}.` : ""; const agentSummary = ` ${registered_agents} registered agents (${active_agents} active).`; const summary = `${healthStatus}. ${blackboard_entries} blackboard entries, ${active_decisions} active decisions, ${graph_entities} graph entities.${agentSummary}${warningsSummary}`; return toolResult({ project, blackboard_entries, active_decisions, provisional_decisions, graph_entities, graph_relations, registered_agents, active_agents, last_activity, needs_archiving, warnings, summary, }); } catch (e) { return toolError( e instanceof Error ? e.message : "Unknown error", "INTERNAL_ERROR", ); } }, ); - src/tools/lifecycle-tools.ts:20-176 (registration)The registerLifecycleTools function registers 'twining_status' (and 'twining_archive') on the MCP server. It is called from src/server.ts when toolMode is 'full'.
export function registerLifecycleTools( server: McpServer, twiningDir: string, blackboardStore: BlackboardStore, decisionStore: DecisionStore, graphStore: GraphStore, archiver: Archiver, config: TwiningConfig, agentStore: AgentStore | null = null, ): void { // twining_status — Overall health check of the Twining state server.registerTool( "twining_status", { description: "Overall health check of the Twining state. Shows blackboard entry count, decision counts, graph entity/relation counts, actionable warnings, and a human-readable summary. Note: twining_assemble now includes a status summary — use this only when you need the full detailed health check.", }, async () => { try { // Get project name from parent directory const projectRoot = path.dirname(twiningDir); const project = path.basename(projectRoot); // Count blackboard entries const { total_count: blackboard_entries } = await blackboardStore.read(); // Count decisions by status const index = await decisionStore.getIndex(); const active_decisions = index.filter( (e) => e.status === "active", ).length; const provisional_decisions = index.filter( (e) => e.status === "provisional", ).length; // Graph counts const entities = await graphStore.getEntities(); const relations = await graphStore.getRelations(); const graph_entities = entities.length; const graph_relations = relations.length; // Find last activity timestamp const recentEntries = await blackboardStore.recent(1); const lastBBActivity = recentEntries.length > 0 ? recentEntries[0]!.timestamp : null; const lastDecisionActivity = index.length > 0 ? index.reduce((latest, e) => e.timestamp > latest ? e.timestamp : latest, index[0]!.timestamp) : null; let last_activity = "none"; if (lastBBActivity && lastDecisionActivity) { last_activity = lastBBActivity > lastDecisionActivity ? lastBBActivity : lastDecisionActivity; } else if (lastBBActivity) { last_activity = lastBBActivity; } else if (lastDecisionActivity) { last_activity = lastDecisionActivity; } // Archiving threshold const archiveThreshold = config.archive.max_blackboard_entries_before_archive; const needs_archiving = blackboard_entries >= archiveThreshold; // Actionable warnings const warnings: string[] = []; // Stale provisionals: older than 7 days const sevenDaysAgo = new Date( Date.now() - 7 * 24 * 60 * 60 * 1000, ).toISOString(); const staleProvisionals = index.filter( (e) => e.status === "provisional" && e.timestamp < sevenDaysAgo, ); if (staleProvisionals.length > 0) { warnings.push( `${staleProvisionals.length} provisional decisions older than 7 days need resolution`, ); } // Archive needed if (needs_archiving) { warnings.push( `Blackboard has ${blackboard_entries} entries, archive recommended (threshold: ${archiveThreshold})`, ); } // Orphan entities: entities with zero relations if (graph_entities > 0) { const entityIds = new Set(entities.map((e) => e.id)); const connectedIds = new Set<string>(); for (const r of relations) { connectedIds.add(r.source); connectedIds.add(r.target); } const orphanCount = [...entityIds].filter( (id) => !connectedIds.has(id), ).length; if (orphanCount > 0) { warnings.push( `${orphanCount} graph entities have no relations`, ); } } // Agent counts let registered_agents = 0; let active_agents = 0; if (agentStore) { const agents = await agentStore.getAll(); registered_agents = agents.length; const thresholds = config.agents?.liveness ?? DEFAULT_LIVENESS_THRESHOLDS; const now = new Date(); active_agents = agents.filter( (a) => computeLiveness(a.last_active, now, thresholds) === "active", ).length; } // Build summary string const healthStatus = warnings.length === 0 ? "Healthy" : "Needs attention"; const warningsSummary = warnings.length > 0 ? ` ${warnings.join(". ")}.` : ""; const agentSummary = ` ${registered_agents} registered agents (${active_agents} active).`; const summary = `${healthStatus}. ${blackboard_entries} blackboard entries, ${active_decisions} active decisions, ${graph_entities} graph entities.${agentSummary}${warningsSummary}`; return toolResult({ project, blackboard_entries, active_decisions, provisional_decisions, graph_entities, graph_relations, registered_agents, active_agents, last_activity, needs_archiving, warnings, summary, }); } catch (e) { return toolError( e instanceof Error ? e.message : "Unknown error", "INTERNAL_ERROR", ); } }, ); - src/tools/lifecycle-tools.ts:33-36 (schema)Input schema for twining_status — no input parameters, only a description showing what the tool returns.
{ description: "Overall health check of the Twining state. Shows blackboard entry count, decision counts, graph entity/relation counts, actionable warnings, and a human-readable summary. Note: twining_assemble now includes a status summary — use this only when you need the full detailed health check.", }, - src/server.ts:220-233 (registration)Where registerLifecycleTools is called, enabling twining_status in 'full' tool mode.
// Extended tools (full mode only) if (toolMode === "full") { registerLifecycleTools( server, twiningDir, blackboardStore, decisionStore, graphStore, archiver, config, agentStore, ); registerGraphTools(server, graphEngine); }