Commit graph

2 commits

Author SHA1 Message Date
chopratejas
2fb905fdb0 fix: integrate B6+B7 — fix cross-test contamination + injector mock parity
Two follow-ups surfaced when B6 and B7 were merged onto the megamerge
branch and the full suite ran:

1. tests/test_proxy_anthropic_cache_stability.py
   PR-B7 added `injector.scan_for_markers(optimized_messages)` to the
   Anthropic handler so the always-on tool-registration logic can see
   detected hashes for the current request. The two pre-existing
   `_FakeInjector` mocks (`test_ccr_system_instruction_injection_disabled_*`
   and `test_ccr_tool_injection_disabled_*`) didn't implement that method.
   Added a no-op `scan_for_markers` returning [] to both mocks — matches
   the real injector's contract for the not-yet-compressed request shape
   these tests exercise.

2. tests/test_memory_tool_mode.py::test_tool_mode_skip_emits_structured_log
   The B6 caplog assertion passed in isolation but failed in the full
   suite. Root cause: when an earlier test triggers proxy startup,
   `_setup_file_logging` flips `headroom.propagate=False` and attaches a
   RotatingFileHandler to the headroom logger. caplog captures via
   propagation to root, so log records stop reaching it. The conftest
   autouse fixture that resets `propagate=True` before every test gets
   shadowed by fixture-ordering edge cases.

   Principled fix: attach `caplog.handler` directly to
   `headroom.proxy.memory_handler` for the duration of the test so the
   capture is independent of propagation state. Restore the original
   level + remove the handler in `finally` to keep the test hermetic.

Both B6 and B7 cherry-picks themselves are unmodified. This commit only
adjusts test harness code so the pre-existing mocks/capture stay
consistent with the new code paths.
2026-05-02 17:05:58 -07:00
chopratejas
2ee05774b9 fix: B6 — memory injection moves to live-zone user-tail
PR-A2 locked the system prompt and routed Anthropic memory injection to the
latest non-frozen user turn. PR-B6 finishes the job: every provider handler
that auto-injects memory context now does so via the live-zone tail, and a
new MemoryMode enum makes the routing explicit and configurable.

What changed
------------
* New `MemoryMode` enum in `headroom/proxy/memory_handler.py` with two
  values:
    - `AUTO_TAIL` (default) — retrieval results auto-append to the latest
      user message. The cache hot zone (system / instructions / frozen
      prefix) is never mutated.
    - `TOOL` — auto-injection is disabled entirely. The model must call
      `memory_search` to retrieve. Memory is opt-in and visible.
* `MemoryConfig.mode: MemoryMode = MemoryMode.AUTO_TAIL` propagates into
  `search_and_format_context`, which now short-circuits to `None` in `TOOL`
  mode. This is the single chokepoint that gates every provider — Anthropic
  /v1/messages, OpenAI /v1/chat/completions, OpenAI /v1/responses, and
  Gemini all funnel through it, so flipping a deployment to tool mode does
  not require auditing every handler.
* New `MemoryHandler._append_to_latest_user_tail(messages, context_text,
  provider=..., frozen_message_count=...)` static helper provides the unified
  tail-append entry point and dispatches to the existing provider-specific
  helpers (`AnthropicHandlerMixin._append_context_to_latest_non_frozen_user_turn`
  for Anthropic, `append_text_to_latest_user_chat_message` for OpenAI).
* Gemini handler swapped from auto-prepending memory as a system message
  (the old P2-24 cache-hot-zone mutation pattern) to using
  `_append_to_latest_user_tail(provider="openai")`.
* `ProxyConfig.memory_mode: Literal["auto_tail", "tool"] = "auto_tail"`
  surfaces the mode for deployment configuration. Server constructs the
  enum via `MemoryMode(config.memory_mode)` and raises loudly on unknown
  values (no silent fallback).
* OpenAI Chat Completions, OpenAI Responses, and Anthropic handlers were
  already routing to the live-zone tail via PR-A2/A3 — no code change
  needed beyond inheriting the `TOOL`-mode skip from the chokepoint.

Tests
-----
* `tests/test_memory_auto_tail.py` (6 tests):
    - `test_memory_appears_in_latest_user_message_tail` — Anthropic shape.
    - `test_memory_appears_in_latest_user_message_tail_openai_shape` —
      OpenAI string + list-content shapes.
    - `test_memory_does_not_modify_system_or_tools` — system prompt and
      tools list are never touched; frozen-prefix tail is a no-op.
    - `test_same_query_byte_identical_across_runs` — two independent runs
      with identical inputs produce byte-identical mutated message lists
      (determinism gate).
    - `test_default_mode_is_auto_tail` — fresh `MemoryConfig` defaults to
      `AUTO_TAIL`.
    - `test_unknown_provider_raises` — invalid provider strings raise
      loudly per the no-silent-fallback policy.
* `tests/test_memory_tool_mode.py` (4 tests):
    - `test_tool_mode_skips_auto_injection` — `search_and_format_context`
      returns `None` and the backend is never queried.
    - `test_tool_mode_skip_emits_structured_log` — skip emits the
      `event=memory_mode_skip` log line for routing-decision auditability.
    - `test_auto_tail_mode_does_query_backend` — inverse contrast pinning
      down that AUTO_TAIL still works end-to-end while TOOL skips.
    - `test_tool_mode_enum_value_is_stable` — string round-trip is pinned
      so deployment configs do not drift on rename.

Determinism
-----------
Tests stub the backend with a fixed, ordered result set so the byte-identical
assertion isolates the tail-injection layer from upstream search non-
determinism. The vector-search layer itself (LocalBackend / HNSW) is
deterministic per-process for the same inputs but has thread-scheduling
variability across processes; per the realignment plan, request-time
determinism is guaranteed by the formatter and the tail-append helpers
(this PR's responsibility), and the backend layer's determinism stays
out-of-scope for B6.

Per-PR-B6 plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 16:52:31 -07:00