alibaba/spring-ai-alibaba · error · Exception

Unable to delete retained checkpoints

Error message

Unable to delete retained checkpoints

What it means

PostgresSaver.deleteCheckpoints wraps any SQLException from executing the batched DELETE of retained checkpoint ids into Exception('Unable to delete retained checkpoints'). This is used when pruning checkpoints that fall outside the retention policy, so failure leaves stale rows behind.

Source

Thrown at spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/checkpoint/savers/postgresql/PostgresSaver.java:570

	}

	@Override
	protected void deleteCheckpoints(String threadId, Collection<String> checkpointIds) throws Exception {
		if (checkpointIds.isEmpty()) {
			return;
		}
		try (Connection conn = getConnection();
				PreparedStatement ps = conn.prepareStatement(
						DELETE_CHECKPOINTS.formatted(String.join(", ", Collections.nCopies(checkpointIds.size(), "?"))))) {
			ps.setString(1, threadId);
			int index = 2;
			for (String checkpointId : checkpointIds) {
				ps.setObject(index++, UUID.fromString(checkpointId), Types.OTHER);
			}
			ps.executeUpdate();
		}
		catch (SQLException ex) {
			throw new Exception("Unable to delete retained checkpoints", ex);
		}
	}

	@Override
	protected void releaseThread(String threadId) throws Exception {
		Connection conn = null;
		try (Connection ignored = conn = getConnection()) {
			conn.setAutoCommit(false);
			log.trace("Executing release Thread:\n---\n{}---", RELEASE_THREAD);
			try (PreparedStatement ps = conn.prepareStatement(RELEASE_THREAD)) {
				ps.setString(1, threadId);
				int rowsAffected = ps.executeUpdate();
				if (rowsAffected == 0) {
					conn.rollback();
					throw new IllegalStateException(format("Thread '%s' not found or already released", threadId));
				}
			}
			conn.commit();

View on GitHub (pinned to f82da0b50f)

Solutions

  1. Check the wrapped SQLException for privilege/schema issues; grant DELETE on the checkpoint table if denied.
  2. Verify all checkpoint ids are valid UUID strings matching stored values.
  3. Confirm the checkpoint table exists and matches the current schema.
  4. Retry the deletion; the operation is safe to re-run since it removes rows by id.

Example fix

// before
// GRANT SELECT, INSERT ON checkpoints TO app_user; -- DELETE missing
// after
// GRANT SELECT, INSERT, UPDATE, DELETE ON checkpoints TO app_user;
saver.deleteCheckpoints(threadId, retainedIds);
Defensive patterns

Strategy: try-catch

Validate before calling

// Java: validate ids before deletion
retainedIds.forEach(id -> UUID.fromString(id)); // throws early on malformed ids

Try / catch

try {
    saver.deleteCheckpoints(threadId, retainedIds);
} catch (Exception e) {
    if ("Unable to delete retained checkpoints".equals(e.getMessage()) && e.getCause() instanceof SQLException sql) {
        // check privilege/schema; deletion is idempotent so retry later
    }
}

Prevention

When it happens

Trigger: Deleting retained (obsolete) checkpoint ids when the DELETE fails: connection loss, malformed checkpoint id that cannot be parsed as UUID (UUID.fromString throws IllegalArgumentException before SQL, otherwise SQL error), table missing, or permission denied on DELETE.

Common situations: User lacks DELETE privilege on the checkpoints table; checkpoint ids in an unexpected format; DB connectivity drops during cleanup; retention cleanup running at the same time as writes causing contention.

Related errors


AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09). Data as JSON: /api/errors/82d4357fb0bdb337. Report an issue: GitHub.