Document conversation<->session mapping and per-conversation working dirs
@@ -16,7 +16,26 @@ Open WebUI -> LiteLLM (model: kimi-agent) -> kimi-agent container (HTTP wrap
|
|||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `Dockerfile` | `node:22-slim` + `npm install -g @moonshot-ai/kimi-code` |
|
| `Dockerfile` | `node:22-slim` + `npm install -g @moonshot-ai/kimi-code` |
|
||||||
| `server.js` | Minimal OpenAI-compatible wrapper (`/v1/models`, `/v1/chat/completions`) — no external deps, built-in `http`/`child_process` only. Spawns `kimi -p "<prompt>" --output-format text` per request. |
|
| `server.js` | Minimal OpenAI-compatible wrapper (`/v1/models`, `/v1/chat/completions`) — no external deps, built-in `http`/`child_process`/`crypto`/`fs` only. Maps each Open WebUI conversation to a persistent kimi session (see below). |
|
||||||
|
|
||||||
|
### Conversation <-> session continuity
|
||||||
|
|
||||||
|
Open WebUI (like any OpenAI chat client) re-sends the **entire message history** on every turn, but the CLI's `-p` mode is stateless — one prompt in, one answer out. The wrapper bridges the two so an Open WebUI thread maps 1:1 onto a persistent kimi session instead of starting cold each turn.
|
||||||
|
|
||||||
|
Per request the wrapper:
|
||||||
|
|
||||||
|
1. **Keys the conversation** by SHA-256 hashing the user/assistant history *up to but excluding the newest user turn* (a "content-hash chain"). This depends only on the standard `messages` array, so it survives LiteLLM sitting in the middle — no reliance on a `chat_id` that LiteLLM may strip.
|
||||||
|
2. **Looks the key up** in a persisted `key -> {convId, sessionId, dir}` map:
|
||||||
|
- **Hit** -> resumes the same kimi session with `kimi -r <session_id> -p "<new message only>"`, in that conversation's own working dir.
|
||||||
|
- **No prior history** -> brand-new conversation: fresh `kimi -p`, new dir + `convId`.
|
||||||
|
- **Miss with prior history** (wrapper restarted / thread edited) -> reseeds a fresh session with the full transcript so continuity is preserved.
|
||||||
|
3. **Captures** the `session_id` from the stream-json `session.resume_hint` meta line and stores the forward mapping (history + this reply -> session), which is exactly what the next turn will hash to.
|
||||||
|
|
||||||
|
**Per-conversation working dirs:** each conversation gets its own directory `/workspace/conversations/<convId>/`, used as the CLI `cwd`. File state is therefore isolated between threads and persists across turns within a thread.
|
||||||
|
|
||||||
|
**State file:** `key -> session` map lives at `/workspace/.kimi-agent/sessions.json` (host-backed, survives restarts; LRU-capped at 1000 entries).
|
||||||
|
|
||||||
|
Both the wrapper-level session map and kimi's own session store (`/root/.kimi-code`, named volume) persist across container rebuilds, so live Open WebUI threads keep working after a redeploy.
|
||||||
|
|
||||||
### Docker Compose
|
### Docker Compose
|
||||||
|
|
||||||
@@ -31,7 +50,7 @@ kimi-agent:
|
|||||||
```
|
```
|
||||||
|
|
||||||
- **Mount scope is deliberately narrow**: only `~/kimi-workspace` is mounted as `/workspace`, not the full home directory. The agent runs prompts via `-p` with no interactive approval gate (see below), so it must never see `~/.ssh`, `~/.claude`, `~/bin/bw`, etc.
|
- **Mount scope is deliberately narrow**: only `~/kimi-workspace` is mounted as `/workspace`, not the full home directory. The agent runs prompts via `-p` with no interactive approval gate (see below), so it must never see `~/.ssh`, `~/.claude`, `~/bin/bw`, etc.
|
||||||
- `kimi-agent-home` is a named volume persisting `/root/.kimi-code` (the OAuth session from `kimi login`) across container restarts/rebuilds.
|
- `kimi-agent-home` is a named volume persisting `/root/.kimi-code` (the OAuth session from `kimi login` **and** kimi's own session history) across container restarts/rebuilds.
|
||||||
- No ports are published — only reachable from other containers on the `openai_default` network (i.e. `litellm`), not from the host or LAN.
|
- No ports are published — only reachable from other containers on the `openai_default` network (i.e. `litellm`), not from the host or LAN.
|
||||||
|
|
||||||
### LiteLLM registration
|
### LiteLLM registration
|
||||||
@@ -60,9 +79,11 @@ Prints a `https://www.kimi.com/code/authorize_device?user_code=...` URL — open
|
|||||||
docker exec kimi-agent kimi doctor
|
docker exec kimi-agent kimi doctor
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key CLI gotcha
|
### Key CLI gotchas
|
||||||
|
|
||||||
`-p` (non-interactive single-prompt mode) **cannot be combined with `--yolo` or `--auto`** — and doesn't need to be. In `-p` mode there's no TTY to prompt for approval, so tool calls (file read/write, shell) execute automatically already. `server.js` calls plain `kimi -p "<prompt>" --output-format text`.
|
- `-p` (non-interactive single-prompt mode) **cannot be combined with `--yolo` or `--auto`** — and doesn't need to be. In `-p` mode there's no TTY to prompt for approval, so tool calls (file read/write, shell) execute automatically already.
|
||||||
|
- Resume a session with `kimi -r <session_id> -p "..."`. With `--output-format stream-json`, the CLI emits a `{"role":"meta","type":"session.resume_hint","session_id":"session_..."}` line the wrapper parses to learn/track the id.
|
||||||
|
- The `node:22-slim` image has **no `curl`** — test the wrapper with Node's `http` module (e.g. `docker exec kimi-agent node -e '...'`).
|
||||||
|
|
||||||
### Rebuild / restart
|
### Rebuild / restart
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user