diff --git a/PATCHNOTES.md b/PATCHNOTES.md
index ae1e4c3..9cb9cfd 100644
--- a/PATCHNOTES.md
+++ b/PATCHNOTES.md
@@ -1,5 +1,23 @@
# Astra Patch Notes
+## v2.2.312 (2026-07-20)
+### ๐ ๋ณด๊ณ ํ์ง โ "## 4.๋ถํฐ ์์ํ๋ ๋ณด๊ณ ์" ๋ฒ๊ทธ + ์ผ๋ฐ๋ก ์ดํ ์ฐจ๋จ
+์ค์ฌ๋ก: "connectai ํ๋ก์ ํธ๋ฅผ ๋ถ์ํ๊ณ ๋ณด๊ณ ํด์ค" โ ๋ต๋ณ์ด "## 4. ์ข
ํฉ ๋ถ์ ๊ฒฐ๊ณผ"๋ถํฐ ์์(1~3 ์ฆ๋ฐ), ๋ด์ฉ์ ํ๋ก์ ํธ๋ช
์ ๋ฐ๊ฟ๋ ์ฑ๋ฆฝํ๋ 7์ค ์ผ๋ฐ๋ก .
+- **์ค๊ฐ ๋ผ์ด๋ ๋ณธ๋ฌธ ํ์ ๋ฒ๊ทธ ์์ (๊ทผ๋ณธ ์์ธ)**: ์ก์
๋ฃจํ์์ ์ก์
๊ณผ *ํจ๊ป* ๋ณธ๋ฌธ ์น์
์ ์ด ๋ผ์ด๋๋ ํ๋ฉด์ ํ ๋ฒ๋ ํ์๋์ง ์๊ณ (๋ผ์ด๋ธ ์คํธ๋ฆฌ๋ฐ์ depth 0 ์ ์ฉ) ์ต์ข
๋ผ์ด๋๋ง append ๋๋ค. ๋ชจ๋ธ์ ํ์คํ ๋ฆฌ์์ ์๊ธฐ ์ด์ ์น์
(1~3)์ ๋ณด๊ณ "## 4."๋ถํฐ ์ด์ด ์จ์ ์ฌ์ฉ์์๊ฒ 4๋ฒ๋ถํฐ ๋์ฐฉ. ์ด์ ์ค๊ฐ ๋ผ์ด๋์ ๊ฐ์ ๋ณธ๋ฌธ์ ์ก์
ํ๊ทธ๋ง ๊ฑท์ด๋ด๊ณ (์ ๊ท stripForDisplay.ts) ๋ฒ๋ธ์ ์์๋๋ก ํ์ํ๋ค.
+- **๋ถ์ ๋ณด๊ณ ์
๋ฌด ์ ํ ์ ์ค (requirementGraph)**: "๋ถ์ํ๊ณ ๋ณด๊ณ ํด์ค/ํํฉ ๋ณด๊ณ /๊ฒํ ๋ณด๊ณ "๊ฐ ์ด๋ค ์ ํ์๋ ์ ๊ฑธ๋ฆฌ๋ ๊ฐญ์ ๋ง์. ํ์ ์์: ๋ณด๊ณ ๊ฐ์(์ฒซ ์ค์ ๋์โ๊ทผ๊ฑฐโ๊ฒฐ๋ก ์๊ธฐ์ ์ธ), **ํ์ผ ๊ทผ๊ฑฐ**(์ฃผ์ฅ๋ง๋ค ์ค์ ์ฝ์ ํ์ผ ์ธ์ฉ, ์ผ๋ฐ๋ก ๊ธ์ง), ๊ตฌ์กฐยทํํฉ, ํต์ฌ ๋ฐ๊ฒฌ, ๋ค์ ๋จ๊ณ. ์ ํ ๊ฐ์ง ์ "์ต๋ 3์น์
" ์ผ๋ฐ ๊ท์น๋ณด๋ค ํ์ ์์ ์ปค๋ฒ ์ฐ์ ๋ช
์(๋ณด๊ณ ์๊ฐ ์์์ง๋ ์์ถฉ ์ ๊ฑฐ).
+- **๊ทผ๊ฑฐ ์๋ ๋ถ์ ๊ฐ์ง (Hollow ์ ๋ฐ๋ ๋ฐฉํฅ)**: ํ์ผ์ ์ฝ๊ณ ๋(read/investigate โฅ1) ๋ต๋ณ์ ํ์ผ ์ธ์ฉ์ด 2๊ฐ ๋ฏธ๋ง์ธ ์ฅ๋ฌธ ๋ถ์์ด๋ฉด "๊ทผ๊ฑฐ ์ธ์ฉ ์์" footer ๋ก ๊ฒฝ๊ณ โ ๊ทธ๋ด๋ฏํ ์ดํ์ด ๊ฒ์ฆ๋ ๋ถ์์ฒ๋ผ ํต๊ณผํ๋ ๊ฒ์ ์ฐจ๋จ.
+๊ฒ์ฆ: tsc ๋ฌด์ค๋ฅ + jest 954 ํต๊ณผ(์ ๊ท 13: analysis-report 3 + ungrounded 5 + stripForDisplay 5) + esbuild ์ ์.
+
+## v2.2.311 (2026-07-20)
+### โก ์๋ต ์ง์ฐ ๊ทผ๋ณธ ๊ฐ์ โ KV ์บ์ ์นํ ํ๋กฌํํธ + ์ 2๋ ์์ฃผ ์บ์
+์ค์ธก(usage.jsonl): ์ถ๋ ฅ 22ํ ํฐ ๋ต๋ณ์ 94.7์ด โ ์ง์ฐ์ 90%+ ๊ฐ ์์ฑ์ด ์๋๋ผ **๋งค ํด 13k+ ํ ํฐ ์ ์ฒด ์ฌํ๋ฆฌํ**์ด์๋ค. ์์ธ์ (a) ์์คํ
ํ๋กฌํํธ ์๋จ์ ๋ถ ๋จ์ ์๊ฐ์ด llama.cpp prompt cache ๋ฅผ ์ฒซ ํ ํฐ๋ถํฐ ๋ฌดํจํ, (b) ํด๋ง๋ค ๋ฐ๋๋ RAG/๋์ ๋ธ๋ก์ด ์ ๋ถ message[0] ์ ์์ด ํ์คํ ๋ฆฌ ์บ์๊น์ง ์ฐ์ ๋ฌดํจํ, (c) 32K ์ฐฝ์์ ๊ฒ์ ๊ฒฐ๊ณผ์๋ง 8k ํ ํฐ ์์ฐ, (d) continuation ๋ง๋ค ๋น ์ฟผ๋ฆฌ๋ก ์ฌ๊ฒ์(3~8์ด), (e) ๋งค ์ง์ 6,500 ํ์ผ ๋๋ ํฐ๋ฆฌ ์ํฌ + statSync.
+- **KV ์บ์ ๋ถ๋ฆฌ (`kvCachePromptSplit`, ๊ธฐ๋ณธ ON)**: message[0] ์ ๋ถ๋ณ ์ ์ ๋ณธ๋ฌธ(ํ๋ฅด์๋ยท๊ท์นยทํ๊ฒฝ)์ผ๋ก ๊ณ ์ ํ๊ณ , ๋ ์ง/RAG/[CONTEXT]/๋์ ๋ธ๋ก ์ ๋ถ๋ฅผ *๋ง์ง๋ง user ๋ฉ์์ง ์ง์ *์ internal system ๋ฉ์์ง๋ก ์ด๋. ํ๋ฆฌํฝ์ค(์ ์ ํ๋กฌํํธ+๊ณผ๊ฑฐ ํ์คํ ๋ฆฌ)๊ฐ ์บ์์์ ์ฌ์ฌ์ฉ๋์ด ์ฌํ๋ฆฌํ์ด "์ง์ ๊ตํ + ๋์ ์ปจํ
์คํธ"๋ก ์ค์ด๋ ๋ค. [CONTEXT] truncation ๋ tail ์ ์ ์ฉ. ์ปค์คํ
systemPrompt ํธ์ถ์(๋ฉํฐ์์ด์ ํธ ๋ฑ)๋ ์ข
์ ๊ฒฝ๋ก ์ ์ง.
+- **๊ฒ์ ํ ํฐ ์์ฐ ํ์คํ (`retrievalTokenBudget`, 0=์๋)**: ์๋๊ฐ์ ์ฐฝ์ 25%(8k~80k) โ 12%(2.5k~6k ํด๋จํ)๋ก. ๋งค ํด ์ฌํ๋ฆฌํ๋๋ tail ์ ์ง์ ์๊ฒ ๋ง๋ ๋ค.
+- **continuation ์ depth-0 memoryCtx ์ฌ์ฌ์ฉ**: ์ก์
๋ผ์ด๋๋ง๋ค์ ์ฌ๊ฒ์ ์ ๊ฑฐ โ ๋ผ์ด๋๋น 3~8์ด ์ ๊ฐ + ๋น ์ฟผ๋ฆฌ ์ฌ๊ฒ์์ผ๋ก ์ฒญํฌ๊ฐ ๊ฐ๋ฆฌ๋ ๋ฌธ์ ์ ๊ฑฐ + ํด ๋ด ํ๋กฌํํธ ์์ ํ.
+- **์ 2๋ ์์ฃผ ์บ์ (์ ๊ท brainWatch.ts)**: ๋ธ๋ ์ธ ๋ฃจํธ์ ์ฌ๊ท fs.watch ๋ฅผ ๊ฑธ๊ณ ์ธ๋ ์นด์ดํฐ๋ก ๋ฌดํจํ โ ๋ณ๊ฒฝ ์์ผ๋ฉด ๋๋ ํฐ๋ฆฌ ์ํฌ์ ํ์ผ๋ณ statSync(์์ฒ ํ)๋ฅผ ํต์งธ๋ก ์๋ตํ๊ณ ๋ฉ๋ชจ๋ฆฌ์ ํ์ผ ๋ชฉ๋กยทํ ํฐ ์ธ๋ฑ์ค๋ฅผ ์ ๋ขฐํ๋ค. "์ 2๋๋ฅผ ํญ์ ๋ฉ๋ชจ๋ฆฌ์" ์์ด๋์ด์ ์์ ํ ๊ตฌํ: ์๋ณธ ๋์ ๊ฒ์ ํ์๋ฌผ์ ์์ฃผ์ํค๊ณ ์์ฒ๊ฐ ์ ํฉ์ฑ์ ๋งก๋๋ค. ์์ ์งํ 3์ด ์ฐฝ์ ์ ๋ขฐ ๋ช
๋ถ์์ ์ ์ธ(์ด๋ฒคํธ ์ง์ฐ ๋ ์ด์ค ๊ฐ๋), ์์ฒ ๋ถ๊ฐ ํ๊ฒฝ์ ์๋์ผ๋ก ์ข
์ (๋งค๋ฒ stat) ํด๋ฐฑ. ํ์ฑํ ์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์๋ฐ์ผ๋ก ์ฒซ ์ง์์ ์ธ๋ฑ์ค ํ์ฑ ๋น์ฉ๋ ์ ๋ถ ์ฒ๋ฆฌ, ์ ํด ํด์ 30๋ถโ2์๊ฐ.
+- ๊ธฐ๋ ํจ๊ณผ: ๋์ผ ์ธ์
๋ฐ๋ณต ํด์ ํ๋ฆฌํ์ด 13k โ ์๋ฐฑ~2k ํ ํฐ ์์ค. LM Studio ์ชฝ GPU ์คํ๋ก๋/Flash Attention ์ค์ ๊ณผ ๋ณํ ๊ถ์ฅ.
+๊ฒ์ฆ: tsc ๋ฌด์ค๋ฅ + jest 941 ํต๊ณผ(์ ๊ท 15: split 6 + brainCache 4 + promptSplit 5) + esbuild ์ ์.
+
## v2.2.307 (2026-07-11)
### ๐๏ธ /benchmark ๋ ์ด์์ ๊ณจ๊ฒฉ ๋ถ์ + ํฉ์ฑ ์์ ํ
koritips ์ค์ธก ํผ๋๋ฐฑ ๋ฐ์: โ ํฉ์ฑ์ด ํํธ 2 ์์ ํ์์์("This operation was aborted")์ผ๋ก ์ฃฝ์ผ๋ฉด์ 105์ด ๊ฑธ๋ฆฐ ํํธ 1๊น์ง ํ๊ธฐ โก ๋จ์ ์์ ์์ฝ์ด ์ยทํฐํธ๋ฟ์ด๋ผ "๋ฐฐ๋๊ฐ ๋กค๋ง์ธ์ง, ๊ฐ ํ์ด์ง๊ฐ ์ด๋ค ๋ธ๋ก ์์์ธ์ง" ์ ๊ธธ์ด ์์์.
diff --git a/package.json b/package.json
index 6c469bd..dd6f01d 100644
--- a/package.json
+++ b/package.json
@@ -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.2.310",
+ "version": "2.2.312",
"publisher": "g1nation",
"license": "MIT",
"icon": "assets/icon.png",
@@ -1294,6 +1294,16 @@
"type": "string",
"default": "",
"markdownDescription": "์กฐ์ฌํ ์์ฒญ('~ํด๋ ์กฐ์ฌํด์ค') ์ ์ฉ ๋ชจ๋ธ ์ค๋ฒ๋ผ์ด๋. ์: 'claude-code:sonnet'. ์ํ ๋ก์ปฌ ๋ชจ๋ธ์ด ํ์ผ๋ช
๋ง ๋ณด๊ณ ๋ด์ฉ์ ์์ํ๋ ํ์กฐ์ฌ ๋ฐฉ์ง์ฑ
์ ํ๋ โ ๋น์ฐ๋ฉด ํ์ฌ ์ ํ ๋ชจ๋ธ ์ฌ์ฉ."
+ },
+ "g1nation.kvCachePromptSplit": {
+ "type": "boolean",
+ "default": true,
+ "markdownDescription": "**KV ์บ์ ์นํ ํ๋กฌํํธ ๋ถ๋ฆฌ** โ ์์คํ
ํ๋กฌํํธ๋ฅผ '๋ถ๋ณ ๋ณธ๋ฌธ(message[0])'๊ณผ 'ํด๋ณ ๋์ ์ปจํ
์คํธ(๋ง์ง๋ง ์ง๋ฌธ ์ง์ ์ฃผ์
)'๋ก ๋ถ๋ฆฌํด LM Studio/llama.cpp ์ prompt cache ํ๋ฆฌํฝ์ค ์ฌ์ฌ์ฉ์ ๊ทน๋ํ. ๋งค ํด ์ ์ฒด ์ฌํ๋ฆฌํ(1๋ง+ ํ ํฐ, ์์ญ ์ด)์ ์ง์ ๊ตํ ๋ถ๋์ผ๋ก ์ค์ธ๋ค. ํน์ ๋ชจ๋ธ ํ
ํ๋ฆฟ์์ ์๋ต ํ์ง ๋ฌธ์ ๊ฐ ๋ณด์ด๋ฉด ๋์ธ์."
+ },
+ "g1nation.retrievalTokenBudget": {
+ "type": "number",
+ "default": 0,
+ "markdownDescription": "**RAG ๊ฒ์ ํ ํฐ ์์ฐ ์ํ** โ ๋๋/๋ฉ๋ชจ๋ฆฌ ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ํ๋กฌํํธ์์ ์ฐจ์งํ ์ต๋ ํ ํฐ. `0 = ์๋`(์ปจํ
์คํธ ์ฐฝ์ 12%, 2,500~6,000 ํด๋จํ). ๊ฐ์ด ํด์๋ก ๊ทผ๊ฑฐ๋ ํ๋ถํด์ง์ง๋ง ๋ก์ปฌ ๋ชจ๋ธ ํ๋ฆฌํ์ด ๋๋ ค์ง๋ค."
}
}
}
diff --git a/src/agent.ts b/src/agent.ts
index 6e2925f..bb9e354 100644
--- a/src/agent.ts
+++ b/src/agent.ts
@@ -5,6 +5,8 @@ import * as fs from 'fs';
import {
findBrainFiles,
getSystemPrompt,
+ getStaticSystemPrompt,
+ getDateTimeContextBlock,
shouldAutoPushBrain,
buildApiUrl,
getActiveBrainProfile,
@@ -322,6 +324,14 @@ export class AgentExecutor {
actionStats: { reads: number; lists: number; investigates: number };
/** [v2.2.309] ์กฐ์ฌ ํด ๋ชจ๋ธ ์ค๋ฒ๋ผ์ด๋ โ depth 0 ์์ ๊ฒฐ์ , continuation ์๋ ์ ์ง. */
investigationModelOverride: string | null;
+ /**
+ * [v2.2.311] depth 0 ์์ ๋น๋ํ memoryCtx ๋ฌธ์์ด ์บ์ โ continuation depth ๋
+ * ์ฌ๊ฒ์ํ์ง ์๊ณ ์ด๊ฑธ ์ฌ์ฌ์ฉํ๋ค. ์ข
์ ์ depth ๋ง๋ค buildMemoryContext ๋ฅผ ๋ค์
+ * ๋๋ ธ๋๋ฐ, (a) ๊ฒ์ 3~8์ด๊ฐ ๋ผ์ด๋๋ง๋ค ์ถ๊ฐ๋๊ณ (b) continuation ์ prompt ๋
+ * null ์ด๋ผ *๋น ์ฟผ๋ฆฌ๋ก* ์ฌ๊ฒ์ํด ์๋ฑํ ์ฒญํฌ๋ก ๊ฐ์๋ผ์ฐ๊ณ (c) ํ๋กฌํํธ๊ฐ ํ๋ค๋ ค
+ * KV ์บ์๋ ๊นจ์ก๋ค. actionStats ์ ๊ฐ์ ์ด์ ๋ก depth 0 ์ง์
๋ถ์์๋ง ์ด๊ธฐํ.
+ */
+ memoryCtxCache: string | null;
} = {
retrieval: null,
lessons: [],
@@ -331,6 +341,7 @@ export class AgentExecutor {
confidenceSignals: null,
actionStats: { reads: 0, lists: 0, investigates: 0 },
investigationModelOverride: null,
+ memoryCtxCache: null,
};
/** Per-turn state ์ผ๊ด ์ ๋ฆฌ. turn ์์/abort/load session ์ ํธ์ถ. */
@@ -560,6 +571,7 @@ export class AgentExecutor {
// [v2.2.309] turn ์ ์ฒด(๋ชจ๋ depth) ๋์ ์ํ โ depth 0 ์์๋ง ์ด๊ธฐํ.
this._turnCtx.actionStats = { reads: 0, lists: 0, investigates: 0 };
this._turnCtx.investigationModelOverride = null;
+ this._turnCtx.memoryCtxCache = null;
}
// 1. Prepare Context
@@ -755,22 +767,29 @@ export class AgentExecutor {
? `\n\n${renderSecondBrainTraceContext(secondBrainTrace)}`
: '';
const retrievalStartMs = Date.now();
- const memoryCtx = isCasualConversation
- ? ''
- : await (async () => {
- this.resetTurnContext();
- return buildMemoryContextFn({
- currentPrompt: prompt || '',
- activeBrain,
- agentSkillFile: options.agentSkillFile,
- chatHistory: this.chatHistory,
- memoryManager: this.memoryManager,
- retrievalOrchestrator: this.retrievalOrchestrator,
- context: this.context,
- currentTaskId: this.currentTaskId,
- turnCtx: this._turnCtx,
- });
- })();
+ // [v2.2.311] continuation depth ๋ depth 0 ์ memoryCtx ๋ฅผ ์ฌ์ฌ์ฉ โ ์ฌ๊ฒ์
+ // 3~8์ด ์ ๊ฑฐ + ๋น ์ฟผ๋ฆฌ ์ฌ๊ฒ์์ผ๋ก ์ฒญํฌ๊ฐ ๊ฐ๋ฆฌ๋ ๋ฌธ์ ์ ๊ฑฐ + ํ๋กฌํํธ ์์ ํ
+ // (dynamicBlocks ๋ฑ turnCtx ํ์๋ฌผ๋ depth 0 ๊ฒ์ด ๊ทธ๋๋ก ์ ์ง๋๋ค).
+ let memoryCtx: string;
+ if (isCasualConversation) {
+ memoryCtx = '';
+ } else if (loopDepth > 0 && this._turnCtx.memoryCtxCache !== null) {
+ memoryCtx = this._turnCtx.memoryCtxCache;
+ } else {
+ this.resetTurnContext();
+ memoryCtx = await buildMemoryContextFn({
+ currentPrompt: prompt || '',
+ activeBrain,
+ agentSkillFile: options.agentSkillFile,
+ chatHistory: this.chatHistory,
+ memoryManager: this.memoryManager,
+ retrievalOrchestrator: this.retrievalOrchestrator,
+ context: this.context,
+ currentTaskId: this.currentTaskId,
+ turnCtx: this._turnCtx,
+ });
+ this._turnCtx.memoryCtxCache = memoryCtx;
+ }
if (loopDepth === 0 && !isCasualConversation && this._turnCtx.retrieval) {
recordTelemetry({
kind: 'retrieval',
@@ -806,9 +825,19 @@ export class AgentExecutor {
? buildPriorTurnConclusionContext(this.chatHistory)
: '';
// System prompt build (agent vs astra mode) โ src/agent/handlePrompt/{buildAgentModeSystemPrompt,buildAstraModeSystemPrompt}.ts
- const fullSystemPrompt: string = isAgentMode
+ //
+ // [KV ์บ์ ๋ถ๋ฆฌ v2.2.311] ๊ธฐ๋ณธ ๊ฒฝ๋ก(ํธ์ถ์๊ฐ systemPrompt ๋ฅผ ๋๊ธฐ์ง ์์ ๊ฒฝ์ฐ)๋
+ // message[0] ์ *์ ์ ๋ณธ๋ฌธ๋ง* ์ผ๋ก ๊ณ ์ ํ๊ณ , ๋ ์ง/RAG/[CONTEXT]/๋์ ๋ธ๋ก ์ ๋ถ๋ฅผ
+ // dynamicContextTail ๋ก ๋ถ๋ฆฌํด computeBudgetedRequest ๊ฐ ๋ง์ง๋ง user ๋ฉ์์ง ์ง์ ์
+ // ์ฝ์
ํ๋ค. llama.cpp prompt cache ๊ฐ ์ ์ ํ๋กฌํํธ+๊ณผ๊ฑฐ ํ์คํ ๋ฆฌ๋ฅผ ์ฌ์ฌ์ฉํ๊ฒ ๋์ด
+ // ๋งค ํด ์ ์ฒด ์ฌํ๋ฆฌํ(์ค์ธก 13k ํ ํฐ โ 90์ด)์ด "์ง์ ๊ตํ + tail" ๋ก ์ค์ด๋ ๋ค.
+ // ์ปค์คํ
systemPrompt ํธ์ถ์(๋ฉํฐ์์ด์ ํธ ๋ฑ)๋ ์ข
์ ๋จ์ผ-์์คํ
๊ฒฝ๋ก ์ ์ง.
+ const kvSplitEnabled = getConfig().kvCachePromptSplit !== false
+ && options.systemPrompt === undefined;
+ const builderBasePrompt = kvSplitEnabled ? '' : systemPrompt;
+ const builtSystemPrompt: string = isAgentMode
? buildAgentModeSystemPrompt({
- systemPrompt,
+ systemPrompt: builderBasePrompt,
agentSkillContext: options.agentSkillContext || '',
modeBridgeCtx,
priorConclusionCtx,
@@ -824,7 +853,7 @@ export class AgentExecutor {
})
: buildAstraModeSystemPrompt({
prompt,
- systemPrompt,
+ systemPrompt: builderBasePrompt,
modeBridgeCtx,
priorConclusionCtx,
designerCtx,
@@ -839,6 +868,12 @@ export class AgentExecutor {
knowledgeMix: this._turnCtx.knowledgeMix,
dynamicBlocks: this._turnCtx.dynamicBlocks,
});
+ // Split ๋ชจ๋: head = ์ ์ ํ๋กฌํํธ(๋ถ๋ณ), tail = ๋ ์ง + ๋น๋ ์ฐ์ถ(๋์ ์ ๋ถ).
+ // Legacy ๋ชจ๋: ์ข
์ ๊ทธ๋๋ก head ์ ์ ๋ถ.
+ const fullSystemPrompt: string = kvSplitEnabled ? getStaticSystemPrompt() : builtSystemPrompt;
+ const dynamicContextTail: string | undefined = kvSplitEnabled
+ ? `${getDateTimeContextBlock()}${builtSystemPrompt}`
+ : undefined;
// Context budget computation โ src/agent/handlePrompt/computeBudgetedRequest.ts
const imageCount = (reqMessages as any[])
.reduce((n, m) => n + (Array.isArray(m?.images) ? m.images.length : 0), 0);
@@ -874,7 +909,7 @@ export class AgentExecutor {
const lastUserIdx = reqMessages.map((m) => m.role).lastIndexOf('user');
const lastUser = lastUserIdx >= 0 ? reqMessages[lastUserIdx] : undefined;
const content = typeof lastUser?.content === 'string' ? lastUser.content : '';
- const sysTokens = estimateTokens(fullSystemPrompt) + 4;
+ const sysTokens = estimateTokens(fullSystemPrompt) + (dynamicContextTail ? estimateTokens(dynamicContextTail) : 0) + 4;
const mrCfg = {
enabled: true,
triggerRatio: config.mapReduceTriggerRatio,
@@ -934,6 +969,7 @@ export class AgentExecutor {
const _budget = computeBudgetedRequest({
fullSystemPrompt,
+ dynamicContextTail,
reqMessages,
actualModel,
config,
@@ -1462,6 +1498,22 @@ export class AgentExecutor {
await this.context.workspaceState.update('lastActionStr', currentActionStr);
logInfo('Autonomous loop continuing after actions.', { loopDepth: loopDepth + 1, actions: report });
+ // [v2.2.312] ์ค๊ฐ ๋ผ์ด๋ ๋ณธ๋ฌธ ํ์ โ ์ก์
๊ณผ *ํจ๊ป* ์์ฑ๋ ์น์
์ด ํ๋ฉด์์
+ // ์ฆ๋ฐํ๋ ๋ฒ๊ทธ ์์ . ์ข
์ ์ ์ก์
์ด ์๋ ๋ผ์ด๋๋ ์ฌ๊ธฐ์ return ํ๋ฉฐ ๋ณธ๋ฌธ์
+ // ํ ๋ฒ๋ webview ์ ๋ณด๋ด์ง ์์๊ณ (ํ์๋ ๋ผ์ด๋ธ ์คํธ๋ฆฌ๋ฐ๋ฟ โ depth 0 ์ ์ฉ),
+ // ์ต์ข
๋ผ์ด๋๋ง streamChunk ๋ก ๋ถ์๋ค. ๊ทธ ๊ฒฐ๊ณผ ๋ชจ๋ธ์ด ํ์คํ ๋ฆฌ์์ ์๊ธฐ ์ด์
+ // ์น์
(1~3)์ ๋ณด๊ณ "## 4."๋ถํฐ ์ด์ด ์จ์, ์ฌ์ฉ์์๊ฒ๋ 4๋ฒ๋ถํฐ ์์ํ๋
+ // ๋ณด๊ณ ์๊ฐ ๋์ฐฉํ๋ค (์ค์ฌ๋ก). ๋ผ์ด๋ธ๋ก ์ด๋ฏธ ํ์๋ depth 0 ๋ ์ค๋ณต ๋ฐฉ์ง๋ก ์ ์ธ.
+ if (loopDepth > 0 || !postLiveDeltas) {
+ try {
+ const { stripActionTagsForDisplay } = await import('./agent/actions/stripForDisplay');
+ const roundVisible = stripActionTagsForDisplay(finalAssistantContent);
+ if (roundVisible) {
+ this.webview.postMessage({ type: 'streamChunk', value: `${loopDepth > 0 ? '\n\n' : ''}${roundVisible}` });
+ }
+ } catch { /* ํ์ ์คํจ๊ฐ ๋ฃจํ๋ฅผ ๋ง์ง ์์ */ }
+ }
+
// Explicitly tell the AI to look at the results and continue
const continuationPrompt = `The requested local action has been executed.\nAction report:\n${report.join('\n')}\nUse the action result messages already in the conversation to answer the user's original request directly, in the user's language. Do not say you are waiting for the next instruction.`;
@@ -1554,6 +1606,14 @@ export class AgentExecutor {
if (hollowInv.hollow) {
this.webview.postMessage({ type: 'streamChunk', value: formatHollowInvestigationFooter(hollowInv.fileMentions) });
logInfo('Hollow Investigation ๊ฐ์ง (continuation).', { files: hollowInv.fileMentions, stats: this._turnCtx.actionStats });
+ } else {
+ // [v2.2.312] ๋ฐ๋ ๋ฐฉํฅ โ ํ์ผ์ ์ฝ๊ณ ๋ ๊ทผ๊ฑฐ ์ธ์ฉ ์๋ ์ผ๋ฐ๋ก ๋ถ์ ๊ฒฝ๊ณ .
+ const { detectUngroundedAnalysis, formatUngroundedAnalysisFooter } = await import('./intelligence/investigationPipeline');
+ const ug = detectUngroundedAnalysis(finalAssistantContent, this._turnCtx.actionStats);
+ if (ug.ungrounded) {
+ this.webview.postMessage({ type: 'streamChunk', value: formatUngroundedAnalysisFooter(ug.readCount) });
+ logInfo('Ungrounded Analysis ๊ฐ์ง (continuation).', { readCount: ug.readCount, fileRefs: ug.fileRefs });
+ }
}
} catch { /* ๊ฐ์ง ์คํจ๊ฐ ๋ต๋ณ์ ๋ง์ง ์์ */ }
}
diff --git a/src/agent/actions/stripForDisplay.ts b/src/agent/actions/stripForDisplay.ts
new file mode 100644
index 0000000..d5e23e0
--- /dev/null
+++ b/src/agent/actions/stripForDisplay.ts
@@ -0,0 +1,31 @@
+/**
+ * [v2.2.312] ํ๋ฉด ํ์์ฉ ์ก์
ํ๊ทธ ์ ๊ฑฐ.
+ *
+ * ์ก์
๋ฃจํ์ ์ค๊ฐ ๋ผ์ด๋(์ก์
๊ณผ ํจ๊ป ๋ณธ๋ฌธ ์น์
์ ์ด ๋ผ์ด๋)๋ฅผ ์ฌ์ฉ์ ๋ฒ๋ธ์
+ * ํ์ํ ๋, ๊ฐ์ ์คํ ํ๊ทธ ์๋ฌธ์ด ๊ทธ๋๋ก ๋
ธ์ถ๋์ง ์๋๋ก ๊ฑท์ด๋ธ๋ค.
+ * ์คํ ์์ฒด๋ ์ด ํจ์์ ๋ฌด๊ด โ executeActions ๋ ์๋ฌธ(cleanedVisible)์ ๊ทธ๋๋ก
+ * ๋ฐ๋๋ค. ์ฌ๊ธฐ๋ *ํ์ ์ ์ฉ* ๊ฒฝ๋ก๋ค.
+ */
+
+const ACTION_TAG_NAMES = [
+ 'read_file', 'list_files', 'investigate_files',
+ 'create_file', 'edit_file', 'delete_file',
+ 'run_command', 'run_code',
+ 'read_brain', 'list_brain',
+ 'read_sheet', 'write_sheet', 'append_sheet',
+ 'fetch_url', 'calculate',
+ 'create_calendar_event', 'add_task', 'update_task', 'complete_task',
+ // edit_file ๋ด๋ถ ์๋ธ ํ๊ทธ โ ๋ธ๋ก ์ ๊ฑฐ๊ฐ ์คํจํ ์์ฌ๊น์ง ์ ๋ฆฌ.
+ 'search', 'replace',
+];
+
+export function stripActionTagsForDisplay(text: string): string {
+ let t = text || '';
+ for (const name of ACTION_TAG_NAMES) {
+ // ์๊ธฐ๋ซํ โ ๋ธ๋ก ... โ ์์ฌ ์ฌ๋/๋ซ๋ ํ๊ทธ ์.
+ t = t.replace(new RegExp(`<${name}\\b[^>]*/>`, 'gi'), '');
+ t = t.replace(new RegExp(`<${name}\\b[^>]*>[\\s\\S]*?${name}>`, 'gi'), '');
+ t = t.replace(new RegExp(`?${name}\\b[^>]*>`, 'gi'), '');
+ }
+ return t.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
+}
diff --git a/src/agent/handlePrompt/computeBudgetedRequest.ts b/src/agent/handlePrompt/computeBudgetedRequest.ts
index 641a276..03401ee 100644
--- a/src/agent/handlePrompt/computeBudgetedRequest.ts
+++ b/src/agent/handlePrompt/computeBudgetedRequest.ts
@@ -13,6 +13,14 @@ import { buildDroppedHistorySummary } from '../../lib/contextBuilders/droppedHis
export interface ComputeBudgetedRequestInput {
fullSystemPrompt: string;
+ /**
+ * [KV ์บ์ ๋ถ๋ฆฌ v2.2.311] ํด๋ง๋ค ๋ณํ๋ ์ปจํ
์คํธ(RAG/[CONTEXT]/๋ ์ง/๋์ ๋ธ๋ก)๋ฅผ
+ * message[0] ์ด ์๋๋ผ *๋ง์ง๋ง user ๋ฉ์์ง ์ง์ *์ internal system ๋ฉ์์ง๋ก ์ฃ๋๋ค.
+ * message[0](fullSystemPrompt)์ ํด ์ฌ์ด์ ๋ถ๋ณ โ llama.cpp prompt cache ๊ฐ
+ * "์ ์ ํ๋กฌํํธ + ๊ณผ๊ฑฐ ํ์คํ ๋ฆฌ" ํ๋ฆฌํฝ์ค๋ฅผ ์ฌ์ฌ์ฉํ๊ณ , ๋งค ํด ํ๋ฆฌํ์
+ * "์ง์ ๊ตํ + ์ด ๋ธ๋ก"๋ง์ผ๋ก ์ค์ด๋ ๋ค. undefined ๋ฉด ์ข
์ ๋จ์ผ-์์คํ
๋์.
+ */
+ dynamicContextTail?: string;
/** Caller is expected to have run `capChatHistory` on this already. */
reqMessages: ChatMessage[];
actualModel: string;
@@ -126,12 +134,27 @@ export function computeBudgetedRequest(input: ComputeBudgetedRequestInput): Comp
1024,
Math.floor((ctxLimits.contextLength - ctxLimits.safetyMargin - preferredOutputReserve - imageTokenReserve) * 0.65)
);
- const { prompt: budgetedSystemPrompt, truncated: systemTruncated } =
- truncateSystemPromptContext(fullSystemPrompt, systemCapTokens);
+ // Split ๋ชจ๋๋ฉด [CONTEXT] ๋ dynamicContextTail ์ชฝ์ ์์ผ๋ฏ๋ก truncation ๋ tail ์ ์ ์ฉ.
+ // (์ ์ head ์๋ [CONTEXT] ๋ง์ปค๊ฐ ์์ด truncate ๊ฐ no-op โ head ๋ ๊ทธ๋๋ก ๋๋ค.)
+ const splitMode = typeof input.dynamicContextTail === 'string' && input.dynamicContextTail.trim().length > 0;
+ let budgetedSystemPrompt = fullSystemPrompt;
+ let budgetedTail = splitMode ? input.dynamicContextTail! : '';
+ let systemTruncated = false;
+ if (splitMode) {
+ const headTokens = estimateTokens(fullSystemPrompt);
+ const tailCap = Math.max(512, systemCapTokens - headTokens);
+ const t = truncateSystemPromptContext(budgetedTail, tailCap);
+ budgetedTail = t.prompt;
+ systemTruncated = t.truncated;
+ } else {
+ const t = truncateSystemPromptContext(fullSystemPrompt, systemCapTokens);
+ budgetedSystemPrompt = t.prompt;
+ systemTruncated = t.truncated;
+ }
if (systemTruncated) {
logInfo('System prompt context truncated to fit the context window.', { model: actualModel, systemCapTokens });
}
- const systemTokens = estimateTokens(budgetedSystemPrompt) + 4;
+ const systemTokens = estimateTokens(budgetedSystemPrompt) + (splitMode ? estimateTokens(budgetedTail) + 4 : 0) + 4;
// (2) ๋ํ ๊ธฐ๋ก ์์ถ.
const historyBudget = Math.max(
@@ -156,9 +179,27 @@ export function computeBudgetedRequest(input: ComputeBudgetedRequestInput): Comp
}
}
+ // Split ๋ชจ๋: ๋์ ์ปจํ
์คํธ๋ฅผ ๋ง์ง๋ง user ๋ฉ์์ง *์ง์ *์ ์ฝ์
. ์ด ์์น๋ผ์ผ
+ // (a) ์ด์ ์์ฒญ๊ณผ์ ๊ณตํต ํ๋ฆฌํฝ์ค(์ ์ ์์คํ
+ ๊ณผ๊ฑฐ ํ์คํ ๋ฆฌ)๊ฐ ์ต๋๋ก ๋ณด์กด๋๊ณ
+ // (b) continuation(์ก์
๊ฒฐ๊ณผ๊ฐ ๋ค์ ๋ถ๋ ๋ผ์ด๋)์์๋ ๊ฐ์ ์๋ฆฌ๋ผ ํด ๋ด ์บ์๊ฐ ์ ์ง๋๋ค.
+ // user ๋ฉ์์ง๊ฐ ์์ผ๋ฉด(์ด๋ก ์) ํ์คํ ๋ฆฌ ๋์ ๋ถ์ธ๋ค โ ์์ฑ ์ง์ ์ด๋ฏ๋ก ์ฌ์ ํ ์ ํจ.
+ let historyWithTail = budgetedHistory;
+ if (splitMode) {
+ const tailMsg: ChatMessage = { role: 'system', content: budgetedTail, internal: true };
+ const lastUserIdx = (() => {
+ for (let i = budgetedHistory.length - 1; i >= 0; i--) {
+ if (budgetedHistory[i].role === 'user') return i;
+ }
+ return -1;
+ })();
+ historyWithTail = lastUserIdx >= 0
+ ? [...budgetedHistory.slice(0, lastUserIdx), tailMsg, ...budgetedHistory.slice(lastUserIdx)]
+ : [...budgetedHistory, tailMsg];
+ }
+
const messagesForRequest: ChatMessage[] = [
{ role: 'system', content: budgetedSystemPrompt, internal: true },
- ...budgetedHistory
+ ...historyWithTail
];
// (3) ๋์ ์ถ๋ ฅ ์ํ.
diff --git a/src/config.ts b/src/config.ts
index 2ef55f3..b7761e2 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -233,6 +233,19 @@ export interface IAgentConfig {
* ํ ๋ฃจ์๋ค์ด์
๋์ฑ
. ๋น ๋ฌธ์์ด์ด๋ฉด ๋นํ์ฑ (ํ์ฌ ์ ํ ๋ชจ๋ธ ๊ทธ๋๋ก).
*/
investigationModel: string;
+ /**
+ * [v2.2.311] KV ์บ์ ์นํ ํ๋กฌํํธ ๋ถ๋ฆฌ โ message[0] ์ ์ ์ ๋ณธ๋ฌธ์ผ๋ก ๊ณ ์ ํ๊ณ
+ * ๋ ์ง/RAG/[CONTEXT]/๋์ ๋ธ๋ก์ ๋ง์ง๋ง user ๋ฉ์์ง ์ง์ ์ system ๋ฉ์์ง๋ก ์ฎ๊ธด๋ค.
+ * llama.cpp prompt cache ํ๋ฆฌํฝ์ค ์ฌ์ฌ์ฉ์ผ๋ก ํด๋น ์ฌํ๋ฆฌํ์ ๋ํญ ์ถ์. ๊ธฐ๋ณธ true.
+ * ํน์ ๋ชจ๋ธ ํ
ํ๋ฆฟ์์ ๋ฌธ์ ์ false ๋ก ์ข
์ ๋จ์ผ-์์คํ
ํ๋กฌํํธ ๋ณต๊ท.
+ */
+ kvCachePromptSplit: boolean;
+ /**
+ * [v2.2.311] RAG/๋ฉ๋ชจ๋ฆฌ ๊ฒ์ ํ ํฐ ์์ฐ ์ํ. 0 = ์๋(์ปจํ
์คํธ ์ฐฝ์ 12%,
+ * 2,500~6,000 ํด๋จํ). ์ข
์ ์๋๊ฐ(์ฐฝ์ 25%, ์ต๋ 80k)์ 32k ์ฐฝ์์ 8k ํ ํฐ์
+ * ๊ฒ์ ๊ฒฐ๊ณผ์ ์จ์ ๋ก์ปฌ ํ๋ฆฌํ๋ง ์์ญ ์ด๋ฅผ ํ์ ๋ค โ ์ํ์ ๋ฎ์ถฐ ๋ต๋ณ ์ง์ฐ์ ์ค์ธ๋ค.
+ */
+ retrievalTokenBudget: number;
/** Self-check ์ ์ฉ ๋ชจ๋ธ ID. ๋น๋ฉด defaultModel. ๋น ๋ฅธ ์์ ๋ชจ๋ธ ๊ถ์ฅ. */
selfCheckModel: string;
/** Self-check LLM ํธ์ถ ํ์์์ (์ด). ๊ธฐ๋ณธ 6. */
@@ -583,6 +596,8 @@ export function getConfig(): IAgentConfig {
intentClarificationStrictness: (cfg.get('intentClarificationStrictness', 'medium') as 'low' | 'medium' | 'high') || 'medium',
citationTraceEnabled: cfg.get('citationTraceEnabled', true),
investigationModel: (cfg.get('investigationModel', '') || '').trim(),
+ kvCachePromptSplit: cfg.get('kvCachePromptSplit', true),
+ retrievalTokenBudget: Math.max(0, cfg.get('retrievalTokenBudget', 0) || 0),
selfCheckEnabled: cfg.get('selfCheckEnabled', false),
selfCheckModel: cfg.get('selfCheckModel', '') || '',
selfCheckTimeoutSec: Math.max(1, Math.min(60, cfg.get('selfCheckTimeoutSec', 6))),
diff --git a/src/extension.ts b/src/extension.ts
index 1117d38..e823dd3 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -86,6 +86,17 @@ export async function activate(context: vscode.ExtensionContext) {
// ๋๋์ ๊ธฐ๋ก. ์๊ธฐ ํ๊ฐ๊ฐ ๊ตฌ์ ์ค๋
์ท ๋์ ํญ์ ํํ ์์ค๋ฅผ ๊ทผ๊ฑฐ๋ก ํ๊ฒ ํ๋ค.
void ensureFeatureInventory(context);
+ // [v2.2.311] ์ 2๋ ์บ์ ์๋ฐ โ ํ์ฑํ ์งํ ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ํ์ผ ๋ชฉ๋ก + ํ ํฐ ์ธ๋ฑ์ค
+ // (์์ญ MB JSON)๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ์ฌ๋ ค, ์ธ์
์ฒซ ์ง์๊ฐ ๊ทธ ๋น์ฉ(์ ์ด)์ ๋ค์ง์ด์ฐ์ง
+ // ์๊ฒ ํ๋ค. ์์ฒ๊ฐ ๊ฐ์ด ๊ฑธ๋ ค ์ดํ ํด์ ๋ณ๊ฒฝ ์์ผ๋ฉด ์ํฌ/stat ์ ํต์งธ๋ก ์๋ต.
+ try {
+ const { getActiveBrainProfile } = require('./utils') as typeof import('./utils');
+ const { warmBrainCache } = require('./retrieval/brainWatch') as typeof import('./retrieval/brainWatch');
+ const { getBrainTokenIndex } = require('./retrieval/brainIndex') as typeof import('./retrieval/brainIndex');
+ const brainPath = getActiveBrainProfile()?.localBrainPath;
+ if (brainPath) warmBrainCache(brainPath, (files) => { getBrainTokenIndex(brainPath, files); });
+ } catch { /* ์๋ฐ ์คํจ๋ ๋น์น๋ช
โ ์ฒซ ์ง์๊ฐ ์ข
์ ๊ฒฝ๋ก๋ก ์ฒ๋ฆฌ */ }
+
// Initialize Astra Path Resolver (.astra โ ConnectAI/.astra/)
initAstraPathResolver(context);
diff --git a/src/intelligence/investigationPipeline.ts b/src/intelligence/investigationPipeline.ts
index f851d02..885c009 100644
--- a/src/intelligence/investigationPipeline.ts
+++ b/src/intelligence/investigationPipeline.ts
@@ -226,3 +226,31 @@ export function formatHollowInvestigationFooter(fileMentions: string[]): string
'๋ด์ฉ ๊ฒ์ฆ์ด ํ์ํ๋ฉด ๋ค์ ์์ฒญํด ์ฃผ์ธ์ โ ๋ค์ ํด์์ ``๋ก ๊ฐ ํ์ผ์ ์ค์ ๋ก ์ฝ๊ณ ๋ตํฉ๋๋ค.',
].join('\n');
}
+
+/**
+ * [v2.2.312] ํ์กฐ์ฌ์ ๋ฐ๋ ๋ฐฉํฅ ์คํจ โ ํ์ผ์ *์ฝ๊ณ ๋* ๋ต๋ณ์ ํ์ผ ๊ทผ๊ฑฐ๋ฅผ ํ๋๋
+ * ์ธ์ฉํ์ง ์์ "์ผ๋ฐ๋ก ๋ถ์" ๊ฐ์ง. ์ค์ฌ๋ก: ํ๋ก์ ํธ ๋ถ์ ๋ณด๊ณ ๊ฐ 7์ค์ง๋ฆฌ
+ * ํ๋ก์ ํธ๋ช
๋ง ๋ฐ๊ฟ๋ ์ฑ๋ฆฝํ๋ ์ดํ("~๊ฐ ์ธ์์ ์
๋๋ค")์ผ๋ก ๋๋จ.
+ * ์กฐ๊ฑด: read/investigate ๋ฅผ 1ํ ์ด์ ํ๊ณ , ๋ต๋ณ์ด ์ค์ง ๋ถ๋(300์+)์ธ๋ฐ
+ * ํ์ผ ์ฐธ์กฐ๊ฐ 2๊ฐ ๋ฏธ๋ง โ ungrounded. (์งง์ ํ์ธํ ์๋ต์ ์คํ ๋ฐฉ์ง๋ก ์ ์ธ.)
+ */
+export function detectUngroundedAnalysis(
+ answer: string,
+ stats: TurnActionStats | null | undefined,
+): { ungrounded: boolean; readCount: number; fileRefs: number } {
+ const readCount = (stats?.reads ?? 0) + (stats?.investigates ?? 0);
+ if (!stats || readCount === 0) return { ungrounded: false, readCount, fileRefs: 0 };
+ const body = (answer || '').trim();
+ if (body.length < 300) return { ungrounded: false, readCount, fileRefs: 0 };
+ const refs = new Set(Array.from(body.matchAll(FILE_MENTION_RE), m => m[0]));
+ return { ungrounded: refs.size < 2, readCount, fileRefs: refs.size };
+}
+
+export function formatUngroundedAnalysisFooter(readCount: number): string {
+ return [
+ '',
+ '---',
+ `โ ๏ธ **๊ทผ๊ฑฐ ์ธ์ฉ ์์** โ ํ์ผ์ ${readCount}ํ ์ฝ๊ณ ๋ ๋ต๋ณ์ ์ค์ ํ์ผยท์ฝ๋ ๊ทผ๊ฑฐ๊ฐ ์ธ์ฉ๋์ง ์์์ต๋๋ค. ์ ๋ด์ฉ์ ์ผ๋ฐ๋ก ์ผ ์ ์์ต๋๋ค.`,
+ '๊ตฌ์ฒด ๊ทผ๊ฑฐ๊ฐ ํ์ํ๋ฉด "์ค์ ์ฝ์ ํ์ผ๋ช
์ ์ธ์ฉํด์ ๋ค์ ์ ๋ฆฌํด์ค"๋ผ๊ณ ์์ฒญํด ์ฃผ์ธ์.',
+ ].join('\n');
+}
diff --git a/src/intelligence/requirementGraph.ts b/src/intelligence/requirementGraph.ts
index f270e04..bf08f2e 100644
--- a/src/intelligence/requirementGraph.ts
+++ b/src/intelligence/requirementGraph.ts
@@ -154,6 +154,42 @@ export const DEFAULT_TASK_REQUIREMENTS: TaskRequirement[] = [
},
],
},
+ {
+ // [v2.2.312] ์ค์ฌ๋ก ๋์ โ "E:\...ํ๋ก์ ํธ๋ฅผ ๋ถ์ํ๊ณ ์ด๋ป๊ฒ ๋์ด ์๋์ง ๋ณด๊ณ ํด์ค"๊ฐ
+ // ์ด๋ค ์ ํ์๋ ์ ๊ฑธ๋ ค 7์ค์ง๋ฆฌ ์ผ๋ฐ๋ก ์ดํ์ผ๋ก ๋๋ฌ๋ค. '๋ณด๊ณ ํด์ค/ํํฉ ๋ณด๊ณ ' ๋ฅ๋ฅผ
+ // ์ก์ ํ์ผ ๊ทผ๊ฑฐ ์ธ์ฉ์ ํ์ ์์๋ก ๊ฐ์ ํ๋ค.
+ id: 'analysis-report',
+ label: '๋ถ์ ๋ณด๊ณ ',
+ detectKeywords: ['๋ถ์[^\\n]{0,20}๋ณด๊ณ ', '๋ณด๊ณ ํด\\s*์ค?', 'ํํฉ\\s*(๋ณด๊ณ |์ ๋ฆฌ)', '์ด๋ป๊ฒ\\s*๋์ด\\s*์๋์ง', '๊ฒํ [^\\n]{0,10}๋ณด๊ณ '],
+ coverageCheck: true,
+ elements: [
+ {
+ id: 'overview', label: '๋ณด๊ณ ๊ฐ์',
+ hint: '์ฒซ ์ค์ "๋ณด๊ณ ๊ฐ์: (๋ถ์ ๋์) โ (ํ์ธํ ๊ทผ๊ฑฐ: ์ค์ ์ฝ์ ํ์ผ ์) โ (ํ ์ค ๊ฒฐ๋ก )" ํ์์ผ๋ก ์์. ์์ฒญ์๊ฐ ์ฒซ ์ค๋ง ์ฝ์ด๋ ๋ฌด์์ ์ด๋ค ๊ทผ๊ฑฐ๋ก ํ๋จํ๋์ง ์ ์ ์๊ฒ.',
+ detectPatterns: ['๋ณด๊ณ ?๊ฐ์', '๋ถ์ ?๋์', '๊ฐ์'],
+ },
+ {
+ id: 'file-evidence', label: 'ํ์ผ ๊ทผ๊ฑฐ',
+ hint: '๋ชจ๋ ์ฃผ์ ์ฃผ์ฅ์ ์ด๋ฒ ๋ํ์์ ์ค์ ๋ก ์ฝ์ ํ์ผ๋ช
ยท๊ฒฝ๋ก๋ฅผ ์ธ์ฉ. ํ๋ก์ ํธ ์ด๋ฆ์ ๋ฐ๊ฟ๋ ์ฑ๋ฆฝํ๋ ์ผ๋ฐ๋ก ("~๊ฐ ์ธ์์ ์
๋๋ค", "~ํ ๊ฐ๋ฅ์ฑ์ด ๋์ต๋๋ค")์ ๊ธ์ง โ ๊ทผ๊ฑฐ ์๋ ๋ฌธ์ฅ์ ์ฐ์ง ๋ง ๊ฒ.',
+ detectPatterns: ['\\.(ts|tsx|js|mjs|cjs|py|md|json|html|css|java|cs)\\b', 'src[\\\\/]', 'package\\.json'],
+ },
+ {
+ id: 'structure', label: '๊ตฌ์กฐยทํํฉ',
+ hint: 'ํ๋ก์ ํธ/๋์์ ์ค์ ๊ตฌ์ฑ โ ํต์ฌ ๋ชจ๋, ์งํ ์ํ, ํ์ธ๋ ์ฌ์ค. STATUS.md ๋ฑ ํ์ฌ ์ํ ๊ธฐ๋ก์ด ์์ผ๋ฉด ๊ทธ๊ฒ์ ๊ธฐ์ค์ผ๋ก.',
+ detectPatterns: ['๊ตฌ์กฐ', '๊ตฌ์ฑ', '๋ชจ๋', '์ํคํ
์ฒ', 'ํํฉ', '์ํ'],
+ },
+ {
+ id: 'findings', label: 'ํต์ฌ ๋ฐ๊ฒฌยท์ด์',
+ hint: '๋ถ์์์ ๋๋ฌ๋ ๊ฐ์ ยท๋ฌธ์ ยท์ด์. ๊ฐ ํญ๋ชฉ์ ๊ทผ๊ฑฐ ํ์ผ ์ฐ๊ฒฐ.',
+ detectPatterns: ['๋ฐ๊ฒฌ', '๊ฐ์ ', '๋ฌธ์ ', '์ด์', '๋ฆฌ์คํฌ', '๊ณผ์ '],
+ },
+ {
+ id: 'next-steps', label: '๋ค์ ๋จ๊ณ',
+ hint: '์ด ๋ณด๊ณ ๋ฅผ ๋ฐ์ ์ฌ๋์ด ์ทจํ ์ ์๋ ๊ตฌ์ฒด์ ๋ค์ ํ๋.',
+ detectPatterns: ['๋ค์ ?๋จ๊ณ', '๊ถ์ฅ', '์ ์', 'ํ์'],
+ },
+ ],
+ },
{
id: 'work-research',
label: '์
๋ฌด์กฐ์ฌ',
@@ -233,6 +269,9 @@ export function buildRequirementGraphBlock(
}
lines.push('');
lines.push('์ ์ถ ์ ์ ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ์ค์ค๋ก ์ ๊ฒํ๊ณ , ๋๋ฝ ์์๊ฐ ์์ผ๋ฉด ๋ณด์ ํ ๋ต๋ณํ ๊ฒ.');
+ // [v2.2.312] ์
๋ฌด ์ ํ์ด ๊ฐ์ง๋ ๋ต๋ณ์ ์ผ๋ฐ "์ต๋ 3์น์
" ๊ท์น๋ณด๋ค ํ์ ์์ ์ปค๋ฒ๊ฐ
+ // ์ฐ์ โ ๋ณด๊ณ ์๊ฐ 3์น์
์ ํ ๋๋ฌธ์ ์์์ง๋ ์์ถฉ ์ ๊ฑฐ.
+ lines.push('์ด ์
๋ฌด ์ ํ์ด ๊ฐ์ง๋ ๋ต๋ณ์์๋ "์ต๋ 3์น์
" ์ผ๋ฐ ๊ท์น๋ณด๋ค ํ์ ์์ ์ปค๋ฒ๊ฐ ์ฐ์ ํ๋ค โ ์์๋ณ๋ก ์น์
์ ๋๋ ๋ ๋๋ค.');
lines.push('[/TASK REQUIREMENTS]');
return lines.join('\n');
}
diff --git a/src/lib/contextBuilders/memoryContext.ts b/src/lib/contextBuilders/memoryContext.ts
index bcf5cd3..7c1c43f 100644
--- a/src/lib/contextBuilders/memoryContext.ts
+++ b/src/lib/contextBuilders/memoryContext.ts
@@ -166,11 +166,13 @@ export async function buildMemoryContext(deps: MemoryContextDeps): Promise 0) effectiveScopeFolders = domainScope.folders;
}
- // Context ์๋์ฐ ๋น๋ก retrieval ์์ฐ. 32K โ 8K, 230K โ 57K, 80K cap (scoring ์๋).
- const scaledTotalBudget = Math.min(
- 80000,
- Math.max(8000, Math.floor(config.contextLength * 0.25)),
- );
+ // Context ์๋์ฐ ๋น๋ก retrieval ์์ฐ.
+ // [v2.2.311] ์ข
์ ์๋๊ฐ(์ฐฝ์ 25%, 8k~80k)์ 32K ์ฐฝ์์ ๊ฒ์ ๊ฒฐ๊ณผ์๋ง 8k ํ ํฐ์ ํ์
+ // ๋ก์ปฌ ํ๋ฆฌํ ์์ญ ์ด์ ์ฃผ๋ฒ์ด์๋ค. ์๋๊ฐ์ ์ฐฝ์ 12%(2.5k~6k ํด๋จํ)๋ก ๋ฎ์ถ๊ณ ,
+ // ๋ ํ๋ถํ ๊ทผ๊ฑฐ๊ฐ ํ์ํ๋ฉด g1nation.retrievalTokenBudget ์ผ๋ก ๋ช
์ ์ํฅ.
+ const scaledTotalBudget = config.retrievalTokenBudget > 0
+ ? config.retrievalTokenBudget
+ : Math.min(6000, Math.max(2500, Math.floor(config.contextLength * 0.12)));
// medium-term layer ์ฉ ์ ์ธ์
ํ๋ณด. sidebar ๊ฐ ์ง์ ์ฐ๋ key ๋ฅผ read-through.
const rawSessions = deps.context.globalState.get('chat_sessions', []) || [];
diff --git a/src/retrieval/brainIndex.ts b/src/retrieval/brainIndex.ts
index 0522281..0e7dfb6 100644
--- a/src/retrieval/brainIndex.ts
+++ b/src/retrieval/brainIndex.ts
@@ -17,6 +17,7 @@ import { tokenize, countConflictIndicators } from './scoring';
import { detectLessonKind } from './lessonHelpers';
import { splitIntoSections } from './chunker';
import { logInfo } from '../utils';
+import { ensureBrainWatcher, getBrainGeneration } from './brainWatch';
// v5 adds optional per-file `chunks` (section-level index, Phase 1-๊ฐ) alongside the
// v4 per-file `embedding`. Older indexes are auto-rebuilt on first load โ no migration
@@ -109,6 +110,16 @@ interface BrainState {
diskPath: string | null; // null if we can't determine a writable path
writeTimer?: ReturnType;
evictTimer?: ReturnType;
+ /**
+ * [v2.2.311] statSync ์๋ต ์ต์ ํ์ ์ฅ๋ถ. statValidatedGen ์ธ๋์์ ์ค์ ๋ก
+ * stat(๋๋ ์ฌ์์ธ)์ ๊ฑฐ์น ํ์ผ๋ง statValidated ์ ๋ค์ด๊ฐ๋ค โ ์ธ๋๊ฐ ๊ทธ๋๋ก๋ฉด
+ * ๊ทธ ํ์ผ๋ค์ ์ฌ-stat ์์ด ์บ์๋ฅผ ์ ๋ขฐํ๋ค. ์ธ๋๊ฐ ๋ฐ๋๋ฉด(ํ์ผ์์คํ
๋ณ๊ฒฝ)
+ * ์งํฉ์ ๋น์ ์ ํ์ผ์ด ๋ค์ ํ ๋ฒ stat ์ ๊ฑฐ์น๋ค. ํ์ผ *๋จ์* ๋ก ๊ธฐ๋กํ๋
+ * ์ด์ : ์ค์ฝํ ๊ฒ์(๋ถ๋ถ ๋ชฉ๋ก) ๋ค ์ ์ฒด ๊ฒ์์ด ์๋, ๊ฒ์ฆ ์ ๋ ํ์ผ์ด
+ * ์ ๋ขฐ ๊ฒฝ๋ก๋ก ์์ง ์๊ฒ ํ๊ธฐ ์ํจ.
+ */
+ statValidatedGen?: number;
+ statValidated?: Set;
}
const _states = new Map();
@@ -117,7 +128,16 @@ const _states = new Map();
// ํ์ฅ ํธ์คํธ ๋ฉ๋ชจ๋ฆฌ์ ์ธ์
๋ด๋ด ์์ฃผํ๋ค. ์ธ๋ฑ์ค๋ mtime ํค ๋์คํฌ ์บ์์์ ์ธ์ ๋ ์ธ๊ฒ
// ์ฌ๋ก๋๋๋ฏ๋ก, ๋ง์ง๋ง ์ฌ์ฉ ํ IDLE_EVICT_MS ๊ฐ ์ง๋๋ฉด dirty ๋ฅผ flush ํ๊ณ ๋ฉ๋ชจ๋ฆฌ์์ ๋ด๋ฆฐ๋ค.
// ์์ ์ธํฐ๋ฒ ์์ด ์ ๊ทผํ ๋๋ง๋ค ์ฌ์์ฝ๋๋ one-shot unref ํ์ด๋จธ๋ผ ๊ทธ ์์ฒด์ ๋ฐ์ด ๋น์ฉ์ ์๋ค.
-const IDLE_EVICT_MS = 30 * 60_000;
+// [v2.2.311] 30๋ถ โ 2์๊ฐ: ์ ์ฌ/ํ์ ๋ค ์ฒซ ์ง์๊ฐ ๋ํ ์ธ๋ฑ์ค(์์ญ MB JSON) ์ฌํ์ฑ์
+// ๋ค์ง์ด์ฐ๋ ๋น๋๋ฅผ ์ค์ธ๋ค. ๋ฉ๋ชจ๋ฆฌ ์ ์ฝ๋ณด๋ค ์๋ต ์ง์ฐ์ด ๋ ์ํ ๋ก์ปฌ-ํผ์คํธ ์ํฌ๋ก๋.
+const IDLE_EVICT_MS = 120 * 60_000;
+
+/**
+ * [v2.2.311] stat ์๋ต ์ ๋ขฐ ๋ช
๋ถ์ ์ฌ๋ฆฌ์ง ์๋ "๊ฐ ์์ ๋ ํ์ผ" ์ฐฝ. fs.watch ์ด๋ฒคํธ๋
+ * ๋น๋๊ธฐ(+๋๋ฐ์ด์ค)๋ผ, ์์ ์งํ ๊ฐ์ ํฑ์ ๋ค์ด์จ ์ง์๋ ์ธ๋ bump ๋ฅผ ์์ง ๋ชป ๋ณธ๋ค โ
+ * ๊ทธ ์ฐฝ ์์ ํ์ผ์ ๋ช
๋ถ์์ ์ ์ธํด ๋ค์ ํธ์ถ์์๋ stat ์ ๊ฑฐ์น๊ฒ ํ๋ค.
+ */
+const RECENT_MTIME_GUARD_MS = 3000;
function touchEviction(st: BrainState, brainPath: string): void {
if (st.evictTimer) clearTimeout(st.evictTimer);
@@ -206,8 +226,35 @@ export function getBrainTokenIndex(brainPath: string, files: string[]): IndexedB
const seen = new Set();
let reindexed = 0;
+ // [v2.2.311] ์์ฒ ์ธ๋๊ฐ ๋ง์ง๋ง stat ๊ฒ์ฆ ์ดํ ๊ทธ๋๋ก๋ฉด โ ํ์ผ์์คํ
๋ณ๊ฒฝ ์์ โ
+ // ๊ทธ ์ธ๋์์ ์ด๋ฏธ stat ์ ๊ฑฐ์น ํ์ผ์ statSync ๋ฅผ ์๋ตํ๊ณ ์บ์ ํญ๋ชฉ์ ์ ๋ขฐ.
+ // ์์ฒ๊ฐ ์์ผ๋ฉด(gen === -1) ์ข
์ ๋๋ก ๋งค๋ฒ stat โ ์ ํ์ฑ ์ ํ ์์.
+ ensureBrainWatcher(brainPath);
+ const watchGen = getBrainGeneration(brainPath);
+ if (watchGen >= 0 && st.statValidatedGen !== watchGen) {
+ st.statValidatedGen = watchGen;
+ st.statValidated = new Set();
+ }
+ const validated = watchGen >= 0 ? (st.statValidated ?? (st.statValidated = new Set())) : null;
+
for (const file of files) {
seen.add(file);
+ const cachedFast = validated && validated.has(file) ? st.index.entries[file] : undefined;
+ if (cachedFast
+ && Array.isArray(cachedFast.tokens)
+ && Array.isArray(cachedFast.titleTokens)) {
+ out.push({
+ filePath: file,
+ relativePath: cachedFast.relativePath,
+ title: cachedFast.title,
+ tokens: cachedFast.tokens,
+ titleTokens: cachedFast.titleTokens,
+ conflictCount: cachedFast.conflictCount || 0,
+ mtimeMs: cachedFast.mtimeMs,
+ kind: cachedFast.kind || '',
+ });
+ continue;
+ }
let stat: fs.Stats;
try {
stat = fs.statSync(file);
@@ -220,6 +267,7 @@ export function getBrainTokenIndex(brainPath: string, files: string[]): IndexedB
&& cached.size === stat.size
&& Array.isArray(cached.tokens)
&& Array.isArray(cached.titleTokens)) {
+ if (Date.now() - stat.mtimeMs > RECENT_MTIME_GUARD_MS) validated?.add(file);
out.push({
filePath: file,
relativePath: cached.relativePath,
@@ -255,6 +303,7 @@ export function getBrainTokenIndex(brainPath: string, files: string[]): IndexedB
st.index.entries[file] = entry;
st.dirty = true;
reindexed++;
+ if (Date.now() - entry.mtimeMs > RECENT_MTIME_GUARD_MS) validated?.add(file);
out.push({
filePath: file,
relativePath,
diff --git a/src/retrieval/brainWatch.ts b/src/retrieval/brainWatch.ts
new file mode 100644
index 0000000..02d0614
--- /dev/null
+++ b/src/retrieval/brainWatch.ts
@@ -0,0 +1,140 @@
+/**
+ * ============================================================
+ * Brain Watch โ ์ 2๋ ํ์ผ์์คํ
์์ฒ + ์์ฃผ ํ์ผ ๋ชฉ๋ก ์บ์ (v2.2.311)
+ *
+ * ๋ฌธ์ : ๊ฒ์์ ์ด๋ฏธ mtime ํค ํ ํฐ ์ธ๋ฑ์ค(brain-index.json)๋ก ํ์ผ *๋ด์ฉ* ์ฌํ์ฑ์
+ * ํผํ์ง๋ง, ๋งค ์ง์๋ง๋ค (a) ์ ์ฒด ๋๋ ํฐ๋ฆฌ ์ฌ๊ท ์ํฌ(์์ฒ ํ์ผ)์ (b) ํ์ผ๋ณ
+ * fs.statSync ์์ฒ ํ๋ฅผ ๋ค์ ์ํํ๋ค โ ์ธ๋ฑ์ค๊ฐ ์ ์ ํ์ง ํ์ธํ ๋ฐฉ๋ฒ์ด "์ ๋ถ
+ * ๋ค์ stat" ๋ฟ์ด์๊ธฐ ๋๋ฌธ. ๋ธ๋ ์ธ 6,500 ํ์ผ ๊ธฐ์ค ํด๋น 0.5~1.5์ด.
+ *
+ * ํด๊ฒฐ: ๋ธ๋ ์ธ ๋ฃจํธ์ ์ฌ๊ท fs.watch ๋ฅผ ํ๋ ๊ฑธ๊ณ ๋ณ๊ฒฝ ์ ์ธ๋(generation) ์นด์ดํฐ๋ง
+ * ์ฌ๋ฆฐ๋ค. ์ธ๋๊ฐ ๊ทธ๋๋ก๋ฉด โ ํ์ผ ๋ชฉ๋ก๋, mtime ๋ ์ ๋ณํ ๊ฒ โ ์ํฌ/stat ์ ํต์งธ๋ก
+ * ์๋ตํ๊ณ ๋ฉ๋ชจ๋ฆฌ์ ๋ชฉ๋กยท์ธ๋ฑ์ค๋ฅผ ๊ทธ๋๋ก ์ ๋ขฐํ๋ค. ์ด๊ฒ์ด "์ 2๋๋ฅผ ํญ์ ๋ฉ๋ชจ๋ฆฌ์
+ * ์ฌ๋ ค๋๊ธฐ"์ ์์ ํ ํํ๋ค: ์๋ณธ ํ์ผ์ ํต์งธ๋ก ์์ฃผ์ํค๋ ๋์ (์๋ฐฑ MB), ๊ฒ์์
+ * ์ค์ ๋ก ์ฐ๋ ํ์๋ฌผ(ํ ํฐ ์ธ๋ฑ์ค + ํ์ผ ๋ชฉ๋ก)์ ์์ฃผ์ํค๊ณ ์์ฒ๊ฐ ๋ฌดํจํ๋ฅผ ๋งก๋๋ค.
+ *
+ * ์์ฒ๋ฅผ ๋ชป ์ฌ๋ ํ๊ฒฝ(๊ถํ/๋คํธ์ํฌ ๋๋ผ์ด๋ธ ๋ฑ)์์๋ generation ์ด -1 ๋ก ๊ณ ์ ๋๊ณ
+ * ๋ชจ๋ ํธ์ถ์๋ ์ข
์ ๋์(๋งค๋ฒ stat)์ผ๋ก ํด๋ฐฑํ๋ค โ ์ ํ์ฑ์ ์์ฒ ๊ฐ์ฉ์ฑ๊ณผ ๋ฌด๊ด.
+ * ============================================================
+ */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import { findBrainFiles, invalidateBrainFilesCache, logInfo } from '../utils';
+
+interface WatchState {
+ watcher: fs.FSWatcher | null;
+ /** ํ์ผ์์คํ
๋ณ๊ฒฝ๋ง๋ค ์ฆ๊ฐ. ์์ฒ ์์ผ๋ฉด ์ํ ์์ฒด๊ฐ failed=true. */
+ generation: number;
+ failed: boolean;
+ /** generation ๊ธฐ์ค ํ์ผ ๋ชฉ๋ก ์บ์. */
+ listGeneration: number;
+ list: string[] | null;
+ /** ์ด๋ฒคํธ ํญ์ฃผ(๋๋ sync) ์ bump ๋ฅผ ๋ฌถ๋ ๋๋ฐ์ด์ค ํ์ด๋จธ. */
+ bumpTimer: ReturnType | null;
+}
+
+const _watches = new Map();
+
+/** ์ธ๋ฑ์ค ์บ์ ๋ฑ ์ฐ๋ฆฌ ์์ ์ ์ฐ์ถ๋ฌผ ๊ฒฝ๋ก โ ์ด ์๋ ๋ณ๊ฒฝ์ ๋ฌดํจํ ์ฌ์ ๊ฐ ์๋๋ค. */
+function isSelfNoise(relPath: string): boolean {
+ // .astra (brain-index.json, eval, growth โฆ) ์ .git ์ ๊ฒ์ ๋์๋ ์๋๊ณ
+ // ํด๋ง๋ค ์ฐ๋ฆฌ๊ฐ ์ง์ ์ฐ๋ ๊ณณ์ด๋ผ, ์ฌ๊ธฐ์ ์ธ๋๋ฅผ ์ฌ๋ฆฌ๋ฉด ์บ์๊ฐ ์์ํ ๋ชป ์ฐ๋ค.
+ return /(^|[\\/])(\.astra|\.git)([\\/]|$)/.test(relPath);
+}
+
+function bump(st: WatchState, brainPath: string): void {
+ if (st.bumpTimer) return; // ์ด๋ฏธ ์์ฝ๋จ โ ๋ชฐ์์ 1ํ
+ st.bumpTimer = setTimeout(() => {
+ st.bumpTimer = null;
+ st.generation++;
+ st.list = null;
+ invalidateBrainFilesCache(brainPath);
+ }, 300);
+ if (typeof (st.bumpTimer as any).unref === 'function') (st.bumpTimer as any).unref();
+}
+
+/**
+ * ๋ธ๋ ์ธ ๋ฃจํธ์ ์์ฒ๋ฅผ ๋ณด์ฅํ๋ค (idempotent). ์คํจํด๋ throw ํ์ง ์๋๋ค โ
+ * ์ดํ getBrainGeneration() ์ด -1 ์ ๋ฐํํด ํธ์ถ์๋ค์ด stat ๊ฒฝ๋ก๋ก ํด๋ฐฑ.
+ */
+export function ensureBrainWatcher(brainPath: string): void {
+ if (!brainPath) return;
+ if (_watches.has(brainPath)) return;
+ const st: WatchState = { watcher: null, generation: 0, failed: false, listGeneration: -1, list: null, bumpTimer: null };
+ _watches.set(brainPath, st);
+ try {
+ // recursive fs.watch ๋ Windows/macOS ์์ ๋ค์ดํฐ๋ธ ์ง์. (Linux ๋ Node 20+.)
+ st.watcher = fs.watch(brainPath, { recursive: true }, (_event, filename) => {
+ const rel = filename ? String(filename) : '';
+ if (rel && isSelfNoise(rel)) return;
+ bump(st, brainPath);
+ });
+ st.watcher.on('error', () => {
+ st.failed = true;
+ try { st.watcher?.close(); } catch { /* already closed */ }
+ st.watcher = null;
+ logInfo('Brain watcher failed โ falling back to per-query stat.', { brainPath });
+ });
+ logInfo('Brain watcher started โ file list & stat checks now cached until change.', { brainPath });
+ } catch (e: any) {
+ st.failed = true;
+ st.watcher = null;
+ logInfo('Brain watcher unavailable โ falling back to per-query stat.', { brainPath, error: e?.message ?? String(e) });
+ }
+}
+
+/**
+ * ํ์ฌ ์ธ๋ ๋ฒํธ. ์์ฒ๊ฐ ์๊ฑฐ๋ ์ฃฝ์์ผ๋ฉด -1 โ ํธ์ถ์๋ ์บ์ ์ ๋ขฐ ์ต์ ํ๋ฅผ ๊บผ์ผ ํ๋ค.
+ */
+export function getBrainGeneration(brainPath: string): number {
+ const st = _watches.get(brainPath);
+ if (!st || st.failed || !st.watcher) return -1;
+ return st.generation;
+}
+
+/**
+ * ์ธ๋ ๊ธฐ๋ฐ ํ์ผ ๋ชฉ๋ก โ ์ธ๋๊ฐ ๊ทธ๋๋ก๋ฉด ๋๋ ํฐ๋ฆฌ ์ํฌ ์์ด ๋ฉ๋ชจ๋ฆฌ ๋ชฉ๋ก์ ๋ฐํ.
+ * ์์ฒ๊ฐ ์์ผ๋ฉด findBrainFiles(5์ด TTL)๋ก ํด๋ฐฑํด ์ข
์ ๊ณผ ๋์ผํ๊ฒ ๋์.
+ */
+export function listBrainFilesCached(brainPath: string): string[] {
+ if (!brainPath) return [];
+ ensureBrainWatcher(brainPath);
+ const st = _watches.get(brainPath)!;
+ if (st.failed || !st.watcher) return findBrainFiles(brainPath);
+ if (st.list !== null && st.listGeneration === st.generation) return st.list.slice();
+ const files = findBrainFiles(brainPath);
+ st.list = files.slice();
+ st.listGeneration = st.generation;
+ return files;
+}
+
+/**
+ * ํ์ฑํ ์งํ ๋ฐฑ๊ทธ๋ผ์ด๋ ์๋ฐ โ ์ฒซ ์ง์๊ฐ 78MB ์ธ๋ฑ์ค ํ์ฑ + ์ ์ฒด ์ํฌ๋ฅผ
+ * ๋ค์ง์ด์ฐ์ง ์๋๋ก ๋ฏธ๋ฆฌ ์ฌ๋ ค ๋๋ค. ์คํจ๋ ์กฐ์ฉํ ๋ฌด์ (๋ค์ ์ง์๊ฐ ์ ์ ๊ฒฝ๋ก).
+ */
+export function warmBrainCache(brainPath: string, warmTokenIndex: (files: string[]) => void): void {
+ if (!brainPath) return;
+ setTimeout(() => {
+ try {
+ const t0 = Date.now();
+ const files = listBrainFilesCached(brainPath);
+ warmTokenIndex(files);
+ logInfo('Brain cache warmed.', { brainPath, files: files.length, ms: Date.now() - t0 });
+ } catch (e: any) {
+ logInfo('Brain cache warm failed (non-fatal).', { brainPath, error: e?.message ?? String(e) });
+ }
+ }, 2500);
+}
+
+/** ํ
์คํธ/๋ธ๋ ์ธ ์ ํ์ฉ โ ์์ฒ ํด์ . */
+export function disposeBrainWatcher(brainPath?: string): void {
+ const targets = brainPath ? [brainPath] : Array.from(_watches.keys());
+ for (const key of targets) {
+ const st = _watches.get(key);
+ if (!st) continue;
+ if (st.bumpTimer) { clearTimeout(st.bumpTimer); st.bumpTimer = null; }
+ try { st.watcher?.close(); } catch { /* noop */ }
+ _watches.delete(key);
+ }
+}
diff --git a/src/retrieval/index.ts b/src/retrieval/index.ts
index 597fb2e..39a9543 100644
--- a/src/retrieval/index.ts
+++ b/src/retrieval/index.ts
@@ -15,7 +15,8 @@
import * as fs from 'fs';
import * as path from 'path';
import { BrainProfile } from '../config';
-import { findBrainFiles, summarizeText } from '../utils';
+import { summarizeText } from '../utils';
+import { listBrainFilesCached } from './brainWatch';
import { isInside } from '../lib/paths';
import { MemoryManager } from '../memory';
import { RetrievalChunk, RetrievalResult, ContextBudgetConfig } from './types';
@@ -329,7 +330,8 @@ export class RetrievalOrchestrator {
try {
const scoped = (file: string) => scopeFolders.length === 0
|| scopeFolders.some((folder) => isInside(folder, file));
- const allFiles = findBrainFiles(brain.localBrainPath)
+ // [v2.2.311] ์์ฒ ์ธ๋ ๊ธฐ๋ฐ ์์ฃผ ๋ชฉ๋ก โ ๋ณ๊ฒฝ ์์ผ๋ฉด ๋๋ ํฐ๋ฆฌ ์ฌ๊ท ์ํฌ ์๋ต.
+ const allFiles = listBrainFilesCached(brain.localBrainPath)
.filter(scoped)
.filter((file) => {
const rel = path.relative(brain.localBrainPath, file);
diff --git a/src/utils.ts b/src/utils.ts
index e9f5d97..04a05da 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -494,14 +494,19 @@ function getEnvironmentBlock(): string {
- Canonical example for this OS: ${example}`;
}
-export function getSystemPrompt(): string {
- const now = new Date();
- const dateTimeStr = now.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long', hour: '2-digit', minute: '2-digit' });
- const isoDate = now.toISOString().split('T')[0];
+/**
+ * [KV ์บ์ ๋ถ๋ฆฌ v2.2.311] ์์คํ
ํ๋กฌํํธ์ *ํด ์ฌ์ด์ ๋ณํ์ง ์๋* ๋ถ๋ถ๋ง ๋ฐํ.
+ * ์ฌ๊ธฐ์ ๋ถ ๋จ์ ์๊ฐ ๊ฐ์ ๊ฐ๋ณ ์์๊ฐ ์์ด๋ฉด LM Studio(llama.cpp)์ prompt cache ๊ฐ
+ * ์์ฒญ ์ฒซ ํ ํฐ๋ถํฐ ๋ฌดํจํ๋์ด ๋งค ํด 1๋ง+ ํ ํฐ ์ ์ฒด๋ฅผ ๋ค์ ํ๋ฆฌํํ๋ค โ ์ค์ธก 94์ด/ํด์
+ * ์ฃผ๋ฒ. ๋ ์ง/์๊ฐ์ `getDateTimeContextBlock()` ์ผ๋ก ๋ถ๋ฆฌํด ์์ฒญ ํ๋ฏธ(๋์ ์ปจํ
์คํธ
+ * ๋ฉ์์ง)์ ์ฃ๋๋ค.
+ */
+export function getStaticSystemPrompt(): string {
const envBlock = getEnvironmentBlock();
- const base = `${BASE_SYSTEM_PROMPT}\n\n[CURRENT DATE/TIME]\nToday: ${isoDate} (${dateTimeStr})\nUse this date as the absolute reference for any date-related calculations (e.g., "this week", "today", "yesterday").\n\n[์ถ๋ ฅ ์์ ๊ท์น โ ๋ฐ๋์ ์ค์]\n- ์์ฐ์ค๋ฌ์ด ํ๊ตญ์ด๋ก ์์ฑํ๊ณ , ํ ๋จ์ด ์์ ํ๊ธ๊ณผ ์๋ฌธ ์ํ๋ฒณ์ ์์ง ๋ง์์ค ("๊ฒฐently", "์ธorp" ๊ฐ์ ๊นจ์ง ํฉ์ฑ ํ๊ธฐ ์ ๋ ๊ธ์ง).\n- ์ธ๋์ดยท๊ธฐ์ ์ฉ์ด๋ ์์ ํ ํ๊ธ ํ๊ธฐ ๋๋ ์์ ํ ์๋ฌธ ๋จ์ด ์ค ํ๋๋ก ์ผ๊ด๋๊ฒ ์ฐ์์ค.\n- ๋ด๋ถ ๊ฒ์ฆยท์ฒดํฌ ๋ก๊ทธ(Consistency/Completeness/Accuracy ๋ฑ) ๋ธ๋ก์ ์ฌ์ฉ์ ์ถ๋ ฅ์ ํฌํจํ์ง ๋ง์์ค.${envBlock}`;
+ const base = `${BASE_SYSTEM_PROMPT}\n\n[์ถ๋ ฅ ์์ ๊ท์น โ ๋ฐ๋์ ์ค์]\n- ์์ฐ์ค๋ฌ์ด ํ๊ตญ์ด๋ก ์์ฑํ๊ณ , ํ ๋จ์ด ์์ ํ๊ธ๊ณผ ์๋ฌธ ์ํ๋ฒณ์ ์์ง ๋ง์์ค ("๊ฒฐently", "์ธorp" ๊ฐ์ ๊นจ์ง ํฉ์ฑ ํ๊ธฐ ์ ๋ ๊ธ์ง).\n- ์ธ๋์ดยท๊ธฐ์ ์ฉ์ด๋ ์์ ํ ํ๊ธ ํ๊ธฐ ๋๋ ์์ ํ ์๋ฌธ ๋จ์ด ์ค ํ๋๋ก ์ผ๊ด๋๊ฒ ์ฐ์์ค.\n- ๋ด๋ถ ๊ฒ์ฆยท์ฒดํฌ ๋ก๊ทธ(Consistency/Completeness/Accuracy ๋ฑ) ๋ธ๋ก์ ์ฌ์ฉ์ ์ถ๋ ฅ์ ํฌํจํ์ง ๋ง์์ค.${envBlock}`;
// Self-Reflector Phase A โ ์ฌ์ฉ์ ์ค์ ์ด ์ผ์ ธ ์์ผ๋ฉด ๋ต๋ณ ๋์ ์๊ธฐ๊ฒ์ฆ
// ๋ธ๋ก์ ๊ฐ์ ํ๋ ๋ฃฐ์ prepend. require๋ก ๋์ ๋ก๋ํด ์ํ import ํํผ.
+ // (์ค์ ์ ์ธ์
์ค ๋ฐ๋ ์ ์์ง๋ง ๋๋ฌผ๋ค โ ๋ฐ๋๋ฉด ๊ทธ ํด๋ง ์บ์ ๋ฏธ์ค, ๋ฌธ์ ์์.)
try {
const { getConfig } = require('./config') as typeof import('./config');
const { appendSelfReflectorRule } = require('./features/selfReflector/selfReflectorPrompt') as typeof import('./features/selfReflector/selfReflectorPrompt');
@@ -513,4 +518,21 @@ export function getSystemPrompt(): string {
}
}
+/** [CURRENT DATE/TIME] ๋ธ๋ก โ ๊ฐ๋ณ์ด๋ฏ๋ก ์ ์ ํ๋กฌํํธ๊ฐ ์๋๋ผ ๋์ ์ปจํ
์คํธ์ ์ฃผ์
. */
+export function getDateTimeContextBlock(): string {
+ const now = new Date();
+ const dateTimeStr = now.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long', hour: '2-digit', minute: '2-digit' });
+ const isoDate = now.toISOString().split('T')[0];
+ return `[CURRENT DATE/TIME]\nToday: ${isoDate} (${dateTimeStr})\nUse this date as the absolute reference for any date-related calculations (e.g., "this week", "today", "yesterday").`;
+}
+
+export function getSystemPrompt(): string {
+ // ๊ธฐ์กด ํธ์ถ๋ถ ํธํ โ ์ ์ ๋ณธ๋ฌธ + ๋ ์ง ๋ธ๋ก์ ํฉ์น ์ข
์ ํํ ๊ทธ๋๋ก.
+ const staticPart = getStaticSystemPrompt();
+ const dateBlock = getDateTimeContextBlock();
+ // BASE ์งํยท์์ ๊ท์น ์์ด๋ ์ข
์ ์์น์ ๋ฌ๋ฆฌ ๋์ ๋ถ์ง๋ง, ๋จ๋
์ฌ์ฉ์ฒ(๋ธ๋ฆฌํยทํ๊ฐ
+ // ๋ฑ 1ํ์ฑ ํธ์ถ)์๋ ์์น๊ฐ ์๋ฏธ ์๋ค. ์ฑํ
๋ณธ๋ฅ๋ split ๊ฒฝ๋ก๋ฅผ ์ด๋ค.
+ return `${staticPart}\n\n${dateBlock}`;
+}
+
export const SYSTEM_PROMPT = BASE_SYSTEM_PROMPT;
diff --git a/tests/brainCache.test.ts b/tests/brainCache.test.ts
new file mode 100644
index 0000000..65e217a
--- /dev/null
+++ b/tests/brainCache.test.ts
@@ -0,0 +1,98 @@
+/**
+ * [v2.2.311] ์ 2๋ ์์ฃผ ์บ์ ๊ณ์ธต โ ์์ฒ ์ธ๋ ๊ธฐ๋ฐ stat ์๋ต + ํ์ผ ๋ชฉ๋ก ์บ์.
+ *
+ * ํต์ฌ ๊ณ์ฝ:
+ * 1. ์์ฒ ์ธ๋๊ฐ ๊ทธ๋๋ก๋ฉด getBrainTokenIndex ๋ ํ์ผ๋ณ statSync ๋ฅผ ์๋ตํ๋ค.
+ * 2. ์ธ๋๊ฐ ๋ฐ๋๋ฉด(ํ์ผ์์คํ
๋ณ๊ฒฝ) ๋ค์ stat ์ ๊ฑฐ์ณ ๋ณ๊ฒฝ๋ถ์ ์ฌ์์ธํ๋ค.
+ * 3. ๋ถ๋ถ(์ค์ฝํ) ๋ชฉ๋ก์ผ๋ก ๊ฒ์ฆ๋ ์ํ์์ ์ ์ฒด ๋ชฉ๋ก ์ง์๊ฐ ์๋, ๊ฒ์ฆ ์ ๋
+ * ํ์ผ์ ์ ๋ขฐ ๊ฒฝ๋ก๋ก ์์ง ์๋๋ค (ํ์ผ ๋จ์ ๊ฒ์ฆ ์ฅ๋ถ).
+ * 4. ์์ฒ ๋ถ๊ฐ ํ๊ฒฝ(gen === -1)์์๋ ์ข
์ ๊ณผ ๋์ผํ๊ฒ ๋งค๋ฒ stat.
+ */
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+
+// brainWatch ๋ฅผ ๊ฒฐ์ ์ ์ผ๋ก ์ ์ด โ ์ค์ fs.watch ์ด๋ฒคํธ ํ์ด๋ฐ์ ์์กดํ์ง ์๋๋ค.
+let mockGen = 0;
+jest.mock('../src/retrieval/brainWatch', () => ({
+ ensureBrainWatcher: jest.fn(),
+ getBrainGeneration: jest.fn(() => mockGen),
+ listBrainFilesCached: jest.fn(),
+ warmBrainCache: jest.fn(),
+ disposeBrainWatcher: jest.fn(),
+}));
+
+import { getBrainTokenIndex } from '../src/retrieval/brainIndex';
+
+function makeBrain(files: Record): { root: string; paths: string[] } {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'astra-brain-'));
+ const paths: string[] = [];
+ for (const [name, content] of Object.entries(files)) {
+ const p = path.join(root, name);
+ fs.writeFileSync(p, content, 'utf8');
+ // RECENT_MTIME_GUARD(3์ด): ๊ฐ ์์ ๋ ํ์ผ์ ์ ๋ขฐ ๋ช
๋ถ์ ์ค๋ฅด์ง ์์ผ๋ฏ๋ก,
+ // "๋ณ๊ฒฝ ์์" ์๋๋ฆฌ์ค๋ฅผ ํ
์คํธํ๋ ค๋ฉด mtime ์ ๊ณผ๊ฑฐ๋ก ๋๋๋ฆฐ๋ค.
+ const past = new Date(Date.now() - 60_000);
+ fs.utimesSync(p, past, past);
+ paths.push(p);
+ }
+ return { root, paths };
+}
+
+function countStatsFor(paths: string[], spy: jest.SpyInstance): number {
+ return spy.mock.calls.filter((c) => paths.includes(String(c[0]))).length;
+}
+
+describe('getBrainTokenIndex โ ์์ฒ ์ธ๋ ๊ธฐ๋ฐ stat ์๋ต', () => {
+ afterEach(() => jest.restoreAllMocks());
+
+ test('๊ฐ์ ์ธ๋์ 2๋ฒ์งธ ํธ์ถ์ statSync 0ํ (์บ์ ์ ๋ขฐ)', () => {
+ mockGen = 0;
+ const { root, paths } = makeBrain({ 'a.md': '์ํ ๋ด์ฉ', 'b.md': '๋ฒ ํ ๋ด์ฉ' });
+ expect(getBrainTokenIndex(root, paths)).toHaveLength(2);
+
+ const spy = jest.spyOn(fs, 'statSync');
+ const r2 = getBrainTokenIndex(root, paths);
+ expect(r2).toHaveLength(2);
+ expect(countStatsFor(paths, spy)).toBe(0);
+ expect(r2.map((d) => d.title).sort()).toEqual(['a', 'b']);
+ });
+
+ test('์ธ๋๊ฐ ๋ฐ๋๋ฉด ์ฌ-stat ํ ๋ณ๊ฒฝ ๋ด์ฉ์ ์ฌ์์ธํ๋ค', () => {
+ mockGen = 0;
+ const { root, paths } = makeBrain({ 'a.md': '์๋ณธ ํ ํฐ๋ค' });
+ getBrainTokenIndex(root, paths);
+
+ // ํ์ผ ๋ณ๊ฒฝ + ์ธ๋ bump (mtime ์ฐจ์ด๋ฅผ ๋ณด์ฅํ๊ธฐ ์ํด utimesSync ๋ก ๊ฐ์ )
+ fs.writeFileSync(paths[0], '์์ ํ ์๋ก์ด ๋ด์ฉ์
๋๋ค', 'utf8');
+ const future = new Date(Date.now() + 5000);
+ fs.utimesSync(paths[0], future, future);
+ mockGen = 1;
+
+ const spy = jest.spyOn(fs, 'statSync');
+ const r = getBrainTokenIndex(root, paths);
+ expect(countStatsFor(paths, spy)).toBeGreaterThan(0);
+ expect(r[0].tokens).toEqual(expect.arrayContaining(['์๋ก์ด']));
+ });
+
+ test('์ค์ฝํ ๊ฒ์ฆ ํ ์ ์ฒด ์ง์ โ ๋ฏธ๊ฒ์ฆ ํ์ผ์ ์ ๋ขฐ ๊ฒฝ๋ก๋ก ์์ง ์๋๋ค', () => {
+ mockGen = 0;
+ const { root, paths } = makeBrain({ 'in-scope.md': '์ค์ฝํ ์', 'out-scope.md': '์ค์ฝํ ๋ฐ' });
+ const [inScope, outScope] = paths;
+ getBrainTokenIndex(root, [inScope]); // ๋ถ๋ถ ๋ชฉ๋ก๋ง ๊ฒ์ฆ๋จ
+
+ const spy = jest.spyOn(fs, 'statSync');
+ getBrainTokenIndex(root, paths); // ์ ์ฒด ์ง์
+ expect(countStatsFor([inScope], spy)).toBe(0); // ๊ฒ์ฆ๋ ํ์ผ์ ์๋ต
+ expect(countStatsFor([outScope], spy)).toBeGreaterThan(0); // ๋ฏธ๊ฒ์ฆ ํ์ผ์ stat
+ });
+
+ test('์์ฒ ๋ถ๊ฐ(gen=-1) ํ๊ฒฝ์ ๋งค๋ฒ stat (์ข
์ ๋์)', () => {
+ mockGen = -1;
+ const { root, paths } = makeBrain({ 'a.md': '๋ด์ฉ' });
+ getBrainTokenIndex(root, paths);
+ const spy = jest.spyOn(fs, 'statSync');
+ getBrainTokenIndex(root, paths);
+ expect(countStatsFor(paths, spy)).toBeGreaterThan(0);
+ });
+});
diff --git a/tests/computeBudgetedRequest.test.ts b/tests/computeBudgetedRequest.test.ts
index 75bb816..82c2972 100644
--- a/tests/computeBudgetedRequest.test.ts
+++ b/tests/computeBudgetedRequest.test.ts
@@ -56,3 +56,92 @@ describe('computeBudgetedRequest โ real-window alignment', () => {
expect(run({ actualContextLength: NaN }).effectiveContextLength).toBe(32768);
});
});
+
+// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+// [v2.2.311] KV ์บ์ ๋ถ๋ฆฌ โ dynamicContextTail ์ฝ์
๊ณ์ฝ.
+// message[0] ์ ๋ถ๋ณ ์ ์ ํ๋กฌํํธ๋ก ๋จ๊ณ , ๋์ ์ปจํ
์คํธ๋ ๋ง์ง๋ง user ๋ฉ์์ง
+// ์ง์ ์ internal system ๋ฉ์์ง๋ก ๋ค์ด๊ฐ์ผ ํ๋กฌํํธ ํ๋ฆฌํฝ์ค ์บ์๊ฐ ์ฐ๋ค.
+// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+describe('computeBudgetedRequest โ dynamicContextTail (KV cache split)', () => {
+ const HEAD = 'STATIC HEAD PROMPT';
+ const TAIL = '[CURRENT DATE/TIME]\nToday: 2026-07-20\n\n[CONTEXT]\nRAG chunk A\n[/CONTEXT]';
+
+ // ์ฃผ์: default parameter ๋ ๋ช
์์ undefined ์๋ ์ ์ฉ๋๋ฏ๋ก ์ฐ์ง ์๋๋ค โ
+ // "tail ์์" ์ผ์ด์ค๋ฅผ ์ ํํ ํํํ๊ธฐ ์ํด ํญ์ ๋ช
์์ ์ผ๋ก ๋๊ธด๋ค.
+ function runSplit(reqMessages: ChatMessage[], tail: string | undefined) {
+ return computeBudgetedRequest({
+ fullSystemPrompt: HEAD,
+ dynamicContextTail: tail,
+ reqMessages,
+ actualModel: 'some-13b-model',
+ config: baseConfig,
+ imageCount: 0,
+ });
+ }
+
+ test('tail ์ ๋ง์ง๋ง user ๋ฉ์์ง ์ง์ ์ internal system ์ผ๋ก ์ฝ์
๋๋ค', () => {
+ const msgs: ChatMessage[] = [
+ { role: 'user', content: '์ฒซ ์ง๋ฌธ' },
+ { role: 'assistant', content: '์ฒซ ๋ต' },
+ { role: 'user', content: '๋๋ฒ์งธ ์ง๋ฌธ' },
+ ];
+ const r = runSplit(msgs, TAIL);
+ expect(r.messagesForRequest[0]).toMatchObject({ role: 'system', content: HEAD });
+ const idx = r.messagesForRequest.findIndex((m) => m.content === TAIL);
+ expect(idx).toBeGreaterThan(0);
+ expect(r.messagesForRequest[idx].role).toBe('system');
+ expect(r.messagesForRequest[idx + 1]).toMatchObject({ role: 'user', content: '๋๋ฒ์งธ ์ง๋ฌธ' });
+ });
+
+ test('continuation(๋ง์ง๋ง user ๋ค ์ก์
๊ฒฐ๊ณผ system)์ด ์์ด๋ tail ์ user ์', () => {
+ const msgs: ChatMessage[] = [
+ { role: 'user', content: '์กฐ์ฌํด์ค' },
+ { role: 'system', content: '[Result of read_file] ...', internal: true },
+ ];
+ const r = runSplit(msgs, TAIL);
+ const tailIdx = r.messagesForRequest.findIndex((m) => m.content === TAIL);
+ const userIdx = r.messagesForRequest.findIndex((m) => m.role === 'user');
+ const actionIdx = r.messagesForRequest.findIndex((m) => String(m.content).startsWith('[Result of'));
+ expect(tailIdx).toBeLessThan(userIdx);
+ expect(actionIdx).toBeGreaterThan(userIdx);
+ });
+
+ test('user ๋ฉ์์ง๊ฐ ์์ผ๋ฉด tail ์ ํ์คํ ๋ฆฌ ๋์ ๋ถ์ธ๋ค', () => {
+ const msgs: ChatMessage[] = [{ role: 'system', content: 'internal only', internal: true }];
+ const r = runSplit(msgs, TAIL);
+ const last = r.messagesForRequest[r.messagesForRequest.length - 1];
+ expect(last.content).toBe(TAIL);
+ });
+
+ test('tail ๋ฏธ์ง์ ์ด๋ฉด ์ข
์ ๋จ์ผ-์์คํ
๋์ ๊ทธ๋๋ก', () => {
+ const msgs: ChatMessage[] = [{ role: 'user', content: 'hello' }];
+ const r = runSplit(msgs, undefined);
+ expect(r.messagesForRequest).toHaveLength(2);
+ expect(r.messagesForRequest[0].content).toBe(HEAD);
+ });
+
+ test('์์ฐ ์ด๊ณผ ์ [CONTEXT] truncation ์ tail ์ ์ ์ฉ๋๊ณ head ๋ ๋ณด์กด', () => {
+ const hugeTail = `[CONTEXT]\n${'๊ธด ๋ฐฐ๊ฒฝ ์ง์ ์ฒญํฌ. '.repeat(20000)}\n[/CONTEXT]`;
+ const msgs: ChatMessage[] = [{ role: 'user', content: 'hello' }];
+ const r = computeBudgetedRequest({
+ fullSystemPrompt: HEAD,
+ dynamicContextTail: hugeTail,
+ reqMessages: msgs,
+ actualModel: 'some-13b-model',
+ config: { ...baseConfig, contextLength: 8192 },
+ imageCount: 0,
+ });
+ expect(r.systemTruncated).toBe(true);
+ expect(r.messagesForRequest[0].content).toBe(HEAD);
+ const tailMsg = r.messagesForRequest.find((m) => String(m.content).includes('[CONTEXT]'));
+ expect(tailMsg).toBeDefined();
+ expect(String(tailMsg!.content).length).toBeLessThan(hugeTail.length);
+ });
+
+ test('systemTokens ๋ head+tail ํฉ์ฐ์ผ๋ก ๋ณด๊ณ ๋๋ค', () => {
+ const msgs: ChatMessage[] = [{ role: 'user', content: 'hello' }];
+ const withTail = runSplit(msgs, TAIL);
+ const withoutTail = runSplit(msgs, undefined);
+ expect(withTail.systemTokens).toBeGreaterThan(withoutTail.systemTokens);
+ });
+});
diff --git a/tests/investigationPipeline.test.ts b/tests/investigationPipeline.test.ts
index 7e8c570..f82ad65 100644
--- a/tests/investigationPipeline.test.ts
+++ b/tests/investigationPipeline.test.ts
@@ -165,3 +165,29 @@ describe('detectHollowInvestigation โ ํ์กฐ์ฌ ๊ฐ์ง', () => {
expect(footer).toContain('investigate_files');
});
});
+
+// [v2.2.312] ๊ทผ๊ฑฐ ์๋ ๋ถ์ ๊ฐ์ง โ ํ์ผ์ ์ฝ๊ณ ๋ ์ธ์ฉ์ด ์๋ ์ผ๋ฐ๋ก ๋ณด๊ณ .
+import { detectUngroundedAnalysis, formatUngroundedAnalysisFooter } from '../src/intelligence/investigationPipeline';
+
+describe('detectUngroundedAnalysis โ ์ผ๋ฐ๋ก ๋ถ์ ๊ฐ์ง', () => {
+ const generic = '์ด ํ๋ก์ ํธ๋ ๋ก์ปฌ LLM๊ณผ RAG ๊ธฐ์ ์ ๊ฒฐํฉ ์๋๊ฐ ๋งค์ฐ ์ธ์์ ์
๋๋ค. '.repeat(10);
+
+ test('read ๋ฅผ ํ๊ณ ๋ ํ์ผ ์ธ์ฉ ์๋ ์ฅ๋ฌธ = ungrounded', () => {
+ const r = detectUngroundedAnalysis(generic, { reads: 3, lists: 1, investigates: 0 });
+ expect(r.ungrounded).toBe(true);
+ expect(r.readCount).toBe(3);
+ });
+ test('ํ์ผ ๊ทผ๊ฑฐ๋ฅผ 2๊ฐ ์ด์ ์ธ์ฉํ๋ฉด ์ ์', () => {
+ const grounded = `${generic} ๊ทผ๊ฑฐ: agent.ts ์ handlePrompt ์ memoryContext.ts ์ buildMemoryContext ๋ฅผ ํ์ธํจ.`;
+ expect(detectUngroundedAnalysis(grounded, { reads: 3, lists: 1, investigates: 0 }).ungrounded).toBe(false);
+ });
+ test('read ๊ฐ ์์ผ๋ฉด ํ์ ๋์ ์๋ (hollow ๊ฐ์ง์ ์์ญ)', () => {
+ expect(detectUngroundedAnalysis(generic, { reads: 0, lists: 1, investigates: 0 }).ungrounded).toBe(false);
+ });
+ test('์งง์ ํ์ธํ ์๋ต์ ์คํ ๋ฐฉ์ง๋ก ์ ์ธ', () => {
+ expect(detectUngroundedAnalysis('๋ค, ์๋ฒ ์ ์์
๋๋ค.', { reads: 1, lists: 0, investigates: 0 }).ungrounded).toBe(false);
+ });
+ test('footer ์ ์ฝ์ ํ์ ํฌํจ', () => {
+ expect(formatUngroundedAnalysisFooter(3)).toContain('3ํ');
+ });
+});
diff --git a/tests/promptSplit.test.ts b/tests/promptSplit.test.ts
new file mode 100644
index 0000000..919a6fb
--- /dev/null
+++ b/tests/promptSplit.test.ts
@@ -0,0 +1,60 @@
+/**
+ * [v2.2.311] KV ์บ์ ์นํ ํ๋กฌํํธ ๋ถ๋ฆฌ โ ์ ์ /๋์ ๊ฒฝ๊ณ ๊ณ์ฝ.
+ *
+ * ์ ์ ํ๋กฌํํธ(getStaticSystemPrompt)์ ๋ถ ๋จ์ ์๊ฐ ๊ฐ์ ๊ฐ๋ณ ์์๊ฐ ์์ด๋ ์๊ฐ
+ * llama.cpp prompt cache ๋ ์์ฒญ ์ฒซ ํ ํฐ๋ถํฐ ๋ฌดํจํ๋๋ค (์ค์ธก: 13k ํ ํฐ ์ ์ฒด ์ฌํ๋ฆฌํ
+ * โ 90์ด/ํด). ์ด ํ
์คํธ๋ ๊ทธ ํ๊ท๋ฅผ ๋ง๋ ๊ฐ๋๋ ์ผ์ด๋ค.
+ */
+import { getStaticSystemPrompt, getDateTimeContextBlock, getSystemPrompt } from '../src/utils';
+import { buildAstraModeSystemPrompt } from '../src/agent/handlePrompt/buildAstraModeSystemPrompt';
+
+describe('์ ์ /๋์ ์์คํ
ํ๋กฌํํธ ๋ถ๋ฆฌ', () => {
+ test('์ ์ ํ๋กฌํํธ์๋ ๋ ์ง/์๊ฐ์ด ์๋ค (์บ์ ๋ฌดํจํ ๊ฐ๋)', () => {
+ const s = getStaticSystemPrompt();
+ expect(s).not.toContain('[CURRENT DATE/TIME]');
+ // ์ค๋ ๋ ์ง ๋ฌธ์์ด์ด ์ด๋ค ํํ๋ก๋ ๋ค์ด์์ผ๋ฉด ์ ๋๋ค.
+ const iso = new Date().toISOString().split('T')[0];
+ expect(s).not.toContain(iso);
+ });
+
+ test('์ ์ ํ๋กฌํํธ๋ ์ฐ์ ํธ์ถ์์ ๋์ผํ๋ค (ํ๋ฆฌํฝ์ค ๋ถ๋ณ)', () => {
+ expect(getStaticSystemPrompt()).toBe(getStaticSystemPrompt());
+ });
+
+ test('๋ ์ง ๋ธ๋ก์ ์ค๋ ๋ ์ง๋ฅผ ํฌํจํ๋ค', () => {
+ const block = getDateTimeContextBlock();
+ expect(block).toContain('[CURRENT DATE/TIME]');
+ expect(block).toContain(new Date().toISOString().split('T')[0]);
+ });
+
+ test('๋ ๊ฑฐ์ getSystemPrompt ๋ ์ ์ ๋ณธ๋ฌธ + ๋ ์ง ๋ธ๋ก์ ๋ชจ๋ ํฌํจ (ํธํ)', () => {
+ const s = getSystemPrompt();
+ expect(s).toContain('[CURRENT DATE/TIME]');
+ expect(s).toContain('[์ถ๋ ฅ ์์ ๊ท์น โ ๋ฐ๋์ ์ค์]');
+ });
+
+ test('๋น๋์ systemPrompt="" ๋ฅผ ๋๊ฒจ๋ [CONTEXT] ๊ณจ๊ฒฉ์ ์ ์ง๋๋ค (tail ์์ฑ ๊ฒฝ๋ก)', () => {
+ const tail = buildAstraModeSystemPrompt({
+ prompt: '์ง๋ฌธ',
+ systemPrompt: '',
+ modeBridgeCtx: '',
+ priorConclusionCtx: '',
+ designerCtx: '',
+ projectArchitectureCtx: '',
+ secondBrainTraceCtx: '',
+ memoryCtx: 'RAG ๋ณธ๋ฌธ',
+ knowledgeContextForPrompt: '',
+ contextBlock: '์ด๋ฆฐ ํ์ผ ๋ด์ฉ',
+ negativeCtx: '',
+ isCasualConversation: false,
+ localPathContext: '',
+ knowledgeMix: null,
+ dynamicBlocks: new Map([['behavior-constraints', '[GROUNDING] ๊ทผ๊ฑฐ ๊ฐ๋: ๋ณดํต']]),
+ });
+ expect(tail).toContain('[CONTEXT]');
+ expect(tail).toContain('RAG ๋ณธ๋ฌธ');
+ expect(tail).toContain('์ด๋ฆฐ ํ์ผ ๋ด์ฉ');
+ expect(tail).toContain('[GROUNDING]');
+ expect(tail.startsWith('You are Astra')).toBe(false); // ์ ์ ๋ณธ๋ฌธ์ด ์์ด๋ฉด ์ ๋จ
+ });
+});
diff --git a/tests/requirementGraph.test.ts b/tests/requirementGraph.test.ts
index a7d218a..65f994e 100644
--- a/tests/requirementGraph.test.ts
+++ b/tests/requirementGraph.test.ts
@@ -124,3 +124,23 @@ describe('DEFAULT_TASK_REQUIREMENTS ๋ฌด๊ฒฐ์ฑ', () => {
}
});
});
+
+// [v2.2.312] ๋ถ์ ๋ณด๊ณ ์ ํ โ "ํ๋ก์ ํธ ๋ถ์ํ๊ณ ๋ณด๊ณ ํด์ค" ์ค์ฌ๋ก๊ฐ ์ ํ ๋ฏธ๊ฐ์ง๋ก
+// ์ผ๋ฐ๋ก ์ดํ์ด ๋๋ ๊ฐญ์ ๋ง๋๋ค.
+describe('detectTaskType โ analysis-report (v2.2.312)', () => {
+ test('์ค์ฌ๋ก ๋ฌธ์ฅ์ ์ ํํ ๊ฐ์งํ๋ค', () => {
+ expect(detectTaskType('E:\Wiki\connectai ํ๋ก์ ํธ๋ฅผ ๋ถ์ํ๊ณ ์ด๋ป๊ฒ ๋์ด ์๋์ง ๋ณด๊ณ ํด์ค')?.id).toBe('analysis-report');
+ expect(detectTaskType('์งํ ํํฉ ๋ณด๊ณ ๋ถํํด')?.id).toBe('analysis-report');
+ expect(detectTaskType('์ด ์ฝ๋ ๊ฒํ ํด์ ๋ณด๊ณ ํด์ค')?.id).toBe('analysis-report');
+ });
+ test('๋ ๊ตฌ์ฒด์ ์ธ ์ ํ์ด ์ฐ์ ํ๋ค (ํ์๋กยท์์ฅ์กฐ์ฌ)', () => {
+ expect(detectTaskType('ํ์๋ก์ผ๋ก ์ ๋ฆฌํด์ ๋ณด๊ณ ํด์ค')?.id).toBe('meeting-minutes');
+ expect(detectTaskType('์์ฅ์กฐ์ฌ ๊ฒฐ๊ณผ ๋ณด๊ณ ํด์ค')?.id).toBe('market-research');
+ });
+ test('๋ธ๋ก์ ํ์ผ ๊ทผ๊ฑฐ ์์์ ์น์
๊ท์น ์ค๋ฒ๋ผ์ด๋๊ฐ ํฌํจ๋๋ค', () => {
+ const block = buildRequirementGraphBlock('ํ๋ก์ ํธ ๋ถ์ํ๊ณ ๋ณด๊ณ ํด์ค');
+ expect(block).toContain('ํ์ผ ๊ทผ๊ฑฐ');
+ expect(block).toContain('๋ณด๊ณ ๊ฐ์');
+ expect(block).toContain('์ต๋ 3์น์
');
+ });
+});
diff --git a/tests/stripForDisplay.test.ts b/tests/stripForDisplay.test.ts
new file mode 100644
index 0000000..f8a4f92
--- /dev/null
+++ b/tests/stripForDisplay.test.ts
@@ -0,0 +1,35 @@
+/**
+ * [v2.2.312] ์ค๊ฐ ๋ผ์ด๋ ํ์์ฉ ์ก์
ํ๊ทธ ์ ๊ฑฐ โ "## 4.๋ถํฐ ์์ํ๋ ๋ณด๊ณ ์" ๋ฒ๊ทธ์
+ * ํ์ ๊ฒฝ๋ก ํฌํผ. ํ๊ทธ๋ ๊ฑท์ด๋ด๋ ๋ณธ๋ฌธ ์น์
์ ๋ณด์กดํด์ผ ํ๋ค.
+ */
+import { stripActionTagsForDisplay } from '../src/agent/actions/stripForDisplay';
+
+describe('stripActionTagsForDisplay', () => {
+ test('์๊ธฐ๋ซํ ํ๊ทธ ์ ๊ฑฐ + ๋ณธ๋ฌธ ๋ณด์กด', () => {
+ const input = '## 1. ํ๋ก์ ํธ ๊ฐ์\n๊ตฌ์กฐ๋ฅผ ํ์ธํ๋ค.\n\n';
+ const out = stripActionTagsForDisplay(input);
+ expect(out).toContain('## 1. ํ๋ก์ ํธ ๊ฐ์');
+ expect(out).not.toContain(' {
+ const input = '๋ถ์ ์์.\ncd x; npm test\n๋ณธ๋ฌธ\n๋.';
+ const out = stripActionTagsForDisplay(input);
+ expect(out).toContain('๋ถ์ ์์.');
+ expect(out).toContain('๋.');
+ expect(out).not.toContain('npm test');
+ expect(out).not.toContain(' {
+ const out = stripActionTagsForDisplay('์ ๋ค');
+ expect(out).not.toContain(' {
+ expect(stripActionTagsForDisplay('## 2. ๊ตฌ์กฐ\n- src/agent.ts')).toBe('## 2. ๊ตฌ์กฐ\n- src/agent.ts');
+ });
+ test('๋น ์
๋ ฅ ์์ ', () => {
+ expect(stripActionTagsForDisplay('')).toBe('');
+ });
+});