Repository navigation
Prometheus v0.9.5
Pre-releaseTokens are redacted before anything is kept, skills are advertised and measured, GEPA only proposes changes, and the first daemon start repairs conversation history once. Read "Upgrading from 0.9.4" before you restart the daemon.
Security
-
Tokens are redacted before anything is kept (#596, #599). Token shapes become
<redacted>before a row is written:lcm.db: messages, summaries, divergence checkpoints, session titles and their full-text indexes;memory.db: facts, including updates;telemetry.db: run summaries, signal payloads, circuit-breaker samples and parsed tool calls. Tool-call errors and outputs were already covered.
The redactor now also knows PEM private keys, AWS keys, passwords in URLs to non-loopback hosts, Slack and Discord webhook paths,
access_token/api_key/client_secretquery values, and about 30 more vendor key prefixes. Every prompt a background subsystem sends to a model is redacted too: skill creation and refinement, the curator, memory extraction, GEPA, LCM summaries and video digests. -
The live conversation is not redacted. A token you paste for the agent to use keeps working for the rest of that session. What reads history back shows
<redacted>: the recall tools, a history reload in Beacon, and a session restored after a restart (sessions.rehydrate). -
Data kept before 0.9.5 is not rewritten for you (#622).
oara scrubreports what it would change, as counts only, with every database opened read-only. With the daemon stopped,oara scrub --applyrewrites in place. It covers telemetry, training pairs, trajectory exports, both LCM files andmemory.db. Before rewriting a database, it copies it to<db>.pre-scrub-<UTC time>. It never prints a matched value. Delete the*.pre-scrub-*copies once you are satisfied: they, and any other backup or snapshot, still hold the old values. Not covered: theMEMORY.mdandUSER.mdfiles the memory tool keeps, and wiki pages compiled from facts before the scrub.
Skills
- The model is offered the
skilltool and told when to use it (#593, #623).skilljoins the default advertised tool set, including in configs that setup wrote with the old list. Its description, and the prompt's skills section, now say what a skill is: saved step-by-step instructions for one kind of task. They also say when to load one: before starting a task it covers. If your config liststools.deferred_loading.always_loaded, see the upgrade notes. - Skill use is measured (#591, #598).
- Every
skillload is recorded intelemetry.db. /skills liston Telegram, Slack and Discord shows when each skill was last used and how many loads it has, not its file date.GET /api/skills/listrows gainloadsandlast_loaded_at, andGET /api/statusgainsskill_loads.- The Curator ages skills by their last load. A skill with no recorded load is never marked stale or pruned by its file date.
- A loaded skill's text is never microcompacted away.
- Every
- Fewer near-duplicate auto skills (#591, #625).
- On the automatic path, a new skill within cosine 0.80 of a skill already served is not written. Set the threshold with
learning.skill_dedupe_threshold. This uses an optional local encoder; see the upgrade notes. - Every writer refuses a description that is literally
name: …. tool_searchlists a skill only when the query matches it (a score of 0.5 or more), not by name distance.
- On the automatic path, a new skill within cosine 0.80 of a skill already served is not written. Set the threshold with
- Skill creation and refinement see what each tool call was given (#597). Each call's input appears in their prompts, redacted and cut to 240 characters. Before, every call read
tool({}). - Machine-written skills are scanned before they are written (#611). A skill with dangerous Python in it is not written. This applies to the automatic path, teacher escalation, Record a Skill and accepting a draft. Record a Skill answers 502 with
skill persistence rejected the generated content, and accepting a draft answers 422 and keeps the draft. - Nothing machine-written replaces a live skill (#615, #617). Skill files are created exclusively and never overwritten.
- Name clashes go to a person. A teacher escalation or a recording whose skill name is already served goes to
skills/drafts/for review. A name counts as served whether it belongs to an auto, a builtin or a user skill. Record a Skill answers"status": "draft". The automatic path skips the name. - Accepting a clashing draft (
POST /api/learning/skill-drafts/{id}/accept) answers 409 with the clash. Send{"replace": true}to archive the auto skill it replaces intoskills/auto/archive/and accept. - Recorded values stay values. Values in a recording are written as JSON inside inline code, so they cannot open markdown in the skill.
- Name clashes go to a person. A teacher escalation or a recording whose skill name is already served goes to
Learning: GEPA
- GEPA works from real skill loads and only proposes (#606). It is still off by default and experimental.
- Candidates: auto skills the model loaded at least
gepa_min_loadstimes (default 3). The runs around those loads are the evidence. - Drafting: the local model drafts variants. A hosted primary is used only with
learning.gepa_allow_hosted: true. - Judging: the judge scores the live skill and each variant on the same runs. A variant becomes a proposal in
~/.prometheus/skills/proposals/only if all three hold:- it beats the live skill by
gepa_min_margin(0.15); - it reaches
gepa_judge_threshold; - it is worse on no run.
- it beats the live skill by
- Promotion: nothing changes until a person runs
oara gepa promote <id>. It checks the live skill is the version that was scored, runs the code scanner, and archives the old version. - New commands:
oara gepa proposals | show <id> | promote <id> | reject <id>, andoara gepa dry-run [--json](counts only: no model calls, no writes).
- Candidates: auto skills the model loaded at least
Evals
- A judge reply with no verdict is no longer a score (#603).
- Before: an empty or unparseable reply scored 0.0, a fallback read any number it found, and out-of-range scores were clamped.
- Now: the metric is listed as unavailable (
unavailable_metrics, plusunparseable_metrics) and is never a pass or a fail. Summaries reportjudge_unavailable,scored_tasks, and apass_rateover scored tasks only. - Parsing: JSON verdicts are read only as JSON, and G-Eval only from a final
SCORE:line. The evals, the model ladder and GEPA share this one parser.
Memory and sessions
- Every message keeps one position in its conversation (#587).
- The bug: restarts,
/resetand retried turns re-used positions that older messages held. History read in order could then interleave two conversations. That fed the LCM summarizer and the golden-trace export; the live prompt was not affected. - The fix: new messages are numbered above the session's highest stored position, and the CLI chat numbers each message separately.
- Existing data: the first daemon start repairs what is already stored and adds a guard (see the upgrade notes).
- API: the
ordinalfield in history and WebSocket messages is now unique within a session. Keep ordering bymessage_id.
- The bug: restarts,
- Restoring a session after a restart finds the conversation (#590, #600). This applies with
sessions.rehydrate: true, which is off by default.- Deeper search: when the newest 40 rows hold no plain human message, as after a long agentic turn, the restore now finds the newest human turn up to 2,000 rows back. Before, it restored nothing.
- Fitting it: tool payloads over 1,000 characters are shortened in the restored copy. If that is still too big, it keeps the request plus the newest rounds, with a note saying what was left out.
- More surfaces restore: Telegram task results, Slack, Discord and
POST /api/chatnow restore before replying. - Sizing: rows are sized with the compactor's own estimate.
- A failed turn in Beacon keeps a message you sent while it ran (#590).
- Reasoning no request sends back no longer counts toward the context estimate (#595). This is the thinking of Ollama thinking models and OpenAI-compatible reasoning providers. Counting it made compaction fire early, made
/contextoverstate use, and could refuse a local prompt as too long. memory.dbmigration snapshots are SQLite backups, so they include rows still in the WAL. They are owner-only, and a second snapshot in the same second gets a-1suffix instead of overwriting the first (#594, #601). A 0.9.4 store needs neither migration.- Golden-trace exports pair each tool call with the conversation just before it (#586). In a session longer than 500 messages, calls after the 500th were paired with the conversation around message 500. Files exported earlier are not rewritten.
Models and providers
- Qwen's XML tool calls run (#582). A tool call written as text in Qwen's
<function=…><parameter=…>format used to be stripped out and lost. It now runs. Calls written in XML and JSON in one reply run in the order written, and an unwrapped XML call no longer shows in the final reply. - A local model the registry does not know gets its tier from the served chat template (#583).
- llama.cpp's
/propsand Ollama's/api/showare asked whether the model calls tools natively: yes giveslight, no givesfull. Before, every such model ran atfull. - One boot line per adapter says which tier and why. A model that falls back to
fullgets a WARNING saying how to name it. - The tier is decided at boot. Restart the daemon after swapping the served model.
- llama.cpp's
adapter.model_tierssets the tier per model by hand (#602).- Keys are case-insensitive substrings of the model name, like the registry's; the longest match wins, and it beats every other source.
- Values are
auto,off,lightandfull. Write"off"in quotes: bareoffis YAML for false, and the entry is refused. - A bad value is refused with an ERROR and the entry ignored.
offon a local backend is allowed with a WARNING. So is lifting a cloud model offoff.- It does not reach router fallback or rule providers.
- Coding runs use the model the server actually serves (#585).
oara code, and the coding tasks the daemon starts, used to build their adapter from the config's model name. That is often blank, so every coding run ran atfullwith no native tools. They now get the same tier as chat, and requests and telemetry carry the served model's name. - Ollama thinking models think only when asked, and the thought is kept (#592).
- For a model whose
/api/showlists thinking, chat turns sendreasoning_effort: "none"; coding runs still think. There is no setting in this release to turn thinking back on for chat. - When a model does think, the thought is stored with the message instead of being thrown away. It is never shown in the reply or sent back to the model.
- A reply that is all thought and no answer is logged, and the call is retried once with thinking off.
- For a model whose
- Cloud and OpenAI-compatible calls record the model that served them (#614), beside the one requested.
Router
- A fallback never hands back the provider that just failed (#584). On a llama.cpp → Ollama → Anthropic chain, an Ollama failure used to route back to Ollama, and Anthropic was never reached. When the chain runs out, one WARNING names what failed and why each entry was passed over.
- Rule and fallback settings are honoured or refused (#584). A rule used to drop everything but provider, model and base URL, so its
api_key_envwas ignored and the provider's default key used. Rules now pass their settings on. An entry with a key its provider does not read is skipped with a WARNING and shown inoara doctor. See the upgrade notes. router.auxiliaryis removed; nothing ever used it (#584).
Telemetry
- Background model calls are counted (#619). The compactor, memory extractor, skill creator and refiner, curator, knowledge synthesis, teacher, live recorder, LCM summaries, session titles, vision and prompt hooks now record tokens. Totals in
/api/usageand/statusrise to match. - Cache counts are recorded, and unknown ones say so (#610, #619). llama.cpp and Anthropic cache counts are recorded. In
/api/usage,cached_input_tokensisnullwhere no row recorded a count, not0, and the newcache_reported_runssays how many did. Clients should handlenull. - Anthropic input counts now include cache reads and writes (#619). The cost estimate prices every input token at the full rate, so for cached Anthropic prompts it now reads higher than before.
- Local rounds are billed as
localwhatever the model is called (#619). A model whose history spans the upgrade can showbilling: "mixed". - Rounds record the model that served them (#609, #614, #619):
- Ollama, Qwen, xAI and Anthropic record it beside the one requested.
- A round served by the provider fallback is recorded under the fallback's model.
- A local fallback's calls are never marked golden.
- Runs with no session of their own are filed under one (#608).
POST /api/chatruns are filed underweb:<session_id>and subagents undersubagent:<parent>:<id>. Failure rows, loop transitions and microcompact rows carry their session (#607, #619). - A tool the model calls without it being advertised writes a marker row, not a second "successful" call (#605).
- Rows written before 0.9.5 are not rewritten. There is no schema change.
Also
- The model ladder, a benchmark suite and harness for local models, runs from a checkout (#588). Parity-harness additions: a rebaseline mode, a stricter undo rule and a twelfth golden (#612, #613, #616). Audits of skill usage and telemetry gaps are in
docs/audits/(#589, #604). - The pre-commit scan checks every staged file, including renames and type changes, and never prints the matched line (#620).
Upgrading from 0.9.4
-
The first
oara daemonstart repairslcm.dbonce (#587). Only the daemon runs this; the CLI chat,--onceandoara codedo not.- No repeated positions (a fresh install, or a store that never had a daemon restart mid-session): it adds the guard and logs, at INFO,
LCM turn_index migration: no duplicates; a trigger now refuses a repeated (session_id, turn_index). No backup file is written. - Repeated positions:
- It first copies
lcm.dbtolcm.db.pre-turn-index-<UTC time>.bakbeside it (~/.prometheus/data/by default). The copy is owner-only (0600) and the same size aslcm.db. It needs that much free space plus 64 MB, or the migration does not run. - It then renumbers in one transaction, checks the result and logs a WARNING:
LCM turn_index migration: renumbered N rows in S sessions (R runs, largest shift K); … Backup of the previous state: <path> (<bytes> bytes). <seconds>s. - On the reference store (18,879 messages, 185 MB) the work took about half a second, plus writing the backup.
- The backup is never deleted for you. Delete it once you are satisfied: it holds your history as it was, including anything kept before this release's redaction.
- It first copies
- If it fails, nothing is committed and it logs an ERROR (
LCM TURN_INDEX MIGRATION FAILED — nothing was committed; the daemon runs on WITHOUT the turn-index guard, as before.). The daemon keeps running and the next start tries again. - Going back to 0.9.4 after this ran: 0.9.4 cannot store a message whose position is already taken: its daemon logs the failed write and stops saving that conversation, and its CLI chat crashes on its first reply. Before downgrading, stop the daemon and run
sqlite3 ~/.prometheus/data/lcm.db "DROP TRIGGER lcm_messages_turn_index_guard; PRAGMA user_version = 0;". Starting 0.9.5 again migrates again.
- No repeated positions (a fresh install, or a store that never had a daemon restart mid-session): it adds the guard and logs, at INFO,
-
skillis in the default advertised tool set (#593), and it reaches the configs setup wrote (#623).- Setup no longer writes
tools.deferred_loading.always_loaded, so new configs follow the shipped default across upgrades. - A list that is exactly one Prometheus shipped as the default follows the current default instead, and each daemon start logs one INFO line saying so. That covers two kinds of config:
- those that
oara setup --fast,--noninteractiveor--provider,prometheus-initor Beacon's first-run page wrote from 2026-08-12 until this release; - those copied from the template.
- those that
- Any other list is yours and is used exactly as written. It still gets
config OVERRIDES a shipped default … THE CONFIG WINSat boot, andoara doctorwarns when it leaves out a shipped tool such asskill. - To keep an old default deliberately, write the list in a different order, or add or remove a tool. It is then treated as your own.
oara doctorand/doctorsay which case applies. The list matters only while deferred loading is on, which is the default for local models; cloud providers are shown every tool.
- Setup no longer writes
-
learning.gepa_min_traces_requiredis renamed tolearning.gepa_min_loads(#606). The old key counted exported traces and is now ignored. A config that still sets it gets one WARNING at every daemon start, whether GEPA is on or off, and fromoara gepa dry-run:
learning.gepa_min_traces_required: config key is deprecated — it was renamed to learning.gepa_min_loads (WP-X.33) and is IGNORED. … Delete the key; set gepa_min_loads to change the minimum.
The shipped template set this key (to 10) from April until this release, so configs copied from it have it. The new key counts skill loads, not traces, and defaults to 3. -
router.auxiliaryis removed (#584). Nothing ever routed a task through it, so no provider set there was ever used.- A config that still has the block, including the template's empty placeholders, boots normally.
- Each start logs
router.auxiliary: config key is deprecated — nothing ever routed a task through it, so no provider configured there has been used. Delete the block. oara doctor(and/doctor) shows a Router warning naming it. Delete the block.
-
Router rules and fallback entries are checked at load (#584). The daemon still boots, and every other entry loads. An entry is skipped with one WARNING, and listed in the doctor's Router row, if it:
- carries a key its provider never reads (for example
max_tokensorapi_key_envon allama_cpporollamaentry); - names an unknown provider;
- has no provider;
- is a rule without
task_typeormodel, or with an unknowntask_type.
The WARNING looks likerouter.fallback[0] ollama/qwen3 refused: max_tokens is not read for provider ollama (it reads base_url, model, provider, timeout), and it names keys, never values.
Also: - Rules now pass the rest of their provider settings (
api_key_env,timeout, …). Before, they were dropped, and the provider's default key was used. - A fallback no longer hands back the provider that just failed.
- carries a key its provider never reads (for example
-
Scrub what was kept before 0.9.5.
- Stop the daemon.
- Run
oara scrub(a dry run, counts only). - Run
oara scrub --apply. - Start the daemon.
- Once you are satisfied, delete the
*.pre-scrub-*copies, and thelcm.db.pre-turn-index-*.bakif the migration made one. Both hold the old values.
It finds the stores where the daemon keeps them, including under
PROMETHEUS_CONFIG_DIRandPROMETHEUS_DATA_DIR. If you moved training pairs or trajectory exports in the config, pass--trainingor--trajectories. -
The near-duplicate check for auto skills is optional and off until you set it up (#591). Skill creation works without it; the first auto skill in each daemon run logs
SkillCreator: near-duplicate check skipped — <reason>and is kept. To turn it on:- Install the runtime into the same environment as
oara:pip install -U 'oara-prometheus[skills]'. That is numpy, onnxruntime and tokenizers.[voice]includes them;[full]does not, because it lacks tokenizers. - Fetch the model with the Python of the environment
oarais installed in (in a venv,<venv>/bin/python):python -m prometheus.skills.similarity fetch.- It downloads BAAI/bge-small-en-v1.5, about 134 MB, from huggingface.co at a pinned revision into
~/.prometheus/models/. - It keeps each file only if its size and SHA-256 match the pin, and the daemon checks them again on first use.
- It downloads BAAI/bge-small-en-v1.5, about 134 MB, from huggingface.co at a pinned revision into
- Check it:
python -m prometheus.skills.similarity verifyprintsavailable(exit 0), orunavailable: <reason>(exit 1). - Restart the daemon. It decides once per run.
On Homebrew the formula carries none of the three packages, so the check stays off.
- Install the runtime into the same environment as
-
An unregistered local model may move from
fulltolight(#583). If the served chat template says the model calls tools natively, it now gets native tool calling instead of the grammar-constrained text format. To keep the old behaviour, setadapter.model_tiers: {"<name>": full}. Models listed in the registry keep their tier. -
Ollama thinking models stop thinking on chat turns (#592). See Models and providers above.
-
The Curator stops aging skills by file date (#591). It is on by default and runs weekly. Until loads are recorded, skills it had marked stale or archived by date become active at its next pass, and nothing is pruned.
-
/api/usagecan returnnull.cached_input_tokensisnullwhere nothing was recorded, and the response gainscache_reported_runs(#610). Anthropic input counts now include cache reads and writes (#619). -
Golden-trace files exported before 0.9.5 are not rewritten (#586). For sessions longer than 500 messages, their context may be wrong. Re-export them if you train on them.
Known issues
- On Intel Macs, pip builds
cryptographyfrom source, because its releases since 49.0 ship no Intel-Mac wheel. That needs Rust and OpenSSL; with Homebrew's OpenSSL, setOPENSSL_DIR=$(brew --prefix openssl@3). On Python 3.14,watchdogalso builds from source, which needs Xcode's command-line tools. - The new
skillsextra needsonnxruntime, which ships no Intel-Mac wheel after 1.23.2, and 1.23.2 stops at Python 3.13.
Install
pip install -U oara-prometheus # the daemon, its web API, the CLI and Telegram
pip install -U 'oara-prometheus[full]' # adds Slack, Discord, MCP, the browser tool, Anthropic and voice output
brew upgrade oara # Homebrew on Apple Silicon; first install: brew install oaralabs/tap/oaraPython 3.11 or later. On PyPI: oara-prometheus 0.9.5.