@securecode-ai/mcp 0.5.8 → 0.6.1
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/README.md +2 -1
- package/dist/api/client.d.ts +11 -1
- package/dist/api/client.js +93 -8
- package/dist/api/client.js.map +1 -1
- package/dist/api/types.d.ts +24 -0
- package/dist/approval/policy.js +5 -0
- package/dist/approval/policy.js.map +1 -1
- package/dist/attack/agentScanBatchProtocol.d.ts +98 -0
- package/dist/attack/agentScanBatchProtocol.js +125 -0
- package/dist/attack/agentScanBatchProtocol.js.map +1 -0
- package/dist/attack/agentScanBatchSelection.d.ts +38 -0
- package/dist/attack/agentScanBatchSelection.js +167 -0
- package/dist/attack/agentScanBatchSelection.js.map +1 -0
- package/dist/attack/agentScanCoordinator.d.ts +33 -0
- package/dist/attack/agentScanCoordinator.js +116 -0
- package/dist/attack/agentScanCoordinator.js.map +1 -0
- package/dist/attack/agentScanCreditPlan.d.ts +40 -0
- package/dist/attack/agentScanCreditPlan.js +50 -0
- package/dist/attack/agentScanCreditPlan.js.map +1 -0
- package/dist/attack/agentScanLoop.js +205 -111
- package/dist/attack/agentScanLoop.js.map +1 -1
- package/dist/attack/agentScanProtocol.d.ts +2 -2
- package/dist/attack/agentScanProtocol.js.map +1 -1
- package/dist/attack/investigationState.d.ts +1 -0
- package/dist/attack/investigationState.js +20 -7
- package/dist/attack/investigationState.js.map +1 -1
- package/dist/attack/scanScheduler.d.ts +5 -0
- package/dist/attack/scanScheduler.js +37 -2
- package/dist/attack/scanScheduler.js.map +1 -1
- package/dist/attack/scanState.d.ts +6 -0
- package/dist/attack/scanState.js +5 -0
- package/dist/attack/scanState.js.map +1 -1
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.js +35 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/project-map/scanCache.d.ts +4 -1
- package/dist/project-map/scanCache.js +15 -0
- package/dist/project-map/scanCache.js.map +1 -1
- package/dist/tools/agentScan.js +10 -1
- package/dist/tools/agentScan.js.map +1 -1
- package/dist/tools/agentScanBatch.d.ts +18 -0
- package/dist/tools/agentScanBatch.js +178 -0
- package/dist/tools/agentScanBatch.js.map +1 -0
- package/package.json +1 -1
|
@@ -66,7 +66,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
66
66
|
const startValidation = (0, protocolValidator_1.validateStartResponse)(startRespRaw);
|
|
67
67
|
if (!startValidation.ok) {
|
|
68
68
|
return {
|
|
69
|
-
status: '
|
|
69
|
+
status: 'failed',
|
|
70
70
|
findings: [],
|
|
71
71
|
transcript: [],
|
|
72
72
|
stepsUsed: 0,
|
|
@@ -253,12 +253,16 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
253
253
|
let consecutiveBlockedReads = 0;
|
|
254
254
|
let meaningfulProgressSinceRecovery = true;
|
|
255
255
|
let aggressiveCompaction = false;
|
|
256
|
+
let lastFinishAction = null;
|
|
257
|
+
let lastFinishRejectionReasons = [];
|
|
258
|
+
const attemptedRecoveryFingerprints = new Set();
|
|
259
|
+
const RECOVERY_FAILURE_LIMIT = 3;
|
|
256
260
|
while (true) {
|
|
257
261
|
// Wall clock check
|
|
258
262
|
if (Date.now() - startTime > wallClockMs) {
|
|
259
263
|
(0, scanState_1.terminateScan)(scanState, 'wall_clock', `Wall clock limit (${wallClockMs}ms) exceeded.`);
|
|
260
264
|
return {
|
|
261
|
-
status: '
|
|
265
|
+
status: 'incomplete',
|
|
262
266
|
findings: [],
|
|
263
267
|
transcript,
|
|
264
268
|
stepsUsed: stepsTaken,
|
|
@@ -293,23 +297,24 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
293
297
|
// 2 blocked reads: recovery mode — if unread ranges exist,
|
|
294
298
|
// require the next unread range; if no unread ranges remain,
|
|
295
299
|
// forbid read_file entirely and require an analysis tool.
|
|
296
|
-
// 3 blocked reads: MCP selects a deterministic recovery action
|
|
300
|
+
// 3+ blocked reads: MCP selects a deterministic recovery action via scheduler
|
|
297
301
|
let actionConstraint;
|
|
298
302
|
if (consecutiveBlockedReads >= 2 && consecutiveBlockedReads < 3) {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
:
|
|
302
|
-
|
|
303
|
+
const schedDecision = (0, scanScheduler_1.schedule)({
|
|
304
|
+
state: scanState,
|
|
305
|
+
evidence: evidenceLedger,
|
|
306
|
+
workItems: workItemQueue,
|
|
307
|
+
handlers: handlerInventory,
|
|
308
|
+
candidates: candidateStore,
|
|
309
|
+
investigation: investigationState,
|
|
310
|
+
target,
|
|
311
|
+
functionBoundaries,
|
|
312
|
+
});
|
|
313
|
+
if (schedDecision.action) {
|
|
303
314
|
actionConstraint = {
|
|
304
315
|
mode: 'recovery',
|
|
305
|
-
requiredAction:
|
|
306
|
-
|
|
307
|
-
path: target.filePath,
|
|
308
|
-
startLine: nextRange.start,
|
|
309
|
-
endLine: nextRange.end,
|
|
310
|
-
rationale: `Read next unread range: lines ${nextRange.start}-${nextRange.end}`,
|
|
311
|
-
},
|
|
312
|
-
reason: `You have been blocked by duplicate/overlapping reads. Read the next unread range: lines ${nextRange.start}-${nextRange.end}, or use an analysis tool (trace_flow, check_guard, check_policy).`,
|
|
316
|
+
requiredAction: schedDecision.action.type,
|
|
317
|
+
reason: `You have been blocked by duplicate/overlapping reads. Next required action: ${schedDecision.action.type}. ${schedDecision.reason}`,
|
|
313
318
|
};
|
|
314
319
|
}
|
|
315
320
|
else {
|
|
@@ -371,7 +376,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
371
376
|
});
|
|
372
377
|
if (consecutiveErrors >= 3 || budget.stepsRemaining <= 0) {
|
|
373
378
|
return {
|
|
374
|
-
status: '
|
|
379
|
+
status: 'incomplete',
|
|
375
380
|
findings: [],
|
|
376
381
|
transcript,
|
|
377
382
|
stepsUsed: stepsTaken,
|
|
@@ -394,16 +399,43 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
394
399
|
// Detect API server restart — the run was lost. Don't waste
|
|
395
400
|
// 3 retry steps on a dead run; return immediately.
|
|
396
401
|
if (apiCode === 'AGENT_RUN_NOT_FOUND' || /AGENT_RUN_NOT_FOUND|Invalid or expired agent run/i.test(errMsg)) {
|
|
402
|
+
if (lastFinishAction) {
|
|
403
|
+
console.warn('[Agent Scan Loop] Agent run already terminated after a delivered finish — surfacing the finish findings.');
|
|
404
|
+
const finish = lastFinishAction;
|
|
405
|
+
const coverageGaps = [...(finish.coverageGaps ?? [])];
|
|
406
|
+
if (lastFinishRejectionReasons.length > 0) {
|
|
407
|
+
coverageGaps.push({
|
|
408
|
+
title: 'Finish accepted by the server despite local gate concerns',
|
|
409
|
+
detail: `Local finish gate still flagged: ${lastFinishRejectionReasons.join('; ')}`,
|
|
410
|
+
requiredEvidence: [],
|
|
411
|
+
suggestedNextAction: 'Re-run a scan to cover the flagged gaps.',
|
|
412
|
+
priority: 'medium',
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
status: 'completed',
|
|
417
|
+
findings: sanitizeFindings(finish.findings),
|
|
418
|
+
investigationNotes: finish.investigationNotes ?? [],
|
|
419
|
+
coverageGaps,
|
|
420
|
+
transcript,
|
|
421
|
+
stepsUsed: stepsTaken,
|
|
422
|
+
stepsGranted,
|
|
423
|
+
extensionsGranted,
|
|
424
|
+
costSpentUsd,
|
|
425
|
+
terminationReason: 'agent_finish',
|
|
426
|
+
summary: finish.summary,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
397
429
|
console.warn(`[Agent Scan Loop] Agent run expired (API server restarted?). Stopping scan.`);
|
|
398
430
|
return {
|
|
399
|
-
status: '
|
|
431
|
+
status: 'failed',
|
|
400
432
|
findings: [],
|
|
401
433
|
transcript,
|
|
402
434
|
stepsUsed: stepsTaken,
|
|
403
435
|
stepsGranted,
|
|
404
436
|
extensionsGranted,
|
|
405
437
|
costSpentUsd,
|
|
406
|
-
terminationReason: '
|
|
438
|
+
terminationReason: 'api_restart',
|
|
407
439
|
error: 'API server restarted mid-scan — the run was lost. Please retry the scan.',
|
|
408
440
|
investigationNotes: [],
|
|
409
441
|
coverageGaps: [],
|
|
@@ -451,7 +483,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
451
483
|
});
|
|
452
484
|
if (consecutiveErrors >= 3 || budget.stepsRemaining <= 0) {
|
|
453
485
|
return {
|
|
454
|
-
status: '
|
|
486
|
+
status: 'incomplete',
|
|
455
487
|
findings: [],
|
|
456
488
|
transcript,
|
|
457
489
|
stepsUsed: stepsTaken,
|
|
@@ -520,8 +552,8 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
520
552
|
// Null next = done (cost capped or steps exhausted)
|
|
521
553
|
if (!stepResp.next) {
|
|
522
554
|
const status = stepResp.costCapped
|
|
523
|
-
? '
|
|
524
|
-
: (stepResp.degraded ? '
|
|
555
|
+
? 'incomplete'
|
|
556
|
+
: (stepResp.degraded ? 'incomplete' : 'completed');
|
|
525
557
|
trace.logRunCompleted(status);
|
|
526
558
|
return {
|
|
527
559
|
status,
|
|
@@ -555,6 +587,8 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
555
587
|
if (action.type === 'finish') {
|
|
556
588
|
scanState.finishAttempts++;
|
|
557
589
|
qualityTracker.recordFinishAttempt();
|
|
590
|
+
lastFinishAction = action;
|
|
591
|
+
lastFinishRejectionReasons = [];
|
|
558
592
|
// Register candidates from findings — each finding becomes a
|
|
559
593
|
// tracked candidate. If the candidate is new (discovered), the
|
|
560
594
|
// finish gate will reject and the agent must gather evidence.
|
|
@@ -700,6 +734,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
700
734
|
// Finish rejected — continue investigation
|
|
701
735
|
trace.logToolBlocked('finish', `Finish rejected: ${gateResult.reasons.map(r => r.description).join('; ')}`);
|
|
702
736
|
qualityTracker.recordFinishRejection();
|
|
737
|
+
lastFinishRejectionReasons = gateResult.reasons.map(r => r.description);
|
|
703
738
|
// Add a system event so the model sees the rejection
|
|
704
739
|
const rejectionMessage = `FINISH REJECTED — your investigation is incomplete:\n${gateResult.reasons.map(r => ` - ${r.description}`).join('\n')}\n\nYou must complete the remaining investigation steps before calling finish.${gateResult.recoveryAction ? `\n\nYour next action MUST be: ${gateResult.recoveryAction.type}` : ''}`;
|
|
705
740
|
transcript.push({
|
|
@@ -955,40 +990,28 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
955
990
|
consecutiveBlockedReads++;
|
|
956
991
|
scanState.recovery.consecutiveBlockedActions = consecutiveBlockedReads;
|
|
957
992
|
scanState.recovery.totalBlockedActions++;
|
|
993
|
+
scanState.recovery.consecutiveModelBlockedActions = consecutiveBlockedReads;
|
|
994
|
+
scanState.recovery.totalModelBlockedActions++;
|
|
958
995
|
scanState.recovery.lastBlockedAction = action.type;
|
|
959
996
|
scanState.recovery.meaningfulProgressSinceRecovery = false;
|
|
960
997
|
const recoveryLimit = agentScanProtocol_1.AGENT_SCAN_DEFAULTS.blockedReadRecoveryLimit;
|
|
961
998
|
if (consecutiveBlockedReads >= 2 && consecutiveBlockedReads < 3) {
|
|
962
|
-
|
|
963
|
-
? investigationState.getPrioritizedUnreadRange(target.filePath, target.fileContent)
|
|
964
|
-
: investigationState.getNextUnreadRange(target.filePath);
|
|
965
|
-
if (nextRange) {
|
|
966
|
-
observation += `\n\nRECOVERY REQUIRED: You have been blocked ${consecutiveBlockedReads} time(s). Read the next unread range: lines ${nextRange.start}-${nextRange.end}, or use an analysis tool (trace_flow, check_guard, check_policy).`;
|
|
967
|
-
}
|
|
968
|
-
else {
|
|
969
|
-
const recommendedTool = investigationState.getRecommendedRecoveryAction();
|
|
970
|
-
if (recommendedTool) {
|
|
971
|
-
observation += `\n\nRECOVERY REQUIRED: You have been blocked ${consecutiveBlockedReads} time(s). No unread ranges remain. Your next action MUST be: ${recommendedTool}. If you have enough evidence, call finish.`;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
999
|
+
observation += `\n\nRECOVERY REQUIRED: You have been blocked ${consecutiveBlockedReads} time(s). Use an analysis tool (trace_flow, check_guard, check_policy) or read a different file.`;
|
|
974
1000
|
}
|
|
975
1001
|
if (consecutiveBlockedReads >= recoveryLimit) {
|
|
976
|
-
const nextRange = target.fileContent
|
|
977
|
-
? investigationState.getPrioritizedUnreadRange(target.filePath, target.fileContent)
|
|
978
|
-
: investigationState.getNextUnreadRange(target.filePath);
|
|
979
1002
|
const hasBudget = budget.stepsRemaining > 0 && costSpentUsd < budget.costCapUsd;
|
|
980
1003
|
const hasWallClock = Date.now() - startTime < wallClockMs;
|
|
981
|
-
if (
|
|
982
|
-
console.warn(`[Agent Scan Loop] ${consecutiveBlockedReads} consecutive blocked reads — forcing deterministic recovery
|
|
1004
|
+
if (hasBudget && hasWallClock) {
|
|
1005
|
+
console.warn(`[Agent Scan Loop] ${consecutiveBlockedReads} consecutive blocked reads — forcing deterministic recovery via scheduler.`);
|
|
983
1006
|
}
|
|
984
1007
|
else {
|
|
985
|
-
console.warn(`[Agent Scan Loop] ${consecutiveBlockedReads} consecutive blocked reads — no recovery available,
|
|
1008
|
+
console.warn(`[Agent Scan Loop] ${consecutiveBlockedReads} consecutive blocked reads — no recovery available, terminating as incomplete.`);
|
|
986
1009
|
transcript.push({ action, observation });
|
|
987
|
-
trace.logRunCompleted('
|
|
1010
|
+
trace.logRunCompleted('incomplete');
|
|
988
1011
|
const incompleteSteps = investigationState.getIncompleteSteps();
|
|
989
1012
|
const autoGaps = incompleteSteps.map(step => ({
|
|
990
1013
|
title: `Investigation step not completed: ${step}`,
|
|
991
|
-
detail: `The agent was
|
|
1014
|
+
detail: `The agent was terminated after repeated blocked reads without completing this required investigation step: ${step}. The investigation was incomplete and vulnerabilities may have been missed.`,
|
|
992
1015
|
file: target.filePath,
|
|
993
1016
|
requiredEvidence: [`Complete the ${step} step before concluding no vulnerabilities exist`],
|
|
994
1017
|
suggestedNextAction: step === 'config-inspection' ? 'read_config'
|
|
@@ -1024,7 +1047,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
1024
1047
|
qualityTracker.recordCoverage(covSummary.totalLines, covSummary.coveredLines, covSummary.uncoveredRangeCount, covSummary.largestUncoveredRange);
|
|
1025
1048
|
qualityTracker.recordBudget(stepsTaken, stepsGranted, extensionsGranted, costSpentUsd, budget.costCapUsd, wallClockMs, Date.now() - startTime);
|
|
1026
1049
|
return {
|
|
1027
|
-
status: '
|
|
1050
|
+
status: 'incomplete',
|
|
1028
1051
|
findings: [],
|
|
1029
1052
|
transcript,
|
|
1030
1053
|
stepsUsed: stepsTaken,
|
|
@@ -1048,6 +1071,8 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
1048
1071
|
if (meaningfulProgressSinceRecovery) {
|
|
1049
1072
|
consecutiveBlockedReads = 0;
|
|
1050
1073
|
scanState.recovery.consecutiveBlockedActions = 0;
|
|
1074
|
+
scanState.recovery.consecutiveModelBlockedActions = 0;
|
|
1075
|
+
scanState.recovery.consecutiveRecoveryFailures = 0;
|
|
1051
1076
|
scanState.recovery.meaningfulProgressSinceRecovery = true;
|
|
1052
1077
|
}
|
|
1053
1078
|
}
|
|
@@ -1247,42 +1272,156 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
1247
1272
|
}
|
|
1248
1273
|
// Deterministic recovery: on the third consecutive blocked read,
|
|
1249
1274
|
// skip the API and directly execute a recovery action selected by
|
|
1250
|
-
// the
|
|
1251
|
-
// stuck re-reading files.
|
|
1275
|
+
// the scheduler. This prevents wasting API calls on an agent that
|
|
1276
|
+
// is stuck re-reading files. Track recovery fingerprints to avoid
|
|
1277
|
+
// repeating the same recovery action. Terminate after
|
|
1278
|
+
// RECOVERY_FAILURE_LIMIT failures without progress.
|
|
1252
1279
|
if (wasBlocked && consecutiveBlockedReads >= 3) {
|
|
1253
|
-
const
|
|
1280
|
+
const schedDecision = (0, scanScheduler_1.schedule)({
|
|
1281
|
+
state: scanState,
|
|
1282
|
+
evidence: evidenceLedger,
|
|
1283
|
+
workItems: workItemQueue,
|
|
1284
|
+
handlers: handlerInventory,
|
|
1285
|
+
candidates: candidateStore,
|
|
1286
|
+
investigation: investigationState,
|
|
1287
|
+
target,
|
|
1288
|
+
functionBoundaries,
|
|
1289
|
+
});
|
|
1290
|
+
const recoveryAction = schedDecision.action || null;
|
|
1254
1291
|
if (recoveryAction) {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
if (
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1292
|
+
const fp = (0, scanScheduler_1.actionFingerprint)(recoveryAction);
|
|
1293
|
+
const isDuplicate = attemptedRecoveryFingerprints.has(fp);
|
|
1294
|
+
scanState.recovery.totalRecoveryAttempts++;
|
|
1295
|
+
scanState.recovery.lastRecoveryAction = recoveryAction.type;
|
|
1296
|
+
if (isDuplicate) {
|
|
1297
|
+
scanState.recovery.consecutiveRecoveryFailures++;
|
|
1298
|
+
scanState.recovery.lastFailureReason = `Duplicate recovery action: ${fp}`;
|
|
1299
|
+
console.warn(`[Agent Scan Loop] Duplicate recovery action rejected: ${fp}. Failures: ${scanState.recovery.consecutiveRecoveryFailures}/${RECOVERY_FAILURE_LIMIT}`);
|
|
1300
|
+
if (scanState.recovery.consecutiveRecoveryFailures >= RECOVERY_FAILURE_LIMIT) {
|
|
1301
|
+
console.warn(`[Agent Scan Loop] ${RECOVERY_FAILURE_LIMIT} recovery failures without progress — terminating as incomplete.`);
|
|
1302
|
+
transcript.push({ action, observation });
|
|
1303
|
+
trace.logRunCompleted('incomplete');
|
|
1304
|
+
(0, scanState_1.terminateScan)(scanState, 'blocked_read_recovery', `${RECOVERY_FAILURE_LIMIT} recovery failures without progress.`);
|
|
1305
|
+
qualityTracker.recordForcedTermination('blocked_read_recovery');
|
|
1306
|
+
const covSummary = investigationState.getCoverageSummary(target.filePath);
|
|
1307
|
+
qualityTracker.recordCoverage(covSummary.totalLines, covSummary.coveredLines, covSummary.uncoveredRangeCount, covSummary.largestUncoveredRange);
|
|
1308
|
+
qualityTracker.recordBudget(stepsTaken, stepsGranted, extensionsGranted, costSpentUsd, budget.costCapUsd, wallClockMs, Date.now() - startTime);
|
|
1309
|
+
const incompleteSteps = investigationState.getIncompleteSteps();
|
|
1310
|
+
const autoGaps = incompleteSteps.map(step => ({
|
|
1311
|
+
title: `Investigation step not completed: ${step}`,
|
|
1312
|
+
detail: `The agent was terminated after ${RECOVERY_FAILURE_LIMIT} recovery failures without completing this required step: ${step}.`,
|
|
1313
|
+
file: target.filePath,
|
|
1314
|
+
requiredEvidence: [`Complete the ${step} step`],
|
|
1315
|
+
suggestedNextAction: 'continue investigation',
|
|
1316
|
+
priority: 'high',
|
|
1317
|
+
}));
|
|
1318
|
+
const unresolvedTasks = investigationState.getUnresolvedTasks();
|
|
1319
|
+
const taskGaps = unresolvedTasks.map(task => ({
|
|
1320
|
+
title: `Architecture risk unresolved: ${task.claim}`,
|
|
1321
|
+
detail: `This architecture-risk task was not resolved: ${task.claim}. Required evidence: ${task.requiredEvidence.join('; ')}.`,
|
|
1322
|
+
file: task.targetFiles[0] || target.filePath,
|
|
1323
|
+
requiredEvidence: task.requiredEvidence,
|
|
1324
|
+
suggestedNextAction: task.requiredTools[0] || 'continue investigation',
|
|
1325
|
+
priority: 'high',
|
|
1326
|
+
}));
|
|
1327
|
+
const uncoveredRanges = investigationState.getUncoveredRanges(target.filePath);
|
|
1328
|
+
const rangeGaps = uncoveredRanges.map(r => ({
|
|
1329
|
+
title: `Unread range: lines ${r.start}-${r.end} of ${target.filePath}`,
|
|
1330
|
+
detail: `This range was never read during the investigation.`,
|
|
1331
|
+
file: target.filePath,
|
|
1332
|
+
requiredEvidence: [`Read lines ${r.start}-${r.end}`],
|
|
1333
|
+
suggestedNextAction: 'read_file',
|
|
1334
|
+
priority: 'high',
|
|
1335
|
+
}));
|
|
1336
|
+
return {
|
|
1337
|
+
status: 'incomplete',
|
|
1338
|
+
findings: [],
|
|
1339
|
+
transcript,
|
|
1340
|
+
stepsUsed: stepsTaken,
|
|
1341
|
+
stepsGranted,
|
|
1342
|
+
extensionsGranted,
|
|
1343
|
+
costSpentUsd,
|
|
1344
|
+
terminationReason: 'blocked_read_recovery',
|
|
1345
|
+
summary: `Investigation incomplete — ${RECOVERY_FAILURE_LIMIT} recovery failures without progress. Model blocked ${scanState.recovery.totalModelBlockedActions} time(s), recovery attempted ${scanState.recovery.totalRecoveryAttempts} time(s).`,
|
|
1346
|
+
investigationNotes: [],
|
|
1347
|
+
coverageGaps: [...autoGaps, ...taskGaps, ...rangeGaps],
|
|
1348
|
+
};
|
|
1268
1349
|
}
|
|
1269
1350
|
}
|
|
1270
1351
|
else {
|
|
1271
|
-
|
|
1352
|
+
attemptedRecoveryFingerprints.add(fp);
|
|
1353
|
+
stepsTaken++;
|
|
1354
|
+
budget.stepsRemaining--;
|
|
1355
|
+
scanState.budget.stepsUsed = stepsTaken;
|
|
1356
|
+
scanState.budget.stepsRemaining = budget.stepsRemaining;
|
|
1357
|
+
trace.nextStep();
|
|
1358
|
+
trace.logActionSelected(recoveryAction.type, 'deterministic-recovery', false);
|
|
1359
|
+
if (options.onProgress) {
|
|
1360
|
+
options.onProgress(stepsTaken, stepsGranted, `Step ${stepsTaken}: deterministic recovery: ${describeAction(recoveryAction)}`);
|
|
1361
|
+
}
|
|
1362
|
+
let recoveryObservation;
|
|
1363
|
+
let recoveryMadeProgress = false;
|
|
1364
|
+
if (recoveryAction.type === 'read_file') {
|
|
1365
|
+
const readResult = await (0, agentScanExecutor_1.executeReadFileAction)(recoveryAction, ctx);
|
|
1366
|
+
recoveryObservation = readResult.observation;
|
|
1367
|
+
if (readResult.totalLines > 0 && !readResult.truncated) {
|
|
1368
|
+
investigationState.recordActualRead(recoveryAction.path, readResult.actualStart, readResult.actualEnd, readResult.totalLines, readResult.truncated);
|
|
1369
|
+
recoveryMadeProgress = true;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
recoveryObservation = await (0, agentScanExecutor_1.executeAction)(recoveryAction, ctx, startResp.runId, client, target);
|
|
1374
|
+
recoveryMadeProgress = true;
|
|
1375
|
+
}
|
|
1376
|
+
qualityTracker.recordToolUse(recoveryAction.type);
|
|
1377
|
+
investigationState.recordToolUse(recoveryAction.type);
|
|
1378
|
+
if (recoveryMadeProgress) {
|
|
1379
|
+
scanState.recovery.successfulRecoveryAttempts++;
|
|
1380
|
+
scanState.recovery.consecutiveRecoveryFailures = 0;
|
|
1381
|
+
consecutiveBlockedReads = 0;
|
|
1382
|
+
scanState.recovery.consecutiveBlockedActions = 0;
|
|
1383
|
+
scanState.recovery.consecutiveModelBlockedActions = 0;
|
|
1384
|
+
meaningfulProgressSinceRecovery = true;
|
|
1385
|
+
}
|
|
1386
|
+
else {
|
|
1387
|
+
scanState.recovery.consecutiveRecoveryFailures++;
|
|
1388
|
+
scanState.recovery.lastFailureReason = `Recovery action made no progress: ${recoveryAction.type}`;
|
|
1389
|
+
}
|
|
1390
|
+
trace.logToolCompleted(recoveryAction.type, recoveryObservation);
|
|
1391
|
+
transcript.push({ action: recoveryAction, observation: `[DETERMINISTIC RECOVERY] ${recoveryObservation}` });
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
else {
|
|
1395
|
+
// Scheduler has no action — finish or let model try
|
|
1396
|
+
if (schedDecision.kind === 'finish-ready') {
|
|
1397
|
+
console.warn(`[Agent Scan Loop] Scheduler says finish-ready during recovery — terminating as incomplete.`);
|
|
1398
|
+
transcript.push({ action, observation });
|
|
1399
|
+
trace.logRunCompleted('incomplete');
|
|
1400
|
+
(0, scanState_1.terminateScan)(scanState, 'blocked_read_recovery', 'Scheduler has no executable work during recovery.');
|
|
1401
|
+
const covSummary = investigationState.getCoverageSummary(target.filePath);
|
|
1402
|
+
qualityTracker.recordCoverage(covSummary.totalLines, covSummary.coveredLines, covSummary.uncoveredRangeCount, covSummary.largestUncoveredRange);
|
|
1403
|
+
qualityTracker.recordBudget(stepsTaken, stepsGranted, extensionsGranted, costSpentUsd, budget.costCapUsd, wallClockMs, Date.now() - startTime);
|
|
1404
|
+
return {
|
|
1405
|
+
status: 'incomplete',
|
|
1406
|
+
findings: [],
|
|
1407
|
+
transcript,
|
|
1408
|
+
stepsUsed: stepsTaken,
|
|
1409
|
+
stepsGranted,
|
|
1410
|
+
extensionsGranted,
|
|
1411
|
+
costSpentUsd,
|
|
1412
|
+
terminationReason: 'blocked_read_recovery',
|
|
1413
|
+
summary: 'Investigation incomplete — scheduler determined no executable work remains during recovery.',
|
|
1414
|
+
investigationNotes: [],
|
|
1415
|
+
coverageGaps: [],
|
|
1416
|
+
};
|
|
1272
1417
|
}
|
|
1273
|
-
qualityTracker.recordToolUse(recoveryAction.type);
|
|
1274
|
-
investigationState.recordToolUse(recoveryAction.type);
|
|
1275
|
-
consecutiveBlockedReads = 0; // recovery resets the counter
|
|
1276
|
-
meaningfulProgressSinceRecovery = true; // recovery is meaningful progress
|
|
1277
|
-
trace.logToolCompleted(recoveryAction.type, recoveryObservation);
|
|
1278
|
-
transcript.push({ action: recoveryAction, observation: `[DETERMINISTIC RECOVERY] ${recoveryObservation}` });
|
|
1279
1418
|
}
|
|
1280
1419
|
}
|
|
1281
1420
|
}
|
|
1282
1421
|
}
|
|
1283
1422
|
catch (err) {
|
|
1284
1423
|
return {
|
|
1285
|
-
status: '
|
|
1424
|
+
status: 'failed',
|
|
1286
1425
|
findings: [],
|
|
1287
1426
|
transcript: [],
|
|
1288
1427
|
stepsUsed: stepsTaken,
|
|
@@ -1467,49 +1606,4 @@ function describeAction(action) {
|
|
|
1467
1606
|
case 'system_event': return `system_event(${action.eventType})`;
|
|
1468
1607
|
}
|
|
1469
1608
|
}
|
|
1470
|
-
/**
|
|
1471
|
-
* Select a deterministic recovery action based on investigation state.
|
|
1472
|
-
*
|
|
1473
|
-
* Priority:
|
|
1474
|
-
* 1. Unread range exists on the target file → read_file(next unread range)
|
|
1475
|
-
* 2. route-discovery missing → get_endpoints
|
|
1476
|
-
* 3. policy-check missing → check_policy
|
|
1477
|
-
* 4. auth-symbol-search missing → search_code
|
|
1478
|
-
* 5. cross-file-flow missing → trace_flow_cross_file
|
|
1479
|
-
* 6. config-inspection missing → read_config
|
|
1480
|
-
* 7. tests-found missing → find_tests
|
|
1481
|
-
* 8. otherwise → null (force-finish will handle it)
|
|
1482
|
-
*/
|
|
1483
|
-
function selectDeterministicRecoveryAction(state, target, ctx) {
|
|
1484
|
-
// Priority 1: unread range on the target file
|
|
1485
|
-
const nextRange = state.getNextUnreadRange(target.filePath);
|
|
1486
|
-
if (nextRange) {
|
|
1487
|
-
return {
|
|
1488
|
-
type: 'read_file',
|
|
1489
|
-
path: target.filePath,
|
|
1490
|
-
startLine: nextRange.start,
|
|
1491
|
-
endLine: nextRange.end,
|
|
1492
|
-
rationale: 'Deterministic recovery: reading next unread range',
|
|
1493
|
-
};
|
|
1494
|
-
}
|
|
1495
|
-
// Priority 2-7: incomplete investigation steps
|
|
1496
|
-
const incomplete = state.getIncompleteSteps();
|
|
1497
|
-
for (const step of incomplete) {
|
|
1498
|
-
switch (step) {
|
|
1499
|
-
case 'route-discovery':
|
|
1500
|
-
return { type: 'get_endpoints', rationale: 'Deterministic recovery: route discovery' };
|
|
1501
|
-
case 'policy-check':
|
|
1502
|
-
return { type: 'check_policy', filePath: target.filePath, rationale: 'Deterministic recovery: policy check' };
|
|
1503
|
-
case 'auth-symbol-search':
|
|
1504
|
-
return { type: 'search_code', pattern: 'auth|require|guard|permission|owner', rationale: 'Deterministic recovery: auth symbol search' };
|
|
1505
|
-
case 'cross-file-flow':
|
|
1506
|
-
return { type: 'trace_flow_cross_file', filePath: target.filePath, rationale: 'Deterministic recovery: cross-file flow' };
|
|
1507
|
-
case 'config-inspection':
|
|
1508
|
-
return { type: 'read_config', configKind: 'all', rationale: 'Deterministic recovery: config inspection' };
|
|
1509
|
-
case 'tests-found':
|
|
1510
|
-
return { type: 'find_tests', filePath: target.filePath, rationale: 'Deterministic recovery: find tests' };
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
return null;
|
|
1514
|
-
}
|
|
1515
1609
|
//# sourceMappingURL=agentScanLoop.js.map
|