상태·메모리 영속성 — 횡단분석
상태·메모리 영속성 — 횡단분석
한 줄 요약
7개 하네스는 “모델은 무상태(stateless)다”라는 같은 출발점에서 세 가지 다른 진실원본(SSOT) 으로 갈라진다 — 마크다운 파일, append-only 이벤트 로그, 세션×경로 JSON 원장. 왜 배우나 — 내 스택(Claude+Codex+OMC)에서 “기억을 어디에 어떻게 적고 어떻게 깨어날지”를 설계하려면, 먼저 7개 하네스가 같은 문제를 어떻게 다르게 풀었는지를 알아야 차용할 강점을 고를 수 있다.
그림
flowchart TD
Q["모델은 세션이 닫히면<br/>전부 잊는다 (무상태)"] --> D{"기억을 어디에<br/>적을까?"}
D -->|"사람이 읽고<br/>모델이 주입받는 지식"| MD["마크다운 파일<br/>Claude Code · OMC · 내 패턴"]
D -->|"재생으로 상태를<br/>재구성하는 사건열"| LOG["append-only 이벤트 로그<br/>Codex rollout · ouroboros events"]
D -->|"게이트가 판정에 쓰는<br/>구조화된 사실"| JSON["세션×경로 JSON 원장<br/>fable-ish · gajae"]
MD --> USE{"이 기억을<br/>어디에 쓰나?"}
LOG --> USE
JSON --> USE
USE -->|"모델 프롬프트에 넣어<br/>행동을 안내"| INJ["주입형<br/>(컨텍스트, 강제 아님)"]
USE -->|"훅·게이트가 읽어<br/>판정·차단"| CTL["제어형<br/>(강제력이 SSOT에)"]
쉽게 풀기
문제부터. AI 모델은 대화창(세션)이 닫히면 방금 한 일을 깡그리 잊는다. 그래서 모든 하네스의 첫 문장은 똑같다 — “세션이 닫히면 잊는다.” 결론도 똑같다 — 디스크가 곧 기억이다. 컴포넌트의 본질은 “닫히기 전에 적고, 깨어날 때 다시 읽힌다”이다.
그런데 ‘어디에 적느냐’에서 길이 갈린다. 비유로 보자.
- 마크다운 파일 = 책상 위 메모지. 사람이 읽고 손으로 고칠 수 있고, 모델에게 그대로 보여주기 좋다. 대신 “3월에 뭐 했지?” 같은 검색·정렬은 약하다. (Claude Code, OMC, 내 패턴)
- 이벤트 로그 = 가계부. “무슨 일이 일어났다”를 시간순으로 계속 적기만 한다. 지금 잔액이 궁금하면 처음부터 다시 더하면(재생, replay) 된다. 틀려도 지우지 않고 “정정” 항목을 새로 적는다. 대신 현재 상태를 알려면 매번 계산해야 한다. (Codex rollout, ouroboros)
- JSON 원장 = 체크리스트. “검증 완료? Y/N” 같은 칸이 정해진 양식. 빠르게 판정하기 좋지만 긴 이야기를 담기엔 부적합하다. (fable-ish, gajae)
두 번째 갈림길은 ‘누구를 위한 기억이냐’다.
- 주입형(보여주는 기억) — 메모지를 모델 눈앞에 펼쳐 “이런 규칙이 있어”라고 안내한다. 단, 강제가 아니라 참고용이다.
- 제어형(판정하는 기억) — 모델에게 직접 안 보여준다. 대신 문지기(훅·게이트)가 그 기억을 읽고 “검증 안 했네? 종료 막아!”라고 제어한다. 강제력이 여기에 있다.
이 두 축(“어디에 적나” × “누구를 위해 쓰나”)을 잡으면 7개 하네스의 설계가 한눈에 정리된다.
핵심 정리
7개 하네스를 “저장 형식 / 핵심 트리거 / 주입 방식”으로 슬림하게 본다. 세부 스키마와 특이점은 아래 콜아웃으로 분산한다.
| 하네스 | 저장 형식(SSOT) | AI 주입 방식 |
|---|---|---|
| Claude Code | CLAUDE.md 계층 + 자동메모리 MD + 세션 JSONL | 시스템 프롬프트 뒤 user 메시지로 주입(강제 아님) |
| Codex | rollout JSONL(원본) + state SQLite + Memories MD | resume 시 이력 재구성 / Memories 파일읽기 |
| OMC | .omc/ 4계층: state·notepad·project-memory·wiki | project-memory를 SessionStart 훅 자동주입 |
| gajae-code | .gjc/ 이중 영수증 + active/audit/transaction | stdout 영수증은 본문 에코 금지(라우팅만) |
| ouroboros | 단일 events 테이블(이벤트소싱) + 프로젝션 | 직접주입 X, replay로 재구성해 소비 |
| fable-ish | 단일 ledger JSON(세션×cwd 해시) | 원장 통째 X, 훅이 만든 짧은 자연어만 |
| 내 패턴 | 3스코프: 사용자전역 MD + 프로젝트 + 세션 JSON | 사용자메모리 자동주입 / 세션상태는 훅이 사용 |
[!note] 각 하네스의 생명주기·특이점 (펼쳐보기)
- Claude Code — CLAUDE.md는 세션시작 전량 로드(트리 머지, @import 4홉). 자동메모리는 모델이 자율 기록·읽기,
MEMORY.md는 처음 200줄·25KB만 시작로드./compact후 루트 CLAUDE.md·자동메모리는 디스크 재주입. 메모리 작성자=Claude 자신. SDKSessionStore로 S3/Redis/Postgres 미러(이중쓰기·best-effort).- Codex — 3분리: rollout JSONL(
sessions/YY/MM/DD/) + state SQLite 4종(state_5/logs_2/goals_1/memories_1) + Memories MD. Memories는 스레드 idle 시 2단계(Phase1 추출→Phase2 통합) 백그라운드. 기본 OFF, 입력 70% truncate, 비밀 redaction,<oai-mem-citation>출처표기.- OMC — state JSON(세션별) + notepad MD(3섹션) + project-memory JSON + wiki MD. notepad는 Priority 항상로드·Working 7일 prune·MANUAL 영구. userDirectives는
critical:/note:라벨로 compaction 견딤. 전부 atomic write, worktree 경계검증. 루트해석OMC_STATE_DIR>.omc-workspace>git>cwd.- gajae-code — CliWriteReceipt(stdout) + WorkflowStateReceipt(sha256 도장). sanctioned writer 단일관문(G1)에서 검증→atomic rename.
fresh_until=mutated_at+30분. 읽기=lenient(fail-open) / 쓰기=strict(fail-closed) 비대칭.- ouroboros — 도메인 이벤트 SSOT는
events(frozen BaseEvent), 나머지는 프로젝션.dot.notation.past_tense명명, sanitize로 replay-unsafe 키 제거. CheckpointStore(SHA-256·3단계 롤백)는 phase/progress/state JSON을 별도 저장.- fable-ish — 격리키
sha256(session_id|cwd)[:24]. UserPromptSubmit=리셋 / PostToolUse=증거 누적 / Stop=should_block_stop 판정. fail-open(깨지면 새 원장), redact 4종 정규식, stop_blocks≥2면 루프방지로 통과.- 내 패턴 — OMC를 실사용 채택+확장. ReadPath/WritePath 브랜드타입, PID-aware liveness(죽은 소유자 상태 회수), 샤드 frontmatter
originSessionId출처추적.
모두가 수렴하는 공통 패턴
- “모델은 무상태”가 제1전제 — 디스크가 곧 기억, 닫히기 전 적고 깨어날 때 읽는다
- 원자적 쓰기(tmp→rename) — 구조화 상태는 atomic write로 반쯤 쓰이다 깨지는 것 방지 (로그는 append+flush로 다른 전략)
- SSOT와 파생캐시 분리 — 파생물은 언제든 원본에서 재생성 가능 (Codex state, ouroboros projection, gajae snapshot)
- 수명 3단 분리 — 항상 로드 / N일 후 prune / 영구 (얇은 것 싸게 주입 + 무거운 본문 분리)
- 인덱스는 얇게, 본문은 지연로드 — 시작 전량로드는 인덱스에만 허용 (토큰 경제학)
- 비밀정보 redaction은 저장 경계에 — 디스크에 적히는 순간이 유출 순간 (fable·Codex·ouroboros)
- 세션 격리 키로 동시성 충돌 방지 — 멀티 세션 전제이므로 세션을 물리적으로 가름
누가 왜 다르게 했나 (분기점)
[!note] 5개 분기점과 트레이드오프 분기1 — 저장 매체. MD파(감사·편집 쉬움, 쿼리 약함) / 이벤트로그파(완벽한 인과 재구성, 매번 replay 비용) / JSON원장파(스키마 강제·빠른 판정, 서사엔 부적합). 분기2 — 누가 쓰나. 모델이 쓴다(자율적이나 품질이 모델 판단 의존: Claude 자동메모리·OMC wiki·Codex Memories) vs 훅/런타임이 쓴다(결정적·검증 가능하나 중요도를 코드가 미리 정함: fable·ouroboros·gajae·내 세션상태). 분기3 — 목적. 주입형(프롬프트에 들어가 행동 안내, 강제 아님) vs 제어형(훅·게이트 판정 입력, 강제력이 SSOT에). 분기4 — 실패 정책. fail-open(읽기, 가용성 우선) vs fail-closed(쓰기, 무결성 우선). gajae는 한 시스템 안에서 읽기/쓰기를 의도적으로 비대칭화한 유일 사례. 분기5 — 무결성. 체크섬 도장(gajae·ouroboros·Codex 마이그레이션) vs 외부 미러(Claude SDK SessionStore) vs 신선도 TTL(gajae 30분·OMC 7일·내 PID liveness).
실제 예시
각 SSOT 형식이 디스크에 실제로 어떤 모습인지 본다.
// 마크다운파 — fable-ish 격리키 (게이트가 읽을 JSON 원장)
// 경로: /tmp/fable-ish/ledgers/<해시>.json
// 키 산출: fable-ish/scripts/ledger.py:89
{
"ledger_key": "sha256(session_id|cwd)[:24]", // 세션×cwd 격리
"changed_paths": ["src/app.ts"], // PostToolUse가 누적
"verification_results": [], // 검증 증거
"failures": [],
"stop_blocks": 0 // ≥2면 루프방지로 통과
}
# 이벤트로그파 — ouroboros 이벤트소싱 (재생으로 상태 재구성)
# 소스: ouroboros/src/ouroboros/{events,persistence,harness}/
# 도메인 이벤트 SSOT = events 테이블 (frozen BaseEvent)
events 테이블 ──replay──> ProjectionBuilder ──> Run/Stage/Step/Artifact/Verdict
정정도 "새 이벤트" (불변, dot.notation.past_tense 명명)
CheckpointStore(SHA-256)는 phase/progress/state JSON을 별도 파일로 저장
# OMC project-memory — SessionStart 훅 자동주입 (주입형)
# 소스: oh-my-claudecode/src/{tools,hooks,lib}/ , session-start.mjs
userDirectives:
- "critical: 빌드 전 반드시 lint" # critical/note 라벨로 compaction 견딤
- "note: 비번은 .env.local 참조"
# 루트해석 우선순위: OMC_STATE_DIR > .omc-workspace > git > cwd
요약 & 셀프체크
- 7개 하네스는 “모델 무상태”라는 같은 전제에서 MD 파일 / 이벤트 로그 / JSON 원장 세 SSOT로 갈라진다.
- 더 중요한 분기는 저장 형식보다 “누가 쓰나(모델 vs 훅) × 무엇을 위해 쓰나(주입 vs 제어)” 이다.
- 내 스택 차용 원칙: 주입형은 “얇게·자동·compaction 생존”, 제어형은 “세션격리·fail-closed·신선도” — 두 축을 섞지 않는다.
스스로 답해보기
- “주입형”과 “제어형” 메모리의 강제력 차이는 어디에서 오는가? 각각 한 사례를 들어보라.
- 이벤트 로그(SSOT) 방식이 “현재 상태”를 알려면 매번 치러야 하는 비용은 무엇이며, ouroboros·Codex는 각각 무엇으로 보완하는가?
- gajae가 읽기는 lenient(fail-open), 쓰기는 strict(fail-closed)로 비대칭화한 이유는? 내 세션상태 JSON에 어떻게 적용할 수 있나?
내 스택에 차용할 구체안
내 실제 스택은 Claude Code 호스트 + OMC 플러그인 + Codex(headless/exec) 보조다. 세 곳의 강점이 안 겹치므로 합치면 보강된다.
- [OMC 유지·강화] project-memory userDirectives
critical:/note:자동주입을 기본 운영선으로. 단 전역 MEMORY.md 샤드와 project-memory 역할을 못박기 — 전역=사용자/스택 규칙, 프로젝트=빌드·비번·함정. (단, formatter 예산 제한 있으니 “중요 지시 3개 내외” 규칙) - [Codex 차용] Memories의 “본문 아닌 교훈만, 2단계 추출”을 내 사용자메모리에 적용. 처음엔 수동
/remember+ diff review로 흉내. 입력 70% truncate + 비밀 redaction은 그대로 베낄 가치(현재 샤드에 비번 평문은 위험). - [Codex 차용] state SQLite식 “파생 색인” 도입. 원본(MD/JSON)은 그대로 두고 세션 검색용 경량 인덱스를 파생캐시로. canonical source 먼저 정하고 rebuild 명령 제공,
goals/memories처럼 재생성 불가 상태는 cache 취급 금지. - [gajae 차용] 읽기 lenient / 쓰기 strict 비대칭 + 30분 신선도. JSON workflow state에만 적용(사람이 편집하는 wiki/MD에 checksum 강제는 마찰 큼). PID-liveness와 결합.
- [fable-ish 차용] Stop 게이트 + stop_blocks 루프방지로 검증 누락 방지. 세션×cwd 해시라 멀티프로젝트서 안 섞임. 처음엔 hard block 말고 advisory mode로 시작(false positive 관리).
- [Claude SDK 차용] SessionStore 미러 — 단, Codex rollout에 그대로는 부적합(SessionStore는 Claude Agent SDK의 local JSONL mirror). Codex엔 별도 adapter 필요,
persistSession:false와 배타.
[!tip] 한 문장 차용 원칙 주입형(OMC·Claude 메모리)은 “얇게·자동·compaction 생존”으로, 제어형(fable Stop·gajae strict-write)은 “세션격리·fail-closed·신선도”로 — 두 축을 섞지 말고 각자 강점에서 가져온다.
근거 — 참조한 기능노트 및 소스 경로
기능노트:
- CC_20_prompt-assembly — CLAUDE.md 4계층·@import 4홉·자동메모리 200줄/25KB·compaction 생존표
- CX_90_persistence-and-memory — rollout JSONL/state SQLite 4종/Memories 2단계·oai-mem-citation
- OMC_70_state-memory-persistence — state/notepad/project-memory/wiki 4계층·atomic write·worktree 경계
- GJ_80_gajae-receipts-and-workflow-state — 이중 영수증·sha256 도장·읽기lenient/쓰기strict·deep-interview 단조게이트
- OB_30_event-sourcing-and-projection-readmodel — events SSOT·BaseEvent frozen·replay·ProjectionBuilder·CheckpointStore
- FB_50_evidence-ledger-state — ledger JSON·sha256(session_id|cwd)·fail-open·redact·Stop 게이트
- MINE_80_state-memory-persistence — 3스코프·ReadPath/WritePath 브랜드·PID-aware liveness·샤드 originSessionId
- 보강: OB_90_evolutionary-loop-drift-and-audit-ledger(ledger 감사), CC_10_agent-loop
공식문서/소스:
/mnt/d/6study/10_프레임워크분석/_원문아카이브/claude-code/memory.md— CLAUDE.md vs 자동메모리(작성자=Claude), 200줄/25KB, /memory, compaction 생존,.claude/rules/계층(:169)/mnt/d/6study/10_프레임워크분석/_원문아카이브/claude-code/agent-sdk__session-storage.md— SessionStore 인터페이스, 이중쓰기·best-effort 미러·mirror_error·persistSession:false충돌(:226)/home/seunghyeong/harness-work/codex/codex-rs/{rollout,state,memories}/— recorder.rs/policy.rs, state DB·migrator(state/src/lib.rs:92,migrations.rs:5), Memories Phase1/2/home/seunghyeong/harness-work/oh-my-claudecode/src/{tools,hooks,lib}/— state-tools/notepad/project-memory/wiki, worktree-paths.ts(:27,:716), session-start.mjs(:262,:648), formatter.ts(:16)/home/seunghyeong/harness-work/fable-ish/scripts/ledger.py(:89,:138) +hooks/{post_tool_use,stop_gate}.py, verify_state.py(:42)/home/seunghyeong/harness-work/ouroboros/src/ouroboros/{events,persistence,harness}/— uow.py(:76), schema.py(:64), checkpoint.py(:28)/home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/gjc-runtime/{state-writer,state-schema,cli-write-receipt}.ts(state-writer.ts:491), deep-interview-recorder.ts(:178)- 내 패턴 실측:
/home/seunghyeong/.claude/projects/-home-seunghyeong/memory/MEMORY.md,/home/seunghyeong/.claude/plugins/marketplaces/omc/templates/hooks/session-start.mjs,/home/seunghyeong/projects/conclave/.omc/
연결
[!tip] Codex 교차검증 (요점 보존) 큰 축 “주입형 메모리 vs 훅/게이트형 제어 상태”는 유효. 단 저장 매체 기준 압축으로 일부 권위 원천을 오분류했으니 아래를 정정·보강했다. 사실 정정 — ① “세션×cwd 해시 원장”은 본래 fable-ish 구조(
ledger.py:89)이고 gajae는 active+audit+transaction+derived snapshot 조합(state-writer.ts:491). ② ouroboros는 이벤트 atomic append와 checkpoint 저장이 별개(checkpoint 실패해도 이벤트는 남음,uow.py:76) — “하나의 원자 트랜잭션”은 틀림. ③ ouroboros 도메인 이벤트 SSOT는events이나brownfield_repos도 존재(schema.py:64), CheckpointStore는 state JSON을 파일로 저장(checkpoint.py:28) — “어디에도 저장 안 함”은 과장. ④ Codexgoals_1·memories_1은 파생캐시 아닌 자체 상태저장소(lib.rs:92). ⑤ “atomic write 표준”·“redaction 공통 경계”는 일반화 불가(로그=append/transaction, redaction은 fable·ouroboros·Codex 한정). 빠진 차이 — Claude.claude/rules/(path-scoped 규칙은 /compact 후 빠질 수 있음,memory.md:169), OMC SessionStart context budget/priority(session-start.mjs:262,:648), append-only도 셋 다 재구성 대상·권위가 다름(rollout=대화replay/events=도메인event/audit=증거추적). 해석 보정 — 분기점은 저장형식보다 “누가 쓰나·읽나·무엇을 막나”가 핵심. fail-open/closed는 프레임워크가 아닌 단계 단위. gajae deep-interview는 “질문할수록 모호성이 준다”가 아니라 unresolved trigger 있으면 남은 모호성을 더 명시적으로 드러내야 함(deep-interview-recorder.ts:178). 차용 현실성 — OMC critical/note는 개수 제한(formatter.ts:16), Codex Memories 자동추출은 운영비 커서 처음엔 수동, gajae checksum은 JSON state에만, fable stop gate는 advisory부터, SessionStore는 Codex에 그대로 부적합(별도 adapter 필요).