Releases: etinpres/mindvault-v3
v3.8.2 — 전체 시스템 적대적 점검 (28개 결함 수정)
v3.8.2 — 전체 시스템 적대적 점검 (28개 결함 수정)
Summary
/goal "전체 시스템 점검 → 버그 수정 → 추가 발견 없을 때까지 반복" 으로 v3.8.1 코드베이스
전체(src 28모듈 + hooks + scripts)를 5라운드 적대적 워크플로(find → adversarial verify
→ runtime 재현)와 프로덕션 debug.log(30k줄) 텔레메트리 분석으로 점검했다. 747 테스트가
이미 그린이던 성숙한 코드에서 28개 실재 결함(데이터 유실 HIGH 1 포함)을 찾아 수정하고
각 수정에 회귀테스트를 붙였다(+29). 라운드별 발견 추세 11 → 13 → 2 → 3(HIGH 1) → 0 으로
수렴 확인. false positive 3건은 런타임으로 기각(예: raw sqlite3.connect 의 busy_timeout 은
Python 기본 5000ms 라 무해), 5건은 근거와 함께 defer.
테스트: 747 passed + 25 subtests → 776 passed + 41 subtests (회귀 0).
Fixed — HIGH (데이터 유실)
_collect_md_files중복 경로 미dedup → dedup-merge 자기삭제 영구 유실 (memory_indexer.py):
sources.json 에 DEFAULT 자동발견 슬롯을 add 하면 union 에 같은 dir 가 두 번 들어가 같은
.md가 두 번 방출 →dedup_cli._scan이 단일 파일을 자기 자신과 'name-dup' 으로 보고 →
cmd_merge가 canonical 을 overwrite 후 삭제(ok:True보고). resolved-path dedup 으로 단일
방출 보장(end-to-end 재현 확인).
Fixed — MED
- FTS5 예약어 누수 (
search.py/memory_search.py):AND/OR/NOT/NEAR포함 쿼리가
fts5: syntax error로 FTS 0건이 되던 회귀(운영 15건+). 토큰을 따옴표 phrase 로 감싸 리터럴
prefix 검색 강제. - 비-문자열 frontmatter 가 인덱서 run 전체 중단 (
memory_indexer.py):description: 2026
같은 YAML 비-str 값에.strip()→ AttributeError 가 per-file 가드 없는 루프를 뚫고 인덱싱
전체를 중단.str()강제. - 세션 인덱서: 임베드 장애 중 재인덱싱 시
sessions_vec영구 stale (indexer.py): 기존
세션 본문 변경 + 임베드 실패가 겹치면 FTS 는 새 본문, vec 는 옛 임베딩으로 고착(mtime skip +
backfillIS NULL둘 다 못 잡음). 실패 시 vec 행 삭제 → backfill 재충전. - extractor
finish_reason=length절단 → 빈 결과 영구 negative 캐시 (memory_extractor.py):
긴 응답이 max_tokens 에서 잘리면 non-empty 절단 문자열이 parse 0건 → 호출 성공 오인 → 고가치
세션 영구 추출 스킵. 절단을 호출 실패로 취급. - query_intent: 일시 장애를 7일 negative 센티넬로 캐시 (
query_intent.py): timeout/서버다운
(None)을 'Gemma 가 other 라 응답' 과 합쳐 캐시 → transient 장애가 해당 prompt 분류를 7일 배제.
호출 실패는 캐시하지 않고 재시도. - recall_cli "항상 exit 0" 계약 위반 (
recall_cli.py): numpy 없는 인터프리터(배포 /recall
컨텍스트)에서 lazy import ImportError 전파 → 비-0 종료. source 별 try/except 로 빈 결과 흡수. - contradiction_review 블록리스트 정규식이 공백 항목 미탐 (
contradiction_review_cli.py):
- some old memory같은 공백 포함 YAML 블록리스트를 못 잡아 mutation 거부 가드 우회 → 중복
inline 키로 YAML 손상.\S[^\r\n]*로 항목 전체 매칭. - memory_review 비원자 promote → 고아가 재시도 영구 차단 (
memory_review_cli.py): target
기록 후 INDEX append/src.unlink실패 시 target 이 남아 'target exists' 로 재시도 차단. 후속
실패 시 target+INDEX 대칭 롤백. - dedup_cli frontmatter raw 보간 (
dedup_cli.py): 멀티라인/비-str name·description·type 이
raw 보간되면 frontmatter 손상._serialize_fm_value단일라인 정규화.
Fixed — LOW (견고성·일관성)
- 모든 Gemma 호출자 5곳(search/memory_extractor/contradiction_detector/alias_generator/
query_intent) 비-문자열 content 가드 +UnicodeDecodeError흡수 — content-block 리스트
응답에.strip()/.splitlines()AttributeError, max_tokens 경계 멀티바이트 절단 시
UnicodeDecodeError 가 회수/추출 루프를 떨구던 동일 안티패턴을 전 호출자에 통일. - self_eval
turns_cacheimport 3곳 — src 가 sys.path[0] 가 아닌 컨텍스트에서 'No module
named turns_cache' 로 캐시 silent 사망(운영 9건/5-31). late-import 패턴 통일. reverify비-dict sidecar →.getAttributeError 로 scan 영구 skip. dict 가드(자가복구).memory_indexer._acquire_lock가 BlockingIOError 외 OSError 시 fd 누수. 대칭 처리.session_memory.cache_set결정적 tmp 이름 → 병렬 SessionStart interleave 손상. PID-tmp.memory_extractor._iter_balanced_arrays선행 불균형[에서 조기 종료로 유효 배열 유실.gemma_rerankLLM 중복 인덱스 미dedup → 동일 세션 중복 회수.backfill_cli--limit 0/음수 인자,provenance_backfill미존재 dir 등 CLI 입력 검증.alias_generatormeta-fail 진단 로그(원인 파일 추적 가능),memory_reviewprocedural 인덱스
링크 경로,deploy_drift_check경고 방향 중립화.
Method — 적대적 수렴 점검
- 5라운드 워크플로: finder 후보 → 독립 verifier 가 적대적 반증(코드 재확인 + 호출 도달성 +
테스트 커버 + runtime 재현). 라운드 간 수정 코드를 재투입해 fix-the-fix 도 검출(round4 가
round3 promote-rollback 비대칭 회귀 + dedup type 누락을 잡음). - 프로덕션 debug.log 텔레메트리로 이론이 아닌 실재 실패 우선 추적(FTS5 syntax error, import
실패, json parse fail, alias_sync failed). - 수렴 기준: 신규 confirmed 0 라운드. R5 = 0 → dry.
Deferred (근거와 함께 미수정 — 유지보수자 판단 영역)
query_intent'전에'시간접속사 오탐 → cosine 게이트 완화 오적용 (튜닝 — live hit rate
영향, query 분포 데이터 필요).alias_generator동시 generate lost-update (CLAUDE.md 가 유사 race 를 수용된 한계로 명시).self_evalrecall_utilization 시간 윈도우가 세션 경계 무시 (측정 정확도 — 침습적 + NEXT-37
baseline 영향, 재baseline 은 유지보수자 결정).- drift 백스톱이 '신규 미배포 모듈' 미탐 (구조적 — install.sh 하드코딩 배열).
- install.sh drift-hook settings 폐기 분기 (노출 ≈ 0).
v3.8.1 — 배포 후크화: git hook 자동 배포 + drift 백스톱
v3.8.1 — 배포 후크화: git hook 자동 배포 + drift 백스톱
배경 (근본 원인)
contradiction 분류 false-positive 를 93% 줄이는 프롬프트 보정 커밋(4a4fdac)이 repo 에는
2026-05-28 존재했으나, 배포 경로(~/.claude/scripts/mindvault)에는 06-01 까지 미반영이었다.
install.sh 가 모델 다운로드·launchctl·헬스체크·인덱싱까지 도는 무거운 설치라 코드 한 줄을
고쳐도 수동 재실행을 미루게 되고, 그 사이 라이브 hook 은 옛 코드로 계속 돈다. 실제로 이
3일 지연 때문에, 이미 수정 완료된 'haruko_novel 서사 타임라인' 항목이 fact_correction
오탐(conf=1.00)으로 review 큐에 올랐다. (구프롬프트 → fact_correction, 현 프롬프트 →
no_conflict, temp=0.0 결정론적 5/5 재현으로 배포 지연이 원인임을 확정.)
변경
- install.sh — MV3_SYNC_ONLY 경량 경로: 파일 복사 + settings.json 등록만 수행하는
sub-second sync (0.594s 실측). 모델 다운로드·pip·plist(launchctl)·헬스체크 대기·
인덱싱·pre-warm 은 전부 skip. src→target 매핑(파일 rename 포함)은 기존 deploy 호출을
그대로 재사용해 단일 진실원본 유지. 비arm64 인터랙티브 프롬프트도 우회. - .githooks/post-commit · post-merge: deploy 소스(src/·hooks/·skill/·scripts/·
install.sh) 변경 시 자동 경량 sync. 문서/테스트-only 커밋은 skip. install.sh 가
core.hooksPath=.githooks 를 self-wire 한다. - scripts/deploy_drift_check.py — SessionStart 백스톱: repo src/*.py ↔ 배포본
sha256 비교, drift 시 한 줄 경고 + 해결 명령(MV3_SYNC_ONLY=1 ./install.sh)을
additionalContext 로 주입. 훅 우회(--no-verify)·외부 동기화 누락까지 포착. 항상
exit 0(세션 비차단), drift 없으면 무출력, repo 경로 없으면(엔드유저 설치) 조용히 skip. - tests/test_sync_only.py: sync 배포·슬로우 단계 skip·drift 검출/복구 6 케이스.
효과
repo 커밋이 ~/.claude 배포 경로에 즉시 반영 → "repo 는 고쳤는데 배포는 stale" 상황이
구조적으로 불가능해진다. 훅을 우회해도 다음 세션 시작에 drift 경고로 포착되는 이중 안전망.
검증
747 passed + 25 subtests (신규 6 포함, 회귀 0). post-commit·post-merge 라이브 발동,
drift 양성/복구 스모크, 라이브 sync 0.594s 실측.
v3.8.0 — Phase 1 ③ 신뢰성 검증 (Phase 1 토대 완성)
v3.8.0 — Phase 1 ③ 신뢰성 검증 (over-trust 해소 / stale 자동 감지)
Phase 1(토대: 신뢰 가능한 효과적 회수)의 마지막 축. 회수된 stale 메모리를 검증 없이 믿는 over-trust를 막는다. 이로써 Phase 1 토대 3축이 모두 완성된다 — ①출처(v3.6.0) / ②효과적 회수(v3.7.0) / ③신뢰성(이번 릴리즈).
핵심 기능 (③)
- Canonical Facts Registry + 라이브 verifier: 메모리의 코드/사실 참조(임베딩 모델명·포트 등)를 현행 코드와 결정론적으로 대조해 stale 의심 판정. Gemma 미사용 — 운영비 0, CI에서 정확히 핀 가능.
- 판별 신호: 메모리가 옛 토큰(예:
bge-m3)을 담으면서 현행 토큰(arctic)을 미언급하면 stale 의심. 둘 다 언급하는 정당한 이력은 면제. - 회수 시 경고 라벨: stale 의심 메모리에 "재검증 필요: … (현행 코드/사실 대조 후 신뢰)" 1줄 동반 주입 (양 포맷터 byte-parity, v1 토큰낭비 금지 준수 — stale에만).
- SessionEnd 증분 트리거 + sidecar(주 1회 self-throttle) + 수동 CLI
reverify_cli(scan / list / verify-registry, 레지스트리 self-check). - 완료 게이트: 2026-05-30 "BGE-M3 → Arctic-ko" over-trust 사고(어시스턴트가 stale 임베딩 표기를 믿고 시스템 상태를 틀리게 답함)를 scan→yaml→formatter end-to-end 회귀 케이스로 고정.
신뢰성 (4-round 적대적 sweep, loop-until-dry)
구현 후 다차원 find→adversarial verify 워크플로로 Phase 1 전체를 새 결함 0건까지 반복 점검 (R1 8 → R2 2 → R3 4 → R4 0 수렴). 누적 14건 처리(13 수정 + 1 문서화):
- CRITICAL: 테스트 스위트가 사용자 실제 메모리를 변경하던 격리 누락(
conftest의MV3_MEMORY_DIR미격리) — 수정 + 회귀 가드. - HIGH:
install.sh가 reverify 모듈 미배포 → 배포 환경에서 ③ silent no-op — 수정 + 가드. - 그 외: YAML 미인용 frontmatter 소실, CRLF 본문 보존, 클록 역행 영구 SKIP,
_is_deprecated2KB cap의 Layer 5 감쇠 누락, parser BOM 불일치,_atomic_write동시 race 등.
검증
- 회귀: 678 → 741 passed, 1 skipped, 25 subtests (회귀 0)
- Layer 5(모순감지) 무손상, 두 회수 포맷터 byte-parity 유지
- 자동 게이트 조정·메모리 본문 auto-edit은 미구현 유지(설계 §4.4 — 잘못 학습된 loop 위험 회피)
설계·plan: docs/specs/2026-05-31-phase1-reliability-design.md, docs/plans/2026-05-31-phase1-reliability.md.
v3.7.0 — Phase 1 ②: 효과적 회수 강화 (self-check 계약 + checkable 게이트)
v3.7.0 — Phase 1 ②: 효과적 회수 강화 (self-check 계약 + checkable 게이트)
Summary
직전 v3.6.0(Phase 1 ① Provenance, 출처 추적)에 이은 Phase-1 두 번째 축 — 효과적 회수(effective recall). 회수된 메모리가 답변 reasoning 에 거의 통합되지 않는 under-integration(strict cited 7.62% baseline — self_eval.recall_utilization 측정) 문제를 두 갈래로 손본다: ① 회수 출력 CONTRACT 에 self-check 조항을 추가해 옵션·권장·다음 단계를 제시할 때 회수 메모리의 룰·제약과 충돌하는 항목을 스스로 제거/표기하도록 의무화하고, ② self_eval 에 checkable pass/fail 게이트(recall_utilization_gate() + --target CLI)를 얹어 spec §4.3② 완료 기준("strict cited 상승")을 prose 가 아니라 운영 가능한 임계값으로 고정한다.
12 커밋. 기능을 먼저 깐 뒤 4라운드 adversarial audit(19렌즈 × 2인 검증) 으로 계약·게이트의 갭을 교차 발견·수정했다. 양 포맷터(Layer 4 hook / compact 재주입) byte-parity 와 self_eval ingestion·sanitize 회귀를 모두 가드. 전체 회귀 678 passed · 1 skipped(pre-existing) · 25 subtests passed.
Added
- 회수 CONTRACT 에 self-check 조항 (
hooks/memory-recall.py _format_output/src/recall_core.py CONTRACT): 기존 "회수 노트:" 의무 출력 + "모순되면 즉시 표기" 뒤에 한 문장을 덧붙인다 — "옵션·권장·다음 단계 제시 시 위 회수 메모리에 명시된 룰·제약과 충돌하는 항목은 제거하거나 \"회수 메모리 <이름> 위반 가능성\"으로 표기." Layer 4 hook 과 compact 재주입(SessionStart)은 별도 구현이지만 동일CONTRACT상수/문구를 싣고, parity 테스트가 두 경로 출력의 byte-동일을 강제한다. - strict cited 목표 게이트 (
src/self_eval.py recall_utilization_gate()):recall_utilization()결과를 받아 strict cited 목표 대비 pass/fail 을 판정한다.judged = cited + marker_only + unused(no_response 제외, 측정 정의와 일치)가min_judged(기본 30) 미만이면 소표본 noise 차단을 위해insufficient_sample(pass=False). 반환 dict 에pass / strict / target / judged / min_judged / scope / reason노출. 측정 로직은 불변 — 게이트는 판정만 얹고 임계값을 자동 튜닝하지 않는다(설계 D6). --targetCLI 플래그 (self_evalargparse):--recall-utilization의 strict cited 목표치를 [0.0~1.0] 유한 실수로 받는다(기본RECALL_UTILIZATION_TARGET = 0.15≈ 2× baseline).--json출력 시 결과에gate키를 동봉.- 상수:
RECALL_UTILIZATION_TARGET = 0.15,RECALL_UTILIZATION_MIN_JUDGED = 30,RECALL_UTILIZATION_SCOPE(측정면 = Layer-4 hook 회수만 — compact 재주입 효과 미측정 명시).
Changed / Fixed (4라운드 adversarial audit)
- R2E-1 — 계약을 비가시 type 의존 → content+이름 기반으로 재서술: 초기 self-check 문구가 회수 출력에 가시화되지 않는 정보(type-scoping)에 의존하던 약점을 교정. 회수 결과에 실제로 보이는 "위 회수 메모리에 명시된 룰·제약" 문구 + 구체 placeholder
<이름>기반으로 재서술해 모델이 실제로 따를 수 있게 했다(parity 테스트가 양 포맷터에서 해당 문구 존재를 명시 단언). - R2C-1 — 게이트 측정 scope 명시: 게이트가 신뢰하는 strict 는
recall_utilization의 측정면 = Layer-4 hook 회수(UserPromptSubmit)만 포함한다. compact 재주입(SessionStart)은 동일 CONTRACT 를 싣지만 measurement 면에서 제외(_compact_metric은 recalled_ids 미기록 +RECALL_INJECTION_HEADERS가 compact intro 의 em-dash 변형 미인식). 게이트가 '효과적 회수' 를 과대 인증하지 않도록scope키로 이를 명시(pass/insufficient 양 경로 모두 노출, 테스트로 가드). - R2A-1 —
--targetnan/inf 검증:_target_arg가[0.0, 1.0]유한 실수만 통과시킨다. nan/inf 가 통과하면json.dump가 비-스펙NaN/Infinitybare token 을 출력해 엄격 JSON 파서를 깨뜨리던 경로를 argparse 단계에서 차단(nan·inf·-inf·범위 밖·비숫자 모두ArgumentTypeError). - R2-D-1 — compact 전파 테스트 갭: "compact 재주입은
format_memory_context경유라 self-check 계약이 자동 전파된다"는 설계 주장(D7)을 prose 로만 두지 않고, 실제COMPACT_INTRO로 렌더해 조항이 들어가는지 직접 고정하는 테스트를 추가. - docs: 설계 doc(D2 content-scope, D3 확정 문구) 및 plan prose(Goal·회귀 예상치 675) 의 stale 기술을 audit round2/3 정정 반영(R3D-1/R3D-2).
Tests
- self_eval 게이트 (
tests/test_self_eval.py TestRecallUtilizationGate, +7): target 이상 pass / 미만 fail /insufficient_sample/strict == target경계 통과(>=) / 기본 상수 / scope 노출(R2C-1) /--targetnan·inf·범위밖 거부(R2A-1). - 계약 parity·회귀 (
tests/test_recall_core_parity.py, +4): self-check 조항이 양 포맷터에 존재하고 기존 "회수 노트:" 계약과 byte-parity 유지(D7), 새 계약 footer 가RECALLED_NAME_RE추출 noise 를 만들지 않음(ingestion 회귀), snippet 의</system-reminder>누출 차단(sanitize 회귀), compact intro(COMPACT_INTRO) 경로 전파(R2-D-1). - 전체 회귀: 678 passed · 1 skipped(pre-existing) · 25 subtests passed.
Notes
- 솔직한 한계 ①: self-check 계약은 회수 출력에 실린 지시일 뿐, hook 이 모델의 준수를 100% 강제하지는 못한다(계약 텍스트가 답변에 들어가도록 보장할 뿐, 옵션·권장 단계의 실제 cross-reference 수행은 모델 재량).
- 솔직한 한계 ②: strict cited 게이트는 Layer-4 hook 회수면(UserPromptSubmit)만 측정한다. compact 재주입(SessionStart) 효과는 미측정이며, 회수면 합산은 Phase 2 후속 과제(측정 로직 동결 — D6). 또한 strict cited 는 substring-match 기반 lower bound 라 목표(0.15) 미달이 곧 실패는 아니다(의역·rephrase 통합은 marker_only/unused 로 과소집계).
- 코드 변경은
src·hooks·tests5개 파일에 국한(+240 -2). 회수 측정 임계값(raw cosine/score)은 게이트 결과로 자동 튜닝하지 않는다. - Phase-1 마지막 축(③ stale 자동 감지)은 별도 릴리즈로 분리.
v3.6.0 — Phase 1: Provenance (영구기억 출처 추적)
v3.6.0 — Phase 1: Provenance (영구기억 출처 추적)
Summary
"신뢰 가능한 효과적 회수(trustworthy effective recall)"를 향한 Phase-1 기반 로드맵의 첫 축 — 출처 추적(Provenance). 영구기억이 어디서 왔는지(어떤 세션·소스에서, 언제 포착됐는지)를 staging 시점에 frontmatter에 기록하고, 회수될 때 그 출처를 결과에 부착·표시한다. 사용자가 회수된 메모리를 볼 때 "이게 언제·어디서 온 기억인지"를 한 줄로 확인할 수 있어, AI의 "딴소리"를 검증·신뢰하는 근거가 된다.
12 커밋. 기능 골격(write → recall → format)을 먼저 깔고, 다단계 적대적 버그헌팅(find → adversarial verify → fix-the-fix, 2 라운드 + completeness sweep) 으로 프로덕션 갭과 직렬화 취약점을 교차 발견·수정했다. provenance 테스트 +28 (전체 668개 수집 · 667 passed · 1 skipped(pre-existing) · 25 subtests passed).
Added
- staging 시 출처 기록 (
write_staged): 영구기억 후보를 staged할 때source_type/source_ref를 frontmatter에 남긴다. 기본값 파라미터(source_type="session",source_ref=session_id)라 기존 호출부는 무변경. Phase-2(URL ingest 등) 다양한 소스 유형을 받을 수 있도록 시그니처를 미리 열어둠. - 회수 시 provenance 부착 (
recall_memory): 회수 결과에 메모리 파일의 frontmatter를 재파싱해provenance(source_type / source_ref / captured_at)를 동반한다. (DB 스키마 불변 — 결과 path frontmatter 재파싱 방식, TOP_K=1이라 비용 무시 가능.) - 회수 출력 "출처:" 라벨 (
_format_output):- [name]라인 직후출처: {source_type} {ref8} {date10}짧은 한 줄을 삽입(ref·날짜는 앞 8·10자만, 토큰 낭비 방지). 노이즈 방지를 위해unknown·빈 source_type은 미표시. Layer 4 hook(hooks/memory-recall.py _format_output)과 compact 재주입(src/recall_core.py format_memory_context)은 별도 구현이지만, parity 테스트(test_formatter_byte_equivalence)가 두 경로의 출력이 byte-동일함을 강제한다(provenance 4 shape 포함 — datetime captured_at / unknown 억제 / None ref / 8자 초과 ref). - 기존 메모리 backfill CLI (
provenance_backfill_cli): 출처가 없는 기존 메모리에 소급 부여. 억측 금지 원칙 — 기록된 session id가 있으면session, 없으면unknownfallback(git blame류 추론 안 함). 실제 기록 형식을 모두 인식하도록 session id 우선순위를staged_from_session→originSessionId(top-level) →metadata.originSessionId(nested — memory-production 파이프라인의 실제 형식)로 확장._procedural/디렉토리 포함(recall 인덱싱 범위와 일치),_staged/·MEMORY.md제외, atomic write(tmp + os.replace), trailing-whitespace fence 파일도 crash 없이 처리. dry-run 기본 /--apply시에만 쓰기, 멱등, skip·실패 파일명 리포트.
Fixed (adversarial sweep)
- cmd_approve 승격 시 출처 소실 — 게이트1 프로덕션 갭 (핵심):
write_staged는 출처를 staged frontmatter에 박았지만,cmd_approve가 승격(promote) 시 frontmatter를 처음부터 재구성하며source_type/source_ref/captured_at를 통째로 떨어뜨려 — 완료 게이트 "신규 영구기억 출처 추적률 100%"를 정작 영구 진입 경로(/memory_review 승인)에서 깨고 있었다._provenance_passthrough헬퍼(_supersede_passthrough패턴 미러)를 NEW-PROMOTE / UPDATE 두 final_fm 블록에 모두 wire-in. UPDATE 경로는 기존 영구메모리의 원본 출처를 우선하고, absent(또는unknown)일 때만 staged 메타로 폴백 — donor를 원자적으로 선택해 incoherent한 source_type/source_ref 짝 섞임을 차단. - recall 핫패스 UnicodeDecodeError 견고화: provenance frontmatter 재파싱 루프가
(OSError, UnicodeDecodeError, KeyError)를 모두 catch — 비-UTF8/손상 메모리 파일 한 개가 recall 결과 전체를 [] 로 날리던 경로 차단(backfill CLI와 대칭). - source_ref str 정규화 (JSON-safe):
recall_memoryprovenance 루프에서source_ref를captured_at과 동일 방식으로 정규화(isoformat 우선, 그 외 비-None/비-빈은str(), None/"" 유지). YAML이 bare 숫자·날짜를 int/date로 파싱해도/recall의json.dumps가 깨지지 않도록 보장(+ recall_clijson.dumps(..., default=str)방어). - contradiction-aware writer kwargs forward (Phase-2 forward-proof):
make_contradiction_aware_writer의 wrapper가**kwargs를 base_writer로 전달하지 않아,source_type/source_ref를 넘기는 미래 호출자(Phase-2 URL ingest 등)가 writer를 통과하며 provenance를 silently drop할 latent 버그를 사전 차단. 기존 caller(kwargs 없음) 무영향, contradiction detection 로직 변경 없음. - backfill 견고화: ref 확정 시
re.sub(r"\s+", " ", str(ref))로 multiline 값을 단일 라인으로 collapse(frontmatter 구조 보호), trailing-ws 닫는 fence를 tolerant 탐지(exactlines.index("---")ValueError → 전체 run abort + --apply half-migration 차단), per-file try/except로 한 파일 실패가 배치를 중단시키지 않음.
Tests
- provenance end-to-end 통합(write_staged → cmd_approve 승격 → index → recall → format 출처 라벨), cmd_approve NEW/UPDATE/fallback·unknown-adopts-staged 커버리지, recall_core parity(provenance shape), JSON-safe 직렬화, trailing-ws fence·multiline-ref collapse 단언 등 +28 테스트.
- vacuous(실제로 fix를 검증하지 못하던) fixture 교정 — multiline collapse 경로를 강제 트리거하도록 fixture에 실제
\nembed, docstring 부정확 기술 정정. - 전체 회귀: 668 collected · 667 passed · 1 skipped(pre-existing) · 25 subtests passed.
Notes
- 기존 메모리 backfill은 별도 CLI(
python -m src.provenance_backfill_cli <memory_dir> [--apply])로 수행 — 운영 메모리 172건(session 83 / unknown 89) 소급 적용 완료. - Phase-1의 나머지 두 축(②효과적 회수 self-check, ③stale 자동 감지)은 별도 릴리즈로 분리.
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 는 선택분에만 적용한다.
v3.4.1 — 전체 시스템 버그 감사 (data-loss/race/파싱 강건성)
v3.4.1 — 전체 시스템 버그 감사 (data-loss / race / 파싱 강건성)
Summary
8천 LOC 전반을 다중 에이전트로 감사(find → 적대적 verify)해 39건 후보 중 28건을
확정하고, 그중 23건을 수정(회귀 테스트 동반, 전체 599 passed). 핵심은 임베딩
서버 일시 장애·동시 write·LLM 출력 파싱이 조용한 영구 데이터 유실로 번지던
경로들을 닫은 것.
Fixed
동시성 / DB (HIGH·MEDIUM)
- WAL/busy_timeout 미설정 (
indexer.open_db):index.db가 기본 DELETE 모드라
동시 reader/writer 가unable to open database file(운영 370건+)·database is locked
를 빈발.PRAGMA journal_mode=WAL+busy_timeout=5000설정 (extractor_cache 선례).
WAL 은 파일 단위 영속이라 embed_cache 등 sub-conn 도 상속. - 세션 인덱서 무 lock:
trigger_background_indexer가 락 없이 detach spawn →
동시 실행 중복 작업.incremental_index에 flock(LOCK_NB) 추가 (session-indexer.lock). - alias
_save고정 tmp명 race: 동시 SessionEnd 가 같은 tmp 충돌/orphan-unlink →
alias_index손상. PID-고유 tmp 로 분리. - contradiction queue append-vs-rewrite race:
_mark_resolved가 tmp FD 에만
flock 해 read~replace 사이 concurrent append 가 유실._mark_resolved와
append_to_review_queue양측이 공용<queue>.lock을 LOCK_EX 로 잡아 직렬화
(v3.4.0 의 "single-writer 가정" 한계 중 append-loss 경로 개선).
임베딩 서버 일시 장애 → 영구 누락 (HIGH·MEDIUM)
- 세션 vec backfill sentinel: 서버 다운 중 backfill 이 영구 빈-blob sentinel 을
박아 복구 후에도 세션이 semantic recall 에서 영구 제외.EmbedUnavailable로
'본문 없음'(영구 skip)과 '서버 장애'(재시도)를 분리. - 메모리 vec 재인덱싱 mtime: embed 실패 시에도 mtime 을 갱신해 다음 run 이 skip,
메모리가 영구히 vec 없이 남음. embed 실패면 mtime/FTS/vec 미갱신 → 다음 run 재시도.
(retry/backoff 는 embed_text 가 recall 핫패스 공용이라 미추가 — mtime-skip 이 안전한
재시도 형태.)
데이터 유실 가드 (HIGH·MEDIUM·LOW)
- dedup merge: Gemma 통합 실패 시에도 non-canonical 삭제(ok:true 위장). 통합
성공한 파일만 삭제하고, 전부 실패/본문 빈 경우 overwrite·삭제 없이 중단. - dedup canonical overwrite: name/description/type 만 다시 써서 supersedes/
deprecated_by audit link 소실. 나머지 frontmatter flow-style 보존 재방출. - dedup
_backup: 기존.bak무조건 덮어쓰기 → 직전 복구본 소실..bak존재 시
타임스탬프 회전. - extractor 부정 캐시: Gemma 호출 실패 빈 결과를 영구 캐시 → 복구 후 추출 영구
스킵. 호출 실패(out is None)면 빈 결과 캐시 안 함. 정상 빈 응답("[]")은 캐시. - turns_cache wipe: append-only jsonl 의 일시 read 실패([] 반환)가 기존 turns 를
삭제+mtime 갱신해 영구 소실. st_size>0 + 기존 turn 존재면 이번 회차 건너뛰고 재시도.
파싱 강건성 (MEDIUM·LOW)
- staged frontmatter 줄바꿈: LLM 값(title/reason/evidence)의 줄바꿈/
---이
frontmatter 구조를 깨거나 조기 종료._fm_oneline으로 단일 라인 정규화 (따옴표
미사용 — naive 파서 호환). promote 경로(cmd_approve) 영구 메모리 write 에도 적용. - contradiction scalar-dup:
_patch_frontmatter_list가 scalarkey: value를
flow/block 어느 정규식에도 못 잡아 중복 key append(YAML 손상). scalar 감지 후 거부. - extractor greedy JSON:
re.search(r"\[[\s\S]*\]")가 배열 밖 산문 대괄호까지
삼켜 후보 폐기. 코드펜스 strip 후 직접 파싱 1순위 + balanced [...] 후보 선택. - alias frontmatter 따옴표:
description: "x"의 따옴표가 alias 프롬프트로 유입.
양끝 짝 따옴표만 제거.
훅 동작 (MEDIUM·LOW)
- 서브에이전트 SessionStart 게이팅: agent_type 미게이팅으로 모든 서브에이전트
시작마다 동기 요약 생성이 블로킹. agent_type 있으면 즉시 종료 (메인 세션 무영향). - alias claude subprocess recursion guard: provider="claude" 시 자식 claude 의
훅 재귀. subprocess env 에MV3_HOOK_RECURSION_GUARD=1주입. - SessionStart MISS 경로 staged 청소 누락: HIT 경로만
purge_staged_memory()
호출하던 것을 양쪽 모두로. - turns_cache dead import:
self_eval(flat-deploy 미배포) import 를 guarded 하여
배포 환경에서 import-time 폭발 방지. - recall alias 후보 게이트 통과: alias fallback 후보(score=0)가 score_threshold
게이트에서 탈락해 "임베딩 약한 메모리 alias 복구" 무력화. normalize 후 게이트 직상
sentinel 부여 (raw_cosine 정렬이 최종 ranking 결정 — 순위 왜곡 없음).
Test count
533 (v3.4.0) → 599 (+66, 회귀 테스트 동반)
Migration
bash install.sh 재실행. 스키마 변경 없음. WAL 은 인덱서 첫 실행에서 index.db 를
1회 전환(이후 모든 conn 상속), -wal/-shm 사이드카 파일이 ~/.claude/mindvault-v3/
에 생긴다 (로컬 FS 라 무해).
Known limitations (현재 상태 — 미수정 항목과 사유)
- recall
score_threshold게이트 vs 정렬 키 불일치 (recall-hot-path-1): 게이트는
normalized score 기준인데 정렬은 raw_cosine 우선이라, score 는 낮지만 raw_cosine 이
가장 강한 후보가 게이트에서 탈락할 수 있다. alias 후보 무력화(-5)는 이번에 sentinel
로 고쳤으나, 일반 게이트 동작은 그대로 둔다 —SCORE_THRESHOLD=0.50이 현재
게이트 의미에 맞춰 hit rate 66.3% 로 튜닝된 값이라, 정렬·게이트 키를 통일하려면
recall eval 로 FP rate 재측정이 선행돼야 한다 (사용자 결정으로 보류). - SIGALRM(ITIMER_REAL) 하드 타임아웃 (recall-hot-path-2/3):
recall_memory전체
구간이 알람 무장 상태라, 드물게 pathlib/sqlite 비-재진입 구간을 인터럽트하면 객체가
반파괴될 수 있다 (PosixPath '_str'에러, 운영 1건). 단, 이 알람은 400ms wall-clock
하드월의 유일한 강제 수단이고 thread/subprocess 격리 재설계는 핫패스 회귀 위험이
과도해 그대로 둔다. 실패는 이미 graceful(FATAL 로그 후 빈 회수). - supersede
_apply_supersedehalf-state (contradiction-halfstate-5): 두 파일
순차 mutation 중 NEW write 가 OLD 성공 후 실패하면 OLD 만 deprecated 인 좁은 윈도우.
재시도가 idempotent 로 수렴하며, 제안된 both-tmp 방식도 윈도우를 제거 못 하고 축소만
하므로 그대로 둔다 (exit 2 + stderr 로 surfaced). - (v3.4.0 의 supersede audit-trace stale, backfill
--memory-dir범위 한계는 그대로.)
Memory ops
- 신규 lock 파일:
~/.claude/mindvault-v3/session-indexer.lock,
~/.claude/mindvault-v3/contradictions.jsonl.lock(둘 다 flock advisory).
v3.4.0 — Contradiction Detection (Layer 5)
v3.4.0 — Contradiction Detection (Layer 5)
Summary
- Memory Compiler 후속 단계로
contradiction_detector자동 fire (session_memory_end.py:make_contradiction_aware_writer) - Hybrid recall (FTS5 + Arctic-ko-MLX RRF) top-5 → Gemma 4 E4B 4-way 분류 (metric_update / decision_reversal / fact_correction / no_conflict)
- Review CLI (
python -m src.contradiction_review_cli) 로 dismiss / supersede / update 결정 - Layer 4 recall hook 이
deprecated_by메모리의 raw_cosine + score 둘 다 × 0.3 감쇠 (primary sort key 가 raw_cosine 이므로 양쪽 감쇠 필요)
Inspiration
외부 CDSS MindVault fork 의 LLM-detected contradictions (fact-layer 4-type 중 detection 부분만 차용). 4-type 메모리 분류는 이번 범위 밖 (현재 detection 만 구현).
What's new
src/contradiction_detector.py— detection engine + jsonl queuesrc/contradiction_review_cli.py— list / show / resolve applyhooks/memory-recall.py+src/memory_search.py— Layer 4 deprecated_by decaysrc/session_memory_end.py—make_contradiction_aware_writerhook integrationscripts/contradiction_backfill.py— one-shot pair-wise sweep for existing memories
Test count
470 (v3.3.0) → 533 (+63)
Migration
bash install.sh재실행 (스키마 변경 없음)- 신규 메모리는 SessionEnd hook 자동 처리
- 기존 메모리는
python scripts/contradiction_backfill.py --dry-run으로 먼저 모집단 확인 후 본 sweep 권장
Limitations
- Gemma cold 응답이 timeout 초과하면 silent skip (graceful fail). debug.log 에 telemetry
- confidence < 0.7 false negative 허용 (silent abstention 정책). 보수적 retrieve 가 필요한 도메인 (e.g. CDSS 임상결정지원 fork) 은 threshold 낮춰야 함
_patch_frontmatter_list는 block-style YAML list 검출 시 mutation refuse (debug.log 기록). 수동 변환 또는 flow-style 로 통일 후 재시도- review CLI 는 single-writer 가정:
resolve --apply의 jsonl rewrite (_mark_resolved) 와 frontmatter list mutation (_patch_frontmatter_list) 은 대상 파일에 lock 을 잡지 않는다. 동시에 두 resolve 를 실행하거나, resolve 도중 다른 세션의 SessionEnd 가 queue 에 append 하면 last-write-wins race 로 한쪽 변경이 소실될 수 있다 (드뭄 — 인터랙티브 단일 사용자 환경에서는 발생하지 않음). append 가 소실돼도 다음 세션에서 같은 메모리가 재staged 되면 재검출되므로 영구 손실은 아니다. - supersede audit-trace 가 staged stem 을 기록:
supersederesolve 는 신규(staged) 파일의 stem (YYYYMMDD-HHMMSS_type_slug) 을deprecated_by/supersedes에 쓴다. promote 후 파일 stem 이 clean slug 로 바뀌므로 이 audit 링크는 더 이상 존재하지 않는 stem 을 가리킨다. decay 는deprecated_by키의 존재만 검사하므로 회수 down-weight 기능 자체는 정상 작동한다 — audit trace 만 부정확하다. - backfill
--memory-dir <custom>는 prod index DB 에 인덱싱된 디렉토리만 의미:recall_memory가 production index DB 를 읽으므로, 인덱싱되지 않은 custom dir 을 주면 0건 검출된다 (스크립트가 stderr 경고 출력). default (prod memory dir) 사용 시 정상.
Memory ops
- 새 jsonl path:
~/.claude/mindvault-v3/contradictions.jsonl(append-only) - Atomic rewrite: tmp + os.replace + fcntl.flock (single-user 가정)
- Frontmatter mutation: tmp + os.replace per file
v3.3.0 — NEXT-37: 회수 메모리 활용률 측정 + Zep/Chain-of-Note hook contract
NEXT-37 — 회수 메모리 본질 결함 해결
[[recalled-memory-weight]] 결함: 회수된 메모리가 메인 Claude deliberation 에 통합 안 되는 v3 본질 약점. v4 (멀티에이전트 통합) 비전 진입 전 v3 장기기억 신뢰성 회복이 선결.
3-agent 자료조사 수렴 결론
회수 메모리를 LLM 답변에 강제 통합하는 유일한 검증된 메커니즘 = 답변에 활용 흔적을 explicit 출력하라는 contract (Self-RAG reflection token / Chain-of-Note 명시 노트 / Cursor positive instruction / Anthropic structured output 모두 같은 아이디어 수렴).
변경
Phase 1 — 측정 framework
- 1A (
hooks/memory-recall.py):_metricdict 에recalled_ids컬럼 추가 — 회수된 메모리 id (name 우선, path fallback) 박힘 - 1B (
src/self_eval.py):recall_utilization메소드 신규 — 4-bucket 분류 (cited / marker_only / unused / no_response). CLI:python3 src/self_eval.py --recall-utilization --hours 168 - 1B retroactive:
--source transcriptsflag — metrics.jsonl 없이 transcript jsonl 의 hook attachment.stdout 직접 파싱. Phase 1A 이전 데이터도 활용도 분석 가능
Phase 2 (Step 2) — Hook contract 변경
_format_output 변경:
옛:
<system-reminder>
# 메모리 회수 (Layer 4 hybrid)
- **name** (score 0.95, vec+fts) — desc
</system-reminder>
신규:
<system-reminder>
MEMORY CONTEXT (다음 fact 를 본 답변 reasoning 에 반드시 통합):
- [name] (score 0.95, vec+fts) — desc
발췌: ...
답변 시작 전 한 줄로 "회수 노트: <위 메모리가 본 질문과 어떻게 관련되는가, 무관하면 '무관'>" 명시 출력 의무. 회수 fact 와 답변이 모순되면 즉시 표기.
</system-reminder>
3 메커니즘 결합:
- Zep "MEMORY CONTEXT:" 라벨 — production 검증 포맷
- Cursor positive instruction ("반드시 통합") — arxiv 2512.18925 수렴
- Chain-of-Note self-report ("회수 노트:") — EM +7.9 효과
Codex 5-round sweep (systematic-debugging "2 연속 0건 close")
- Round 1 (9건): M1 (marker), M2 (substring length floor), M3 (TZ 명시), L1 (source sanitize), L2 (빈 results 가드), L3 (name
]escape), L4 (splitext multi-dot), L5 (header prefix) - Round 2 (5건 root-cause): A2/B4/B5 →
_get_naive_tzlazy resolution + env-keyed cache, A7 (system-reminder wrapper 필수), B1 (events_source ValueError) - Round 3 (B3 scope 외): 3 test file env var leak —
patch.dict(os.environ, {}, clear=False)패턴 - Round 4 (vacuous pass): integration test 의
if r.stdout.strip(): assert→self.skipTest(...)명시 - Round 5: 자체 + codex 둘 다 0건 → close
현재 상태
- pytest 470 passed + 25 subtests (회귀 0)
- Production retroactive baseline (30일, N=213): cited 17 (7.62%), marker_only 2 (0.95%), unused 194 (91.43%)
- Hook contract deploy timestamp: 2026-05-27T09:56 KST
- 1주 dogfood 진행 중 — 정량 효과 측정은 누적 후 가능
호환성
- Apple Silicon Mac 전용 (Intel/Linux/Windows 미지원, 로컬 LLM 은 Gemma 4 E4B 만)
- Phase 1B retroactive 분석은 옛 (Layer 4 hybrid) + 신규 (MEMORY CONTEXT) 두 hook format 모두 backward compat
MV3_NAIVE_TZenv var 로 timezone override 가능 (default Asia/Seoul)
회수 결함 직접 측정
python3 src/self_eval.py --recall-utilization --source transcripts --hours 168
설치 후 1주 사용 → 위 명령으로 본인 환경 활용률 측정 가능.
v3.2.10 — /recall 결함 sweep (NEXT-36)
Summary
/recall 스킬 (memory_search.py + search.py + recall_cli.py) 에 대한 systematic-debugging 4-phase + codex 독립 검증 sweep. 2 fix + 4 false alarm confirm + 7 회귀 가드 + 1 cosmetic 정정. 2 연속 라운드 0 critical/medium 으로 수렴.
Critical Fix
memory_search._vec_top_k — mat ↔ meta 인덱스 미스매치
- 증상:
memories_vec에 invalid embedding row (빈 bytes, dim mismatch) 1건만 있어도:results = [(meta[i][0], ...) for ..., i in enumerate(idx_sorted)]→IndexError또는 잘못된 path 반환raw_map의 cosine 값을 잘못된 path 에 매핑 (cross-contamination, p2 cosine 0.775 → 0.0 게이트 차단, p3 cosine 0.884 → 0.775)
- root cause:
mat[i] = arr(row 인덱스) 와meta.append(...)(valid 만) 의 인덱스 정합 깨짐. 같은 모듈의search.vec_candidates는valid카운터 +mat[:valid]안전 패턴이었으나 한 함수만 fix 누락 - fix:
valid카운터 +mat[:valid]패턴으로 교체. 두 모듈 안전 패턴 통일 - silent 이유: 운영 DB invalid row 0건. 인덱서가 partial write 1회만 일으키면
recall_memory의 outerexcept Exception가드가 잡아 빈 결과 반환 → 사용자는 검색 0건으로만 보고 결함 모름
Medium Fix
_resolve_wikilink — ORDER BY path 추가
- 증상: 다중 후보 시 SQLite 반환 순서 미보장 → 같은 wikilink 가 호출마다 다른 메모리로 resolve
- fix:
path LIKE ?쿼리에ORDER BY path추가 (lexicographic 안정 결정성)
False Alarm 분류 (의도된 동작 confirm)
| 항목 | 분류 |
|---|---|
| M1: 무의미 query ("1234") 5건 통과 | /recall 명시 호출의 user-driven sift 정책 (score_threshold=0.0, raw_cosine_min=0.32) 일치 |
M3: gemma_rerank fallback 정렬 |
RRF rank 기준 첫 K 가 합리적, 게이트 후 절대-상대 분리 정책 |
L1: fts_escape 두 모듈 중복 |
8/8 sample 동등성 confirm. DRY 통합 비용 > 효용. skew 회귀 가드만 추가 |
L2: recall_cli.py top_k 옵션 없음 |
spec(recall.md) 의도, 명시 호출은 user-driven sift |
회귀 가드 7건 (tests/test_memory_search.py)
TestVecTopKInvalidRowRegression× 4 (empty / bad-dim / all-invalid / all-valid backwards compat)TestFtsEscapeParity× 1 (search.fts_escapevsmemory_search._fts_escape12 sample 동등성)TestResolveWikilinkDeterminism× 2 (lex order + 20회 반복 안정성)
검증
- pytest 422 → 429 passed (회귀 0)
- 운영 본
~/.claude/scripts/mindvault/memory_search.pymd5 동기화 - 실 query verify — 한국어/영문 query + sessions search + Gemma summary 모두 정상
- Round 2 자체 sweep → 0 critical/medium
- codex:codex-rescue 독립 검증 → 0 critical/medium (1 cosmetic 주석 정정만)
- Round 3 자체 sweep (alias race / raw_cosine_map 사용 / import 순환) → 0 new
- → 2 연속 라운드 0건 close 수렴 기준 (
feedback-systematic-debugging-code-review) 충족
Files changed
src/memory_search.py(+19/-4)tests/test_memory_search.py(+170)
누적
결함 99건.