# dogfood runtime

This checkout now has one normal execution path: the Rust runtime/store owns state and
mechanisms, a persistent Python workbench supplies policy and self-hosted code work, and the
browser uses only the v2 lane protocol. The legacy agent/memory/tool loop is not started.
Configured Discord ingress is normalized into the same durable runtime admission path. The
transport owns no inbox database or model loop, and outbound sends remain explicit host effects.

## source checkout

The commands below are argv commands and work from Nushell. They do not depend on Bash.

```text
bun run --cwd klbr-web build
cargo run -p klbr-daemon --bin klbr-daemon -- init
cargo run -p klbr-daemon --bin klbr-daemon -- doctor
cargo run -p klbr-daemon --bin klbr-daemon -- serve
```

Open `http://127.0.0.1:8765/`. `serve` is also the default subcommand. The service publishes
the editable `behavior/` tree as an immutable content-addressed release before opening a
session. It refuses a non-loopback bind. Browser WebSockets also reject non-loopback origins;
native local clients do not need an `Origin` header.

Runtime paths are explicit:

| variable | default |
|---|---|
| `KLBR_WORKSPACE` | current directory |
| `KLBR_DATA_DIR` | the platform data directory plus `klbr` |
| `KLBR_PYTHON` | `python3` |
| `KLBR_WORKER_RUNNER` | `<workspace>/python/klbr-runtime/src/klbr_runtime/__main__.py` |
| `KLBR_BEHAVIOR_DIR` | `<workspace>/behavior` |
| `KLBR_SKILLS_DIR` | `<workspace>/skills` |
| `KLBR_WEB_DIR` | `<workspace>/klbr-web/dist` |
| `KLBR_BIND_ADDR` | `127.0.0.1:8765` |
| `KLBR_REMOTE_PLACEMENT` | unset; local workbench |

The state directory contains `runtime.sqlite`, `releases/`, `drafts/`, `artifacts/`, and
`skills/`.
The old `agent.db` is neither opened nor seeded by this path.

## Nix later

Dogfooding is source-first for now. The repository's existing NCI/crate2nix-oriented flake is
left intact; a relocatable package and service are deferred until the runtime path is settled.

## working on this checkout from a cell

```python
print(await search("TODO", "klbr-runtime/src", max_results=20))
note = await open_file("docs/note.md")
await note.replace("old", "new")                    # one occurrence, or an error
check = await run(("cargo", "check", "-p", "klbr-runtime"), timeout=120)
print(check.exit_code, check.stderr.decode(errors="replace"))
```

`open_file`, `ls`, `search`, `delete` and `run` are bound in every cell and act
on the session's workspace, resolved once per kernel generation
(`klbr.workspace.PRELUDE_NAMES` lists them; `ws = await workspace.current()` is the object form).
`ls` lists one level and `ls(path, recursive=True, match="*.rs")` walks with a name glob, and `search` reports
`truncated` rather than letting a capped result read as a complete one.

Paths are root-bound and symlink escapes are rejected. `workspace.run` is a typed host RPC,
not a shell call inside Python. The Rust host owns the process group, output cap, timeout, and
cancellation cleanup, so kernel replacement cannot strand a build process.

## skills

Instruction bodies live in `skills/<id>/SKILL.md`. The first non-empty line is `# <id>` and the
directory name must equal it; that title is the only schema, so the id is a handle and the body
is the trail to the state the id names. Nothing is stored beside the body, because a description
would become a second, staler definition of it.

```python
import klbr.skills as skills

dir(skills)                 # the ids this kernel generation can import
help(skills.workspace)      # one body
skills.refused              # ids found and not offered, with the reason
skills.revision             # content identity of the whole offered set
skills.roots                # where those ids were read from
```

Builtin ids are read from `KLBR_SKILLS_DIR` (default `<workspace>/skills`), so they ship with the
checkout. Managed ids are read from `<data>/skills`, which `init` and `serve` create; a skill the
agent writes there survives a checkout move. Both are one namespace, and an id defined in both is
refused instead of silently winning - `klbr doctor` prints the ids and the refusals, and the
daemon logs the index at startup.

The host validates the roots and hands the bodies to the worker as a spawn payload, so a worker
never reads a skills directory itself: one reader, one validator, and a set that cannot change
under a running turn. The kernel generation's `kernel.reset` record carries the revision it
loaded, so the durable log says which instruction set a turn could have read rather than what the
directories happen to hold now.

The model sees the ids in its contract message and imports a body by id. A maintenance turn
additionally loads `maintenance` by default. Because the set is fixed at spawn, a skill written
during a turn is offered on the next generation rather than mid-turn.

## operator path

These commands use the same v2 socket as the UI and do not need a successful model or policy
callback:

```text
klbr status
klbr status default
klbr reset default
klbr compact default
klbr drain default
klbr stop default
klbr activate default 3 /absolute/path/to/releases/<sha256>
```

`reset` states explicitly that Python heap state was not restored. `drain` waits for accepted
host work before stopping. Behavior activation is an epoch compare-and-swap against a verified
content-addressed release.

## model seam

`klbr-core` still reads the existing KDL model configuration. Defaults target OpenAI-compatible
local endpoints at `http://localhost:8001/v1`, `:8002/v1`, and `:8003/v1`. `doctor` validates
the configuration and worker import path; a normal send is the live model/provider check.
Provider keys belong in the process environment or the configured KDL file, not source control.

Prompt caching is a route property, declared per route and never guessed:

```kdl
llm {
    url "https://api.example/v1"
    model "m"
    cache_profile "openai_prompt_cache_key"   // or openai_prompt_cache_retention | openai_prompt_cache_options
    cache_affinity "klbr:work"                // stable key: the same one for related requests, never per request
    cache_retention "24h"                     // only on a profile that accepts it: in_memory | 24h, or 30m on an options route
}
```

`cache_profile` is opt-in because `/chat/completions` is shared by many incompatible
OpenAI-style routes, and an unknown route must not receive a field it may reject. A profile
declares which cache fields that route accepts; `cache_retention` values are checked against it
at parse time, so a mismatch is a configuration error here rather than a parameter error from
the route after the request was paid for. Route `extra_options` cannot smuggle a cache field:
`cache_prompt`, `cache_control`, `prompt_cache_key`, `prompt_cache_retention` and
`prompt_cache_options` are removed from route options and only re-added by the declared profile.

Cache reads and writes are reported separately and are never inferred: a provider that reports
nothing leaves the measurement absent, which is a different statement from a provider that
reported zero. OpenAI's `prompt_tokens_details.cached_tokens` and `cache_write_tokens`, and
DeepSeek's `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens`, are all read; a route with an
automatic cache (DeepSeek) needs no profile at all.

Rebuild the source UI after frontend changes with `bun run --cwd klbr-web build`.
