# Why Overnight Coding Runs Stop After 15 Minutes: How Checkpoint Leases Fix It

Published: 2026-10-18 | Domain: https://agentafk.si | Surface: https://agentafk.si/plan

## Outcome first: deterministic multi-hour execution without premature stops or hung zombies

When developers try to run a coding agent overnight in an ordinary chat window, one of two failures almost always happens by 2:00 AM. Either the model completes the first sub-step after 12 minutes and stops to ask *"Would you like me to proceed with Step 2?"*, or a single test subprocess hangs on stdin/network I/O and blocks the entire night's queue. `agentafk.si` eliminates both failure modes using **explicit completion predicates** and **heartbeat checkpoint leases**.

## Why chat-tuned models bail out early on long assignments

Instruction-tuned models are heavily rewarded during RLHF for short, conversational turns that defer to the user after producing a partial summary. In an interactive session, asking for confirmation is polite; in an unattended 6-hour overnight queue, it wastes the entire night.

In `agentafk.si`, a discrete cognitive or act step Loop node does not decide when the queue is finished by emitting conversational prose. Instead, the Loop controller evaluates an external, machine-checkable `exit_condition` after every turn:

1. **Premature-stop refusal**: If the model stops calling tools while `verifier.py` still reports unmet specification rules and `calls_used < call_budget`, the Loop controller automatically re-prompts the node with the exact list of remaining unverified rules.
2. **Bounded subprocess leases (`lease_ttl = 300s`)**: Every tool execution acquires a monotonic lease with a strict heartbeat deadline. If a test runner emits no progress heartbeat before `lease_ttl` expires, the supervisor kills the process group, records a typed `subprocess_lease_expired` receipt, and transitions cleanly without hanging the queue.

```python
# AgentAFK Loop Supervisor: Enforces Verifier Exit Conditions & Lease Deadlines
def supervise_overnight_task(task_spec: dict, runner, verifier) -> dict:
    calls_used = 0
    max_calls = task_spec["max_calls"]
    while calls_used < max_calls:
        calls_used += 1
        step_out = runner.run_step_with_lease(
            assignment=task_spec["assignment"],
            lease_seconds=task_spec.get("lease_seconds", 300)
        )
        if step_out["status"] == "unknown_external_outcome":
            return {"state": "paused_for_reconciliation", "calls_used": calls_used}

        verdict = verifier.check(task_spec["invariants"])
        if verdict["all_passed"]:
            return {"state": "finished_verified", "calls_used": calls_used, "verdict": verdict}

        runner.update_counterexample_memory(verdict["minimal_counterexample"])
    return {"state": "halted_at_call_ceiling", "calls_used": calls_used}
```
