TL;DR
- 파일 diff 는 판정 도구가 아니다:
src_w2 ↔ src 는 15파일이 달랐지만 모델이 받는 프롬프트는 48개 전부 바이트 동일. 차이가 전부 다른 가족(routine)·다른 실험(Oracle-plan)을 위한 추가분이고 기본값이 종전 동작을 재현했다.
- 게이트 신설:
scripts/check_online_prompt_parity.py + 기준선 JSON. 세 트리(src_w2 · src_r1 · src) 계약 전부 일치. 역방향 검증 — 노트 규칙 한 단어(uncertain:→maybe:)만 바꾼 복사본에서 정확히 16개 프롬프트를 적발하고 종료코드 1.
- 정정 — textlog 는 온라인에서 로그가 안 쌓인다:
WRITER_POLICY["textlog"]["notes"]=="none" → oracle_log=False → record_oracle() 이 한 번도 실행되지 않는다. 21칸 0/21 실측이고, blind 와 프롬프트 md5 가 같다(7a5678ecfd79).
- KEEP 축출 문장이 뒤집었다: vis-pure 가 온라인 프레임을 보유하는 칸이 11% → 81%.
1배경 — 왜 계약을 기계로 재야 하나
온라인 실행 트랙은 워킹트리(src/)가 아니라 동결 스냅샷에서 돈다. 스냅샷은 심링크가 아니라 하드 카피라, 한 세션이 워킹트리를 고쳐도 남의 런은 영향받지 않는다.
| 트리 | 쓰는 곳 | 상태 |
tmp/plan_main/src_w2 | find v1.2 + restore 본 런 (이 세션) | 기준 |
tmp/routine_online/src_r1 | routine 온라인 (다른 세션) | 계약 일치 |
src/ (워킹트리) | 다음 스냅샷 원본 | 계약 일치 |
안전장치의 이면. 스냅샷 분리는 런을 지키지만, 반대로 한 세션의 수정이 다른 세션 스냅샷에 자동 반영되지 않고 그 사실이 보이지 않는다. "우리 둘 다 같은 메모리 계약으로 재고 있는가"를 눈으로 diff 해서는 답할 수 없다.
판정 기준을 옮겼다
파일이 몇 개 다른지는 신호가 아니다. 15파일이 달라도 프롬프트는 같을 수 있고, 1파일이 달라도 16개 프롬프트가 갈릴 수 있다(둘 다 이번에 실측했다). 판정은 모델이 실제로 받는 바이트로 한다.
2프롬프트 조립 순서와 전략별 계약
① 역할·입력 설명 (전 전략 동일)
② Skills / Rules (navigate·pick·pick_and_place·open/close·done·abort
+ "저수준 정책은 자주 실패한다. 프레임으로 판단하라")
③ 메모리 계약 블록 ← 전략별로 다른 유일한 부분 (memory_block)
④ Plan 블록 (plan_block: fixed | rewrite)
⑤ 출력 JSON 스키마 {"note", ["keep"], "reasoning", "next"}
WRITER_POLICY × PLAN_UPDATE
| 전략 | notes | images | plan | 온라인에 실제로 쌓이는 것 |
blind | none | none | fixed | 없음 |
textlog | none | none | fixed | 없음 (§3) |
frames-uniform | none | uniform | fixed | 자동 균등 샘플 (순증 ≈0) |
rollforward-v3 | rewrite | none | rewrite | 재작성된 노트 전체 |
rollforward-vis-pure | none | keep | fixed | 모델이 고른 프레임만 |
tagmem-r3 | append | keep | fixed | 태그 엔트리 + [now] + 프레임 |
노트를 쓰는 전략의 공통 머리말 — 1~4는 동일, 5번만 갈린다
Notes — this is the ONLY record you will have of earlier turns; you will not see those frames again:
1. 마지막 프레임이 명확히 보여주지 않으면 서브골 완료를 주장하지 마라
2. 확신 없는 것은 "uncertain: " 접두사
3. 시도한 것 / 눈에 띄게 바뀐 것 / 남은 것을 기록
4. 행동 안 했어도 목표 관련 물체를 본 방·표면 기록
5. ← writer 규칙 (rewrite: 전체 대체 / append: 한 줄 + [now])
1~4가 전 전략 공통이라 편향이 없다. 이것이 "전략 차이는 메모리 방식뿐"이라는 주장의 근거다.
KEEP 축출 정상화 문장 (260920, 기본 켬)
3. Your memory starts full with {cap} frames from the context phase. Dropping one of them to
record what you just did is expected, not a loss — keep whichever set best explains where
things are now.
예산·규칙을 바꾸지 않고 하네스가 만든 조건(시드 8장으로 꽉 찬 채 시작)을 명시할 뿐이다. SBENCH_KEEP_EVICTION_NOTE=0 이면 260920 이전 프롬프트를 바이트 단위로 재현한다.
tagmem 온라인 저장/재검색 (결정 D)
프롬프트에 보이는 것 = 시드 줄 + [예산 안의 과거 줄] + [now] 상태 한 줄
- 노트 줄 → 결정론 태거
query_tags 로 {obj, act, room, surf} 태그를 달아 엔트리로 누적 (추가 API 0)
- 온라인 줄이
BUDGET_TEXT=12 를 넘을 때만 retrieve(always_tail=3) 발동 — 예산 이내면 종전과 바이트 동일(find 87% 칸)
[now] 는 누적이 아니라 매 턴 갱신, 항상 맨 아래. 쓰인 줄은 notes_final 에 전부 남는다
3정정 — textlog 는 온라인에서 로그가 쌓이지 않는다
# online.py:590
oracle_log=(wpolicy["notes"] == "oracle")
# WRITER_POLICY["textlog"] = {"notes": "none", ...} → oracle_log=False
# orchestrator.py:810 record_oracle()
if not (memory_mode == "unified" and oracle_log):
return # ← textlog 는 항상 여기서 반환. 줄이 한 번도 안 붙는다
memory_block 에 oracle 분기가 존재하지만 아무도 그 분기로 들어가지 않는다. 260920 사용자 결정에서 "A(textlog 오라클 로그)는 안 함"으로 정했기 때문이며, 의도된 상태다.
결과: 온라인 textlog = blind + 얼어붙은 컨텍스트 로그 시드. 21칸에서 온라인 줄 0/21, 프롬프트 md5 도 blind 와 같다(7a5678ecfd79). memory_block(notes="oracle", ...) 을 손으로 호출해 렌더하면 실제와 다른 문장이 나오니 주의 — 이 세션도 한 번 그렇게 잘못 읽었다.
실제 에피소드(03986eff86b2, 8턴) — 첫 턴부터 마지막 턴까지 노트 블록이 글자 하나 안 변한다:
[context] [ 0s– 126s] I picked up the key chain from the tv stand in the bedroom and placed it on the drawer...
[context] [ 126s– 176s] I picked up remote control #1 ... but I could not place it
[context] [ 176s– 307s] I picked up the potato from the countertop in the kitchen and placed it on the chair...
[context] [ 307s– 451s] ...
[context] [ 452s– 673s] ...
[plan] 1. [ ] pick up the key chain — on the dresser in the bedroom
[plan] 2. [ ] pick up the irish potato — on the chair in the living room
4실제로 갱신되고 있는가 — v1.2 진행 중 런 실측
260921 10:40 UTC 기준. find v1.2 3팔 + restore 가 도는 중의 중간 집계다.
| 전략 | 칸 | 턴/칸 | 시드줄 | 최종줄 | 온라인 줄 | 줄 증가 칸 | 온라인 프레임 |
blind | 4 | 10.5 | 1.5 | 1.5 | 0.00 | 0/4 | — |
textlog | 21 | 25.0 | 6.9 | 6.9 | 0.00 | 0/21 | — |
frames-uniform | 13 | 18.6 | 1.8 | 1.8 | 0.00 | 0/13 | 1.00 (자동) |
rollforward-v3 | 11 | 15.9 | 2.5 | 5.0 | 2.82 | 11/11 | — |
rollforward-vis-pure | 132 | 7.0 | 1.2 | 1.2 | 0.00 | 0/132 | 0.91 · 81% 칸 |
tagmem-r3 | 125 | 7.1 | 35.9 | 43.0 | 7.11 | 125/125 | 2.13 · 98% 칸 |
vis-pure 턴당KEEP=0.273 온라인프레임 보유칸 106/131 (81%) 시드잔존 7.08/8
tagmem-r3 턴당KEEP=0.568 온라인프레임 보유칸 122/124 (98%) 시드잔존 5.86/8
축출 문장이 뒤집었다. 문장 전에는 vis-pure 가 89% 칸에서 온라인 프레임 0장이었다 → 지금 81% 칸이 보유. 능력이 아니라 축출 비용의 프레이밍 문제였다는 진단이 맞았다.
함정. online_frames_in_memory 필드는 uniform 전용이고 keep 전략에서는 null 이다. keep 전략 지표는 keep_stats.{keep, drop, evict, seed_left, online_kept} 를 봐야 한다. 이걸 모르고 보면 "KEEP 이 하나도 안 된다"는 오판을 한다 — 이번에 실제로 한 번 했다.
실제 갱신 예시 — tagmem [now] 상태 슬롯 (920acd6c51b0, 성공)
t=4 시점에 모델이 보는 노트 블록:
[context] ... (35줄: 태그 엔트리 + TIMELINE/PREV-HOP/LEDGER)
[plan] 1. [ ] pick up the mug — on the toilet in the bathroom
[t=0] Current view shows kitchen countertop. The mug moved earlier was placed on the toilet in the bathroom.
[t=1] Navigated to bathroom toilet. Attempting to pick mug on toilet.
[t=2] Grasped the mug on the toilet with gripper; continuing pick to lift it.
[t=3] Gripper closed around mug rim on toilet, continuing pick to ensure full lift.
[now] Gripper is grasping the mug on the toilet; continuing pick to lift it.
→ 그 턴 출력: note = 새 한 줄, keep = "KEEP: F4 DROP: m8"
과거 줄은 [t=n] 도장이 붙어 쌓이고, 최신 줄만 [now] 로 갱신되어 항상 맨 아래 붙는다. KEEP 도 DROP: m8 로 시드 프레임을 실제로 버리며 온라인 프레임을 넣는다.
rf-v3 계획 체크 (b2b0790f88de, 성공)
t=0 출력 → [plan] 1. [ ] navigate to the egg 2. [ ] navigate to the tissue box
3. [ ] pick the tissue box ← 항목을 스스로 추가
t=1 출력 → 1. [x] navigate to the egg - Arrived at bedroom dresser (egg location).
t=2 출력 → 2. [x] navigate to the tissue box
t=4 출력 → 3. [x] pick the tissue box - Tissue box was successfully lifted. Goal completed.
[plan] 을 노트 안에 넣은 설계가 의도대로 작동한 첫 증거다.
5남은 문제 2건
(1) 노트가 같은 말을 반복한다
| 노트 줄 | 유사도>0.8 연속쌍 |
tagmem-r3 | 908 | 146/781 (19%) |
rollforward-v3 | 175 | 114/164 (70%) |
위 mug 예시에서 t=2~t=10의 9턴이 전부 같은 문장의 변주다. 파지가 7~9턴 걸리는 동안 상태가 안 바뀌어 쓸 새 정보가 없는 것이라 파지 병목(실패의 84%)이 메모리에 비친 것이지 메모리 결함은 아니다. 다만 tagmem 은 이 반복 줄이 장부에 누적돼 검색 예산을 먹는다.
(2) rf-v3 는 여전히 계획을 지운다
rf-v3 11칸
마지막 턴까지 계획줄 생존 : 1/11
계획 전멸 : 10/11
턴 합계 removed_open=16 vs checked=2
"안 끝난 항목은 절대 지우지 마라"는 지시를 받고도 미완 항목을 8배 더 지운다. 260918의 "rf-v3 계획 100% 삭제"가 완화됐을 뿐 해결되진 않았다. §4에 보인 예시는 사실 1/11 소수 케이스였다. rf-v3 는 아직 11칸뿐이라 잠정치 — delta4 완주 후 확정한다.
대응 방침
둘 다 기록만 하고 프롬프트를 더 밀어붙이지 않는다(evaluate-don't-optimize). 계획 보존이 안 되는 것은 rewrite 전략의 성질이고, 그걸 프롬프트로 강제하면 재는 대상이 바뀐다.
6패리티 게이트 — scripts/check_online_prompt_parity.py
전략 6 × 가족 × 관절 2 × press 2 격자의 시스템 프롬프트를 각 트리마다 별도 프로세스에서 렌더해 md5 를 비교한다(한 프로세스에 두 트리의 sbench_exec 를 같이 올릴 수 없다). 배선 파일 6개 해시, 4개 수정 마커, WRITER_POLICY/PLAN_UPDATE, BUDGET_TEXT·ONLINE_NOTE_TAIL 설정도 함께 본다.
conda activate mlspaces
# (a) 트리끼리 비교 — 첫 트리가 기준
python3 scripts/check_online_prompt_parity.py \
--tree tmp/plan_main/src_w2 --tree tmp/routine_online/src_r1 --tree src
# (b) 기준선과 비교 — 스냅샷 뜨기 직전 게이트 (종료코드 0/1)
python3 scripts/check_online_prompt_parity.py --tree src \
--baseline claude/260921/online_prompt_baseline_v12.json
# (c) 기준선 갱신 — 계약을 의도적으로 바꿨을 때만
python3 scripts/check_online_prompt_parity.py --tree <새 스냅샷> \
--write-baseline claude/260921/online_prompt_baseline_vNN.json
파괴적 / 추가분을 나눠 판정한다. 가족·전략이 늘어난 것은 기준이 재던 것을 건드리지 않으므로 실패가 아니다(+ 로 표시). 공유 프롬프트 바이트가 바뀌거나, 정책·설정이 달라지거나, 수정 마커가 빠지거나, 기준에 있던 가족이 사라지면 종료코드 1. 이 구분이 없으면 캠페인이 늘어날 때마다 거짓 경보가 난다.
실행 결과
기준: claude/260921/online_prompt_baseline_v12.json (프롬프트 48개)
가족 ['find','restore'] · eviction_note=True store_tile=True budget_text=12
수정 마커: tag_entries_and_requery=O · now_state_slot=O · keep_eviction_note=O · keep_store_tile=O
── tmp/routine_online/src_r1
✅ 계약 일치 — 공유 프롬프트 48개 바이트 동일, 정책·설정·수정 마커 동일
+ 가족 'routine' 추가 — 프롬프트 24개 (기준이 재던 것에는 영향 없음)
ℹ️ 배선 파일 4/6개가 다름 (계약이 같으니 추가분)
── src
✅ 계약 일치 — 공유 프롬프트 48개 바이트 동일, 정책·설정·수정 마커 동일
+ 가족 'routine' 추가 — 프롬프트 24개
전부 계약 일치. 스냅샷을 새로 떠도 같은 것을 잰다.
종료코드=0
역방향 검증 — 노트 규칙 uncertain: → maybe: 한 단어
❌ 계약 불일치 16건
- 프롬프트 rollforward-v3|find|art=0|press=0: c01c71dc19c4(3754) → c1e905778764(3750)
- 프롬프트 rollforward-v3|find|art=1|press=0: ab762d959afb(3797) → b75eebea0437(3793)
- 프롬프트 tagmem-r3|find|art=1|press=0: ef375aa9f05c(4585) → a41445330236(4581)
... (노트 블록을 받는 두 전략 × 2가족 × art 2 × press 2 = 16개만 정확히 적발)
ℹ️ 배선 파일 1/6개가 다름
종료코드=1
기준선 프롬프트 md5 (art=1, press=0)
| 전략 | find | restore |
blind | 7a5678ecfd79 (2947) | 50f7a153dd33 (3009) |
textlog | 7a5678ecfd79 (2947) | 50f7a153dd33 (3009) |
frames-uniform | 2f58f1e1dec4 (2978) | 778aec25f738 (3040) |
rollforward-v3 | ab762d959afb (3797) | 0373dd1ff3d9 (3859) |
rollforward-vis-pure | 63fa98083c7c (3649) | 750b9c35818f (3711) |
tagmem-r3 | ef375aa9f05c (4585) | 9cd06c16f85a (4647) |
blind 와 textlog 가 같은 해시라는 것이 §3 의 기계적 증명이다.
7Takeaway & Next
- 스냅샷 분리는 런을 지키지만 세션 간 계약 드리프트를 보이지 않게 만든다. 판정은 모델이 받는 바이트로 한다.
- 260920 4개 수정(
TagmemNoteSelector · STATE_PREFIX · keep_eviction_note · keep_store_tile)은 세 트리 전부에 있고, find/restore 계약은 완전히 동일하다. routine 세션과 같은 메모리 계약으로 재고 있다.
- execute-state 채널은 6전략 중 3개(rf-v3 텍스트 · vis-pure 이미지 · tagmem 둘 다)에서 실제로 동작한다. textlog·blind·frames-uniform 의 순증 0 은 설계상 그런 것이며 대조군 역할이다.
Next
- rf-v3 계획 전멸 10/11 — delta4 완주 후 재집계. 유지되면 논문에 "rewrite 전략은 계획을 보존하지 못한다"로 기록
- tagmem 반복 줄(19%)이 장부 예산을 먹는 문제 —
retrieve 가 중복 줄을 어떻게 다루는지 확인 (측정만, 규칙 변경 아님)
- 새 스냅샷을 뜰 때마다 §6 (b) 를 절차로 실행. 기준선은 계약을 의도적으로 바꿀 때만 갱신
- resume 가족이 붙으면 자동으로 격자에 포함된다(스크립트가
TASK_FAMILIES 를 읽는다)
산출물
scripts/check_online_prompt_parity.py — 게이트
claude/260921/online_prompt_baseline_v12.json — 캠페인 기준선(48프롬프트)
claude/260921/analysis-online_memory_contract_parity.md — 계약 정본 문서
claude/260921/plan-resume_routine_online_handoff.md — 다른 세션용 포인터 추가