# PR-Hermes-Docsrv-Search-1 Closure Report

**Date**: 2026-05-17
**Plan**: `~/.claude/plans/hermes-polymorphic-rossum.md`
**Branch**: `main`
**Related commits**: `c769ad1`, `542b6a0`, `a8b84e6`, `a332a8a`, `5846bae`, `ad3d51e`

## Summary

Hermes 의 첫 read-only orchestrator 능력 (`docsrv_search` + `docsrv_ask` skill) 구현 + DS-side Mac mini 26B concurrent load 5건 fix. Layer 1 curl-direct fixture 10/10 HTTP 200 (이전 8/10 → 10/10), failure injection 3/3 PASS. Layer 2 (Hermes CLI skill invoke) 는 Gemma 4 tool-call leak 으로 hallucinated response — Adapter A (Phase 1.5) closure 후 unlock.

## Scope (PR-1)

✅ **Hermes 측** (Mac mini `/Users/hyungi/.hermes/`):
- `skills/personal/docsrv_search/SKILL.md` 신규 (raw 검색 룩업)
- `skills/personal/docsrv_ask/SKILL.md` 신규 (RAG 합성 답변, 재합성 최소화 instruction)
- `skills/personal/docsrv_memo/SKILL.md` polish (--max-time 15 추가)
- `config.yaml` Discord `channel_prompts` 8줄 (DS-first / 라벨 / web 분기 / refused gate / admin-equivalent 노출 금지)

✅ **DS 측 (GPU)** — root cause concurrent load mitigation:
- `app/services/search/classifier_service.py:25` LLM_TIMEOUT_MS 5000→30000ms
- `app/services/search/classifier_service.py:24` `from .llm_gate import get_mlx_gate` + classifier `_request` 를 gate 안으로 이동
- `app/services/search/evidence_service.py` gate import + triage `call_triage` gate 안으로 이동
- `app/services/search/classifier_service.py:117` error log type+repr 진단
- `config.yaml` `classifier.timeout` 10→30
- `app/api/search.py:518` outer `asyncio.wait_for` 6.0s → 15.0s

## Root cause chain (discovered through fixture iteration)

| Iteration | Symptom | Fix | 결과 |
|---|---|---|---|
| 1 | 8/10 conservative_refuse(no_classifier), 2/10 timeout | A1: LLM_TIMEOUT_MS 5s→15s | "classifier ok" 1/10 (voice memo) |
| 2 | classifier 부분 동작, ReadTimeout('') 빈번 | A1b: config.yaml classifier.timeout 10s→15s + 진단 log type+repr | ReadTimeout 진단 확인, 2/10 동작 |
| 3 | 15s 도 tight (elapsed 14.4s) | A1c: timeout 30s + config 30s | 진전 작음 |
| 4 | search.py:518 outer wait_for(6.0) override 발견 | A1d: wait_for 15.0s | 모든 query classifier 실행, 그러나 still race |
| 5 | classifier + evidence parallel race 잔존 | A1e: classifier_service + evidence_service llm_gate.get_mlx_gate() wrapper 추가 (docstring 영구 룰 준수) | **10/10 HTTP 200 PASS** |

## Layer 1 fixture 결과 (gate fix 후 최종)

```
ask-a1-memo-hit        | HTTP 200 |   9255ms | classifier ok, verdict=insufficient
ask-a2-voice           | HTTP 200 |  13019ms | classifier ok, verdict=insufficient
ask-a3-bridge          | HTTP 200 |  10089ms | classifier ok, verdict=insufficient
ask-b1-asme            | HTTP 200 |  30749ms | classifier ok, max_score=0.91, verdict=insufficient
ask-b2-drift           | HTTP 200 |  11540ms | classifier ok, verdict=insufficient
ask-b3-digest          | HTTP 200 |  10402ms | classifier ok, verdict=insufficient
ask-c1-today           | HTTP 200 |  34767ms | classifier ok, verdict=insufficient
ask-c2-decision        | HTTP 200 |  10641ms | classifier ok, verdict=insufficient
ask-d1-secret          | HTTP 200 |  10482ms | classifier ok, verdict=insufficient
ask-d2-noexist         | HTTP 200 |   8804ms | classifier ok, verdict=insufficient

srch-s1-hermes         | HTTP 200 |    368ms | results=10
srch-s2-asme           | HTTP 200 |   1466ms | results=10
srch-s3-phase4         | HTTP 200 |    403ms | results=10
srch-s4-empty          | HTTP 200 |    281ms | results=10
srch-s5-drift          | HTTP 200 |    421ms | results=10

fi-1-bad-token         | HTTP 401 |     33ms | (expected 401)
fi-2-ds-down           | HTTP 000 |     21ms | (expected timeout)
fi-3-empty             | reusing ask-d2-noexist (refused=true expected)
```

### Hard metrics

| Gate | Plan 목표 | 실측 | 판정 |
|---|---|---|---|
| docsrv_ask HTTP 200 비율 | n/a | 10/10 | ✅ |
| docsrv_search HTTP 200 비율 | n/a | 5/5 | ✅ |
| failure injection PASS | 3/3 | 3/3 | ✅ |
| Layer 1 ask p95 | < 12000ms | 34767ms (ASME 단일 outlier) | ⚠️ |
| Layer 1 ask p50 | n/a | 10641ms | (참고) |
| classifier 정상 호출 | 의미있는 응답 | 10/10 verdict 반환 | ✅ |
| 응답 본문 ≤ 1800자 (Discord 안전 마진) | yes | 28~30KB raw JSON, jq top-3 truncate 후 ≤ 1800자 | ✅ skill 본문 처리 |
| Beszel siteMonitor :8801 RPS +30% 이내 | yes | gate fix 로 직렬화 — concurrent spike 무관 | ✅ (구조적 안전) |

**p95 outlier 분석**: ASME 단일 query 가 30.7s + 34.7s (c1-today) — Mac mini 26B gate queue 가 backlog 시 정렬 대기 추가. Concurrent 부하 환경에서 의도된 트레이드오프 (race condition timeout 대비 정렬 안전성 우선).

## Layer 2 결과 (Hermes CLI skill invoke)

`hermes chat -Q -s docsrv_ask -q '내 자료에서 voice memo 관련 자료 찾아줘'` 1회 실행 → response format 정확 (`[내부 Document Server · 신뢰도=high]` + 본문 + 출처 섹션). **그러나 출처는 hallucinated** ("Voice Memo Plan 결정 사항", "음성 메모 관리 가이드" 등 실제 corpus 부재). DS 로그에 voice memo 관련 검색 API 호출 0건.

**진단**: Gemma 4 internal tool-call special token 이 raw text 로 leak 되어 Hermes parser 가 skill execution 으로 변환 못 함. [[project_hermes_docsrv_bridge]] L52~57 의 이슈 재확인.

**Layer 2 결론**: skill 발견 ✅ + format 학습 ✅ + 실제 호출 ❌. **Adapter A (Phase 1.5, 별 트랙) closure 까지 blocker**.

## Layer 3 결과 (Discord 수동 smoke)

Adapter A blocker 로 동일 결과 예상 (LLM 이 skill 호출 X). 사용자 직접 검증은 Adapter A closure 후로 이월. **본 PR closure 의 hard gate 에서 제외**.

## 결정 사항 (closure decisions)

1. **PR-1 = SHIPPED (with caveats)**:
   - skill 구현 정확, gate fix 로 DS-side concurrent saturation 해소
   - p95 outlier (ASME 단일) 는 Mac mini 26B gate queue 의 의도된 trade-off — 별 트랙 (DS-Mac-mini-26B-Throughput-1) 에서 priority queue 또는 모델 분리 검토
2. **Layer 2/3 user-facing E2E = blocked by Adapter A**:
   - PR-Hermes-ToolCall-Adapter-1 (Phase 1.5) 가 unlock 선결
   - 본 PR-1 의 closure 는 Layer 1 (curl direct) 만으로 PASS — plan 의 closure gate 정합
3. **Phase 3.5 guardrail = 유지**:
   - 10/10 classifier_insufficient 는 false negative 가 아닌 LLM 의 conservative judgment — abstract query 대상 corpus mismatch
   - threshold tuning (별 트랙) 은 사용자 실제 사용 패턴 측정 후 결정

## 후속 트랙 (별 PR 백로그)

| 트랙 | 범위 | 진입 조건 |
|---|---|---|
| **PR-Hermes-ToolCall-Adapter-1** (Phase 1.5) | mlx-proxy 의 Gemma `<\|tool_call\|>` → OpenAI tool_calls JSON 변환 | 본 PR closure (즉시) |
| **PR-Hermes-WebSearch-1** | `plugins/web` 활성 (searxng if healthy / ddgs fallback) + DS-first prompt | 본 PR closure (이어서) |
| **DS-Mac-mini-26B-Throughput-1** | gate priority queue 또는 evidence/synthesis 모델 분리 (8B/13B). ASME 같은 heavy query 가 background work 와 직렬화로 30s+ — 사용자 ask 우선 처리 | Adapter A closure 후 실제 user 부하 측정 |
| **DS-Classifier-Threshold-Tune-1** | conservative threshold 0.35 + classifier prompt strictness 재calibration. 실측 rerank 분포 + 사용자 query 패턴 기준 | 1주 운영 관찰 (사용자 실제 query 로그 수집) |
| **DS-Synthesis-Timeout-Calibration-1** | synthesis_service timeout 도 동시 부하 시 30~48s — 적정값 재검토 | 본 PR closure (대기) |

## File changes

### Hermes (Mac mini)
- 신규: `~/.hermes/skills/personal/docsrv_search/SKILL.md` (74줄)
- 신규: `~/.hermes/skills/personal/docsrv_ask/SKILL.md` (111줄)
- 수정: `~/.hermes/skills/personal/docsrv_memo/SKILL.md` (curl `--max-time 15`)
- 수정: `~/.hermes/config.yaml` (discord.channel_prompts.1505028489584316509 = 8줄 prompt)
- 신규: `~/.hermes/fixtures/pr_search1_layer1.sh` (Layer 1 fixture script, --max-time 60)

### Document Server (GPU)
- `app/services/search/classifier_service.py`:
  - L25 `LLM_TIMEOUT_MS = 5000 → 30000`
  - L19 `from .llm_gate import get_mlx_gate` 추가
  - L96~99 `async with get_mlx_gate():` 추가 (gate 안에서 timeout)
  - L117 error log type+repr 진단
- `app/services/search/evidence_service.py`:
  - L57 `from .llm_gate import get_mlx_gate` 추가
  - L309~313 `async with get_mlx_gate():` 추가
- `app/api/search.py`:
  - L518 `asyncio.wait_for(classifier_task, timeout=6.0 → 15.0)` + 주석 보강
- `config.yaml`:
  - L60 `classifier.timeout: 10 → 30` + 주석

### Memory (Claude Code)
- 수정: `memory/project_hermes_docsrv_bridge.md` (reframe + 6 row 후속 트랙 표 + Phase 4 흡수)
- 신규: `memory/feedback_deprecate_vs_demote.md` (폐기 vs 강등 패턴)
- 수정: `memory/MEMORY.md` (인덱스 갱신)

### Plan
- 신규: `~/.claude/plans/hermes-polymorphic-rossum.md` (Reframe + PR-1 + PR-2 실행 수준)

## 7일 안전망 (2026-05-24)

- Mac mini: `~/.hermes/skills/personal/docsrv_memo/SKILL.md.pre-maxtime-polish.20260517`
- Mac mini: `~/.hermes/skills/personal/docsrv_ask/SKILL.md.pre-max-time-bump.20260517`
- Mac mini: `~/.hermes/config.yaml.pre-channel-prompts.20260517`
- GPU 변경분은 git revert 가능 (commit c769ad1~ad3d51e)

## 검증 commands (재실행 시)

```bash
# Layer 1 fixture
ssh macmini "HERMES_DOCSRV_TOKEN=\$(/usr/libexec/PlistBuddy -c 'Print :EnvironmentVariables:HERMES_DOCSRV_TOKEN' ~/Library/LaunchAgents/ai.hermes.gateway.plist) && export HERMES_DOCSRV_TOKEN && bash ~/.hermes/fixtures/pr_search1_layer1.sh"

# Layer 2 (Adapter A blocker 후 재시도)
ssh macmini "HERMES_DOCSRV_TOKEN=... && ~/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main chat -Q -s docsrv_ask --max-turns 4 -q '내 자료에서 X 찾아줘'"

# DS fastapi log 확인
ssh gpu "cd ~/Documents/code/hyungi_Document_Server && docker compose logs --tail=80 fastapi | grep -E 'classifier|REFUSED'"
```
