e3fcce413b
CI / lint (pull_request) Successful in 24s
CI / typecheck (pull_request) Successful in 59s
CI / security (pull_request) Successful in 53s
CI / quality (pull_request) Successful in 36s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 27s
CI / integration_tests (pull_request) Successful in 5m30s
CI / benchmark-regression (pull_request) Successful in 25m38s
CI / unit_tests (pull_request) Successful in 36m51s
CI / docker (pull_request) Successful in 1m3s
CI / coverage (pull_request) Successful in 1h48m49s
CI / lint (push) Successful in 22s
CI / security (push) Successful in 58s
CI / typecheck (push) Successful in 1m3s
CI / quality (push) Successful in 46s
CI / build (push) Successful in 23s
CI / integration_tests (push) Successful in 5m23s
CI / benchmark-regression (push) Has been skipped
CI / benchmark-publish (push) Successful in 15m5s
CI / unit_tests (push) Successful in 20m1s
CI / docker (push) Successful in 1m0s
CI / coverage (push) Successful in 1h45m36s
Add SQLite persistence for ChangeSet entries and ToolInvocation records via new changeset_repository module implementing the ChangeSetStore protocol. - Alembic migration d0_001 creates changeset_entries and tool_invocations tables - SqliteChangeSetStore, ChangeSetEntryRepository, ToolInvocationRepository - PlanApplyService.cleanup_changeset() for cancel/failure cleanup - Behave tests (17 scenarios), Robot tests (5 cases), ASV benchmarks - Reference documentation in docs/reference/changeset.md Closes #163
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)