recall-echo 4.7.0

Persistent memory system with knowledge graph — for any LLM tool
# Configuration

`.recall-echo.toml` lives in the memory directory (`{pulse_root}/memory/`). Every key is
optional; a missing file or an invalid value falls back to the default, silently.

```toml
[ephemeral]
max_entries = 5               # rolling window of session summaries, 1-50

[llm]
provider = "anthropic"        # default when unset; `init` writes the CLI it found
model = ""                    # empty = the provider's default
api_base = ""                 # HTTP providers only

[llm.cli]                     # CLI providers; each key defaults to the preset
# command = "mycli"           # binary name or path
# args = ["chat"]             # fixed args before the generated flags
# prompt_delivery = "flag"    # stdin | flag | arg
# prompt_flag = "--ask"       # used when prompt_delivery = "flag"
# model_flag = "--model"      # omitted when empty, or when no model is set
# output_format_flag = "--format"   # passed alone when the value is empty
# output_format_value = "json"
# system_prompt_flag = ""     # empty prepends the system prompt to the message
# output_mode = "single-json" # raw | single-json | ndjson
# result_json_path = "data.text"    # dotted path; empty = stdout is the answer
# ndjson_match = []           # path=value line selectors, ndjson mode only
# usage_input_path = ""       # where the CLI reports token counts; empty = estimated
# usage_output_path = ""
# extra_args = ["--quiet"]    # appended after the generated flags
timeout_secs = 300            # per call; 0 waits forever

[graph]
mode = "embedded"             # embedded (single-process SurrealKV) | server (shared SurrealDB)
url = "ws://localhost:8787"   # server mode only

[graph.dedup]
certain_similarity = 0.92     # at or above: same entity, no model call
review_similarity = 0.82      # below: new entity, no model call
max_candidates = 3            # existing entities compared per candidate

[graph.provenance]
weight_external = 1.0         # independent sources
weight_user = 0.8             # the human, and `graph correct --wrong`
weight_self = 0.05            # the agent restating its own belief

[serve]
socket_path = ""              # default: $XDG_RUNTIME_DIR/recall-echo/
idle_timeout_secs = 3600      # daemon exits after this much inactivity; 0 = never

[extraction]
background_enabled = true     # let the daemon extract when the machine is quiet
idle_after_secs = 120         # quiet period before a batch
batch_size = 3                # archives per batch

[capture]
enabled = true                # let the daemon import transcripts from hook-less CLIs
# sources = ["claude-code", "codex", "grok"]   # default: every CLI with sessions here
settle_secs = 300             # a transcript untouched this long counts as finished

[pipeline]                    # pulse-null stack only
docs_dir = ""                 # LEARNING.md, THOUGHTS.md, CURIOSITY.md, REFLECTIONS.md, PRAXIS.md
auto_sync = false             # sync them into the graph on every archive
```

`recall-echo config show` prints the effective settings and the exact command a CLI
provider will run. `recall-echo config set KEY VALUE` changes one (`provider`, `model`,
`api_base`, `llm.cli.*`, `ephemeral.max_entries`, `graph.provenance.*`, …).

## Providers

| `provider` | How it runs | What it costs |
|---|---|---|
| `claude-code` | spawns `claude` | Claude subscription |
| `gemini` | spawns `gemini` | Gemini subscription or free tier |
| `grok` | spawns `grok` | Grok subscription |
| `codex` | spawns `codex exec` | ChatGPT/Codex subscription |
| `cli` | spawns whatever `[llm.cli]` describes | whatever that CLI costs |
| `ollama` | HTTP to `localhost:11434/v1` | free, fully local |
| `openai` | HTTP to any OpenAI-compatible endpoint | per token |
| `anthropic` | HTTP to the Anthropic API | per token |

Embeddings are always local (fastembed/ONNX, no network after the model downloads once),
so semantic search, HNSW indexing and traversal cost nothing whatever this is set to.

The five CLI providers are one implementation. A provider name selects a preset, a set
of defaults for the `[llm.cli]` keys, and every key can be overridden, so a CLI with no
preset is configuration rather than a new release. The presets, exactly as they are
called (`<prompt>` is the message, `<system>` the system prompt, `<` means stdin):

```text
claude-code  claude -p --model <M> --output-format text --system-prompt <system> \
                    --no-session-persistence  < <prompt>
gemini       gemini -m <M> -o json -p "<system>\n\n<prompt>"
grok         grok -m <M> --output-format json -p "<system>\n\n<prompt>"
codex        codex exec -m <M> --json --skip-git-repo-check  < "<system>\n\n<prompt>"
```

Only `claude` takes the system prompt as a flag; the others get it prepended. The model
flag is dropped when no model is configured, so each CLI uses its own default. Point at a
binary somewhere unusual with `CLAUDE_BIN`, `GEMINI_BIN`, `GROK_BIN`, `CODEX_BIN`,
`RECALL_CLI_BIN` or `[llm.cli] command`.

Output shapes differ, so `output_mode` is part of the config: `claude` prints prose
(`raw`), `gemini` and `grok` print one JSON document (`single-json` plus
`result_json_path`), and `codex --json` prints one event per line (`ndjson`), where the
answer is the last event matching `type=item.completed` and `item.type=agent_message`:

```toml
[llm.cli]
output_mode = "ndjson"
ndjson_match = ["type=item.completed", "item.type=agent_message"]
result_json_path = "item.text"
```

Output handling is forgiving: JSON that does not parse, a missing path or a non-string
value falls back to the CLI's stdout verbatim. A non-zero exit is an error carrying the
CLI's stderr, and so is a JSON envelope that reports its own failure while exiting zero.

## Caveats

- **codex**: `--skip-git-repo-check` is required, because codex refuses to run outside a
  trusted git directory and a memory directory usually is not one. Its `-p` is
  `--profile`, not the prompt, so the prompt goes in on stdin.
- **gemini's result field is unverified.** The flags were checked against `gemini` 0.27.3,
  but no authenticated call was made, so the preset tries `response`, then `result`, then
  raw stdout. If yours wraps the answer elsewhere, set `result_json_path`, or clear
  `output_format_flag` to take plain text.
- **Token counts are measured where the CLI reports them and estimated where it does
  not.** `codex`, `grok` and both HTTP APIs report usage, so `graph extract` quotes their
  numbers ("14K measured"). `claude-code` answers in prose with no envelope, so its calls
  are a length heuristic labelled `~ estimated`; a mixed run prints both rather than
  adding a guess to a measurement. Any CLI can be measured by setting `usage_input_path`
  and `usage_output_path`.
- **The background daemon only sees file-based CLI auth.** It starts with an allowlisted
  environment (`PATH`, `HOME`, …) that excludes API keys. See
  [internals: background work]internals.md#background-work.