# Correction Attempts Module **Package:** `cleveragents.domain.models.core.correction` **Introduced:** M4 (v3.3.0); `CorrectionAttemptRecord` and `correction_attempts` table added in M7 (v3.6.0, issue #920) The correction module provides the domain models and lifecycle management for plan decision tree corrections. When a plan's execution produces an undesirable result, a correction allows the user to revert to a prior decision point or append new guidance without restarting the entire plan. For the design rationale see [ADR-007](../adr/ADR-007-decision-tree-and-correction.md) (Decision Tree & Correction), [ADR-035](../adr/ADR-035-decision-tree-rollback-and-replay.md) (Rollback & Replay). --- ## Purpose During plan execution, the actor makes a series of decisions (tool calls, resource writes, sub-plan spawns). These decisions form a tree. If the user or an automated check determines that a decision was wrong, a correction provides two recovery paths: | Mode | Description | |------|-------------| | **Revert** | Invalidate the subtree rooted at the target decision; restore checkpoints; re-execute from that point with new guidance | | **Append** | Add a new child plan at the target decision without invalidating existing decisions | --- ## Core Domain Models ### `CorrectionRequest` ```python from cleveragents.domain.models.core.correction import CorrectionRequest, CorrectionMode request = CorrectionRequest( plan_id="01ARZ3...", target_decision_id="01BCD4...", mode=CorrectionMode.REVERT, guidance="The file path was wrong — use src/api/ not src/app/", ) ``` | Field | Type | Description | |-------|------|-------------| | `correction_id` | `str` (ULID) | Unique identifier (auto-generated) | | `plan_id` | `str` | Plan that owns the targeted decision tree | | `target_decision_id` | `str` | Decision node to apply correction at | | `mode` | `CorrectionMode` | `revert` or `append` | | `guidance` | `str` (max 10,000 chars) | Human-supplied guidance | | `dry_run` | `bool` | When `True`, only compute impact without executing | | `status` | `CorrectionStatus` | Current lifecycle status | | `created_at` | `datetime` (UTC) | When the request was created | !!! warning "Security: validate untrusted `guidance` input" The `guidance` field is injected directly into the LLM actor during re-execution. When accepting guidance from untrusted integrations, validate and sanitise the input to reduce prompt-injection risk. The 10,000-character limit is enforced at the model layer but does not prevent malicious instructions. --- ### `CorrectionMode` ```python from cleveragents.domain.models.core.correction import CorrectionMode CorrectionMode.REVERT # "revert" — invalidate subtree and re-execute CorrectionMode.APPEND # "append" — add new child plan at target ``` --- ### `CorrectionStatus` Lifecycle states for a correction request: ``` PENDING → ANALYZING → EXECUTING → APPLIED └──► FAILED └──► CANCELLED └──► REJECTED ``` | Status | Meaning | |--------|---------| | `PENDING` | Request created, not yet processed | | `ANALYZING` | Impact analysis in progress | | `EXECUTING` | Correction is being applied | | `APPLIED` | Correction successfully applied | | `FAILED` | Correction failed | | `CANCELLED` | Correction was cancelled | | `REJECTED` | Correction rejected (e.g. applied child plans block revert) | --- ### `CorrectionImpact` Result of the impact analysis — computed before executing a correction. ```python from cleveragents.domain.models.core.correction import CorrectionImpact impact = CorrectionImpact( affected_decisions=["01BCD4...", "01BCD5..."], excluded_decisions=["01BCD6..."], affected_files=["src/api/handler.py"], affected_child_plans=["01EFG7..."], risk_level="medium", rollback_tier="full", rollback_tier_depth=2, ) ``` | Field | Type | Description | |-------|------|-------------| | `affected_decisions` | `list[str]` | Decision IDs in the affected subtree (BFS order) | | `excluded_decisions` | `list[str]` | Decision IDs NOT in the affected subtree (preserved) | | `affected_files` | `list[str]` | Files touched by affected decisions | | `affected_child_plans` | `list[str]` | Child plan IDs spawned by affected decisions | | `estimated_cost` | `float \| None` | Estimated recompute cost | | `risk_level` | `str` | `low`, `medium`, or `high` | | `rollback_tier` | `str` | `full`, `phase`, or `append_only` | | `rollback_tier_depth` | `int` | Depth of the targeted decision from the tree root | | `artifacts_to_archive` | `list[str]` | Artifact paths to archive on revert | --- ### `CorrectionResult` Outcome of executing a correction. For revert corrections, includes fields for checkpoint restoration and re-execution signalling. ```python from cleveragents.domain.models.core.correction import CorrectionResult, CorrectionStatus result = CorrectionResult( correction_id="01ARZ3...", status=CorrectionStatus.APPLIED, reverted_decisions=["01BCD4...", "01BCD5..."], checkpoint_restored=True, actor_state_ref="lc:checkpoint:01XYZ...", user_intervention_decision_id="01HIJ8...", phase_transition_target="strategize", ) ``` **Revert re-execution fields** (spec § Correction Flow, Revert Mode): | Field | Type | Description | |-------|------|-------------| | `checkpoint_restored` | `bool` | Whether a sandbox checkpoint was restored | | `actor_state_ref` | `str` | LangGraph actor checkpoint reference for reasoning rollback | | `user_intervention_decision_id` | `str` | ULID of the user_intervention decision created to inject guidance | | `phase_transition_target` | `str` | Target plan phase after revert (e.g. `"strategize"`) | --- ### `CorrectionDryRunReport` Detailed report showing what a correction *would* do, without modifying state. ```python from cleveragents.domain.models.core.correction import CorrectionDryRunReport report = service.generate_dry_run_report(request) # Access impact data via the nested impact object: print(report.impact.affected_decisions) print(report.impact.rollback_tier_depth) print(report.impact.affected_child_plans) ``` > **Note:** `excluded_decisions`, `rollback_tier_depth`, and > `child_plans_to_rollback` were previously duplicated at the top level. > They were removed in M7 (issue #1087) to eliminate divergence risk. > Access them via `report.impact.*` instead. --- ## Correction Attempt Records The `correction_attempts` table (added in M7, issue #920) provides a persistent audit trail of every correction execution attempt. ### `CorrectionAttemptRecord` ```python from cleveragents.domain.models.core.correction import ( CorrectionAttemptRecord, CorrectionAttemptState, CorrectionMode, ) record = CorrectionAttemptRecord( plan_id="01ARZ3...", original_decision_id="01BCD4...", mode=CorrectionMode.REVERT, guidance="Use src/api/ not src/app/", ) # record.state == CorrectionAttemptState.PENDING # record.correction_attempt_id — auto-generated ULID ``` | Field | Type | Description | |-------|------|-------------| | `correction_attempt_id` | `str` (ULID) | Unique identifier (auto-generated) | | `plan_id` | `str` | Plan that owns the targeted decision tree | | `original_decision_id` | `str` | Decision node being corrected | | `new_decision_id` | `str \| None` | New decision created by the correction (if any) | | `mode` | `CorrectionMode` | `revert` or `append` | | `guidance` | `str` (max 10,000 chars) | Human-supplied guidance (required, non-empty) | | `archived_artifacts_path` | `str \| None` | Path to archived artifacts (revert mode) | | `state` | `CorrectionAttemptState` | Current lifecycle state | | `created_at` | `datetime` (UTC) | When the record was created | | `completed_at` | `datetime \| None` | When the attempt completed | > **Note:** `archived_artifacts_path` is always system-generated within the > configured artifacts directory. The path is validated to stay inside that > directory before storage, and no direct user input is accepted for this > field. **Validation rules:** - `plan_id`, `original_decision_id`, and `guidance` must be non-empty (whitespace stripped) - `new_decision_id` must be non-empty when set - `created_at` and `completed_at` are normalised to UTC (naive datetimes are coerced) --- ### `CorrectionAttemptState` Spec-defined lifecycle for correction attempt records: ``` PENDING → EXECUTING → COMPLETE └──► FAILED ``` | State | Meaning | |-------|---------| | `PENDING` | Attempt created, not yet started | | `EXECUTING` | Correction is actively running | | `COMPLETE` | Correction completed successfully (terminal) | | `FAILED` | Correction failed (terminal) | ```python from cleveragents.domain.models.core.correction import ( CorrectionAttemptState, CORRECTION_ATTEMPT_VALID_TRANSITIONS, CORRECTION_ATTEMPT_TERMINAL_STATES, validate_correction_state_transition, ) # Check valid transitions CORRECTION_ATTEMPT_VALID_TRANSITIONS[CorrectionAttemptState.PENDING] # → frozenset({CorrectionAttemptState.EXECUTING}) # Check terminal states CorrectionAttemptState.COMPLETE in CORRECTION_ATTEMPT_TERMINAL_STATES # → True # Validate a transition (raises ValueError on invalid) validate_correction_state_transition( CorrectionAttemptState.PENDING, CorrectionAttemptState.EXECUTING, ) ``` --- ## Cross-Plan Cascade When a correction targets a decision that spawned child plans, the cascade mechanism determines what to do with each child plan. ### `ChildPlanState` Observed state of a child plan for cascade classification: | State | Meaning | |-------|---------| | `NOT_STARTED` | Child plan exists but has not begun executing | | `IN_PROGRESS` | Child plan is currently executing | | `COMPLETED_UNAPPLIED` | Child plan finished but changes not yet applied | | `APPLIED` | Child plan's changes have been applied (blocks revert) | ### `CascadeAction` ```python from cleveragents.domain.models.core.correction import CascadeAction, ChildPlanState action = CascadeAction( child_plan_id="01EFG7...", child_plan_state=ChildPlanState.IN_PROGRESS, action="cancel_and_rollback", sandbox_rolled_back=True, ) ``` | `action` value | Meaning | |----------------|---------| | `cancel` | Cancel the child plan (no sandbox rollback needed) | | `cancel_and_rollback` | Cancel and roll back the child plan's sandbox | | `reject` | Cannot cascade — child plan is already applied | ### `CorrectionRejection` Returned when a correction cannot proceed because applied child plans block it: ```python from cleveragents.domain.models.core.correction import CorrectionRejection rejection = CorrectionRejection( correction_id="01ARZ3...", reason="Child plan 01EFG7... has already been applied and cannot be reverted", affected_applied_child_plan_ids=["01EFG7..."], ) ``` ### `CascadeResult` Outcome of the full cascade operation: ```python from cleveragents.domain.models.core.correction import CascadeResult result = CascadeResult( correction_id="01ARZ3...", cascade_actions=[...], rejected=False, all_cancelled_plan_ids=["01EFG7..."], all_rolled_back_plan_ids=["01EFG7..."], ) ``` --- ## Using the Correction Service The `CorrectionService` (in `cleveragents.application.services`) orchestrates the full correction workflow. Obtain it via the DI container: ```python service = container.correction_service() # Dry run — compute impact without executing report = service.generate_dry_run_report(request) print(f"Risk: {report.impact.risk_level}") print(f"Affected decisions: {report.impact.affected_decisions}") # Execute a revert correction result = service.execute_revert(request) if result.status == CorrectionStatus.APPLIED: print(f"Reverted {len(result.reverted_decisions)} decisions") print(f"Phase transition target: {result.phase_transition_target}") # Execute an append correction result = service.execute_append(request) if result.spawned_child_plan_id: print(f"Spawned child plan: {result.spawned_child_plan_id}") ``` --- ## CLI Usage ```bash # Revert to a prior decision point agents plan correct --plan --decision \ --mode revert \ --guidance "Use src/api/ not src/app/" # Dry run first to see impact agents plan correct --plan --decision \ --mode revert --dry-run # Append new guidance without reverting agents plan correct --plan --decision \ --mode append \ --guidance "Also add error handling for the 404 case" ``` --- ## Database Schema The `correction_attempts` table stores `CorrectionAttemptRecord` instances: ```sql CREATE TABLE correction_attempts ( correction_attempt_id TEXT PRIMARY KEY, plan_id TEXT NOT NULL REFERENCES v3_plans(plan_id) ON DELETE CASCADE, original_decision_id TEXT NOT NULL REFERENCES decisions(decision_id) ON DELETE RESTRICT, new_decision_id TEXT REFERENCES decisions(decision_id) ON DELETE RESTRICT, mode TEXT NOT NULL, guidance TEXT NOT NULL, archived_artifacts_path TEXT, state TEXT NOT NULL DEFAULT 'pending', created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')), completed_at TEXT ); ``` **FK semantics:** - `plan_id` → `CASCADE` (correction attempts are deleted with the plan) - `original_decision_id` → `RESTRICT` (preserves correction audit trail when decisions are cleaned up) - `new_decision_id` → `RESTRICT` (consistent with `original_decision_id`) --- ## Testing The correction module is covered by 53 BDD scenarios in `features/correction_attempts.feature` and 5 Robot Framework integration tests in `robot/correction_attempts.robot`. ```python from cleveragents.domain.models.core.correction import ( CorrectionAttemptRecord, CorrectionAttemptState, CorrectionMode, validate_correction_state_transition, ) # Create a record record = CorrectionAttemptRecord( plan_id="plan-001", original_decision_id="dec-001", mode=CorrectionMode.REVERT, guidance="Fix the path", ) assert record.state == CorrectionAttemptState.PENDING # Validate transition validate_correction_state_transition( CorrectionAttemptState.PENDING, CorrectionAttemptState.EXECUTING, ) # OK validate_correction_state_transition( CorrectionAttemptState.COMPLETE, CorrectionAttemptState.EXECUTING, ) # raises ValueError — terminal state ``` --- ## Related Documentation - [ADR-007 Decision Tree & Correction](../adr/ADR-007-decision-tree-and-correction.md) - [ADR-035 Decision Tree Rollback & Replay](../adr/ADR-035-decision-tree-rollback-and-replay.md) - [Architecture — Plan Lifecycle](../architecture.md#plan-lifecycle) - [Sandbox Module](sandbox.md) — checkpoint/rollback used during revert