fleet-memory/hindsight-integrations/codex/tests/conftest.py
Ben 0b17a67c70
feat: add Hindsight memory integration for OpenAI Codex CLI (#730)
* feat(codex): add Hindsight memory integration for OpenAI Codex CLI

Hooks-based integration that gives Codex CLI long-term memory via Hindsight.
Three hooks keep memory in sync: SessionStart (daemon pre-warm), UserPromptSubmit
(recall + context injection), Stop (retain conversation to memory).

Key differences from the Claude Code integration:
- Codex transcript format: JSONL with {msg: {type, message}} (user_message/agent_message)
- No CODEX_PLUGIN_ROOT env var — install.sh writes hooks.json with absolute paths
- State stored in ~/.hindsight/codex/state/ (not CLAUDE_PLUGIN_DATA)
- No async: true in hooks (not supported by Codex)
- No SessionEnd event
- hooks.json written to ~/.codex/hooks.json with codex_hooks = true in config.toml

* fix(codex): fix transcript parser for actual Codex disk format

Codex stores sessions as rollout-*.jsonl with response_item entries:
  User:      {type:response_item, payload:{type:message, role:user, content:[{type:input_text, text:...}]}}
  Assistant: {type:response_item, payload:{type:message, role:assistant, phase:final_answer, content:[{type:output_text, text:...}]}}

Previous parser expected an undocumented {msg:{type:user_message}} format from the Rust protocol spec
that does not match the actual on-disk storage format.

* feat(codex): add reflect mode to UserPromptSubmit hook

Add recallMode config option (default: 'recall') that switches the
UserPromptSubmit hook between:
- 'recall': existing behavior, fast raw facts list
- 'reflect': agentic synthesis loop, returns coherent prose answer

Also adds reflect() method to HindsightClient and HINDSIGHT_RECALL_MODE
env var override. Reflect uses a 25s timeout (vs 10s for recall).

* feat(codex): auto mode for recall/reflect selection

Add recallMode: 'auto' (new default) that picks the operation per-query:
- Synthesis patterns (what do you know, what's my, summarize, etc.) → reflect
- All other prompts → recall (fast, raw facts, better for code tasks)

* feat(codex): add automated test suite and finalize recall-only mode

* docs(codex): add docs page and sidebar entry for Codex CLI integration
2026-03-30 10:51:53 +02:00

90 lines
2.6 KiB
Python

"""Shared fixtures for Hindsight Codex plugin tests."""
import io
import json
import os
import sys
import pytest
# Make scripts/ importable as the root — the hook scripts do:
# sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# so lib.* imports resolve relative to scripts/
SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts")
if SCRIPTS_DIR not in sys.path:
sys.path.insert(0, os.path.abspath(SCRIPTS_DIR))
def make_hook_input(
prompt="What is the capital of France?",
session_id="sess-abc123",
cwd="/home/user/myproject",
transcript_path="",
):
return {
"prompt": prompt,
"session_id": session_id,
"cwd": cwd,
"transcript_path": transcript_path,
}
def make_transcript_file(tmp_path, messages, codex_format=False):
"""Write messages as a JSONL transcript file.
By default writes flat format {role, content} which read_transcript() accepts.
Set codex_format=True to write actual Codex response_item format.
"""
f = tmp_path / "rollout-test.jsonl"
lines = []
for msg in messages:
if codex_format:
role = msg["role"]
text = msg["content"]
content_type = "input_text" if role == "user" else "output_text"
entry = {
"type": "response_item",
"payload": {
"type": "message",
"role": role,
"content": [{"type": content_type, "text": text}],
},
}
if role == "assistant":
entry["payload"]["phase"] = "final_answer"
lines.append(json.dumps(entry))
else:
lines.append(json.dumps(msg))
f.write_text("\n".join(lines))
return str(f)
def make_memory(text, mem_type="experience", mentioned_at="2024-01-15"):
return {"text": text, "type": mem_type, "mentioned_at": mentioned_at}
def make_user_config(tmp_path, overrides=None):
"""Write a ~/.hindsight/codex.json in tmp_path with test defaults."""
hindsight_dir = tmp_path / ".hindsight"
hindsight_dir.mkdir(exist_ok=True)
config = {"retainEveryNTurns": 1}
if overrides:
config.update(overrides)
(hindsight_dir / "codex.json").write_text(json.dumps(config))
class FakeHTTPResponse:
"""Minimal urllib response mock."""
def __init__(self, data: dict, status: int = 200):
self.status = status
self._data = json.dumps(data).encode()
def read(self):
return self._data
def __enter__(self):
return self
def __exit__(self, *_):
pass