# 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 <plan_id> --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 <plan_id> --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 <plan_id>`)

```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)
```
