# ChangeSet Persistence ## Overview ChangeSets track every file mutation made during the Execute phase of a plan. Each mutation is recorded as a `ChangeEntry` with content hashes, operation type, and tool provenance. Tool executions are recorded as `ToolInvocation` records for full auditability. ## Persistence Fields ### changeset_entries table | Column | Type | Description | |---------------|--------------|--------------------------------------| | entry_id | String(26) | ULID primary key | | changeset_id | String(26) | ULID of the owning changeset | | plan_id | String(26) | ULID of the owning plan | | resource_id | String(26) | ULID of the affected resource | | tool_name | String(255) | Namespaced tool name | | operation | String(20) | create, modify, delete, or rename | | path | String(1024) | Repo-relative file path | | before_hash | String(64) | SHA-256 before change (nullable) | | after_hash | String(64) | SHA-256 after change (nullable) | | before_mode | Integer | File mode before change (nullable) | | after_mode | Integer | File mode after change (nullable) | | timestamp | String(30) | ISO-8601 UTC timestamp | ### tool_invocations table | Column | Type | Description | |---------------------|--------------|----------------------------------| | invocation_id | String(26) | ULID primary key | | changeset_id | String(26) | Optional owning changeset | | plan_id | String(26) | Owning plan ULID | | tool_name | String(255) | Namespaced tool name | | skill_name | String(255) | Skill that provided the tool | | arguments_json | Text | JSON input parameters | | result_json | Text | JSON output payload | | error | Text | Error message on failure | | success | Boolean | Whether execution succeeded | | duration_ms | Float | Execution time in milliseconds | | started_at | String(30) | ISO-8601 start timestamp | | completed_at | String(30) | ISO-8601 completion timestamp | | change_ids_json | Text | JSON list of ChangeEntry IDs | | sequence_number | Integer | Ordering within a plan | | sandbox_path | String(1024) | Sandbox root path | | resource_refs_json | Text | JSON list of resource IDs | | provider_metadata_json | Text | JSON provider info | ## Diff Output Examples ### Plain format (`agents plan diff --format plain`) ```text ChangeSet: 01HXYZ123456789ABCDEFG Plan: 01HABCDEF123456789XYZ Total changes: 3 --- a/src/new_module.py +++ b/src/new_module.py @@ new file @@ + hash: abc123def456... --- a/src/existing.py +++ b/src/existing.py @@ modified @@ - hash: old_hash_val... + hash: new_hash_val... --- a/src/removed.py +++ b/src/removed.py @@ deleted @@ - hash: file_hash_be... ``` ### JSON format (`agents plan diff --format json`) ```json { "changeset_id": "01HXYZ123456789ABCDEFG", "plan_id": "01HABCDEF123456789XYZ", "total_changes": 3, "summary": { "total": 3, "creates": 1, "modifies": 1, "deletes": 1, "renames": 0, "paths_changed": 3, "resources_involved": 1 }, "entries": [ { "path": "src/new_module.py", "operation": "create", "before_hash": null, "after_hash": "abc123def456", "resource_id": "01HRES001", "tool_name": "file-write", "timestamp": "2026-02-24T12:00:00+00:00" } ] } ``` ### Artifacts output (`agents plan artifacts `) ```json { "plan_id": "01HABCDEF123456789XYZ", "phase": "execute", "processing_state": "complete", "changeset_id": "01HXYZ123456789ABCDEFG", "sandbox_refs": ["sandbox-001"], "changeset_summary": { "total": 3, "creates": 1, "modifies": 1, "deletes": 1, "renames": 0 }, "files_changed": [ {"path": "src/new_module.py", "operation": "create"}, {"path": "src/existing.py", "operation": "modify"}, {"path": "src/removed.py", "operation": "delete"} ] } ``` ## Cleanup Behaviour Changeset artifacts are automatically cleaned up when: - **Plan cancel**: `cleanup_changeset()` deletes all persisted entries and invocations for the cancelled plan. - **Apply failure**: `cleanup_changeset()` is called after `handle_merge_failure()` to free storage for failed plans. ## API ### SqliteChangeSetStore Implements the `ChangeSetStore` protocol with SQLite persistence. ```python from cleveragents.infrastructure.database.changeset_repository import ( SqliteChangeSetStore, ) store = SqliteChangeSetStore(session_factory) changeset_id = store.start(plan_id) store.record(changeset_id, entry) changeset = store.get(changeset_id) store.delete_for_plan(plan_id) ```