-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcodex-map.yaml
More file actions
739 lines (727 loc) · 45.8 KB
/
Copy pathcodex-map.yaml
File metadata and controls
739 lines (727 loc) · 45.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
# codex-map.yaml
#
# Provenance map: opencode-codex-memory is a TypeScript port of codex's Rust memory
# system. This file is the single source of truth for "which codex file does
# each of our files come from" and "which codex commit did we last audit".
#
# Update flow:
# 1. Point CODEX_REPO at a codex checkout.
# 2. Run scripts/check-codex-drift.sh
# 3. Port intentional upstream changes; record deliberate divergences in `note`.
# 4. Bump `codex_ref` / `codex_ref_date` to the commit you audited against.
#
# Design invariant (do not break without codex breaking it first):
# Memory is GLOBAL. Project/cwd separation exists ONLY as a soft routing hint
# inside the consolidation + read-path prompts, never as schema-level,
# read-path, or job-level partitioning. See consolidation.md mapping note.
# codex commit this map was last verified against.
codex_ref: ce7df362375772367bbba3deb9fb41632e96be1a
codex_ref_date: 2026-09-23
# Upstream directories/files to watch for ANY change since codex_ref.
# The drift script diffs these path prefixes between codex_ref..HEAD.
watch_paths:
- codex-rs/ext/memories
- codex-rs/memories
- codex-rs/state/src/runtime/memories.rs
- codex-rs/state/src/model/memories.rs
- codex-rs/state/memory_migrations
- codex-rs/protocol/src/memory_citation.rs
- codex-rs/core/src/memory_usage.rs
- codex-rs/core/src/stream_events_utils.rs
- codex-rs/git-utils/src/baseline.rs
- codex-rs/utils/output-truncation
- codex-rs/utils/home-dir/src/lib.rs
# Added after the 2026-07-04 parity audit — these are where past blind spots
# lived (config options, lifecycle triggers, pollution channels, dormant
# remote backend):
- codex-rs/config/src/types.rs
- codex-rs/core/src/session/session.rs
- codex-rs/core/src/mcp_tool_call.rs
- codex-rs/core/src/tools/registry.rs
- codex-rs/app-server/src/request_processors/turn_processor.rs
- codex-rs/codex-api/src/endpoint/memories.rs
# Added after the 2026-07-12 review audit — previously unwatched inputs the
# port depends on (redaction patterns, rollout persistence policy, feature
# stage, MCP pollution metadata):
- codex-rs/secrets/src/sanitizer.rs
- codex-rs/rollout/src/policy.rs
- codex-rs/features/src/lib.rs
- codex-rs/codex-mcp/src/server.rs
# Added after the 2026-07-21 audit — paginated-thread memory eligibility,
# captured MCP pollution metadata, and the optional external-memory importer
# also have inputs outside the core memory directories:
- codex-rs/rollout/src/metadata.rs
- codex-rs/rollout/src/state_db.rs
- codex-rs/thread-store/src/thread_metadata_sync.rs
- codex-rs/thread-store/src/local/update_thread_metadata.rs
- codex-rs/codex-mcp/src/binding.rs
- codex-rs/external-agent-migration/src/detect/memory.rs
- codex-rs/external-agent-migration/src/memory.rs
- codex-rs/external-agent-migration/src/memory_import.rs
- codex-rs/app-server/src/external_agent_migration/processor.rs
- codex-rs/app-server/src/external_agent_migration/protocol.rs
- codex-rs/app-server-protocol/src/protocol/v2/config.rs
# Added after the 2026-09-14 audit — v2 is opt-in; watch these so a
# default flip or dual_write-on-by-default cannot land unnoticed:
- codex-rs/protocol/src/memory_version.rs
- codex-rs/state/src/runtime/memory_versions.rs
- codex-rs/state/src/runtime/memory_readiness.rs
- codex-rs/app-server/src/request_processors/memory_status.rs
# Added after the 2026-09-23 audit — inputs the port depends on that sat
# outside the list (MCP-hook pollution, truncation, interactive sources,
# preferred memory models):
- codex-rs/core/src/hook_runtime.rs
- codex-rs/utils/string/src/truncate.rs
- codex-rs/rollout/src/lib.rs
- codex-rs/model-provider/src/provider.rs
# ours: path in this repo. theirs: path in the codex checkout.
# Every `theirs` is existence-checked by the drift script (catches renames like
# the read_path.md move from memories/read/ -> ext/memories/).
mappings:
# --- Read path (system-prompt injection + tools) ---
- ours: src/templates/read_path.md
theirs: codex-rs/ext/memories/templates/memories/read_path.md
note: >
Synced from codex_ref with platform adaptations (do NOT byte-sync):
citation block is <memory-citation>/<session_ids> with ses_* ids (codex:
<oai-mem-citation>/<rollout_ids> UUIDs) — citation.ts parses these tags;
quick-pass search + update-note guidance are {{ search_step }} /
{{ update_instructions }} placeholders filled by source.ts: with
dedicated_tools on they instruct the memory_search/memory_read/
memory_add_note tools incl. since/until (memory dir lives outside the
workspace), with dedicated_tools off they revert to codex's file-based
wording (agent reads/writes the FS directly, like codex);
rollout_summaries described as per-session .md recaps (codex: raw .jsonl
rollouts with session_meta/rollout_path).
Injects a SINGLE global memory_summary.md regardless of project.
- ours: src/templates/read_path_v2.md
theirs: codex-rs/ext/memories/templates/memories/read_path_v2.md
note: >
Adapted, not byte-copied. Citation tags are plugin-shaped
(<memory-citation>/<session_ids>/ses_*). search_step and
update_instructions placeholders match read_path.md; the codex
"explicit remember, forget, or correction request" trigger is kept in
front of update_instructions. Citation guidance is restructured into a
bullet list (codex: one paragraph).
No MEMORY.md hop. Recaps are per-session .md files. Opt-in; default v1.
- ours: src/source.ts
theirs: codex-rs/ext/memories/src/prompts.rs
note: >
builds the injected summary block; takes no cwd/project arg (codex takes
codex_home only). Extra dedicatedTools flag selects tool-based vs
codex-style file-based guidance snippets for read_path.md.
DELIBERATE EXTENSION (same as the memory tools' path guard): resolves
memory_summary.md component-by-component and refuses injection when the
root or summary file is a symlink, then reads through the same O_NOFOLLOW
file descriptor it verified to close the final-component race; codex
prompts.rs trusts the root and only rejects a symlinked root on clear.
V2 keeps the complete valid summary in one cache-stable system string
instead of Codex's 8900-byte contextual fragments (OpenCode has no such
per-fragment limit). Cache is keyed by summary path, not only mtime.
- ours: src/session-version.ts
theirs: codex-rs/ext/memories/src/extension.rs
note: >
Preserve one read version for injection, retrieval, notes and citation
accounting. DELIBERATE ADAPTATION: Codex freezes version in thread-extension
runtime state; OpenCode plugin reloads do not end conversations, so stamps
persist in memory_session_versions on the shared memory.db catalog.
Selection happens at first memory use/turn, not host session creation.
Status peeks never create a stamp. Reset preserves stamps and modes.
- ours: tools/memory.ts
theirs: codex-rs/ext/memories/src/tools/search.rs
note: >
memory_search/memory_read/memory_list/memory_add_note. codex splits these
across tools/{search,read,list,ad_hoc_note}.rs and local/*.rs.
search ports local/search.rs fully: queries[], match modes
(any / all_on_same_line / all_within_lines with minimal-window pruning),
path scoping (file or dir), integer-cursor pagination with
next_cursor/truncated, context_lines, normalized (alphanumeric-only)
comparison, case-sensitive default, 200-result cap, (path,line)
ordering, binary-file skip. DIVERGENCES: match_mode is flattened for
the zod schema (enum + separate line_count instead of codex's
internally-tagged {type, line_count}); max_results is the arg name for
codex's max_results (was `limit` pre-0.2); queries[] is optional
because the port adds since/until time filtering with a no-query
chronological listing mode (no upstream equivalent); that listing
paginates with the same integer cursor as query matches.
read uses line_offset/max_lines like codex plus a 256KiB byte backstop
(codex: 20k-token budget); read on a directory returns a listing (codex:
NotFile error) and skips hidden/symlink names like list/search.
add_note generates the filename plugin-side (codex: model
supplies it) with codex's hyphen layout and append-only collision
handling; port caps note<=4000/title<=120 chars and prepends a
title/created/session header (codex: verbatim unlimited note body).
list supports integer cursor pagination and returns a single entry for
file paths, matching codex. Listing/search walkers use Dirent
file types (codex read_sorted_dir_entries entry.file_type()) so
symlinks are skipped without following.
Note writes reject symlinked path components, matching codex local/
ad_hoc_note.rs ensure_notes_dir.
- ours: src/paths.ts
theirs: codex-rs/utils/home-dir/src/lib.rs
note: >
Upstream data-root discovery used by the local memory-root mapping below.
- ours: src/paths.ts
theirs: codex-rs/ext/memories/src/local.rs
note: >
memory root = <data root>/memories, mirroring local.rs from_codex_home
(codex_home.join("memories")). DELIBERATE ADAPTATION: codex's data root is
find_codex_home (CODEX_HOME env, else ~/.codex); the opencode analog is
opencode's own data dir, path.join(xdgData, "opencode") computed with the
SAME xdg-basedir lib opencode uses (packages/core/src/global.ts), so the
root tracks opencode across platforms and XDG_DATA_HOME. opencode does not
expose this dir through the plugin API (/path returns
home/config/state/worktree/directory, not data), hence the recompute.
OPENCODE_CODEX_MEMORY_TEST_ROOT is the CODEX_HOME analog: an outright
override used by tests and the write-pipeline sandbox. DELIBERATE
ADAPTATION: plugin option `home` and OPENCODE_CODEX_MEMORY_HOME relocate
only this plugin's files (memory.db + memories/) away from OpenCode's
data dir so a sandbox can mount memory without the host session
database. Codex has no equivalent; its data root already is CODEX_HOME.
Opt-in version=v2 uses memories_v2/ + memory_v2.db (Codex
version.directory_name). session_meta stays on memory.db (shared
catalog; Codex shares the host thread DB).
- ours: src/path-guard.ts
theirs: codex-rs/ext/memories/src/local/path.rs
note: >
safe path resolution that cannot escape the memory root: lexical
containment + per-component symlink rejection + dot-components hidden,
mirroring local.rs resolve_scoped_path. DELIBERATE EXTENSION: the root
itself is also rejected when symlinked (assertMemoryRootSafe, applied by
every memory tool; source.ts uses the full safeResolveMemoryPath for the
injected summary). Reads and overwrites open the final component with
O_NOFOLLOW, verify fstat against a post-open lstat, and truncate only
after verification, closing path-swap races in tools/workspace/baseline;
codex only enforces the root rule when clearing
(control.rs clear_memory_root_contents) and trusts the root elsewhere.
# --- Citations ---
- ours: src/citation.ts
theirs: codex-rs/memories/read/src/citations.rs
note: >
strip/parse <memory-citation><citation_entries>/<session_ids>. Also cross-check
codex-rs/protocol/src/memory_citation.rs for the wire schema.
Primary strip/record seam is opencode's experimental.text.complete hook
(fires before final part persistence), which matches codex's behavior of
stripping from the displayed/persisted message; the
messages.transform + message.part.updated paths remain as fallbacks for
older hosts and pre-existing history (codex keeps citations in raw
history — the port's persisted history is stripped instead, a remaining
divergence). Port requires a closing tag
(codex's stream parser auto-closes at EOF); port drops codex's legacy
<thread_ids> alias and instead accepts its own legacy comma-separated id
list. OpenCode 2 renders citations as a fenced ```memory-citation block
(entries + `sessions:` line) instead of XML: src/v2/citation-overlay.ts
swaps the read-path section, and the parser accepts both forms.
# --- Storage / DB ---
- ours: src/db.ts
theirs: codex-rs/state/memory_migrations/0001_memories.sql
note: >
SQLite schema (stage1 outputs, jobs, session meta). v2 jobs/outputs open
memory_v2.db; session_meta always migrates on memory.db.
Both DB handles remain open concurrently. Migration 3 adds per-store
consolidation_progress and the shared session-version routing table.
- ours: src/memory-version.ts
theirs: codex-rs/memories/write/src/start.rs
note: >
Codex clones config per writer. Node AsyncLocalStorage provides immutable
version routing through async calls instead of changing global options;
dual_write selects both V1 and V2 regardless of the selected read default.
- ours: src/pipeline.ts
theirs: codex-rs/memories/write/src/start.rs
note: >
Shared OpenCode 1/2 orchestration runs phase 1 then phase 2 independently
per write version. Failures do not reject the sibling writer. Local
guards, throttles and abort scopes are keyed by versioned root/DB path.
Host startup/idle triggers and provider quota adaptations remain as
documented on index.ts and ratelimit.ts.
- ours: src/migration.ts
theirs: codex-rs/app-server/src/request_processors/memory_status.rs
note: >
Read-only readiness: valid V2 summary AND maximum distinct sessions in one
successful consolidation >= threshold (default 20, allowed 1..4096).
Exposed via memory_inspect and OpenCode 2 status RPC/UI. No automatic
cutover. Missing V2 DB reports zero without creating an inactive store;
summary reads additionally reject symlinks via the plugin path guard.
- ours: src/store.ts
theirs: codex-rs/state/src/runtime/memories.rs
note: >
claim/lease/retry job logic, phase2 input selection, usage recording,
pruning. The behavioral heart of the port; audit carefully on any change.
DIVERGENCES: stage-1 claim gates on the jobs watermark only (codex also
probes stage1_outputs.source_updated_at — diverges only in exotic DB
states); phase-2 success watermark = max(selected source_updated_at)
(codex additionally folds in the claim-time input_watermark;
informational column, nothing reads it); phase-2 selection pages ranked
candidates and probes session.get until maxRaw live rows are collected
(404 → deleteSessionMemory, same as session.deleted; timeout/error keeps
the row). Codex performs the equivalent live-thread check while paging.
Host adaptation (no live quota API): quota/rate-limit stage-1 failures
keep status=pending and do not decrement retry_remaining; exhausted
quota-failed rows are requeued without a newer session watermark. Codex
instead skips claiming via guard.rs before any job is started.
Opt-in version=v2: jobs/outputs on memory_v2.db via openDb();
session_meta on memory.db via openSessionMetaDb() (shared catalog).
Shared mode/pollution checks run before counting ranked rows toward the
selection limit. Success atomically advances consolidation_progress using
a distinct-session high-water count; stale ownership cannot advance it.
Pruning preserves progress; reset clears it and citation dedupe in both
stores without closing live handles. Session deletion covers both stores.
Port-only orphan sweep (no codex equivalent; codex waits out the lease):
a running phase-2 row tagged `pid:<pid>@<host>:` is released when the
pid is dead on this host AND the heartbeat is stale (3 missed beats);
runs at boot and before each phase-2 claim.
# --- Write path: phase 1 (extraction) ---
- ours: src/phase1.ts
theirs: codex-rs/memories/write/src/phase1.rs
note: >
transcript build + eligibility + stage1 extraction orchestration.
Transcript budget 600k chars, 50/50 head+tail split ~ codex's 150k-token
fallback (codex uses 70% of the model context window when known).
Reasoning parts are excluded like codex (rollout policy Reasoning=false).
DIVERGENCES: a first-time empty transcript is treated as
success-with-no-output (codex fails when a rollout cannot be loaded)
because empty opencode sessions are normal; if a prior extraction exists,
empty fails/retries so a transient empty API response cannot erase it.
Partial-empty stage-1 fields
(empty raw_memory OR empty rollout_summary) match codex → no-output.
Citation blocks are stripped from the transcript before extraction
(codex rollouts retain them; keeps the extractor from mining our own
citation markup). After an observed quota/rate-limit error, remaining
claimed jobs in the same pass are finalized without calling the model
(Codex guard.rs skips the whole startup instead).
Opt-in version=v2: summary+slug schema, empty raw_memory, 9k-byte
summary truncate after redact, tiered input via rollout-input.ts.
- ours: src/capture.ts
theirs: codex-rs/memories/write/src/phase1.rs
note: >
transcript/session loading. In codex this lives inside phase1.rs (reads
rollout .jsonl files directly); the port uses opencode's official API
exclusively — session.messages for transcripts (session-scoped routing
covers cross-project sessions; errors propagate so the job retries
instead of finalizing a false no-output) and GET /experimental/session
(Session.listGlobal, roots=true) for global discovery via the host
client's hey-api transport (V1 SDK has no experimental namespace;
fail-safe: failures skip, never finalize). opencode.db is never read;
the only SQLite is the plugin's own memory.db (D5).
Codex now extracts from legacy and paginated threads alike; the port was
already history-mode agnostic because these APIs expose the session
transcript independently of Codex's rollout storage mode.
Compaction summaries (assistant rows with summary=true) are dropped like
codex drops Compacted items (rollout policy + phase1.rs); OpenCode 2
compaction rows carry no text and add nothing. Exception: without a
registered service, OpenCode 2 transcripts come from session.context,
which starts at the latest compaction (the originals are not
available), so its summary is kept as the only record of them.
See D4 in ARCHITECTURE.md.
- ours: src/rollout-input.ts
theirs: codex-rs/memories/write/src/rollout_input.rs
note: >
V2 evidence ranking over OpenCode TranscriptMessage parts, not Codex
RolloutItem. Human/final/other-agent/context/tool tiers, newest first per
tier, restored source order with omission markers. OpenCode's native
question tool already pairs input/output; answered replies are human
evidence with the question retained. Host transcripts expose neither
Codex MessagePhase::Commentary nor internal content-item provenance;
unmarked assistant text stays final. UTF-8 byte bounds and head/tail
truncation follow Codex, after redaction/citation stripping. V1 unchanged.
Extraction is one inline host prompt rather than 8900-byte contextual
fragments; OpenCode does not impose Codex's per-fragment limit.
- ours: src/templates/stage_one_system.md
theirs: codex-rs/memories/write/templates/memories/stage_one_system.md
note: >
Byte-synced from codex_ref with title only retitled "Single Session"
(opencode UX); keeps codex "rollout" domain language for artifacts.
- ours: src/templates/stage_one_system_v2.md
theirs: codex-rs/memories/write/templates/memories/stage_one_system_v2.md
note: >
Adapted: [REDACTED:secret]; session text not rollout jsonl in the
evidence warning. JSON contract is summary+slug only. Opt-in; default v1.
- ours: src/templates/stage_one_input.md
theirs: codex-rs/memories/write/templates/memories/stage_one_input.md
note: >
Platform-shaped: session_id/session_cwd/transcript placeholders instead of
codex rollout_path/rollout_cwd/rollout_contents (opencode has no rollout file).
- ours: src/templates/stage_one_input_v2.md
theirs: codex-rs/memories/write/templates/memories/stage_one_input_v2.md
note: >
Platform-shaped: session_id/session_cwd/session_git_branch/transcript
instead of rollout_path/cwd/git_branch/contents. Opt-in; default v1.
# --- Write path: phase 2 (consolidation) ---
- ours: src/phase2.ts
theirs: codex-rs/memories/write/src/phase2.rs
note: >
consolidation orchestration + heartbeat/lease. Ports codex #32193
validate_consolidation_artifacts gating: early no-diff succeed only when
MEMORY.md exists and memory_summary.md starts with `v1`; post-agent
invalid artifacts fail the job without baseline reset. Like codex, the
consolidator edits the LIVE artifacts (no staging/promote step): a
failed or timed-out run can leave a partial summary until the retry
repairs it — reviewed 2026-07-12 and kept as-is for parity; do not add
staging unless codex does.
The opencode session.prompt transport can return HTTP 200 while the
assistant message carries an error; llm.ts treats that as failed_agent
so phase2 preserves the workspace diff, matching codex's requirement
that AgentStatus be Completed before validation/baseline reset.
Observed-quota stamp (src/ratelimit.ts) skips claiming phase 2 the same
way Codex start.rs skips both phases when guard.rs fails.
DIVERGENCE (codex_ref 1bbdb32, 2026-07-15 audit): codex now threads a
parent_permission_profile into phase2::run/agent::get_config so the
consolidation agent inherits the parent session's OS sandbox choice
(Disabled->Disabled, External->External, Managed->the old
WorkspaceWrite(memory root, no network)). The port has no equivalent:
the consolidator's sandbox is the opencode `memorize` agent allowlist
(D2), not a codex SandboxPolicy, and opencode plugins get no parent
permission profile. No behavior to port; consolidateViaSubagent stays.
DIVERGENCE (codex_ref ba42e68, 2026-07-30 audit): codex holds the lease
when the consolidation agent's shutdown fails, where "failed" is a thread
handle that did not terminate. The port has no thread handle: a failed
shutdown is a failed or timed-out session.delete call, deliberately NOT
an unconfirmed 404 — sessionDeletionConfirmed returns false on hosts without session.get
and would otherwise stall every job for a full lease.
Opt-in version=v2: skip raw_memories rebuild; validate summary headings
and <10k bytes, no MEMORY.md.
DELIBERATE DIVERGENCE: post-success storage-byte OTEL histogram added
upstream is not emitted (no plugin telemetry backend). OpenCode helper
permissions restrict each job to its own memory root even in dual mode;
V1 uses session external_directory rules, V2 uses session deny-first rules.
- ours: src/workspace.ts
theirs: codex-rs/memories/write/src/storage.rs
note: >
rebuild raw_memories, write rollout summaries, prune extension resources,
validate_consolidation_artifacts (also codex workspace.rs).
DIVERGENCES: raw_memories.md entries are truncated at 10k chars each
(codex: unbounded; guards the consolidation prompt against giant
extractions); rollout summary headers add usage_count and drop
rollout_path/git_branch (metadata the port does not have); ensureLayout
pre-seeds placeholder MEMORY.md + empty memory_summary.md (codex: the
consolidator INITs them; outcome-equivalent, first diff shows M not A).
Claude project memory import is no longer a divergence: see
src/claude-import.ts. Codex-CLI consolidated-memory exchange remains in
src/codex-interop.ts. rollout_summaries/ file stems use a 4-hex sha1 of
the session id and a collapsed/trimmed slug (codex: base62 x31 hash,
every non-alnum char to "_"); only names differ, content is the same.
raw_memories.md and selection sort by session id in byte order, like
codex sort_by_key(thread_id).
- ours: src/claude-import.ts
theirs: codex-rs/external-agent-migration/src/memory_import.rs
note: >
Port of codex's Claude (external-agent) memory import into
extensions/external_agent_import/. Also tracks memory.rs for discovery.
Discover ~/.claude/projects/<key>/memory markdown (skip symlinks), resolve
project cwd from newest session *.jsonl with an absolute existing cwd
(project_cwd_from_sessions), whole-project replace with scope.json +
EXTENSION_INSTRUCTIONS (byte-equality change detection, source-gone
removes resources). Discovery I/O errors propagate before pruning;
unavailable cwd only removes unscoped targets (a regular scope.json
preserves existing resources), matching project_has_unscoped_target.
DIVERGENCES: no migration UI / selected_memory list —
continuous phase-2 sync when claude_import.enabled (optional projects
allowlist; omit = all with cwd); unreachable Claude home is a quiet no-op
(not fatal); no state-DB enqueue (sync runs inside the claimed phase-2 job
after baseline, before diff, same ordering as codex-interop import);
project keys rejected if path-unsafe or hidden (path-guard); instructions
add optional [from claude] provenance tag. Default-off via claude_import
plugin option (codex: migration feature / include_memory detect gate).
- ours: src/codex-interop.ts
theirs: codex-rs/external-agent-migration/src/memory_import.rs
note: >
DELIBERATE ADAPTATION, not a 1:1 port: reuses codex's external-agent
memory-import pattern (extensions/<name>/instructions.md + resources/,
byte-equality change detection, instructions refreshed on constant
change, source-gone removes resources) but with per-file in-place
replace instead of codex's whole-dir delete+rewrite (fixed 2-artifact
set; non-regular files at target paths are replaced, never written
through), and the source is Codex's own GLOBAL consolidated memory
(MEMORY.md + memory_summary.md), not per-project Claude memories — so
no scope.json, no project selection surface, no state-DB gating.
Claude per-project import lives in src/claude-import.ts (same extension
name/layout as upstream). An unreachable codex home is NOT a deletion
signal (only an existing memories root with missing artifacts is),
diverging from codex's fatal-on-missing-home because the plugin must
degrade quietly. Codex syncs out-of-band and enqueues consolidation; the
port syncs inside the claimed phase-2 job (after baseline, before diff
capture) because its claim is time-gated and pre-claim workspace writes
could race a running consolidator's lease. Also adds an export direction
with no codex equivalent: our consolidated artifacts are written into
$CODEX_HOME/memories/extensions/opencode_import/ for codex's consolidator
to merge via its generic extensions contract (additive only; codex's
workspace is never bootstrapped and its state DB never touched).
Instructions constants adapt codex's EXTENSION_INSTRUCTIONS with
cross-system provenance tags ([from codex]/[from opencode]) as an echo
guard — needed because this exchange is two-way while codex's Claude
import is one-way and cannot echo. The guard is deliberately
instruction-level, matching codex's own all-instruction content rules
(see ARCHITECTURE.md D6). Default-off via the opencode-specific
codex_interop plugin option.
Handbook exchange remains V1-only; the V1 writer can still exchange while
dual_write is on and the read default is V2. No automatic V1-to-V2 import.
- ours: src/git-baseline.ts
theirs: codex-rs/git-utils/src/baseline.rs
note: >
git baseline diffing for phase 2. Port uses isomorphic-git instead of
libgit2; also see codex-rs/memories/write/src/workspace.rs for the diff
wiring. captureWorkspaceDiff throws on failure (ports codex
failed_workspace_status — an errored diff must not look like "no
changes"); ensureBaseline self-heals corrupt git metadata by destructive
re-init (ports reset_git_repository_sync-on-unusable).
DIVERGENCES: patches use the `diff` package's createPatch headers
(Index:/---/+++) instead of codex's git-style `diff --git` rendering; no
file-mode/symlink diff rendering; workdir content reads fail closed on
symlinks instead of including their targets in the generated diff;
hidden paths (dot components, e.g. Finder `.DS_Store`) are omitted from
staging and workspace diffs because path-guard reports them as not found
— including them failed phase 2. Codex's walker includes them and reads
bytes directly. resetBaseline keeps `extensions/` files added/edited
after the diff capture out of the new baseline (codex resets the whole
tree, so a note written during the consolidator turn is never
consolidated). Recovery on unusable metadata stays codex-shaped: any
open/HEAD failure re-inits, as ensure_git_baseline_repository does.
- ours: src/templates/consolidation.md
theirs: codex-rs/memories/write/templates/memories/consolidation.md
note: >
Synced from codex_ref with platform adaptations (do NOT byte-sync):
rollout annotations demand cwd/session_id/updated_at (codex:
cwd/rollout_path/thread_id — metadata our artifacts do not have);
thread-id terminology replaced with session ids throughout; shell-command
examples replaced with file-tool equivalents (codex's consolidator has a
sandboxed shell — wc -l / rg --files; our memorize agent has
read/edit/write/glob/grep only); the optional housekeeping step tells the
agent it cannot delete files (summary pruning is plugin-side keep-set
logic in workspace.ts, codex expects agent-driven deletion); secret
replacement token is [REDACTED:secret] (redact.ts style, codex:
[REDACTED_SECRET]); "managed by Codex" branding rewritten to the plugin.
{{ memory_extensions_* }} placeholders are rendered by
buildConsolidationPrompt in src/llm.ts, mirroring codex prompts.rs +
lib.rs prompt_blocks. INVARIANT: project/cwd scoping lives ONLY here,
as an LLM instruction to "organize the index first by cwd / project scope,
then by topic". Do not add hard scoping elsewhere unless codex does.
- ours: src/templates/consolidation_v2.md
theirs: codex-rs/memories/write/templates/memories/consolidation_v2.md
note: >
Adapted: session_id not thread_id; [REDACTED:secret]; file tools only
(no shell/delete); same extension placeholders as consolidation.md.
Summary-only (no MEMORY.md). Opt-in; default v1.
# --- Control / lifecycle ---
- ours: tools/control.ts
theirs: codex-rs/memories/write/src/control.rs
note: >
memory_reset/inspect/mode; clearMemoryRootsContents equivalent.
DELIBERATE DIVERGENCE: these are model-invocable tools
(codex exposes reset/mode only as client-side RPCs / CLI, never to the
model) — "reset my memory" in plain language is core plugin UX. Guard
rails: memory_reset and memory_mode polluted→enabled request user
approval via ctx.ask (the model-chosen `confirm` is not approval).
OpenCode 2 plugin tools cannot request approval, so V2 omits the
memory_reset tool and offers the wipe as a confirmed panel RPC
(resetMemory, closer to codex's client-side reset); V2 memory_mode
fails closed when lifting pollution. memory_mode additionally accepts
"polluted" (codex protocol: enabled|disabled, polluted is set only
automatically); memory_inspect is a port addition (read-only status).
memory_reset wipes memories/ and memories_v2/ plus both job DBs,
preserving memory_session_meta (Codex clears both version roots). Reset
leaves a phase-2 "done" row as a cooldown marker (codex writes none) so
the next hook cannot first-run-claim phase 2 and re-seed the root.
- ours: src/index.ts
theirs: codex-rs/core/src/stream_events_utils.rs
note: >
citation extraction from streamed assistant text + turn-start/idle
pipeline triggering.
Loosest mapping; the plugin hook wiring is opencode-specific.
DELIBERATE DIVERGENCES vs codex lifecycle: (1) codex kicks the write
pipeline at turn start (app-server turn_processor.rs -> start.rs); the
port pumps at the first chat.message of a session (same turn-start
semantics) AND on idle events — an extra trigger codex does not have.
(2) codex ships memories default-off behind Feature::MemoryTool with a
TUI consent prompt (the feature is Stable now, still default-off); the
port is on-by-default — installing the plugin is the consent (README
states this). memory_mode stamping happens at the first chat.message of
a session (closest plugin-visible moment to codex's
stamp-at-thread-creation, session.rs), with the idle path as fallback;
the same hook pumps phase 1 so extraction no longer depends on idle
events; pollution marking happens in tool.execute.before — at INVOCATION,
matching codex, which marks inside handle_approved_mcp_tool_call before
the call runs (mcp_tool_call.rs maybe_mark_thread_memory_mode_polluted)
and marks web search on the completed response item
(stream_events_utils.rs response_item_may_include_external_context:
WebSearchCall/ToolSearchCall/ToolSearchOutput). tool.execute.after is
deliberately NOT used: opencode does not guarantee it (session/tools.ts
awaits execute() with no ensuring/catchAll; an abort interrupts the
fiber), so a failed or cancelled call left the session unmarked.
DIVERGENCE: marking at invocation also marks permission-denied calls,
which codex does not — the safe direction for an opt-in guard, since the
alternative under-marks. Classification covers websearch/webfetch plus
every live MCP server via client.mcp.status(); server names use
opencode's own tool-prefix sanitization (codex marks all MCP servers
pollutes_memory=true). The live status request is aborted after 1s so a
stalled local endpoint cannot block tool execution. codex has two more
channels with no port:
registry.rs marks on output.contains_external_context() after a
successful builtin tool — opencode exposes no equivalent output flag;
hook_runtime.rs marks on MCP-tool hooks ("mcp_tool_hook") — opencode
has no MCP hook handlers. OpenCode2 codemode `fetch()` is a sandbox
extension, not a tool, so it never reaches tool.execute.before — an
accepted gap (no `fetch(` code heuristics; see ARCHITECTURE known gaps).
Pollution re-queues phase 2 when a consumed
output is affected (store.markPolluted, codex
mark_thread_memory_mode_polluted). (3) the memorize / memorize-extract
sub-agents self-register through the plugin config hook (user-defined
agents of the same name win) — pure opencode packaging; codex ships its
memory agents inside the binary; injection appends an external_directory
allow for <memory root>/* to memorize because opencode gates file tools
outside the session's project and the global memory workspace is always
external (no codex equivalent — Seatbelt sandboxing is process-level);
the memorize prompt adds a
10k-char/2500-token summary-size instruction codex's consolidator does
not get (both sides also truncate at 2500 tokens on read).
(4) generate_memories=false turns off the whole write pipeline, agent
injection, and delete-triggered consolidation (README documents it as
the pipeline switch); codex only stamps NEW threads memory_mode=disabled
and keeps the pipeline running for previously-enabled ones.
# --- Prompt building ---
- ours: src/llm.ts
theirs: codex-rs/memories/write/src/prompts.rs
note: >
buildConsolidationPrompt ports build_consolidation_prompt including the
lib.rs prompt_blocks extension blocks (rendered only when
<memory_root>/extensions exists). The rest of llm.ts (sub-agent session
plumbing) is opencode-specific — see D3 in ARCHITECTURE.md.
D3-adjacent DIVERGENCES: stage-1 requests opencode's json_schema output
format (EXTRACTION_SCHEMA in src/llm.ts) — the platform equivalent of
codex's output_schema + output_schema_strict — and reads the result from
AssistantMessage.structured; validateExtraction rejects prompt-skeleton
echoes and treats empty fields as no-op. opencode enforces json_schema via
a forced StructuredOutput tool call (toolChoice: required), so the
otherwise tool-less memorize-extract agent allows that one
side-effect-free tool (opencode.json; tests/agents.test.ts enforces the
exception). Model config lookup is timeout-bounded and only successful
responses are cached, so transient host failures cannot pin missing model
config until reload. parseExtraction (brace-scrape, unknown-field tolerant)
stays as a fallback for hosts/models that don't populate structured. Per-request
timeouts stay inside the 1h job leases (codex relies on the lease alone):
consolidation mirrors it; extraction stops 10 min early because its
stage-1 lease has no heartbeat and pre-prompt work also runs on it;
timed-out or ownership-cancelled prompts call the bound session.abort SDK
method with a 1s bound before session deletion; deletion is bounded to
codex's 10s shutdown timeout. Cleanup is awaited for consolidation because the phase-2 lease
depends on it (see the src/phase2.ts note), fire-and-forget for
extraction, which has no codex agent-shutdown counterpart and no write
tools. Dispose: extractViaSubagent defaults to pluginShutdownSignal();
consolidateViaSubagent takes the phase-2 AbortSignal (heartbeat can cancel
consolidator without full plugin shutdown). Both also session.abort for
host cleanup. plugin-created sessions carry a
durable metadata ownership marker, and startup cleanup requires both that
marker and an exact generated title because OpenCode copies metadata to
user forks. Exact legacy titles are temporarily reseeded but never
deleted; plugin startup awaits a bounded, title-filtered, cursor-paginated
host-wide list/reseed pass while stale deletions continue through a
concurrency- and time-bounded background batch and retain ownership
unless a follow-up session.get confirms 404 (OpenCode may return success
after an internally failed remove);
AssistantMessage.error is treated as a failed sub-agent turn even though
opencode returns it with HTTP 200;
reasoning effort is pinned via PromptInput.variant low/medium (codex:
Low extract / Medium consolidate). SDK types omit variant (same lag as
format). DELIBERATE DIVERGENCE: Codex only talks to Codex models and
hardcodes those efforts. This plugin looks up the model's OpenCode
variants and, if the pin is missing, selects the nearest listed effort
on none < minimal < low < medium < high < xhigh < max (ties → higher).
Empty variant lists omit the pin (host default). Catalog lookup failure
or a missing variants field keeps the Codex pin (unknown variant is a
host no-op). Custom picker names such as "fast xhigh" are ignored.
Opt-in version=v2 selects consolidation_v2.md / stage_one_*_v2.md.
# --- Utilities ---
- ours: src/redact.ts
theirs: codex-rs/secrets/src/sanitizer.rs
note: >
secret redaction for stage-1 outputs (codex calls redact_secrets on
raw_memory/rollout_summary/slug and on the serialized rollout; the port
redacts the transcript lines and the extraction outputs).
DELIBERATE SUPERSET: codex ships 4 patterns (openai key, AKIA, bearer,
bare-key assignment); the port adds anthropic/github/slack/private-key
patterns, uses labeled [REDACTED:*] tokens vs codex's [REDACTED_SECRET],
allows quotes around the assignment KEY so JSON/YAML forms
like "password": "..." are caught — codex's SECRET_ASSIGNMENT_REGEX
misses those. An escape-aware value scanner preserves keys, separators,
comments, JSON trailing delimiters, and nested object/array boundaries;
unquoted values become quoted [REDACTED] strings, so redacting JSON/YAML
payloads remains parseable without leaking punctuation-bearing suffixes;
assignment-like content inside an enclosing quoted string is replaced
without adding another quote layer. A value on the line after the
separator follows codex's `\s*` separator + `[^\s"']{8,}` value rule
(short YAML block keys stay intact, as in codex).
Not beyond codex (user decision 2026-09-23): sk-proj-/github_pat_/AIza/
JWT/URL-userinfo formats pass through, as they do upstream.
Bearer matches codex (word-boundary, space/tab only, 16+ body, trailing
=*, applied before key patterns). AKIA still omits codex's \b anchor.
isMemoryExcludedFragment lives here too (upstream: memories/write
phase1.rs is_memory_excluded_contextual_user_fragment) but is INERT on
opencode: AGENTS.md is joined into system[0] and skills are rendered as
an <available_skills> system catalog (skill/index.ts fmt), so neither
ever appears as a user text part. Kept for parity; the structural
`ignored`-part filter in capture.ts is what actually excludes
non-conversation content here.
- ours: src/token.ts
theirs: codex-rs/utils/output-truncation
note: token-limit truncation for the 2500-token summary cap (chars/4 estimate here).
- ours: src/ratelimit.ts
theirs: codex-rs/memories/write/src/guard.rs
note: >
DELIBERATE DIVERGENCE: codex gates on real provider rate-limit windows
(guard.rs, min_rate_limit_remaining_percent, fails open; called from
start.rs once per startup — a pure read with no process-local stamp).
Opencode does not expose provider rate limits, so the port keeps a phase-1
process-local anti-stampede (30s) that stamps only AFTER a stage-1 claim
succeeds; empty/no-claim passes do not burn the interval (matches codex
skipped_no_candidates free retry). After an observed quota/rate-limit
error, a 1h process stamp skips further claims for the same configured
model (or the same phase when no model is configured). Codex start.rs skips
both phases when its account-level live guard fails; model scoping avoids
blocking independently configured opencode providers. Phase 2 still has
no 30s timer — DB claim/cooldown only, like codex. Wire
min_rate_limit_remaining_percent when the host exposes provider quota.
Classification: a 429 or quota/rate-limit wording counts as capacity,
except single-request size errors ("request too large", context length),
which can never succeed on retry and consume retries normally.
# Platform-only modules (no codex theirs path — do not map with theirs: null,
# the drift script existence-checks every theirs value):
# src/lifecycle.ts — dispose/reload abort for consolidator + sub-sessions
# src/host-client.ts — OpenAPI type-lag cast adapters
# src/reasoning-variant.ts — nearest OpenCode effort when Codex low/medium is absent
# src/diagnostics.ts — inspect ring buffer (codex has OTEL metrics instead)
# --- Extensions (ad-hoc notes) ---
- ours: src/workspace.ts
theirs: codex-rs/memories/write/src/extensions
note: >
ad-hoc notes extension instructions + retention pruning. codex keeps this
in a dedicated extensions/ module; the port folds it into workspace.ts.
# --- Config options ---
- ours: src/index.ts
theirs: codex-rs/config/src/types.rs
note: >
Plugin options mirror codex MemoriesToml/MemoriesConfig 1:1 by NAME so
syncing stays trivial. Supported: generate_memories, use_memories,
dedicated_tools, disable_on_external_context, max_raw_memories_for_consolidation,
max_unused_days, max_rollout_age_days, max_rollouts_per_startup,
min_rollout_idle_hours, extract_model, consolidation_model. Defaults match
codex (max_rollout_age_days=10, min_rollout_idle_hours=6,
max_rollouts_per_startup=2, max_raw_memories_for_consolidation=256).
Also supported: version (default v1), dual_write (default false).
Numeric knobs are clamped to codex's ranges (types.rs From<MemoriesToml>):
(1,4096)/(0,365)/(0,90)/(1,128)/(1,48). Unknown option keys warn (codex
deny_unknown_fields errors). use_memories=false hides the memory tools
like codex's extension gating.
DIVERGENCES: (1) dedicated_tools defaults to true here (codex: false) so the
standalone plugin ships its memory tools out of the box; (2) codex's
min_rate_limit_remaining_percent is not implemented — see src/ratelimit.ts
stub; (3) unset extract_model/consolidation_model fall back to opencode's
configured small_model/model respectively (mirroring codex's
gpt-5.6-luna/gpt-5.6-terra cheap-vs-capable split), then to the session default
— opencode's automatic small-model pick (provider.getSmallModel) is not
exposed to plugins, only the explicit config keys are;
(4) the legacy alias no_memories_if_mcp_or_web_search is not accepted
(fresh plugin, no legacy configs); (5) wrong-TYPED values on known keys
warn via memory_inspect and fall back to the default (codex: serde
hard-fails the whole config parse — a plugin must not break the host on
bad options); (6) options.test (live harness only) skips Codex numeric clamps
so e2e can set min_rollout_idle_hours=0.01. Production stays integer 1–48
like MemoriesToml. version defaults to v1; dual_write defaults to false.