Skip to content

Commit eb48536

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

5 files changed

Lines changed: 101 additions & 2 deletions

File tree

CHANGELOG.md

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

99
- Sync failure messages now distinguish **invalid repository URLs** (e.g. sub-paths like `/tree/main/...`) from **incompatible repository structures** (repo is reachable but has no supported prompt folders). The summary toast shows a categorised count, and "Show Details" lists the specific reason per repository.
1010

11+
### Added
12+
13+
- 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.
14+
- Disabled (inactive) prompts are now cleaned up from the active prompts directory during sync, preventing them from being loaded by Copilot.
15+
1116
## [1.5.4] - 2026-01-12
1217

1318
### Fixed

src/constant.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,9 @@ 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;

src/syncManager.ts

Lines changed: 63 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, } 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) {
@@ -1290,6 +1297,60 @@ export class SyncManager {
12901297
}
12911298
}
12921299

1300+
/**
1301+
* Validate total instructions size and count, warning the user if thresholds are exceeded.
1302+
* Instructions (.instructions.md) are automatically loaded into Copilot's context window,
1303+
* unlike .prompt.md files which are invoked on-demand.
1304+
*/
1305+
private async validateInstructionSize(): Promise<void> {
1306+
try {
1307+
const promptsDir = this.config.getPromptsDirectory();
1308+
1309+
// Ensure the prompts directory exists
1310+
await this.fileSystem.ensureDirectoryExists(promptsDir);
1311+
1312+
const entries = await this.fileSystem.readDirectory(promptsDir);
1313+
// Only count instructions files — they are auto-loaded into Copilot's context
1314+
const instructionFiles = entries.filter(f => f.endsWith('.instructions.md'));
1315+
const activeCount = instructionFiles.length;
1316+
1317+
// Calculate total size of active instruction files
1318+
let totalSize = 0;
1319+
1320+
for (const file of instructionFiles) {
1321+
try {
1322+
const filePath = this.fileSystem.joinPath(promptsDir, file);
1323+
totalSize += await this.fileSystem.getFileSize(filePath);
1324+
} catch {
1325+
this.logger.debug(`Skipped unreadable instruction file: ${file}`);
1326+
}
1327+
}
1328+
1329+
const totalSizeKB = totalSize / 1024;
1330+
const reasons: string[] = [];
1331+
1332+
if (totalSize > INSTRUCTION_SIZE_WARNING_THRESHOLD_BYTES) {
1333+
reasons.push(`total size (${totalSizeKB.toFixed(0)} KB) exceeds ${(INSTRUCTION_SIZE_WARNING_THRESHOLD_BYTES / 1024).toFixed(0)} KB`);
1334+
}
1335+
if (activeCount > INSTRUCTION_COUNT_WARNING_THRESHOLD) {
1336+
reasons.push(`${activeCount} active instructions exceeds limit of ${INSTRUCTION_COUNT_WARNING_THRESHOLD}`);
1337+
}
1338+
1339+
if (reasons.length > 0) {
1340+
this.logger.warn(`Instructions size safeguard triggered: ${reasons.join('; ')}`);
1341+
await this.notifications.showPromptSizeWarning({
1342+
totalSizeKB,
1343+
activeCount,
1344+
reasons
1345+
});
1346+
} else {
1347+
this.logger.debug(`Instructions size check passed: ${activeCount} instructions, ${totalSizeKB.toFixed(1)} KB`);
1348+
}
1349+
} catch (error) {
1350+
this.logger.error('Failed to validate prompt size', error instanceof Error ? error : undefined);
1351+
}
1352+
}
1353+
12931354
dispose(): void {
12941355
this.logger.info('Disposing SyncManager...');
12951356

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
@@ -182,4 +182,27 @@ export class NotificationManager {
182182
await this.showError(`Failed to setup Azure DevOps authentication: ${errorMessage}`);
183183
}
184184
}
185+
186+
/**
187+
* Show a warning when the total instructions size or count exceeds safe thresholds.
188+
* Instructions are auto-loaded into Copilot's context, unlike prompts which are on-demand.
189+
*/
190+
async showPromptSizeWarning(details: { totalSizeKB: number; activeCount: number; reasons: string[] }): Promise<void> {
191+
const reasonText = details.reasons.join('; ');
192+
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). ${reasonText}. Consider disabling unused instructions.`;
193+
194+
// Always show this warning regardless of notification settings — it's a safeguard
195+
const result = await vscode.window.showWarningMessage(
196+
message,
197+
'Manage Prompts',
198+
'Open Settings',
199+
'Dismiss'
200+
);
201+
202+
if (result === 'Manage Prompts') {
203+
vscode.commands.executeCommand('promptitude.cards.focus');
204+
} else if (result === 'Open Settings') {
205+
vscode.commands.executeCommand('workbench.action.openSettings', 'promptitude');
206+
}
207+
}
185208
}

0 commit comments

Comments
 (0)