Date: 2026-06-17
Version: v1.5.12+ (patch)
Status: โ
All fixes applied and verified
This document details the investigation, diagnosis, and resolution of 4 critical bugs in the Auto-Track token threshold system that prevented automatic session memory saving from working correctly when users interacted with checkpoint prompts.
| Issue | Severity | Root Cause | Status |
|---|---|---|---|
| #1: Config Default Mismatch | ๐ข Low | Constructor defaults contradicted schema/DEFAULT_CONFIG | โ Fixed |
| #2: Dead Code Path | ๐ข Low | Unused getAndClearPendingWarning() method | โ Removed |
| #3: "NO" Reply Warning Loop | ๐ด Medium | Flag never reset on decline โ warning repeats forever | โ Fixed |
| #4: Buffer Auto-Flush Race Condition | ๐ก Medium | Concurrent flushes from checkpoint save + buffer overflow | โ Fixed |
Impact: If someone instantiated new AutoTracker() directly (bypassing the preprocessor's updateConfig() call), auto-tracking would be disabled by default, contradicting both the schema and UI config which expect it to be true.
Verification: All three defaults now align โ constructor, Zod schema, and DEFAULT_CONFIG all use true for autoTrackingEnabled.
The method getAndClearPendingWarning() was defined but never called anywhere in the codebase. It's an exact duplicate of consumePendingConfirmation():
Impact: None functionally, but adds dead code that could confuse future maintainers about which method to use.
getAndClearPendingWarning() method (7 lines)consumePendingConfirmation() remains as the canonical methodThis was the most impactful bug causing user frustration:
| Turn | Action | State | Result |
|---|---|---|---|
| Turn 1 (threshold reached) | checkAndGeneratePrompt() called | Sets lastTokenThresholdCheck = true โ
| Warning shown to user โ |
| Turn 2 (user says "NO") | Goes to else branch โ consumePendingConfirmation() returns warning, clears it โ re-injects into pendingWarning โ | lastTokenThresholdCheck still true, warning text loops forever | User sees same warning every turn until session ends ๐ |
Meanwhile, lastTokenThresholdCheck was set to true in Turn 1 and never reset on "NO", so the threshold could never re-evaluate even if token usage climbed higher.
YES Reply:
resetTokenThreshold() โ clears lastTokenThresholdCheckcheckAndSaveTokenThreshold() โ passes threshold check, saves checkpoint โ
pendingWarning = undefined โ no repeat โ
NO Reply (FIXED):
consumePendingConfirmation() โ returns & clears the warning textresetTokenThreshold() โ resets flag for next evaluationpendingWarning = undefined โ doesn't re-injectScenario: When buffer hits 51 entries while a checkpoint save is mid-flush:
flushActionsToMemory() calls (auto-flush + checkpoint save) run concurrentlyContextStorageManager.load()/save() (read-modify-write without locking)Added an isFlushing guard flag:
flushActionsToMemory(): Returns early if already flushing, ensuring only one flush runs at a time (lines 190โ219). Uses finally block to guarantee reset even on error.!this.isFlushing, preventing concurrent attempts from buffer overflow path and checkpoint save path.| Turn | User Action | lastTokenThresholdCheck | pendingCheckpointWarning | isFlushing | Result |
|---|---|---|---|---|---|
| 1 | Normal message โ threshold reached | Set to true by checkAndGeneratePrompt() | Warning stored โ | false | AI shows warning prompt โ |
| 2a | User says "YES" | Reset to false by resetTokenThreshold() | Cleared โ | false โ true (during save) โ false | Checkpoint saved โ |
| 2b | User says "NO" | Reset to false by NEW resetTokenThreshold() call | Cleared, NOT re-injected โ | false | No warning loop, flag reset for next climb โ |
| 3+ (after NO) |
npm run typecheck โ 0 errors)getAndClearPendingWarning() deleted, verified via grep)true)[AutoTracker] Session memory checkpoint saved successfullyisFlushing guard in place with try/finally cleanup| File | Lines Changed | Type of Change |
|---|---|---|
src/autoTracker.ts | 3 locations | Bug fixes (#1, #2, #4) |
src/promptPreprocessor.ts | 1 location (lines 371โ380) | Bug fix (#3 โ critical UX) |
Location: src/contextGuard.ts vs autoTracker.ts
Problem: ContextGuard adds ~8 tokens for BOS/system overhead, but AutoTracker's percentage calculation uses raw count. At exactly 74% by raw count, the AI might actually be at ~76% effective usage โ triggering warning slightly earlier than intended.
Impact: Very minor (~0.1-0.2% at typical context sizes). Only noticeable in very short sessions where 8 tokens represents a larger fraction of total usage.
Recommendation: Document this behavior or adjust threshold calculation to account for offset in next iteration. Not critical for current release.
All 4 identified issues have been resolved with minimal, surgical changes:
The auto-track token threshold system now works correctly for both YES and NO user replies, with proper guard mechanisms preventing race conditions during concurrent flush operations.
| More messages โ tokens climb higher |
checkAndGeneratePrompt() sees !flag && usage >= threshold โ triggers fresh prompt |
| New warning stored โ |
| โ |
| Fresh prompt shown (not repeated old one) โ |
// src/autoTracker.ts line 90 (BEFORE):
autoTrackingEnabled: false, // โ Constructor default is FALSE
// But in src/config.ts:
autoTrackingEnabled: z.boolean().default(true), // โ Schema default is TRUE
autoTrackingEnabled: true, // โ DEFAULT_CONFIG is TRUE
// src/autoTracker.ts line 90 (AFTER):
autoTrackingEnabled: true, // โ Matches schema & DEFAULT_CONFIG default (true)
// src/autoTracker.ts (BEFORE): lines 183-190
/** Consume and clear the pending warning (legacy alias) */
getAndClearPendingWarning(): string | undefined {
const warn = this.pendingCheckpointWarning;
if (warn) { this.pendingCheckpointWarning = undefined; }
return warn;
}
// src/promptPreprocessor.ts (BEFORE): lines 371-375
} else {
// Consume any pending warning (clears it so it doesn't repeat)
const warn = autoTracker.consumePendingConfirmation();
if (!warn) { /* check fresh */ }
else { pendingWarning = warn; } // โ BUG: Re-injects the same warning text!
}
// src/promptPreprocessor.ts (AFTER): lines 371-380
} else {
// User said "NO" โ reset flag so it can re-evaluate on next token climb, and clear warning
autoTracker.resetTokenThreshold(); // ๐น Reset flag for fresh evaluation
console.warn('[Auto-Track] User declined checkpoint โ threshold flag reset for next evaluation');
pendingWarning = undefined; // ๐น FIX: Don't re-inject the same warning forever
}
// src/autoTracker.ts (BEFORE): lines 357-362
if (this.actionBuffer.length > 50) {
console.warn('[AutoTracker] Buffer exceeded safety limit, flushing early...');
void this.flushActionsToMemory(); // ๐น Fire-and-forget โ intentionally unawaited
}
// src/autoTracker.ts line 83 (NEW):
private isFlushing = false;
// src/autoTracker.ts lines 190-219 (AFTER):
async flushActionsToMemory(): Promise<number> {
// ๐น FIX #4: Prevent concurrent flushes (race condition with checkpoint save)
if (this.isFlushing || this.actionBuffer.length === 0) return 0;
this.isFlushing = true;
const flushed = this.actionBuffer.splice(0);
let savedCount = 0;
try {
// ... flush logic ...
} catch (error) {
console.error(`[AutoTracker] Failed to flush actions: ${message}`);
} finally {
// ๐น Always reset guard, even on failure
this.isFlushing = false;
}
return savedCount;
}
// src/autoTracker.ts line 358 (AFTER):
if (this.actionBuffer.length > 50 && !this.isFlushing) { // โ Added guard check
User Message Arrives
โ
โผ
Step 0.5: ContextGuard Token Counting
โ
โโโ Check if autoTrackingEnabled && hasPendingWarning()
โ โ
โ โโโ YES reply? โ resetTokenThreshold() โ checkAndSaveTokenThreshold() โ SAVE โ
โ โ
โ โโโ NO reply? โ resetTokenThreshold() + pendingWarning=undefined โ CLEAR โ
โ
โโโ No prior warning?
โ
โโโ checkAndGeneratePrompt():
โโโ usagePercentage >= threshold?
โ โโโ YES โ Set lastTokenThresholdCheck=true, store warning โ
โ โโโ NO โ Skip (not at threshold yet)
โ
โผ
Return { triggered: true, warning } to inject into prompt
# autoTracker.ts
+ private isFlushing = false; // Line 83 โ NEW guard flag
- autoTrackingEnabled: false,
+ autoTrackingEnabled: true, // Line 90 โ Match schema default
- /** legacy alias method */ getAndClearPendingWarning() { ... } // DELETED
+ // Method removed entirely (dead code)
# Buffer overflow check
- if (this.actionBuffer.length > 50) {
+ if (this.actionBuffer.length > 50 && !this.isFlushing) {
# flushActionsToMemory() guard
+ if (this.isFlushing || this.actionBuffer.length === 0) return 0;
+ this.isFlushing = true;
...
+ } finally { this.isFlushing = false; }
# promptPreprocessor.ts
- else { pendingWarning = warn; }
+ else {
+ autoTracker.resetTokenThreshold();
+ console.warn('[Auto-Track] User declined checkpoint โ threshold flag reset for next evaluation');
+ pendingWarning = undefined; // Don't re-inject warning forever
+ }