v3.5.0 — compact 재주입 (압축 후 회수 맥락 복원)
v3.5.0 — compact 재주입 (압축 후 회수 맥락 복원)
Summary
긴 세션에서 컨텍스트 **압축(compaction)**이 일어나면 그동안 회수돼 있던 메모리 맥락이
함께 사라진다. v3.5.0 은 압축 직후 SessionStart 가 source="compact" 로 재발화하는
시점에, 무거운 5-세션 요약 대신 현재 세션의 최근 사용자 발화로 hybrid recall 을 돌려
관련 메모리만 경량 재주입해 그 맥락을 되살린다 (Layer 1 보강, agentmemory 차용 후보
A — docs/specs/2026-05-30-agentmemory-adoption-candidates.md).
기능 자체는 hook 1개 분량이지만, 5라운드 적대적 코드감사(find → adversarial verify
→ mutation 입증)로 진짜 버그 3종을 잡고 fix-the-fix 까지 수렴시켰다. 전 과정 mutation
15/15 통과(각 fix 를 망가뜨리면 해당 테스트가 정확히 실패), 두 compact 테스트 파일
인접 실행 flakiness 0/5, 617 → 639 passed.
Added
- compact 재주입 (
session_memory.handle_compact_reinjection): PreCompact hook 은
압축 이후 컨텍스트에 살아남는additionalContext를 주입할 수 없으므로(공식: decision
필드만), 압축 직후 다시 fire 하는 SessionStart +source=compact경로를 사용한다.
SessionStart 가matcher="*"로 등록돼 있어 별도 등록 변경 없이 compact source 가
들어온다. 현재 세션 transcript 의 최근 genuine user 발화(최대 4개)로recall_memory
를 호출해COMPACT_TOP_K=3메모리를hookSpecificOutput.additionalContext로 재주입. recall_core공용 게이트 (src/recall_core.py): Layer 4(memory-recall.py)와
compact 경로가 같은 임계값(SCORE_THRESHOLD/RAW_COSINE_MIN_*/RECALL_HINTS)과
포맷터(format_memory_context)를 공유하는 single source of truth. parity 테스트가
두 경로의 상수 동등성과 포맷 byte-동일성을 강제해 silent skew 를 차단.- compact 관측성 (
_compact_metric): 종결점마다metrics.jsonl에 1줄 기록
(injected/no_results/intent_skip/short_query/budget_timeout/
no_transcript/no_turns/import_skip) — Layer 4 와 대칭으로 발동률·스킵사유
분포·latency 를 집계 가능.
Fixed (5-round audit)
Round 1 — query 오염 + 시간예산 (MEDIUM)
- 압축 시점 query 오염: 압축 직후 transcript 에는
isCompactSummary=True요약
메시지(~13KB)·스킬 본문·<local-command-*>스캐폴딩이type=user로 섞인다. 초기
구현이 이들을 회수 query 로 포착해 "사용자 의도" 대신 boilerplate 로 회수가 빗나갔다
(recall-on-recalled)._recent_genuine_turns가 isCompactSummary·노이즈 프리픽스를
스킵하고deque(maxlen=N)로 마지막 N개 genuine 발화만 보존(redact N→k), per-turn
캡(MAX_MSG_CHARS) 후 tail-keep(query[-MAX:])으로 가장 최근 발화를 보존. - 시간예산 부재: compact recall 에 Layer 4(400ms SIGALRM)에 해당하는 가드가 없어
cold/hung Arctic-ko 서버(embed 5s) 가 compaction 재개를 통째로 블로킹할 수 있었다.
COMPACT_BUDGET_S=2.0SIGALRM +_CompactTimeout(BaseException)sentinel 로 상한.
Round 2 — vacuous 테스트 + intent 오탐 (HIGH)
- vacuous 회귀 테스트 2건: (a) intent-skip 테스트가 sentinel 을
raise해
broadexcept에 삼켜져 fix 제거에도 green, (b) tail-keep 테스트 fixture 가 너무
작아 truncation 미발생. mutation 으로 폭로 후 spy-flag / 강제-truncation fixture 로
교정. - intent join-blob 오분류:
query_intent.classify는 단일 prompt 설계인데 join 된
multi-turn blob 을 분류해, oldest 턴의 인사말("안녕")이나 어딘가의 meta 어구("토큰")
하나가 전체를 chat/meta 로 오분류해 회수를 스킵시켰다. - source 스칼라 가드 + parity:
"+".join(source)가 스칼라"vec"를 글자분해
('v+e+c')하던 잠재 버그.recall_core와memory-recall._format_output양쪽에
isinstance 가드 mirror + scalar parity 테스트.
Round 4 — SIGALRM 핸들러 누수 (MEDIUM)
- 핸들러 미복원: 시간예산 SIGALRM 핸들러를 설치만 하고 이전 핸들러를 복원하지
않아, 프로세스에_compact_alarm이 남고 이후 stray SIGALRM 이 무관한 코드에서
_CompactTimeout(BaseException)을 던졌다. 단발 hook 프로세스는 무해하나 장수
인터프리터(pytest)에서 flaky._prev저장→finally 복원으로 차단(compact·Layer 4
양쪽, defense-in-depth). - compact
introsanitize, short-query 테스트 vacuity, ImportError 별도 outcome 등
관측성·테스트 보강.
Round 5 — intent all-chat + 구조 정리 (MEDIUM)
- Layer 4 핸들러 복원 무테스트: round-4 가 복원을 양쪽에 적용했으나 compact 쪽만
테스트했다. mutation 이 전체 스위트를 통과(silent skew). Layer 4 복원 회귀 테스트 추가. - intent 게이트: 최신 턴만 분류하면 final ack("ok") 가 substantive 맥락을 억제. 최근
genuine 턴이 전부 chat/meta 일 때만 스킵하도록(all(...)) — join-blob(oldest 억제)·
latest-only(ack 억제) 양쪽 오탐 해소. - query build 중복(
extract_compact_queryvs inline) →_build_compact_query공유로
drift 차단. itimer 를 recall 직후 disarm 해 emit/metric 이 예산 밖(injected→
budget_timeout 오기록 차단). 복원 테스트를 distinct sentinel 로 강화(SIG_DFL-always
mutant 도 검출).
Test count
599 (v3.4.1) → 639 (+40, 회귀 테스트 동반). mutation 15/15 GOOD, flakiness 0/5.
Migration
bash install.sh 재실행. 스키마 변경 없음. compact 경로는 SessionStart hook 에 통합돼
별도 hook 등록이 필요 없다(matcher="*" 가 compact source 도 수신). recall_core.py 가
RUNTIME_EXTRA_SRC 로 ~/.claude/scripts/mindvault/ 에 신규 배포된다.
Known limitations (현재 상태 — 의도적 수용)
- numpy re-exec 인터프리터 선택: bootstrap 이 numpy 보유 인터프리터를 "첫 번째 다른"
것으로 고르며 numpy 유무를 재검증하지 않는다(Layer 4 와 공유하는 pre-existing 패턴).
배포 환경(framework python 3.10 우선)에선 발동 안 하고, 발동해도 compact 만 graceful
skip 이라 그대로 둔다. - compact + Layer 4 1회성 중복: 압축 직후 compact 가 주입한 메모리를 다음 메시지의
Layer 4 회수가 같은 topic 이면 1건 중복 주입할 수 있다. compaction 은 드문 1회 이벤트라
(v1 의 매-메시지 낭비와 다름) 수용. - transcript O(N) parse: 최근 4개 발화를 찾으려 모든 줄을
json.loads하지만 2s
예산이 cover 하고 redact 는 선택분에만 적용한다.