상태·메모리 영속성 — 횡단분석

한 줄 요약

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 CodeCLAUDE.md 계층 + 자동메모리 MD + 세션 JSONL시스템 프롬프트 뒤 user 메시지로 주입(강제 아님)
Codexrollout JSONL(원본) + state SQLite + Memories MDresume 시 이력 재구성 / Memories 파일읽기
OMC.omc/ 4계층: state·notepad·project-memory·wikiproject-memory를 SessionStart 훅 자동주입
gajae-code.gjc/ 이중 영수증 + active/audit/transactionstdout 영수증은 본문 에코 금지(라우팅만)
ouroboros단일 events 테이블(이벤트소싱) + 프로젝션직접주입 X, replay로 재구성해 소비
fable-ish단일 ledger JSON(세션×cwd 해시)원장 통째 X, 훅이 만든 짧은 자연어만
내 패턴3스코프: 사용자전역 MD + 프로젝트 + 세션 JSON사용자메모리 자동주입 / 세션상태는 훅이 사용

각 하네스의 생명주기·특이점 (펼쳐보기)

  • Claude Code — CLAUDE.md는 세션시작 전량 로드(트리 머지, @import 4홉). 자동메모리는 모델이 자율 기록·읽기, MEMORY.md는 처음 200줄·25KB만 시작로드. /compact 후 루트 CLAUDE.md·자동메모리는 디스크 재주입. 메모리 작성자=Claude 자신. SDK SessionStore로 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)
  • 세션 격리 키로 동시성 충돌 방지 — 멀티 세션 전제이므로 세션을 물리적으로 가름

누가 왜 다르게 했나 (분기점)

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·신선도” — 두 축을 섞지 않는다.

스스로 답해보기

  1. “주입형”과 “제어형” 메모리의 강제력 차이는 어디에서 오는가? 각각 한 사례를 들어보라.
  2. 이벤트 로그(SSOT) 방식이 “현재 상태”를 알려면 매번 치러야 하는 비용은 무엇이며, ouroboros·Codex는 각각 무엇으로 보완하는가?
  3. 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와 배타.

한 문장 차용 원칙

주입형(OMC·Claude 메모리)은 “얇게·자동·compaction 생존”으로, 제어형(fable Stop·gajae strict-write)은 “세션격리·fail-closed·신선도”로 — 두 축을 섞지 말고 각자 강점에서 가져온다.


근거 — 참조한 기능노트 및 소스 경로

기능노트:

공식문서/소스:

  • /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/

연결

_분석축_루브릭 · HOME


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) — “어디에도 저장 안 함”은 과장. ④ Codex goals_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 필요).