|
2 | 2 | * Report progress tool - Progress updates without file exposure |
3 | 3 | * Updates progress markers without exposing file operations |
4 | 4 | * Enhanced with context status tracking (Issue #51) |
| 5 | + * Enhanced with stepCount metadata usage (Issue #60) |
5 | 6 | */ |
6 | 7 |
|
7 | 8 | import { ServerConfig } from '../types.js'; |
8 | 9 | import { TaskContextManager, ProgressUpdate, ProgressReportResult } from '../core/TaskContextManager.js'; |
9 | 10 | import { validateRequiredString, validateRequiredConfig } from '../utils/validation.js'; |
10 | 11 | import { ErrorLogEntry } from '../logging/ErrorLogger.js'; |
11 | 12 | import type { ContextStatus, CapabilityChanges } from '../types/context-types.js'; |
| 13 | +import { parsePlanCheckboxes } from '../utils/plan-parser.js'; |
| 14 | +import { PlanMetadata } from '../types/plan-metadata.js'; |
| 15 | +import * as fs from '../utils/fs-extra-safe.js'; |
| 16 | +import path from 'path'; |
12 | 17 | import debug from 'debug'; |
13 | 18 |
|
14 | | - |
15 | | -const log = debug('agent-comm:tools:reportprogress'); |
| 19 | +const log = debug('agent-comm:tools:report-progress'); |
16 | 20 | /** |
17 | 21 | * Report progress updates without file exposure |
18 | 22 | */ |
@@ -245,31 +249,8 @@ export async function reportProgress( |
245 | 249 | // Continue processing - don't throw |
246 | 250 | } |
247 | 251 |
|
248 | | - // Log extremely large step numbers but don't block (for resilience) |
249 | | - if (step > 100) { |
250 | | - log('Warning: Extremely large step number detected: %d at index %d', step, index); |
251 | | - // Log unusual condition for analysis but don't block |
252 | | - if (config.errorLogger) { |
253 | | - const errorEntry: ErrorLogEntry = { |
254 | | - timestamp: new Date(), |
255 | | - source: 'validation', |
256 | | - operation: 'report_progress', |
257 | | - agent, |
258 | | - ...(taskId && { taskId }), |
259 | | - error: { |
260 | | - message: `Extremely large step number detected: ${step} at index ${index}`, |
261 | | - name: 'ValidationWarning' |
262 | | - }, |
263 | | - context: { |
264 | | - tool: 'report_progress', |
265 | | - parameters: { unusualStep: step, typicalMax: 100, updateIndex: index } |
266 | | - }, |
267 | | - severity: 'medium' // Not blocking, just unusual |
268 | | - }; |
269 | | - await config.errorLogger.logError(errorEntry); |
270 | | - } |
271 | | - // Continue processing - don't throw |
272 | | - } |
| 252 | + // Note: Extremely large step number validation moved to after stepCount determination |
| 253 | + // to avoid double logging when step is both large and out of range |
273 | 254 |
|
274 | 255 | if (typeof status !== 'string' || !['COMPLETE', 'IN_PROGRESS', 'PENDING', 'BLOCKED'].includes(status)) { |
275 | 256 | const error = new Error(`Update at index ${index}: status must be one of COMPLETE, IN_PROGRESS, PENDING, BLOCKED`); |
@@ -330,7 +311,136 @@ export async function reportProgress( |
330 | 311 | ...(typeof blocker === 'string' && blocker.trim() && { blocker: blocker.trim() }) |
331 | 312 | }); |
332 | 313 | } |
333 | | - |
| 314 | + |
| 315 | + // Validate step numbers against stepCount if metadata exists (Issue #60) |
| 316 | + try { |
| 317 | + const startTime = Date.now(); |
| 318 | + const taskPath = path.join(config.commDir, agent, taskId ?? 'current-task'); |
| 319 | + const metadataPath = path.join(taskPath, 'PLAN.metadata.json'); |
| 320 | + |
| 321 | + let stepCount: number | undefined; |
| 322 | + |
| 323 | + // Try to read metadata first for performance |
| 324 | + if (await fs.pathExists(metadataPath)) { |
| 325 | + try { |
| 326 | + const metadata = await fs.readJSON(metadataPath) as PlanMetadata; |
| 327 | + stepCount = metadata.stepCount; |
| 328 | + log('Using cached stepCount from metadata: %d', stepCount); |
| 329 | + } catch (error) { |
| 330 | + log('Failed to read metadata, falling back to plan parsing: %s', (error as Error).message); |
| 331 | + } |
| 332 | + } |
| 333 | + |
| 334 | + // Fall back to parsing PLAN.md if no metadata |
| 335 | + if (stepCount === undefined) { |
| 336 | + const planPath = path.join(taskPath, 'PLAN.md'); |
| 337 | + if (await fs.pathExists(planPath)) { |
| 338 | + const planContent = await fs.readFile(planPath, 'utf8'); |
| 339 | + const checkboxes = parsePlanCheckboxes(planContent); |
| 340 | + stepCount = checkboxes.length; |
| 341 | + log('Parsed stepCount from PLAN.md: %d', stepCount); |
| 342 | + } |
| 343 | + } |
| 344 | + |
| 345 | + const validationTime = Date.now() - startTime; |
| 346 | + log('Step validation completed in %dms', validationTime); |
| 347 | + |
| 348 | + if (validationTime > 10) { |
| 349 | + log('PERFORMANCE WARNING: Step validation took %dms (>10ms threshold)', validationTime); |
| 350 | + } |
| 351 | + |
| 352 | + // Validate all step numbers are within range and check for extremely large steps |
| 353 | + if (stepCount !== undefined) { |
| 354 | + for (const update of updates) { |
| 355 | + if (update.step > stepCount) { |
| 356 | + // Step is out of range - this takes priority over "extremely large" warning |
| 357 | + const errorMessage = `Step ${update.step} is out of range (max: ${stepCount})`; |
| 358 | + |
| 359 | + if (config.errorLogger) { |
| 360 | + const errorEntry: ErrorLogEntry = { |
| 361 | + timestamp: new Date(), |
| 362 | + source: 'validation', |
| 363 | + operation: 'report_progress', |
| 364 | + agent, |
| 365 | + ...(taskId && { taskId }), |
| 366 | + error: { |
| 367 | + message: errorMessage, |
| 368 | + name: 'StepOutOfRangeWarning', |
| 369 | + code: 'STEP_OUT_OF_RANGE' |
| 370 | + }, |
| 371 | + context: { |
| 372 | + tool: 'report_progress', |
| 373 | + parameters: { |
| 374 | + invalidStep: update.step, |
| 375 | + maxStep: stepCount |
| 376 | + } |
| 377 | + }, |
| 378 | + severity: 'medium' |
| 379 | + }; |
| 380 | + await config.errorLogger.logError(errorEntry); |
| 381 | + } |
| 382 | + |
| 383 | + // Log warning but continue processing (permissive handling) |
| 384 | + log('Warning: %s', errorMessage); |
| 385 | + } else if (update.step > 100) { |
| 386 | + // Step is within range but extremely large - log separately |
| 387 | + log('Warning: Extremely large step number detected: %d', update.step); |
| 388 | + if (config.errorLogger) { |
| 389 | + const errorEntry: ErrorLogEntry = { |
| 390 | + timestamp: new Date(), |
| 391 | + source: 'validation', |
| 392 | + operation: 'report_progress', |
| 393 | + agent, |
| 394 | + ...(taskId && { taskId }), |
| 395 | + error: { |
| 396 | + message: `Extremely large step number detected: ${update.step}`, |
| 397 | + name: 'ValidationWarning' |
| 398 | + }, |
| 399 | + context: { |
| 400 | + tool: 'report_progress', |
| 401 | + parameters: { unusualStep: update.step, typicalMax: 100, maxStep: stepCount } |
| 402 | + }, |
| 403 | + severity: 'medium' |
| 404 | + }; |
| 405 | + await config.errorLogger.logError(errorEntry); |
| 406 | + } |
| 407 | + } |
| 408 | + } |
| 409 | + } else { |
| 410 | + // No stepCount available, fall back to extremely large step validation only |
| 411 | + for (const update of updates) { |
| 412 | + if (update.step > 100) { |
| 413 | + log('Warning: Extremely large step number detected: %d', update.step); |
| 414 | + if (config.errorLogger) { |
| 415 | + const errorEntry: ErrorLogEntry = { |
| 416 | + timestamp: new Date(), |
| 417 | + source: 'validation', |
| 418 | + operation: 'report_progress', |
| 419 | + agent, |
| 420 | + ...(taskId && { taskId }), |
| 421 | + error: { |
| 422 | + message: `Extremely large step number detected: ${update.step}`, |
| 423 | + name: 'ValidationWarning' |
| 424 | + }, |
| 425 | + context: { |
| 426 | + tool: 'report_progress', |
| 427 | + parameters: { unusualStep: update.step, typicalMax: 100 } |
| 428 | + }, |
| 429 | + severity: 'medium' |
| 430 | + }; |
| 431 | + await config.errorLogger.logError(errorEntry); |
| 432 | + } |
| 433 | + } |
| 434 | + } |
| 435 | + } |
| 436 | + } catch (error) { |
| 437 | + // Log error but only fail if it's a validation error |
| 438 | + if ((error as Error).message.includes('out of range')) { |
| 439 | + throw error; |
| 440 | + } |
| 441 | + log('Non-critical error during step validation: %s', (error as Error).message); |
| 442 | + } |
| 443 | + |
334 | 444 | // Create connection for the agent with optional taskId |
335 | 445 | const connection = { |
336 | 446 | id: `report-progress-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, |
|
0 commit comments