diff --git a/apps/mcp-server/src/tools/hivemind.test.ts b/apps/mcp-server/src/tools/hivemind.test.ts new file mode 100644 index 0000000..eab583b --- /dev/null +++ b/apps/mcp-server/src/tools/hivemind.test.ts @@ -0,0 +1,83 @@ +import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { performance } from 'node:perf_hooks'; +import { defaultSettings } from '@colony/config'; +import { MemoryStore } from '@colony/core'; +import { Client } from '@modelcontextprotocol/sdk/client'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { buildServer } from '../server.js'; + +let dir: string; +let store: MemoryStore; +let client: Client; + +beforeEach(async () => { + dir = mkdtempSync(join(tmpdir(), 'colony-hivemind-')); + store = new MemoryStore({ dbPath: join(dir, 'data.db'), settings: defaultSettings }); + const server = buildServer(store, defaultSettings); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + client = new Client({ name: 'test', version: '0.0.0' }); + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); +}); + +afterEach(async () => { + await client.close(); + store.close(); + rmSync(dir, { recursive: true, force: true }); +}); + +describe('hivemind_context performance', () => { + it('keeps the compact startup path bounded with large telemetry history', async () => { + const repoRoot = join(dir, 'repo'); + mkdirSync(repoRoot, { recursive: true }); + store.startSession({ id: 'history', ide: 'test', cwd: repoRoot }); + + for (let i = 0; i < 2000; i += 1) { + const tool = i % 10 === 0 ? 'Edit' : 'mcp__colony__task_list'; + store.addObservation({ + session_id: 'history', + kind: 'tool_use', + content: tool, + metadata: { + tool, + ...(tool === 'Edit' ? { file_path: `src/file-${i}.ts` } : {}), + }, + }); + } + + for (let i = 0; i < 500; i += 1) { + store.addObservation({ + session_id: 'history', + kind: 'note', + content: `background observation ${i}`, + }); + } + + const start = performance.now(); + const res = await client.callTool({ + name: 'hivemind_context', + arguments: { + repo_root: repoRoot, + session_id: 'agent-session', + agent: 'codex', + limit: 5, + }, + }); + const elapsedMs = performance.now() - start; + const text = (res.content as Array<{ type: string; text: string }>)[0]?.text ?? '{}'; + const payload = JSON.parse(text) as { + summary: { + adoption_nudges: unknown[]; + memory_hit_count: number; + ready_work_count: number; + }; + }; + + expect(elapsedMs).toBeLessThan(500); + expect(payload.summary.memory_hit_count).toBe(0); + expect(payload.summary.adoption_nudges).toEqual([]); + expect(payload.summary.ready_work_count).toBe(0); + }); +}); diff --git a/apps/mcp-server/src/tools/hivemind.ts b/apps/mcp-server/src/tools/hivemind.ts index ae42224..a918117 100644 --- a/apps/mcp-server/src/tools/hivemind.ts +++ b/apps/mcp-server/src/tools/hivemind.ts @@ -118,6 +118,7 @@ export function register(server: McpServer, ctx: ToolContext): void { files: files ?? [], }) : null; + const explicitQuery = query?.trim() ?? ''; const contextQuery = localMode ? buildLocalContextQuery({ query, @@ -129,7 +130,7 @@ export function register(server: McpServer, ctx: ToolContext): void { let memoryHits: SearchResult[] = []; let negativeWarnings: CompactNegativeWarning[] = []; - if (contextQuery) { + if (shouldSearchContext({ explicitQuery, localMode }) && contextQuery) { const e = (await resolveEmbedder()) ?? undefined; memoryHits = localMode ? await searchLocalMemoryHits( @@ -229,7 +230,9 @@ export function register(server: McpServer, ctx: ToolContext): void { }) : undefined; const readyWorkCount = countReadyWork(store, { repo_root, repo_roots }); - const adoptionNudges = buildAdoptionNudges(store); + const adoptionNudges = shouldBuildAdoptionNudges({ explicitQuery, localMode }) + ? buildAdoptionNudges(store) + : []; const mustCheckAttention = !hasRecentAttentionInboxCall( store, attentionIdentity.session_id, @@ -280,6 +283,13 @@ function countReadyWork( store: MemoryStore, input: { repo_root: string | undefined; repo_roots: string[] | undefined }, ): number { + if (input.repo_root !== undefined && (input.repo_roots?.length ?? 0) === 0) { + return listPlans(store, { repo_root: input.repo_root, limit: 2000 }).reduce( + (total, plan) => total + plan.next_available.length, + 0, + ); + } + const roots = new Set( [input.repo_root, ...(input.repo_roots ?? [])] .filter((root): root is string => typeof root === 'string' && root.trim().length > 0) @@ -290,6 +300,17 @@ function countReadyWork( .reduce((total, plan) => total + plan.next_available.length, 0); } +function shouldSearchContext(input: { explicitQuery: string; localMode: boolean }): boolean { + return input.localMode || input.explicitQuery.length > 0; +} + +function shouldBuildAdoptionNudges(input: { + explicitQuery: string; + localMode: boolean; +}): boolean { + return input.localMode || input.explicitQuery.length > 0; +} + function buildAdoptionNudges(store: MemoryStore, now = Date.now()): HivemindAdoptionNudge[] { try { return adoptionNudgesFromMetrics(store, now);