Adolf: decommission Cognee (H4) + standalone hindsight-llm + vault access
H4 (kb#76) - Cognee -> Hindsight migration finished: - New openai/hindsight-llm/ (clone of cognee-llm, :8012, own volume) so Hindsight's Kimi LLM no longer depends on the cognee stack - Repointed hindsight service LLM at hindsight-llm:8012 (+ depends_on) - Removed cognee, cognee-mcp, cognee-llm services + cognee-llm-home volume from openai/docker-compose.yml - Removed the disabled cognee-memory plugin entry from openclaw.json Vault access (kb#64): wired the shared agap-mcp (:3100, same MCP Claude uses) into Adolf's registry - "agap" server in shared-mcp.json + openclaw.json mcp.servers. Adolf can now fetch credentials from Vaultwarden (verified). Note: agap-mcp/docker-compose.yml (repointed to the adolf46 account) is deliberately NOT in this commit - it holds the master password in plaintext. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014t8Qg9gi7H7HtT8MncoXAB
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
11
openai/hindsight-llm/Dockerfile
Normal file
11
openai/hindsight-llm/Dockerfile
Normal file
@@ -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"]
|
||||
178
openai/hindsight-llm/server.js
Normal file
178
openai/hindsight-llm/server.js
Normal file
@@ -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}`));
|
||||
@@ -11,6 +11,10 @@
|
||||
"kanboard": {
|
||||
"type": "http",
|
||||
"url": "http://host.docker.internal:3104/mcp"
|
||||
},
|
||||
"agap": {
|
||||
"type": "http",
|
||||
"url": "http://host.docker.internal:3100/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user