* 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
90 lines
2.6 KiB
Python
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
|