v2.2.256: 코어 채팅 큰 입력 청킹·통합 + 실제 컨텍스트 창 정렬 + 모델 핸들 race 수정

큰 입력 시 "Failed to acquire LM Studio model handle … Operation canceled"
로 턴 전체가 죽던 문제를 3계층으로 해결. 일반 채팅(코어 경로)은 그동안
단일 예산 호출이라 약한 모델·큰 입력에서 무너졌다 — 그 갭을 메움.

- 핸들 race 수정: getModelHandle 을 재시도 루프 안으로 이동. 취소/죽은-핸들
  류 에러는 SDK 재생성 후 1회 자동 재시도(실제 사용자 취소는 존중). 라이프
  사이클의 동시 로드가 abort 되며 SDK 가 coalesce 한 JIT 조회까지 죽던 것.
- Phase 1 실제 창 정렬: llm.getContextLength()(캐시)로 실측 창에 예산 클램프.
  설정값보다 작은 창으로 로드된 경우 서버 truncation/빈 답변 차단. 배지에 표시.
- Phase 2 코어 Map-Reduce: 단일 입력이 (유효 창 × ratio) 초과 시 청크→질의
  인지형 추출→통합. 부분/전체 폴백, 무관 시 정직 신호. 동시성 기본 2.
- Phase 3 메타 노출: 진행/결과 배지 표시, [조각 k] 출처 옵트인.

신규 설정 5종. /meet·/review 전용 경로는 불변. 테스트 +25건, 전체 684 통과.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-19 18:05:44 +09:00
parent 6adbc2a6fa
commit 76d5fedfb5
13 changed files with 883 additions and 19 deletions
+30 -2
View File
@@ -444,13 +444,38 @@
if (b.droppedHistory > 0) parts.push(`기록 ${b.droppedHistory}`);
if (b.systemTruncated) parts.push('컨텍스트 일부 생략');
if (b.cappedForSmallModel) parts.push('🔻 작은 모델 모드');
if (b.windowMismatch && typeof b.actualContextLength === 'number') parts.push('⚠ 실제 창 ' + fmtK(b.actualContextLength) + '↓');
if (b.tight) parts.push('⚠ 컨텍스트 거의 가득');
const warn = b.tight || b.systemTruncated;
const warn = b.tight || b.systemTruncated || b.windowMismatch;
ctxBadge.textContent = parts.join(' · ');
ctxBadge.className = 'ctx-badge' + (warn ? ' warn' : ' ok');
// New turn starts → drop stale stats from the previous answer.
lastLmStats = null;
ctxBadge.title = `model: ${b.model || ''}${b.paramB != null ? ' (~' + b.paramB + 'B)' : ''}\n입력 ≈ ${b.inputTokens} tokens (시스템 ${b.systemTokens}, 기록 ${b.historyKept}개)\n출력 상한 ${b.maxOutputTokens} tokens / 유효 context window ${b.contextLength} tokens${b.cappedForSmallModel ? ' (작은 모델용 축소; 설정값 ' + b.nominalContextLength + ')' : ''}`;
const mismatchNote = (b.windowMismatch && typeof b.actualContextLength === 'number')
? `\n⚠ 모델이 실제로는 ${b.actualContextLength} 토큰 창으로 로드됨 (설정 ${b.nominalContextLength}). 그 한도에 맞춰 예산함.`
: '';
ctxBadge.title = `model: ${b.model || ''}${b.paramB != null ? ' (~' + b.paramB + 'B)' : ''}\n입력 ≈ ${b.inputTokens} tokens (시스템 ${b.systemTokens}, 기록 ${b.historyKept}개)\n출력 상한 ${b.maxOutputTokens} tokens / 유효 context window ${b.contextLength} tokens${b.cappedForSmallModel ? ' (작은 모델용 축소; 설정값 ' + b.nominalContextLength + ')' : ''}${mismatchNote}`;
}
function renderMapReduceStatus(v) {
if (!ctxBadge || !v) return;
if (v.phase === 'start') {
ctxBadge.textContent = '🧩 큰 입력을 조각으로 나눠 관련 내용 추출 중…';
ctxBadge.className = 'ctx-badge warn';
ctxBadge.title = '입력이 컨텍스트 창보다 커서 청크→추출→통합(map-reduce)으로 처리 중입니다.';
} else if (v.phase === 'done') {
if (v.allIrrelevant) {
ctxBadge.textContent = '🧩 추출 결과: 요청 관련 내용 없음';
ctxBadge.className = 'ctx-badge warn';
ctxBadge.title = '긴 입력의 모든 조각에서 요청과 직접 관련된 내용을 찾지 못했습니다. 원본을 그대로(예산 내에서 잘라) 전달합니다.';
} else {
ctxBadge.textContent = `🧩 ${v.chunkCount}조각 → ${v.relevantCount}조각 추출·통합`;
ctxBadge.className = 'ctx-badge ok';
ctxBadge.title = `큰 입력을 ${v.chunkCount}개 조각으로 나눠 그중 ${v.relevantCount}개에서 관련 내용을 추출·통합했습니다.`;
}
} else if (v.phase === 'error') {
ctxBadge.textContent = '🧩 분할 처리 실패 — 단발 처리로 진행';
ctxBadge.className = 'ctx-badge warn';
}
}
function renderLmStudioStats(s) {
if (!ctxBadge || !s) return;
@@ -995,6 +1020,9 @@
case 'lmStudioStats':
renderLmStudioStats(msg.value);
break;
case 'mapReduceStatus':
renderMapReduceStatus(msg.value);
break;
case 'usedScope': {
let target = streamBody && streamBody._parent;
if (!target) {