diff --git a/adolf/openclaw.json b/adolf/openclaw.json index 777ed29..5eba227 100644 --- a/adolf/openclaw.json +++ b/adolf/openclaw.json @@ -131,6 +131,15 @@ Authorization: "Bearer ${MARKETPLACE_MCP_TOKEN}", }, }, + // agap-mcp (kb#64) -- the SAME shared agap-mcp instance Claude Code uses + // (network_mode: host, :3100, unauthenticated on localhost). Grants Adolf + // the same access as Claude: vault (vw_* for credential fetching) plus + // gitea/ha/zabbix/radicale. Reached via host.docker.internal like + // kanboard/marketplace above. + agap: { + type: "http", + url: "http://host.docker.internal:3100/mcp", + }, }, }, @@ -138,27 +147,6 @@ // load a plugin at startup (discovery alone is not enough). plugins: { entries: { - // Cognee memory plugin — DISABLED as of kb #75 (H3): superseded by - // hindsight-memory below. Left `enabled: false` rather than removed — - // full teardown (plugin dir, cognee/cognee-mcp/cognee-llm containers) - // is kb #75's H4, a separate step so the Hindsight path can be proven - // out first. Kept disabled (not both active) to avoid double - // prependContext injection and double persisted turns while both - // backends exist side by side. - "cognee-memory": { - enabled: false, - // External (non-bundled) plugins must opt in to conversation + prompt-injection - // hook access explicitly. before_prompt_build => allowPromptInjection; - // agent_end => allowConversationAccess. - hooks: { allowConversationAccess: true, allowPromptInjection: true }, - // Throttle background cognify hard to stop it draining the Kimi quota: - // one cognify per dataset per hour (was 5 min default), and a longer - // persist timeout so /add doesn't fail-and-retry when cognee is busy. - config: { - sweepIntervalMs: 3600000, // 1h (default 300000 = 5min) - persistTimeoutMs: 20000, // 20s (default 8000) - }, - }, // Hindsight memory plugin (kb #75, H3) — installed external plugin // under .openclaw/extensions/hindsight-memory, bind-mounted read-only // from openai/hindsight-openclaw-plugin (see that project's diff --git a/openai/docker-compose.yml b/openai/docker-compose.yml index 20e6edf..24a5750 100644 --- a/openai/docker-compose.yml +++ b/openai/docker-compose.yml @@ -210,17 +210,17 @@ services: ["node", "dist/index.js", "gateway", "--bind", "lan", "--port", "18789"] restart: unless-stopped - # cognee-llm — stateless one-shot Kimi-CLI wrapper for Cognee's batch cognify - # (P3). Opposite policy to kimi-agent: no resume, non-streaming, text-only. - # Note (SPIKE-FINDINGS gate 5): Cognee should DEFAULT its LLM to LiteLLM; this - # is the optional low-volume path. Needs `kimi login` in its own volume. - cognee-llm: - build: ./cognee-llm - container_name: cognee-llm + # hindsight-llm — standalone clone of cognee-llm (kb#76, H4 option B): the + # dedicated Kimi-CLI wrapper that is now Hindsight's LLM, so the whole cognee + # stack (incl. cognee-llm) can be decommissioned. Own port (:8012) + own + # kimi-code volume; needs a one-time `kimi login` seeded into hindsight-llm-home. + hindsight-llm: + build: ./hindsight-llm + container_name: hindsight-llm ports: - - "8011:8011" + - "8012:8012" volumes: - - cognee-llm-home:/root/.kimi-code + - hindsight-llm-home:/root/.kimi-code restart: unless-stopped # adolf-llm — conversational Kimi-CLI wrapper (:8010), the model backend for @@ -244,68 +244,6 @@ services: - "host.docker.internal:host-gateway" restart: unless-stopped - # cognee — Adolf's memory backend (P4). FastAPI + embedded Kuzu graph + - # Qdrant vectors. LLM via cognee-llm:8011 (Kimi CLI wrapper), embeddings via - # ollama directly (host.docker.internal:11436, separate compose project — - # hence extra_hosts below). Sole owner of the on-disk Kuzu/SQLite files - # under /mnt/ssd/dbs/cognee/ (Kuzu is not safe for concurrent multi-process - # access) — never run a second process against those files. - cognee: - build: ./cognee - container_name: cognee - restart: unless-stopped - environment: - # Real OS env var, not just the mounted .env file: the qdrant vector - # adapter's registration hook (cognee/Dockerfile's sitecustomize.py) - # gates on os.environ.get("VECTOR_DB_PROVIDER") at Python interpreter - # start, which only sees actual container env vars — pydantic-settings' - # env_file=".env" parsing (used for the rest of cognee.env) never - # populates os.environ itself. Without this, cognee raises - # "Unsupported vector database provider: qdrant" at startup even though - # cognee.env sets VECTOR_DB_PROVIDER=qdrant. Verified 2026-07-05. - - VECTOR_DB_PROVIDER=qdrant - volumes: - - ./cognee/cognee.env:/app/.env - - /mnt/ssd/dbs/cognee/data:/data - - /mnt/ssd/dbs/cognee/system:/system - extra_hosts: - - "host.docker.internal:host-gateway" - # Not published to the host — only cognee-mcp (same compose network) - # needs to reach it. Uncomment for local debugging: - # ports: - # - "8000:8000" - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # cognee-mcp — thin MCP-to-HTTP proxy in API mode (API_URL=cognee:8000). - # Never opens the graph/vector files itself, so it's safe to run alongside - # `cognee` without a second writer on the same Kuzu database. Exposes 3 - # tools: remember / recall / forget. - # - # Built from a local Dockerfile (kb#70 fix) instead of the bare upstream - # image: forget was missing a data_id parameter end-to-end, so agents - # could delete a whole dataset but never a single entry. See - # ./cognee-mcp/Dockerfile and ./cognee-mcp/src/ for the patched files. - cognee-mcp: - build: ./cognee-mcp - container_name: cognee-mcp - restart: unless-stopped - environment: - - ENV=local - - LOG_LEVEL=INFO - - PYTHONUNBUFFERED=1 - - TRANSPORT_MODE=http - - API_URL=http://cognee:8000 - - MCP_ALLOWED_HOSTS=cognee-mcp:* - ports: - - "8001:8000" - depends_on: - - cognee - # hindsight — Adolf memory backend, replacing cognee/cognee-mcp/cognee-llm # (kb#73, migration doc agap_git/adolf/HINDSIGHT-MIGRATION.md, H1). One # container: REST API :8888 (also serves the built-in MCP at /mcp/{bank}/), @@ -317,7 +255,7 @@ services: # LLM + embeddings reconfigured 2026-07-15 (kb#84) to fix two wrong H1 # choices for a Russian/multilingual use case: # - # LLM -> cognee-llm:8011 (the existing Kimi-CLI wrapper, same shim cognee + # LLM -> hindsight-llm:8012 (dedicated Kimi-CLI wrapper cloned from the shim cognee # uses — see cognee/cognee.env's LLM section for the full precedent, # including why LLM_INSTRUCTOR_MODE=json_mode isn't needed here since # Hindsight's own client doesn't go through `instructor`). Replaces the @@ -353,11 +291,11 @@ services: restart: unless-stopped environment: - HINDSIGHT_API_LLM_PROVIDER=openai - - HINDSIGHT_API_LLM_BASE_URL=http://cognee-llm:8011/v1 - - HINDSIGHT_API_LLM_MODEL=openai/cognee-llm - # cognee-llm ignores the key entirely (Kimi CLI wrapper, no real - # OpenAI auth) — same dummy value cognee.env uses for LLM_API_KEY. - - HINDSIGHT_API_LLM_API_KEY=sk-cognee-llm-local + - HINDSIGHT_API_LLM_BASE_URL=http://hindsight-llm:8012/v1 + - HINDSIGHT_API_LLM_MODEL=openai/hindsight-llm + # hindsight-llm ignores the key entirely (Kimi CLI wrapper, no real + # OpenAI auth) — dummy value, non-empty so the client constructs. + - HINDSIGHT_API_LLM_API_KEY=sk-hindsight-llm-local - HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai - HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL=http://host.docker.internal:11436/v1 - HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=bge-m3 @@ -393,7 +331,7 @@ services: # file. - "host.docker.internal:host-gateway" depends_on: - - cognee-llm + - hindsight-llm # openclaw-tools — MCP bridge (P5) exposing a minimal slice of the Adolf # OpenClaw gateway's agent tools (message/cron/nodes/browser) over MCP @@ -417,6 +355,6 @@ services: volumes: kimi-agent-home: adolf-state: - cognee-llm-home: + hindsight-llm-home: adolf-llm-workspace: adolf-llm-home: diff --git a/openai/hindsight-llm/Dockerfile b/openai/hindsight-llm/Dockerfile new file mode 100644 index 0000000..444ebf1 --- /dev/null +++ b/openai/hindsight-llm/Dockerfile @@ -0,0 +1,11 @@ +FROM node:22-slim + +RUN npm install -g @moonshot-ai/kimi-code + +WORKDIR /workspace + +COPY server.js /app/server.js + +EXPOSE 8012 + +ENTRYPOINT ["node", "/app/server.js"] diff --git a/openai/hindsight-llm/server.js b/openai/hindsight-llm/server.js new file mode 100644 index 0000000..6df9ab8 --- /dev/null +++ b/openai/hindsight-llm/server.js @@ -0,0 +1,178 @@ +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const { spawn } = require('child_process'); + +const PORT = 8012; +const MODEL_ID = 'hindsight-llm'; +const TIMEOUT_MS = 5 * 60 * 1000; // one-shot structured calls; generous but bounded +// Bounded parallelism: SPIKE-FINDINGS.md gate 5 flagged the Kimi subscription as a +// single-seat, interactive-oriented plan — batch cognify must not hammer it with +// unbounded concurrent CLI spawns (rate-limit/throttle risk on a shared live account). +const MAX_CONCURRENCY = 3; + +const WORKSPACE = '/workspace'; +fs.mkdirSync(WORKSPACE, { recursive: true }); + +// The CLI has no raw sampling-temperature knob (it's an agent loop, not a +// completions API) — "low temperature" for structured extraction is enforced +// via an instruction preamble instead, prepended to whatever system prompt +// the caller (Cognee) supplies. +const STRUCTURED_SYSTEM_PREAMBLE = [ + 'You are a stateless structured-extraction engine.', + 'This is a one-shot call with no memory of prior calls: do not reference earlier turns.', + 'Respond deterministically and concisely. When asked for JSON, output raw JSON only', + '- no prose, no markdown code fences, no commentary before or after.', +].join(' '); + +// --- message helpers --------------------------------------------------------- +// Text only, no media parts: this wrapper's policy is no-media/no-MCP, unlike +// adolf-llm which persists inbound images and lets the CLI's ReadMediaFile +// tool read them. +function textOf(msg) { + const c = msg.content; + if (Array.isArray(c)) return c.map(p => (typeof p.text === 'string' ? p.text : '')).join('\n'); + return c == null ? '' : String(c); +} + +function buildPrompt(messages) { + const systemParts = messages.filter(m => m.role === 'system').map(textOf); + const rest = messages.filter(m => m.role !== 'system'); + const preamble = [STRUCTURED_SYSTEM_PREAMBLE, ...systemParts].join('\n\n'); + const transcript = rest + .map(m => `${m.role === 'assistant' ? 'Assistant' : 'User'}: ${textOf(m)}`) + .join('\n\n'); + return `${preamble}\n\n${transcript}`.trim(); +} + +// --- bounded concurrency queue ----------------------------------------------- +let active = 0; +const queue = []; +function drain() { + if (queue.length && active < MAX_CONCURRENCY) queue.shift()(); +} +function withSlot(fn) { + return new Promise((resolve, reject) => { + const run = () => { + active++; + fn().then( + v => { active--; drain(); resolve(v); }, + e => { active--; drain(); reject(e); }, + ); + }; + if (active < MAX_CONCURRENCY) run(); + else queue.push(run); + }); +} + +// --- kimi invocation: stateless one-shot, no resume -------------------------- +// Fresh temp dir per call, NO -r/-S session flag, discard the dir after. +// Returns the assembled text from --output-format stream-json: +// {"role":"assistant","content":"..."} +// (reuses the same parse core as kimi-agent/server.js's runKimi, minus the +// resume/session-id bookkeeping that wrapper needs and this one deliberately +// does not). +function runKimi({ prompt, cwd }) { + return new Promise((resolve, reject) => { + const args = ['-p', prompt, '--output-format', 'stream-json']; + const child = spawn('kimi', args, { cwd, timeout: TIMEOUT_MS }); + + let stdout = ''; + let stderr = ''; + child.stdout.on('data', d => { stdout += d; }); + child.stderr.on('data', d => { stderr += d; }); + + child.on('error', reject); + child.on('close', code => { + const parts = []; + for (const line of stdout.split('\n')) { + const t = line.trim(); + if (!t) continue; + let obj; + try { obj = JSON.parse(t); } catch { continue; } + if (obj.role === 'assistant' && obj.content) parts.push(obj.content); + } + const text = parts.join('').trim(); + if (!text && code !== 0) { + reject(new Error(`kimi exited ${code}: ${stderr.slice(0, 2000)}`)); + } else { + resolve(text); + } + }); + }); +} + +async function handleTurn(messages) { + const prompt = buildPrompt(messages || []); + const reqId = crypto.randomUUID(); + const dir = path.join(WORKSPACE, reqId); + fs.mkdirSync(dir, { recursive: true }); + try { + return await withSlot(() => runKimi({ prompt, cwd: dir })); + } finally { + // Stateless one-shot: nothing about this call is meant to survive it, so + // the temp dir is discarded unconditionally, success or failure. + fs.rm(dir, { recursive: true, force: true }, () => {}); + } +} + +// --- OpenAI-compatible HTTP surface (non-streaming only) --------------------- +function completionBody(text) { + return { + id: `chatcmpl-${Date.now()}`, + object: 'chat.completion', + created: Math.floor(Date.now() / 1000), + model: MODEL_ID, + choices: [{ + index: 0, + message: { role: 'assistant', content: text }, + finish_reason: 'stop', + }], + usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }, + }; +} + +const server = http.createServer((req, res) => { + if (req.method === 'GET' && req.url === '/v1/models') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + object: 'list', + data: [{ id: MODEL_ID, object: 'model', owned_by: 'moonshot' }], + })); + return; + } + + if (req.method === 'POST' && req.url === '/v1/chat/completions') { + let body = ''; + req.on('data', d => { body += d; }); + req.on('end', async () => { + let parsed; + try { + parsed = JSON.parse(body); + } catch { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid JSON body' })); + return; + } + + try { + const text = await handleTurn(parsed.messages || []); + // Non-streaming policy: always return the full body even if the + // caller sets stream:true. Cognee's batch cognify has no use for SSE, + // and a one-shot call has nothing to incrementally stream anyway. + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(completionBody(text))); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: String(err.message || err) })); + } + }); + return; + } + + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'not found' })); +}); + +server.listen(PORT, () => console.log(`hindsight-llm wrapper listening on :${PORT}`)); diff --git a/openai/shared-mcp.json b/openai/shared-mcp.json index 9b8fa01..b6f4291 100644 --- a/openai/shared-mcp.json +++ b/openai/shared-mcp.json @@ -11,6 +11,10 @@ "kanboard": { "type": "http", "url": "http://host.docker.internal:3104/mcp" + }, + "agap": { + "type": "http", + "url": "http://host.docker.internal:3100/mcp" } } }