chore: version up to 2.80.38 and package with refined recovery

This commit is contained in:
g1nation
2026-05-13 00:15:45 +09:00
parent 6c4bc3494f
commit eb36cec050
15 changed files with 202 additions and 62 deletions
@@ -1,5 +1,5 @@
{
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
"createdAt": 1778597639298,
"createdAt": 1778598898519,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
"createdAt": 1778597639290,
"createdAt": 1778598898518,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
"createdAt": 1778597639286,
"createdAt": 1778598898517,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "---\nid: stress_conflict_1778597639274\ndate: 2026-05-12T14:53:59.302Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (1ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (8ms)\n",
"createdAt": 1778597639302,
"result": "---\nid: stress_conflict_1778598898506\ndate: 2026-05-12T15:14:58.520Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (10ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (1ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (2ms)\n",
"createdAt": 1778598898520,
"modelVersion": "unknown"
}
@@ -1,8 +1,8 @@
{
"missionId": "stress_conflict_1778597639274",
"missionId": "stress_conflict_1778598898506",
"status": "completed",
"startTime": "2026-05-12T14:53:59.274Z",
"totalElapsedMs": 28,
"startTime": "2026-05-12T15:14:58.506Z",
"totalElapsedMs": 14,
"results": {
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
@@ -16,30 +16,30 @@
{
"from": "idle",
"to": "planner",
"durationMs": 11,
"durationMs": 10,
"message": "전략 수립 중...",
"ts": "2026-05-12T14:53:59.285Z"
"ts": "2026-05-12T15:14:58.516Z"
},
{
"from": "planner",
"to": "researcher",
"durationMs": 1,
"message": "핵심 정보 수집 및 분석 중...",
"ts": "2026-05-12T14:53:59.286Z"
"ts": "2026-05-12T15:14:58.517Z"
},
{
"from": "researcher",
"to": "writer",
"durationMs": 8,
"durationMs": 2,
"message": "최종 리포트 작성 및 편집 중...",
"ts": "2026-05-12T14:53:59.294Z"
"ts": "2026-05-12T15:14:58.519Z"
},
{
"from": "writer",
"to": "completed",
"durationMs": 8,
"durationMs": 1,
"message": "미션 완료",
"ts": "2026-05-12T14:53:59.302Z"
"ts": "2026-05-12T15:14:58.520Z"
}
],
"resilienceMetrics": {
+2 -2
View File
@@ -6,6 +6,6 @@
"description": "Auto-detected from the local project path in the conversation.",
"corePurpose": "Capture project direction, architecture discussion, decisions, and development notes as Markdown.",
"detailLevel": "standard",
"createdAt": "2026-05-10T07:42:38.921Z",
"updatedAt": "2026-05-10T07:42:38.928Z"
"createdAt": "2026-05-12T15:13:04.937Z",
"updatedAt": "2026-05-12T15:13:04.938Z"
}
@@ -0,0 +1,22 @@
# Development Log: /Volumes/Data/project/Antigravity/ConnectAI 분석하고 부족한 부분이나 개선이 필요한 부분을 알려주면 좋겠어.
## Purpose
Record the implementation or verification outcome from the current conversation.
## Implementation Summary
## 요약 현재 프로젝트 구조는 기본적인 개발 환경 설정 파일과 아스트라(Astra)의 배포 관련 파일들로 구성되어 있습니다. 전반적으로 기능적인 코드가 눈에 띄지 않아, 이 디렉토리 자체만으로는 시스템 전체의 아키텍처나 로직 흐름을 파악하기 어렵습니다. 개선이 필요하다면, 핵심 비즈니스 로직과 관련된 소스 코드와 API 문서화가 가장 시급합니다. ## 상세 설명 - **현재 구조 분석:** `/Volumes/Data/project/Antigravity/ConnectAI` 디렉토리는 프로젝트의 메타 정보(README.md, LICENSE, PATCHNOTES.md), 의존성 관리 파일(package.json, package-lock.json), 빌드 설정(jest.config.js, tsconfig.json), 그리고 여러 버전의 Astra 관련 바이너리 및 스킨 파일들(`astra-*.vsix`)을 포함하고 있습니다. - **핵심 문제:** 이 디렉토리에는 `src/` 폴더가 존재...
## Architecture
Captured automatically from the assistant response and active project context.
## Changed Files
- ARCHITECTURE_ANALYSIS.md
## Dependency Notes
No new dependency note was captured automatically.
## Bugs
No bugs recorded.
## Lessons
- Automatic project records should be generated in the background when the turn contains durable project knowledge.
@@ -0,0 +1,22 @@
# Development Log: /Volumes/Data/project/Antigravity/ConnectAI 분석하고 부족한 부분이나 개선이 필요한 부분을 알려주면 좋겠어.
## Purpose
Record the implementation or verification outcome from the current conversation.
## Implementation Summary
## 요약
## Architecture
Captured automatically from the assistant response and active project context.
## Changed Files
- No explicit changed file list was captured automatically.
## Dependency Notes
No new dependency note was captured automatically.
## Bugs
No bugs recorded.
## Lessons
- Automatic project records should be generated in the background when the turn contains durable project knowledge.
+6
View File
@@ -84,3 +84,9 @@
## 2026-05-10
- Auto decision record created: decisions/ADR-0007-volumes-data-project-antigravity-connectai-이거에-기능-개선을-하고-싶어-.md
## 2026-05-12
- Auto development record created: development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation.md
## 2026-05-12
- Auto development record created: development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation-2.md
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "astra",
"displayName": "Astra",
"description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.",
"version": "2.80.37",
"version": "2.80.38",
"publisher": "g1nation",
"license": "MIT",
"icon": "assets/icon.png",
+32 -18
View File
@@ -45,6 +45,7 @@ import {
extractVisibleFinal,
shouldFinalOnlyRetry,
shouldAutoContinue,
looksCutOff,
mergeContinuationParts,
buildContinuationUserPrompt,
FINAL_ONLY_DIRECTIVE,
@@ -485,27 +486,36 @@ export class AgentExecutor {
let fullSystemPrompt: string;
if (isAgentMode) {
// 1. 기본 시스템 프롬프트에서 에이전트 포맷과 충돌하는 섹션 제거
// The Agent's prompt IS the primary directive (role / persona / tone / output format),
// so it LEADS the system prompt — models anchor on the first persona they see, not the
// last, especially small ones. The Astra base prompt is reduced to neutral scaffolding
// (action tags, current date, anti-leak rules) and follows; a short reminder at the very
// end keeps the model from drifting back to a generic assistant.
const strippedSystemPrompt = this.stripAstraFormattingForAgentMode(systemPrompt);
const agentPromptText = (options.agentSkillContext || '').trim();
if (estimateTokens(agentPromptText) > Math.floor(config.contextLength * 0.5)) {
logInfo('Agent prompt is unusually large relative to the context window.', {
model: actualModel, agentPromptTokens: estimateTokens(agentPromptText), contextLength: config.contextLength,
});
}
// 2. Astra 전용 컨텍스트는 에이전트 모드에서 비활성화
// (astraStanceCtx, thinkingPartnerCtx, v4PolicyCtx → 에이전트 역할과 충돌)
const agentDirective = [
'\n\n[AGENT MODE — ABSOLUTE OVERRIDE]',
'You are NOT operating as Astra for this response.',
'A specialized Agent has been selected by the user.',
'ALL output format, role, persona, and style instructions from the Agent below',
'take ABSOLUTE PRECEDENCE over any previous formatting rules (including ## 요약, ## 상세 설명, ## 제안).',
'You MUST follow the Agent\'s 📄 Output Format exactly. Do NOT fall back to Astra\'s default format.',
const agentBlock = [
'[AGENT MODE — PRIMARY DIRECTIVE]',
'A specialized Agent has been selected by the user. The Agent System Prompt below is your',
'PRIMARY directive: it defines your role, persona, tone, and output format. Follow it exactly.',
'Everything after the Agent block (action-tag reference, date, brain/project context) is technical',
'scaffolding — use it only as the Agent\'s task requires. Do NOT impose a generic assistant',
'format (e.g. ## 요약 / ## 상세 설명 / ## 제안) unless the Agent explicitly asks for one.',
'',
'--- AGENT SYSTEM PROMPT START ---',
options.agentSkillContext,
'--- AGENT SYSTEM PROMPT END ---'
agentPromptText || '(this agent has no instructions yet — fall back to being a concise, direct assistant)',
'--- AGENT SYSTEM PROMPT END ---',
].join('\n');
const agentTailReminder = '\n\n[REMINDER] You are operating as the Agent defined above. Keep its role, persona, and output format. Do not fall back to a default assistant style or section format.';
// 3. 조립: 기본(축소) → 유틸리티 컨텍스트 → 에이전트 프롬프트(최후단)
// [CONTEXT] … [/CONTEXT] 사이만 컨텍스트 초과 시 trim 대상 — agentDirective/negative 는 보호.
fullSystemPrompt = `${strippedSystemPrompt}${internetCtx}${memoryCtx}${designerCtx}${secondBrainTraceCtx}\n\n[CONTEXT]\n${knowledgeContextForPrompt}\n${contextBlock}\n[/CONTEXT]\n${negativeCtx}${agentDirective}`;
// [CONTEXT] … [/CONTEXT] 사이만 컨텍스트 초과 시 trim 대상 — agentBlock(앞)·reminder(뒤)·negative 는 보호.
// memoryCtx(RAG/메모리/lessons)도 [CONTEXT] 안에 넣어 토큰이 빡빡할 때 대화 기록보다 먼저 잘리게 한다.
fullSystemPrompt = `${agentBlock}\n\n${strippedSystemPrompt}${internetCtx}${designerCtx}${secondBrainTraceCtx}\n\n[CONTEXT]\n${memoryCtx}\n${knowledgeContextForPrompt}\n${contextBlock}\n[/CONTEXT]\n${negativeCtx}${agentTailReminder}`;
} else {
// 기존 Astra 모드 (에이전트 미선택)
const localProjectKnowledgeCtx = prompt && localPathContext && this.isProjectKnowledgeCreationRequest(prompt)
@@ -530,7 +540,8 @@ export class AgentExecutor {
const casualCtx = isCasualConversation
? '\n\n[CASUAL CONVERSATION MODE]\nThe user sent a greeting, acknowledgement, or light conversational message. Reply naturally and briefly to the message itself. Do not use Second Brain, memory, project records, reports, references, or analysis unless the user explicitly asks for them.'
: '';
fullSystemPrompt = `${systemPrompt}${internetCtx}${memoryCtx}${designerCtx}${localProjectKnowledgeCtx}${thinkingPartnerCtx}${astraStanceCtx}${secondBrainTraceCtx}${v4PolicyCtx}${casualCtx}\n\n[CONTEXT]\n${knowledgeContextForPrompt}\n${contextBlock}\n[/CONTEXT]\n${negativeCtx}`;
// memoryCtx(RAG/메모리/lessons)는 [CONTEXT] 안에 — 토큰이 빡빡하면 대화 기록보다 먼저 잘림.
fullSystemPrompt = `${systemPrompt}${internetCtx}${designerCtx}${localProjectKnowledgeCtx}${thinkingPartnerCtx}${astraStanceCtx}${secondBrainTraceCtx}${v4PolicyCtx}${casualCtx}\n\n[CONTEXT]\n${memoryCtx}\n${knowledgeContextForPrompt}\n${contextBlock}\n[/CONTEXT]\n${negativeCtx}`;
}
// ──────────────────────────────────────────────────────────────────
// [Context Limit Manager] context length 는 "답변을 그만큼 길게 써도 된다"
@@ -980,8 +991,11 @@ export class AgentExecutor {
});
}
const outputTokens = estimateTokens(assistantContent);
const notice = shouldShowTruncationNotice(stopKind, outputTokens, maxOutputTokens)
? truncationNotice(stopKind)
// Show the "incomplete" notice when the engine said output-limit/context-overflow/error,
// OR when (after all auto-continuation rounds) the answer still plainly ends mid-sentence.
const notice =
shouldShowTruncationNotice(stopKind, outputTokens, maxOutputTokens) ? truncationNotice(stopKind)
: looksCutOff(assistantContent) ? truncationNotice('output-limit')
: '';
if (notice && assistantContent.trim()) {
assistantContent = assistantContent.trimEnd() + notice;
+38 -6
View File
@@ -125,8 +125,36 @@ export function shouldFinalOnlyRetry(cleaned: CleanedAssistantOutput): boolean {
}
/**
* Should we silently continue from where the answer was cut off? Only when it actually hit the
* output-token ceiling and we already have a non-trivial visible answer to continue from.
* Does the answer plainly end mid-sentence / mid-structure? Conservative — only flags *unambiguous*
* incompleteness (a complete Korean sentence may legitimately end without a period, so we never flag
* a plain syllable like `다`/`요`; we only flag connective particles, mid-English-words, mid-clause
* commas/colons, unclosed code fences/brackets, and dangling markdown bullets/headings).
*/
export function looksCutOff(text: string): boolean {
const t = (text || '').replace(/\s+$/, '');
if (t.length < 12) return false;
// unclosed code fence
if ((t.match(/```/g) || []).length % 2 === 1) return true;
// ends with an opening bracket / quote (unclosed pair)
if (/[([{“‘"'`]$/.test(t)) return true;
// dangling markdown bullet / heading / blockquote with no content after the marker
if (/(?:^|\n)\s*(?:[-*+]|#{1,6}|>|\d+\.)\s*$/.test(t)) return true;
// ends mid-English-word or mid-number
if (/[A-Za-z0-9]$/.test(t)) return true;
// ends mid-clause (comma / colon / semicolon / list separator)
if (/[,:;·、,]$/.test(t)) return true;
// ends with a Korean particle / connective ending that NEVER closes a sentence
if (/(?:으로|로서|로써|로|의|에서|에게|한테|에|을|를|과|와|이랑|랑|는|은|이|가|도|만|까지|부터|마다|조차|마저|밖에|뿐|처럼|같이|보다|이나|거나|든지|든가|고|며|면서|면|어서|아서|여서|니까|는데|은데|ㄴ데|지만|던|도록)$/.test(t)) return true;
return false;
}
/**
* Should we silently continue from where the answer was cut off? The point is to recover regardless
* of *why* it stopped, since local engines / SDKs often report the stop reason wrongly or not at all:
* - the engine said it hit the output cap (`output-limit`), OR
* - it generated close to the cap (a complete answer wouldn't dangle that early), OR
* - the visible answer plainly ends mid-sentence and the engine didn't give a clean "done" reason.
* Never continues from a too-short fragment, and never from a clean ending (terminal punctuation).
*/
export function shouldAutoContinue(
stopKind: GenerationStopKind,
@@ -134,10 +162,14 @@ export function shouldAutoContinue(
outputTokens: number,
maxOutputTokens: number
): boolean {
if (stopKind !== 'output-limit') return false;
if (!visibleAnswer || visibleAnswer.trim().length < 40) return false;
if (!Number.isFinite(maxOutputTokens) || maxOutputTokens <= 0) return true;
return outputTokens >= Math.floor(maxOutputTokens * 0.8);
const v = (visibleAnswer || '').trim();
if (v.length < 24) return false;
// These won't be fixed by generating more text — don't auto-continue.
if (stopKind === 'user-stopped' || stopKind === 'context-overflow' || stopKind === 'error' || stopKind === 'tool-calls') return false;
if (stopKind === 'output-limit') return true;
if (Number.isFinite(maxOutputTokens) && maxOutputTokens > 0 && outputTokens >= Math.floor(maxOutputTokens * 0.85)) return true;
// 'complete' (eosFound) or 'unknown' but the text is plainly unfinished → continue.
return looksCutOff(v);
}
/** Appended to the system prompt for a final-only retry — the previous reply was reasoning-only. */
+3 -1
View File
@@ -128,7 +128,9 @@ export class LMStudioStreamer implements IChatStreamer {
logInfo('LM Studio SDK chat stream finished.', { model: trimmedModel, stopReason, tokensYielded: yielded });
}
} catch { /* result unavailable on some SDK versions — non-fatal */ }
yield { token: '', stopReason: stopReason ?? 'eosFound' };
// Don't claim `eosFound` if we couldn't actually read the stop reason — leave it
// undefined so the caller treats it as 'unknown' (and its mid-sentence heuristics kick in).
yield { token: '', stopReason };
return;
}
+24 -15
View File
@@ -1789,22 +1789,31 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
this._currentSessionBrainId = selectedBrainId;
let agentSkillContext = undefined;
if (agentFile && fs.existsSync(agentFile)) {
agentSkillContext = fs.readFileSync(agentFile, 'utf8');
// Merge in any external skill .md files the user has mapped to this
// agent. We concatenate into the same agentSkillContext blob so the
// rest of the pipeline (agent.ts, agent-mode override) treats them
// identically to the agent's own .md — no further changes needed.
try {
const entry = getOrCreateAgentEntry(agentFile);
const bundle = loadExternalSkills(entry.skillFolders);
const block = formatSkillsAsPromptBlock(bundle);
if (block) {
agentSkillContext = `${agentSkillContext.trim()}\n\n${block}`;
if (agentFile && agentFile !== 'none' && fs.existsSync(agentFile)) {
const fileContent = fs.readFileSync(agentFile, 'utf8');
// Guard: a freshly-created agent still has only the placeholder template
// ("# Agent Persona: …\n\nAdd your instructions here…"). Treating that as a real
// agent prompt just confuses the model — fall back to normal mode and tell the user.
const body = fileContent.replace(/^?#\s*Agent\s*Persona\s*:.*$/im, '').trim();
const isPlaceholder = !body || /^add your instructions here/i.test(body);
if (isPlaceholder) {
logInfo('Selected agent has no real instructions — running without agent mode.', { agentFile });
this._view?.webview.postMessage({ type: 'lmStudioError', value: '선택한 에이전트에 내용이 없습니다 — 에이전트 프롬프트를 작성한 뒤 다시 시도하세요. (이번 응답은 에이전트 없이 처리합니다)' });
} else {
agentSkillContext = fileContent;
// Merge in any external skill .md files the user has mapped to this agent. We concatenate
// into the same agentSkillContext blob so the rest of the pipeline (agent.ts, agent-mode
// override) treats them identically to the agent's own .md — no further changes needed.
try {
const entry = getOrCreateAgentEntry(agentFile);
const bundle = loadExternalSkills(entry.skillFolders);
const block = formatSkillsAsPromptBlock(bundle);
if (block) {
agentSkillContext = `${agentSkillContext.trim()}\n\n${block}`;
}
} catch (e: any) {
logError('External skill load failed.', { error: e?.message || String(e) });
}
} catch (e: any) {
logError('External skill load failed.', { error: e?.message || String(e) });
}
}
+37 -4
View File
@@ -2,6 +2,7 @@ import {
extractVisibleFinal,
shouldFinalOnlyRetry,
shouldAutoContinue,
looksCutOff,
mergeContinuationParts,
buildContinuationUserPrompt,
} from '../src/core/responseRecovery';
@@ -74,14 +75,46 @@ describe('responseRecovery.extractVisibleFinal — thought quarantine', () => {
});
});
describe('responseRecovery.looksCutOff', () => {
it('flags answers that plainly end mid-sentence / mid-structure', () => {
expect(looksCutOff('당신은 복잡한 아이디어나 목표를 구체적인 실행 계획과 체계적인 문서화로')).toBe(true); // ends with the particle "로"
expect(looksCutOff('우리는 이 문제를 해결하기 위해 다음과 같은 단계를')).toBe(true); // ends with object marker "를"
expect(looksCutOff('the implementation is not yet complete and we need to')).toBe(true); // mid-English
expect(looksCutOff('the items are: foo, bar,')).toBe(true); // trailing comma
expect(looksCutOff('here is the code:\n```python\nprint("hi")')).toBe(true); // unclosed fence
expect(looksCutOff('정리하면 다음 항목들이 중요합니다:\n- 첫 번째 항목\n- 두 번째 항목\n- ')).toBe(true); // dangling bullet
});
it('does NOT flag complete-looking answers', () => {
expect(looksCutOff('이것은 완전히 끝난 답변이고 마침표도 붙어 있습니다.')).toBe(false);
expect(looksCutOff('이것은 마침표 없이 끝나는 한국어 문장입니다')).toBe(false); // ends with "다" — valid
expect(looksCutOff('네, 그렇게 하면 됩니다')).toBe(false);
expect(looksCutOff('done.')).toBe(false);
expect(looksCutOff('짧음')).toBe(false); // too short to judge
});
});
describe('responseRecovery.shouldAutoContinue', () => {
it('continues only when output-limit AND a real visible answer AND near the cap', () => {
it('continues when the engine reports the output cap was hit', () => {
expect(shouldAutoContinue('output-limit', 'x'.repeat(200), 3500, 4096)).toBe(true);
expect(shouldAutoContinue('output-limit', 'short', 4000, 4096)).toBe(false); // no real answer
expect(shouldAutoContinue('output-limit', 'x'.repeat(200), 100, 4096)).toBe(false); // didn't actually hit the cap
expect(shouldAutoContinue('complete', 'x'.repeat(200), 4000, 4096)).toBe(false);
expect(shouldAutoContinue('output-limit', 'x'.repeat(200), 50, 4096)).toBe(true); // engine said so → trust it
});
it('continues when generation reached ~the cap even if the engine said "complete"', () => {
expect(shouldAutoContinue('complete', 'x'.repeat(200), 4000, 4096)).toBe(true);
});
it('continues when the answer plainly ends mid-sentence (engine reason unclear)', () => {
expect(shouldAutoContinue('unknown', '당신은 복잡한 아이디어나 목표를 구체적인 실행 계획과 체계적인 문서화로', 60, 4096)).toBe(true);
expect(shouldAutoContinue('complete', 'the implementation continues here and we still need to', 100, 4096)).toBe(true);
});
it('does NOT continue from a tiny fragment or a complete-looking answer', () => {
expect(shouldAutoContinue('output-limit', 'short', 4000, 4096)).toBe(false); // < 24 chars
expect(shouldAutoContinue('complete', '이것은 완전히 끝난 답변이고 마침표도 붙어 있습니다.', 100, 4096)).toBe(false);
expect(shouldAutoContinue('unknown', '이것은 마침표 없이 끝나는 한국어 문장이고 충분히 길다고 본다', 100, 4096)).toBe(false);
});
it('does NOT continue on stop reasons that more text cannot fix', () => {
expect(shouldAutoContinue('context-overflow', 'x'.repeat(200), 4000, 4096)).toBe(false);
expect(shouldAutoContinue('error', 'x'.repeat(200), 4000, 4096)).toBe(false);
expect(shouldAutoContinue('user-stopped', 'x'.repeat(200), 4000, 4096)).toBe(false);
expect(shouldAutoContinue('tool-calls', 'x'.repeat(200), 4000, 4096)).toBe(false);
});
});