Files
freemo 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
feat(changeset): persist changesets and diff artifacts
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
2026-02-26 02:47:14 +00:00

157 lines
5.0 KiB
Markdown

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