fix: nvr_snapshot 후속턴에 도구 사라지던 문제 + 캡처 이미지 채팅에 안 뜨던 문제

실사용(papa, 2026-09-07)에서 드러난 2건:
- "cctv확인해봐"로 도구가 돌았는데 다음 "아니다 cam2"/"사진 보여줘" 후속턴엔
  hasCctvKeyword가 현재 메시지만 보느라 도구가 빠졌고, 모델이 "저는 그런 툴이
  없습니다"를 3턴 반복 → topicText(직전 2턴 포함)로 검사, cam N·스냅샷·캡처 등 추가
- 캡처된 프레임을 모델이 프로즈로만 설명하고 이미지 마크다운을 안 뽑아 사용자가
  사진을 못 봄 → satellite_snapshot처럼 execute-tool에서 SSE로 이미지 직접 push
- 도구 결과 지시문을 "이전 대화 짐작 말고 이 프레임에 보이는 것만 구체적으로"로 강화
  (gemma4가 첫 응답에서 맥락에 기대 뭉뚱그린 사례)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
This commit is contained in:
kim
2026-09-07 11:14:24 +09:00
co-authored by Claude Sonnet 5
parent 1a40512270
commit 58a079fcb1
4 changed files with 24 additions and 2 deletions
+7
View File
@@ -1413,6 +1413,13 @@ print(json.dumps({'slides': slides, 'total': len(prs.slides)}, ensure_ascii=Fals
const imgMatch = _resultText.match(/!\[[^\]]*\]\(\/api\/files\/satellite-images\/[^)]+\)/);
if (imgMatch) sendSSE?.('token', { text: `\n${imgMatch[0]}\n` });
}
// nvr_snapshot: same problem — the captured frame must appear in the chat regardless of
// whether the model bothers to reproduce the markdown. Observed 2026-09-07: the model
// described the frame in prose and never emitted the image, so the user saw no photo.
if (name === 'nvr_snapshot' && tr.success) {
const imgMatch = _resultText.match(/!\[[^\]]*\]\(\/api\/files\/nvr-snapshots\/[^)]+\)/);
if (imgMatch) sendSSE?.('token', { text: `\n${imgMatch[0]}\n` });
}
// nhc_active_storms: observed the model retyping the forecast-track URL with a
// wrong basin/slot digit (e.g. graphics_ep2.shtml → graphics_ep5.shtml) instead of
// copying it verbatim — silently pointing at an unrelated or dissipated storm. When
+5 -1
View File
@@ -170,7 +170,11 @@ export function buildSkillToolFilter(input: ToolScopeInput): (toolName: string)
// nvr_snapshot — 집 CCTV 한 프레임 캡처해 모델이 직접 확인. "마당에 누구 있어?", "택배 왔나
// 봐줘", "현관/주차장 확인", "고양이 있나" 같은 요청에만. 카메라/감시 어휘 + "확인해줘·봐줘"류
// 동사, 또는 알려진 카메라 위치명이 있을 때 연다.
const hasCctvKeyword = /cctv|감시\s*카메라|카메라(로|에|를|\s|$)|웹캠|현관(?:문|앞|쪽)?|마당|주차장|차고|대문|택배|초인종|누가?\s*(?:왔|있|지나)|누구\s*(?:있|왔)|밖에?\s*(?:누구|뭐|무슨)|집\s*(?:앞|밖|상황)|door\s*cam|security\s*cam|who'?s?\s*(?:at\s*the\s*door|outside)/i.test(message);
//
// topicText(직전 2턴 포함)로 검사한다 — 실사고(2026-09-07): "cctv확인해봐"로 도구가 한 번
// 돌았는데, 바로 다음 "아니다 cam2" / "사진 보여줘" 후속턴엔 키워드가 없어 도구가 사라지자
// 모델이 "저는 그런 툴이 없습니다"라고 3턴 연속 답했다. 스테일 매치 비용은 작은 스키마 토큰뿐.
const hasCctvKeyword = /cctv|감시\s*카메라|카메라(로|에|를|\s|$)|웹캠|현관(?:문|앞|쪽)?|마당|주차장|차고|대문|택배|초인종|스냅샷|캡처해|누가?\s*(?:왔|있|지나)|누구\s*(?:있|왔)|밖에?\s*(?:누구|뭐|무슨)|집\s*(?:앞|밖|상황)|\bcam\s*\d|채널\s*\d.*(?:봐|보여|확인|찍)|door\s*cam|security\s*cam|who'?s?\s*(?:at\s*the\s*door|outside)/i.test(topicText);
// 2026-08-10: news_search was gated on the literal word "뉴스" (hasNewsKeyword below), so a
// disaster question with no news-flavored wording ("돌핀 지금 어디 있지?") never got access to
// it — even after handle-chat.ts's reminder started explicitly recommending news_search for
+1 -1
View File
@@ -121,7 +121,7 @@ export const nvrSnapshotTool = {
const now = new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', hour12: false });
return {
success: true,
stdout: `${resolved.label} 카메라 — ${now} 현재 화면:\n\n${buildImageMarkdown(outPath, workspacePath)}\n\n위 이미지를 보고 화면에 실제로 보이는 것만 설명하세요(사람/차량/택배/동물/조명 상태 등). 어두워서 안 보이거나 화면이 흐리면 그렇다고 말하고, 보이지 않는 것을 추측하지 마세요.`,
stdout: `${resolved.label} 카메라 — ${now} 현재 화면:\n\n${buildImageMarkdown(outPath, workspacePath)}\n\n[중요] 위 이미지를 실제로 보고 답하세요. 이전 대화 내용이나 이 카메라가 어디를 비출 것이라는 짐작이 아니라, **지금 이 프레임에 보이는 것**만 구체적으로: 눈에 띄는 사물, 사람/동물/차량 유무, 문·조명·날씨 상태. 이미지가 어둡거나 흐리거나 나뭇가지 등에 가려 잘 안 보이면 그렇다고 말하세요. 프레임에 없는 것은 언급하지 마세요.`,
};
},
};
+11
View File
@@ -187,6 +187,17 @@ describe('키워드 게이트 — 필요할 때 확실히 나타난다', () => {
assert.equal(filterFor('CCTV 좀 보자')('nvr_snapshot'), true);
});
// [2026-09-07] 실사고: "cctv확인해봐"로 도구가 한 번 돌았는데 다음 "아니다 cam2" / "사진
// 보여줘" 후속턴에 키워드가 없어 도구가 사라지자 모델이 "그런 툴 없습니다"를 3턴 반복.
test('nvr_snapshot — 직전 턴이 CCTV였으면 후속턴에도 열린 채로 둔다', () => {
const f = (msg: string, recentUserText: string) =>
buildSkillToolFilter({ ...base, message: msg, recentUserText })('nvr_snapshot');
assert.equal(f('아니다 cam2', '두번째 채널이 옥상인데 cctv확인해봐\n아니다 cam2'), true);
assert.equal(f('사진 보여줘', '두번째 채널이 옥상인데 cctv확인해봐\n아니다 cam2\n사진 보여줘'), true);
// CCTV 맥락이 전혀 없으면 여전히 닫혀 있다
assert.equal(f('사진 보여줘', '점심 뭐 먹었어\n사진 보여줘'), false);
});
// [2026-08-10] 실제 사고: "태풍 돌핀 지금 어디 있지?"에 일반 채팅에서 news_search와
// weather_map_screenshot(기상청/JMA 태풍 진로 지도) 둘 다 안 열려 있어서 web_search만
// 쓰다가 검색결과를 놓쳤다(실제로는 중국 상륙·100만명 대피 중이었는데 "이미 소멸했을