Skip to content

Commit a054e9f

Browse files
fix: add instruction size safeguard to prevent Copilot context overflow
1 parent b2f75d0 commit a054e9f

5 files changed

Lines changed: 108 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to the "promptitude" extension will be documented in this fi
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- Instructions size safeguard: warns when active instructions (`.instructions.md`) may overwhelm GitHub Copilot's context window. Checks total instructions file size (>500 KB), active instructions count (>50), and repository count (>10) after each sync. Only instructions are counted since they are auto-loaded into context, unlike `.prompt.md` files which are invoked on-demand.
10+
- Disabled (inactive) prompts are now cleaned up from the active prompts directory during sync, preventing them from being loaded by Copilot.
11+
712
## [1.5.4] - 2026-01-12
813

914
### Fixed

src/constant.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@ export const REPO_SYNC_CHAT_MODE_LEGACY_PATH = `chatmodes/`;
44
export const REPO_SYNC_CHAT_MODE_LEGACY_SINGULAR_PATH = `chatmode/`;
55
export const REPO_SYNC_INSTRUCTIONS_PATH = `instructions/`;
66
export const REPO_SYNC_PROMPT_PATH = `prompts/`;
7+
8+
// Instructions size safeguard thresholds
9+
/** Maximum total size (in bytes) of all active instructions before warning the user */
10+
export const INSTRUCTION_SIZE_WARNING_THRESHOLD_BYTES = 500 * 1024; // 500 KB
11+
/** Maximum number of active instructions before warning the user */
12+
export const INSTRUCTION_COUNT_WARNING_THRESHOLD = 50;
13+
/** Maximum number of configured repositories before warning the user */
14+
export const REPO_COUNT_WARNING_THRESHOLD = 10;

src/syncManager.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { GitProviderFactory } from './utils/gitProviderFactory';
1010
import { FileSystemManager } from './utils/fileSystem';
1111
import { AzureDevOpsApiManager } from './utils/azureDevOps';
1212
import { PromptTreeDataProvider } from './ui/promptTreeProvider';
13-
import { REPO_SYNC_CHAT_MODE_PATH, REPO_SYNC_CHAT_MODE_LEGACY_PATH, REPO_SYNC_CHAT_MODE_LEGACY_SINGULAR_PATH, REPO_SYNC_INSTRUCTIONS_PATH, REPO_SYNC_PROMPT_PATH, } from './constant';
13+
import { REPO_SYNC_CHAT_MODE_PATH, REPO_SYNC_CHAT_MODE_LEGACY_PATH, REPO_SYNC_CHAT_MODE_LEGACY_SINGULAR_PATH, REPO_SYNC_INSTRUCTIONS_PATH, REPO_SYNC_PROMPT_PATH, INSTRUCTION_SIZE_WARNING_THRESHOLD_BYTES, INSTRUCTION_COUNT_WARNING_THRESHOLD, REPO_COUNT_WARNING_THRESHOLD, } from './constant';
1414
export interface SyncResult {
1515
success: boolean;
1616
itemsUpdated: number;
@@ -102,12 +102,18 @@ export class SyncManager {
102102
// Recreate symlinks for active prompts (in case they were manually deleted)
103103
await this.recreateActivePromptSymlinks();
104104

105+
// Remove disabled prompts from the active prompts directory
106+
await this.cleanupInactivePrompts();
107+
105108
// Clean up orphaned regular files in prompts directory
106109
const cleanup = await this.cleanupOrphanedPrompts();
107110
if (cleanup.removed > 0) {
108111
this.logger.info(`Cleaned up ${cleanup.removed} orphaned prompt files`);
109112
}
110113

114+
// Validate total instruction size to prevent Copilot context overflow
115+
await this.validateInstructionSize();
116+
111117
// Update status based on overall result
112118
if (result.overallSuccess) {
113119
this.statusBar.setStatus(SyncStatus.Success);
@@ -289,7 +295,8 @@ export class SyncManager {
289295
}
290296

291297
const filePath = this.fileSystem.joinPath(promptsDir, fileName);
292-
const matchingPrompt = allPrompts.find(prompt => prompt.name === fileName);
298+
// Match on both prompt.name and prompt.workspaceName to handle disambiguated files (e.g., prompt@org-repo.md)
299+
const matchingPrompt = allPrompts.find(prompt => prompt.name === fileName || prompt.workspaceName === fileName);
293300

294301
// Remove file if prompt exists but is not active
295302
if (matchingPrompt && !matchingPrompt.active) {
@@ -1226,6 +1233,65 @@ export class SyncManager {
12261233
}
12271234
}
12281235

1236+
/**
1237+
* Validate total instructions size and count, warning the user if thresholds are exceeded.
1238+
* Instructions (.instructions.md) are automatically loaded into Copilot's context window,
1239+
* unlike .prompt.md files which are invoked on-demand.
1240+
*/
1241+
private async validateInstructionSize(): Promise<void> {
1242+
try {
1243+
const promptsDir = this.config.getPromptsDirectory();
1244+
1245+
// Ensure the prompts directory exists
1246+
await this.fileSystem.ensureDirectoryExists(promptsDir);
1247+
1248+
const entries = await this.fileSystem.readDirectory(promptsDir);
1249+
// Only count instructions files — they are auto-loaded into Copilot's context
1250+
const instructionFiles = entries.filter(f => f.endsWith('.instructions.md'));
1251+
const activeCount = instructionFiles.length;
1252+
1253+
// Calculate total size of active instruction files
1254+
let totalSize = 0;
1255+
1256+
for (const file of instructionFiles) {
1257+
try {
1258+
const filePath = this.fileSystem.joinPath(promptsDir, file);
1259+
totalSize += await this.fileSystem.getFileSize(filePath);
1260+
} catch {
1261+
this.logger.debug(`Skipped unreadable instruction file: ${file}`);
1262+
}
1263+
}
1264+
1265+
const repoCount = this.config.repositories.length;
1266+
const totalSizeKB = totalSize / 1024;
1267+
const reasons: string[] = [];
1268+
1269+
if (totalSize > INSTRUCTION_SIZE_WARNING_THRESHOLD_BYTES) {
1270+
reasons.push(`total size (${totalSizeKB.toFixed(0)} KB) exceeds ${(INSTRUCTION_SIZE_WARNING_THRESHOLD_BYTES / 1024).toFixed(0)} KB`);
1271+
}
1272+
if (activeCount > INSTRUCTION_COUNT_WARNING_THRESHOLD) {
1273+
reasons.push(`${activeCount} active instructions exceeds limit of ${INSTRUCTION_COUNT_WARNING_THRESHOLD}`);
1274+
}
1275+
if (repoCount > REPO_COUNT_WARNING_THRESHOLD && activeCount > 0) {
1276+
reasons.push(`${repoCount} repositories exceeds limit of ${REPO_COUNT_WARNING_THRESHOLD}`);
1277+
}
1278+
1279+
if (reasons.length > 0) {
1280+
this.logger.warn(`Instructions size safeguard triggered: ${reasons.join('; ')}`);
1281+
await this.notifications.showPromptSizeWarning({
1282+
totalSizeKB,
1283+
activeCount,
1284+
repoCount,
1285+
reasons
1286+
});
1287+
} else {
1288+
this.logger.debug(`Instructions size check passed: ${activeCount} instructions, ${totalSizeKB.toFixed(1)} KB, ${repoCount} repos`);
1289+
}
1290+
} catch (error) {
1291+
this.logger.error('Failed to validate prompt size', error instanceof Error ? error : undefined);
1292+
}
1293+
}
1294+
12291295
dispose(): void {
12301296
this.logger.info('Disposing SyncManager...');
12311297

src/utils/fileSystem.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export class FileSystemManager {
5858
}
5959
}
6060

61+
async getFileSize(filePath: string): Promise<number> {
62+
const stats = await stat(filePath);
63+
return stats.size;
64+
}
6165

6266
joinPath(...paths: string[]): string {
6367
return path.join(...paths);

src/utils/notifications.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,27 @@ export class NotificationManager {
162162
await this.showError(`Failed to setup Azure DevOps authentication: ${errorMessage}`);
163163
}
164164
}
165+
166+
/**
167+
* Show a warning when the total instructions size or count exceeds safe thresholds.
168+
* Instructions are auto-loaded into Copilot's context, unlike prompts which are on-demand.
169+
*/
170+
async showPromptSizeWarning(details: { totalSizeKB: number; activeCount: number; repoCount: number; reasons: string[] }): Promise<void> {
171+
const reasonText = details.reasons.join('; ');
172+
const message = `⚠️ Promptitude: Your active instructions may be too large for GitHub Copilot's context window (${details.totalSizeKB.toFixed(0)} KB across ${details.activeCount} instructions from ${details.repoCount} repos). ${reasonText}. Consider disabling unused instructions or reducing synced repositories.`;
173+
174+
// Always show this warning regardless of notification settings — it's a safeguard
175+
const result = await vscode.window.showWarningMessage(
176+
message,
177+
'Manage Prompts',
178+
'Open Settings',
179+
'Dismiss'
180+
);
181+
182+
if (result === 'Manage Prompts') {
183+
vscode.commands.executeCommand('promptitude.cards.focus');
184+
} else if (result === 'Open Settings') {
185+
vscode.commands.executeCommand('workbench.action.openSettings', 'promptitude');
186+
}
187+
}
165188
}

0 commit comments

Comments
 (0)