# Concurrency Locks CleverAgents uses advisory locking to prevent concurrent modifications to plans and projects. The lock mechanism is backed by a SQLite table and enforced at the application layer before lifecycle transitions. ## Overview The `LockService` provides mutual-exclusion primitives for two resource types: **plan** and **project**. Each resource can have at most one active lock at any time. Locks are identified by the tuple `(resource_type, resource_id)` and scoped to an **owner** (typically the session or process identity). ## Lock Lifecycle ```text acquire --> active --> release / expire | +--> renew (extend TTL) ``` 1. **Acquire**: Creates a new lock row or re-acquires for the same owner. 2. **Renew**: Extends the TTL of an active lock. 3. **Release**: Explicitly removes the lock. 4. **Expire**: The lock becomes stale after `expires_at` passes. ## Re-entrant Acquisition If the same owner calls `acquire()` on a resource it already holds, the TTL is silently extended. No error is raised. ## Conflict Handling If a **different** owner attempts to acquire a lock that is still active (not expired), a `LockConflictError` is raised with details about the current holder. Expired locks from other owners are transparently replaced. ## TTL Defaults and Renewal Strategy | Constant | Default | Description | |---------------------------|---------|--------------------------------| | `DEFAULT_LOCK_TTL_SECS` | 300 | Default lock lifetime (5 min) | | `MAX_LOCK_TTL_SECS` | 3600 | Maximum allowed TTL (1 hr) | | `MIN_LOCK_TTL_SECS` | 5 | Minimum allowed TTL (5 sec) | For long-running phases (e.g. Execute), the caller should periodically call `renew()` with the desired TTL before the current lock expires. A recommended renewal interval is `ttl / 2` (e.g. every 150 seconds for the default 300-second TTL). ## Database Schema The `locks` table (migration `m4_001_concurrency_locks`): | Column | Type | Description | |-----------------|-------------|-------------------------------| | `id` | INTEGER PK | Auto-increment primary key | | `owner_id` | VARCHAR(255) | Lock owner identity | | `resource_type` | VARCHAR(50) | `plan` or `project` | | `resource_id` | VARCHAR(255) | Resource identifier | | `acquired_at` | VARCHAR(30) | ISO-8601 acquisition time | | `expires_at` | VARCHAR(30) | ISO-8601 expiry time | A unique constraint on `(resource_type, resource_id)` enforces at most one lock per resource. ## Startup Cleanup On application startup, `cleanup_expired()` is called to purge any locks whose `expires_at` has passed. This handles the case where a previous process terminated without releasing its locks. ## Graceful Shutdown During graceful shutdown, `release_all_for_owner(owner_id)` removes all locks held by the current process so that resources become immediately available to other instances. ## Diagnostics The `agents diagnostics` command includes a **Stale locks** check that reports the number of expired-but-not-yet-cleaned locks. A non-zero count indicates that the cleanup routine has not run recently and may warrant investigation. ## Integration Points - **PlanLifecycleService**: Transitions acquire a plan-level lock before mutating phase/state and release it after persistence. - **Startup**: The application container calls `cleanup_expired()` during initialisation. ## Error Types | Exception | Condition | |---------------------|------------------------------------------------| | `LockConflictError` | Another owner holds an active lock | | `LockExpiredError` | Attempted to renew an already-expired lock | | `ValidationError` | Invalid parameters (empty strings, bad TTL) |