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

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 after handle_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)