Skip to content

Commit a7f2077

Browse files
committed
fix: expire sparsekernel leases at deadline
1 parent fa965e4 commit a7f2077

2 files changed

Lines changed: 77 additions & 3 deletions

File tree

crates/sparsekernel-core/src/lib.rs

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,7 +1358,7 @@ impl SparseKernelDb {
13581358
let task_ids: Vec<String> = {
13591359
let mut stmt = self
13601360
.conn
1361-
.prepare("SELECT id FROM tasks WHERE status = 'running' AND lease_until IS NOT NULL AND lease_until < ?")?;
1361+
.prepare("SELECT id FROM tasks WHERE status = 'running' AND lease_until IS NOT NULL AND lease_until <= ?")?;
13621362
let rows = stmt.query_map(params![now], |row| row.get(0))?;
13631363
rows.collect::<std::result::Result<Vec<_>, _>>()?
13641364
};
@@ -1370,9 +1370,22 @@ impl SparseKernelDb {
13701370
self.record_task_event(id, "lease_expired", json!({}))?;
13711371
}
13721372
let resource_count = self.conn.execute(
1373-
"UPDATE resource_leases SET status = 'expired', updated_at = ? WHERE status = 'active' AND lease_until IS NOT NULL AND lease_until < ?",
1373+
"UPDATE resource_leases SET status = 'expired', updated_at = ? WHERE status = 'active' AND lease_until IS NOT NULL AND lease_until <= ?",
13741374
params![now, now],
13751375
)?;
1376+
if !task_ids.is_empty() || resource_count > 0 {
1377+
self.record_audit(AuditInput {
1378+
actor_type: Some("runtime".to_string()),
1379+
actor_id: None,
1380+
action: "leases.expired_released".to_string(),
1381+
object_type: None,
1382+
object_id: None,
1383+
payload: Some(json!({
1384+
"tasks": task_ids.len(),
1385+
"resourceLeases": resource_count,
1386+
})),
1387+
})?;
1388+
}
13761389
Ok((task_ids.len(), resource_count))
13771390
}
13781391

@@ -3481,6 +3494,67 @@ mod tests {
34813494
assert_eq!(db.get_task("task-b").unwrap().status, "completed");
34823495
}
34833496

3497+
#[test]
3498+
fn lease_expiration_includes_exact_deadlines_and_audits() {
3499+
let (_dir, mut db) = temp_db();
3500+
let now = "2026-04-29T00:00:00Z";
3501+
db.enqueue_task(EnqueueTaskInput {
3502+
id: Some("task-expired".to_string()),
3503+
agent_id: None,
3504+
session_id: None,
3505+
kind: "demo".to_string(),
3506+
priority: 1,
3507+
idempotency_key: None,
3508+
input: None,
3509+
})
3510+
.unwrap();
3511+
db.claim_task("task-expired", "worker-a", 60)
3512+
.unwrap()
3513+
.unwrap();
3514+
db.conn
3515+
.execute(
3516+
"UPDATE tasks SET lease_until = ? WHERE id = ?",
3517+
params![now, "task-expired"],
3518+
)
3519+
.unwrap();
3520+
db.conn
3521+
.execute(
3522+
"INSERT INTO resource_leases(
3523+
id, resource_type, resource_id, owner_task_id, trust_zone_id,
3524+
status, lease_until, created_at, updated_at
3525+
) VALUES(?, 'sandbox', ?, ?, 'code_execution', 'active', ?, ?, ?)",
3526+
params![
3527+
"lease-expired",
3528+
"sandbox-expired",
3529+
"task-expired",
3530+
now,
3531+
now,
3532+
now
3533+
],
3534+
)
3535+
.unwrap();
3536+
3537+
assert_eq!(db.release_expired_leases(now).unwrap(), (1, 1));
3538+
let task = db.get_task("task-expired").unwrap();
3539+
assert_eq!(task.status, "queued");
3540+
assert_eq!(task.lease_until, None);
3541+
let resource_status: String = db
3542+
.conn
3543+
.query_row(
3544+
"SELECT status FROM resource_leases WHERE id = ?",
3545+
params!["lease-expired"],
3546+
|row| row.get(0),
3547+
)
3548+
.unwrap();
3549+
assert_eq!(resource_status, "expired");
3550+
let audit = db.list_audit(1).unwrap().into_iter().next().unwrap();
3551+
assert_eq!(audit.action, "leases.expired_released");
3552+
assert_eq!(
3553+
audit.payload,
3554+
Some(json!({ "tasks": 1, "resourceLeases": 1 }))
3555+
);
3556+
}
3557+
34843558
#[test]
34853559
fn task_claiming_respects_small_vm_budgets() {
34863560
let (_dir, mut db) = temp_db();

docs/architecture/local-agent-kernel.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Retention policies are:
8787
- `durable`
8888
- `debug`
8989

90-
`openclaw runtime prune` currently prunes old `ephemeral` and `debug` artifacts by default. Pass `--retention ephemeral,debug,session` only when session-scoped artifacts should also be removed. `openclaw runtime maintain` combines expired lease recovery, embedded-run recovery, artifact pruning, browser-observation pruning, and idle native browser pool sweeps for local maintenance jobs. `openclaw runtime maintain --schedule-every <duration>` stores a local maintenance cadence in `runtime_info`; `--run-due` lets a cron, launchd, or systemd timer invoke maintenance frequently while the ledger skips work until the cadence is due.
90+
`openclaw runtime prune` currently prunes old `ephemeral` and `debug` artifacts by default. Pass `--retention ephemeral,debug,session` only when session-scoped artifacts should also be removed. `openclaw runtime maintain` combines expired lease recovery, embedded-run recovery, artifact pruning, browser-observation pruning, and idle native browser pool sweeps for local maintenance jobs. Lease recovery treats `lease_until <= now` as due and records a `leases.expired_released` audit event when it recovers task or resource leases. `openclaw runtime maintain --schedule-every <duration>` stores a local maintenance cadence in `runtime_info`; `--run-due` lets a cron, launchd, or systemd timer invoke maintenance frequently while the ledger skips work until the cadence is due.
9191

9292
## Trust zones
9393

0 commit comments

Comments
 (0)