5.0 KiB
5.0 KiB
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)
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)
{
"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>)
{
"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 afterhandle_merge_failure()to free storage for failed plans.
API
SqliteChangeSetStore
Implements the ChangeSetStore protocol with SQLite persistence.
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)