Skip to content

Codex extension interop - #2

Merged
moritzfl merged 5 commits into
mainfrom
codex-extension-interop
Jul 25, 2026
Merged

moritzfl merged 5 commits into
mainfrom
codex-extension-interop

Conversation

@moritzfl

@moritzfl moritzfl commented Jul 25, 2026 •

Copy link
Copy Markdown
Owner

Closes #1

What this adds

If you use both OpenCode and the Codex CLI on the same machine, this plugin can now share consolidated memory between the two — in either or both directions. Everything is off by default and enabled per direction:

{
  "plugin": [
    ["opencode-codex-memory", { "codex_interop": { "import": true, "export": true } }]
  ]
}
  • import (what [feature-request] : Is it possible to reuse the Codex memory? #1 asked for): Codex's consolidated MEMORY.md / memory_summary.md are copied into a memory extension (extensions/codex_import/) during each consolidation pass. The consolidator merges what is new into OpenCode's memory, tagging it [from codex].
  • export (the symmetric return path): after each successful consolidation, this plugin's consolidated memory is written into Codex's workspace as an extension (extensions/opencode_import/), together with an instructions.md for Codex's own consolidator. Codex picks it up on its next consolidation — no Codex-side configuration needed.
  • codex_home overrides where Codex lives (default: $CODEX_HOME, else ~/.codex).

How it works

Both memory systems already ship a generic extension contract: whenever extensions/ exists, the consolidation prompt instructs the agent to read every extension's instructions.md and interpret its resources. Codex itself uses exactly this mechanism to import Claude memories (external_agent_import). This PR mirrors that pattern in both directions: byte-equality change detection, per-file replace, deletion of copies as the "forgetting" signal in the workspace diff, and resource files placed so retention pruning never touches them.

To keep memories from ping-ponging between the systems, both instruction files enforce provenance tags ([from codex] / [from opencode]) and forbid re-importing content that carries the other side's tag. Foreign metadata (Codex thread UUIDs, OpenCode ses_* ids, citation blocks) is never reinterpreted.

Why not the originally discussed approach

The first design explored for #1 mounted Codex's memory directory as a second, read-only memory root. That made imported memory instantly readable, but it dragged complexity into the core of the memory implementation: every read tool needed to become source-aware, the injected summary needed a split token budget across two roots, search results needed virtual path prefixes, and the "memory is global, one root" invariant — which this port inherits from Codex — would have been strained permanently.

The extension approach keeps the core untouched: no read-path changes, no schema changes, no source-aware tools. Sharing is pure content that flows through the consolidation pipeline both systems already have. The trade-off is latency: imported knowledge becomes visible after the next consolidation pass rather than immediately. In exchange, the imported material is properly merged, deduplicated, routed, and pruned like any other memory instead of being a second bolted-on view.

Safety properties

  • Default-off; each direction is opt-in.
  • The export never bootstraps Codex's workspace (nothing is written until Codex's own memory feature has created $CODEX_HOME/memories), never modifies Codex-owned files, and never touches Codex's state database. Deletions cannot escape extensions/opencode_import/.
  • An unreachable Codex home is treated as "no-op", never as a deletion signal — a missing mount or unset env var cannot trigger forgetting.
  • memory_inspect now echoes the effective plugin options, configuration warnings (unknown/malformed keys), and the resolved interop state, so the setup can be verified in-band.

moritzfl added 5 commits July 25, 2026 06:39
#1)

New default-off codex_interop option ({ import, export, codex_home }):

- import: Codex's consolidated MEMORY.md/memory_summary.md are byte-compared
  and copied into extensions/codex_import/ inside the claimed phase-2 job
  (after baseline, before diff capture), so changes consolidate in the same
  run. Artifacts-gone drops the copies as a forgetting signal; an unreachable
  Codex home is a no-op, never a deletion signal.
- export: after a successful consolidation, validated artifacts are copied
  into $CODEX_HOME/memories/extensions/opencode_import/ with instructions
  for Codex's consolidator. Strictly additive: never bootstraps Codex's
  workspace, never touches its state DB.

Both instructions files carry provenance tags ([from codex]/[from opencode])
and forbid re-importing the other side's tag, so memories don't ping-pong.
Follows the extension mechanism Codex itself uses to import Claude memories
(external_agent_import); adaptation recorded in codex-map.yaml.
…rules

The overlap guard case-folded paths on darwin/win32 only. Actual case
sensitivity is per volume/directory (case-sensitive APFS, per-dir Windows
flags, casefold ext4), and Unicode normalization aliases paths besides case.

Compare existing paths by dev:ino instead: self-or-ancestor walk catches
case aliasing, NFC/NFD aliasing, symlinks, and bind mounts on any platform,
and walks over nonexistent tail components. When both roots exist and the
inode walk finds no relation, trust it over the lexical guess (a case-variant
path on case-sensitive APFS is a real, distinct directory). The case-folding
lexical comparison remains only as fallback for roots that do not exist yet.
The plugin never hard-fails on invalid options, and plugin console warnings
are effectively invisible in the TUI — so there was no reliable place to
verify the configuration took effect.

Move the options state into src/options.ts (leaf module, no import cycle)
with a config-warning registry; applyPluginOptions records unknown-key and
malformed-value warnings there. memory_inspect now echoes the effective
(post-parse, post-clamp) options, any recorded warnings, and the resolved
codex_interop state including whether the Codex memories root exists.
Document memory_inspect as the configuration check in the README.
Drift audit 6c00dc0..4c43465 (17 watched files, +1209/-656): all changes are
no-ops for the port.

- state/runtime/memories.rs: is_pinned column plumbing only; memory
  eligibility query passes is_pinned: None (no pin filter). Claim/lease/
  enqueue/watermark/cooldown semantics untouched.
- memories/write/runtime.rs: StartThreadOptions spawn refactor with
  equivalent defaults; ItemIds feature retired (always-on).
- ext/memories: step-store trait-param removal; injection/gating identical.
- features/config: Memories + ExternalAgentMemoryImport specs and
  MemoriesToml defaults/clamps unchanged.
- codex-mcp/mcp_tool_call: connection-set refactor; pollutes_memory still
  unconditionally true, marking still gated on disable_on_external_context.
- external-agent-migration memory importer: zero diff (memory_import.rs,
  memory.rs, detect/memory.rs); upstream additions are provider-attribution
  import-history bookkeeping in the state-DB layer the port deliberately
  lacks.
- thread pinning is memory-neutral (reconcile stamps no memory_mode).

No ports, no note changes needed.
@moritzfl
moritzfl force-pushed the codex-extension-interop branch from 740a80e to a38375f Compare July 25, 2026 05:39
@moritzfl
moritzfl marked this pull request as ready for review July 25, 2026 05:40
@moritzfl
moritzfl merged commit 1290726 into main Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature-request] : Is it possible to reuse the Codex memory?

1 participant