chore(release): v0.3.14 — AI fail 원인 가시화

- NoteCard: failed 노트에 <details> "원인 보기" 접힘 섹션 추가.
  ai_error 전체 노출 (wrap + word-break).
- AiWorker: markAiFailed 시 [reason] provider prefix 추가.
  사용자가 timeout/schema/other 카테고리 + 모델명 즉시 식별.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
th-kim0823
2026-05-12 14:07:53 +09:00
parent bd71bba2da
commit 4266376b23
5 changed files with 70 additions and 21 deletions

View File

@@ -3,6 +3,33 @@
본 파일은 Inkling 의 버전별 사용자 영향 변경 사항을 기록한다. 본 파일은 Inkling 의 버전별 사용자 영향 변경 사항을 기록한다.
형식은 [Keep a Changelog](https://keepachangelog.com/) 를 느슨하게 따른다. 형식은 [Keep a Changelog](https://keepachangelog.com/) 를 느슨하게 따른다.
## [0.3.14] — 2026-05-12
AI 처리 fail 원인 가시화. 이전엔 ai_error 가 NoteCard tooltip (title attribute) 에만 있어 사용자가 마우스 오버해야 보이는 데다 raw 메시지만 노출 → 무엇이 fail 했는지 불명.
### 수정
- **NoteCard failed 노트에 "원인 보기" 접힘 섹션 (P1).** `<details>` summary 클릭하면 `<pre>``ai_error` 전체 노출. wrap + word-break 적용. 사용자가 직접 메시지를 보고 모델/네트워크/JSON 등 fail 카테고리 진단 가능.
- **`ai_error` 에 reason + provider name prefix 추가.** AiWorker 의 markAiFailed 시 `[schema|other] local-ollama/gemma4:26b\n<원본 message>` 형식. 사용자가 어느 카테고리에서, 어느 모델로 실패했는지 즉시 식별. log 의 ai.failed 에도 reason/provider 필드 함께 출력.
### 게이트
- 단위 752 PASS (ai_error 포맷 변경은 test 영향 없음 — 기존 test 가 정확한 prefix 매칭 안 함)
- typecheck 0 errors
- 신규 npm dependency 0
### 사용자 안내
이미지 AI 처리가 fail 한다면 NoteCard 의 "정리 보류" 옆 "원인 보기" 클릭 → 표시되는 메시지로:
- `[timeout] ...` → vision 모델 cold-start 가 5분 초과. `ollama run gemma4:26b` 으로 한번 warm-up 후 재시도
- `[schema] title must contain Korean characters` → vision 모델이 영어 title 반환. prompt 가 한국어 강조했지만 일부 모델은 여전히 영어. `gemma3:27b` 등 다른 vision 모델로 대체 고려
- `[schema] unparseable response: ...` → vision 모델 JSON 출력 안 따름. v0.3.12 의 loose parse 가 실패한 경우
- `[other] missing response field` → Ollama 가 빈 응답 반환. 모델 자체 문제
### 업그레이드
v0.3.13 인스톨러 위에 v0.3.14 인스톨러를 같은 위치에 실행하면 in-place 업그레이드.
## [0.3.13] — 2026-05-12 ## [0.3.13] — 2026-05-12
대형 vision 모델 (gemma4:26b 등) 의 cold-start timeout 으로 인한 AI 처리 실패 fix. 대형 vision 모델 (gemma4:26b 등) 의 cold-start timeout 으로 인한 AI 처리 실패 fix.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "inkling", "name": "inkling",
"version": "0.3.13", "version": "0.3.14",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "inkling", "name": "inkling",
"version": "0.3.13", "version": "0.3.14",
"dependencies": { "dependencies": {
"better-sqlite3": "12.9.0", "better-sqlite3": "12.9.0",
"electron-log": "5.2.0", "electron-log": "5.2.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "inkling", "name": "inkling",
"version": "0.3.13", "version": "0.3.14",
"private": true, "private": true,
"description": "Inkling — local-first 한 줄 보관 도구", "description": "Inkling — local-first 한 줄 보관 도구",
"author": "altair823 <dlsrks0734@gmail.com>", "author": "altair823 <dlsrks0734@gmail.com>",

View File

@@ -233,8 +233,13 @@ export class AiWorker {
const nextRunAt = new Date(Date.now() + (this.backoffsMs[attempt + 1] ?? 0)).toISOString(); const nextRunAt = new Date(Date.now() + (this.backoffsMs[attempt + 1] ?? 0)).toISOString();
this.repo.incrementJobAttempt(job.noteId, nextRunAt, msg); this.repo.incrementJobAttempt(job.noteId, nextRunAt, msg);
if (isLast) { if (isLast) {
this.repo.markAiFailed(job.noteId, msg); // v0.3.14 — ai_error 에 reason + provider name prefix 추가. NoteCard 의 "원인 보기"
this.logger.error('ai.failed', { noteId: job.noteId, err: msg }); // 가 사용자에게 보여주는 raw 메시지에 context (timeout/unreachable/schema/other +
// 어느 모델이 fail 했는지) 가 포함되어 진단성 향상.
const provider = this.holder.get().name;
const annotated = `[${reason}] ${provider}\n${msg}`;
this.repo.markAiFailed(job.noteId, annotated);
this.logger.error('ai.failed', { noteId: job.noteId, err: msg, reason, provider });
if (this.telemetry) { if (this.telemetry) {
await this.telemetry.emit({ await this.telemetry.emit({
kind: 'ai_failed', kind: 'ai_failed',

View File

@@ -236,8 +236,9 @@ export function NoteCard({ note, onDeleted, onUpdated, mode = 'inbox', onRestore
</div> </div>
)} )}
{local.aiStatus === 'failed' && ( {local.aiStatus === 'failed' && (
<div style={{ marginTop: 4, display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ marginTop: 4 }}>
<div title={local.aiError ?? ''} style={{ fontSize: 16, fontWeight: 600, color: '#a55' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ fontSize: 16, fontWeight: 600, color: '#a55' }}>
</div> </div>
{/* v0.3.9 — per-note 재시도 UI. FailedBanner 의 일괄 재시도와 별개. */} {/* v0.3.9 — per-note 재시도 UI. FailedBanner 의 일괄 재시도와 별개. */}
@@ -254,6 +255,22 @@ export function NoteCard({ note, onDeleted, onUpdated, mode = 'inbox', onRestore
</button> </button>
</div> </div>
{/* v0.3.14 — fail 원인 inline 표시. ai_error 의 raw message 가 그대로 사용자에게
보여서 디버깅 + 모델/네트워크 이슈 진단 가능. 너무 길면 <details> 로 접힘. */}
{local.aiError !== null && local.aiError.length > 0 && (
<details style={{ marginTop: 4 }}>
<summary style={{ fontSize: 12, color: '#a55', cursor: 'pointer' }}>
</summary>
<pre style={{
fontSize: 11, color: '#666', background: '#fff0f0', padding: 6,
borderRadius: 4, marginTop: 4, whiteSpace: 'pre-wrap', wordBreak: 'break-word'
}}>
{local.aiError}
</pre>
</details>
)}
</div>
)} )}
{/* v0.2.9 Cut B Task 13 — ai_status='disabled': raw_text 첫 줄 fallback title. {/* v0.2.9 Cut B Task 13 — ai_status='disabled': raw_text 첫 줄 fallback title.
summary/tags 는 hide. 원문은 아래 "원문 보기" 영역에서 항상 표시. */} summary/tags 는 hide. 원문은 아래 "원문 보기" 영역에서 항상 표시. */}