Skip to content

feat: optional result cache (off by default, never persists keys) — closes #6#37

Merged
ernestprovo23 merged 1 commit into
mainfrom
feat/result-cache
Jun 9, 2026
Merged

feat: optional result cache (off by default, never persists keys) — closes #6#37
ernestprovo23 merged 1 commit into
mainfrom
feat/result-cache

Conversation

@ernestprovo23

Copy link
Copy Markdown
Member

Summary

Implements issue #6 / PDD §9 #4: an opt-in result cache keyed on (prompt, council, mode, model ids) so repeated and eval runs are cheap. Off by default. Never persists API keys.

Behavior

  • Off by default. No caching unless explicitly opted in (config cache: true or --cache).
  • Opt-in two ways, mirroring existing config+CLI wiring:
    • config field cache: bool = False on ConclaveConfig
    • CLI flag --cache/--no-cache (default unset → defers to config)
    • Council(..., cache: bool | None = None) — explicit arg overrides config
  • Cache hit returns the prior CouncilResult with new field cached=True and does not call the providers.
  • Wired into ask (synthesize/raw), debate, and adversarial.

Cache key

SHA-256 over a canonical JSON document of only output-affecting, secret-free identity:
normalized prompt · mode · ordered (friendly_name, resolved_model_id) member pairs · synthesizer/judge name + resolved id · temperature · rounds (debate) · proposer (adversarial).

Member order is preserved, not sorted: for synthesize/raw order is irrelevant, but debate assigns letter labels by position and adversarial defaults the proposer to the first member — so preserving order is the conservative, always-correct choice.

Storage

One JSON file per entry under $XDG_CACHE_HOME/conclave (else ~/.cache/conclave), written atomically (temp + replace). Payload is CouncilResult.model_dump(mode="json") — no secrets. A corrupt/unreadable entry is a miss (logged), never a crash; write failures degrade to a live run. call_model's never-raises invariant and the BYO-keys posture are untouched.

Security

The only os.environ access in cache.py is XDG_CACHE_HOME (a path). No key value or env-var name reaches the cache key or the stored payload. A dedicated test sets a fake key and asserts it appears in neither the key string nor the on-disk artifact.

Tests

  • Off by default → both identical runs execute, no file written
  • On → first miss populates, second identical run is a hit with zero new provider calls (call-counter on the patched path)
  • Key sensitivity → changing prompt / council membership / mode / resolved model id all miss
  • Security → fake key absent from key + persisted payload + filename
  • Corrupt entry → miss, run completes, no crash, entry rewritten
  • Write/path failure → degrades to live run
  • CLI --cache / --no-cache end-to-end
  • Direct make_key unit checks (determinism, order-sensitivity, whitespace normalization)

142 tests pass (126 before → +16), ruff check clean, ruff format --check clean.

Files

src/conclave/cache.py (new), src/conclave/{models,config,council,cli}.py, tests/test_cache.py (new), tests/test_cli.py, README.md, config.example.yml.

Do not merge — orchestrator reviews and merges.

Adds an opt-in on-disk result cache (PDD §9 #4 / issue #6) so repeated and
eval runs are cheap. Off by default; enabled via config (cache: true) or the
--cache/--no-cache CLI flag. The cache key is a SHA-256 over the normalized
prompt, ordered council members (friendly name + resolved model id), mode,
synthesizer/judge identity, and mode params (temperature, debate rounds,
adversarial proposer). Member order is preserved because debate/adversarial
ordering affects output.

Security posture: no env-var value or name reaches the cache key or the stored
payload; the only os.environ read in cache.py is XDG_CACHE_HOME (a path). The
stored payload is CouncilResult.model_dump(mode=json), which carries no secrets.
A security test sets a fake key and asserts it appears in neither the key nor
the on-disk artifact.

Storage: one JSON file per entry under $XDG_CACHE_HOME/conclave (else
~/.cache/conclave), written atomically via temp+replace. A corrupt/unreadable
entry is treated as a miss (logged), never a crash; write failures degrade to a
live run. call_model's never-raises invariant and BYO-keys posture are untouched.

Cache hits are signaled via a new CouncilResult.cached bool (default False).

closes #6
@ernestprovo23 ernestprovo23 merged commit 8bea14f into main Jun 9, 2026
5 checks passed
@ernestprovo23 ernestprovo23 deleted the feat/result-cache branch June 10, 2026 00:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant