@securecode-ai/mcp 0.8.0 → 0.9.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/dist/attack/agentScanBatchProtocol.d.ts +1 -1
- package/dist/attack/agentScanBatchProtocol.js.map +1 -1
- package/dist/attack/agentScanLoop.js +135 -57
- package/dist/attack/agentScanLoop.js.map +1 -1
- package/dist/attack/agentScanProtocol.d.ts +1 -1
- package/dist/attack/agentScanProtocol.js.map +1 -1
- package/dist/attack/investigationProfiles.js +18 -1
- package/dist/attack/investigationProfiles.js.map +1 -1
- package/dist/attack/llmHealth.d.ts +53 -0
- package/dist/attack/llmHealth.js +100 -0
- package/dist/attack/llmHealth.js.map +1 -0
- package/dist/attack/runtimeProbe.d.ts +30 -0
- package/dist/attack/runtimeProbe.js +97 -0
- package/dist/attack/runtimeProbe.js.map +1 -1
- package/dist/attack/scanState.d.ts +1 -1
- package/dist/attack/scanState.js.map +1 -1
- package/dist/project-map/endpointDiscovery.d.ts +3 -0
- package/dist/project-map/endpointDiscovery.js +7 -0
- package/dist/project-map/endpointDiscovery.js.map +1 -1
- package/dist/project-map/scanCache.d.ts +1 -1
- package/dist/project-map/scanCache.js +1 -1
- package/dist/tools/agentScan.js +13 -5
- package/dist/tools/agentScan.js.map +1 -1
- package/dist/tools/agentScanBatch.js +7 -1
- package/dist/tools/agentScanBatch.js.map +1 -1
- package/package.json +1 -1
|
@@ -54,7 +54,7 @@ export interface AgentScanBatchFileResult {
|
|
|
54
54
|
costSpentUsd: number;
|
|
55
55
|
error?: AgentScanBatchFileError;
|
|
56
56
|
}
|
|
57
|
-
export type AgentScanBatchStopReason = 'completed' | 'architecture-failed' | 'architecture-incomplete' | 'insufficient-credits' | 'scan-incomplete' | 'scan-failed' | 'cancelled';
|
|
57
|
+
export type AgentScanBatchStopReason = 'completed' | 'architecture-failed' | 'architecture-incomplete' | 'insufficient-credits' | 'scan-incomplete' | 'scan-failed' | 'llm-degraded' | 'cancelled';
|
|
58
58
|
export interface AgentScanBatchResult {
|
|
59
59
|
status: 'completed' | 'incomplete' | 'failed' | 'preflight-failed' | 'cancelled';
|
|
60
60
|
stopReason: AgentScanBatchStopReason;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentScanBatchProtocol.js","sourceRoot":"","sources":["../../src/attack/agentScanBatchProtocol.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;
|
|
1
|
+
{"version":3,"file":"agentScanBatchProtocol.js","sourceRoot":"","sources":["../../src/attack/agentScanBatchProtocol.ts"],"names":[],"mappings":"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"}
|
|
@@ -39,6 +39,7 @@ const workItem_1 = require("./workItem");
|
|
|
39
39
|
const handlerInventory_1 = require("../project-map/handlerInventory");
|
|
40
40
|
const candidateStore_1 = require("./candidateStore");
|
|
41
41
|
const scanScheduler_1 = require("./scanScheduler");
|
|
42
|
+
const llmHealth_1 = require("./llmHealth");
|
|
42
43
|
const finishGate_1 = require("./finishGate");
|
|
43
44
|
const qualityMetrics_1 = require("./qualityMetrics");
|
|
44
45
|
const agentScanExecutor_2 = require("./agentScanExecutor");
|
|
@@ -61,6 +62,24 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
61
62
|
let stepsGranted = budget.stepsGranted;
|
|
62
63
|
let extensionsGranted = budget.extensionsGranted;
|
|
63
64
|
let meaningfulProgressSinceLastExtension = false;
|
|
65
|
+
let activeRunId = null;
|
|
66
|
+
// Best-effort run close: on any exit that is not a delivered
|
|
67
|
+
// agent_finish, tell the API to mark the run terminal immediately
|
|
68
|
+
// instead of waiting up to 30 minutes for the janitor. The janitor
|
|
69
|
+
// remains the fallback (refund happens there, not here).
|
|
70
|
+
const closeRun = async (result) => {
|
|
71
|
+
if (activeRunId && result.terminationReason !== 'agent_finish') {
|
|
72
|
+
try {
|
|
73
|
+
await client.postJson('/agent/scan/close', {
|
|
74
|
+
runId: activeRunId,
|
|
75
|
+
status: result.status,
|
|
76
|
+
terminationReason: result.terminationReason,
|
|
77
|
+
}, options.signal);
|
|
78
|
+
}
|
|
79
|
+
catch { /* best-effort — janitor will expire the run */ }
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
};
|
|
64
83
|
try {
|
|
65
84
|
const startRespRaw = await client.postJson('/agent/scan/start', {}, options.signal);
|
|
66
85
|
const startValidation = (0, protocolValidator_1.validateStartResponse)(startRespRaw);
|
|
@@ -80,8 +99,13 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
80
99
|
};
|
|
81
100
|
}
|
|
82
101
|
const startResp = startValidation.value;
|
|
102
|
+
activeRunId = startResp.runId;
|
|
83
103
|
const trace = new agentTrace_1.AgentTraceLogger(ctx.workspaceRoot, startResp.runId);
|
|
84
104
|
trace.logRunStarted();
|
|
105
|
+
// LLM health monitor — trips on degraded-model pathologies
|
|
106
|
+
// (repeat bursts, A/B alternation, provider degraded streaks) that
|
|
107
|
+
// the consecutive-blocked counters miss.
|
|
108
|
+
const llmHealth = (0, llmHealth_1.createLlmHealthMonitor)();
|
|
85
109
|
// Authoritative scan state — the single source of truth for phase,
|
|
86
110
|
// budget, recovery, and lifecycle. Local counters below are kept in
|
|
87
111
|
// sync with this state until they are fully replaced.
|
|
@@ -331,11 +355,49 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
331
355
|
let lastFinishRejectionReasons = [];
|
|
332
356
|
const attemptedRecoveryFingerprints = new Set();
|
|
333
357
|
const RECOVERY_FAILURE_LIMIT = 3;
|
|
358
|
+
// Coverage-gap snapshot for abnormal terminations — documents what
|
|
359
|
+
// the investigation did NOT get to, so "no findings" is never read
|
|
360
|
+
// as "clean" after a cut-short run.
|
|
361
|
+
const buildCoverageGaps = () => {
|
|
362
|
+
const incompleteSteps = investigationState.getIncompleteSteps();
|
|
363
|
+
const autoGaps = incompleteSteps.map(step => ({
|
|
364
|
+
title: `Investigation step not completed: ${step}`,
|
|
365
|
+
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.`,
|
|
366
|
+
file: target.filePath,
|
|
367
|
+
requiredEvidence: [`Complete the ${step} step before concluding no vulnerabilities exist`],
|
|
368
|
+
suggestedNextAction: step === 'config-inspection' ? 'read_config'
|
|
369
|
+
: step === 'policy-check' ? 'check_policy'
|
|
370
|
+
: step === 'cross-file-flow' ? 'trace_flow_cross_file'
|
|
371
|
+
: step === 'route-discovery' ? 'get_endpoints'
|
|
372
|
+
: step === 'auth-symbol-search' ? 'search_code'
|
|
373
|
+
: 'continue investigation',
|
|
374
|
+
priority: 'high',
|
|
375
|
+
}));
|
|
376
|
+
const unresolvedTasks = investigationState.getUnresolvedTasks();
|
|
377
|
+
const taskGaps = unresolvedTasks.map(task => ({
|
|
378
|
+
title: `Architecture risk unresolved: ${task.claim}`,
|
|
379
|
+
detail: `This architecture-risk investigation task was not resolved: ${task.claim}. Required evidence: ${task.requiredEvidence.join('; ')}.`,
|
|
380
|
+
file: task.targetFiles[0] || target.filePath,
|
|
381
|
+
requiredEvidence: task.requiredEvidence,
|
|
382
|
+
suggestedNextAction: task.requiredTools[0] || 'continue investigation',
|
|
383
|
+
priority: 'high',
|
|
384
|
+
}));
|
|
385
|
+
const uncoveredRanges = investigationState.getUncoveredRanges(target.filePath);
|
|
386
|
+
const rangeGaps = uncoveredRanges.map(r => ({
|
|
387
|
+
title: `Unread range: lines ${r.start}-${r.end} of ${target.filePath}`,
|
|
388
|
+
detail: `This range was never read during the investigation. Vulnerabilities in this range were not checked.`,
|
|
389
|
+
file: target.filePath,
|
|
390
|
+
requiredEvidence: [`Read lines ${r.start}-${r.end} and analyze for vulnerabilities`],
|
|
391
|
+
suggestedNextAction: 'read_file',
|
|
392
|
+
priority: 'high',
|
|
393
|
+
}));
|
|
394
|
+
return [...autoGaps, ...taskGaps, ...rangeGaps];
|
|
395
|
+
};
|
|
334
396
|
while (true) {
|
|
335
397
|
// Wall clock check
|
|
336
398
|
if (Date.now() - startTime > wallClockMs) {
|
|
337
399
|
(0, scanState_1.terminateScan)(scanState, 'wall_clock', `Wall clock limit (${wallClockMs}ms) exceeded.`);
|
|
338
|
-
return {
|
|
400
|
+
return closeRun({
|
|
339
401
|
status: 'incomplete',
|
|
340
402
|
findings: [],
|
|
341
403
|
transcript,
|
|
@@ -347,12 +409,12 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
347
409
|
summary: `Wall clock limit (${wallClockMs}ms) exceeded.`,
|
|
348
410
|
investigationNotes: [],
|
|
349
411
|
coverageGaps: [],
|
|
350
|
-
};
|
|
412
|
+
});
|
|
351
413
|
}
|
|
352
414
|
// Abort check
|
|
353
415
|
if (options.signal?.aborted) {
|
|
354
416
|
(0, scanState_1.terminateScan)(scanState, 'cancelled', 'Cancelled by user.');
|
|
355
|
-
return {
|
|
417
|
+
return closeRun({
|
|
356
418
|
status: 'cancelled',
|
|
357
419
|
findings: [],
|
|
358
420
|
transcript,
|
|
@@ -364,7 +426,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
364
426
|
summary: 'Cancelled by user.',
|
|
365
427
|
investigationNotes: [],
|
|
366
428
|
coverageGaps: [],
|
|
367
|
-
};
|
|
429
|
+
});
|
|
368
430
|
}
|
|
369
431
|
// Build action constraint for blocked-read recovery.
|
|
370
432
|
// 0-1 blocked reads: normal (no constraint)
|
|
@@ -451,7 +513,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
451
513
|
observation: errMsg,
|
|
452
514
|
});
|
|
453
515
|
if (consecutiveErrors >= 3 || budget.stepsRemaining <= 0) {
|
|
454
|
-
return {
|
|
516
|
+
return closeRun({
|
|
455
517
|
status: 'incomplete',
|
|
456
518
|
findings: [],
|
|
457
519
|
transcript,
|
|
@@ -463,7 +525,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
463
525
|
summary: `Step budget exhausted after ${consecutiveErrors} consecutive malformed API responses. Last error: ${vErr}`,
|
|
464
526
|
investigationNotes: [],
|
|
465
527
|
coverageGaps: [],
|
|
466
|
-
};
|
|
528
|
+
});
|
|
467
529
|
}
|
|
468
530
|
continue;
|
|
469
531
|
}
|
|
@@ -488,7 +550,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
488
550
|
priority: 'medium',
|
|
489
551
|
});
|
|
490
552
|
}
|
|
491
|
-
return {
|
|
553
|
+
return closeRun({
|
|
492
554
|
status: 'completed',
|
|
493
555
|
findings: sanitizeFindings(finish.findings),
|
|
494
556
|
investigationNotes: finish.investigationNotes ?? [],
|
|
@@ -500,10 +562,10 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
500
562
|
costSpentUsd,
|
|
501
563
|
terminationReason: 'agent_finish',
|
|
502
564
|
summary: finish.summary,
|
|
503
|
-
};
|
|
565
|
+
});
|
|
504
566
|
}
|
|
505
567
|
console.warn(`[Agent Scan Loop] Agent run expired (API server restarted?). Stopping scan.`);
|
|
506
|
-
return {
|
|
568
|
+
return closeRun({
|
|
507
569
|
status: 'failed',
|
|
508
570
|
findings: [],
|
|
509
571
|
transcript,
|
|
@@ -515,11 +577,11 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
515
577
|
error: 'API server restarted mid-scan — the run was lost. Please retry the scan.',
|
|
516
578
|
investigationNotes: [],
|
|
517
579
|
coverageGaps: [],
|
|
518
|
-
};
|
|
580
|
+
});
|
|
519
581
|
}
|
|
520
582
|
// Detect abort — the user cancelled. Don't treat as error.
|
|
521
583
|
if (options.signal?.aborted || /aborted/i.test(errMsg)) {
|
|
522
|
-
return {
|
|
584
|
+
return closeRun({
|
|
523
585
|
status: 'cancelled',
|
|
524
586
|
findings: [],
|
|
525
587
|
transcript,
|
|
@@ -531,7 +593,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
531
593
|
summary: 'Cancelled by user.',
|
|
532
594
|
investigationNotes: [],
|
|
533
595
|
coverageGaps: [],
|
|
534
|
-
};
|
|
596
|
+
});
|
|
535
597
|
}
|
|
536
598
|
// Detect timeout / network errors — retry once with aggressive compaction
|
|
537
599
|
if (!aggressiveCompaction && /timeout|ETIMEDOUT|ESOCKETTIMEDOUT|socket hang up|ECONNRESET|fetch failed/i.test(errMsg)) {
|
|
@@ -558,7 +620,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
558
620
|
observation: errorMsg,
|
|
559
621
|
});
|
|
560
622
|
if (consecutiveErrors >= 3 || budget.stepsRemaining <= 0) {
|
|
561
|
-
return {
|
|
623
|
+
return closeRun({
|
|
562
624
|
status: 'incomplete',
|
|
563
625
|
findings: [],
|
|
564
626
|
transcript,
|
|
@@ -570,13 +632,14 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
570
632
|
summary: `Step budget exhausted after ${consecutiveErrors} consecutive API errors. Last error: ${errMsg}`,
|
|
571
633
|
investigationNotes: [],
|
|
572
634
|
coverageGaps: [],
|
|
573
|
-
};
|
|
635
|
+
});
|
|
574
636
|
}
|
|
575
637
|
continue;
|
|
576
638
|
}
|
|
577
639
|
costSpentUsd += stepResp.costUsd || 0;
|
|
578
640
|
scanState.budget.costSpentUsd = costSpentUsd;
|
|
579
641
|
consecutiveErrors = 0;
|
|
642
|
+
llmHealth.recordStepTelemetry(!!stepResp.degraded || !!stepResp.fallbackFired);
|
|
580
643
|
trace.logStepRequested(stepResp.model, stepResp.tokens, stepResp.costUsd, stepResp.latencyMs);
|
|
581
644
|
// Transition from planning to surveying on first successful step
|
|
582
645
|
if (scanState.phase === 'planning') {
|
|
@@ -631,7 +694,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
631
694
|
? 'incomplete'
|
|
632
695
|
: (stepResp.degraded ? 'incomplete' : 'completed');
|
|
633
696
|
trace.logRunCompleted(status);
|
|
634
|
-
return {
|
|
697
|
+
return closeRun({
|
|
635
698
|
status,
|
|
636
699
|
findings: [],
|
|
637
700
|
transcript,
|
|
@@ -645,9 +708,46 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
645
708
|
: 'Agent completed without explicit finish.',
|
|
646
709
|
investigationNotes: [],
|
|
647
710
|
coverageGaps: [],
|
|
648
|
-
};
|
|
711
|
+
});
|
|
649
712
|
}
|
|
650
713
|
const action = stepResp.next;
|
|
714
|
+
llmHealth.recordModelAction((0, scanScheduler_1.actionFingerprint)(action));
|
|
715
|
+
const healthTrip = llmHealth.evaluate();
|
|
716
|
+
if (healthTrip.tripped) {
|
|
717
|
+
// Degraded-model circuit breaker — the model is looping
|
|
718
|
+
// (repeat bursts / alternation) or the provider reports a
|
|
719
|
+
// sustained degraded streak. Continuing burns budget without
|
|
720
|
+
// producing evidence; terminate with documented gaps instead.
|
|
721
|
+
console.warn(`[Agent Scan Loop] LLM health trip (${healthTrip.signal}) at step ${stepsTaken + 1}: ${healthTrip.detail}`);
|
|
722
|
+
(0, scanState_1.terminateScan)(scanState, 'llm_degraded', healthTrip.detail);
|
|
723
|
+
qualityTracker.recordForcedTermination('llm_degraded');
|
|
724
|
+
transcript.push({
|
|
725
|
+
action: {
|
|
726
|
+
type: 'system_event',
|
|
727
|
+
eventType: 'error',
|
|
728
|
+
message: `Scan terminated: the model server appears degraded (${healthTrip.detail})`,
|
|
729
|
+
},
|
|
730
|
+
observation: healthTrip.detail,
|
|
731
|
+
});
|
|
732
|
+
trace.logRunCompleted('incomplete');
|
|
733
|
+
const allGaps = buildCoverageGaps();
|
|
734
|
+
const covSummary = investigationState.getCoverageSummary(target.filePath);
|
|
735
|
+
qualityTracker.recordCoverage(covSummary.totalLines, covSummary.coveredLines, covSummary.uncoveredRangeCount, covSummary.largestUncoveredRange);
|
|
736
|
+
qualityTracker.recordBudget(stepsTaken, stepsGranted, extensionsGranted, costSpentUsd, budget.costCapUsd, wallClockMs, Date.now() - startTime);
|
|
737
|
+
return closeRun({
|
|
738
|
+
status: 'incomplete',
|
|
739
|
+
findings: [],
|
|
740
|
+
transcript,
|
|
741
|
+
stepsUsed: stepsTaken,
|
|
742
|
+
stepsGranted,
|
|
743
|
+
extensionsGranted,
|
|
744
|
+
costSpentUsd,
|
|
745
|
+
terminationReason: 'llm_degraded',
|
|
746
|
+
summary: `Model server degraded — ${healthTrip.detail} ${allGaps.length} coverage gaps identified. Retry the scan later; no credits are lost (the run is auto-refunded).`,
|
|
747
|
+
investigationNotes: [],
|
|
748
|
+
coverageGaps: allGaps,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
651
751
|
stepsTaken++;
|
|
652
752
|
budget.stepsRemaining = stepResp.stepsRemaining;
|
|
653
753
|
scanState.budget.stepsUsed = stepsTaken;
|
|
@@ -792,7 +892,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
792
892
|
candidateStore.setUnproven(candidate.id, `Converted to note: missing proof dimensions ${missingDims.join(', ')}`);
|
|
793
893
|
}
|
|
794
894
|
}
|
|
795
|
-
return {
|
|
895
|
+
return closeRun({
|
|
796
896
|
status: 'completed',
|
|
797
897
|
findings: sanitizeFindings(action.findings),
|
|
798
898
|
investigationNotes,
|
|
@@ -805,7 +905,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
805
905
|
terminationReason: gateResult.mode === 'forced-incomplete' ? 'forced_incomplete' : 'agent_finish',
|
|
806
906
|
summary: action.summary,
|
|
807
907
|
qualityMetrics: qualityTracker.getMetrics(),
|
|
808
|
-
};
|
|
908
|
+
});
|
|
809
909
|
}
|
|
810
910
|
// Finish rejected — continue investigation
|
|
811
911
|
trace.logToolBlocked('finish', `Finish rejected: ${gateResult.reasons.map(r => r.description).join('; ')}`);
|
|
@@ -1104,45 +1204,13 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
1104
1204
|
console.warn(`[Agent Scan Loop] ${consecutiveBlockedReads} consecutive blocked reads — no recovery available, terminating as incomplete.`);
|
|
1105
1205
|
transcript.push({ action, observation });
|
|
1106
1206
|
trace.logRunCompleted('incomplete');
|
|
1107
|
-
const
|
|
1108
|
-
const autoGaps = incompleteSteps.map(step => ({
|
|
1109
|
-
title: `Investigation step not completed: ${step}`,
|
|
1110
|
-
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.`,
|
|
1111
|
-
file: target.filePath,
|
|
1112
|
-
requiredEvidence: [`Complete the ${step} step before concluding no vulnerabilities exist`],
|
|
1113
|
-
suggestedNextAction: step === 'config-inspection' ? 'read_config'
|
|
1114
|
-
: step === 'policy-check' ? 'check_policy'
|
|
1115
|
-
: step === 'cross-file-flow' ? 'trace_flow_cross_file'
|
|
1116
|
-
: step === 'route-discovery' ? 'get_endpoints'
|
|
1117
|
-
: step === 'auth-symbol-search' ? 'search_code'
|
|
1118
|
-
: 'continue investigation',
|
|
1119
|
-
priority: 'high',
|
|
1120
|
-
}));
|
|
1121
|
-
const unresolvedTasks = investigationState.getUnresolvedTasks();
|
|
1122
|
-
const taskGaps = unresolvedTasks.map(task => ({
|
|
1123
|
-
title: `Architecture risk unresolved: ${task.claim}`,
|
|
1124
|
-
detail: `This architecture-risk investigation task was not resolved: ${task.claim}. Required evidence: ${task.requiredEvidence.join('; ')}.`,
|
|
1125
|
-
file: task.targetFiles[0] || target.filePath,
|
|
1126
|
-
requiredEvidence: task.requiredEvidence,
|
|
1127
|
-
suggestedNextAction: task.requiredTools[0] || 'continue investigation',
|
|
1128
|
-
priority: 'high',
|
|
1129
|
-
}));
|
|
1130
|
-
const uncoveredRanges = investigationState.getUncoveredRanges(target.filePath);
|
|
1131
|
-
const rangeGaps = uncoveredRanges.map(r => ({
|
|
1132
|
-
title: `Unread range: lines ${r.start}-${r.end} of ${target.filePath}`,
|
|
1133
|
-
detail: `This range was never read during the investigation. Vulnerabilities in this range were not checked.`,
|
|
1134
|
-
file: target.filePath,
|
|
1135
|
-
requiredEvidence: [`Read lines ${r.start}-${r.end} and analyze for vulnerabilities`],
|
|
1136
|
-
suggestedNextAction: 'read_file',
|
|
1137
|
-
priority: 'high',
|
|
1138
|
-
}));
|
|
1139
|
-
const allGaps = [...autoGaps, ...taskGaps, ...rangeGaps];
|
|
1207
|
+
const allGaps = buildCoverageGaps();
|
|
1140
1208
|
(0, scanState_1.terminateScan)(scanState, 'blocked_read_recovery', `Agent stuck re-reading files. ${allGaps.length} coverage gaps.`);
|
|
1141
1209
|
qualityTracker.recordForcedTermination('blocked_read_recovery');
|
|
1142
1210
|
const covSummary = investigationState.getCoverageSummary(target.filePath);
|
|
1143
1211
|
qualityTracker.recordCoverage(covSummary.totalLines, covSummary.coveredLines, covSummary.uncoveredRangeCount, covSummary.largestUncoveredRange);
|
|
1144
1212
|
qualityTracker.recordBudget(stepsTaken, stepsGranted, extensionsGranted, costSpentUsd, budget.costCapUsd, wallClockMs, Date.now() - startTime);
|
|
1145
|
-
return {
|
|
1213
|
+
return closeRun({
|
|
1146
1214
|
status: 'incomplete',
|
|
1147
1215
|
findings: [],
|
|
1148
1216
|
transcript,
|
|
@@ -1154,7 +1222,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
1154
1222
|
summary: `Investigation cut short — agent was stuck re-reading files. ${allGaps.length} coverage gaps identified.`,
|
|
1155
1223
|
investigationNotes: [],
|
|
1156
1224
|
coverageGaps: allGaps,
|
|
1157
|
-
};
|
|
1225
|
+
});
|
|
1158
1226
|
}
|
|
1159
1227
|
}
|
|
1160
1228
|
}
|
|
@@ -1354,7 +1422,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
1354
1422
|
suggestedNextAction: 'read_file',
|
|
1355
1423
|
priority: 'high',
|
|
1356
1424
|
}));
|
|
1357
|
-
return {
|
|
1425
|
+
return closeRun({
|
|
1358
1426
|
status: 'incomplete',
|
|
1359
1427
|
findings: [],
|
|
1360
1428
|
transcript,
|
|
@@ -1366,7 +1434,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
1366
1434
|
summary: `Investigation incomplete — ${RECOVERY_FAILURE_LIMIT} recovery failures without progress. Model blocked ${scanState.recovery.totalModelBlockedActions} time(s), recovery attempted ${scanState.recovery.totalRecoveryAttempts} time(s).`,
|
|
1367
1435
|
investigationNotes: [],
|
|
1368
1436
|
coverageGaps: [...autoGaps, ...taskGaps, ...rangeGaps],
|
|
1369
|
-
};
|
|
1437
|
+
});
|
|
1370
1438
|
}
|
|
1371
1439
|
}
|
|
1372
1440
|
else {
|
|
@@ -1437,7 +1505,7 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
1437
1505
|
const covSummary = investigationState.getCoverageSummary(target.filePath);
|
|
1438
1506
|
qualityTracker.recordCoverage(covSummary.totalLines, covSummary.coveredLines, covSummary.uncoveredRangeCount, covSummary.largestUncoveredRange);
|
|
1439
1507
|
qualityTracker.recordBudget(stepsTaken, stepsGranted, extensionsGranted, costSpentUsd, budget.costCapUsd, wallClockMs, Date.now() - startTime);
|
|
1440
|
-
return {
|
|
1508
|
+
return closeRun({
|
|
1441
1509
|
status: 'incomplete',
|
|
1442
1510
|
findings: [],
|
|
1443
1511
|
transcript,
|
|
@@ -1449,13 +1517,23 @@ async function runAgentScan(ctx, target, options = {}) {
|
|
|
1449
1517
|
summary: 'Investigation incomplete — scheduler determined no executable work remains during recovery.',
|
|
1450
1518
|
investigationNotes: [],
|
|
1451
1519
|
coverageGaps: [],
|
|
1452
|
-
};
|
|
1520
|
+
});
|
|
1453
1521
|
}
|
|
1454
1522
|
}
|
|
1455
1523
|
}
|
|
1456
1524
|
}
|
|
1457
1525
|
}
|
|
1458
1526
|
catch (err) {
|
|
1527
|
+
if (activeRunId) {
|
|
1528
|
+
try {
|
|
1529
|
+
await client.postJson('/agent/scan/close', {
|
|
1530
|
+
runId: activeRunId,
|
|
1531
|
+
status: 'failed',
|
|
1532
|
+
terminationReason: 'api_error',
|
|
1533
|
+
}, options.signal);
|
|
1534
|
+
}
|
|
1535
|
+
catch { /* best-effort — janitor will expire the run */ }
|
|
1536
|
+
}
|
|
1459
1537
|
return {
|
|
1460
1538
|
status: 'failed',
|
|
1461
1539
|
findings: [],
|