@juspay/yama 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -569,6 +569,7 @@ export interface TokenBudgetManagerInterface {
569
569
  getAvailableBudget(): number;
570
570
  getTotalBudget(): number;
571
571
  getUsedTokens(): number;
572
+ preAllocateAllBatches(allocations: Map<number, number>): boolean;
572
573
  }
573
574
  export declare class GuardianError extends Error {
574
575
  code: string;
@@ -584,4 +585,40 @@ export declare class ProviderError extends GuardianError {
584
585
  export declare class ValidationError extends GuardianError {
585
586
  constructor(message: string, context?: any);
586
587
  }
588
+ export declare enum CacheErrorCode {
589
+ CACHE_SYSTEM_FAILURE = "CACHE_SYSTEM_FAILURE",
590
+ CACHE_MEMORY_EXHAUSTED = "CACHE_MEMORY_EXHAUSTED",
591
+ CACHE_INITIALIZATION_FAILED = "CACHE_INITIALIZATION_FAILED",
592
+ CACHE_STORAGE_FULL = "CACHE_STORAGE_FULL",
593
+ CACHE_STORAGE_PERMISSION = "CACHE_STORAGE_PERMISSION",
594
+ CACHE_STORAGE_CORRUPTION = "CACHE_STORAGE_CORRUPTION",
595
+ CACHE_NETWORK_CONNECTION = "CACHE_NETWORK_CONNECTION",
596
+ CACHE_NETWORK_TIMEOUT = "CACHE_NETWORK_TIMEOUT",
597
+ CACHE_NETWORK_AUTH = "CACHE_NETWORK_AUTH",
598
+ CACHE_CONFIG_INVALID = "CACHE_CONFIG_INVALID",
599
+ CACHE_CONFIG_MISSING = "CACHE_CONFIG_MISSING",
600
+ CACHE_OPERATION_FAILED = "CACHE_OPERATION_FAILED",
601
+ CACHE_SERIALIZATION_ERROR = "CACHE_SERIALIZATION_ERROR",
602
+ CACHE_KEY_INVALID = "CACHE_KEY_INVALID"
603
+ }
604
+ export declare abstract class CacheError extends GuardianError {
605
+ operation?: string | undefined;
606
+ key?: string | undefined;
607
+ constructor(code: CacheErrorCode, message: string, operation?: string | undefined, key?: string | undefined, context?: any);
608
+ }
609
+ export declare class CacheSystemError extends CacheError {
610
+ constructor(message: string, operation?: string, key?: string, context?: any);
611
+ }
612
+ export declare class CacheStorageError extends CacheError {
613
+ constructor(code: CacheErrorCode | undefined, message: string, operation?: string, key?: string, context?: any);
614
+ }
615
+ export declare class CacheNetworkError extends CacheError {
616
+ constructor(code: CacheErrorCode | undefined, message: string, operation?: string, key?: string, context?: any);
617
+ }
618
+ export declare class CacheConfigurationError extends CacheError {
619
+ constructor(code: CacheErrorCode | undefined, message: string, operation?: string, key?: string, context?: any);
620
+ }
621
+ export declare class CacheOperationError extends CacheError {
622
+ constructor(code: CacheErrorCode | undefined, message: string, operation?: string, key?: string, context?: any);
623
+ }
587
624
  //# sourceMappingURL=index.d.ts.map
@@ -34,6 +34,71 @@ export class ValidationError extends GuardianError {
34
34
  }
35
35
  }
36
36
  // ============================================================================
37
+ // Cache Error Types
38
+ // ============================================================================
39
+ export var CacheErrorCode;
40
+ (function (CacheErrorCode) {
41
+ // System-level cache errors
42
+ CacheErrorCode["CACHE_SYSTEM_FAILURE"] = "CACHE_SYSTEM_FAILURE";
43
+ CacheErrorCode["CACHE_MEMORY_EXHAUSTED"] = "CACHE_MEMORY_EXHAUSTED";
44
+ CacheErrorCode["CACHE_INITIALIZATION_FAILED"] = "CACHE_INITIALIZATION_FAILED";
45
+ // Storage-related errors
46
+ CacheErrorCode["CACHE_STORAGE_FULL"] = "CACHE_STORAGE_FULL";
47
+ CacheErrorCode["CACHE_STORAGE_PERMISSION"] = "CACHE_STORAGE_PERMISSION";
48
+ CacheErrorCode["CACHE_STORAGE_CORRUPTION"] = "CACHE_STORAGE_CORRUPTION";
49
+ // Network-related errors (for future Redis support)
50
+ CacheErrorCode["CACHE_NETWORK_CONNECTION"] = "CACHE_NETWORK_CONNECTION";
51
+ CacheErrorCode["CACHE_NETWORK_TIMEOUT"] = "CACHE_NETWORK_TIMEOUT";
52
+ CacheErrorCode["CACHE_NETWORK_AUTH"] = "CACHE_NETWORK_AUTH";
53
+ // Configuration errors
54
+ CacheErrorCode["CACHE_CONFIG_INVALID"] = "CACHE_CONFIG_INVALID";
55
+ CacheErrorCode["CACHE_CONFIG_MISSING"] = "CACHE_CONFIG_MISSING";
56
+ // Operation errors
57
+ CacheErrorCode["CACHE_OPERATION_FAILED"] = "CACHE_OPERATION_FAILED";
58
+ CacheErrorCode["CACHE_SERIALIZATION_ERROR"] = "CACHE_SERIALIZATION_ERROR";
59
+ CacheErrorCode["CACHE_KEY_INVALID"] = "CACHE_KEY_INVALID";
60
+ })(CacheErrorCode || (CacheErrorCode = {}));
61
+ export class CacheError extends GuardianError {
62
+ operation;
63
+ key;
64
+ constructor(code, message, operation, key, context) {
65
+ super(code, message, context);
66
+ this.operation = operation;
67
+ this.key = key;
68
+ this.name = "CacheError";
69
+ }
70
+ }
71
+ export class CacheSystemError extends CacheError {
72
+ constructor(message, operation, key, context) {
73
+ super(CacheErrorCode.CACHE_SYSTEM_FAILURE, message, operation, key, context);
74
+ this.name = "CacheSystemError";
75
+ }
76
+ }
77
+ export class CacheStorageError extends CacheError {
78
+ constructor(code = CacheErrorCode.CACHE_STORAGE_FULL, message, operation, key, context) {
79
+ super(code, message, operation, key, context);
80
+ this.name = "CacheStorageError";
81
+ }
82
+ }
83
+ export class CacheNetworkError extends CacheError {
84
+ constructor(code = CacheErrorCode.CACHE_NETWORK_CONNECTION, message, operation, key, context) {
85
+ super(code, message, operation, key, context);
86
+ this.name = "CacheNetworkError";
87
+ }
88
+ }
89
+ export class CacheConfigurationError extends CacheError {
90
+ constructor(code = CacheErrorCode.CACHE_CONFIG_INVALID, message, operation, key, context) {
91
+ super(code, message, operation, key, context);
92
+ this.name = "CacheConfigurationError";
93
+ }
94
+ }
95
+ export class CacheOperationError extends CacheError {
96
+ constructor(code = CacheErrorCode.CACHE_OPERATION_FAILED, message, operation, key, context) {
97
+ super(code, message, operation, key, context);
98
+ this.name = "CacheOperationError";
99
+ }
100
+ }
101
+ // ============================================================================
37
102
  // Export all types - Main file, no re-exports needed
38
103
  // ============================================================================
39
104
  //# sourceMappingURL=index.js.map
@@ -8,7 +8,7 @@ export declare class Cache implements ICache {
8
8
  private statsData;
9
9
  constructor(options?: CacheOptions);
10
10
  /**
11
- * Get value from cache
11
+ * Get value from cache with resilient error handling
12
12
  */
13
13
  get<T>(key: string): T | undefined;
14
14
  /**
@@ -39,15 +39,21 @@ export declare class Cache implements ICache {
39
39
  misses: number;
40
40
  keys: number;
41
41
  size: number;
42
+ cacheErrors: number;
43
+ nonCacheErrors: number;
42
44
  };
43
45
  /**
44
46
  * Get detailed cache statistics from node-cache
45
47
  */
46
48
  getDetailedStats(): any;
47
49
  /**
48
- * Get or set pattern - common caching pattern
50
+ * Get or set pattern with automatic fallback on cache failures
49
51
  */
50
52
  getOrSet<T>(key: string, fetchFn: () => Promise<T>, ttl?: number): Promise<T>;
53
+ /**
54
+ * Resilient get or set pattern that bypasses cache entirely on cache system failures
55
+ */
56
+ getOrSetResilient<T>(key: string, fetchFn: () => Promise<T>, ttl?: number): Promise<T>;
51
57
  /**
52
58
  * Cache with tags for group invalidation
53
59
  */
@@ -3,12 +3,146 @@
3
3
  * Provides intelligent caching for PR data, file contents, and AI responses
4
4
  */
5
5
  import NodeCache from "node-cache";
6
+ import { CacheError, } from "../types/index.js";
6
7
  import { logger } from "./Logger.js";
8
+ /**
9
+ * Enhanced cache error detection utility
10
+ * Provides multi-layer error classification to avoid false positives
11
+ */
12
+ class CacheErrorDetector {
13
+ /**
14
+ * Detect if an error is cache-related using multiple strategies
15
+ */
16
+ static isCacheError(error, operation, key) {
17
+ // Strategy 1: Check error type/class (most reliable)
18
+ if (error instanceof CacheError) {
19
+ return true;
20
+ }
21
+ // Strategy 2: Check for specific cache error patterns in NodeCache
22
+ if (error instanceof Error) {
23
+ const errorMessage = error.message.toLowerCase();
24
+ const stackTrace = error.stack?.toLowerCase() || "";
25
+ // Check for NodeCache-specific error patterns
26
+ const nodeCachePatterns = [
27
+ /node_modules\/node-cache/,
28
+ /cache\.js:\d+/,
29
+ /nodecache/,
30
+ ];
31
+ const isNodeCacheError = nodeCachePatterns.some((pattern) => pattern.test(stackTrace));
32
+ if (isNodeCacheError) {
33
+ return true;
34
+ }
35
+ // Strategy 3: Check for specific cache-related error messages (more targeted)
36
+ const cacheSpecificPatterns = [
37
+ /cache.*(?:full|exhausted|limit)/,
38
+ /memory.*(?:cache|allocation).*(?:failed|error)/,
39
+ /storage.*(?:cache|quota).*(?:exceeded|full)/,
40
+ /cache.*(?:initialization|setup).*(?:failed|error)/,
41
+ /ttl.*(?:invalid|expired)/,
42
+ /cache.*(?:key|value).*(?:invalid|malformed)/,
43
+ ];
44
+ const hasCacheSpecificError = cacheSpecificPatterns.some((pattern) => pattern.test(errorMessage));
45
+ if (hasCacheSpecificError) {
46
+ return true;
47
+ }
48
+ // Strategy 4: Context-aware detection
49
+ if (operation && key) {
50
+ // If we're in a cache operation and get memory/storage errors, likely cache-related
51
+ const cacheOperations = [
52
+ "get",
53
+ "set",
54
+ "del",
55
+ "clear",
56
+ "has",
57
+ "getorset",
58
+ "getorsetresilient",
59
+ ];
60
+ const isCacheOperation = cacheOperations.includes(operation.toLowerCase());
61
+ const contextualPatterns = [
62
+ /^out of memory$/,
63
+ /storage quota exceeded/,
64
+ /disk full/,
65
+ ];
66
+ if (isCacheOperation &&
67
+ contextualPatterns.some((pattern) => pattern.test(errorMessage))) {
68
+ return true;
69
+ }
70
+ }
71
+ }
72
+ return false;
73
+ }
74
+ /**
75
+ * Classify cache error for better handling and logging
76
+ */
77
+ static classifyError(error, operation, key) {
78
+ if (!this.isCacheError(error, operation, key)) {
79
+ return {
80
+ isCache: false,
81
+ category: "unknown",
82
+ confidence: "high",
83
+ reason: "Not identified as cache-related error",
84
+ };
85
+ }
86
+ if (error instanceof CacheError) {
87
+ const category = error.code.includes("STORAGE")
88
+ ? "storage"
89
+ : error.code.includes("NETWORK")
90
+ ? "network"
91
+ : error.code.includes("SYSTEM")
92
+ ? "system"
93
+ : "operation";
94
+ return {
95
+ isCache: true,
96
+ category,
97
+ confidence: "high",
98
+ reason: `Explicit cache error: ${error.code}`,
99
+ };
100
+ }
101
+ if (error instanceof Error) {
102
+ const message = error.message.toLowerCase();
103
+ const stack = error.stack?.toLowerCase() || "";
104
+ // High confidence patterns
105
+ if (/node_modules\/node-cache/.test(stack)) {
106
+ return {
107
+ isCache: true,
108
+ category: "system",
109
+ confidence: "high",
110
+ reason: "NodeCache stack trace detected",
111
+ };
112
+ }
113
+ // Medium confidence patterns
114
+ if (/cache.*(?:full|exhausted)/.test(message)) {
115
+ return {
116
+ isCache: true,
117
+ category: "storage",
118
+ confidence: "medium",
119
+ reason: "Cache capacity error pattern",
120
+ };
121
+ }
122
+ if (/memory.*cache.*failed/.test(message)) {
123
+ return {
124
+ isCache: true,
125
+ category: "system",
126
+ confidence: "medium",
127
+ reason: "Memory allocation error in cache context",
128
+ };
129
+ }
130
+ }
131
+ return {
132
+ isCache: true,
133
+ category: "unknown",
134
+ confidence: "low",
135
+ reason: "Fallback detection",
136
+ };
137
+ }
138
+ }
7
139
  export class Cache {
8
140
  cache;
9
141
  statsData = {
10
142
  hits: 0,
11
143
  misses: 0,
144
+ cacheErrors: 0,
145
+ nonCacheErrors: 0,
12
146
  };
13
147
  constructor(options = {}) {
14
148
  const { ttl = 3600, // 1 hour default
@@ -33,18 +167,25 @@ export class Cache {
33
167
  });
34
168
  }
35
169
  /**
36
- * Get value from cache
170
+ * Get value from cache with resilient error handling
37
171
  */
38
172
  get(key) {
39
- const value = this.cache.get(key);
40
- if (value !== undefined) {
41
- this.statsData.hits++;
42
- logger.debug(`Cache HIT: ${key}`);
43
- return value;
173
+ try {
174
+ const value = this.cache.get(key);
175
+ if (value !== undefined) {
176
+ this.statsData.hits++;
177
+ logger.debug(`Cache HIT: ${key}`);
178
+ return value;
179
+ }
180
+ else {
181
+ this.statsData.misses++;
182
+ logger.debug(`Cache MISS: ${key}`);
183
+ return undefined;
184
+ }
44
185
  }
45
- else {
186
+ catch (error) {
46
187
  this.statsData.misses++;
47
- logger.debug(`Cache MISS: ${key}`);
188
+ logger.warn(`Cache GET error for ${key}, treating as miss:`, error);
48
189
  return undefined;
49
190
  }
50
191
  }
@@ -105,6 +246,8 @@ export class Cache {
105
246
  misses: this.statsData.misses,
106
247
  keys: this.cache.keys().length,
107
248
  size: this.cache.getStats().keys,
249
+ cacheErrors: this.statsData.cacheErrors,
250
+ nonCacheErrors: this.statsData.nonCacheErrors,
108
251
  };
109
252
  }
110
253
  /**
@@ -114,9 +257,10 @@ export class Cache {
114
257
  return this.cache.getStats();
115
258
  }
116
259
  /**
117
- * Get or set pattern - common caching pattern
260
+ * Get or set pattern with automatic fallback on cache failures
118
261
  */
119
262
  async getOrSet(key, fetchFn, ttl) {
263
+ // Try to get from cache with resilient error handling
120
264
  const cached = this.get(key);
121
265
  if (cached !== undefined) {
122
266
  return cached;
@@ -124,7 +268,13 @@ export class Cache {
124
268
  try {
125
269
  logger.debug(`Cache FETCH: ${key}`);
126
270
  const value = await fetchFn();
127
- this.set(key, value, ttl);
271
+ // Try to cache the result, but don't fail if caching fails
272
+ try {
273
+ this.set(key, value, ttl);
274
+ }
275
+ catch (cacheError) {
276
+ logger.warn(`Cache SET failed for ${key}, continuing without cache:`, cacheError);
277
+ }
128
278
  return value;
129
279
  }
130
280
  catch (error) {
@@ -132,6 +282,36 @@ export class Cache {
132
282
  throw error;
133
283
  }
134
284
  }
285
+ /**
286
+ * Resilient get or set pattern that bypasses cache entirely on cache system failures
287
+ */
288
+ async getOrSetResilient(key, fetchFn, ttl) {
289
+ try {
290
+ // Try normal cache flow first
291
+ return await this.getOrSet(key, fetchFn, ttl);
292
+ }
293
+ catch (error) {
294
+ // Use enhanced error detection to determine if this is a cache-related error
295
+ const errorClassification = CacheErrorDetector.classifyError(error, "getOrSet", key);
296
+ if (errorClassification.isCache) {
297
+ // Track cache error statistics
298
+ this.statsData.cacheErrors++;
299
+ logger.warn(`Cache system error detected for ${key} (${errorClassification.confidence} confidence: ${errorClassification.reason}), bypassing cache entirely`, {
300
+ error: error instanceof Error ? error.message : String(error),
301
+ category: errorClassification.category,
302
+ confidence: errorClassification.confidence,
303
+ key,
304
+ operation: "getOrSet",
305
+ });
306
+ // Bypass cache completely and just fetch the data
307
+ return await fetchFn();
308
+ }
309
+ // Track non-cache errors for debugging
310
+ this.statsData.nonCacheErrors++;
311
+ // Re-throw non-cache errors
312
+ throw error;
313
+ }
314
+ }
135
315
  /**
136
316
  * Cache with tags for group invalidation
137
317
  */
@@ -80,7 +80,7 @@ export class ConfigManager {
80
80
  },
81
81
  ],
82
82
  autoFormat: true,
83
- systemPrompt: "You are an Expert Technical Writer specializing in pull request documentation. Your role is to:\n\n📝 CLARITY FIRST: Create clear, comprehensive PR descriptions that help reviewers understand the changes\n🎥 STORY TELLING: Explain the 'why' behind changes, not just the 'what'\n📈 STRUCTURED: Follow consistent formatting with required sections\n🔗 CONTEXTUAL: Link changes to business value and technical rationale\n\nCRITICAL INSTRUCTION: Return ONLY the enhanced PR description content as clean markdown. Do NOT include any meta-commentary, explanations about what you're doing, or introductory text like \"I will enhance...\" or \"Here is the enhanced description:\". \n\nOutput the enhanced description directly without any wrapper text or explanations.",
83
+ systemPrompt: "You are an Expert Technical Writer specializing in pull request documentation. Your role is to:\n\n📝 CLARITY FIRST: Create clear, comprehensive PR descriptions that help reviewers understand the changes\n🎥 STORY TELLING: Explain the 'why' behind changes, not just the 'what'\n📈 STRUCTURED: Follow consistent formatting with required sections\n🔗 CONTEXTUAL: Link changes to business value and technical rationale\n\nCRITICAL INSTRUCTION: Return ONLY the enhanced PR description content as clean markdown.\n- DO NOT add meta-commentary like \"No description provided\" or \"Here is the enhanced description\"\n- DO NOT add explanatory text about what you're doing\n- START directly with the actual PR content (sections, lists, explanations)\n- If there's no existing description, just write the new sections without mentioning it\n\nOutput the enhanced description directly without any wrapper text or explanations.",
84
84
  outputTemplate: "# PR Title Enhancement (if needed)\n\n## Summary\n[Clear overview of what this PR accomplishes]\n\n## Changes Made\n[Specific technical changes - be precise]\n\n## Testing\n[How the changes were tested]\n\n## Impact\n[Business/technical impact and considerations]\n\n## Additional Notes\n[Any deployment notes, follow-ups, or special considerations]",
85
85
  enhancementInstructions: 'Return ONLY the enhanced PR description as clean markdown. Do NOT include any explanatory text, meta-commentary, or phrases like "Here is the enhanced description:" or "I will enhance...".\n\nStart directly with the enhanced description content using this structure:',
86
86
  },
@@ -244,11 +244,10 @@ export class ConfigManager {
244
244
  * Apply provider-aware token limits using shared utility
245
245
  */
246
246
  applyProviderTokenLimits(config) {
247
- const provider = config.providers.ai.provider || 'auto';
247
+ const provider = config.providers.ai.provider || "auto";
248
248
  const configuredTokens = config.providers.ai.maxTokens;
249
249
  // Use the shared utility to validate and adjust token limits
250
- const validatedTokens = validateProviderTokenLimit(provider, configuredTokens, false // Use standard limits for configuration
251
- );
250
+ const validatedTokens = validateProviderTokenLimit(provider, configuredTokens, false);
252
251
  config.providers.ai.maxTokens = validatedTokens;
253
252
  return config;
254
253
  }
@@ -46,6 +46,9 @@ export declare class TokenBudgetManager implements TokenBudgetManagerInterface {
46
46
  private usedTokens;
47
47
  private batchAllocations;
48
48
  private reservedTokens;
49
+ private preAllocationMode;
50
+ private preAllocatedBatches;
51
+ private batchStates;
49
52
  constructor(totalBudget: number);
50
53
  /**
51
54
  * Allocate tokens for a specific batch
@@ -92,6 +95,31 @@ export declare class TokenBudgetManager implements TokenBudgetManagerInterface {
92
95
  * Reset the budget manager (for testing or reuse)
93
96
  */
94
97
  reset(): void;
98
+ /**
99
+ * Pre-allocate tokens for all batches upfront
100
+ * This ensures all batches have guaranteed token allocation before processing starts
101
+ */
102
+ preAllocateAllBatches(allocations: Map<number, number>): boolean;
103
+ /**
104
+ * Mark a batch as failed and handle cleanup
105
+ */
106
+ markBatchFailed(batchIndex: number, error?: string): void;
107
+ /**
108
+ * Get the current state of a batch
109
+ */
110
+ getBatchState(batchIndex: number): "pending" | "processing" | "completed" | "failed" | undefined;
111
+ /**
112
+ * Check if pre-allocation mode is active
113
+ */
114
+ isPreAllocationMode(): boolean;
115
+ /**
116
+ * Get all batch states for debugging
117
+ */
118
+ getAllBatchStates(): Map<number, "pending" | "processing" | "completed" | "failed">;
119
+ /**
120
+ * Disable pre-allocation mode and clean up
121
+ */
122
+ disablePreAllocationMode(): void;
95
123
  /**
96
124
  * Update the total budget (useful for dynamic adjustment)
97
125
  */
@@ -77,12 +77,18 @@ export class TokenBudgetManager {
77
77
  usedTokens = 0;
78
78
  batchAllocations = new Map();
79
79
  reservedTokens = 0; // Tokens allocated but not yet used
80
+ // NEW: Pre-allocation mode tracking
81
+ preAllocationMode = false;
82
+ preAllocatedBatches = new Set();
83
+ batchStates = new Map();
80
84
  constructor(totalBudget) {
81
85
  if (totalBudget <= 0) {
82
86
  throw new Error("Token budget must be greater than 0");
83
87
  }
84
- this.totalBudget = totalBudget;
85
- logger.debug(`TokenBudgetManager created with budget of ${totalBudget} tokens`);
88
+ // Floor the budget to ensure integer arithmetic and avoid floating-point precision issues.
89
+ // The fractional part is discarded, so the budget is always rounded down.
90
+ this.totalBudget = Math.floor(totalBudget);
91
+ logger.debug(`TokenBudgetManager created with budget of ${this.totalBudget} tokens (original: ${totalBudget})`);
86
92
  }
87
93
  /**
88
94
  * Allocate tokens for a specific batch
@@ -93,8 +99,31 @@ export class TokenBudgetManager {
93
99
  logger.warn(`Invalid token estimate for batch ${batchIndex}: ${estimatedTokens}`);
94
100
  return false;
95
101
  }
96
- // Check if we already have an allocation for this batch
102
+ // NEW: Handle pre-allocation mode
103
+ if (this.preAllocationMode && this.preAllocatedBatches.has(batchIndex)) {
104
+ // Check if batch is already being processed
105
+ const currentState = this.batchStates.get(batchIndex);
106
+ if (currentState === "processing") {
107
+ logger.warn(`Batch ${batchIndex} is already being processed`);
108
+ return false;
109
+ }
110
+ if (currentState !== "pending") {
111
+ logger.warn(`Batch ${batchIndex} is not in pending state (current: ${currentState})`);
112
+ return false;
113
+ }
114
+ // In pre-allocation mode, just mark batch as processing and return success
115
+ this.batchStates.set(batchIndex, "processing");
116
+ logger.debug(`Batch ${batchIndex} using pre-allocated tokens (${this.batchAllocations.get(batchIndex)} tokens)`);
117
+ return true;
118
+ }
119
+ // Check if we already have an allocation for this batch (non-pre-allocation mode)
97
120
  if (this.batchAllocations.has(batchIndex)) {
121
+ // Check if batch is already being processed
122
+ const currentState = this.batchStates.get(batchIndex);
123
+ if (currentState === "processing") {
124
+ logger.warn(`Batch ${batchIndex} is already being processed`);
125
+ return false;
126
+ }
98
127
  logger.warn(`Batch ${batchIndex} already has token allocation`);
99
128
  return false;
100
129
  }
@@ -108,6 +137,7 @@ export class TokenBudgetManager {
108
137
  // Allocate the tokens
109
138
  this.reservedTokens += estimatedTokens;
110
139
  this.batchAllocations.set(batchIndex, estimatedTokens);
140
+ this.batchStates.set(batchIndex, "processing");
111
141
  logger.debug(`Allocated ${estimatedTokens} tokens for batch ${batchIndex} ` +
112
142
  `(${this.getAvailableBudget()} remaining)`);
113
143
  return true;
@@ -122,10 +152,19 @@ export class TokenBudgetManager {
122
152
  logger.warn(`No token allocation found for batch ${batchIndex}`);
123
153
  return;
124
154
  }
155
+ // Update batch state to completed only if not already failed
156
+ const currentState = this.batchStates.get(batchIndex);
157
+ if (currentState !== "failed") {
158
+ this.batchStates.set(batchIndex, "completed");
159
+ }
125
160
  // Move from reserved to used (assuming the tokens were actually used)
126
161
  this.reservedTokens -= allocated;
127
162
  this.usedTokens += allocated;
128
163
  this.batchAllocations.delete(batchIndex);
164
+ // Clean up pre-allocation tracking
165
+ if (this.preAllocationMode) {
166
+ this.preAllocatedBatches.delete(batchIndex);
167
+ }
129
168
  logger.debug(`Released ${allocated} tokens from batch ${batchIndex} ` +
130
169
  `(${this.getAvailableBudget()} now available)`);
131
170
  }
@@ -182,6 +221,72 @@ export class TokenBudgetManager {
182
221
  this.batchAllocations.clear();
183
222
  logger.debug("TokenBudgetManager reset");
184
223
  }
224
+ /**
225
+ * Pre-allocate tokens for all batches upfront
226
+ * This ensures all batches have guaranteed token allocation before processing starts
227
+ */
228
+ preAllocateAllBatches(allocations) {
229
+ const totalRequired = Array.from(allocations.values()).reduce((sum, tokens) => sum + tokens, 0);
230
+ if (totalRequired > this.totalBudget) {
231
+ logger.error(`Pre-allocation failed: total required (${totalRequired}) exceeds budget (${this.totalBudget})`);
232
+ return false;
233
+ }
234
+ // Clear any existing allocations and reset state
235
+ this.batchAllocations.clear();
236
+ this.reservedTokens = 0;
237
+ this.batchStates.clear();
238
+ this.preAllocatedBatches.clear();
239
+ // Enable pre-allocation mode
240
+ this.preAllocationMode = true;
241
+ // Reserve all tokens upfront
242
+ allocations.forEach((tokens, batchIndex) => {
243
+ this.batchAllocations.set(batchIndex, tokens);
244
+ this.reservedTokens += tokens;
245
+ this.preAllocatedBatches.add(batchIndex);
246
+ this.batchStates.set(batchIndex, "pending");
247
+ });
248
+ logger.info(`Pre-allocated ${totalRequired} tokens across ${allocations.size} batches ` +
249
+ `(${this.getAvailableBudget()} remaining)`);
250
+ return true;
251
+ }
252
+ /**
253
+ * Mark a batch as failed and handle cleanup
254
+ */
255
+ markBatchFailed(batchIndex, error) {
256
+ this.batchStates.set(batchIndex, "failed");
257
+ if (error) {
258
+ logger.debug(`Batch ${batchIndex} marked as failed: ${error}`);
259
+ }
260
+ else {
261
+ logger.debug(`Batch ${batchIndex} marked as failed`);
262
+ }
263
+ }
264
+ /**
265
+ * Get the current state of a batch
266
+ */
267
+ getBatchState(batchIndex) {
268
+ return this.batchStates.get(batchIndex);
269
+ }
270
+ /**
271
+ * Check if pre-allocation mode is active
272
+ */
273
+ isPreAllocationMode() {
274
+ return this.preAllocationMode;
275
+ }
276
+ /**
277
+ * Get all batch states for debugging
278
+ */
279
+ getAllBatchStates() {
280
+ return new Map(this.batchStates);
281
+ }
282
+ /**
283
+ * Disable pre-allocation mode and clean up
284
+ */
285
+ disablePreAllocationMode() {
286
+ this.preAllocationMode = false;
287
+ this.preAllocatedBatches.clear();
288
+ logger.debug("Pre-allocation mode disabled");
289
+ }
185
290
  /**
186
291
  * Update the total budget (useful for dynamic adjustment)
187
292
  */