Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/codex_memory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .delivery import CodexDeliverer
from .local_security import secure_windows_tree
from .queue import DurableEventQueue
from .scope import capture_scope
from .state import HookState

PROVIDER_ID = "codex_memory"
Expand Down Expand Up @@ -44,7 +45,7 @@ def _capture_parts(
secrets = tuple(dict.fromkeys((*secrets, token)))
queue = DurableEventQueue(root / "queue")
builder = CaptureEventBuilder(
{"platform": "codex", "agent_id": "codex-cli"},
capture_scope(),
provider_id=PROVIDER_ID,
secrets=secrets,
)
Expand Down Expand Up @@ -80,7 +81,7 @@ def runtime() -> tuple[
root = plugin_data()
token = credential_store(root).get()
queue, builder, _state = _capture_parts(root, token)
client = SubstrateClient(token, timeout=10.0) if token else None
client = SubstrateClient(token, timeout=60.0) if token else None
return client, queue, CodexDeliverer(queue, client), builder


Expand Down
2 changes: 1 addition & 1 deletion src/codex_memory/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def validate_connection(self) -> dict[str, Any]:
class CredentialBoundClient:
"""Mutable hosted client that follows onboarding credential rotation."""

def __init__(self, store: CredentialStore, *, timeout: float = 10.0) -> None:
def __init__(self, store: CredentialStore, *, timeout: float = 60.0) -> None:
self.store = store
self.timeout = timeout
self._mutex = threading.RLock()
Expand Down
3 changes: 2 additions & 1 deletion src/codex_memory/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from .app_server import CodexAppServer, CodexAppServerError
from .import_checkpoint import HistoryCheckpoint
from .scope import capture_scope

_MAX_MESSAGES_PER_THREAD = 1000
_MAX_VISIBLE_CHARS_PER_THREAD = 2_000_000
Expand Down Expand Up @@ -222,7 +223,7 @@ def _deliver(self, event: dict[str, Any]) -> tuple[dict[str, Any], int]:

def _builder(self) -> CaptureEventBuilder:
return CaptureEventBuilder(
{"platform": "codex", "agent_id": "codex-cli"},
capture_scope(),
provider_id="codex_memory", secrets=self.secrets,
)

Expand Down
5 changes: 4 additions & 1 deletion src/codex_memory/hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from substrate_capture.events import content_digest

from . import capture_runtime, hook_runtime
from .scope import recall_scope

_MAX_STDIN_BYTES = 4 * 1024 * 1024
_MAX_CAPTURE_CHARS = 200_000
Expand Down Expand Up @@ -172,7 +173,9 @@ def _recall_result(
return callback(prompt[:_MAX_RECALL_QUERY_CHARS], 5)
if client is None:
return None
return client.memory_search(prompt[:_MAX_RECALL_QUERY_CHARS], limit=5)
return client.memory_search(
prompt[:_MAX_RECALL_QUERY_CHARS], limit=5, scope=recall_scope()
)


def _canonical_entity_path(item: dict[str, Any]) -> str | None:
Expand Down
24 changes: 24 additions & 0 deletions src/codex_memory/scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Stable single-user identity scope inside each authenticated Substrate tenant."""
from __future__ import annotations

CODEX_PLATFORM = "codex"
CODEX_USER_ID = "owner"
CODEX_AGENT_ID = "codex-cli"


def capture_scope() -> dict[str, str]:
"""Return a fresh scope shared by live hooks, tools, and history replay."""
return {
"platform": CODEX_PLATFORM,
"user_id": CODEX_USER_ID,
"agent_id": CODEX_AGENT_ID,
}


def recall_scope() -> dict[str, str]:
"""Return the matching canonical-recall identity scope."""
return {
"platform": CODEX_PLATFORM,
"user_id": CODEX_USER_ID,
"agent_id": CODEX_AGENT_ID,
}
5 changes: 3 additions & 2 deletions src/codex_memory/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from .client import SubstrateClient
from .onboarding import OnboardingError, OnboardingManager
from .scope import recall_scope

_MAX_QUERY = 4096
_MAX_CONTENT = 200_000
Expand All @@ -34,7 +35,7 @@ def refresh_client() -> SubstrateClient | None:
nonlocal client
token = onboarding.store.get()
if token and (client is None or client.api_key != token):
client = SubstrateClient(token, timeout=10.0)
client = SubstrateClient(token, timeout=60.0)
deliverer.client = client
return client

Expand Down Expand Up @@ -68,7 +69,7 @@ def search(args: dict[str, Any]) -> dict[str, Any]:
return guard(
lambda: {
"results": require_client().memory_search(
text(args, "query"), limit=limit(args), scope={"platform": "codex"}
text(args, "query"), limit=limit(args), scope=recall_scope()
).get("results", [])
}
)
Expand Down
38 changes: 36 additions & 2 deletions tests/test_codex_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from substrate_capture.spool import DurableSpool # noqa: E402
from substrate_capture.client import SubstrateAPIError # noqa: E402
from codex_memory.queue import DurableEventQueue # noqa: E402
from codex_memory.scope import capture_scope, recall_scope # noqa: E402


class EmptyStore(CredentialStore):
Expand All @@ -30,6 +31,24 @@ def delete(self, slot: str = "access-token") -> None:
pass


def test_codex_owner_scope_matches_capture_and_canonical_recall() -> None:
assert capture_scope() == {
"platform": "codex",
"user_id": "owner",
"agent_id": "codex-cli",
}
assert recall_scope() == {
"platform": "codex",
"user_id": "owner",
"agent_id": "codex-cli",
}
event = CaptureEventBuilder(capture_scope(), provider_id="codex_memory").payload_event(
"turn", "session", {"content": "safe"}
)
assert event["scope"]["subject_id"]
assert event["scope"]["user_id"] == recall_scope()["user_id"]


class WikiClient:
api_key = "test-placeholder-key"

Expand All @@ -39,7 +58,11 @@ def search(self, query: str, *, limit: int) -> dict[str, Any]:
def memory_search(
self, query: str, *, limit: int, scope: dict[str, Any] | None = None
) -> dict[str, Any]:
return {"results": [{"surface": "canonical", "query": query, "limit": limit}]}
return {
"results": [
{"surface": "canonical", "query": query, "limit": limit, "scope": scope}
]
}

def job_status(self, job_id: str) -> dict[str, Any]:
return {"job_id": job_id, "status": "queued"}
Expand All @@ -66,7 +89,18 @@ def test_codex_tools_use_wiki_search_job_status_and_truthful_queue_status(
}
assert tools["substrate_search"].handler({"query": "decision", "limit": 3}) == {
"ok": True,
"results": [{"surface": "canonical", "query": "decision", "limit": 3}],
"results": [
{
"surface": "canonical",
"query": "decision",
"limit": 3,
"scope": {
"platform": "codex",
"user_id": "owner",
"agent_id": "codex-cli",
},
}
],
}
assert tools["substrate_wiki_search"].handler({"query": "pages", "limit": 2}) == {
"ok": True,
Expand Down
2 changes: 2 additions & 0 deletions tests/test_history_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ def test_import_is_deterministic_resumable_and_checkpoint_content_free(tmp_path:
first_ids = [event["event_id"] for event in client.events]
assert len(first_ids) == len(set(first_ids))
assert {event["capture_origin"] for event in client.events} == {"history_replay"}
assert {event["scope"]["user_id"] for event in client.events} == {"owner"}
assert {event["scope"]["platform"] for event in client.events} == {"codex"}
importer.close()

raw = b"".join(path.read_bytes() for path in (tmp_path / "imports").glob("checkpoint.sqlite3*"))
Expand Down
1 change: 1 addition & 0 deletions tests/test_hosted_onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ def test_running_mcp_client_refreshes_after_setup_and_credential_repair(tmp_path
store = MemoryStore()
oauth = OAuth()
client = CredentialBoundClient(store)
assert client.timeout == 60.0
manager = OnboardingManager(
tmp_path, api=oauth, store=store,
capability_check=lambda _token: {}, credential_changed=client.refresh,
Expand Down
24 changes: 23 additions & 1 deletion tests/test_native_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,31 @@
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))

from codex_memory import capture_runtime # noqa: E402
from codex_memory.hook import handle, main # noqa: E402
from codex_memory.hook import _recall_result, handle, main # noqa: E402
from codex_memory.state import HookState # noqa: E402
from substrate_capture.client import SubstrateClient # noqa: E402


def test_native_recall_uses_the_same_owner_and_agent_scope_as_capture() -> None:
observed: list[tuple[str, int, dict[str, object]]] = []

class Client:
def memory_search(
self, query: str, *, limit: int, scope: dict[str, object]
) -> dict[str, object]:
observed.append((query, limit, scope))
return {"results": []}

assert _recall_result(None, "decision", Client()) == {"results": []}
assert observed == [
(
"decision",
5,
{"platform": "codex", "user_id": "owner", "agent_id": "codex-cli"},
)
]


def _events() -> list[dict[str, object]]:
queue, _builder, _state = capture_runtime()
values = []
Expand Down Expand Up @@ -87,6 +107,8 @@ def test_native_hooks_capture_exact_turn_lineage_and_dedupe(
]
assert all(event["payload"]["turn_id"] == "turn-1" for event in turns)
assert all(event["payload"]["session_id"] == "session-1" for event in turns)
assert all(event["scope"]["user_id"] == "owner" for event in turns)
assert all(event["scope"]["platform"] == "codex" for event in turns)



Expand Down
Loading