From 184a11be997bc53855d3551e02d495e99bc63a71 Mon Sep 17 00:00:00 2001
From: Pavel Tkachyk
Date: Tue, 18 Aug 2026 16:56:33 -0400
Subject: [PATCH 1/2] fix: align live Codex scope and MCP timeouts
---
src/codex_memory/__init__.py | 5 +++--
src/codex_memory/client.py | 2 +-
src/codex_memory/history.py | 3 ++-
src/codex_memory/scope.py | 20 ++++++++++++++++++++
src/codex_memory/tools.py | 5 +++--
tests/test_codex_tools.py | 30 ++++++++++++++++++++++++++++--
tests/test_history_import.py | 2 ++
tests/test_hosted_onboarding.py | 1 +
tests/test_native_hooks.py | 2 ++
9 files changed, 62 insertions(+), 8 deletions(-)
create mode 100644 src/codex_memory/scope.py
diff --git a/src/codex_memory/__init__.py b/src/codex_memory/__init__.py
index c635b89..3005461 100644
--- a/src/codex_memory/__init__.py
+++ b/src/codex_memory/__init__.py
@@ -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"
@@ -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,
)
@@ -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
diff --git a/src/codex_memory/client.py b/src/codex_memory/client.py
index 45a227b..8eb9632 100644
--- a/src/codex_memory/client.py
+++ b/src/codex_memory/client.py
@@ -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()
diff --git a/src/codex_memory/history.py b/src/codex_memory/history.py
index d5a9c20..8673526 100644
--- a/src/codex_memory/history.py
+++ b/src/codex_memory/history.py
@@ -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
@@ -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,
)
diff --git a/src/codex_memory/scope.py b/src/codex_memory/scope.py
new file mode 100644
index 0000000..ef1144a
--- /dev/null
+++ b/src/codex_memory/scope.py
@@ -0,0 +1,20 @@
+"""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}
diff --git a/src/codex_memory/tools.py b/src/codex_memory/tools.py
index b858c81..1909690 100644
--- a/src/codex_memory/tools.py
+++ b/src/codex_memory/tools.py
@@ -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
@@ -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
@@ -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", [])
}
)
diff --git a/tests/test_codex_tools.py b/tests/test_codex_tools.py
index 32dbe90..4f713bb 100644
--- a/tests/test_codex_tools.py
+++ b/tests/test_codex_tools.py
@@ -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):
@@ -30,6 +31,20 @@ 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"}
+ 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"
@@ -39,7 +54,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"}
@@ -66,7 +85,14 @@ 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"},
+ }
+ ],
}
assert tools["substrate_wiki_search"].handler({"query": "pages", "limit": 2}) == {
"ok": True,
diff --git a/tests/test_history_import.py b/tests/test_history_import.py
index 16a47c3..fb2502d 100644
--- a/tests/test_history_import.py
+++ b/tests/test_history_import.py
@@ -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*"))
diff --git a/tests/test_hosted_onboarding.py b/tests/test_hosted_onboarding.py
index 3432633..f435a99 100644
--- a/tests/test_hosted_onboarding.py
+++ b/tests/test_hosted_onboarding.py
@@ -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,
diff --git a/tests/test_native_hooks.py b/tests/test_native_hooks.py
index e9eadf2..dd80d79 100644
--- a/tests/test_native_hooks.py
+++ b/tests/test_native_hooks.py
@@ -87,6 +87,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)
From 09e7bb51182c1fb848224dd386c2ad445c17bc18 Mon Sep 17 00:00:00 2001
From: Pavel Tkachyk
Date: Tue, 18 Aug 2026 17:03:01 -0400
Subject: [PATCH 2/2] fix: share the exact canonical recall scope
---
src/codex_memory/hook.py | 5 ++++-
src/codex_memory/scope.py | 6 +++++-
tests/test_codex_tools.py | 12 ++++++++++--
tests/test_native_hooks.py | 22 +++++++++++++++++++++-
4 files changed, 40 insertions(+), 5 deletions(-)
diff --git a/src/codex_memory/hook.py b/src/codex_memory/hook.py
index f7af477..16afa8a 100644
--- a/src/codex_memory/hook.py
+++ b/src/codex_memory/hook.py
@@ -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
@@ -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:
diff --git a/src/codex_memory/scope.py b/src/codex_memory/scope.py
index ef1144a..20864c1 100644
--- a/src/codex_memory/scope.py
+++ b/src/codex_memory/scope.py
@@ -17,4 +17,8 @@ def capture_scope() -> dict[str, str]:
def recall_scope() -> dict[str, str]:
"""Return the matching canonical-recall identity scope."""
- return {"platform": CODEX_PLATFORM, "user_id": CODEX_USER_ID}
+ return {
+ "platform": CODEX_PLATFORM,
+ "user_id": CODEX_USER_ID,
+ "agent_id": CODEX_AGENT_ID,
+ }
diff --git a/tests/test_codex_tools.py b/tests/test_codex_tools.py
index 4f713bb..97fee83 100644
--- a/tests/test_codex_tools.py
+++ b/tests/test_codex_tools.py
@@ -37,7 +37,11 @@ def test_codex_owner_scope_matches_capture_and_canonical_recall() -> None:
"user_id": "owner",
"agent_id": "codex-cli",
}
- assert recall_scope() == {"platform": "codex", "user_id": "owner"}
+ 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"}
)
@@ -90,7 +94,11 @@ def test_codex_tools_use_wiki_search_job_status_and_truthful_queue_status(
"surface": "canonical",
"query": "decision",
"limit": 3,
- "scope": {"platform": "codex", "user_id": "owner"},
+ "scope": {
+ "platform": "codex",
+ "user_id": "owner",
+ "agent_id": "codex-cli",
+ },
}
],
}
diff --git a/tests/test_native_hooks.py b/tests/test_native_hooks.py
index dd80d79..67750b7 100644
--- a/tests/test_native_hooks.py
+++ b/tests/test_native_hooks.py
@@ -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 = []