Skip to content

v3.4.1 — 전체 시스템 버그 감사 (data-loss/race/파싱 강건성)

Choose a tag to compare

@etinpres etinpres released this 29 May 04:34
· 72 commits to master since this release

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 가 scalar key: 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_supersede half-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).