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
18 changes: 12 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,11 @@ jobs:
"$cli_bin/codex" plugin list --json \
| python -c 'import json,sys; d=json.load(sys.stdin); p=[x for x in d.get("installed",[]) if x.get("pluginId")=="hypermnesia-mcp-codex@cortex-codex-plugins"]; assert len(p)==1, p; assert p[0]["installed"] and p[0]["enabled"]'
"$cli_bin/codex" mcp list --json \
| python -c 'import json,sys; ss=[s for s in json.load(sys.stdin) if s.get("name")=="cortex"]; assert len(ss)==1, ss; s=ss[0]; assert s["startup_timeout_sec"]==180.0, s; assert s["transport"]["command"]=="uvx", s; assert s["transport"]["env"]["CORTEX_RUNTIME"]=="cowork", s'
| python -c 'import json,sys; ss=[s for s in json.load(sys.stdin) if s.get("name")=="cortex"]; assert len(ss)==1, ss; s=ss[0]; assert s["startup_timeout_sec"]==180.0, s; assert s["transport"]["command"]=="python3", s; assert s["transport"]["env"]["CORTEX_RUNTIME"]=="cowork", s'
mapfile -t plugin_command < <(
python -c 'import json; s=json.load(open("plugins/hypermnesia-mcp-codex/.mcp.json"))["mcpServers"]["cortex"]; print(s["command"]); print(*s["args"], sep="\n")'
# PLUGIN_ROOT is a literal Codex placeholder resolved by Python.
# shellcheck disable=SC2016
python -c 'import json; from pathlib import Path; root=str(Path("plugins/hypermnesia-mcp-codex").resolve()); s=json.load(open(Path(root)/".mcp.json"))["mcpServers"]["cortex"]; print(s["command"]); print(*(arg.replace("${PLUGIN_ROOT}",root) for arg in s["args"]), sep="\n")'
)
plugin_timeout="$(
python -c 'import json; print(json.load(open("plugins/hypermnesia-mcp-codex/.mcp.json"))["mcpServers"]["cortex"]["startup_timeout_sec"])'
Expand All @@ -351,13 +353,17 @@ jobs:
python -c 'import json; print(json.load(open("plugins/hypermnesia-mcp-codex/.mcp.json"))["mcpServers"]["cortex"]["env"]["CORTEX_RUNTIME"])'
)"
candidate_wheel="$(find "$RUNNER_TEMP/cortex-codex-wheel" -name '*.whl' -print -quit)"
# Install verifier dependencies outside the cold cache used by the
# manifest command; full-profile bounds come from this exact tree.
# source: docs/verification/codex-hooks-20261002.md
# Prepare the candidate explicitly; event startup never installs it.
export UV_FIND_LINKS="$RUNNER_TEMP/cortex-codex-wheel"
export UV_CACHE_DIR="$RUNNER_TEMP/cortex-codex-cold-uv-cache"
export UV_TOOL_DIR="$RUNNER_TEMP/cortex-codex-cold-uv-tools"
uv tool install --python "$(command -v python3)" --no-build \
"hypermnesia-mcp[postgresql,sqlite] @ file://${candidate_wheel}"
# The verifier and manifest runtime inherit the same prepared paths.
uv run --no-project --with "$candidate_wheel" -- env \
CORTEX_RUNTIME="$plugin_runtime" \
UV_FIND_LINKS="$RUNNER_TEMP/cortex-codex-wheel" \
UV_CACHE_DIR="$RUNNER_TEMP/cortex-codex-cold-uv-cache" \
UV_TOOL_DIR="$RUNNER_TEMP/cortex-codex-cold-uv-tools" \
PYTHONPATH="$GITHUB_WORKSPACE" \
python scripts/verify_mcp_hosts.py \
--timeout "$plugin_timeout" --clients codex-cli --profiles full \
Expand Down
109 changes: 52 additions & 57 deletions docs/codex-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,34 +23,21 @@ MCP-only, `lean`-profile package.

## How the hooks run under Codex

A Codex plugin ships only its own directory, never this repository, and never
`scripts/launcher.py`, which is Claude-only. Runtime hooks invoke the
console script the wheel declares (`hypermnesia-mcp-hook`, `pyproject.toml`,
added in #605):
The bundled `scripts/runtime.py` checks the installed uv tool's wheel version
and executes its isolated interpreter (`-I`). The hook command is:

```bash
uvx --from "hypermnesia-mcp[postgresql,sqlite]==4.23.5" hypermnesia-mcp-hook <module>
```sh
python3 "${PLUGIN_ROOT}/scripts/runtime.py" <module>
```

`mcp_server/hooks/entry.py` validates `<module>` against `HOOK_MODULES`, wires
the composition root (issue #560), reads the one stdin event, normalizes it
(`mcp_server/hooks/host_event.py`, #608) and dispatches to the hook module
unchanged. Codex's `apply_patch` calls are expanded into one Edit/Write event
per file operation, `exec_command`/`shell_command` become a `Bash`-shaped
event, and `SubagentStart`'s `agent_type` is mapped to `agent_name`; a
Claude-shaped event passes through untouched.

Runtime commands check that `uvx` is present and report a named diagnostic if
it is missing. Session-end intake and startup recovery use the bundled
`${PLUGIN_ROOT}/scripts/session_queue.py` through Python 3. They do not import
the runtime before persisting or scheduling work.
No event resolves dependencies, downloads packages or compiles native extensions.
Setup prepares the environment separately; absent or stale installations fail
with a named setup diagnostic. The wheel's `mcp_server/hooks/entry.py` validates
the hook module and wires the configured backend. SessionEnd first persists
its event using the bundled standard-library queue, then invokes this same
runtime in a replayable worker. No repository module can shadow the wheel.

The MCP server and runtime hooks pin the wheel to the plugin version. This
prevents a cached older wheel from silently handling newer event contracts.
CI builds that candidate wheel and supplies it through `UV_FIND_LINKS` before
the version is published; deployment uses the same requirement from PyPI.

### Event names and matchers
## Event names and matchers

Event names are Codex's own, verified against
[learn.chatgpt.com/docs/hooks](https://learn.chatgpt.com/docs/hooks) (read
Expand Down Expand Up @@ -125,20 +112,18 @@ The call continues through the normal permission flow, so don't count on a
stalled hook to act as a gate." A timeout on a gate therefore fails **open**,
so a tight one would trade a rare long wait for silently ungated edits.
Warm, these cost 0.16s to 0.26s (measured 2026-09-22, same conditions as
above); the long wait is only the first cold `uvx` resolve, which the prewarm
removes. Note that this exposure is identical on Claude Code today, so it is
a property of both manifests rather than something Codex introduces.
above). The current Codex dispatcher requires prepared dependencies;
no package resolution occurs during an event.

Leaving the rest unset would not have been the parity case: on
`UserPromptSubmit`, a stalled `uvx` resolve or a blocked database would hold
`UserPromptSubmit`, a blocked database would hold
up every prompt for ten minutes where Claude Code caps the same hook at five
seconds. For those, failing open is the right trade, because what a cancelled
run costs is one skipped enrichment. A cold `uv` cache will exceed those
budgets; prewarming (below) removes the window.
run costs is one skipped enrichment. An absent runtime is reported immediately with the setup instruction.

### Cost per edit

Every runtime hook is its own `uvx` process, so one `apply_patch`, `Edit` or `Write`
Every runtime hook is its own Python process, so one `apply_patch`, `Edit` or `Write`
fires up to five of them: `decision_gate` and `no_deps_gate` before the call,
then `post_tool_capture`, `preemptive_context` and `pipeline_impact_bump`
after it. A patch touching several files still costs five processes, not five
Expand All @@ -149,8 +134,8 @@ Measured on 2026-09-22 (macOS 26.6.2 arm64, uv 0.11.3, warm `uv` cache,
published 4.23.1 wheel), one such process takes 0.18s to 0.21s in steady
state, with the first invocation of a given module slower (1.3s to 5.1s
observed) while its caches fill. The two gates are the ones in the blocking
path, at roughly 0.4s of that total. On a cold cache the first hook pays the
full resolve instead, which is what the prewarm below is for.
path, at roughly 0.4s of that total. These historical measurements used the former uvx dispatcher.
Current dispatcher measurements are in `verification/codex-hooks-20261002.md`.

Matchers are widened to carry Codex's native tool names alongside Claude's.
The Codex docs state that for `apply_patch` "hook input still reports
Expand Down Expand Up @@ -189,7 +174,7 @@ codex plugin add hypermnesia-mcp-codex@cortex-codex-plugins
```

Restart the ChatGPT desktop app and start a new task so Codex loads the new
plugin components. The plugin uses `uvx`, so `uv` must be available on `PATH`.
plugin components. The dispatcher uses `uv tool dir`, so `uv` must be available on `PATH`.
The first launch installs both storage drivers. Direct MCP startup reads the
same `~/.claude/methodology/backend.json` selection as the Claude launcher
before loading memory settings. `CORTEX_CLAUDE_DIR` relocates that shared
Expand All @@ -207,34 +192,20 @@ configured but unreachable database URL remains an error rather than silently
redirecting writes. Both hosts must use the same configuration root and storage
settings to share memories; this does not merge previously separate stores.

A prewarm downloads the package before restarting Codex:
Prepare the matching wheel with the native Python before restarting Codex:

```bash
uv tool install "hypermnesia-mcp[postgresql,sqlite]==4.23.5"
```sh
python3 "<installed-plugin>/scripts/runtime.py" setup
```

For the MCP server this is only a startup optimization, `startup_timeout_sec`
already covers a cold resolve. For the lifecycle hooks it matters more: every
hook is its own `uvx` invocation, so a cold cache makes the first one pay the
full package resolve. Prewarming removes that window. Session-end intake runs before package resolution; a cold worker keeps its event
queued until recording completes.

The bundled server declares `startup_timeout_sec: 180`. This is a bounded
startup ceiling, not a delay. Re-measured for the full profile on 2026-09-22
with `scripts/verify_mcp_hosts.py`, the same script and flags CI runs, the
exact two-driver command below completed `initialize`, `tools/list`, and a real
`memory_stats` call over **59 tools in 120.17 seconds** from clean
`UV_CACHE_DIR` and `UV_TOOL_DIR` directories on macOS 26.6.2 arm64 with uv
0.11.3. The next run from that cache completed the same contract in 4.24
seconds. CI reads the command, runtime policy, and timeout from the manifest
itself and repeats the clean-cache contract against the `full` profile.

This explicit setup is required for both MCP and lifecycle hooks. It installs
binary wheels into the uv tool environment. SessionEnd preserves failures in
its queue for later recovery. The MCP server retains its 180-second startup
ceiling; this includes server initialization, not dependency preparation.
The bundled MCP command is equivalent to:

```bash
env CORTEX_RUNTIME=cowork \
uvx --from "hypermnesia-mcp[postgresql,sqlite]==4.23.5" \
hypermnesia-mcp
```sh
env CORTEX_RUNTIME=cowork python3 "${PLUGIN_ROOT}/scripts/runtime.py" server
```

`CORTEX_RUNTIME=cowork` selects Cortex's existing DB-optional local-runtime
Expand Down Expand Up @@ -265,3 +236,27 @@ this local package.

See [shared memory and decisions](shared-host-memory.md) for the common storage
and ADR-root contract, opt-in authoring, and the limits of the handoff tests.

## Codex runtime setup and diagnosis (2 October 2026)

The bundled `scripts/runtime.py` dispatches hooks through the installed
`hypermnesia-mcp` uv tool environment and verifies its version against the plugin
manifest. An absent or mismatched runtime fails with an explicit setup message.
Hooks and SessionEnd replay do not resolve, download or compile dependencies.

Before trusting the hooks, install the matching wheel using the native Python:

```sh
python3 "<installed-plugin>/scripts/runtime.py" setup
```

Setup uses `uv tool install --python <the setup interpreter> --no-build`; use a
native interpreter on Apple Silicon. Source changes cannot repair an already
published wheel: the published Intel cryptography bound needs a future release.
After a plugin update, review changed definitions in the native `/hooks` browser.
Do not write trust hashes or bypass review. A prepared runtime or direct-hook
test proves neither native event delivery nor successful memory persistence.

The 2 October failure occurred before hook code loaded: unqualified uvx selected
Intel CPython on an ARM host and attempted cryptography compilation, then exceeded
5/10-second hook deadlines. See `docs/verification/codex-hooks-20261002.md`.
67 changes: 67 additions & 0 deletions docs/verification/codex-hooks-20261002.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Codex hooks repair — 2 October 2026

Source baseline: c32a6a025319407630cab4733a4e0b17ae2d9588, local edits only.
Host: macOS ARM, Codex CLI 0.160.0; desktop binary 0.159.0.

## Confirmed failure

All 28 original hooks were enabled and trusted according to native `hooks/list`.
The screenshot supplied by the owner shows repeated deadlines of 5/10 seconds
and exit code 1. Running the exact Cortex uvx command reproduced compilation of
cryptography50.0.2 for x86_64 on an aarch64 host. The compile failed because the
Intel Rust target and cross-compilation OpenSSL configuration were absent.
Installing the ARM uv tool alone did not change unqualified uvx selection.

## Local changes

The plugin dispatcher now validates the installed wheel version and launches its
isolated Python. Hooks, SessionEnd replay and MCP startup do not resolve packages.
Setup explicitly installs binary wheels with its native Python. The standalone
MCP configuration uses the same prepared Python; its previous pin was4.23.4
while installed hooks used4.23.5. Backend environment remains PostgreSQL.
Published runtime metadata now contains the Intel cryptography<49 bound already
documented by ADR-1092; no package, version or source changed in uv.lock.

## Verification

136 targeted tests pass (130 lifecycle/runtime tests, three documentation
contracts, two CI contracts and one telemetry regression): manifest contracts, runtime version checks, isolated
server/hook arguments, interruption recovery and concurrent queue deduplication,
plus published platform bounds. Ruff, craftsmanship and git diff --check pass.
Direct event probes return0: post_tool_capture1.148s, preemptive_context0.628s,
post_commit_reindex0.424s. Capture reports persistence pending; these exit codes
do not prove memory was committed. MCP initialization3.073s, tools/list59 tools;
query_methodology returns successfully through the restored local MCP server.
Recall exceeded both the initial 30-second diagnostic budget and a subsequent
20-second official-client budget while loading the embedding model; tool latency
remains unverified. These probes do not establish a recall regression. Owned
diagnostic servers were stopped.

Full installed SessionStart now returns0 in two consecutive probes:7.302s
and1.057s, emitting3128/3131bytes without printing memory content. Its connection
disables JIT locally, the grooming query uses the existing tags index, and the
banner checks distribution metadata without importing Torch/SciPy. See
[captured query evidence](codex-session-start-query-20261002.md). These are direct
manual probes, not native lifecycle delivery evidence. Cache/load conditions
differ; the measurements do not establish a guaranteed latency.

Native hooks/list after explicit owner approval:28 definitions, all28 trusted,
no parsing errors in Transartica, Cortex, Session Optimizer and Zetetic.
Native app-server notifications from two fresh threads confirm Cortex
SessionStart1.467/1.709s, UserPromptSubmit0.322/0.333s, three matching
PostToolUse handlers0.183–0.334s and SessionEnd0.102/0.107s all completed.
The command probe was a real model-issued printf, followed by thread/archive.
The two captures also found invalid SessionStart JSON from disk-hygiene and
statusline; those are separate plugin repairs and prevent claiming the whole
installation has zero hook errors. PreCompact, edit gates and subagent events
were not triggered in these cycles and remain unverified natively.

The first native trust-all attempt was rejected by automatic approval review.
The owner subsequently approved the exact11Hypermnesia hooks; approval was
applied through Codex /hooks, then verified across the four repositories.
No trust hashes were rewritten outside the native interface.

This report precedes PR publication. No release, database migration or deletion
of shared caches was performed.
Concurrent untracked work remains untouched. Old installed definitions and
config are preserved in ~/.codex/backups for review/recovery.
Loading
Loading