headroom/tests/fixtures/issues/headroom_issue_2059.json
Rod Boev 38479fcda1
fix(codex): rerun memory lookup on every response.create WS frame (#2113)
## Description

Long-lived Codex `/v1/responses` WebSocket sessions only ran memory
decision, query construction, and context injection on the first
`response.create` frame because `handle_openai_responses_ws` kept that
logic outside the relay loop. This change extracts that path into a
local helper reused before compression for every eligible
`response.create`, while keeping sticky memory tools deduplicated by the
existing session helper.

Closes #2059

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Extracted the first-frame memory preparation path into a local async
helper inside `handle_openai_responses_ws`.
- Reused that helper for the initial frame and every later eligible
`response.create` before compression and shaping.
- Added focused two-turn WebSocket regressions for per-frame lookup,
bypass and disabled-memory handling, list-shaped later inputs,
memory-handler fail-open recovery, and sticky-tool replay.
- Raised the locked production floors for `click` and `pillow` to clear
the current `pip-audit` findings that now fail external PR merge
snapshots.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_codex_ws_per_frame_memory.py
tests/test_openai_codex_ws_lifecycle.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/proxy/handlers/openai.py
tests/test_codex_ws_per_frame_memory.py
tests/test_openai_codex_ws_lifecycle.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
uv run pytest tests/test_codex_ws_per_frame_memory.py tests/test_openai_codex_ws_lifecycle.py -q

============================= test session starts =============================
platform win32 -- Python 3.12.13, pytest-9.0.3, pluggy-1.6.0
rootdir: D:\Repos\headroom-pr-2059-codex-ws-memory-lookup
configfile: pyproject.toml
plugins: anyio-4.12.1, langsmith-0.9.3, asyncio-1.3.0, cov-7.0.0
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 35 items

tests\test_codex_ws_per_frame_memory.py .........                        [ 25%]
tests\test_openai_codex_ws_lifecycle.py ..........................       [100%]

============================= 35 passed in 1.89s ==============================

uv run ruff check headroom/proxy/handlers/openai.py tests/test_codex_ws_per_frame_memory.py tests/test_openai_codex_ws_lifecycle.py
All checks passed!

uv run ruff format headroom/proxy/handlers/openai.py tests/test_codex_ws_per_frame_memory.py tests/test_openai_codex_ws_lifecycle.py --check
3 files already formatted
```

## Real Behavior Proof

- Environment: Windows, Python 3.12.13, in-process WebSocket harness
with a fake memory handler
- Exact command / steps: `uv run pytest
tests/test_codex_ws_per_frame_memory.py
tests/test_openai_codex_ws_lifecycle.py -q`, which opens one connection,
sends distinct `response.create` frames, and records each memory lookup
plus the forwarded tool set
- Observed result: the handler performs one lookup per eligible frame,
later-frame compression sees current-turn memory-prepared input, and
both forwarded turns carry the same deduplicated `memory_search` and
`memory_save` definitions
- Not tested: live Codex subscription WebSocket

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

- Security CI currently flags `click 8.3.1` and `pillow 12.2.0`, so this
PR carries the narrow floor bump to `click>=8.3.3` and `pillow>=12.3.0`
as a supply-chain unblock for the same final merge snapshot.
- `CHANGELOG.md` remains untouched because Headroom generates release
notes from conventional commits.
- This PR is scoped to the OpenAI Responses WebSocket relay lifecycle;
it does not change the HTTP `/v1/responses` path or the Anthropic
handler.

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-13 14:42:27 -04:00

1 line
3 KiB
JSON

{"author":"superdiaodiao","body":"## Description\n\nFor a long-lived Codex WebSocket session, Headroom performs memory context lookup and tool injection only for the first client frame. Later `response.create` frames are compressed and output-shaped, but they do not run the memory decision/query/injection path again.\n\nThis means a user prompt sent on a later turn cannot retrieve newly relevant memories automatically, even though memory is enabled and the same WebSocket remains open.\n\n## To Reproduce\n\n1. Start Headroom with OpenAI/Codex memory enabled, including context injection.\n2. Connect Codex through `/v1/responses` WebSocket.\n3. Send a first `response.create` frame that establishes the session.\n4. Save a durable memory, or choose an existing memory relevant only to a later prompt.\n5. Send a second `response.create` frame on the same WebSocket whose user input should match that memory.\n6. Observe that the second frame is forwarded without a new memory lookup or context injection.\n\nA static source check shows the same lifecycle gap: the memory block in `handle_openai_responses_websocket` runs before the relay loop, while `_client_to_upstream` applies `_maybe_compress_response_create_frame` and output shaping to subsequent frames but does not rerun memory lookup/injection.\n\n## Expected Behavior\n\nEvery new `response.create` turn should independently:\n\n- resolve project/user memory scope;\n- build a query from that turn's current user input;\n- search and inject relevant memory;\n- preserve session-sticky memory tool definitions without duplicating them.\n\n## Actual Behavior\n\nOnly the first frame gets the memory pipeline. Later turns on the same WebSocket miss automatic retrieval.\n\n## Code Sample\n\nConceptual frame sequence:\n\n```json\n{\"type\":\"response.create\",\"response\":{\"input\":\"initial turn\"}}\n{\"type\":\"response.create\",\"response\":{\"input\":\"later turn requiring stored preference\"}}\n```\n\nThe second frame reaches the compression/shaping path but not the memory lookup path.\n\n## Error Output\n\nNo exception is emitted. This is a silent behavior gap; logs show no memory lookup/injection event for later `response.create` frames.\n\n## Environment\n\n- **Headroom version**: 0.31.0 and current main at `868b88bc6400c98f11134dbbe3cb03d1ecff7e1d`\n- **Python version**: 3.13\n- **OS**: macOS\n- **LLM Provider**: OpenAI Responses / Codex subscription WebSocket\n\n## Additional Context\n\nThis appears independent of project DB routing fixes such as GH #462/#1147. The correct DB may be selected, but the lookup is never invoked for the later turn.\n\nA regression test could open one WS connection, send two `response.create` frames with distinct inputs, and assert that memory query construction/injection runs once per frame while tool definitions remain deduplicated.\n","labels":["bug"],"number":2059,"state":"OPEN","title":"[BUG] Codex WebSocket memory lookup only runs on the first response.create frame","updatedAt":"2026-07-12T13:32:05Z","url":"https://github.com/headroomlabs-ai/headroom/issues/2059"}