@probelabs/probe 0.6.0-rc215 → 0.6.0-rc217
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.
- package/bin/binaries/probe-v0.6.0-rc217-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc217-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc217-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc217-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc217-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.js +18 -0
- package/build/agent/index.js +157 -23
- package/build/delegate.js +167 -17
- package/build/tools/analyzeAll.js +12 -6
- package/build/tools/vercel.js +6 -4
- package/cjs/agent/ProbeAgent.cjs +157 -23
- package/cjs/index.cjs +157 -23
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +18 -0
- package/src/delegate.js +167 -17
- package/src/tools/analyzeAll.js +12 -6
- package/src/tools/vercel.js +6 -4
- package/bin/binaries/probe-v0.6.0-rc215-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc215-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc215-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc215-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc215-x86_64-unknown-linux-musl.tar.gz +0 -0
package/build/delegate.js
CHANGED
|
@@ -22,12 +22,18 @@ class DelegationManager {
|
|
|
22
22
|
constructor() {
|
|
23
23
|
this.maxConcurrent = parseInt(process.env.MAX_CONCURRENT_DELEGATIONS || '3', 10);
|
|
24
24
|
this.maxPerSession = parseInt(process.env.MAX_DELEGATIONS_PER_SESSION || '10', 10);
|
|
25
|
+
// Default queue timeout: 60 seconds. Set DELEGATION_QUEUE_TIMEOUT=0 to disable.
|
|
26
|
+
this.defaultQueueTimeout = parseInt(process.env.DELEGATION_QUEUE_TIMEOUT || '60000', 10);
|
|
25
27
|
|
|
26
28
|
// Track delegations per session with timestamp for potential TTL cleanup
|
|
27
29
|
// Map<string, { count: number, lastUpdated: number }>
|
|
28
30
|
this.sessionDelegations = new Map();
|
|
29
31
|
this.globalActive = 0;
|
|
30
32
|
|
|
33
|
+
// Queue for waiting delegations (FIFO)
|
|
34
|
+
// Each entry: { resolve, reject, parentSessionId, queuedAt, timeoutId }
|
|
35
|
+
this.waitQueue = [];
|
|
36
|
+
|
|
31
37
|
// Start periodic cleanup of stale sessions (every 5 minutes)
|
|
32
38
|
// Wrapped in try-catch to prevent interval errors from crashing the process
|
|
33
39
|
this.cleanupInterval = setInterval(() => {
|
|
@@ -47,6 +53,7 @@ class DelegationManager {
|
|
|
47
53
|
/**
|
|
48
54
|
* Check limits and increment counters (synchronous, atomic in Node.js event loop)
|
|
49
55
|
* @param {string|null|undefined} parentSessionId - Parent session ID for tracking
|
|
56
|
+
* @returns {boolean} true if acquired, false if would need to wait
|
|
50
57
|
*/
|
|
51
58
|
tryAcquire(parentSessionId) {
|
|
52
59
|
// Validate parentSessionId parameter
|
|
@@ -54,9 +61,9 @@ class DelegationManager {
|
|
|
54
61
|
throw new TypeError('parentSessionId must be a string, null, or undefined');
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
// Check global limit
|
|
64
|
+
// Check global limit - return false instead of throwing
|
|
58
65
|
if (this.globalActive >= this.maxConcurrent) {
|
|
59
|
-
|
|
66
|
+
return false;
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
// Check per-session limit
|
|
@@ -70,6 +77,16 @@ class DelegationManager {
|
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
// Increment counters (atomic in single-threaded Node.js)
|
|
80
|
+
this._incrementCounters(parentSessionId);
|
|
81
|
+
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Internal helper to increment counters
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
_incrementCounters(parentSessionId) {
|
|
73
90
|
this.globalActive++;
|
|
74
91
|
|
|
75
92
|
if (parentSessionId) {
|
|
@@ -84,12 +101,73 @@ class DelegationManager {
|
|
|
84
101
|
});
|
|
85
102
|
}
|
|
86
103
|
}
|
|
104
|
+
}
|
|
87
105
|
|
|
88
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Acquire a delegation slot, waiting in queue if necessary
|
|
108
|
+
* @param {string|null|undefined} parentSessionId - Parent session ID for tracking
|
|
109
|
+
* @param {boolean} debug - Enable debug logging
|
|
110
|
+
* @param {number|null} queueTimeout - Max time to wait in queue (ms). Defaults to this.defaultQueueTimeout. Set to 0 to disable.
|
|
111
|
+
* @returns {Promise<boolean>} Resolves when slot is acquired, rejects on timeout or session limit error
|
|
112
|
+
*/
|
|
113
|
+
async acquire(parentSessionId, debug = false, queueTimeout = null) {
|
|
114
|
+
// Use instance default if not specified
|
|
115
|
+
const effectiveTimeout = queueTimeout !== null ? queueTimeout : this.defaultQueueTimeout;
|
|
116
|
+
// Try immediate acquisition first
|
|
117
|
+
if (this.tryAcquire(parentSessionId)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Need to wait in queue
|
|
122
|
+
if (debug) {
|
|
123
|
+
console.error(`[DelegationManager] Slot unavailable (${this.globalActive}/${this.maxConcurrent}), queuing... (queue size: ${this.waitQueue.length}, timeout: ${effectiveTimeout}ms)`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Create a promise that will be resolved when a slot becomes available
|
|
127
|
+
// or rejected if session limit is exceeded or queue timeout expires
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const entry = {
|
|
130
|
+
resolve: null, // Will be wrapped below
|
|
131
|
+
reject: null, // Will be wrapped below
|
|
132
|
+
parentSessionId,
|
|
133
|
+
debug,
|
|
134
|
+
queuedAt: Date.now(),
|
|
135
|
+
timeoutId: null
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Wrap resolve/reject to clear timeout and prevent double-settling
|
|
139
|
+
let settled = false;
|
|
140
|
+
entry.resolve = (value) => {
|
|
141
|
+
if (settled) return;
|
|
142
|
+
settled = true;
|
|
143
|
+
if (entry.timeoutId) clearTimeout(entry.timeoutId);
|
|
144
|
+
resolve(value);
|
|
145
|
+
};
|
|
146
|
+
entry.reject = (error) => {
|
|
147
|
+
if (settled) return;
|
|
148
|
+
settled = true;
|
|
149
|
+
if (entry.timeoutId) clearTimeout(entry.timeoutId);
|
|
150
|
+
reject(error);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Set up queue timeout if enabled
|
|
154
|
+
if (effectiveTimeout > 0) {
|
|
155
|
+
entry.timeoutId = setTimeout(() => {
|
|
156
|
+
// Remove from queue if still there
|
|
157
|
+
const index = this.waitQueue.indexOf(entry);
|
|
158
|
+
if (index !== -1) {
|
|
159
|
+
this.waitQueue.splice(index, 1);
|
|
160
|
+
}
|
|
161
|
+
entry.reject(new Error(`Delegation queue timeout: waited ${effectiveTimeout}ms for an available slot`));
|
|
162
|
+
}, effectiveTimeout);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.waitQueue.push(entry);
|
|
166
|
+
});
|
|
89
167
|
}
|
|
90
168
|
|
|
91
169
|
/**
|
|
92
|
-
* Decrement counters (synchronous, atomic in Node.js event loop)
|
|
170
|
+
* Decrement counters and process queue (synchronous, atomic in Node.js event loop)
|
|
93
171
|
*/
|
|
94
172
|
release(parentSessionId, debug = false) {
|
|
95
173
|
this.globalActive = Math.max(0, this.globalActive - 1);
|
|
@@ -107,7 +185,52 @@ class DelegationManager {
|
|
|
107
185
|
}
|
|
108
186
|
|
|
109
187
|
if (debug) {
|
|
110
|
-
console.error(`[DELEGATE] Released. Global active: ${this.globalActive}`);
|
|
188
|
+
console.error(`[DELEGATE] Released. Global active: ${this.globalActive}, queue size: ${this.waitQueue.length}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Process next item in queue if there's capacity
|
|
192
|
+
this._processQueue(debug);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Process the wait queue - grant slot to next waiting delegation
|
|
197
|
+
* @private
|
|
198
|
+
*/
|
|
199
|
+
_processQueue(debug = false) {
|
|
200
|
+
// Process queue items one at a time when slots are available
|
|
201
|
+
// Items are only removed when they can be granted or must be rejected
|
|
202
|
+
while (this.waitQueue.length > 0 && this.globalActive < this.maxConcurrent) {
|
|
203
|
+
const next = this.waitQueue.shift();
|
|
204
|
+
if (!next) break;
|
|
205
|
+
|
|
206
|
+
const { resolve, reject, parentSessionId, queuedAt } = next;
|
|
207
|
+
|
|
208
|
+
// Check per-session limit before granting
|
|
209
|
+
if (parentSessionId) {
|
|
210
|
+
const sessionData = this.sessionDelegations.get(parentSessionId);
|
|
211
|
+
const sessionCount = sessionData?.count || 0;
|
|
212
|
+
|
|
213
|
+
if (sessionCount >= this.maxPerSession) {
|
|
214
|
+
// Session limit reached - reject with error (consistent with tryAcquire behavior)
|
|
215
|
+
// This is a hard limit, not something that will resolve by waiting longer
|
|
216
|
+
if (debug) {
|
|
217
|
+
console.error(`[DelegationManager] Session limit (${this.maxPerSession}) reached for queued item, rejecting`);
|
|
218
|
+
}
|
|
219
|
+
reject(new Error(`Maximum delegations per session (${this.maxPerSession}) reached for session ${parentSessionId}`));
|
|
220
|
+
// Continue to process next item in queue
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Grant the slot
|
|
226
|
+
this._incrementCounters(parentSessionId);
|
|
227
|
+
|
|
228
|
+
if (debug) {
|
|
229
|
+
const waitTime = Date.now() - queuedAt;
|
|
230
|
+
console.error(`[DelegationManager] Granted slot from queue (waited ${waitTime}ms). Active: ${this.globalActive}/${this.maxConcurrent}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
resolve(true);
|
|
111
234
|
}
|
|
112
235
|
}
|
|
113
236
|
|
|
@@ -119,7 +242,9 @@ class DelegationManager {
|
|
|
119
242
|
globalActive: this.globalActive,
|
|
120
243
|
maxConcurrent: this.maxConcurrent,
|
|
121
244
|
maxPerSession: this.maxPerSession,
|
|
122
|
-
|
|
245
|
+
defaultQueueTimeout: this.defaultQueueTimeout,
|
|
246
|
+
sessionCount: this.sessionDelegations.size,
|
|
247
|
+
queueSize: this.waitQueue.length
|
|
123
248
|
};
|
|
124
249
|
}
|
|
125
250
|
|
|
@@ -143,13 +268,30 @@ class DelegationManager {
|
|
|
143
268
|
clearInterval(this.cleanupInterval);
|
|
144
269
|
this.cleanupInterval = null;
|
|
145
270
|
}
|
|
271
|
+
|
|
272
|
+
// Clear all pending queue entries and their timeouts
|
|
273
|
+
for (const entry of this.waitQueue) {
|
|
274
|
+
if (entry.timeoutId) {
|
|
275
|
+
clearTimeout(entry.timeoutId);
|
|
276
|
+
}
|
|
277
|
+
// Reject pending entries so they don't hang
|
|
278
|
+
if (entry.reject) {
|
|
279
|
+
entry.reject(new Error('DelegationManager was cleaned up'));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
this.waitQueue = [];
|
|
283
|
+
|
|
146
284
|
this.sessionDelegations.clear();
|
|
147
285
|
this.globalActive = 0;
|
|
148
286
|
}
|
|
149
287
|
}
|
|
150
288
|
|
|
151
|
-
//
|
|
152
|
-
|
|
289
|
+
// Default singleton instance for backward compatibility
|
|
290
|
+
// New code should create per-instance DelegationManager via ProbeAgent
|
|
291
|
+
const defaultDelegationManager = new DelegationManager();
|
|
292
|
+
|
|
293
|
+
// Export the class for per-instance usage
|
|
294
|
+
export { DelegationManager };
|
|
153
295
|
|
|
154
296
|
/**
|
|
155
297
|
* Delegate a big distinct task to a probe subagent (used automatically by AI agents)
|
|
@@ -214,12 +356,16 @@ export async function delegate({
|
|
|
214
356
|
enableTasks = false,
|
|
215
357
|
enableMcp = false,
|
|
216
358
|
mcpConfig = null,
|
|
217
|
-
mcpConfigPath = null
|
|
359
|
+
mcpConfigPath = null,
|
|
360
|
+
delegationManager = null // Optional per-instance manager, falls back to default singleton
|
|
218
361
|
}) {
|
|
219
362
|
if (!task || typeof task !== 'string') {
|
|
220
363
|
throw new Error('Task parameter is required and must be a string');
|
|
221
364
|
}
|
|
222
365
|
|
|
366
|
+
// Use provided manager or fall back to default singleton
|
|
367
|
+
const manager = delegationManager || defaultDelegationManager;
|
|
368
|
+
|
|
223
369
|
const sessionId = randomUUID();
|
|
224
370
|
const startTime = Date.now();
|
|
225
371
|
|
|
@@ -235,19 +381,19 @@ export async function delegate({
|
|
|
235
381
|
let acquired = false;
|
|
236
382
|
|
|
237
383
|
try {
|
|
238
|
-
//
|
|
239
|
-
|
|
384
|
+
// Acquire delegation slot (waits in queue if necessary)
|
|
385
|
+
await manager.acquire(parentSessionId, debug);
|
|
240
386
|
acquired = true;
|
|
241
387
|
|
|
242
388
|
if (debug) {
|
|
243
|
-
const stats =
|
|
389
|
+
const stats = manager.getStats();
|
|
244
390
|
console.error(`[DELEGATE] Starting delegation session ${sessionId}`);
|
|
245
391
|
console.error(`[DELEGATE] Parent session: ${parentSessionId || 'none'}`);
|
|
246
392
|
console.error(`[DELEGATE] Task: ${task}`);
|
|
247
393
|
console.error(`[DELEGATE] Current iteration: ${currentIteration}/${maxIterations}`);
|
|
248
394
|
console.error(`[DELEGATE] Remaining iterations for subagent: ${remainingIterations}`);
|
|
249
395
|
console.error(`[DELEGATE] Timeout configured: ${timeout} seconds`);
|
|
250
|
-
console.error(`[DELEGATE] Global active delegations: ${stats.globalActive}/${stats.maxConcurrent}`);
|
|
396
|
+
console.error(`[DELEGATE] Global active delegations: ${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize}`);
|
|
251
397
|
console.error(`[DELEGATE] Using ProbeAgent SDK with ${promptType} prompt`);
|
|
252
398
|
}
|
|
253
399
|
// Create a new ProbeAgent instance for the delegated task
|
|
@@ -360,7 +506,7 @@ export async function delegate({
|
|
|
360
506
|
|
|
361
507
|
// Release delegation slot
|
|
362
508
|
if (acquired) {
|
|
363
|
-
|
|
509
|
+
manager.release(parentSessionId, debug);
|
|
364
510
|
}
|
|
365
511
|
|
|
366
512
|
return response;
|
|
@@ -376,7 +522,7 @@ export async function delegate({
|
|
|
376
522
|
|
|
377
523
|
// Release delegation slot on error (only if it was acquired)
|
|
378
524
|
if (acquired) {
|
|
379
|
-
|
|
525
|
+
manager.release(parentSessionId, debug);
|
|
380
526
|
}
|
|
381
527
|
|
|
382
528
|
if (debug) {
|
|
@@ -423,16 +569,20 @@ export async function isDelegateAvailable() {
|
|
|
423
569
|
|
|
424
570
|
/**
|
|
425
571
|
* Get delegation statistics (for monitoring/debugging)
|
|
572
|
+
* Note: Returns stats from the default singleton manager.
|
|
573
|
+
* For per-instance stats, use manager.getStats() directly.
|
|
426
574
|
*
|
|
427
575
|
* @returns {Object} Current delegation stats
|
|
428
576
|
*/
|
|
429
577
|
export function getDelegationStats() {
|
|
430
|
-
return
|
|
578
|
+
return defaultDelegationManager.getStats();
|
|
431
579
|
}
|
|
432
580
|
|
|
433
581
|
/**
|
|
434
582
|
* Cleanup delegation manager (for testing or shutdown)
|
|
583
|
+
* Note: Cleans up the default singleton manager.
|
|
584
|
+
* For per-instance cleanup, use manager.cleanup() directly.
|
|
435
585
|
*/
|
|
436
586
|
export async function cleanupDelegationManager() {
|
|
437
|
-
return
|
|
587
|
+
return defaultDelegationManager.cleanup();
|
|
438
588
|
}
|
|
@@ -192,7 +192,8 @@ Instructions:
|
|
|
192
192
|
enableBash: false,
|
|
193
193
|
promptType: 'code-researcher',
|
|
194
194
|
allowedTools: ['extract'],
|
|
195
|
-
maxIterations: 5
|
|
195
|
+
maxIterations: 5,
|
|
196
|
+
delegationManager: options.delegationManager // Per-instance delegation limits
|
|
196
197
|
// timeout removed - inherit default from delegate (300s)
|
|
197
198
|
});
|
|
198
199
|
|
|
@@ -327,7 +328,8 @@ Organize all findings into clear categories with items listed under each.${compl
|
|
|
327
328
|
enableBash: false,
|
|
328
329
|
promptType: 'code-researcher',
|
|
329
330
|
allowedTools: [],
|
|
330
|
-
maxIterations: 5
|
|
331
|
+
maxIterations: 5,
|
|
332
|
+
delegationManager: options.delegationManager // Per-instance delegation limits
|
|
331
333
|
// timeout removed - inherit default from delegate (300s)
|
|
332
334
|
});
|
|
333
335
|
|
|
@@ -402,7 +404,8 @@ CRITICAL: Do NOT guess keywords. Actually run searches and see what returns resu
|
|
|
402
404
|
enableBash: false,
|
|
403
405
|
promptType: 'code-researcher',
|
|
404
406
|
// Full tool access for exploration and experimentation
|
|
405
|
-
maxIterations: 15
|
|
407
|
+
maxIterations: 15,
|
|
408
|
+
delegationManager: options.delegationManager // Per-instance delegation limits
|
|
406
409
|
// timeout removed - inherit default from delegate (300s)
|
|
407
410
|
});
|
|
408
411
|
|
|
@@ -472,7 +475,8 @@ IMPORTANT: When completing, use the FULL format: <attempt_completion><result>YOU
|
|
|
472
475
|
enableBash: false,
|
|
473
476
|
promptType: 'code-researcher',
|
|
474
477
|
allowedTools: [],
|
|
475
|
-
maxIterations: 5
|
|
478
|
+
maxIterations: 5,
|
|
479
|
+
delegationManager: options.delegationManager // Per-instance delegation limits
|
|
476
480
|
// timeout removed - inherit default from delegate (300s)
|
|
477
481
|
});
|
|
478
482
|
|
|
@@ -515,7 +519,8 @@ export async function analyzeAll(options) {
|
|
|
515
519
|
model,
|
|
516
520
|
tracer,
|
|
517
521
|
chunkSizeTokens = DEFAULT_CHUNK_SIZE_TOKENS,
|
|
518
|
-
maxChunks = MAX_CHUNKS
|
|
522
|
+
maxChunks = MAX_CHUNKS,
|
|
523
|
+
delegationManager = null // Per-instance delegation limits
|
|
519
524
|
} = options;
|
|
520
525
|
|
|
521
526
|
if (!question) {
|
|
@@ -529,7 +534,8 @@ export async function analyzeAll(options) {
|
|
|
529
534
|
allowedFolders,
|
|
530
535
|
provider,
|
|
531
536
|
model,
|
|
532
|
-
tracer
|
|
537
|
+
tracer,
|
|
538
|
+
delegationManager // Per-instance delegation limits
|
|
533
539
|
};
|
|
534
540
|
|
|
535
541
|
// ============================================================
|
package/build/tools/vercel.js
CHANGED
|
@@ -470,7 +470,7 @@ export const extractTool = (options = {}) => {
|
|
|
470
470
|
* @returns {Object} Configured delegate tool
|
|
471
471
|
*/
|
|
472
472
|
export const delegateTool = (options = {}) => {
|
|
473
|
-
const { debug = false, timeout = 300, cwd, allowedFolders, enableBash = false, bashConfig, architectureFileName, enableMcp = false, mcpConfig = null, mcpConfigPath = null } = options;
|
|
473
|
+
const { debug = false, timeout = 300, cwd, allowedFolders, enableBash = false, bashConfig, architectureFileName, enableMcp = false, mcpConfig = null, mcpConfigPath = null, delegationManager = null } = options;
|
|
474
474
|
|
|
475
475
|
return tool({
|
|
476
476
|
name: 'delegate',
|
|
@@ -562,7 +562,8 @@ export const delegateTool = (options = {}) => {
|
|
|
562
562
|
searchDelegate,
|
|
563
563
|
enableMcp,
|
|
564
564
|
mcpConfig,
|
|
565
|
-
mcpConfigPath
|
|
565
|
+
mcpConfigPath,
|
|
566
|
+
delegationManager // Per-instance delegation limits
|
|
566
567
|
});
|
|
567
568
|
|
|
568
569
|
return result;
|
|
@@ -584,7 +585,7 @@ export const delegateTool = (options = {}) => {
|
|
|
584
585
|
* @returns {Object} Configured analyze_all tool
|
|
585
586
|
*/
|
|
586
587
|
export const analyzeAllTool = (options = {}) => {
|
|
587
|
-
const { sessionId, debug = false } = options;
|
|
588
|
+
const { sessionId, debug = false, delegationManager = null } = options;
|
|
588
589
|
|
|
589
590
|
return tool({
|
|
590
591
|
name: 'analyze_all',
|
|
@@ -616,7 +617,8 @@ export const analyzeAllTool = (options = {}) => {
|
|
|
616
617
|
allowedFolders: options.allowedFolders,
|
|
617
618
|
provider: options.provider,
|
|
618
619
|
model: options.model,
|
|
619
|
-
tracer: options.tracer
|
|
620
|
+
tracer: options.tracer,
|
|
621
|
+
delegationManager // Per-instance delegation limits
|
|
620
622
|
});
|
|
621
623
|
|
|
622
624
|
return result;
|